How to use UIKit in SwiftUI
Using UIView and UIViewController in SwiftUI
Table of Contents
The good thing about SwiftUI
is whenever you hit a roadblock; you can always come back to good old UIKit
(and AppKit/WatchKit
). Today I'm going to show you some examples of how to do it.
Representable Protocol
Apple provided you with five representable protocol to wrap UIKit/AppKit/WatchKit
into SwiftUI
.
UIKit/AppKit/WatchKit | Protocol |
---|---|
UIView | UIViewRepresentable |
NSView | NSViewRepresentable |
WKInterfaceObject | WKInterfaceObjectRepresentable |
UIViewController | UIViewControllerRepresentable |
NSViewController | NSViewControllerRepresentable |
These protocols have a very same life cycle and methods, with a purpose to bring the reactive capability to UIKit/AppKit/WatchKit
(I will use just UIKit
in the rest of the post, but everything applies to all three).
You can easily support sarunw.com by checking out this sponsor.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
Simple Views
Let's start with the simplest form of view, where we only set some state and render. I will use an UIActivityIndicator
as an example.
First, we need to create a representable SwiftUI view for UIActivityIndicator
that conform to UIViewRepresentable
.
struct ActivityIndicator: UIViewRepresentable {
func makeUIView(context: Context) -> UIActivityIndicatorView {
let v = UIActivityIndicatorView()
return v
}
func updateUIView(_ activityIndicator: UIActivityIndicatorView, context: Context) {
activityIndicator.startAnimating()
}
}
Use this view, and you will see the spinning indicator.
struct ContentView : View {
var body: some View {
ActivityIndicator()
}
}
You can expose startAnimating
and stopAnimating
in the form of binding value.
struct ActivityIndicator: UIViewRepresentable {
@Binding var isAnimating: Bool
func makeUIView(context: Context) -> UIActivityIndicatorView {
let v = UIActivityIndicatorView()
return v
}
func updateUIView(_ activityIndicator: UIActivityIndicatorView, context: Context) {
if isAnimating {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
}
}
}
This will let the parent view inject isAnimating
state and control the animation, func updateUIView
will be getting called repeatedly with the latest configuration.
Advanced Views
Not every view is created simple; if your view relies on old ways of data binding like target/action and delegate, you need Coordinator
. Coordinator
is a bridge between UIView
and SwiftUI
, its main job is to listen to UIKit
whether as target
or delegate
and communicate that back to SwiftUI
. The best way to understand is to see it in action.
We will use UIPageControl
as an example here.
struct PageControl: UIViewRepresentable {
var numberOfPages: Int
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPageControl {
let pageControl = UIPageControl()
pageControl.numberOfPages = numberOfPages
pageControl.addTarget(
context.coordinator,
action: #selector(Coordinator.updateCurrentPage(sender:)),
for: .valueChanged)
return pageControl
}
func updateUIView(_ pageControl: UIPageControl, context: Context) {
pageControl.currentPage = currentPage
}
class Coordinator: NSObject {
var pageControl: PageControl
init(_ pageControl: PageControl) {
self.pageControl = pageControl
}
@objc func updateCurrentPage(sender: UIPageControl) {
pageControl.currentPage = sender.currentPage
}
}
}
In this example Coordinator
act as a target for UIPageControl
's .valueChanged
event and keep currentPage
in sync with the view.
UIViewController
It is no difference between UIView
and UIViewController
since everything is a view in SwiftUI
.
We use UIPageViewController
as an example here. You will see it is not much different. The only change is Coordinator
act as delegate
and dataSuorce
instead of target
.
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController)
{
parent.currentPage = index
}
}
}
}
Life cycle
In the lifetime of a representable view, SwiftUI initializes a view/view controller and a coordinator, updates them (one or more time), and deinitializes them.
For each phase, SwiftUI calls the following methods in order:
Initialization phase:
makeCoordinator()
make[UIView|UIViewController|NSView|NSViewController|WKInterfaceObject](context:)
update[UIView|UIViewController|NSView|NSViewController|WKInterfaceObject](_:context:)
Update phase:
update[UIView|UIViewController|NSView|NSViewController|WKInterfaceObject](_:context:)
Deinitialization phase:
dismantle[UIView|UIViewController|NSView|NSViewController|WKInterfaceObject](_:coordinator:)
You can easily support sarunw.com by checking out this sponsor.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
Conclusion
This backward compatibility gives me peace of mind about adopting SwiftUI in my next project (which would take another year since its only compatible with iOS13). Knowing that I can always come back and rely on the old framework is a big win for me.
SwiftUI caught my attention the most for this year in WWDC. I still have some concerns and questions about this new declarative UI, which I will cover in upcoming weeks. Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading, and see you next time.
Read more article about Swift, iOS, SwiftUI, UIKit, or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet ShareBrowse SF Symbols on Mac
SF Symbols contain over 1,500 icons. It would be hard if you don't know the existence of SF Symbols App.