How to Use UIViewController in SwiftUI

For as long as anybody can remember, UIKit has been a part of iOS development. It offers a wide variety of system and user-created application programming interfaces. SwiftUI views integrate well with UIView and UIViewController, making up for the lack of features that SwiftUI possesses.

Here are the topics we'll discuss:

  • Learn to include a UIViewController and UIView into a SwiftUI window.
  • Understanding the mechanics of information exchange between UIKit and SwiftUI.
  • The UIViewRepresentable view and the UIViewControllerRepresentable view's lifetimes.

Integrating a UIViewController into a SwiftUI Application

Next, we'll look at how to add a view controller to the SwiftUI view hierarchy:

  • Make use of the UIViewControllerRepresentable declaration to declare a view in SwiftUI.
  • Use makeUIViewController() and updateUIViewController() to complete the controller's implementation ().

Start out by enclosing a font selector in a SwiftUI view:

By using the UIFontPickerViewController, we can choose a font family among the available fonts on the device.

import UIKit
import SwiftUI
// 1.
struct FontPicker: UIViewControllerRepresentable {
    // 2.
    func makeUIViewController(context: Context) -> UIFontPickerViewController {
        return UIFontPickerViewController()
    }
    // 3.
    func updateUIViewController(_ uiViewController: UIFontPickerViewController, context: Context) {
    }
}
  • Use the FontPicker declaration to indicate an instance of the UIFontPickerViewController class.
  • The first view controller is given back by the make function.
  • ·Because of the update function, UIViewController is always up-to-date with the latest changes to SwiftUI's state.

In this case, we don't fill it in since our view controller doesn't rely on information from the rest of our SwiftUI app.

Display the font selection modal at the end of ContentView. When displaying modals in SwiftUI, we use the sheet() view modifier. The modifier exposes or conceals a view depending on the value of a binding:

struct ContentView: View {
    @State private var isPresented = false
    var body: some View {
        Button("Pick a font") {
            self.isPresented = true
        }.sheet(isPresented: $isPresented) {
            FontPicker()
        }
    }
}

The result looks next:

Information Exchange Between UIViewController and SwiftUI

Nothing occurs when you launch the app and choose a font from the menu. To do this, we'd need FontPicker to return the font's chosen value and then exit.

SwiftUI's @Binding property wrapper is how a view may communicate with its underlying model. Please include this feature into FontPicker:

@Binding var font: UIFontDescriptor?

Information and actions performed in the view controller are not immediately sent to other portions of a SwiftUI project. To facilitate these connections, we must provide an instance of the Coordinator class. Communicating with UIKit through delegation, target-actions, callbacks, and KVO is the coordinator's job.

extension FontPicker {
    class Coordinator: NSObject, UIFontPickerViewControllerDelegate {
        var parent: FontPicker
        init(_ parent: FontPicker) {
            self.parent = parent
        }
        func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
            parent.font = viewController.selectedFontDescriptor
        }
    }
}


Having a coordinator class is necessary. Using this naming convention and placing it within the appropriate view is not required.

Then, modify FontPicker by include the makeCoordinator() function. The original coordinator is returned.

func makeCoordinator() -> FontPicker.Coordinator {
    return Coordinator(self)
}


Co-ordinator is sent to make and update implicitly through the context parameter. The coordinator is usually wired up using the make method:

func makeUIViewController(context: UIViewControllerRepresentableContext<FontPicker>) -> UIFontPickerViewController {
    let picker = UIFontPickerViewController()
    picker.delegate = context.coordinator
    return picker
}


We're down to a single remaining option. After the user has chosen a font, we'd want to have the modal disappear. We may leverage the presentation mode of the environment for this purpose. To get the presentationMode from the FontPicker's environment, add the @Environment property wrapper.

@Environment(\.presentationMode) var presentation Mode


The app's environment is similar to a dictionary in that it stores global settings and preferences. The data is propagated automatically from the parent view to any subviews.


The correct location to dismiss the font selector is in the coordinator's delegate method. To fontPickerViewControllerDidPickFont(), add the following line:

parent.presentation
Mode.wrappedValue.dismiss()


Finally, let's use ContentView to show off the chosen typeface:

struct ContentView: View {
    @State private var isPresented = false
    @State private var font: UIFontDescriptor?
    var body: some View {
        VStack(spacing: 30) {
            Text(font?.postscriptName ?? "")
            Button("Pick a font") {
                self.isPresented = true
            }
        }.sheet(isPresented: $isPresented) {
            FontPicker(font: self.$font)
        }
    }
}


Here’s the output:

Putting SwiftUI's UIView to Work

Adding a UIView is quite similar to adding a UIViewController. In particular, the SwiftUI view has to follow the same set of rules as the UIViewRepresentable protocol and implement the same set of functions.

For a sample of how UIActivityIndicatorView may be implemented in SwiftUI, have a look at the following:

struct Spinner: UIViewRepresentable {
    let isAnimating: Bool
    let style: UIActivityIndicatorView.Style
    func makeUIView(context: Context) -> UIActivityIndicatorView {
        let spinner = UIActivityIndicatorView(style: style)
        spinner.hidesWhenStopped = true
        return spinner
    }
    func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
    }
}


Let’s display the spinner in ContentView:

struct ContentView: View {   
    @State private var isAnimating = false
    var toggle: some View {
        Toggle(isOn: $isAnimating) { EmptyView() }
            .labelsHidden()
    }
    var body: some View {
        VStack(spacing: 30) {
            toggle
            Spinner(isAnimating: isAnimating, style: .large)
        }
    }
}


You'll get the following output if you execute this code: