Integration of SwiftUI into UIKit

So, the requirements are met — but how can you begin with the migration? Fortunately, SwiftUI offers the possibility to integrate single units (e.g. a screen or even a control element) written in SwiftUI into UIKit code. This makes it possible to migrate the old UIKit code to SwiftUI step by step or to implement only new UI elements in SwiftUI.

To get a feeling for SwiftUI, we choose an existing simple view controller for migration: the TextViewController. Its purpose is to display information like imprint, privacy policy, and license agreement consisting of a headline and a multiline text.

TextViewController in Storyboard (UIKit)
TextViewController in Storyboard (UIKit)

First, a new file TextView.swift is created in which the view can be setup. In a VStack, which vertically aligns the contained elements, an HStack consisting of the cronn logo and a label (Text) is placed. Utilizing the alignment: .leading parameter of the VStack the contained elements can be aligned according to the set reading direction. The appearance of the Text is adjusted by so-called ViewModifiers. Using the lineLimit(1) ViewModifier ensures that the Text is presented as a single line without line breaks. The font(.title) displays the Text in standard iOS title style. The actual information text is shown in a ScrollView below the header line. Finally, a Button consisting of the Text “OK” is inserted, which calls a dismissAction() method passed to the view when used. To center the label a Spacer is inserted before and after the Button. Using the padding() ViewModifier on the VStack creates some space towards to the contained elements.

import SwiftUI

struct TextView: View {
    @ObservedObject var textStore: TextStore
    var dismissAction: (() -> Void)?

    var body: some View {
        VStack(alignment: .leading) {
            HStack() {
                Image("cronnGreyscale")
                Text(textStore.displayText.title)
                    .lineLimit(1)
                    .font(.title)
            }
            
            ScrollView {
                Text(textStore.displayText.body)
            }
            
            Button(action: {
                (self.dismissAction ?? {})()
            }) {
                Spacer()
                Text("OK")
                Spacer()
            }
        }.padding()
    }
}
TextView in SwiftUI

According to the Model-view-viewmodel (MVVM) design pattern used by SwiftUI, we also create a viewmodel TextStore which accesses the already existing model DisplayText. In this simple case, the sole purpose of the viewmodel is to monitor changes in the model and to inform the view about such changes by utilizing the @Published property wrappers. So, if the text stored in the model changes in the meantime, the view will be redrawn automatically. This will not happen in the current example but is very useful for other use cases.

class TextStore: ObservableObject {
    @Published var displayText: DisplayText

    init(displayText: DisplayText) {
        self.displayText = displayText
    }
}

struct DisplayText: Codable {
    var title: String
    var body: String
}
TextStore (viewmodel) and DisplayText (model)

But what are the options for instant visual feedback while designing a view? In the UIKit world, there are storyboards or .xib files through which views can be graphically designed and composed. This allows you to get an impression of the user interface even before testing on the simulator or real device. SwiftUI provides previews for this purpose, which are implemented in the same file as the view. These allow a graphical preview to be displayed next to the code. To create the preview, we initialize a corresponding view with sample data in the previews property of a PreviewProvider. If something in the code changes in the view, the preview is automatically redrawn. Since Xcode 14, the previews can also be displayed on different device types using the previewDevice(_:) ViewModifier.

struct TextView_Previews: PreviewProvider {
    static var previews: some View {
        TextView(textStore:
            TextStore(displayText:
                DisplayText(
                    title: "Lorem ipsum dolor sit er elit lamet, consectetaur",
                    body: """
                            Lorem ipsum dolor sit amet, consetetur sadipscing 
                            elitr, sed diam nonumy eirmod tempor invidunt ut 
                            labore et dolore magna aliquyam erat, sed diam 
                            ...
                            Lorem ipsum dolor sit amet,
                          """
                )
            )
        )
        .previewDevice(PreviewDevice(rawValue: "iPhone 8"))
        .previewDisplayName("iPhone 8")
    }
}
TextView Preview in SwiftUI
TextView Preview (SwiftUI)
TextView Preview (SwiftUI)

How can the TextView be integrated into the existing project to replace the old TextViewController? For this purpose, there is the UIHostingController, which allows you to embed any SwiftUI view into a UIKit View Controller. The view model of the type TextStore is passed to the initializer of the view controller, which is needed to initialize the TextView. Using super.init(rootView: textView) we then initialize the view controller with the SwiftUI view as rootView. Finally, the dismissAction() comes into play. If the OK button is tapped, we let the view controller dismiss itself.

import SwiftUI

class TextViewController: UIHostingController<TextView> {
    init(textStore: TextStore) {
        let textView = TextView(textStore: textStore)
        super.init(rootView: textView)

        rootView.dismissAction = {
            self.dismiss(animated: true, completion: nil)
        }
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
TextView embedded in TextViewController (UIHostingController)

Depending on how the original view controller was implemented and used, some minor adjustments must be made. In the case of the Image Recognition Playground app, the TextViewController was previously presented programmatically and not using storyboard segues. However, it was previously initialized via the storyboard, which is now also done programmatically. Therefore, the following change is made.

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let textVC = storyboard.instantiateViewController(
                            withIdentifier: "TextVC")
                            as! TextViewController
textVC.displayText = displayText
self.present(textVC, animated: true, completion: nil)
Before: Initialization and presentation of the UIKit-TextViewController
let textStore = TextStore(displayText: displayText)
let textVC = TextViewController(textStore: textStore)
self.present(textVC, animated: true, completion: nil)
After: Initialization and presentation of the SwiftUI-TextViewController

Finally, the TextViewController can now be removed from the storyboard. The next post of the series will be about the integration of UIKit code into SwiftUI and how to set the app’s entry point to a SwiftUI view.