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() {
    guard window != nil else { return }

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

    parent.add(hostingController, contentView: self)

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) {
    let superView: UIView = contentView ?? self.view
    child.view.translatesAutoresizingMaskIntoConstraints = false
    if let layoutGuide {
        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 {
        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 {

    willMove(toParent: nil)


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 = 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 {
let myUIKitView = UIView()

// Setup constraints...

Happy coding!