Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a SwiftUI Sidebar

I want to build a very simple iOS 14 sidebar using SwiftUI. The setup is quite simple, I have three views HomeView, LibraryView and SettingsView and an enum representing each screen.

enum Screen: Hashable {
   case home, library, settings
}

My end-goal is to automatically switch between a tab view and a sidebar depending on the size class but some things don't quite work as expected.

The global state is owned by the MainNavigationView, which is also the root view for my WindowGroup.

struct MainNavigationView: View {
    @State var screen: Screen? = .home
   
    var body: some View {
        NavigationView {
            SidebarView(state: $screen)
        }
        .navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}

The SidebarView is a simple List containing three NavigationLink, one for each Screen.

struct SidebarView: View {
    @Binding var state: Screen?
    var body: some View {
        List {
            NavigationLink(
                destination: HomeView(),
                tag: Screen.home,
                selection: $state,
                label: {
                    Label("Home", systemImage: "house" )
                })
            NavigationLink(
                destination: LibraryView(),
                tag: Screen.library,
                selection: $state,
                label: {
                    Label("Library", systemImage: "book")
                })
            NavigationLink(
                destination: SettingsView(),
                tag: Screen.settings,
                selection: $state,
                label: {
                    Label("Settings", systemImage: "gearshape")
                })
        }
        .listStyle(SidebarListStyle())
        .navigationTitle("Sidebar")
    
    }
}

I use the NavigationLink(destination:tag:selection:label) initializer so that the selected screen is set in my MainNavigationView so I can reuse that for my TabView later.

However, a lot of things don't quite work as expected.

First, when launching the app in a portrait-mode iPad (I used the iPad Pro 11-inch simulator), no screen is selected when launching the app. Only after I click Back in the navigation bar, the initial screen shows and my home view gets shown.

First bug: HomeView is only shown after the Back button was tapped

The second weird thing is that the binding seems to be set to nil whenever the sidebar gets hidden. In landscape mode the view works as expected, however when toggling the sidebar to hide and then shown again, the selection gets lost. The content view stays correct, but the sidebar selection is lost.

Toggling the sidebar resets the state

Are these just SwiftUI bugs or is there a different way to create a sidebar with a Binding?

like image 246
jlsiewert Avatar asked Jul 06 '20 17:07

jlsiewert


People also ask

How to create a sidebar menu in Swift?

In Swift, you just need to specify the method name as a string literal to construct a selector. Lastly, we add a gesture recognizer. Not only you can use the menu button to bring out the sidebar menu, the user can swipe the content area to activate the sidebar as well. Cool! Let’s compile and run the app in the simulator.

How to create sidebar menu in WordPress?

Normally, the navigation menu is hidden behind the front view. The menu can then be triggered by tapping a list button in the navigation bar. Once the menu is expanded and becomes visible, users can close it by using the list button or simply swiping left on the content area. You can build the sidebar menu from the ground up.

How to activate the sidebar menu in iOS simulator?

Not only you can use the menu button to bring out the sidebar menu, the user can swipe the content area to activate the sidebar as well. Cool! Let’s compile and run the app in the simulator. Tap the menu button and the sidebar menu should appear. You can hide the sidebar menu by tapping the menu button again.

How to create a sidebar menu using swrevealviewcontroller?

To use SWRevealViewController for building a sidebar menu, you create a container view controller, which is actually an empty view controller, to hold both the menu view controller and a set of content view controllers. I have already created the menu view controller for you. It is just a static table view with three menu items.


1 Answers

You need to include a default secondary view within the NavigationView { }, usually it would be a placeholder but you could use the HomeScreen, e.g.

struct MainNavigationView: View {
    @State var screen: Screen? = .home
   
    var body: some View {
        NavigationView {
            SidebarView(state: $screen)
            HomeScreen()
        }
        .navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}

Regarding the cell not re-selecting - as of iOS 14.2 there is no list selection binding (when not in editing mode) so selection is lost. Although the List API has a $selection param, it is only supported on macOS at the moment. You can see that info the header:

/// On iOS and tvOS, you must explicitly put the list into edit mode for
/// the selection to apply.

It's a bit convoluted but it means that selection binding that we need for a sidebar is only for macOS, on iOS it is only for multi-select (i.e. checkmarks) in edit mode. The reason could be since UITableView's selection is event driven, maybe it wasn't possible to translate into SwiftUI's state driven nature. If you've ever tried to do state restoration with a view already pushed on a nav controller and try to show the cell unhighlight animation when popping back and that table view wasn't loaded and cell was never highlighted in the first place you'll know what I mean. It was a nightmare to load the table synchronously, make the selected cell be drawn and then start the unhighlight animation. I expect that Apple will be reimplementing List, Sidebar and NavigationView in pure SwiftUI to overcome these issues so for now we just have to live with it.

Once this has been fixed it will be as simple as List(selection:$screen) { } like how it would work on macOS. As a workaround on iOS you could highlight the icon or text in your own way instead, e.g. try using bold text:

    NavigationLink(
        destination: HomeView(),
        tag: Screen.home,
        selection: $state,
        label: {
            Label("Home", systemImage: "house" )
        })
        .font(Font.headline.weight(state == Screen.home ? .bold : .regular))

enter image description here

It doesn't look very nice when in compact because after popping the main view, the bold is removed when the row is un-highlighted. There might be a way to disable using bold in that case.

There are 2 other bugs you should be aware of:

  1. In portrait the sidebar only shows on the second tap of the Sidebar nav button.
  2. In portrait if you show the sidebar and select the same item that is already showing, the sidebar does not dismiss.
like image 67
malhal Avatar answered Oct 21 '22 14:10

malhal