How to use UIKit in SwiftUI Using UIView and UIViewController in SwiftUI

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

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.

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:

makeCoordinator ( )

make [ UIView | UIViewController | NSView | NSViewController | WKInterfaceObject ] ( context : )

update [ UIView | UIViewController | NSView | NSViewController | WKInterfaceObject ] ( _ : context : )

update [ UIView | UIViewController | NSView | NSViewController | WKInterfaceObject ] ( _ : context : )

dismantle [ UIView | UIViewController | NSView | NSViewController | WKInterfaceObject ] ( _ : coordinator : )

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.

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

Tweet

Share

← Home