最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - Issue Navigating to View from Side Menu in Swiftui - Stack Overflow

programmeradmin0浏览0评论

I am currently making a side menu for an iOS app in SwiftUI and having navigation issues. Navigating to a view from the side menu is fine, but the view will show up inside the side menu itself, rather than it being like Twitter, where the side menu is dismissed and then the view opens up in the app. I have made a reproducible mock of the issue, which can be seen below.

First is the data model for the side menu:

struct SideMenuOBJ: Identifiable, Hashable {
    let id = UUID()
    let image: UIImage
    let label: String
    var vc: UIViewController?
}

struct SideMenuRepository {
    static func profile () -> [SideMenuOBJ] {
        let base = [
            SideMenuOBJ(image: UIImage(systemName: "person.circle")!, label: "Profile"),
            SideMenuOBJ(image: UIImage(systemName: "list.clipboard")!, label: "Account Summary"),
            SideMenuOBJ(image: UIImage(systemName: "fossil.shell")!, label: "History")
        ]
        
        return base
    }
}

The Drawer creates the side menu:

public struct Drawer<Menu: View, Content: View>: View {
    @Binding private var isOpened: Bool
    private let menu: Menu
    private let content: Content
    
    public init(
        isOpened: Binding<Bool>,
        @ViewBuilder menu:  () -> Menu,
        @ViewBuilder content: () -> Content
    ) {
        _isOpened = isOpened
        self.menu = menu()
        self.content = content()
    }
    
    public var body: some View {
        ZStack(alignment: .leading) {
            content
            
            if isOpened {
                Color.clear
                    .contentShape(Rectangle())
                    .onTapGesture {
                        if isOpened {
                            isOpened.toggle()
                        }
                    }
                menu
                    .transition(.move(edge: .leading))
                    .zIndex(1)
            }
        }
        .animation(.spring(), value: isOpened)
        .environment(\.drawerPresentationMode, $isOpened.mappedToDrawerPresentationMode())
    }
}

public struct DrawerPresentationMode {
    @Binding private var _isOpened: Bool
    
    init(isOpened: Binding<Bool>) {
        __isOpened = isOpened
    }
    
    public var isOpened: Bool {
        _isOpened
    }
    
    mutating func open() {
        if !_isOpened {
            _isOpened = true
        }
    }
    
    mutating func close() {
        if _isOpened {
            _isOpened = false
        }
    }
}

extension Binding where Value == Bool {
    func mappedToDrawerPresentationMode() -> Binding<DrawerPresentationMode> {
        Binding<DrawerPresentationMode>(
            get: {
                DrawerPresentationMode(isOpened: self)
            },
            set: { newValue in
                self.wrappedValue = newValue.isOpened
            }
        )
    }
}

extension DrawerPresentationMode {
    static var placeholder: DrawerPresentationMode {
        DrawerPresentationMode(isOpened: .constant(false))
    }
}

private struct DrawerPresentationModeKey: EnvironmentKey {
    static var defaultValue: Binding<DrawerPresentationMode> = .constant(.placeholder)
}

extension EnvironmentValues {
    public var drawerPresentationMode: Binding<DrawerPresentationMode> {
        get { self[DrawerPresentationModeKey.self] }
        set { self[DrawerPresentationModeKey.self] = newValue }
    }
}

The view model is pretty straightforward:

class SideMenuViewModel: ObservableObject {
    @Published var profileOptions: [SideMenuOBJ] = SideMenuRepository.profile()
    
    @ViewBuilder
    func handleProfileOptionTap(option: SideMenuOBJ?) -> some View {
        switch option?.label {
        case "My Profile":
            ProfileView()
        case "Account":
            AccountSummaryView()
        case "My Takes History":
            History()
        default:
            EmptyView()
        }
    }
}

Finally, here is the side menu and the parent view that calls the side menu:

struct SideMenu: View {
    @ObservedObject var viewModel: SideMenuViewModel
    @State private var selectedOption: SideMenuOBJ?
    @State private var isNavigating: Bool = false
    
    var body: some View {
        NavigationStack {
            ZStack {
                ScrollView {
                    VStack(alignment: .leading, spacing: 16) {
                        VStack(alignment: .leading, spacing: 12) {
                            ForEach(viewModel.profileOptions) { option in
                                Button {
                                    selectedOption = option
                                    isNavigating.toggle()
                                } label: {
                                    HStack {
                                        Image(uiImage: option.image)
                                            .renderingMode(.template)
                                            .foregroundColor(.primary)
                                            .frame(width: 24, height: 24)
                                        Text(option.label)
                                            .fontWeight(.bold)
                                            .foregroundStyle(Color.primary)
                                            .font(.body)
                                            .padding(.vertical, 8)
                                            .padding(.horizontal)
                                            .cornerRadius(8)
                                    }
                                    .cornerRadius(8)
                                }
                            }
                        }
                        .padding(.horizontal)
                    }
                }
                .scrollIndicators(.never)
            }
            .navigationDestination(item: $selectedOption) { option in
                viewModel.handleProfileOptionTap(option: option)
            }
        }
    }
}
struct SideMenuIssueApp: App {
    @State private var showSideMenu: Bool = false
    var body: some Scene {
        WindowGroup {
            Drawer(isOpened: $showSideMenu) {
                ZStack {
                    SideMenu(viewModel: SideMenuViewModel())
                        .frame(width: 270)
                }
            } content: {
                contentView
            }
        }
    }
    
    var contentView: some View {
        NavigationStack {
            TabView {
                ContentView()
                    .tabItem {
                        Label("Content View", systemImage: "tray")
                    }
            }
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button {
                        showSideMenu.toggle()
                    } label: {
                        Image(systemName: "menucard")
                    }
                }
            }
        }
    }
}

ProfileView, AccountSummaryView, and HistoryView are all default views for navigation testing. What am I doing wrong in regards to the navigation that's making the selected view show up in the Side Menu instead of on the app itself? All help is greatly appreciated!

I am currently making a side menu for an iOS app in SwiftUI and having navigation issues. Navigating to a view from the side menu is fine, but the view will show up inside the side menu itself, rather than it being like Twitter, where the side menu is dismissed and then the view opens up in the app. I have made a reproducible mock of the issue, which can be seen below.

First is the data model for the side menu:

struct SideMenuOBJ: Identifiable, Hashable {
    let id = UUID()
    let image: UIImage
    let label: String
    var vc: UIViewController?
}

struct SideMenuRepository {
    static func profile () -> [SideMenuOBJ] {
        let base = [
            SideMenuOBJ(image: UIImage(systemName: "person.circle")!, label: "Profile"),
            SideMenuOBJ(image: UIImage(systemName: "list.clipboard")!, label: "Account Summary"),
            SideMenuOBJ(image: UIImage(systemName: "fossil.shell")!, label: "History")
        ]
        
        return base
    }
}

The Drawer creates the side menu:

public struct Drawer<Menu: View, Content: View>: View {
    @Binding private var isOpened: Bool
    private let menu: Menu
    private let content: Content
    
    public init(
        isOpened: Binding<Bool>,
        @ViewBuilder menu:  () -> Menu,
        @ViewBuilder content: () -> Content
    ) {
        _isOpened = isOpened
        self.menu = menu()
        self.content = content()
    }
    
    public var body: some View {
        ZStack(alignment: .leading) {
            content
            
            if isOpened {
                Color.clear
                    .contentShape(Rectangle())
                    .onTapGesture {
                        if isOpened {
                            isOpened.toggle()
                        }
                    }
                menu
                    .transition(.move(edge: .leading))
                    .zIndex(1)
            }
        }
        .animation(.spring(), value: isOpened)
        .environment(\.drawerPresentationMode, $isOpened.mappedToDrawerPresentationMode())
    }
}

public struct DrawerPresentationMode {
    @Binding private var _isOpened: Bool
    
    init(isOpened: Binding<Bool>) {
        __isOpened = isOpened
    }
    
    public var isOpened: Bool {
        _isOpened
    }
    
    mutating func open() {
        if !_isOpened {
            _isOpened = true
        }
    }
    
    mutating func close() {
        if _isOpened {
            _isOpened = false
        }
    }
}

extension Binding where Value == Bool {
    func mappedToDrawerPresentationMode() -> Binding<DrawerPresentationMode> {
        Binding<DrawerPresentationMode>(
            get: {
                DrawerPresentationMode(isOpened: self)
            },
            set: { newValue in
                self.wrappedValue = newValue.isOpened
            }
        )
    }
}

extension DrawerPresentationMode {
    static var placeholder: DrawerPresentationMode {
        DrawerPresentationMode(isOpened: .constant(false))
    }
}

private struct DrawerPresentationModeKey: EnvironmentKey {
    static var defaultValue: Binding<DrawerPresentationMode> = .constant(.placeholder)
}

extension EnvironmentValues {
    public var drawerPresentationMode: Binding<DrawerPresentationMode> {
        get { self[DrawerPresentationModeKey.self] }
        set { self[DrawerPresentationModeKey.self] = newValue }
    }
}

The view model is pretty straightforward:

class SideMenuViewModel: ObservableObject {
    @Published var profileOptions: [SideMenuOBJ] = SideMenuRepository.profile()
    
    @ViewBuilder
    func handleProfileOptionTap(option: SideMenuOBJ?) -> some View {
        switch option?.label {
        case "My Profile":
            ProfileView()
        case "Account":
            AccountSummaryView()
        case "My Takes History":
            History()
        default:
            EmptyView()
        }
    }
}

Finally, here is the side menu and the parent view that calls the side menu:

struct SideMenu: View {
    @ObservedObject var viewModel: SideMenuViewModel
    @State private var selectedOption: SideMenuOBJ?
    @State private var isNavigating: Bool = false
    
    var body: some View {
        NavigationStack {
            ZStack {
                ScrollView {
                    VStack(alignment: .leading, spacing: 16) {
                        VStack(alignment: .leading, spacing: 12) {
                            ForEach(viewModel.profileOptions) { option in
                                Button {
                                    selectedOption = option
                                    isNavigating.toggle()
                                } label: {
                                    HStack {
                                        Image(uiImage: option.image)
                                            .renderingMode(.template)
                                            .foregroundColor(.primary)
                                            .frame(width: 24, height: 24)
                                        Text(option.label)
                                            .fontWeight(.bold)
                                            .foregroundStyle(Color.primary)
                                            .font(.body)
                                            .padding(.vertical, 8)
                                            .padding(.horizontal)
                                            .cornerRadius(8)
                                    }
                                    .cornerRadius(8)
                                }
                            }
                        }
                        .padding(.horizontal)
                    }
                }
                .scrollIndicators(.never)
            }
            .navigationDestination(item: $selectedOption) { option in
                viewModel.handleProfileOptionTap(option: option)
            }
        }
    }
}
struct SideMenuIssueApp: App {
    @State private var showSideMenu: Bool = false
    var body: some Scene {
        WindowGroup {
            Drawer(isOpened: $showSideMenu) {
                ZStack {
                    SideMenu(viewModel: SideMenuViewModel())
                        .frame(width: 270)
                }
            } content: {
                contentView
            }
        }
    }
    
    var contentView: some View {
        NavigationStack {
            TabView {
                ContentView()
                    .tabItem {
                        Label("Content View", systemImage: "tray")
                    }
            }
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button {
                        showSideMenu.toggle()
                    } label: {
                        Image(systemName: "menucard")
                    }
                }
            }
        }
    }
}

ProfileView, AccountSummaryView, and HistoryView are all default views for navigation testing. What am I doing wrong in regards to the navigation that's making the selected view show up in the Side Menu instead of on the app itself? All help is greatly appreciated!

Share Improve this question asked Jan 20 at 2:15 user16961399user16961399 276 bronze badges 3
  • Note, @Binding is only meaningful in a View, not in DrawerPresentationMode. Also, in your SideMenuIssueApp you should declare @StateObject private var viewModel = SideMenuViewModel() so that it is observed by the App/view, then pass it down to your SideMenu(viewModel: viewModel). Note also, TabView should not be inside a NavigationStack – workingdog support Ukraine Commented Jan 20 at 2:47
  • @workingdogsupportUkraine Without the NavigationStack, the side menu button doesn't show up on the view. – user16961399 Commented Jan 20 at 3:20
  • @workingdogsupportUkraine Any ideas for the navigation fix? – user16961399 Commented Jan 20 at 20:24
Add a comment  | 

1 Answer 1

Reset to default 0

Your view hierarchy needs to be adjusted. This is the code I debugged.

struct SideMenuIssueApp: App {
    @State private var showSideMenu: Bool = false
    @State private var selectedOption: SideMenuOBJ?
    var body: some Scene {
        WindowGroup {
            Drawer(isOpened: $showSideMenu) {
                ZStack {
                    SideMenu(viewModel: SideMenuViewModel()) {
                        selectedOption = $0
                        showSideMenu.toggle()
                    }
                    .frame(width: 270)
                }
            } content: {
                contentView
            }
        }
    }
    
    var contentView: some View {
        NavigationStack {
            TabView {
                ContentView()
                    .tabItem {
                        Label("Content View", systemImage: "tray")
                    }
            }
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button {
                        showSideMenu.toggle()
                    } label: {
                        Image(systemName: "menucard")
                    }
                }
            }
            .navigationDestination(item: $selectedOption) { option in
                Text(option.label)
            }
        }
    }
}

struct SideMenuOBJ: Identifiable, Hashable {
    let id = UUID()
    let image: UIImage
    let label: String
    var vc: UIViewController?
}

struct SideMenuRepository {
    static func profile () -> [SideMenuOBJ] {
        let base = [
            SideMenuOBJ(image: UIImage(systemName: "person.circle")!, label: "Profile"),
            SideMenuOBJ(image: UIImage(systemName: "list.clipboard")!, label: "Account Summary"),
            SideMenuOBJ(image: UIImage(systemName: "fossil.shell")!, label: "History")
        ]
        
        return base
    }
}

public struct Drawer<Menu: View, Content: View>: View {
    @Binding private var isOpened: Bool
    private let menu: Menu
    private let content: Content
    
    public init(
        isOpened: Binding<Bool>,
        @ViewBuilder menu:  () -> Menu,
        @ViewBuilder content: () -> Content
    ) {
        _isOpened = isOpened
        self.menu = menu()
        self.content = content()
    }
    
    public var body: some View {
        ZStack(alignment: .leading) {
            content
            
            if isOpened {
                Color.clear
                    .contentShape(Rectangle())
                    .onTapGesture {
                        if isOpened {
                            isOpened.toggle()
                        }
                    }
                menu
                    .transition(.move(edge: .leading))
                    .zIndex(1)
            }
        }
        .animation(.spring(), value: isOpened)
        .environment(\.drawerPresentationMode, $isOpened.mappedToDrawerPresentationMode())
    }
}

public struct DrawerPresentationMode {
    @Binding private var _isOpened: Bool
    
    init(isOpened: Binding<Bool>) {
        __isOpened = isOpened
    }
    
    public var isOpened: Bool {
        _isOpened
    }
    
    mutating func open() {
        if !_isOpened {
            _isOpened = true
        }
    }
    
    mutating func close() {
        if _isOpened {
            _isOpened = false
        }
    }
}

extension Binding where Value == Bool {
    func mappedToDrawerPresentationMode() -> Binding<DrawerPresentationMode> {
        Binding<DrawerPresentationMode>(
            get: {
                DrawerPresentationMode(isOpened: self)
            },
            set: { newValue in
                self.wrappedValue = newValue.isOpened
            }
        )
    }
}

extension DrawerPresentationMode {
    static var placeholder: DrawerPresentationMode {
        DrawerPresentationMode(isOpened: .constant(false))
    }
}

private struct DrawerPresentationModeKey: EnvironmentKey {
    static var defaultValue: Binding<DrawerPresentationMode> = .constant(.placeholder)
}

extension EnvironmentValues {
    public var drawerPresentationMode: Binding<DrawerPresentationMode> {
        get { self[DrawerPresentationModeKey.self] }
        set { self[DrawerPresentationModeKey.self] = newValue }
    }
}

class SideMenuViewModel: ObservableObject {
    @Published var profileOptions: [SideMenuOBJ] = SideMenuRepository.profile()
}

struct SideMenu: View {
    @ObservedObject var viewModel: SideMenuViewModel
    let action: (SideMenuOBJ) -> Void
    @State private var isNavigating: Bool = false
    
    var body: some View {
        NavigationStack {
            ZStack {
                ScrollView {
                    VStack(alignment: .leading, spacing: 16) {
                        VStack(alignment: .leading, spacing: 12) {
                            ForEach(viewModel.profileOptions) { option in
                                Button {
                                    action(option)
                                    isNavigating.toggle()
                                } label: {
                                    HStack {
                                        Image(uiImage: option.image)
                                            .renderingMode(.template)
                                            .foregroundColor(.primary)
                                            .frame(width: 24, height: 24)
                                        Text(option.label)
                                            .fontWeight(.bold)
                                            .foregroundStyle(Color.primary)
                                            .font(.body)
                                            .padding(.vertical, 8)
                                            .padding(.horizontal)
                                            .cornerRadius(8)
                                    }
                                    .cornerRadius(8)
                                }
                            }
                        }
                        .padding(.horizontal)
                    }
                }
                .scrollIndicators(.never)
            }
        }
    }
}
发布评论

评论列表(0)

  1. 暂无评论