SwiftUI meets UIKit

luca d'angelo
6 min readApr 4, 2022

--

💻 Branch — FrontEnd — SwiftUI Meets UIKit

🎯 Our goal is to give a first approach on how to use SwiftUI and UIKit together.

Introduction

In this article we will show how SwiftUI and “his older brother” UIKit can interact together.

We will give you a first theoretical approach and later we will show you some code. Have Fun 😁

UIKit

UIKit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.

It provides the window and view architecture for implementing your interface, the event handling infrastructure for delivering Multi-Touch and other types of input to your app, and the main run loop needed to manage interactions among the user, the system, and your app. Other features offered by the framework include animation support, document support, drawing and printing support.

SwiftUI

SwiftUI was introduced in WWDC 2019, it is a native framework that provides all the tools needed to create a native app’s user interface.

The framework provides event handlers for delivering taps, gestures, and other types of input to your app, and tools to manage the flow of data from your app’s models down to the views and controls that users will see and interact with.

One of SwiftUI’s major strengths is the ability to integrate UIKit.

How SwiftUI and UIKit can interact

UIKit meets SwiftUI

Many UIKit based applications today are able to integrate new SwiftUI functionalities thanks to ContainerView and a UIHostingController.

UIHostingController

The UIHostingController is a UIKit view controller that manages a SwiftUI view hierarchy. At creation time, specify the SwiftUI view you want to use as the root view for this view controller; you can change that view later using the [rootView](<https://developer.apple.com/documentation/swiftui/uihostingcontroller/rootview>) property.

ContainerView

A container view is a proxy view that stands in for the content of a child view controller. When you add one to your interface, it looks like a normal view, but it has an attached view controller.

Size and position a container view the same way you would other views in your interface. Add constraints to specify the size and position of the view for different devices and in different configurations.

SwiftUI meets UIKit

In this section we will see how to introduce UI elements in SwiftUI. It is possible thanks to a wrapper called UIViewRepresentable for a UIKit view that you use to integrate that view into your SwiftUI view hierarchy.

Adopting this protocol we can integrate in a SwiftUI app an UIView object. To add your view into your SwiftUI interface, create your UIViewRepresentable instance and add it to your SwiftUI interface.

In order to create an UIViewRepresentable object some methods are mandatory:

  • makeUIView configures the view using your app’s current data and contents of the contextparameter. The system calls this method only once, when it creates your view for the first time.
  • func makeUIView(context: Self.Context) -> Self.UIViewType
  • updateUIView updates the state of the specified view with new information from SwiftUI.
  • func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)

SwiftUI calls this method for any changes affecting the corresponding UIKit view.

Go Inside the project

What we want to achieve is shown in this Demo.

UIKit non-modal view in a SwiftUI project

Now we will show our little project in which we integrated artifact 1 app with a UIKit non-modal view with the help of UIViewRepresentable object.

1 - The first step is to create a UIViewRepresentable object in the existing SwiftUI project.

struct NonModalButton : UIViewRepresentable {
//insert your code here
}

In order to create an UIViewRepresentable object some methods are mandatory:

makeUIView configures the view using your app’s current data and contents of the contextparameter. The system calls this method only once, when it creates your view for the first time.

func makeUIView(context: Self.Context) -> Self.UIViewType

updateUIView updates the state of the specified view with new information from SwiftUI.

func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)

SwiftUI calls this method for any changes affecting the corresponding UIKit view.

2 - Create our makeUIView function, it will create our view (UIButton) in which we define our UI components and also it includes an action that is triggered when the button is tapped.

func makeUIView(context: Context) -> UIButton {
// Create and Configure UIButton
let button = UIButton(type: .custom)
let imageConfiguration = UIImage.SymbolConfiguration(pointSize: 100, weight: .medium, scale: .default)
let image = UIImage(systemName: "network", withConfiguration: imageConfiguration)
button.setImage(image, for: .normal)
button.tintColor = UIColor.black

// Create and Add action
let action = UIAction { _ in
self.isPresenting.toggle()
}

button.addAction(action, for: .touchUpInside)
// Return Button
return button
}

Then we created a new Storyboard file that includes a label and a dismiss button as we can see in figure.

To manage the dismiss button we have to create a UIViewController and we have to assign it to our ViewController.

import UIKit
import SwiftUI
class ViewController: UIViewController {

@IBOutlet weak var dismissUIButton: UIButton!

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let imageConfiguration = UIImage.SymbolConfiguration(pointSize: 50, weight: .medium, scale: .default)
let image = UIImage(systemName: "xmark.circle.fill", withConfiguration: imageConfiguration)
dismissUIButton.setImage(image, for: .normal)
dismissUIButton.tintColor = UIColor.label
// Do any additional setup after loading the vi
}

@IBSegueAction func embed(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(coder: coder, rootView: downloadingView())
}


/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/

}

How to assign the UIViewController to our ViewController.

Assign ViewController

updateUIView function let us access in the UIViewController made in the Storyboard file and instantiate it with instantiateViewController Then we need to make a UISheetPresentationController to create a non-modal view with the help of sheet.detents .

In the end we switch from two rootViewController depending by isPresenting boolean variable.

func updateUIView(_ uiView: UIButton, context: Context) {
// Access the UIViewController that has been created in the Storyboard
let storyboard = UIStoryboard.init(name: "Storyboard", bundle: .main)
let presentedViewController = storyboard.instantiateViewController(withIdentifier: "vc") as! ViewController

// Set the coordinator (delegate)
// We need the delegate to use the presentationControllerDidDismiss function
presentedViewController.presentationController?.delegate = context.coordinator

let sheet = presentedViewController.presentationController as! UISheetPresentationController
sheet.prefersGrabberVisible = true
sheet.detents = [.medium() , .large()]

if isPresenting {
uiView.window?.rootViewController?.present(presentedViewController, animated: true)
// adding the dissmissing func to the UIButton inside the presented ViewController
addDismissAction(button: presentedViewController.dismissUIButton)
} else {
uiView.window?.rootViewController?.dismiss(animated: true, completion: nil)
}
}

Last step is to show SwiftUI view (in this case downloadingView) in our storyboard with the help of ContainerView object.

To insert a ContainerView go on “+” button on the top-right panel as in figure and find ContainerView.

How to embed UIView into a Hosting Controller

you have to “ctrl + drag” the UIView into a HostingController and select embed.

In our ContentView we show NonModalButtonView and the API request done in the previous Artifact project (you can find it below).

import SwiftUI
struct ContentView: View {
@State var countries = [Country]()
@State var isPresenting : Bool = false
@State var isDownloading: Bool = false

var body: some View {
NavigationView {
if (isDownloading == false) {
VStack {
Text("Tap to download!").font(.system(size: 30))
.fontWeight(.bold)
ZStack {
NonModalButton(isPresenting: $isPresenting, isDownloading: $isDownloading).frame(width: 150, height: 150, alignment: .center)
}
}.onAppear {
Task {
await getcountriesData()
}
}
.navigationTitle("Welcome")
} else {
List(countries, id:\\.self) {
country in

HStack(spacing: 10) {
Text(country.name).bold()

Spacer()

Text("\\(country.population)")
.bold()
}

}
.navigationTitle("Countries")
}
}

}
}

Watch out our first artifact

Resources

If you want to explore the whole project you can find it here.

Documentation

https://developer.apple.com/documentation/swiftui/uihostingcontroller

https://developer.apple.com/documentation/swiftui/uiviewrepresentable

https://developer.apple.com/documentation/uikit/view_controllers/creating_a_custom_container_view_controller

https://www.notion.so/appledeveloperacademyunina/Gli-Alfieri-Artifact-1-3403a074a5d6451e9ccc7c6318e814af

Article by Luca D’Angelo & Jacopo Santoro

--

--

luca d'angelo

Hello my name is Luca, I’m 25 y/o and I’m a software Engineer, tech addicted and Apple Developer Academy student in Naples (Italy)