jonkykong/SideMenu

SwiftUI Support

jonkykong opened this issue · 10 comments

With SwiftUI coming, it would be best if SideMenu supported it more readily without relying on UIKit. As of this writing, there are still some issues with presentation transitions as well as a lack of adoption in the community, but as that adoption changes it will become increasingly necessary to adapt SideMenu with it.

Voice your support on this feature request to help build interest in this feature. Additionally, if you are interested in helping create this next version, we can use the help!

noe commented

I managed to integrate SideMenu with SwiftUI, but it was not easy, and it is not problem-free. Some notes, for anyone wanting to do the same:

  • The menu view controller can be easily implemented by means of UIHostingController.
  • A big problem is how to get the UINavigationController to set the pan gesture. This controller is not available from SwiftUI (there are some StackOverflow questions asking this). To get the navigation controller, I created an extension of class UINavigationController and overrode viewDidLoad. In that function, I stored a weak reference to self in an external list of UINavigationController, which later I use to set the pan gesture. To distinguish the "good one", I choose the one whose debug description contains the string "UINavigationController"; I am sure there are better ways, btw.
  • Another problem is that after dismissing the menu the first time, it never responded to tap gestures (i.e. clicking on the menu items) ever again. As a workaround, I re-generated the view controller every time the menu is hidden.

The reason why I replaced my original custom SwiftUI-based implementation with SideMenu is that SwiftUI presents problems with gestures and scroll views and, as a result, sometimes the pan gesture was initiated (i.e. DragGesture.onChanged was called) but never finished (i.e. onEnded was never called), leaving the menu half-open.

Thanks @noe. If you're comfortable sharing any of your implementation details here, please do.

I have also done some experimentation getting SideMenu to work in SwiftUI. These are some of the challenges I've noted:

  • Custom transitions (which SideMenu and UIKit use) do not seem to be supported from within SwiftUI; they do work when switching to UIKit and using UIKit to manage presentation.
  • In a pure SwiftUI implementation, a side menu would need to be defined at or near the root view hierarchy to function as it does currently. This is something I would prefer to avoid as it complicates implementation for novice developers. The native .sheet(...) call to present a view can be done from anywhere within the view hierarchy and appear from the bottom of the screen.
  • Passing back NavigationLink actions to the view that displayed the SideMenu in an intuitive way that does not require multiple NavigationLink to be defined on any screen that displays a SideMenu.
noe commented

About this:

In a pure SwiftUI implementation, a side menu would need to be defined at or near the root view hierarchy to function as it does currently. This is something I would prefer to avoid as it complicates implementation for novice developers. The native .sheet(...) call to present a view can be done from anywhere within the view hierarchy and appear from the bottom of the screen.

The menu content can be placed in an ObservableObject that is then placed in the environment. Any view can then change it dynamically by retrieving the object from the environment and setting the new contents

noe commented

I created a gist with the SwiftUI component that encapsulates SideMenu: https://gist.github.com/noe/d177bb9758f047ffc11b151d36bf5b53

noe commented

Another problem that I found and forgot to mention:

  • The navigation bar does not play well with the side menu when using SideMenuPresentationStyle.menuSlideIn: the menu content is drawn just below the navigation bar, but the navigation bar is also occluded by what seems to be the menu itself. The separation line between the navigation bar and the rest of the screen is drawn over the menu, though. Here you can see a screenshot of the upper part of the screen where the menu is half-open. I added a red border to the SwiftUI menu to highlight what the drawable area is. You can see how the navigation bar ("My app") is occluded by the menu as it is open, but I can't draw on that part.
    .

  • Setting property sideMenuDelegate of SideMenuNavigationController to get menu event notifications got me no notifications. I guess the only option is to implement the SideMenuNavigationControllerDelegate functions directly in your view controller, but this was not an option in my case because I was using UIHostingController.

@noe this is wonderful, thank you for sharing. I'm still getting my bearings with SwiftUI, so there are going to be some things I haven't yet learned that will be helpful here -- perhaps we can collaborate.

My approach with SideMenu is for the most painless integration possible. With this in mind, I've tried to define what I think is the most sensible interface and then seeing if I could make that work through code without introducing ugly hacks. My current approach is this:

  1. SideMenu should be displayable similar to how other screens are displayable (in UIKit it works with present/dismiss). Currently, .sheet(...) is the supported way to present a new screen (from anywhere in the view hierarchy) and so I am looking to follow this pattern.
  2. SideMenu should allow for SwiftUI to be used within it.
  3. SideMenu should deliver the existing set of functionality as much as possible, abstracting away behaviors wherever they can be so that the developer doesn't have to write code. Some notable challenges so far include passing back navigation actions to the main screen and gesture support.

Generally, I like your approach, however I do have some concerns:

  1. It looks like you have to define SideMenu as your root view object, and you wrap both your main content and menu content underneath it. Per goal 1 above, I'll consider this implementation only if I can't find a clean way to implement its interface similar to .sheet().
  2. View hierarchy inspections/traversals, e.g.:
    fileprivate func connect(controllerFactory: IControllerFactory) {
        self.rootController = (UIApplication.shared.windows.last?.rootViewController)!
        self.controllerFactory = controllerFactory
        replaceController()
    }

This may breakdown when multiple windows are used or if the developer switches to UIKit in other places.

extension UINavigationController {
    open override func viewDidLoad() {
        if self.debugDescription.contains("UINavigationController") {
            SideDrawerControl.registerNavigation(navigation: self)
        }
    }
}

This may not work when viewDidLoad is overridden or subclasses are used (unconfirmed).

I'm working on a beta now that I'll make available for your feedback soon; stay tuned.

noe commented

I totally agree that my implementation is fragile and tailored to my use case. I'm looking forward to your progress @jonkykong !

noe commented

For the .sheet way, I suggest you implement an extension to View together with a view modifier, much like they do in this popup library.

We're on the same page:

@available(iOS 13.0, *)
extension View {
    public func bottomSheet<Content: View>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View {
        modifier(SideMenuModifier(isPresented: isPresented, content: content))
    }
}
noe commented
  • The navigation bar does not play well with the side menu when using SideMenuPresentationStyle.menuSlideIn: the menu content is drawn just below the navigation bar, but the navigation bar is also occluded by what seems to be the menu itself. The separation line between the navigation bar and the rest of the screen is drawn over the menu, though. Here you can see a screenshot of the upper part of the screen where the menu is half-open. I added a red border to the SwiftUI menu to highlight what the drawable area is. You can see how the navigation bar ("My app") is occluded by the menu as it is open, but I can't draw on that part.
    .

About the issue that I was having regarding the navigation bar not playing well with the side menu when using SideMenuPresentationStyle.menuSlideIn, what was happening is that the menu UIHostController was painting a navigation bar header. It went away by having a .navigationBarHidden(true) in the menu contents view.