SwiftUI meets UIKit
💻 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
context
parameter. 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 context
parameter. 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 SwiftUIclass 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
.
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.
How to embed UIView into a Hosting Controller
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
Article by Luca D’Angelo & Jacopo Santoro