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!
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 classUINavigationController
and overrodeviewDidLoad
. In that function, I stored a weak reference toself
in an external list ofUINavigationController
, 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 multipleNavigationLink
to be defined on any screen that displays a SideMenu.
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
I created a gist with the SwiftUI component that encapsulates SideMenu
: https://gist.github.com/noe/d177bb9758f047ffc11b151d36bf5b53
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
ofSideMenuNavigationController
to get menu event notifications got me no notifications. I guess the only option is to implement theSideMenuNavigationControllerDelegate
functions directly in your view controller, but this was not an option in my case because I was usingUIHostingController
.
@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:
- 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. - SideMenu should allow for SwiftUI to be used within it.
- 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:
- 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().
- 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.
I totally agree that my implementation is fragile and tailored to my use case. I'm looking forward to your progress @jonkykong !
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))
}
}
- 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.