listforeachswiftuiswiftui-navigationlinkswiftui-swipeactions

With SwiftUI 3.0 .swipeActions in a ForEach how do you have an action go to another view while passing that view the input argument of the ForEach?


I am tying to add a .swipeAction to a ForEach list in which I want to pass the element in the list that was selected by the user to another invoked View. In other words when the user swipes on an item in the list, I want the user to be taken to a new View which has the contents of that item in the list so that it can display details from that item in that new view.

With that said, I have mocked up this simple example which I hope helps show the issue I am having.

import SwiftUI

struct ContentView: View {
  
  var colors : [Color] = [Color.red, Color.green,
                          Color.blue, Color.yellow,
                          Color.brown, Color.cyan, Color.pink]
  
  var body: some View {
    NavigationView {
      VStack {
        List {
          ForEach(colors, id: \.self) { color in
            ColorRowView(color: color)
              .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                Button(action: { print("Hello From The First Button") },
                       label: {Label("Hello", systemImage: "face.smiling")})
                .tint(Color.orange)
                NavigationLink(destination: ColorShowView(color: color),
                               label: {Image(systemName: "magnifyingglass")})
               .tint(.yellow)
              }
          }
        }
        Spacer()
        NavigationLink(destination: ColorShowView(color: Color.red),
                       label: { Image(systemName: "magnifyingglass" ) } ).tint(.yellow)
      }
      .navigationViewStyle(StackNavigationViewStyle())
      .navigationTitle(Text("List Of Colors"))
      .navigationBarTitleDisplayMode(.inline)
    }
  }
  
}

struct ColorRowView: View {
  
  var color: Color
  
  var body: some View {
    VStack {
      Text("Color \(color.description)").foregroundColor(color)
    }
  }
  
}

struct ColorShowView: View {
  
  var color: Color
  
  var body: some View {
    VStack {
      Text("Color Name \(color.description)").foregroundColor(color)
      Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
    }
  }
  
}

What I find is that if I put a NavigationLink as a button on the .swipeActions it shows up correctly, but when tapped the NavigationLink does not execute and hence does not take you to a new View.

If I move that same Navigation Link down to after the .swipeActions, and invoke it with some @State it works, but it adds another row in-between each row in the list of the ForEach. In other words, the ForEach of course sees it as part of its list and adds it in with the other items in the list. Even if I add a .hidden() onto that NavigationLink, it still takes up space with a new row, it just hides the contents of the row, not the row itself.

If I move the NavigationLink outside of the ForEach, then the input argument of color from the ForEach is out of scope. It will correctly build the view and execute the link (using an action and some @State), but it can not pass the color input from the ForEach because of course it is out of scope. If you hard code a color in its place it works fine, except of course for the fact that it does not have the color from the users selection from the list.

Note I put a simple NavigationLink on the bottom of the view as well just so that I could see that it worked correctly outside of the issue with the .swipeActions, and it does work fine with a hard coded color value like Color.red.

This is of course a very made up example, but I think it does show the issue.

Has anyone used a .swipeActions to invoke a NavigationLink to a new view passing into that view the users selected item (in this case the color)? If so how do you get that to work. It feels like a chicken and the egg problem, I can not seem to both have access to the scope in which the input data (the color) is available, and a NavigationLink that does not become part of the view of the ForEach list.

Anyone have any ideas? Thanks in advance for any and all commentary, corrections, ideas, etc.


Solution

  • First solution: by using fullScreenCover and @State var selectedColor @Environment(.presentationMode) var presentationMode

    //  ContentView.swift
            //  StackOverFlow
            //
            //  Created by Mustafa T Mohammed on 12/31/21.
            //
        
        import SwiftUI
        
        struct ContentView: View {
            @State var isColorShowViewPresented = false
            @State var selectedColor: Color = .yellow
            var colors : [Color] = [Color.red, Color.green,
                                    Color.blue, Color.yellow,
    Color.brown, Color.cyan, Color.pink]
        
            var body: some View {
                NavigationView {
                    VStack {
                            // you can use List instead of ForEach loop it's the same
                            // less code :)
                        List(colors, id: \.self) { color in
                            ColorRowView(color: color)
                                .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                                    Button(action: {
                                        isColorShowViewPresented.toggle() // toggle isColorShowViewPresented to trigger the
                                                                                                         // fullScreenCover
                                        selectedColor = color
                                        print("Hello From The First Button")
                                    },
                                                 label: {Label("Hello", systemImage: "face.smiling")})
                                        .tint(Color.orange)
                                }
        
                        }
                        Spacer()
                            .fullScreenCover(isPresented: $isColorRowViewPresented) {
                                    // if you like to implement what happen when user dismiss the presented view
                                print("user dissmissed ColorRowView")
                            } content: {
                                ColorShowView(color: selectedColor) // view that you want to present
                            }
                    }
                    .navigationViewStyle(StackNavigationViewStyle())
                    .navigationTitle(Text("List Of Colors"))
                    .navigationBarTitleDisplayMode(.inline)
                }
            }
        
        }
        
        struct ColorRowView: View {
        
            var color: Color
            var body: some View {
                VStack {
                    Text("Color \(color.description)").foregroundColor(color)
                }
            }
        
        }
        
        struct ColorShowView: View {
            @Environment(\.presentationMode) var presentationMode
        
            var color: Color
        
            var body: some View {
                VStack {
                    Button {
                        presentationMode.wrappedValue.dismiss()
                    } label: {
                        Text("Dismiss")
                    }
                    Spacer()
                    Text("Color Name \(color.description)").foregroundColor(color)
                    Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
                    Spacer()
                }
            }
        
        }
    

    Second solution: NavigationLink and @State var selectedColor

        //
        //  ContentView.swift
        //  StackOverFlow
        //
        //  Created by Mustafa T Mohammed on 12/31/21.
        //
    
    import SwiftUI
    
    struct ContentView: View {
        @State var isColorShowViewPresented = false
        @State var selectedColor: Color = .yellow
        var colors : [Color] = [Color.red, Color.green,
                                                        Color.blue, Color.yellow,
                                                        Color.brown, Color.cyan, Color.pink]
    
        var body: some View {
            NavigationView {
                VStack {
                        // you can use List instead of ForEach loop it's the same
                        // less code :)
                    List(colors, id: \.self) { color in
                        ColorRowView(color: color)
                            .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                                Button(action: {
                                    isColorShowViewPresented.toggle() // toggle isColorShowViewPresented to trigger the
                                                                                                     // NavigationLink
                                    selectedColor = color
                                    print("Hello From The First Button")
                                },
                                             label: {Label("Hello", systemImage: "face.smiling")})
                                    .tint(Color.orange)
                            }
    
                    }
                    Spacer()
                    NavigationLink("", isActive: $isColorShowViewPresented) {
                        ColorShowView(color: selectedColor)
                    }
                }
                .navigationViewStyle(StackNavigationViewStyle())
                .navigationTitle(Text("List Of Colors"))
                .navigationBarTitleDisplayMode(.inline)
            }
        }
    
    }
    
    struct ColorRowView: View {
    
        var color: Color
        var body: some View {
            VStack {
                Text("Color \(color.description)").foregroundColor(color)
            }
        }
    
    }
    
    struct ColorShowView: View {
        var color: Color
    
        var body: some View {
            VStack {
                Text("Color Name \(color.description)").foregroundColor(color)
                Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
            }
        }
    
    }