Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

UIViewRepresentable explained to host UIView instances in SwiftUI

Adopting the UIViewRepresentable protocol allows you to host UIView instances in SwiftUI. Your SwiftUI code is converted to UIKit views behind the scenes. SwiftUI requires you to declare the view representation, but the underlying SwiftUI implementation will optimize how final views are drawn on the screen. You can use a similar technique to make SwiftUI views available in UIKit views by implementing the UIHostingController.

The UIViewRepresentable protocol allows you to adapt to the mechanism of SwiftUI declarative views and creates a bridge between UIKit and SwiftUI. Whether it’s a custom UIView created by yourself or a UIView provided by Apple that isn’t available in UIKit yet: you can make them available in SwiftUI. In UIKit, you use the UIHostingController, which acts like a SwiftUI host for your views.

While picking your minimum iOS version to support, you might have opened the door to fully adopting SwiftUI after dropping iOS 12. You don’t want to rewrite all your existing UIKit views immediately, so you can smoothen the process of migrating to SwiftUI by making your custom views available in SwiftUI.

Adding a UIKit View to a SwiftUI View

When migrating your projects to SwiftUI, you probably have a lot of existing code that you don’t want to throw away but rather use in our newly built views. I’ve earlier explained how you can use Xcode Previews with existing UIKit views, which we’ll take to the next level in this post by adapting existing UIKit views in SwiftUI using the UIViewPresentable protocol.

Hosting a UIKit View in SwiftUI using UIViewRepresentable

Not every UIKit view is already available in SwiftUI for you to use. You can run into examples like the SWAttributionView from the new SharedWithYou framework that has to be implemented using a UIKit view.

To make these views available in SwiftUI, you’ll have to create a hosting view using the UIViewRepresentable protocol:

struct SharedWithYouAttributionView: UIViewRepresentable {
    let highlight: SWHighlight
    let displayContext: SWAttributionView.DisplayContext

    func makeUIView(context: Context) -> SWAttributionView {
        return SWAttributionView()
    }

    func updateUIView(_ uiView: SWAttributionView, context: Context) {
        uiView.highlight = highlight
        uiView.displayContext = displayContext
    }
}

SwiftUI calls the makeUIView method once to let you instantiate the UIKit view. For any redraw, SwiftUI will call into the updateUIView method to let you update the underlying UIKit view.

It’s important to understand that the outer struct (in this case SharedWithYouAttributionView) will be initialized for every redraw. Therefore, you shouldn’t do any heavy lifting in the initializer of this instance and mainly implement your custom code inside the make and update method.

Once defined, you can start making use of your UIView representing SwiftUI view:

struct ContentView: View {

    // ..

    var body: some View {
        SharedWithYouAttributionView(highlight: highlight, displayContext: .summary)
    }
}

While this works great, you can see that we have to bridge every property that we want to configure. In this case, we had to define accessors for the highlight and the display context.

I’m not sure about you, but I found this quite annoying. I wrote a convenience library to simplify adding UIKit views to SwiftUI.

Adding a UIKit UIView to SwiftUI using SwiftUIKitView

To make it easier to work with UIView elements in SwiftUI I’ve created a convenience library called SwiftUIKitView. After explaining how my framework works, I’ll go into more detail about the implementation, which uses the UIViewRepresentable protocol underneath.

By importing SwiftUIKitView, we can make use of a new view called UIViewContainer(_,layout:) which will convert our UIView into a SwiftUI View:

import SwiftUI
import SwiftUIKitView

struct SwiftUIwithUIKitView: View {
    var body: some View {
        UIViewContainer(SWAttributionView(), layout: .intrinsic) // <- This can host any `UIKit` view.
    }
}

The framework allows setting different kinds of layouts:

  • Intrinsic: auto size to the intrinsic size of the UIView
  • Fixed width: combine an intrinsic height with a fixed width
  • Fixed-size: apply a fixed size to the UIView

Depending on your use case, you can pick the layout you need.

Using Writable Key Paths references to update properties

We can make use of the KeyPathReferenceWritable protocol that comes with the framework to update the views in a way we’re used to with SwiftUI:

struct ContentView: View {

    // ..

    var body: some View {
        UIViewContainer(SWAttributionView(), layout: .intrinsic)
            .set(\.highlight, to: highlight) // <- Use key paths for updates.
            .set(\.displayContext, to: .summary)
    }
}

As you can see in this example, we can configure the SWAttributionView attributes directly without creating any custom bridges. The set(_, to:) method allows modifying any writable property on the UIKit view you’re hosting. The modifier is available on UIViewContainer instances that adapt the UIViewRepresentable protocol underneath.

Using the same technique to create Xcode Previews of UIView elements

Another significant benefit of using the SwiftUIKitView framework is creating Xcode Previews of existing UIView elements in your project.

struct UILabelExample_Preview: PreviewProvider {
    static var previews: some View {
        UILabel() // <- This is a `UIKit` view.
            .swiftUIView(layout: .intrinsic) // <- This is a SwiftUI `View`.
            .set(\.text, to: "Hello, UIKit!")
            .preview(displayName: "UILabel Preview Example")
    }
}

This results in the following preview:

An Xcode Preview of a UIKit UIView element adapted using the UIViewRepresentable protocol.
An Xcode Preview of a UIKit UIView element adapted using the UIViewRepresentable protocol.

Redraw performance for SwiftUI Previews is less performant, so we can initiate the UIKit view directly. The swiftUIView(layout🙂 modifier converts the UIKit view into a SwiftUI view, making it available as a preview. Using SwiftUIKitView in the above manner works excellent if you only want to create previews for your UIKit views.

Using composition for modifiers

The underlying mechanism of the SwiftUIKitView framework makes use of composition for each applied modifier. Like I explained to you earlier, each hosting struct adapting to the UIViewRepresentable protocol will be initialized on every SwiftUI redraw. SwiftUI does this to compare the previous state with the new state of your view and determines whether your UIKit view has changed. The structs adapting to the UIViewRepresentable protocol can’t be mutated, which made it hard to store the key path modifiers inside the root hosting view.

Therefore, the framework makes use of composition, resulting in views like ModifiedUIViewContainer<UIViewContainer, SWAttributionView, SWHighlight>. SwiftUI will recursively run through each modified container, and each modified container will call into its parent to apply its update methods accordingly.

I won’t go into more detail for this article, but if I sparked your interest, I encourage you to read through the source files.

Presenting a SwiftUI view in a UIKit view controller

You might also have the need to adapt SwiftUI views inside UIKit view controllers. When you’re starting a new feature, it’s a good idea to develop it in SwiftUI. In cases your app uses UIKit in its foundation, you might have the need to create a bridge for which SwiftUI introduced an instance called UIHostingController that takes the responsibility of “hosting” a SwiftUI view.

UIHostingController itself inherits from UIViewController which makes it possible to work with it like you would with any other view controller instance. Therefore, the code to present a SwiftUI view is as simple as this:

func presentSwiftUIView() {
    let swiftUIView = SwiftUIView()
    let hostingController = UIHostingController(rootView: swiftUIView)
    present(hostingController, animated: true, completion: nil)
}

Adding a SwiftUI view to a UIKit view

The same technique applies to adding a SwiftUI view to a UIKit view hierarchy. The only difference is that you’re not presenting the view but adding it as a child view controller:

func addSwiftUIView() {
    let swiftUIView = SwiftUIView()
    let hostingController = UIHostingController(rootView: swiftUIView)

    /// Add as a child of the current view controller.
    addChild(hostingController)

    /// Add the SwiftUI view to the view controller view hierarchy.
    view.addSubview(hostingController.view)

    /// Setup the constraints to update the SwiftUI view boundaries.
    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)

    /// Notify the hosting controller that it has been moved to the current view controller.
    hostingController.didMove(toParent: self)
}

There are a few things that we do here:

  • First, we add the hosting controller as a child to the current view controller
  • The view is added to the view hierarchy of the current view controller
  • Constraints are set up in code to update the boundaries of our SwiftUI view. You can learn more about writing Auto Layout in code here
  • The hosting controller is notified that its containing SwiftUI view has moved to a parent

Creating an extension to simplify adding SwiftUI views

To make this more efficient, we can wrap this logic inside an extension on UIViewController. It allows us to write the same code as above as follows:

func addSwiftUIView() {
    let swiftUIView = SwiftUIView()
    addSubSwiftUIView(swiftUIView, to: view)
}

The extension for this looks mostly the same as our code before:

extension UIViewController {

    /// Add a SwiftUI `View` as a child of the input `UIView`.
    /// - Parameters:
    ///   - swiftUIView: The SwiftUI `View` to add as a child.
    ///   - view: The `UIView` instance to which the view should be added.
    func addSubSwiftUIView<Content>(_ swiftUIView: Content, to view: UIView) where Content : View {
        let hostingController = UIHostingController(rootView: swiftUIView)

        /// Add as a child of the current view controller.
        addChild(hostingController)

        /// Add the SwiftUI view to the view controller view hierarchy.
        view.addSubview(hostingController.view)

        /// Setup the contraints to update the SwiftUI view boundaries.
        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)

        /// Notify the hosting controller that it has been moved to the current view controller.
        hostingController.didMove(toParent: self)
    }
}

This makes it really easy to add views to your UIKit view hierarchy and even allows you to add SwiftUI views to container views defined in the interface builder.

The benefits of using SwiftUI as soon as possible in a UIKit app

Transitioning from a UIKit app to SwiftUI is time-consuming and will probably take years for larger apps. Apps written in Objective-C had to take the same transition when Swift was introduced and are likely still not completely moved over. Therefore, adopting SwiftUI early on comes with a few benefits:

  • No need to rewrite your new views in the future to SwiftUI
  • You start learning SwiftUI while developing as you usually do
  • The transition to SwiftUI takes time. The earlier you start, the earlier your app will be completely written in SwiftUI

At WeTransfer, we’ve started this transition early on to make steps forward as soon as possible. You can read more about our early conclusions in this blog post.

Things to consider when adopting SwiftUI in a UIKit application

It’s important to know that you can only use SwiftUI on iOS 13 and up. On top of that, SwiftUI is a new technology that has been improved in later updates. Dropping iOS 12 means you can start with SwiftUI, but it also means that you start using the first version of SwiftUI. This could lead to unexpected behavior and bugs from the early days.

Using SwiftUI for new features only

Regarding this matter, you could decide to use SwiftUI for new features only. This scenario is possible even if you’re not dropping iOS 12. You will use the available APIs that make your newly written views available only on the versions supporting SwiftUI.

if #available(iOS 13.0, *) {
    presentSwiftUIView()
} else {
    // Fallback on earlier versions
}

You can decide to deliver new features with SwiftUI, making that code future-proof as it removes the need to rewrite it in a few years. This obviously only works if you can decide to make a specific feature only available to users on iOS 13 and up.

Conclusion

Adding SwiftUI views in a UIKit application early on makes your future self a lot happier as you don’t have to rewrite it later on again. The UIViewController extension method allows you to add a SwiftUI view in a few lines of code. Decide whether or not you can build a new feature with SwiftUI and make use of the UIHostingController to present SwiftUI views. By making use of SwiftUIKitView you can leverage adding UIKit views to SwiftUI without having to define custom bridges yourself.

If you want to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

 

Stay Updated with the Latest in SwiftUI

You don't need to master everything, but staying informed is crucial. Join our community of 18,250 developers and stay ahead of the curve:


Featured SwiftLee Jobs

Find your next Swift career step at world-class companies with impressive apps by joining the SwiftLee Talent Collective. I'll match engineers in my collective with exciting app development companies. SwiftLee Jobs