swift 将高度可变的UIHostingController插入UIStackView

oyjwcjzk  于 2023-03-28  发布在  Swift
关注(0)|答案(1)|浏览(122)

我在尝试将SwiftUI视图插入现有UIKit视图时遇到了一个问题。SwiftUI视图可以动态更改高度,但UIStackView无法调整到新的大小。我创建了一个(可怕的)测试项目来突出显示这一点。
按钮:

class TestButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButton()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupButton()
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupButton() {
        setTitleColor(.white, for: .normal)
        backgroundColor = .red
        titleLabel?.font = .boldSystemFont(ofSize: 25)
        layer.cornerRadius = 10
    }
}

SwiftUI视图:

struct TestSwiftUIView: View {
    @State var text: [String] = ["This is a line of text"]
    var body: some View {
        VStack {
            ForEach(text, id: \.self) { text in
                Text(text)
            }
            Button {
                text.append("New line")
            } label: {
                Text("Add line")
            }
            .padding()
            .background(Color.red)
        }
        .foregroundColor(.white)
        .background(Color.green)
    }
}

视图控制器:

class ViewController: UIViewController {
    
    var titleLabel = UILabel()
    var stackView = UIStackView()

    override func viewDidLoad() {
        super.viewDidLoad()
        configureTitleLabels()
        configureStackView()
    }
    
    func configureStackView() {
        view.addSubview(stackView)
        
        stackView.axis = .vertical
        stackView.distribution = .fillEqually
        stackView.spacing = 20
        
        addButtonsToStackView()
        setStackViewConstraints()
        
        let hostingController = UIHostingController(rootView: TestSwiftUIView())
        stackView.insertArrangedSubview(hostingController.view, at: 3)
    }
    
    func addButtonsToStackView() {
        let numberOfButtons = 5
        
        for i in 1...numberOfButtons {
            let button = TestButton()
            button.setTitle("\(i)", for: .normal)
            stackView.addArrangedSubview(button)
        }
    }
    
    func setStackViewConstraints() {
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30)
            .isActive = true
        stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50).isActive = true
        stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -50).isActive = true
        stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30).isActive = true
    }
    
    func configureTitleLabels() {
        view.addSubview(titleLabel)
        titleLabel.text = "Test project"
        titleLabel.font = .systemFont(ofSize: 30)
        titleLabel.textAlignment = .center
        titleLabel.numberOfLines = 0
        titleLabel.adjustsFontSizeToFitWidth = true
        
        setTitleLabelConstaints()
    }
    
    func setTitleLabelConstaints() {
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20)
            .isActive = true
        titleLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true
        titleLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true
    }
}

结果如下:

我们如何确保UIStackView为扩展的SwiftUI视图提供足够的空间?

更新

我有一个想法,我可能需要对layoutIfNeeded()做一些事情。所以我在SwiftUI视图中添加了一个回调函数,如下所示:

let callback: () -> Void

然后在按钮功能中:

Button {
    text.append("New line")
    callback()
} label: {
    Text("Add line")
        .font(.system(size: 18, weight: .bold, design: nil))
}

然后在ViewController中:

let hostingController = UIHostingController(rootView: TestSwiftUIView(callback: {
            self.stackView.subviews.forEach { view in
                view.sizeToFit()
                view.layoutIfNeeded()
            }
        }))

可惜没有影响:(

fnvucqvd

fnvucqvd1#

你遇到了几个问题...实际上,有几个:(
TL;DR版本:

  • UIHostingController视图与UIKit视图的工作方式不同
  • 你已经将stack view设置为.fillEqually,这意味着排列好的子视图将永不改变大小。

详细版本:
首先,当UIKit加载UIHostingController视图时,它使用该视图的.intrinsicContentSize。但是,由于SwiftUI视图的工作方式,它不会“自动”更新。
您可以通过将UIKit视图控制器分解为托管控制器来查看此信息:

class SimpleViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let hostingController = UIHostingController(rootView: TestSwiftUIView())

        // let's unwrap the view to save a little typing
        guard let v = hostingController.view else { return }
        
        v.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(v)
        
        // now let's add that view leading/trailing with 40-points on each side
        //  and vertically centered (no Height constraint)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        ])
        
        // let's give the view a background color
        //  so we can see its frame
        v.backgroundColor = .systemYellow
        
    }
    
}

这是我们得到的:

正如我们所看到的,绿色的vStack垂直扩展,但黄色视图没有变化。
有几种方法可以“解决”这个问题...
首先,您的callback方法是正确的,但是让我们这样做。
我们将添加一个var属性来保存对托管视图的引用:

// so we have a reference to the hosting controller's view
var hcView: UIView!

当我们得到回调时,我们会像这样更新它:

func updateSize() {
    hcView.setNeedsLayout()
    hcView.layoutIfNeeded()
    hcView.invalidateIntrinsicContentSize()
}

下面是一个修改后的示例:

struct CallbackTestSwiftUIView: View {
    let callback: () -> Void
    @State var strings: [String] = [
        "This is a line of text",
    ]
    var body: some View {
        VStack {
            ForEach(strings, id: \.self) { text in
                Text(text)
            }
            Button {
                strings.append("New line \(strings.count)")
                callback()
            } label: {
                Text("Add line")
            }
            .padding()
            .background(Color.red)
        }
        .foregroundColor(.white)
        .background(Color.green)
    }
}

class CallbackSimpleViewController: UIViewController {

    // so we have a reference to the hosting controller's view
    var hcView: UIView!
    
    func updateSize() {
        hcView.setNeedsLayout()
        hcView.layoutIfNeeded()
        hcView.invalidateIntrinsicContentSize()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let hostingController = UIHostingController(rootView: CallbackTestSwiftUIView(callback: {
            self.updateSize()
        }))
        
        // let's unwrap the view to save a little typing
        guard let v = hostingController.view else { return }
        
        // save the view reference
        self.hcView = v
        
        v.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(v)
        
        // now let's add that view leading/trailing with 40-points on each side
        //  and vertically centered (no Height constraint)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        ])
        
        // let's give the view a background color
        //  so we can see its frame
        v.backgroundColor = .systemYellow
        
    }
    
}

这是输出,黄色视图随着绿色视图沿着增长:

所以--喔喔!只要用你的栈视图布局实现它,我们就完成了,对吗?
不完全是。
您为堆栈视图设置了Top和Bottom约束,并设置了.distribution = .fillEqually
所以,堆栈视图说:“我现在有6个排列好的子视图,所以让它们都有相同的高度***,永远不要让高度改变!!!***
你可能会尝试.distribution = .fillProportionally...不幸的是,当堆栈视图的间距不为零时,它会失败。有一些方法可以解决这个问题,但它可能不会给予你想要的结果。
因此,让我们使用CallbackTestSwiftUIView和原始ViewController的稍微修改版本,并使用堆栈视图和按钮:

class CallbackInStackViewController: UIViewController {
    
    var titleLabel = UILabel()
    var stackView = UIStackView()
    
    // so we have a reference to the hosting controller's view
    var hcView: UIView!
    
    func updateSize() {
        hcView.setNeedsLayout()
        hcView.layoutIfNeeded()
        hcView.invalidateIntrinsicContentSize()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        configureTitleLabels()
        configureStackView()
    }
    
    func configureStackView() {
        view.addSubview(stackView)
        
        stackView.axis = .vertical
        
        // use .fill instead of .fillEqually
        //stackView.distribution = .fillEqually
        stackView.distribution = .fill
        
        stackView.spacing = 20

        addButtonsToStackView()
        setStackViewConstraints()
        
        let hostingController = UIHostingController(rootView: CallbackTestSwiftUIView(callback: {
            self.updateSize()
        }))
        
        // let's unwrap the view to save a little typing
        guard let v = hostingController.view else { return }
        
        // save the view reference
        self.hcView = v
        
        stackView.insertArrangedSubview(v, at: 3)
        
        // let's give the view a background color
        //  so we can see its frame
        v.backgroundColor = .systemYellow
    }
    
    func addButtonsToStackView() {
        let numberOfButtons = 5
        
        for i in 1...numberOfButtons {
            let button = TestButton()
            button.setTitle("\(i)", for: .normal)
            stackView.addArrangedSubview(button)
        }
    }
    
    func setStackViewConstraints() {
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30).isActive = true
        stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50).isActive = true
        stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -50).isActive = true
        
        // remove stack view's bottom anchor
        //stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30).isActive = true
    }
    
    func configureTitleLabels() {
        view.addSubview(titleLabel)
        titleLabel.text = "Test project"
        titleLabel.font = .systemFont(ofSize: 30)
        titleLabel.textAlignment = .center
        titleLabel.numberOfLines = 0
        titleLabel.adjustsFontSizeToFitWidth = true
        
        setTitleLabelConstaints()
    }
    
    func setTitleLabelConstaints() {
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
        titleLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true
        titleLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true

        // don't let title label stretch vertically
        titleLabel.setContentHuggingPriority(.required, for: .vertical)

    }
    
}

可能会也可能不会给予你你想要的:

但至少我们在堆栈视图中调整了大小。
一些搜索提出了各种方法来制作“自动调整大小”的托管控制器......我很少使用SwiftUI,所以我不能给予你推荐哪种方法可能是最好的。

相关问题