NEW: Start my new Ultimate Portfolio App course with a free Hacking with Swift+ trial! >>

Using TabbedSidebar with enum and fileImporter

Forums > SwiftUI

I'm trying to present fileImporter from a menu I've made using TabbedSidebar (from this HWS+ tutorial)

I've got my enum for my menu options:

enum SidebarMenuOption: String, CaseIterable, Identifiable {
    var id: String { self.rawValue }

    case import = "Import"
    // etc

    var icon: String { ... }

    var view: some View {
        return Group {
            switch self {
                 case .import: importView()
               // etc 
            }
        }
    }
}

And I'm presenting it as a variation on TabbedSidebar by iterating over the cases:

struct ConvertibleSidebar: View {
    @Environment(\.horizontalSizeClass) var sizeClass
    @State private var selection: SidebarMenuOption? = SidebarMenuOption.none

    var body: some View {
        if sizeClass == .compact {
            TabView(selection: $selection) {
                ForEach(SidebarMenuOption.allCases) { option in
                    option.view
                        .tabItem {
                            Text(option.rawValue)
                            Image(systemName: option.icon)
                        }
                }
            }
        } else {
            NavigationView {
                List(selection: $selection) {
                    ForEach(SidebarMenuOption.allCases) { option in
                        NavigationLink(
                            destination: option.view,
                            label: {
                                Label( title: { Text(option.rawValue) },
                                    icon: { Image(systemName: option.icon) })
                            })
                    }
                }
            }
        }
    }
}

Most of those views are probably going to be alerts, action sheets, or sheets, starting with the import. However, tutorials for fileImporter are a bit thin on the ground.

I can launch it by attaching it as a view modifier that toggles the boolean, but I'm having trouble figuring out how to make the leap from the list selection in the sidebar to presenting the fileImporter sheet, or how I should convert the selection in the TabbedSidebar to a toggle of the boolean value required for isPresented.

    var importView: some View {
        Button(action: { showImporter.toggle() }, label: {
         // ...
        })
        .fileImporter(isPresented: $showImporter,
                      allowedContentTypes: [ ... ],
                      allowsMultipleSelection: true,
                      onCompletion: { result in
                      // ...
                      })

    }

   

I'm terribly confused about how fileImporter works. The isPresented parameter suggests it's just a specialized sheet, but it doesn't seem to play very nicely with other sheets?

Giving up on iterating over my menu enum, I've instead tried to implement a list of buttons, each one triggering a sheet:

struct ContentView: View {
    @State private var showImporter = false
    @State private var showSettingsMenu = false
    // other options follow

    var body: some View {
        if database.isEmpty {
            importPrompt
        } else {
            NavigationView {
                sidebar
                listView // tba
                detailView // tba
            }
        }
    }

    var sidebar: some View {
        List {
            importPrompt
            showSettingsButton
            // other options here
        }
    }

    var importPrompt: some View {
        Button(
            action: { showImporter.toggle() },
            label: {
                HStack {
                    Image(systemName: "square.and.arrow.down")
                    Text("Import")
                        .font(.title)
                }
            })
            .fileImporter(
                isPresented: $showImporter,
                allowedContentTypes: [ // ],
                allowsMultipleSelection: true,
                onCompletion: { result in
                    // do stuff
                })

    }

    var showSettingsButton: some View {
        Button(
            action: { showSettingsMenu.toggle() },
            label: {
                HStack {
                    Image(systemName: "gearshape")
                    Text("Settings")
                        .font(.title)
                }
            })
            .sheet(isPresented: $showSettingsMenu, content: {
                SettingsView()
            })
    }

    // other views for each option in the list

If I set up my preview so that the first condition in body is true, (the database is empty) the importPrompt button is displayed as it should be, and the fileImporter modal is displayed when it's tapped.

However, if I set up my preview so that the sidebar list is shown, every button EXCEPT importPrompt will work, but nothing will happen when the importPrompt button is tapped.

There's almost no material out there on how to work with fileImporter and I'm starting to suspect this is why...is it still a work in progress?

   

Leaving the issue of fileImporter behaving differently to other ViewPresentation items like sheets or alerts, I've almost got this working, with the exception of the fact that I can't seem to get the view presenting modally in the smaller size classes.

        if sizeClass == .compact {
            TabView(selection: $selection) {
                ForEach(SidebarMenuOption.allCases) { option in
                    option.destination
                    .tabItem {
                        Text(option.rawValue)
                        Image(systemName: option.icon)
                    }
                }
            }

This works as expected, i.e. it presents the views, but not as modals/sheets.

        if sizeClass == .compact {
            TabView(selection: $selection) {
                ForEach(SidebarMenuOption.allCases) { option in
                   sheet(item: $option) { sheet in
                       sheet.destination
                   }
                    .tabItem {
                        Text(option.stringValue)
                        Image(systemName: option.icon)
                    }
                }
            }

This doesn't.

Similarly, the list view also behaves a little contrary to what I would expect, and I'm not sure I understand why.

Nothing happens doing it this way when tapping one of the list rows:

            List(SidebarMenuOption.allCases, selection: $selection) { option in
                HStack {
                    Image(systemName: option.icon)
                    Text(option.stringValue)
                }
                .sheet(item: $selection) { sheet in
                    sheet.destination
                }
            }.listStyle(SidebarListStyle())
        }

This brings up modal sheets for each row, but they're all empty, ie they're not getting the specified content:

            List {
                ForEach(SidebarMenuOption.allCases) { option in
                    Button(action: { self.selection = option }) {
                        HStack {
                            Image(systemName: option.icon)
                            Text(option.stringValue)
                        }
                    }
                    .sheet(item: $selection) { sheet in
                        sheet.destination
                    }
                }
            }.listStyle(SidebarListStyle())
        }

And this displays every sheet correctly except the first one, which is blank, as well as being redundant? (isn't the button action just doing what the List selection is supposed to be doing?) ETA: Experimentation has shown it doesn't matter WHICH one is first, the first item in the list always brings up an empty sheet instead of the view it should.

            List(selection: $selection) {
                ForEach(SidebarMenuOption.allCases) { option in
                    Button(action: { self.selection = option }) {
                        HStack {
                            Image(systemName: option.icon)
                            Text(option.stringValue)
                        }
                    }
                    .sheet(item: $selection) { sheet in
                        sheet.destination
                    }
                }
            }.listStyle(SidebarListStyle())
        }

When I introduce fileImporter to one of the sheets being presented in this, all bets are off, but I need to make sure the rest is working correctly (and that I understand why) before I can figure out why that isn't working.

   

Well, after four days of trial and error, this is the best I've been able to manage:

enum SidebarMenuOption: String, Identifiable, CaseIterable {
    var id: String { self.rawValue }

    case import = "Import"
    case optionB = "Option B"
    case optionC = "Option C"
    case optionD = "Option D"
    case optionE = "Option E"

    var icon: String {
        switch self {
            case .import: return "plus"
            // etc
        }
    }

    @ViewBuilder
    var listLabel: some View {
        HStack {
            Image(systemName: self.icon)
            Text(self.rawValue)
        }
    }

    @ViewBuilder
    var tabLabel: some View {
        VStack {
            Text(self.rawValue)
            Image(systemName: self.icon)
        }
    }

    @ViewBuilder
    var destination: some View {
        switch self {
            // isPresented is constant because if the SidebarMenuOption == .import, that will be the case.
            case .import: ImportView(isPresented: .constant(true), progress: Progress())
            case .optionB: OptionBView()
            // etc
        }
    }
}

struct ImportView: View {
    @Binding var isPresented: Bool
    @ObservedObject var progress: ImportProgress
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        VStack {
            Spacer()
            // This is redundant, but fileImporter has to be attached as a view modifier to SOMETHING
            // so it's either this, or Color.clear, which would leave behind an empty view when fileImporter is dismissed.
            // This way, at least the user has something to interact with instead of an empty view to dismiss.
            Button(
                action: { isPresented.toggle() },
                label: { ... })
                .fileImporter(
                    isPresented: $isPresented,
                    allowedContentTypes: [ ... ],
                    allowsMultipleSelection: true) { result in
                    if let urls = try? result.get() {
                        do { // stuff }
                }
            Spacer()
            Button(action: { presentationMode.wrappedValue.dismiss() }, label: {
                Text("Done")
                    .font(.largeTitle)
                    .opacity(0.7)
            })
            Spacer()
        }
    }
}

struct SidebarMenuView: View {
    @Environment(\.horizontalSizeClass) var sizeClass
    @State private var selection : SidebarMenuOption? = nil

    var body: some View {
        if sizeClass == .compact {

            // I still haven't found a way to present these as sheets from the tab bar.
            TabView(selection: $selection) {
                ForEach(SidebarMenuOption.allCases) { option in
                    option.destination
                        .tabItem { option.tabLabel }
                }
            }
        } else {
            List(selection: $selection) {
                ForEach(SidebarMenuOption.allCases) { option in
                    Button(action: { self.selection = option} ) {
                        option.listLabel
                    }
                    .sheet(item: $selection) { sheet in
                        sheet.destination
                    }
                }
            }
            .listStyle(SidebarListStyle())
        }
    }
}

So I guess this is as good as it's going to get.

   

Hacking with Swift is sponsored by Emerge

SPONSORED Emerge helps iOS devs write better, smaller apps by profiling binary size on each pull request and surfacing insights and suggestions. Companies using Emerge have reduced the size of their apps by up to 50% in just the first day. Built by a team with years of experience reducing app size at Airbnb.

Set up a demo!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.