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