Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I update a SwiftUI View that was embedded into UIKit?

I'm looking for an equivalent of AppKit's NSHostingView for UIKit so that I can embed a SwiftUI view in UIKit. Unfortunately, UIKit does not have an equivalent class to NSHostingView. The closest we have as an equivalent of NSHostingController, named UIHostingController. Since a view controller contains a view, we should be able to call the appropriate UIViewController embedding methods, and then grab the view and use it directly.

There are many articles that explain that this is the way to embed a SwiftUI view inside UIKit. However, they typically fall short in explaining how you would communicate from UIKit ➡️ SwiftUI. For example, imagine I implemented a SwiftUI view that acts as a progress bar, periodically, I'd like the progress to be updated. I want my legacy/UIKit code to update the SwiftUI view to display the new progress.

The only article I found that came close to explaining how to manipulate an embedded view's content suggested we do so by using @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)"
        }
    }
}

This seems to make sense as the timer should trigger a chain of events that update the view:

  1. self?.model.text = "Tick 1" (In ViewController.startTimer()).
  2. didChange.send() (In CircleModel.text.didSet)
  3. Text(model.text) (In CircleView.body)

As you can see by the indicators (which specify if something was run or not), the problem is that didChange.send() never triggers a re-run of CircleView.body.

How do I communicate from UIKit > SwiftUI to manipulate a SwiftUI view that was embedded in UIKit?

like image 290
Senseful Avatar asked Dec 06 '19 19:12

Senseful


People also ask

How to use UIViewController in Swift UI?

SwiftUI has no concept of UIViewController, everything is just a View. For SwiftUI to work as UIViewController you just set it as a rootView of UIHostingController like the example above.

How do I integrate a SwiftUI view with a UIKit scene?

Using a HostingViewController, a SwiftUI view can be treated either as an entire scene (occupying the full screen) or as an individual component within an existing UIKit scene. In this tutorial, we will walk through how to integrate a SwiftUI view by treating it as an entire scene.

Is the uihostingcontroller compatible with UIKit?

The hosting controller is compatible with UIKit since it is a subclass of UIViewController. The purpose of the UIHostingController is to enclose a SwiftUI view so that it can be integrated into an existing UIKit based project. Check Apple's documentation about UIHostingController if you want to know more.

What is hostingviewcontroller in SwiftUI?

Check Apple's documentation about UIHostingController if you want to know more. Using a HostingViewController, a SwiftUI view can be treated either as an entire scene (occupying the full screen) or as an individual component within an existing UIKit scene.


1 Answers

All you need is to throw away that custom subject, and use standard @Published, as below

class CircleModel: ObservableObject {

    @Published var text: String

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

Tested on: Xcode 11.2 / iOS 13.2

like image 190
Asperi Avatar answered Oct 16 '22 17:10

Asperi