Is there a way to work with UIKit within the context of a SwiftUI ViewModifier?
I would like to drop to UIKit here:
struct MyViewModifier: ViewModifier {
func body(content: Content) -> some View {
// Work on `content` using UIKit here.
// E.g. I would like to add a subview or CALayer to the existing `content` view
}
}
If you want to add a UIView
to your SwiftUI view hierarchy, wrap it in a UIViewRepresentable
. You can learn more in the WWDC 2019 session Integrating SwiftUI starting at 8m08s.
If you want to add a CALayer
, you'll need to add it as a sublayer of a UIView
's layer, and host that UIView
using UIViewRepresentable
.
Based on your question title, “Access the underlying UIKit view from a SwiftUI View Modifier”, I wonder if you think each SwiftUI view has a corresponding UIView
. It doesn't. SwiftUI can use a single (private, custom) UIView
to draw many SwiftUI views. Let's take a look.
Here's a UIViewRepresentable
that wraps a custom UIView
subclass. The subclass does just one custom thing: it prints the UIKit view hierarchy when it's moved to a window.
struct UIKitPeeker: UIViewRepresentable {
func makeUIView(context: Context) -> MyView {
return MyView()
}
func updateUIView(_ view: MyView, context: Context) { }
class MyView: UIView {
var callback: (UIView) -> () = { _ in }
override func didMoveToWindow() {
super.didMoveToWindow()
// The recursiveDescription method is only for debugging.
// Don't use it in App Store submissions!
print(window?.value(forKey: "recursiveDescription") as? String ?? "nil")
}
}
}
I'll use it like this:
struct RootView: View {
var body: some View {
VStack {
Text("hello")
Text("world")
Color.pink.frame(width: 10, height: 10)
Toggle("toggle", isOn: .constant(false))
Text("lots of SwiftUI views here")
}
.background { UIKitPeeker() }
}
}
Here's the output (using Xcode 15.0 beta 2):
<UIWindow: 0x10570c230; frame = (0 0; 393 852); gestureRecognizers = <NSArray: 0x600000c20510>; layer = <UIWindowLayer: 0x600000c02310>>
| <UITransitionView: 0x105710b20; frame = (0 0; 393 852); autoresize = W+H; layer = <CALayer: 0x60000025b400>>
| | <UIDropShadowView: 0x1057115c0; frame = (0 0; 393 852); autoresize = W+H; layer = <CALayer: 0x60000025b8c0>>
| | | <_TtGC7SwiftUI14_UIHostingViewVS_7AnyView_: 0x10a9055f0; frame = (0 0; 393 852); autoresize = W+H; gestureRecognizers = <NSArray: 0x600000c10cf0>; backgroundColor = <UIDynamicSystemColor: 0x600001712900; name = systemBackgroundColor>; layer = <CALayer: 0x6000002128c0>>
| | | | <_TtGC7SwiftUI16PlatformViewHostGVS_P10$107f9077432PlatformViewRepresentableAdaptorV11iPhoneStudy11UIKitPeeker__: 0x10a90c550; baseClass = _UIConstraintBasedLayoutHostingView; frame = (0 374.333; 393 128.333); anchorPoint = (0, 0); tintColor = UIExtendedSRGBColorSpace 0 0.478431 1 1; layer = <CALayer: 0x60000024ada0>>
| | | | | <_TtCV11iPhoneStudy11UIKitPeeker6MyView: 0x10a90bf60; frame = (0 0; 393 128.333); autoresize = W+H; layer = <CALayer: 0x60000024ad40>>
We can see here that SwiftUI does all the drawing for the VStack
and its subviews with just two instances of UIView
subclasses (the ones whose names start with _TtGC7SwiftUI
). It doesn't even turn the Toggle
into a UISwitch
.