How to Use UIKit/AppKit/WatchKit Components in SwiftUI: A QuickNote
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.
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. 👍