如何更新嵌入到UIKit中的SwiftUI视图?

15
我正在寻找一个类似于AppKit的NSHostingView的UIKit等效类,以便我可以在UIKit中嵌入SwiftUI视图。不幸的是,UIKit没有与NSHostingView相对应的等效类。我们最接近的是NSHostingController的等效类UIHostingController。由于视图控制器包含一个视图,因此我们应该能够调用适当的UIViewController嵌入方法,然后获取view并直接使用它。 许多 文章 解释 这是在UIKit中嵌入SwiftUI视图的方法。但是,它们通常无法很好地解释您如何从UIKit ➡️ SwiftUI 进行通信。例如,想象一下我实现了一个作为进度条的SwiftUI视图,我希望定期更新进度。我希望我的传统/UIKit代码可以更新SwiftUI视图以显示新进度。 唯一一篇找到的接近解释如何操作嵌入式视图内容的文章建议我们使用@ObservedObject
import UIKit
import SwiftUI
import Combine

class CircleModel: ObservableObject {
    var didChange = PassthroughSubject<Void, Never>()

    var text: String { didSet { didChange.send() } }

    init(text: String) {
        self.text = text
    }
}

struct CircleView : View {
    @ObservedObject var model: CircleModel

    var body: some View {
        ZStack {
            Circle()
                .fill(Color.blue)
            Text(model.text)
                .foregroundColor(Color.white)
        }
    }
}

class ViewController: UIViewController {
    private weak var timer: Timer?
    private var model = CircleModel(text: "")

    override func viewDidLoad() {
        super.viewDidLoad()

        addCircleView()
        startTimer()
    }

    deinit {
        timer?.invalidate()
    }
}

private extension ViewController {
    func addCircleView() {
        let circleView = CircleView(model: model)
        let controller = UIHostingController(rootView: circleView)
        addChild(controller)
        controller.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(controller.view)
        controller.didMove(toParent: self)

        NSLayoutConstraint.activate([
            controller.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
            controller.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
            controller.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            controller.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    func startTimer() {
        var index = 0
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            index += 1
            self?.model.text = "Tick \(index)"
        }
    }
}


这似乎是有道理的,因为计时器应该会触发一系列事件来更新视图:
  1. self?.model.text = "Tick 1"(在ViewController.startTimer()中)。
  2. didChange.send()(在CircleModel.text.didSet中)
  3. Text(model.text)(在CircleView.body中)
如您所见,指示器(指定是否运行某些内容)表明问题在于didChange.send()从未触发对CircleView.body的重新运行。
我该如何从UIKit > SwiftUI通信以操作嵌入在UIKit中的SwiftUI视图?

您的信息已经过时。在iOS 13.0仍处于beta版时,ObservableObject已更改为需要一个objectWillChange发布者而不是一个didChange发布者。Asperi的答案展示了一种更简单的实现ObservableObject的方法,即让它从其@Published属性中合成自己的发布者。 - rob mayoff
2个回答

11

你只需要扔掉那个自定义的主题,使用标准的@Published,如下所示

class CircleModel: ObservableObject {

    @Published var text: String

    init(text: String) {
        self.text = text
    }
}

测试环境:Xcode 11.2 / iOS 13.2


2
对于其他想知道如何在SwiftUI视图中获取model.text作为绑定以进行读/写访问,但将其与SwiftUI外部的变量同步而不是model.text的人,只需使用$model.text即可,因为@ObservableObject的包装值是一个绑定,可以像绑定到包装的@State属性一样使用。您的基于绑定的SwiftUI视图代码不需要更改。(CircleModel应该使用@ObservedObject包装器声明,而不是@State或只读普通属性。)(忽略\字符。) - Jared Updike
非常感谢。我发现Asperi的答案和Jared的评论都完美无缺。 - Andres
分享一下让我困扰的问题。只有CircleModel的text属性带有@Published注释,因此您必须仅在视图控制器中更新该属性。例如通过编写self?.model = CircleModel(text: "Tick \(index)")来替换整个模型是行不通的。 - wrightak

0
我的解决方案适用于来自SwiftUI视图的更新。
let hostingController = UIHostingController(rootView: contentView)
        if #available(iOS 16.0, *) {
            hostingController.sizingOptions = .intrinsicContentSize
        } else {
       
        cancellable = state.objectWillChange
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                  self?.hostingController?.view.invalidateIntrinsicContentSize()
            }
    }
        
        if let hostingView = hostingController.view {
            addSubview(hostingView)
            hostingView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                hostingView.leadingAnchor.constraint(equalTo: leadingAnchor),
                hostingView.trailingAnchor.constraint(equalTo: trailingAnchor),
                hostingView.topAnchor.constraint(equalTo: topAnchor),
                hostingView.bottomAnchor.constraint(equalTo: bottomAnchor),
            ])
        }
        
        self.hostingController = hostingController

如果您正在从UIKit更新视图并重置根视图,那么这种方法似乎是足够的。

if let hostingController {
            hostingController.rootView = contentView
            hostingController.view.invalidateIntrinsicContentSize()
        } else {
            let hostingController = UIHostingController(rootView: contentView)
            
            if let hostingView = hostingController.view {
                addSubview(hostingView)
                hostingView.translatesAutoresizingMaskIntoConstraints = false
                NSLayoutConstraint.activate([
                    hostingView.leadingAnchor.constraint(equalTo: leadingAnchor),
                    hostingView.trailingAnchor.constraint(equalTo: trailingAnchor),
                    hostingView.topAnchor.constraint(equalTo: topAnchor),
                    hostingView.bottomAnchor.constraint(equalTo: bottomAnchor),
                ])
            }
            
            self.hostingController = hostingController
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接