Swift and SwiftUI tutorials for Swift Developers

How to add a SwiftUI view to UIKit

Introduction: The Reality of Modern iOS Development

If you are an iOS developer today, you likely live in a constant dichotomy. On one hand, you have SwiftUI, the declarative, modern, and shiny framework that Apple promotes at every WWDC. On the other hand, you have reality: a massive codebase, built over years in UIKit, which works, generates revenue, and is simply too large to rewrite from scratch.

The question we all ask isn’t “Should I use SwiftUI?”, but rather “How do I start using SwiftUI without scrapping years of work in UIKit?”.

The good news is that Apple designed SwiftUI with interoperability as a fundamental pillar. You don’t have to choose one or the other. You can (and should) mix them. This strategy, known as “incremental adoption,” allows you to write new screens in SwiftUI while maintaining the legacy core in UIKit.

In this comprehensive tutorial, we are going to break down the magic tool that makes this possible: UIHostingController. You will learn how to present modal views, embed small components inside existing view controllers, and even how to use SwiftUI cells inside an old UITableView.


Part 1: Understanding the Protagonist: UIHostingController

Before writing code, let’s talk architecture. The bridge between these two worlds isn’t black magic; it’s a very specific class called UIHostingController.

What is it?

UIHostingController is, in essence, a wrapper. It is a class that inherits from UIViewController, but its content (its rootView) is a SwiftUI view.

To UIKit, UIHostingController is just another View Controller. You can present it, push it, or add it as a child (addChild). But to SwiftUI, it is the container that manages the lifecycle and rendering.

The anatomy of the bridge:

// In your UIKit code
let swiftUIView = MyModernView()
let controller = UIHostingController(rootView: swiftUIView)

Once you have that controller variable, you are back in familiar UIKit territory.


Part 2: Scenario A – Modal Presentation (Simple Navigation)

The simplest use case for starting to integrate SwiftUI is when you create a completely new screen. Imagine your UIKit app needs a “Settings” screen or an “Onboarding” flow. This is the perfect candidate for SwiftUI.

Step 1: Create your SwiftUI View

Let’s assume we have a simple user detail view.

import SwiftUI

struct UserDetailView: View {
    var username: String
    var onDismiss: () -> Void // We'll talk about this later

    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "person.circle.fill")
                .resizable()
                .frame(width: 100, height: 100)
                .foregroundColor(.blue)
            
            Text("Hello, \(username)")
                .font(.title)
                .fontWeight(.bold)
            
            Text("This screen is built 100% in SwiftUI but lives in a UIKit app.")
                .multilineTextAlignment(.center)
                .padding()
            
            Button(action: {
                onDismiss()
            }) {
                Text("Close")
                    .bold()
                    .frame(width: 200, height: 50)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
    }
}

Step 2: Present it from your UIViewController

In your legacy code (e.g., HomeViewController.swift), when the user taps a button, we instantiate the hosting controller and present it.

import UIKit
import SwiftUI // Don't forget to import SwiftUI in the UIKit file

class HomeViewController: UIViewController {

    @objc func didTapShowProfile() {
        // 1. Configure the SwiftUI View
        // We pass a closure to handle the dismiss from within SwiftUI if necessary
        let detailView = UserDetailView(username: "Carlos") { [weak self] in
            self?.dismiss(animated: true, completion: nil)
        }
        
        // 2. Create the Hosting Controller
        let hostingController = UIHostingController(rootView: detailView)
        
        // 3. Optional: Configure UIKit modal properties
        hostingController.modalPresentationStyle = .pageSheet
        
        if let sheet = hostingController.sheetPresentationController {
            sheet.detents = [.medium(), .large()] // iOS 15+ Magic!
        }
        
        // 4. Present like any other VC
        self.present(hostingController, animated: true)
    }
}

Why does this work so well? Because you aren’t mixing layouts. The UIHostingController occupies the entire screen (or sheet). UIKit handles the transition, and SwiftUI handles the content. It is the cleanest integration with the lowest risk of visual bugs.


Part 3: Scenario B – Embedding SwiftUI (Hybrid Components)

Here is where things get interesting (and slightly more complex). What if you don’t want a new screen, but rather want to insert a SwiftUI chart inside an existing UIViewController that already contains other UIKit elements?

Here, we must use the View Controller Containment pattern. It is a 4-step ritual that every senior iOS developer knows well.

The Inclusion Ritual

Suppose you have a DashboardViewController and want to add a weather widget made in SwiftUI at the top.

  1. Add the child: addChild(hostingController)
  2. Add the view: view.addSubview(hostingController.view)
  3. Configure Constraints: Use Auto Layout to tell UIKit where that view goes.
  4. Confirm the move: hostingController.didMove(toParent: self)

Practical Implementation

class DashboardViewController: UIViewController {
    
    // A container UIView you already placed in the Storyboard or via code
    @IBOutlet weak var chartContainerView: UIView! 
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupSwiftUIChart()
    }
    
    private func setupSwiftUIChart() {
        // 1. Instantiate the SwiftUI view
        let chartView = MarketChartView(data: [10, 20, 15, 30, 25])
        
        // 2. Create the Hosting Controller
        let hostingController = UIHostingController(rootView: chartView)
        
        // 3. Add as child to the current VC
        addChild(hostingController)
        
        // 4. Add the hosting view to the container
        // Important: The hosting view needs to be configured for Auto Layout
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        chartContainerView.addSubview(hostingController.view)
        
        // 5. Configure Constraints (Anchor to the container's edges)
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: chartContainerView.topAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: chartContainerView.bottomAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: chartContainerView.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: chartContainerView.trailingAnchor)
        ])
        
        // 6. Notify that the transition has finished
        hostingController.didMove(toParent: self)
        
        // Pro Tip: Make the background transparent if needed
        hostingController.view.backgroundColor = .clear
    }
}

Performance Considerations: SwiftUI is very efficient, but UIHostingController is not free. Avoid creating hundreds of these controllers if you can avoid it. For long lists, see the next section.


Part 4: Scenario C – SwiftUI in UITableView and UICollectionView

Historically, using SwiftUI inside UIKit cells was a headache regarding performance and cell recycling issues. However, since iOS 16, Apple gifted us UIHostingConfiguration.

If your project supports iOS 16+, forget everything you knew about stuffing controllers into cells. This is the new way to do it.

Using UIHostingConfiguration (The Modern Way)

Imagine you have a classic UITableView and want the cells to be rendered with SwiftUI to take advantage of its layout ease.

class MyTableViewController: UITableViewController {
    
    let data = ["Item 1", "Item 2", "Item 3"]
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = data[indexPath.row]
        
        // iOS 16+ Magic
        cell.contentConfiguration = UIHostingConfiguration {
            // Inside here, you write pure SwiftUI
            HStack {
                Image(systemName: "star.fill")
                    .foregroundColor(.yellow)
                VStack(alignment: .leading) {
                    Text(item)
                        .font(.headline)
                    Text("Description generated in SwiftUI")
                        .font(.caption)
                        .foregroundColor(.gray)
                }
                Spacer()
                Toggle("", isOn: .constant(true))
            }
            .padding()
        }
        
        return cell
    }
}

Advantages:

  • Automatically handles Self-Sizing. You no longer have to fight with heightForRowAt and math calculations to know the cell height. SwiftUI calculates its size and tells the table.
  • It is incredibly clean.
  • Automatically manages system margins and separators.

Part 5: Data Communication (The Real Challenge)

Putting a view on the screen is easy. Making that view talk to the rest of your UIKit app is where many developers fail.

We have three main strategies for passing data between UIKit and SwiftUI.

1. Input Data (Initialization)

This is the simplest. You pass the data in the init of the SwiftUI view.

  • Type: Unidirectional (UIKit -> SwiftUI).
  • Usage: Showing static or initial information.

2. Delegation via Closures

SwiftUI doesn’t use the classic UIKit delegate pattern (though you could force it); it prefers closures.

  • Type: Events (SwiftUI -> UIKit).
  • Example: A button in SwiftUI that needs to trigger navigation in the parent UINavigationController.
struct ActionView: View {
    var onAction: () -> Void
    
    var body: some View {
        Button("Perform Action") {
            onAction() // Calls the closure
        }
    }
}

// In UIKit
let view = ActionView {
    print("The SwiftUI button was pressed. UIKit responds.")
    self.navigationController?.pushViewController(OtherVC(), animated: true)
}

3. State Synchronization (Combine and ObservableObject)

If you need a change in SwiftUI to update a label in UIKit in real-time (or vice versa), you need Combine.

We create an intermediary class that both sides are aware of.

import Combine

// 1. The Shared ViewModel
class SharedViewModel: ObservableObject {
    @Published var counter: Int = 0
}

// 2. SwiftUI Side
struct CounterView: View {
    @ObservedObject var viewModel: SharedViewModel
    
    var body: some View {
        Button("Increment: \(viewModel.counter)") {
            viewModel.counter += 1
        }
    }
}

// 3. UIKit Side
class ViewController: UIViewController {
    let viewModel = SharedViewModel()
    var cancellables = Set<AnyCancellable>()
    @IBOutlet weak var uikitLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Bind SwiftUI
        let swiftUIView = CounterView(viewModel: viewModel)
        let hosting = UIHostingController(rootView: swiftUIView)
        // ... add hosting ...
        
        // Listen for changes in UIKit
        viewModel.$counter
            .receive(on: RunLoop.main)
            .sink { [weak self] newValue in
                self?.uikitLabel.text = "Value from SwiftUI: \(newValue)"
            }
            .store(in: &cancellables)
    }
}

This pattern is powerful because it decouples the view from the logic. UIKit reacts to the data, not the view.


Part 6: Common Pitfalls and How to Fix Them

Integrating two such different UI frameworks is not without its traps. Here are the most common ones I’ve found in production.

1. The “Safe Area” Problem

Sometimes, UIHostingController adds strange insets or double spacing at the top/bottom.

  • Solution: You often need to set hostingController.view.insetsLayoutMarginsFromSafeArea = false in UIKit, or use .ignoresSafeArea() in the SwiftUI view, depending on who you want controlling the edges.

2. Auto-Sizing (Intrinsic Content)

When you put a UIHostingController inside a container UIView, sometimes UIKit doesn’t know how big the SwiftUI view is, resulting in a view with 0 height.

  • Solution: Make sure to anchor all 4 sides (top, bottom, leading, trailing) with constraints.
  • Advanced: If you need the UIHostingController to adjust its size to the SwiftUI content (for example, inside a UIKit StackView), enable:
hostingController.view.invalidateIntrinsicContentSize()
  • And occasionally, you might need a UIHostingController subclass that updates its preferredContentSize.

3. Navigation Cycles

Avoid using NavigationView or NavigationStack inside a SwiftUI view that is already inside a UIKit UINavigationController. You will end up with two navigation bars (one stacked on top of the other).

  • Rule: If UIKit handles the navigation, SwiftUI should only render the content. Let UIKit do the “Push”.

Part 7: Is it worth the effort?

The short answer is: Yes, absolutely.

Integrating SwiftUI into UIKit is not just a technical issue; it’s a team and product strategy issue.

  1. Iteration Speed: Once the “bridge” is set up, building complex interfaces in SwiftUI is 3x or 4x faster than using Auto Layout and Storyboards.
  2. Future Proofing: UIKit isn’t disappearing tomorrow, but Apple’s new APIs (Widgets, Live Activities, visionOS) are SwiftUI-first or SwiftUI-only. Having a hybrid architecture prepares you for those platforms.
  3. Team Morale: Developers want to work with new technologies. Allowing SwiftUI in a legacy project helps retain talent.

Roadmap Summary

  • Start Small: Don’t rewrite your MainViewController. Start with the “Settings” or “About” screen.
  • Use UIHostingConfiguration: If you have lists, it’s the easiest place to start in iOS 16+.
  • Respect Data Flow: Use Combine or Closures to keep state synchronized; don’t try to hack direct references.

SwiftUI and UIKit can be best friends. They just need a good UIHostingController to introduce them.


Additional Resources

  • Apple Documentation: Interfacing with UIKit.
  • WWDC Videos: “Use SwiftUI with UIKit” (search for the ones from the last 3 years to see the evolution).

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

How to learn Swift Programming Language

Related Posts