Skip to content

Using SwiftUI `View`s in `UIView`s

Posted on:April 15, 2024

Sometimes, I’d like to use a SwiftUI view in UIKit-based applications without a ton of fuss. The UIKit-approved way of integrating a SwiftUI view is via a UIHostingController, but that often creates difficulty when wanting to add that view to a UIView.

We can draw inspiration from AppKit’s HostingView to find an elegant solution to this problem:

import SwiftUI

public final class HostingView<Content>: UIView where Content: View {
  private let hostingController: UIHostingController<Content>
  public var rootView: Content { hostingController.rootView }

  public convenience init(@ViewBuilder content: () -> Content) {
    self.init(content: content())
  }

  public init(content: Content) {
    self.hostingController = UIHostingController(rootView: content)
    super.init(frame: .zero)
    self.backgroundColor = .clear
    self.hostingController.view.backgroundColor = .clear
  }

  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override public func didMoveToWindow() {
    super.didMoveToWindow()
    guard window != nil else { return }
    setUpHostingControllerIfNeeded()
  }

  private func setUpHostingControllerIfNeeded() {
    guard let parent = nextViewController else {
      return assertionFailure("No UIViewController found")
    }
    guard hostingController.parent !== parent else { return }
    if hostingController.parent != nil {
      hostingController.remove()
    }

    parent.add(hostingController, contentView: self)
    layoutIfNeeded()
  }
}

private extension UIViewController {
  /// Adds a child `UIViewController` to `UIViewController`
  /// - Parameters:
  ///   - child: The child `UIViewController` to add
  ///   - layoutGuide: An optional UILayoutGuide to pin the  child `UIViewController` to. If this is nil, the child will pin itself to the parent.
  func add(_ child: UIViewController, contentView: UIView? = nil, layoutGuide: UILayoutGuide? = nil) {
    addChild(child)
    let superView: UIView = contentView ?? self.view
    superView.addSubview(child.view)
    child.view.translatesAutoresizingMaskIntoConstraints = false
    if let layoutGuide {
      NSLayoutConstraint.activate([
        child.view.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
        child.view.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
        child.view.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
        child.view.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor),
      ])
    } else {
      NSLayoutConstraint.activate([
        child.view.leadingAnchor.constraint(equalTo: superView.leadingAnchor),
        child.view.trailingAnchor.constraint(equalTo: superView.trailingAnchor),
        child.view.topAnchor.constraint(equalTo: superView.topAnchor),
        child.view.bottomAnchor.constraint(equalTo: superView.bottomAnchor),
      ])
    }
    child.didMove(toParent: self)
  }

  /// Removes the child `UIViewController` from the parent
  func remove() {
    // Just to be safe, we check that this view controller
    // is actually added to a parent before removing it.
    guard parent != nil else {
      return
    }

    willMove(toParent: nil)
    view.removeFromSuperview()
    removeFromParent()
  }

}

private extension UIResponder {
  // Mimics the private function `_viewControllerForAncestor`
  // `_viewControllerForAncestor` walks up the responder chain looking for the next responder that is a `UIViewController`.
  @nonobjc var nextViewController: UIViewController? {
    guard let next = self.next else { return nil }
    if let next = next as? UIViewController {
      return next
    } else {
      return next.nextViewController
    }
  }
}

The code above creates a HostingView that can accept in-lined SwiftUI and be added to any arbitrary UIView:

let mySwiftUIView = HostingView {
  Text("Hello!")
}
let myUIKitView = UIView()

myUIKitView.addSubview(mySwiftUIView)
// Setup constraints...

Happy coding!