Expandable SwiftUI View in UIKit gets vertically centered - ios

I'm integrating some new SwiftUI Views in a UIKit application but I ran into a problem. I've looked at the following issue for quite a while but am yet to find a cause and solution. The problem specifically occurs when integrating the View in UIKit. I'm trying to create a simple tappable View that expands/collapses vertically when tapped.
This is how the preview of just the SwiftUI View looks like (and exactly how it should behave):
Screenshot
Video
And here is what I get when I implement the SwiftUI View in UIKit:
Screenshot
Video
It seems like even though I constrain the top op the UIHostingController view to the parent's view, the UIHostingController view gets vertically centered.
As mentioned in the comments, I could constrain the bottom of the HostController's view to the bottom of its parent, but that would make the content below uninteractable.
What I'm looking for is a solution where the HostController view constraints (specifically the height) matches the SwiftUI View frame.
The code for the SwiftUI View:
import SwiftUI
struct ColorView: View {
#State var isCollapsed = true
var body: some View {
VStack {
VStack(spacing: 5) {
HStack {
Spacer()
Text("Title")
Spacer()
}
.frame(height: 100)
if !isCollapsed {
HStack {
Spacer()
Text("description")
Spacer()
}
.padding(40)
}
}
.background(Color(isCollapsed ? UIColor.red : UIColor.blue))
.onTapGesture {
withAnimation {
self.isCollapsed.toggle()
}
}
Spacer()
}
}
}
struct ColorView_Previews: PreviewProvider {
static var previews: some View {
return ColorView()
}
}
And the UIKit implementation in the ViewController of the above-mentioned SwiftUI View:
struct ViewControllerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = ViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<ViewControllerRepresentable>) -> ViewControllerRepresentable.UIViewControllerType {
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewControllerRepresentable.UIViewControllerType, context: UIViewControllerRepresentableContext<ViewControllerRepresentable>) { }
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let colorView = ColorView()
let colorController = UIHostingController(rootView: colorView)
addChild(colorController)
view.addSubview(colorController.view)
colorController.didMove(toParent: self)
colorController.view.translatesAutoresizingMaskIntoConstraints = false
colorController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
colorController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
colorController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
}
}
struct ViewControllerRepresentable_Previews: PreviewProvider {
static var previews: some View {
Group {
ViewControllerRepresentable()
}
}
}
Any help is greatly appreciated.

Here is the snapshot of code that was tested as worked in iOS Single View Playground (Xcode 11.2). The original ColorView was not changed, so not duplicated below.
The behaviour of integrated SwiftUI view inside UIKit UIViewController is the same as in pure SwiftUI environment.
import UIKit
import PlaygroundSupport
import SwiftUI
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
let child = UIHostingController(rootView: ColorView())
addChild(child)
self.view.addSubview(child.view)
child.didMove(toParent: self)
child.view.translatesAutoresizingMaskIntoConstraints = false
child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
child.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Related

UITextView inputAccessoryView not behaving properly on safe area phones?

I am trying use UIKit's UITextView in an swiftUI project using UIViewRepresentable. Everything works great but I am facing some issue with inputAccessoryView on phones with safe area.
My inputAccessoryView is a swiftUI view wrapped in UIHostingController which lets me use swiftUI views as UIView.
I have attached my code and some images, To simplify my issue I am using simple swiftUI colorView as inputAccessoryView. As you can see the swiftUI view moves out of the frame of inputAccessoryView when it comes near bottom safe area. When you swipe down to close the keyboard it disappears weirdly. I have tried everything but could not find the solution. Can you please help me?
Thank You!
UIViewRepresentable, UITextView
struct UITextViewWrapper: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.text = text
textView.inputAccessoryView = makeKeyboardBar()
textView.keyboardDismissMode = .interactive
textView.alwaysBounceVertical = true
textView.delegate = context.coordinator
textView.becomeFirstResponder()
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
//
}
class Coordinator: NSObject, UITextViewDelegate {
let parent: UITextViewWrapper
init(_ parent: UITextViewWrapper) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeKeyboardBar() -> UIView {
let swiftUIView = ColorView()
let hostingController = UIHostingController(rootView: swiftUIView)
let uiView = hostingController.view!
uiView.frame = CGRect(origin: .zero, size: hostingController.sizeThatFits(in: uiView.frame.size))
uiView.layer.borderWidth = 1
uiView.layer.borderColor = UIColor.red.cgColor
return uiView
}
}
SwiftUI View to use as inputAccessoryView
struct ColorView: View {
var body: some View {
Color.yellow
.frame(height: 55)
}
}
ContentView
struct ContentView: View {
#State private var text = "Hello World"
var body: some View {
UITextViewWrapper(text: $text)
}
}
image
SwiftUI has own safe area tracking so you have to manage safe area behaviour explicitly inside SwiftUI view, like
var body: some View {
Color.yellow.edgesIgnoringSafeArea(.bottom) // << here !!
.frame(height: 55)
}

Interacting with a SwiftUI view below a UIKit PKCanvasView

I have a view Structure as follows:
ZStack {
UIKitView //PKCanvasView
.zIndex(1)
SwiftUIView //SwiftUI view which has a gesture recogniser
.zIndex(0)
}
I wanted to know what the best way was to allow simultaneous interaction to the SwiftUI view below the UIKit view.
This would work best if the SwiftUI view had a higher priority gesture over the UIKit view on top.
My UIKit representable view is as follows:
struct CanvasView: UIViewRepresentable {
#Binding var canvasView: PKCanvasView
#Binding var isFirstResponder: Bool
#State var toolPicker = PKToolPicker()
func makeUIView(context: Context) -> PKCanvasView {
canvasView.drawingPolicy = .pencilOnly
canvasView.backgroundColor = .clear
canvasView.isOpaque = false
canvasView.alwaysBounceVertical = true
canvasView.alwaysBounceHorizontal = true
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
//canvasView.becomeFirstResponder()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
if isFirstResponder != uiView.isFirstResponder {
if isFirstResponder {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
}
}
Ideally I would like to handle the layering of views in SwiftUI as in reality a have multiple SwiftUI views below the canvas view which when held & dragged (that is the compound gesture I am applying to them) should appear above it. (This is achieved using dynamic zIndexs). I'm not comfortable enough with UIKit to know how to do this there with subviews and a UIViewRepresentable structure.
Any help would be greatly appreciated!

Center SwiftUI view in top-level view

I am creating a loading indicator in SwiftUI that should always be centered in the top-level view of the view hierarchy (i.e centered in the whole screen in a fullscreen app). This would be easy in UIKit, but SwiftUI centres views relative to their parent view only and I am not able to get the positions of the parent views of the parent view.
Sadly my app is not fully SwiftUI based, so I cannot easily set properties on my root views that I could then access in my loading view - I need this view to be centered regardless of what the view hierarchy looks like (mixed UIKit - SwiftUI parent views). This is why answers like SwiftUI set position to centre of different view don't work for my use case, since in that example, you need to modify the view in which you want to centre your child view.
I have tried playing around with the .offset and .position functions of View, however, I couldn't get the correct inputs to always dynamically centre my loadingView regardless of screen size or regardless of what part of the whole screen rootView takes up.
Please find a minimal reproducible example of the problem below:
/// Loading view that should always be centered in the whole screen on the XY axis and should be the top view in the Z axis
struct CenteredLoadingView<RootView: View>: View {
private let rootView: RootView
init(rootView: RootView) {
self.rootView = rootView
}
var body: some View {
ZStack {
rootView
loadingView
}
// Ensure that `AnimatedLoadingView` is displayed above all other views, whose `zIndex` would be higher than `rootView`'s by default
.zIndex(.infinity)
}
private var loadingView: some View {
VStack {
Color.white
.frame(width: 48, height: 72)
Text("Loading")
.foregroundColor(.white)
}
.frame(width: 142, height: 142)
.background(Color.primary.opacity(0.7))
.cornerRadius(10)
}
}
View above which the loading view should be displayed:
struct CenterView: View {
var body: some View {
return VStack {
Color.gray
HStack {
CenteredLoadingView(rootView: list)
otherList
}
}
}
var list: some View {
List {
ForEach(1..<6) {
Text($0.description)
}
}
}
var otherList: some View {
List {
ForEach(6..<11) {
Text($0.description)
}
}
}
}
This is what the result looks like:
This is how the UI should look like:
I have tried modifying the body of CenteredLoadingView using a GeometryReader and .frame(in: .global) to get the global screen size, but what I've achieved is that now my loadingView is not visible at all.
var body: some View {
GeometryReader<AnyView> { geo in
let screen = geo.frame(in: .global)
let stack = ZStack {
self.rootView
self.loadingView
.position(x: screen.midX, y: screen.midY)
// Offset doesn't work either
//.offset(x: -screen.origin.x, y: -screen.origin.y)
}
// Ensure that `AnimatedLoadingView` is displayed above all other views, whose `zIndex` would be higher than `rootView`'s by default
.zIndex(.infinity)
return AnyView(stack)
}
}
Here is a demo of possible approach. The idea is to use injected UIView to access UIWindow and then show loading view as a top view of window's root viewcontroller view.
Tested with Xcode 12 / iOS 14 (but SwiftUI 1.0 compatible)
Note: animations, effects, etc. are possible but are out scope for simplicity
struct CenteredLoadingView<RootView: View>: View {
private let rootView: RootView
#Binding var isActive: Bool
init(rootView: RootView, isActive: Binding<Bool>) {
self.rootView = rootView
self._isActive = isActive
}
var body: some View {
rootView
.background(Activator(showLoading: $isActive))
}
struct Activator: UIViewRepresentable {
#Binding var showLoading: Bool
#State private var myWindow: UIWindow? = nil
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async {
self.myWindow = view.window
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let holder = myWindow?.rootViewController?.view else { return }
if showLoading && context.coordinator.controller == nil {
context.coordinator.controller = UIHostingController(rootView: loadingView)
let view = context.coordinator.controller!.view
view?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
view?.translatesAutoresizingMaskIntoConstraints = false
holder.addSubview(view!)
holder.isUserInteractionEnabled = false
view?.leadingAnchor.constraint(equalTo: holder.leadingAnchor).isActive = true
view?.trailingAnchor.constraint(equalTo: holder.trailingAnchor).isActive = true
view?.topAnchor.constraint(equalTo: holder.topAnchor).isActive = true
view?.bottomAnchor.constraint(equalTo: holder.bottomAnchor).isActive = true
} else if !showLoading {
context.coordinator.controller?.view.removeFromSuperview()
context.coordinator.controller = nil
holder.isUserInteractionEnabled = true
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator {
var controller: UIViewController? = nil
}
private var loadingView: some View {
VStack {
Color.white
.frame(width: 48, height: 72)
Text("Loading")
.foregroundColor(.white)
}
.frame(width: 142, height: 142)
.background(Color.primary.opacity(0.7))
.cornerRadius(10)
}
}
}
struct CenterView: View {
#State private var isLoading = false
var body: some View {
return VStack {
Color.gray
HStack {
CenteredLoadingView(rootView: list, isActive: $isLoading)
otherList
}
Button("Demo", action: load)
}
.onAppear(perform: load)
}
func load() {
self.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.isLoading = false
}
}
var list: some View {
List {
ForEach(1..<6) {
Text($0.description)
}
}
}
var otherList: some View {
List {
ForEach(6..<11) {
Text($0.description)
}
}
}
}

Remove back button text from navigationbar in SwiftUI

I've recently started working in SwiftUI, came to the conclusion that working with navigation isn't really great yet. What I'm trying to achieve is the following. I finally managed to get rid of the translucent background without making the application crash, but now I ran into the next issue. How can I get rid of the "back" text inside the navbaritem?
I achieved the view above by setting the default appearance in the SceneDelegate.swift file like this.
let newNavAppearance = UINavigationBarAppearance()
newNavAppearance.configureWithTransparentBackground()
newNavAppearance.setBackIndicatorImage(UIImage(named: "backButton"), transitionMaskImage: UIImage(named: "backButton"))
newNavAppearance.titleTextAttributes = [
.font: UIFont(name: GTWalsheim.bold.name, size: 18)!,
.backgroundColor: UIColor.white
]
UINavigationBar.appearance().standardAppearance = newNavAppearance
One possible way that I could achieve this is by overriding the navigation bar items, however this has one downside (SwiftUI Custom Back Button Text for NavigationView) as the creator of this issue already said, the back gesture stops working after you override the navigation bar items. With that I'm also wondering how I could set the foregroundColor of the back button. It now has the default blue color, however I'd like to overwrite this with another color.
Piggy-backing on the solution #Pitchbloas offered, this method just involves setting the backButtonDisplayMode property to .minimal:
extension UINavigationController {
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}
It's actually really easy. The following solution is the fastest and cleanest i made.
Put this at the bottom of your SceneDelegate for example.
extension UINavigationController {
// Remove back button text
open override func viewWillLayoutSubviews() {
navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
This will remove the back button text from every NavigationView (UINavigationController) in your app.
I have found a straightforward approach to remove the back button text using SwiftUI only, and keeping the original chevron.
A drag gesture is added to mimic the classic navigation back button
when user wants to go back by swiping right. Following this, an extension of View is created to create a SwiftUI like modifier.
This is how to use it in code:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
// Your main view code here with a ZStack to have the
// gesture on all the view.
}
.navigationBarBackButtonTitleHidden()
}
}
This is how to create the navigationBarBackButtonTitleHidden() modifier:
import SwiftUI
extension View {
func navigationBarBackButtonTitleHidden() -> some View {
self.modifier(NavigationBarBackButtonTitleHiddenModifier())
}
}
struct NavigationBarBackButtonTitleHiddenModifier: ViewModifier {
#Environment(\.dismiss) var dismiss
#ViewBuilder #MainActor func body(content: Content) -> some View {
content
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: Button(action: { dismiss() }) {
Image(systemName: "chevron.left")
.foregroundColor(.blue)
.imageScale(.large) })
.contentShape(Rectangle()) // Start of the gesture to dismiss the navigation
.gesture(
DragGesture(coordinateSpace: .local)
.onEnded { value in
if value.translation.width > .zero
&& value.translation.height > -30
&& value.translation.height < 30 {
dismiss()
}
}
)
}
}
Standard Back button title is taken from navigation bar title of previous screen.
It is possible the following approach to get needed effect:
struct TestBackButtonTitle: View {
#State private var hasTitle = true
var body: some View {
NavigationView {
NavigationLink("Go", destination:
Text("Details")
.onAppear {
self.hasTitle = false
}
.onDisappear {
self.hasTitle = true
}
)
.navigationBarTitle(self.hasTitle ? "Master" : "")
}
}
}
So I actually ended up with the following solution that actually works. I am overwriting the navigation bar items like so
.navigationBarItems(leading:
Image("backButton")
.foregroundColor(.blue)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
)
The only issue with this was that the back gesture wasn't working so that was solved by actually extending the UINavigationController
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
Now it's looking exactly the way I want it, the solution is kinda hacky... but it works for now, hopefully SwiftUI will mature a little bit so this can be done easier.
Using the Introspect framework, you can easily gain access to the underlying navigation item and set the backButtonDisplayMode to minimal.
Here’s how you might use that in the view that was pushed
var body: some View {
Text("Your body here")
.introspectNavigationController { navController in
navController.navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}
If you want to:
Do it globally
Keep the standard back button (along with custom behaviours like an ability to navigate a few screens back on a long press)
Avoid introducing any third party frameworks
You can do it by setting the back button text color to Clear Color via appearance:
let navigationBarAppearance = UINavigationBarAppearance()
let backButtonAppearance = UIBarButtonItemAppearance(style: .plain)
backButtonAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.highlighted.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear]
navigationBarAppearance.backButtonAppearance = backButtonAppearance
//Not sure you'll need both of these, but feel free to adjust to your needs.
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
You can do it once when the app starts and forget about it.
A potential downside (depending on your preferences) is that the transition to the clear color is animated as the title of the current window slides to the left as you move to a different one.
You can also experiment with different text attributes.
Works on iOS 16
Solutions above didn't work for me. I wanted to make changes specific to view without any global (appearance or extension) and with minimal boilerplate code.
Since you can update NavigationItem inside the init of the View. You can solve this in 2 steps:
Get visible View Controller.
// Get Visible ViewController
extension UIApplication {
static var visibleVC: UIViewController? {
var currentVC = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController
while let presentedVC = currentVC?.presentedViewController {
if let navVC = (presentedVC as? UINavigationController)?.viewControllers.last {
currentVC = navVC
} else if let tabVC = (presentedVC as? UITabBarController)?.selectedViewController {
currentVC = tabVC
} else {
currentVC = presentedVC
}
}
return currentVC
}
}
Update NavigationItem inside init of the View.
struct YourView: View {
init(hideBackLabel: Bool = true) {
if hideBackLabel {
// iOS 14+
UIApplication.visibleVC?.navigationItem.backButtonDisplayMode = .minimal
// iOS 13-
let button = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
UIApplication.visibleVC?.navigationItem.backBarButtonItem = button
}
}
}
custom navigationBarItems and self.presentationMode.wrappedValue.dismiss() worked but you are not allow to perform swiping back
You can either add the following code to make the swipe back again
//perform gesture go back
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
but the problem is, sometimes it will make your app crashed when you swipe half the screen and then cancel.
I would suggest the other way to remove the "Back" text.
Adding the isActive state to monitor whether the current screen is active or not. :)
struct ContentView: View {
#State var isActive = false
var body: some View {
NavigationView() {
NavigationLink(
"Next",
destination: Text("Second Page").navigationBarTitle("Second"),
isActive: $isActive
)
.navigationBarTitle(!isActive ? "Title" : "", displayMode: .inline)
}
}
}
I am accomplishing this by changing the title of the master screen before pushing the detail screen and then setting it back when it re-appears. The only caveat is when you go back to the master screen the title's re-appearance is a little noticeable.
Summary:
on master view add state var (e.g. isDetailShowing) to store if detail screen is showing or not
on master view use the navigationTitle modifier to set the title based on the current value of isDetailShowing
on master view use onAppear modifier to set the value of isDetailShowing to false
on the NavigationLink in master screen use the simultaneousGesture modifier to set the isDetailShowing to true
struct MasterView: View {
#State var isDetailShowing = false
var body: some View {
VStack {
Spacer()
.frame(height: 20)
Text("Master Screen")
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
.frame(height: 20)
NavigationLink(destination: DetailView()) {
Text("Go to detail screen")
}
.simultaneousGesture(TapGesture().onEnded() {
isDetailShowing = true
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(isDetailShowing ? "" : "Master Screen Title")
.onAppear() {
isDetailShowing = false
}
}
}
struct DetailView: View {
var body: some View {
Text("This is the detail screen")
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Detail Screen Title")
}
}
you can use .toolbarRole(.editor)
Why not use Custom BackButton with Default Back Button Hidden
You could use Any Design You Prefer !
Works on iOS 16
First View
struct ContentView: View {
var body: some View {
NavigationView {
VStack(){
Spacer()
NavigationLink(destination: View2()) {
Text("Navigate")
.font(.title)
}
Spacer()
}
}
}
}
Second View
struct View2: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack{
VStack{
HStack(alignment:.center){
//Any Design You Like
Image(systemName: "chevron.left")
.font(.title)
.foregroundColor(.blue)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
.padding()
Spacer()
}
Spacer()
}
}
}
.navigationBarBackButtonHidden(true)
}
}

SwiftUI passing touch events from UIViewControllerRepresentable to View behind

I'm working on a project which uses a mixture of UIKit and SwiftUI. I currently have a ZStack which I hold my SwiftUI content in, I need to display a UIViewController over top of that content. So the last item in my ZStack is a UIViewControllerRepresentable. ie:
ZStack {
...
SwiftUIContent
...
MyUIViewControllerRepresentative()
}
My overlaid UIViewControllerRepresentative is a container for other UIViewControllers. Those child view controllers don't always need to take up the full screen, so the UIViewControllerRepresentative has a transparent background so the user can interact with the SwiftUI content behind.
The problem I'm facing is that the UIViewControllerRepresentative blocks all touch events from reaching the SwiftUI content.
I've tried overriding the UIViewControllers views hit test like so:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// return nil if the touch event should propagate to the SwiftUI content
}
I've also event tried completely removing the touch events on the view controllers view with:
view.isUserInteractionEnabled = false
Even that doesn't work.
Any help would be really appreciated.
SOLUTION:
I managed to come up with a solution that works.
Instead of putting the UIViewControllerRepresentable inside the ZStack, I created a custom ViewModifier which passes the content value into the UIViewControllerRepresentable. That content is then embedded within my UIViewController by wrapping it inside of a UIHostingController and adding it as a child view controller.
ZStack {
... SwiftUI content ...
}.modifier(MyUIViewControllerRepresentative)
You could use allowsHitTesting() modifier on the UIViewControllerRepresentable.
As Paul Hudson states on his website: https://www.hackingwithswift.com/quick-start/swiftui/how-to-disable-taps-for-a-view-using-allowshittesting
"If hit testing is disallowed for a view, any taps automatically continue through the view on to whatever is behind."
ZStack {
...
SwiftUIContent
...
MyUIViewControllerRepresentative()
.allowsHitTesting(false)
}
The solution I've found was to pass my SwiftUI view down to the overlaid UIViewControllerRepresentable.
What makes it possible is to make all entities involved generic and using #ViewBuilder in your representable.
It's possible to make this even cleaner by using a View Modifier instead of wrapping your content in the representable.
ContentView
struct ContentView: View {
var body: some View {
ViewControllerRepresentable {
Text("SwiftUI Content")
}
}
}
UIViewControllerRepresentable
struct ViewControllerRepresentable<Content: View>: UIViewControllerRepresentable {
#ViewBuilder let content: Content
typealias UIViewControllerType = ViewController<Content>
func makeUIViewController(context: Context) -> BottomSheetViewController<Content> {
ViewController<Content>(content: UIHostingController(rootView: content))
}
func updateUIViewController(_ uiViewController: ViewController<Content>, context: Context) {
}
}
UIViewController
import SwiftUI
import UIKit
final class ViewController<Content: View>: UIViewController {
private let content: UIHostingController<Content>
init(content: UIHostingController<Content>) {
self.content = content
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
return nil
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
setupLayout()
}
private func setupLayout() {
addChild(content)
view.addSubview(content.view)
content.didMove(toParent: self)
// Here you can add UIKit views above your SwiftUI content while keeping it interactive
NSLayoutConstraint.activate([
content.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
content.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
content.view.topAnchor.constraint(equalTo: view.topAnchor),
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}

Resources