52

An alternative question title could be "How to add an UIHostingController's view as subview for an UIView?".

I am creating a new piece of UI component and would love to give SwiftUI a try. The image below is the current view structure. The UIView is what I am using right now (top right), and SwiftUI view is what I try to use (bottom right).

enter image description here

After I watched all SwiftUI videos from WWDC 2019. I still have no clue on how can I use a SwiftUI view and put it at where a UIView instance should go.

I noticed from "Integrating SwiftUI" talk is that there is an NSHostingView for macOS, https://developer.apple.com/documentation/swiftui/nshostingview# which made me wonder if there is something similar to it or what is the alternative to achieve it.

I read questions like Include SwiftUI views in existing UIKit application mentioned that SwiftUI and UIKit can play together with UIHostingController. However, what I am trying to do is to only adopt one small piece of SwiftUI and put it inside of my existing UIKit view component, not use it as a controller.

I am new to iOS development, please leave a comment if there is a way I can use view controller as UIView view. Thank you.

XY L
  • 25,431
  • 14
  • 84
  • 143

3 Answers3

100

View controllers are not just for the top level scene. We often place view controllers within view controllers. It’s called “view controller containment” and/or “child view controllers”. (BTW, view controller containers are, in general, a great way to fight view controller bloat in traditional UIKit apps, breaking complicated scenes into multiple view controllers.)

So,

  • Go ahead and use UIHostingController:

    let controller = UIHostingController(rootView: ...)
    

    and;

  • Add the view controller can then add the hosting controller as a child view controller:

    addChild(controller)
    view.addSubview(controller.view)
    controller.didMove(toParent: self)
    

    Obviously, you’d also set the frame or the layout constraints for the hosting controller’s view.

    See the Implementing a Container View Controller section of the UIViewController documentation for general information about embedding one view controller within another.


For example, let’s imagine that we had a SwiftUI View to render a circle with text in it:

struct CircleView : View {
    @ObservedObject var model: CircleModel

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

And let’s say this was our view’s model:

import Combine

class CircleModel: ObservableObject {
    @Published var text: String

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

Then our UIKit view controller could add the SwiftUI view, set its frame/constraints within the UIView, and update its model as you see fit:

import UIKit
import SwiftUI

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)"
        }
    }
}
Senseful
  • 86,719
  • 67
  • 308
  • 465
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 2
    I have the problem that the UIHostingController is shrinking to a size, smaller than the SwiftUI Views are. It's embedded into an ScrollView ContentView, so I think it has to "communicate" it's intrinsic / minimal size, but I'm not quite sure how. – Moritz Mahringer Jul 19 '19 at 12:44
  • @Moritz Mahringer You may need to constrain your `UIHostingController`'s view or simply set its frame. Note that Rob's example sets constraints for the hosting controller's view. – JWK Jul 20 '19 at 19:13
  • Thanks for addressing the problem of "how to update the view's state after it has been installed in the hierarchy." I was hoping there was a solution simpler than `@ObservedObject`. E.g. maybe defining something like `@State var text:String` on `CircleView` and obviating the need for `CircleModel` altogether, but alas if creating an `ObservableObject` is the only way to do it, then so be it. -- However, even **the `ObservableObject` solution does not seem to work**... The `text` property is updated correctly, `didChange.send()` gets called, but this never causes `body` to trigger again. – Senseful Dec 05 '19 at 23:40
  • I asked this as a separate question here: https://stackoverflow.com/questions/59219019/how-do-i-update-a-swiftui-view-that-was-embedded-into-uikit – Senseful Dec 06 '19 at 19:15
  • 1
    Great point about using view controller containment to break up large view controllers. I've seen some mad patterns people have invented to try to solve it in other ways! – malhal Mar 25 '20 at 11:07
  • is autosizing with AutoLayout possible for UHostingController ? I have it added to view to some container, and it work ok but if SwiftUI view changes its size then UIHostinController takes the same place and its rootView expands outside its bounds and cover sibling UIKit views in this container view. – Michał Ziobro Jul 13 '23 at 10:19
5

I have some idea in mind.

  1. Wrap the SwiftUI with a UIHostingController
  2. Initialize the controller
  3. Add the new controller as a child view controller
  4. Add the controller view as a subview to where it should go

Thus:

addChild(hostingViewController)
hostingViewController.view.frame = ...
view.addSubview(hostingViewController.view)
hostingViewController.didMove(toParent: self)

A view controller always uses other view controllers as views.

Stanford CS193P, https://youtu.be/w7a79cx3UaY?t=679

Reference

Rob
  • 415,655
  • 72
  • 787
  • 1,044
XY L
  • 25,431
  • 14
  • 84
  • 143
0

You can also use UIHostingView, though it does looks like a private API:

let swiftUIView = MySwiftUIView()
let myUIKitView = _UIHostingView(rootView: swiftUIView)
view.addSubview(myUIKitView)
// Add constraints or whatever you need
OhadM
  • 4,687
  • 1
  • 47
  • 57