How to Use UIKit/AppKit/WatchKit Components in SwiftUI: A QuickNote

Darshit Shah
4 min readAug 2, 2022

SwiftUI is a fantastic thing, but there are still some things that can be accomplished more easily with UIKit. If you ever find yourself having trouble with something in SwiftUI, don’t forget that you can always fall back on UIKit. Today, I’m going to show you some techniques for using UIKit in SwiftUI.

Apple gave you five protocols to use in order to wrap UIKit/AppKit/WatchKit into SwiftUI. By using these protocols, you’ll be able to more easily create SwiftUI code that can represent your UIKit/AppKit/WatchKit code.

UIView → UIViewRepresentable

NSView → NSViewRepresentable

WKInterfaceObject → WKInterfaceObjectRepresentable

UIViewController → UIViewControllerRepresentable

NSViewController → NSViewControllerRepresentable

Protocols that all have the same life cycle and methods. They’re all designed to give UIKit, AppKit, and WatchKit the reactive capability they need. By working together, these protocols can help create a more responsive and user-friendly experience for everyone involved.

Lets explore how UIKit applies to the rest of the post, but keep in mind that everything mentioned can be applied to all three types of products.

UIActivityIndicator — Simple View example

The simplest way to create a view is to set some state and render it. For example, we can use an UIActivityIndicator.

To do this, we need to create a representable SwiftUI view for UIActivityIndicator that conforms to UIViewRepresentable.

struct ActivityIndicator: UIViewRepresentable {

func makeUIView(context: Context) -> UIActivityIndicatorView {
let v = UIActivityIndicatorView()

return v
}

func updateUIView(_ activityIndicator: UIActivityIndicatorView, context: Context) {
activityIndicator.startAnimating()
}
}

The spinning indicator can be found by using below view.

struct ContentView : View {

var body: some View {
ActivityIndicator()
}
}

You can make startAnimating and stopAnimating work together by using a 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()
}
}
}

The parent view can inject the isAnimating state to control the animation, and func updateUIView will be called repeatedly with the latest configuration.

UIPageControl — Complex View example

Views that rely on old data binding methods like target and delegate can be complex to create. Coordinator is a tool that helps simplify the process by connecting UIView and SwiftUI. Coordinator listens for events from UIKit and then communicates that information back to SwiftUI. The best way to understand how it works 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, the UIPageControl’s .valueChanged event is used to update the currentPage property of the Coordinator. This keeps the two values in sync and allows the view to be updated accordingly.

UIPageViewController — Multiple Views Example

Since everything in SwiftUI is a view, there is no real difference between UIView and UIViewController. You can think of UIViewController as a type of view that is specifically designed to control other views. One key difference between the two, however, is that UIViewController can be used to manage multiple views at once, whereas UIView can only manage one view at a time.

Here, we will use UIPageViewController as an example. You’ll see that it’s not much different. The only notable change is that the coordinator now acts as both the delegate and datasource instead of just being a 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
}
}
}
}

Backward compatibility is something that a lot of developers look for when deciding whether or not to adopt a new technology. For me, it was a big selling point when it comes to SwiftUI. Knowing that I can always go back to the old framework if I need to is a reassuring thought.

Give your support

If you found this post useful, please share it with others.

Keep an eye out for the next interesting article. 😇
I hope you enjoyed it, and have a productive development day. 😀
If you have any new ideas or suggestions, please leave them in the comments section below.

Thank you for reading. 👍

--

--

Darshit Shah

Sr. Software Developer, Passionate for App UI/UX, Blogger