Inline wrapping of UIKit or AppKit views within SwiftUI
Discover page available: SwiftUIThe fact that any UIKit or AppKit view can be wrapped in order to become SwiftUI-compatible is incredibly useful, as that sort of provides an “escape hatch” for whenever SwiftUI does not yet natively support a given type of control or UI element.
However, if we need to rely on a lot of (for the lack of a better term) “legacy” views, having to constantly write wrappers for each of them can start to become a bit tedious — especially for simpler views that don’t require any sophisticated logic, or views that we only want to use in a single place.
For example, let’s say we wanted to use UIActivityIndicatorView
to display a loading spinner within a SwiftUI-based iOS app. In order to do that, we’d have to write a wrapper that’ll look something like this:
struct ActivityIndicator: UIViewRepresentable {
func makeUIView(context: Context) -> UIActivityIndicatorView {
UIActivityIndicatorView(style: .medium)
}
func updateUIView(_ view: UIActivityIndicatorView, context: Context) {
view.startAnimating()
}
}
While writing a wrapper that conforms to UIViewRepresentable
(or NSViewRepresentable
on the Mac) isn’t a huge task — wouldn’t it be nice if we instead could just wrap any legacy view inline, right where we need to use it?
Let’s make that happen by writing a generic type that can be used to wrap any UIView
. Let’s call it Wrap
, and have it take two closures, each corresponding to one of the method requirements of UIViewRepresentable
— like this:
struct Wrap<Wrapped: UIView>: UIViewRepresentable {
typealias Updater = (Wrapped, Context) -> Void
var makeView: () -> Wrapped
var update: (Wrapped, Context) -> Void
init(_ makeView: @escaping @autoclosure () -> Wrapped,
updater update: @escaping Updater) {
self.makeView = makeView
self.update = update
}
func makeUIView(context: Context) -> Wrapped {
makeView()
}
func updateUIView(_ view: Wrapped, context: Context) {
update(view, context)
}
}
To make an equivalent generic wrapper for macOS, simply replace all instances of “UI” with “NS”.
Note the usage of @autoclosure
above, which will enable us to keep following the conventions of UIViewRepresentable
and create our views lazily, without requiring any additional syntax at the call sites.
However, when updating our view, our new Wrap
type currently requires us to always handle both the view itself, and the current Context
. While having access to the Context
argument might be important for some use cases, let’s make it optional — by also introducing two convenience APIs that’ll let us either accept just our view as a single argument, or to opt out of updates entirely in case our view is completely static:
extension Wrap {
init(_ makeView: @escaping @autoclosure () -> Wrapped,
updater update: @escaping (Wrapped) -> Void) {
self.makeView = makeView
self.update = { view, _ in update(view) }
}
init(_ makeView: @escaping @autoclosure () -> Wrapped) {
self.makeView = makeView
self.update = { _, _ in }
}
}
With the above in place, we can now easily wrap any UIView
completely inline, while also being able to update it whenever our underlying state changes — like this:
struct ContentView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
ZStack {
...
Wrap(UIActivityIndicatorView()) {
if self.viewModel.isLoading {
$0.startAnimating()
} else {
$0.stopAnimating()
}
}
}
}
}
Very nice! This of course doesn’t mean that we should completely abandon building proper wrappers for certain views, but for simpler ones the above Wrap
type is incredibly convenient.