How to use UIKit in SwiftUI

Using UIView and UIViewController in SwiftUI

⋅ 6 min read ⋅ Swift iOS SwiftUI UIKit

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.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

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).

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.

Sponsor sarunw.com and reach thousands of iOS developers.

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 Share
Previous
Browse 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.

Next
SwiftUI changes in Xcode 11 Beta 3

Highlight changes for SwiftUI in beta 3

← Home