Sizing SwiftUI in UIKit

September 23, 2025

SwiftUI provides us a nice way of integrating SwiftUI into UIKit — UIHostingConfiguration:

let config = UIHostingConfiguration {
    Text("Hello world!")
}

We can turn this into a UIView like so:

let contentView: (UIView & UIContentView) = config.makeContentView()

We can then apply constraints:

addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ ... ])

But here is the thing — if the SwiftUI view resizes internally, our constraints won't be invalidated.

The missing callback

I found out through experimenation, that we can leverage onGeometryChange as a callback into the UIKit world:

final class HostingView<T: View>: UIView {

    private var contentView: (UIView & UIContentView)?
    private var heightConstraint: NSLayoutConstraint?

    init(@ViewBuilder content: () -> T) {
        super.init(frame: .zero)
        let config = makeConfiguration(content: content())
        contentView = config.makeContentView()
        layout()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func update(_ content: T) {
        let config = makeConfiguration(content: content)
        contentView?.configuration = config
    }

    private func layout() {
        guard let contentView else { return }
        addSubview(contentView)
        contentView.translatesAutoresizingMaskIntoConstraints = false

        // 100 can be initial value
        let heightConstraint = heightAnchor.constraint(equalToConstant: 100)
        self.heightConstraint = heightConstraint

        NSLayoutConstraint.activate([
            contentView.topAnchor.constraint(equalTo: topAnchor),
            contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
            // Allow SwiftUI to expand height arbitrarily
            contentView.heightAnchor.constraint(equalToConstant: UIView.layoutFittingExpandedSize.height),
            heightConstraint,
        ])
    }

    private func heightDidChange(_ newHeight: CGFloat) {
        heightConstraint?.constant = newHeight
        self.setNeedsLayout()
    }

    private func makeConfiguration(content: T) -> some UIContentConfiguration {
        UIHostingConfiguration {
            VStack(spacing: .zero) {
                content
                    .onGeometryChange(for: CGFloat.self) { g in
                        g.size.height
                    } action: { [weak self] newHeight in
                        self?.heightDidChange(newHeight)
                    }
                Spacer(minLength: .zero)
            }
        }
    }
}

This is really nice, because onGeometryChange triggers in the same layout pass — which means, we can resize our UIKit constraints in the same layout pass as SwiftUI itself — theres no need for scheduling the UIKit layout pass on a future run loop iteration.

Note: the SwiftUI UIHostingConfiguration needs enough space to grow — you might need to give it a large height that exceeds your HostingView's bounds, and then manually adjust the height of your HostingView based on the new size.