22

I have a Plus button in my first view. Looks like a FAB button. I want to hide it after I tap some step wrapped in NavigationLink. So far I have something like this:

ForEach(0 ..< 12) {item in
    NavigationLink(destination: TransactionsDetailsView()) {
        VStack {
            HStack(alignment: .top) {
                Text("List item")
            }
            .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
            .foregroundColor(.black)
            Divider()
        }
    }
    .simultaneousGesture(TapGesture().onEnded{
        self.showPlusButton = false
    })
        .onAppear(){
            self.showPlusButton = true
    }
}

It works fine with single tap. But when I long press NavigationLink it doesn't work. How should I rewrite my code to include long press as well? Or maybe I should make it work different than using simultaneousGesture?

Asperi
  • 228,894
  • 20
  • 464
  • 690
mallow
  • 2,368
  • 2
  • 22
  • 63

4 Answers4

14

Yes, NavigationLink does not allow such simultaneous gestures (might be as designed, might be due to issue, whatever).

The behavior that you expect might be implemented as follows (of course if you need some chevron in the list item, you will need to add it manually)

struct TestSimultaneousGesture: View {
    @State var showPlusButton = false
    @State var currentTag: Int?
    var body: some View {

        NavigationView {
            List {
                ForEach(0 ..< 12) { item in
                    VStack {
                        HStack(alignment: .top) {
                            Text("List item")
                            NavigationLink(destination: Text("Details"), tag: item, selection: self.$currentTag) {
                                EmptyView()
                            }
                        }
                        .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
                        .foregroundColor(.black)
                        Divider()
                    }
                    .simultaneousGesture(TapGesture().onEnded{
                        print("Got Tap")
                        self.currentTag = item
                        self.showPlusButton = false
                    })
                    .simultaneousGesture(LongPressGesture().onEnded{_ in
                        print("Got Long Press")
                        self.currentTag = item
                        self.showPlusButton = false
                    })
                    .onAppear(){
                        self.showPlusButton = true
                    }
                }
            }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thank you, @Asperi. Adding .simultaneousGesture(LongPressGesture(...) did help with triggering long press too. But I am still wondering if it's the best way. I can see that your code is inside a List. Initially I have used ScrollView instead, because List made a problem with one part of code I had in this list. I have also tried using a little different code (I will post example soon below in my answer to my question) and than simultaneousGesture was not needed at all and chevrons were added automatically – mallow Nov 18 '19 at 11:36
  • BTW, what do you mean that "NavigationLink does not allow such simultaneous gestures" if in your example you can use 2 gestures. You mean that there is no way to handle both gestures with one command? – mallow Nov 18 '19 at 11:39
  • 1
    No, I meant adding gestures modifiers directly to regular NavigationLink in List - navigation stops working at all. BTW, you did not tell anything in question about using ScrollView - container matters in this case, because NavigationLink in ScrollView and in List behaves differently. Just in case. – Asperi Nov 18 '19 at 11:49
  • Yes, @Asperi, you are correct. That time I didn't know that it will work different. Please see my code in my answer below. I have tried using a list this time. But a little different than in your example. Also, I have another idea if using ScrollView. Instead of simultaneousGesture modifier I have tried to use onDisappear modifier on the same level where I had onAppear in my original post. It kinda worked, but there was a slight delay. The PlusButton was disappearing after the new screen slides in. I wanted to hide it before this. – mallow Nov 18 '19 at 12:00
  • Thanks for help, @Asperi. After more tries I think that your answer is the best solution for my question. – mallow Nov 18 '19 at 12:48
14

I'm using the following code. I prefer it to just NavigationLink by itself because it lets me reuse my existing ButtonStyles.


struct NavigationButton<Destination: View, Label: View>: View {
    var action: () -> Void = { }
    var destination: () -> Destination
    var label: () -> Label

    @State private var isActive: Bool = false

    var body: some View {
        Button(action: {
            self.action()
            self.isActive.toggle()
        }) {
            self.label()
              .background(
                ScrollView { // Fixes a bug where the navigation bar may become hidden on the pushed view
                    NavigationLink(destination: LazyDestination { self.destination() },
                                                 isActive: self.$isActive) { EmptyView() }
                }
              )
        }
    }
}

// This view lets us avoid instantiating our Destination before it has been pushed.
struct LazyDestination<Destination: View>: View {
    var destination: () -> Destination
    var body: some View {
        self.destination()
    }
}

And to use it:

var body: some View {
  NavigationButton(
    action: { print("tapped!") },
    destination: { Text("Pushed View") },
    label: { Text("Tap me") }
  )
}
arsenius
  • 12,090
  • 7
  • 58
  • 76
  • Thanks. Maybe it's me having not enough of experience with SwiftUI, but I don't know how to implement your solution in my example. And you write that you prefer this to NavigationLink, but later you still have NavigationLink in your code. – mallow Nov 18 '19 at 11:19
  • I've added an example of how to use this. Just put the initial code in a new file. I meant, I use this in places where I would *otherwise* have used `NavigationLink` by itself. – arsenius Nov 18 '19 at 13:06
  • what's a LazyDestination?? – Ravindra_Bhati Feb 28 '20 at 07:23
  • @Ravindra_Bhati Added code for that `LazyDestination`. It's not critical, but can be convenient. You can check out [this article](https://medium.com/better-programming/swiftui-navigation-links-and-the-common-pitfalls-faced-505cbfd8029b) for more info. – arsenius Mar 02 '20 at 01:22
  • 2
    Wow! This solution is so elegant that NavigationButton should become part of SwiftUI in 2020! – Paul D. Jun 17 '20 at 18:46
  • @arsenius this is so elegant but I have issues where I have 2 NavigationButtons, and the `@State var isActive` preventing navigation....any ideas to solve this? see here: https://stackoverflow.com/questions/67779867/swiftui-how-to-build-multiple-navigation-links – GarySabo Jun 18 '21 at 14:29
  • Works in SwiftUI 3.0!!! amazing! – TimBigDev Oct 06 '21 at 08:56
1

Another alternative I have tried. Not using simultaneousGesture, but an onDisappear modifier instead. Code is simple and It works. One downside is that those actions happen with a slight delay. Because first the destination view slides in and after this the actions are performed. This is why I still prefer @Asperi's answer where he added .simultaneousGesture(LongPressGesture) to my code.

ForEach(0 ..< 12) {item in
    NavigationLink(destination: TransactionsDetailsView()) {
        VStack {
            HStack(alignment: .top) {
                Text("List item")
            }
            .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
            .foregroundColor(.black)
            Divider()
        }
    }
    .onDisappear(){
        self.showPlusButton = false
    }
    .onAppear(){
        self.showPlusButton = true
    }
}
mallow
  • 2,368
  • 2
  • 22
  • 63
0

I have tried an alternative approach to solving my problem. Initially I didn't use "List" because I had a problem with part of my code. But it cause another problem: PlusButton not disappearing on next screen after tapping NavigationLink. This is why I wanted to use simultaneousGesture - after tapping a link some actions would be performed as well (here: PlusButton would be hidden). But it didn't work well.

I have tried an alternative solution. Using List (and maybe I will solve another problem later.

Here is my alternative code. simultaneousGesture is not needed at all. Chevrons are added automatically to the list. And PlusButton hides the same I wanted.

import SwiftUI

struct BookingView: View {

    @State private var show_modal: Bool = false

    var body: some View {
        NavigationView {
            ZStack {
                List {
                    DateView()
                        .listRowInsets(EdgeInsets())

                    ForEach(0 ..< 12) {item in
                        NavigationLink(destination: BookingDetailsView()) {
                            HStack {
                                Text("Booking list item")
                                Spacer()
                            }
                            .padding()
                        }
                    }
                }.navigationBarTitle(Text("Booking"))

                VStack {
                    Spacer()
                    Button(action: {
                        print("Button Pushed")
                        self.show_modal = true
                    }) {
                        Image(systemName: "plus")
                            .font(.largeTitle)
                            .frame(width: 60, height: 60)
                            .foregroundColor(Color.white)
                    }.sheet(isPresented: self.$show_modal) {
                        BookingAddView()
                    }
                    .background(Color.blue)
                    .cornerRadius(30)
                    .padding()
                    .shadow(color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3)
                }
            }

        }
    }

}
mallow
  • 2,368
  • 2
  • 22
  • 63
  • Hmmm... This does not look like a code related to the one in question – Asperi Nov 18 '19 at 12:10
  • Yup. Your answer is better. My code is a way of avoiding calling another actions at all to solve my original problem - hiding PlusButton in my view. I could not post this code in a comment. This is why I sent it as an answer. Sorry. Still new here – mallow Nov 18 '19 at 12:31