skip to Main Content

Noob in watchOS and SwiftUI. I have created a grid view with multiple buttons on it. Whenever a button is clicked, I wish to open a new view with navigation link. Since there are multiple buttons on the view, I have created a reusable view and having a hard time to implement navigation to next view. Below is my code:

Content View:

struct ContentView: View {
    @Namespace var namespace
    @State var selected: [MenuItem] = []
    
    var body: some View {
        MainMenuCircularGridView()
    }
}

MainMenuCircularGridView:

struct MainMenuCircularGridView: View {
    let columns = Array(repeating: GridItem(.fixed(72.0), spacing: 10), count: 2)
    
    var body: some View {
        NavigationView {
            ScrollView {
                let menuOptions = MenuOptions()
                let menuOptionsAction = MenuActions()
                LazyVGrid(columns: columns, spacing: 5) {
                    ForEach(menuOptions.menu) { item in
                        MenuItemCircularGridView(imageName: item.imageName, menuItemName: item.name, action: (menuOptionsAction.menuActions.first {$0.id == item.id})?.action ?? {})
                    }
                }.padding()
            }.navigationTitle("Sample App")
        }
    }
}

MenuItemCircularGridView:

struct MenuItemCircularGridView: View {
    var imageName: String = ""
    var menuItemName: String = ""
    var action: () -> Void
    
    var body: some View {
        VStack {
            CircularButtonWithImage(imageName:imageName,
                                    imageBackgroundColor:Color(red: 34 / 255, green: 34 / 255, blue: 34 / 255),
                                    imageForegroundColor: Color(red: 23 / 255, green: 121 / 255, blue: 232 / 255),
                                    imageFrameWidth: 30.0,
                                    imageFrameHeight: 30.0,
                                    imagePadding: 10.0,
                                    action: action)
            Text(menuItemName).font(.system(size: 10))
        }.padding(10)
    }
}

CircularButtonWithImage:

struct CircularButtonWithImage: View {
    var imageName: String = ""
    var imageBackgroundColor: Color?
    var imageForegroundColor: Color?
    var imageFrameWidth: CGFloat = 0.0
    var imageFrameHeight: CGFloat = 0.0
    var imagePadding: CGFloat = 0.0
    var action: () -> Void
    
    var body: some View {
        Button(action: { action() }) {
            VStack{
                Image(imageName)
                    .renderingMode(.template)
                    .resizable()
                    .scaledToFill()
                    .frame(width: imageFrameWidth, height: imageFrameHeight)
                    .padding(imagePadding)
                    .background(imageBackgroundColor)
                    .foregroundColor(imageForegroundColor)
                    .clipShape(Circle())
            }
        }
        .buttonStyle(PlainButtonStyle())
    }
}

This is kind of how my app looks:

enter image description here

Whenever I click on any of those buttons, I want to open a new view with navigation link. Something like below:

NavigationLink(destination: DetailView()) {
     Text("Show Detail View")
}.navigationBarTitle("Navigation")

Since I have broken the view down into multiple reusable files, I am not sure where exactly should I put this logic to open a new view on button click.

Edit: Adding the hardcoded data that I am using. I was trying to pass navigation link as action to the button.

struct MenuOptions {
    let menu: [MenuItem] = [
        MenuItem(id: 0, name: "Option 1", imageName: "settings-gray"),
        MenuItem(id: 1, name: "Option 2", imageName: "settings-gray"),
        MenuItem(id: 2, name: "Option 3", imageName: "settings-gray"),
        MenuItem(id: 3, name: "Set 1", imageName: "settings-gray"),
        MenuItem(id: 4, name: "Set 2", imageName: "settings-gray"),
        MenuItem(id: 5, name: "Settings", imageName: "settings-gray")
    ]
}

struct MenuActions {
    let menuActions: [MenuItemAction] = [
        MenuItemAction(id: 1, action: { NavigationLink("New View 1", destination: View1()) }),
        MenuItemAction(id: 5, action: { NavigationLink("Settings", destination: SettingsView()) })
    ]
}

2

Answers


  1. Chosen as BEST ANSWER

    I realized me breaking down the views into different files was probably making things more complicated. Since I just have 6 buttons, I decided to keep the view into a single file and used your idea of creating MenuDestination for passing down the right view. Sharing my updated code below.

    However the main reason I wanted to use NavigationLink is to get the back button button and navigation title name on the destination view to get back to the main menu. I am not seeing that back button with navigation title on top left of the destination view. Please do let me know if you have an idea of what I could be missing. My understanding is using NavigationLink automatically adds that to the destination view.

    Updated code - MainMenuCircularGridViewNew:

    import SwiftUI
    
    struct MainMenuCircularGridViewNew: View {
        let columns = Array(repeating: GridItem(.fixed(72.0), spacing: 10), count: 2)
        @State private var destination: MenuDestination?
        var isActive: Binding<Bool> { Binding(get: { destination != nil }, set: { _ in destination = nil } ) }
        
        var body: some View {
            NavigationView {
                ScrollView {
                    let menuOptions = MenuOptions()
                    LazyVGrid(columns: columns, spacing: 5) {
                        ForEach(menuOptions.menu) { item in
                            VStack {
                                Button(action: { self.destination = MenuDestination(rawValue: item.id) }) {
                                    VStack{
                                        Image(item.imageName)
                                            .circularImageStyle(width: 30, height: 30, padding: 10,
                                                                backgroundColor: Color(red: 34 / 255, green: 34 / 255, blue: 34 / 255),
                                                                foregroundColor: Color(red: 23 / 255, green: 121 / 255, blue: 232 / 255))
                                    }
                                }
                                .buttonStyle(PlainButtonStyle())
                                Text(item.name).font(.system(size: 10))
                            }.padding(10)
                        }
                    }
                }.background(
                    NavigationLink("", isActive: isActive, destination: { destination?.view }).buttonStyle(PlainButtonStyle()).navigationViewStyle(.stack)
            )
            }.navigationTitle("Sample Application")
        }
    }
    

    Menu Destination:

    enum MenuDestination: Int, CaseIterable, Hashable {
        case option1 = 0, option2 = 1, option3 = 2, option4 = 3, option5 = 4, settings = 5
    
        @ViewBuilder var view: some View {
            switch self {
                case .option1: EmptyView()
                case .option2: MyCustomView()
                case .option3: EmptyView()
                case .option4: EmptyView()
                case .option5: EmptyView()
                case .settings: SettingsView() 
            }
        }
    }
    

  2. A NavigationLink must be in view hierarchy, so instead of putting it in action we need to put some model there.

    A sketch of possible approach

    1. destination model
    enum MenuDestination: String, CaseIterable, Hashable {
        case set1(MenuItem), set2
    
        @ViewBuilder var view: some View {
            switch self {
            case .set1(let item): View1(item: item)
            case .set2: SettingsView()
            }
        }
    }
    
    1. navigation link in view
        @State private var selection: MenuDestination?
        var isActive: Binding<Bool> {
          Binding(get: { selection != nil }, set: { selection = nil } )
        }
    
        var body: some View {
            NavigationView {
                ScrollView {
                   // ...
                }
                .background(
                    if let selection = selection {
                       NavigationLink(isActive: isActive, destination: { selection.view }) {
                       EmptyView()
                 }})
            }
        }
    
    1. button action assigns corresponding value, say MenuItemAction take as argument binding to selection and internally assign destination to that binding
    MenuItemCircularGridView(imageName: item.imageName, menuItemName: item.name, 
       action: (menuOptionsAction.menuActions.first {$0.id == item.id})?.action($selection) ?? { _ in })
    

    and MenuItemAction inited with case of corresponding MenuDestination

    See also this post

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search