How to use SwiftUI Views in UIKit projects?

Eyup Cimen
Ozan SuperApp
Published in
5 min readMay 12, 2023
My little setup :)

When SwiftUI was introduced, like many iOS developers, I was like “wow”. Personally, I didn’t expect such an attack from Apple. I had some unpleasant memories of writing React Native, which also has a declarative structure, but I think the main reason for that was my experience with TypeScript. Anyway, I was very excited when Apple introduced SwiftUI, because this development was the beginning of a change in UI design.

We can say that SwiftUI is the beginning of a change in UI design in the Apple ecosystem. It provides a level of comfort that is incomparable to UIKit. It’s in your pocket. And yes, you can play Pollyanna by opening a zero project there. You can bounce the views, jump from one animation to another, but when it comes to using SwiftUI in your existing project, that’s where things change. It’s at this point that you come face to face with bitter truths. The first of these bitter truths is your existing UIKit project :)

Since its first introduction in 2014, SwiftUI 1.0 has been updated with many versions and constantly filled its shortcomings. However, if you ask me, even today, it does not have all the capabilities of the current UIKit (as of May 2023). Even if you think of a project from scratch and support ios 16 for the minimum development target, this is still the case. Therefore, this brings with it a very nice API like SwiftUI, but how much is SwiftUI’s place in the industry? Which companies develop applications from scratch with SwiftUI or how much do they support SwiftUI, or how much do they integrate it into their existing projects and write new feature sets with it. I don’t have all the answers to these questions, but the impression is still that UIKit is still dominant. The reason for this impression is that the code base of large companies and existing large projects is written with UIKit. Imagine starting a project and developing it using an API that still has a lot of missing pieces, or a structure that has been completed with all its features. If you don’t have extra time to spend on development, probably everyone will say UIKit. There are many disadvantages to using SwiftUI in projects starting from scratch. Navigation problems, text field focus problems, etc. For example, if your minimum development target is not iOS 16 and you will support lower iOS versions, using SwiftUI will cause you a lot of problems. The stone you throw may not be worth the frog you scare.

You can write it, but you may encounter tons of problems while trying to implement your design and business logic. If you say we will enter this fantasy, of course, the decision is yours :)

So you might be wondering how we can benefit from this beauty in our current projects. You’re right, we need to take advantage of this new API and not fall behind on the latest innovations, right?

Yes, we want to use SwiftUI in our current project, but how can we use it without affecting the screens that are already built with UIKit, and still take advantage of the benefits of SwiftUI?

At Ozan SuperApp, we’re trying to use SwiftUI in a way that’s possible and won’t disrupt the existing layout. We treat the new feature that will be added as a separate module and gather as a team to analyze the screens that will be added to the application. If using SwiftUI doesn’t cause any obstacles, we decide to use it.

Note: We can specifically add SwiftUI views into view controllers without disrupting the existing structure. This is because there is a deep link mechanism inside and the existing routing mechanism is dependent on view controllers and navigation controllers.

UIHostingController

So what happens next? I will try to explain it simply using an empty view controller so that everyone can easily follow along. You have a new view controller and you want to add a SwiftUI view to it. You can design your SwiftUI view as you wish. I created an example view as follows.

import SwiftUI

struct MySwiftUIView: View {
var body: some View {
ZStack {
Color.green.opacity(0.3)
.ignoresSafeArea()
Text("Hello, World!")
.font(.title)
}
}
}

struct MySwiftUIView_Previews: PreviewProvider {
static var previews: some View {
MySwiftUIView()
}
}

And call this SwiftUI view optionally in your View Controller. Here, I created mySwiftUIView in viewDidLoad, but normally you should create it from a different screen and set it from there, which is more appropriate. Since this is an example, I had to create it in viewDidLoad.

class ViewController: UIViewController {

private var mySwiftUIView: UIHostingController<MySwiftUIView>?

override func viewDidLoad() {
super.viewDidLoad()
mySwiftUIView = UIHostingController(rootView: MySwiftUIView())
addSUIView()
}

private func addSUIView() {
guard let mySwiftUIView = mySwiftUIView else {
return
}
addChild(mySwiftUIView)
mySwiftUIView.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mySwiftUIView.view)
mySwiftUIView.didMove(toParent: self)
NSLayoutConstraint.activate([
mySwiftUIView.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
mySwiftUIView.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
mySwiftUIView.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
mySwiftUIView.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
}

If you write an extension for UIViewController, then you can call the desired SwiftUI view in all your view controllers with just one line like this:

import UIKit
import SwiftUI

class ViewController: UIViewController {

private var mySwiftUIView: MySwiftUIView?

override func viewDidLoad() {
super.viewDidLoad()
mySwiftUIView = MySwiftUIView()
addSUIView()
}

private func addSUIView() {
guard let mySwiftUIView = mySwiftUIView else {
return
}
addSubSwiftUIView(mySwiftUIView, to: self.view)
}
}

extension UIViewController {
@discardableResult
func addSubSwiftUIView<Content>(_ swiftUIView: Content, to view: UIView) -> UIHostingController<Content> where Content: View {
let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
view.bottomAnchor.constraint(equalTo: hostingController.view.bottomAnchor),
view.rightAnchor.constraint(equalTo: hostingController.view.rightAnchor)
]
NSLayoutConstraint.activate(constraints)
hostingController.didMove(toParent: self)
return hostingController
}
}

We defined MySwiftUIView as optional here as well. I recommend injecting these properties from the outside. I tried to write it in its simplest form for this example. You can download this example from the following link:

Of course, this example is not exactly the same as the one we use in our main project. There, we create our View Model where we create this view controller and inject it into the SwiftUI view from the outside. We pass data fetch and user interaction operations through the SwiftUI view, first to the View Model and then to the view controller. This architectural approach is the subject of another article. Who knows, maybe we’ll write about it someday :)

--

--