Home > database >  SwiftUI - Opening a new view on button click
SwiftUI - Opening a new view on button click

Time:06-21

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()) })
    ]
}

CodePudding user response:

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

CodePudding user response:

Thanks @Asperi for helping me out!

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() 
        }
    }
}
  • Related