Swift and SwiftUI tutorials for Swift Developers

Integrating SwiftUI into UIKit

The Apple development ecosystem has experienced a massive revolution in recent years. As an iOS Developer, it is very likely that you have found yourself at the crossroads between maintaining a legacy codebase and the desire to adopt the new technologies that Apple introduces every year. The leap in Swift programming has never been as exciting as it is now.

SwiftUI has changed the way we build user interfaces, offering a declarative syntax that drastically reduces lines of code and common errors. However, rewriting a complete application from scratch is rarely a viable option for companies. This is where one of the most valuable skills today comes in: knowing exactly how to integrate SwiftUI into UIKit.

In this extensive tutorial, we will deeply explore how you can combine the best of both worlds. Using Swift and Xcode, we will learn how to inject modern views into your existing applications, not only on iOS, but also expanding our horizons to macOS and watchOS.


1. The Bridge Between Two Worlds: The HostingController Family

The fundamental secret to embedding SwiftUI in the classic imperative frameworks (UIKit, AppKit, and WatchKit) lies in a special family of controllers provided by Apple. These controllers act as native wrappers that translate the declarative view hierarchy of SwiftUI into something the classic system can understand and render.

Depending on the platform you are working on within Xcode, you will use a different controller:

  • iOS / tvOS: UIHostingController (inherits from UIViewController).
  • macOS: NSHostingController (inherits from NSViewController).
  • watchOS: WKHostingController (inherits from WKInterfaceController).

By inheriting from the standard base classes of each platform, these hosting controllers can be introduced into your existing navigation hierarchy (like a UINavigationController or presented as a modal) exactly like any other controller in your application.


2. Integration in iOS: Injecting SwiftUI into UIKit

Let’s dive into the code. Imagine you have a traditional iOS application and your design team has created a spectacular new user profile screen. Instead of dealing with Auto Layout and UITableView, you decide to build it in SwiftUI.

Step 2.1: Creating the View in SwiftUI

First, we create our declarative view in Swift. Open your project in Xcode and create a new SwiftUI View file.

import SwiftUI

struct UserProfileView: View {
    var username: String
    var bio: String
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "person.circle.fill")
                .resizable()
                .frame(width: 100, height: 100)
                .foregroundColor(.blue)
            
            Text(username)
                .font(.largeTitle)
                .fontWeight(.bold)
            
            Text(bio)
                .font(.body)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
                .padding()
            
            Button(action: {
                print("Profile edited")
            }) {
                Text("Edit Profile")
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            .padding(.horizontal)
            
            Spacer()
        }
        .padding()
    }
}

Step 2.2: Presenting Programmatically

If your architecture in Swift programming is based on initializing views programmatically (without Storyboards), the process is extremely straightforward. We need to instantiate a UIHostingController and pass our UserProfileView as the root view.

From any existing UIViewController, you can do the following:

import UIKit
import SwiftUI

class HomeViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setupNavigateButton()
    }
    
    private func setupNavigateButton() {
        let button = UIButton(type: .system)
        button.setTitle("View Profile (SwiftUI)", for: .normal)
        button.addTarget(self, action: #selector(showProfile), for: .touchUpInside)
        
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    @objc private func showProfile() {
        // 1. Instantiate the SwiftUI view
        let swiftUIView = UserProfileView(username: "AppleFan99", bio: "Passionate iOS Developer creating the future.")
        
        // 2. Wrap the view in a UIHostingController
        let hostingController = UIHostingController(rootView: swiftUIView)
        
        // 3. Present the controller as we would with any UIViewController
        navigationController?.pushViewController(hostingController, animated: true)
        
        // Alternatively, to present modally:
        // present(hostingController, animated: true)
    }
}

Step 2.3: Integration using Storyboards / Interface Builder

If your current project relies heavily on Storyboards, Xcode makes the job much easier. Apple introduced the Hosting Controller directly into the Object Library of Interface Builder.

  1. Open your Main.storyboard file.
  2. Press Cmd + Shift + L to open the library.
  3. Search for Hosting Controller and drag it to your canvas.
  4. Create a Segue from your source controller to this new Hosting Controller.
  5. To inject the view, we will use an IBSegueAction.

In your source UIViewController, add the following code:

import UIKit
import SwiftUI

class DashboardViewController: UIViewController {

    // This function is linked directly from the Storyboard
    @IBSegueAction func showSwiftUIProfile(_ coder: NSCoder) -> UIViewController? {
        let profileView = UserProfileView(username: "StoryboardUser", bio: "Integrating SwiftUI visually.")
        return UIHostingController(coder: coder, rootView: profileView)
    }
}

Note: Drag from the segue in the Storyboard to the code of your controller to connect this action.


3. Two-Way Communication: Data and Delegates

Knowing how to integrate SwiftUI into UIKit is only the first step. The real challenge for an iOS Developer is getting both technologies to communicate smoothly. What happens if a button in SwiftUI needs to update a label in UIKit, or if UIKit downloads data from the internet that SwiftUI needs to show?

From UIKit to SwiftUI: ObservableObject

To pass dynamic data from the imperative world to the declarative one, the best tool in Swift is the ObservableObject protocol along with the Combine framework.

We create a View Model (ViewModel):

import Foundation
import Combine

class UserViewModel: ObservableObject {
    @Published var followerCount: Int = 0
    
    func fetchFollowers() {
        // Simulate a network call
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.followerCount = Int.random(in: 100...5000)
        }
    }
}

We modify our SwiftUI view to react to this model:

import SwiftUI

struct StatsView: View {
    @ObservedObject var viewModel: UserViewModel
    
    var body: some View {
        VStack {
            Text("Followers")
                .font(.headline)
            Text("\(viewModel.followerCount)")
                .font(.system(size: 50, weight: .bold))
                .foregroundColor(.green)
        }
    }
}

In our UIKit controller, we keep the reference of the ViewModel and update the data when necessary. SwiftUI will redraw automatically.

class StatsViewController: UIViewController {
    var viewModel = UserViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let swiftUIView = StatsView(viewModel: viewModel)
        let hostingController = UIHostingController(rootView: swiftUIView)
        
        // Add the hosting controller as a child
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.frame = view.bounds
        hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        hostingController.didMove(toParent: self)
        
        // Start downloading data from UIKit
        viewModel.fetchFollowers()
    }
}

From SwiftUI to UIKit: Closures and Delegates

If you need to notify UIKit about a user action in SwiftUI (for example, tapping a “Close” or “Save” button), closures (code blocks) are the most “Swift-friendly” way to achieve this.

import SwiftUI

struct ActionView: View {
    // Closure that will be implemented by UIKit
    var onDismiss: (() -> Void)?
    
    var body: some View {
        Button(action: {
            // Execute the closure when the button is tapped
            onDismiss?()
        }) {
            Text("Close Screen")
                .foregroundColor(.red)
        }
    }
}

In UIKit:

class PresenterViewController: UIViewController {
    
    func presentActionView() {
        var actionView = ActionView()
        
        // Define the behavior of the closure
        actionView.onDismiss = { [weak self] in
            self?.dismiss(animated: true, completion: nil)
        }
        
        let hostingController = UIHostingController(rootView: actionView)
        present(hostingController, animated: true)
    }
}

4. Expanding Horizons: Integration in macOS (AppKit)

The power of modern Swift programming is that the declarative paradigm is cross-platform. If you are developing for Mac computers using Xcode, the traditional framework is not UIKit, but AppKit.

To integrate SwiftUI into a legacy macOS application, the process is conceptually identical to iOS, but we use NSHostingController or NSHostingView.

Using NSHostingView

Unlike iOS, in AppKit it is very common to work directly at the view level rather than relying solely on View Controllers. NSHostingView allows us to embed SwiftUI directly into an NSView hierarchy.

import Cocoa
import SwiftUI

class MacSettingsViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. Create the SwiftUI view
        let settingsSwiftUIView = Text("System Preferences (SwiftUI)")
            .font(.title)
            .padding()
        
        // 2. Create the NSHostingView
        let hostingView = NSHostingView(rootView: settingsSwiftUIView)
        hostingView.translatesAutoresizingMaskIntoConstraints = false
        
        // 3. Add it to the main AppKit view
        self.view.addSubview(hostingView)
        
        // 4. Configure constraints
        NSLayoutConstraint.activate([
            hostingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            hostingView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

Using NSHostingController

If you prefer to work at the window/view controller level in macOS:

import AppKit
import SwiftUI

func openSwiftUIWindow() {
    let mySwiftUIView = UserProfileView(username: "MacUser", bio: "macOS Development")
    let hostingController = NSHostingController(rootView: mySwiftUIView)
    
    let window = NSWindow(contentViewController: hostingController)
    window.title = "SwiftUI Window in AppKit"
    window.makeKeyAndOrderFront(nil)
}

5. The Smartwatch: Integration in watchOS (WatchKit)

Development on watchOS was one of the first to adopt SwiftUI natively. In fact, Apple highly recommends that all new watchOS applications be built purely with this declarative framework. However, if as an iOS Developer you must maintain an older application that uses WatchKit Storyboards (WKInterfaceController), there is also a bridge.

We use WKHostingController. It works slightly differently than its counterparts in iOS and macOS, since WatchKit controllers do not have the same flexible view hierarchies.

import WatchKit
import Foundation
import SwiftUI

// Your modern SwiftUI view
struct HeartRateView: View {
    var bpm: Int
    
    var body: some View {
        VStack {
            Image(systemName: "heart.fill")
                .foregroundColor(.red)
                .font(.largeTitle)
            Text("\(bpm) BPM")
                .font(.title2)
        }
    }
}

// The classic WatchKit controller hosting the view
class HeartRateHostingController: WKHostingController<HeartRateView> {
    
    // You must override this computed property to return your SwiftUI view
    override var body: HeartRateView {
        return HeartRateView(bpm: 72)
    }
    
    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        // Additional initial configuration
    }
}

In WatchKit, you need to specify the generic view type (<HeartRateView>) in the class declaration and provide the instance in the body property.


6. Best Practices and Performance for the iOS Developer

Learning the syntax of how to integrate SwiftUI into UIKit is relatively quick, but mastering the architecture requires experience. Here are several key recommendations when combining these frameworks in Xcode:

Lifecycle Management

Declarative views do not have an exact lifecycle like viewDidLoad or viewWillAppear. They have modifiers like .onAppear and .onDisappear.

  • Common problem: If you trigger heavy network calls in .onAppear inside a cell of a mixed UIKit/SwiftUI list, you could saturate the main thread because .onAppear might be called multiple times during scrolling.
  • Solution: Coordinate network calls from your parent UIViewController and simply inject the resulting data via an @ObservedObject or through state passed to the embedded view.

Intrinsic Content Size

When you add a UIHostingController inside a layout based on Auto Layout, you often want the UIKit view to adapt to the exact size of your declarative view’s content.
Starting from iOS 16, Apple improved the sizingOptions property on the hosting controller.

let hostingController = UIHostingController(rootView: myView)
// Allows the UIKit view to automatically size based on SwiftUI content
hostingController.sizingOptions = .intrinsicContentSize

Limit Bridge Points

Although it is technically possible to have a UIViewController containing a UIHostingController, which in turn has a UIViewRepresentable to show a classic UIKit UILabel, do not do it.
The context switch between the imperative Core Animation rendering system and the declarative state engine comes with a small performance cost.

  • Golden rule: Try to make the “bridge” happen only at the “Full Screen” level or “Full Component” level (like a complete complex table cell). Avoid deep, nested embeddings like Russian dolls.

Beware of Retain Cycles in Closures

As we saw in the two-way communication section, when passing closures from your UIViewController to your SwiftUI views, always make sure to capture [weak self] or [unowned self] if the closure references properties of the parent controller. Since the parent controller retains the UIHostingController, which retains the SwiftUI view (where the closure lives), it is very easy to create a retain cycle (Memory Leak) in Swift programming.


Final Summary

The path of the modern iOS Developer requires adaptability. Knowing how to integrate SwiftUI into UIKit (and its AppKit and WatchKit counterparts) gives you a superpower: the ability to modernize applications iteratively, one screen at a time, without the risk and cost of rewriting an entire project from scratch.

Xcode has matured enough for this coexistence to be robust and efficient. We have seen that the master key for this workflow in Swift programming are the hosting controllers (UIHostingController, NSHostingController, and WKHostingController). By combining these tools with proper data flow management using ObservableObject and Combine, you can create flawless, hybrid user experiences across the entire Swift ecosystem.

Leave a Reply

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

Previous Article

SwiftUI and AppKit Integration

Related Posts