How to perform an action after NavigationLink is tapped? - swiftui

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?

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") }
)
}

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
}
}
}
}
}
}

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
}
}

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

Related

swipeActions in a list: button can not be pressed

I'm trying to add a simple swipe action to my list. But for some reason the button can not be pressed. When I perform a full swipe it works though.
This is my code:
var listView: some View {
List {
ForEach(Array(debits.enumerated()), id: \.element) { index, debit in
HStack {
Text(debit.name)
.swipeActions(allowsFullSwipe: true) {
Button(action: {
print("Hi \(index)")
}, label: {
Image(systemName: "slider.horizontal.3")
})
}
Spacer()
Text(String(debit.value))
.frame(width: 70, alignment: .trailing)
Text("€")
Divider().padding(.leading, 5)
Toggle("", isOn: $debits[index].toggle)
.onChange(of: debit.toggle) { newValue in
calculateAvailable()
}
.frame(width: 50)
}
}
}.listStyle(.plain).lineLimit(1)
}
Like I said, a press on the button does not print anything but the full swipe does.
Move the .swipeActions modifier to the HStack containing the whole row:
List {
ForEach(Array(debits.enumerated()), id: \.element) { index, debit in
HStack {
Text(debit.name)
Spacer()
Text(String(debit.value))
.frame(width: 70, alignment: .trailing)
Text("€")
Divider().padding(.leading, 5)
Toggle("", isOn: $debits[index].toggle)
.onChange(of: debit.toggle) { newValue in
calculateAvailable()
}
.frame(width: 50)
}
.swipeActions(allowsFullSwipe: true) {
Button(action: {
print("Hi \(index)")
}, label: {
Image(systemName: "slider.horizontal.3")
})
}
}
}
Also, I would recommend adding a stable id to your Debit structure. Something like this:
struct Debit: Hashable, Identifiable {
var name: String
var value: Double
var toggle: Bool
let id = UUID()
}
and use that as your id:
ForEach(Array(debits.enumerated()), id: \.element.id) { index, debit in
If you weren’t enumerating to get the index, then you’d simply be able to iterate over debits since the items are Identifiable:
ForEach($debits) { $debit in
This is working fine, the HStack is your problem. Try to set the swipaction on the HStack
List {
ForEach(YourList, id: \.self) { debit in
Text(debit)
.swipeActions(allowsFullSwipe: true) {
Button(action: {
print("Hi \(index)")
}, label: {
Text("test")
})
}
}
}
enter code here
I just found the answer. I used this code to dismiss the keyboard when tapping somewhere. This prevented the button from being pressed.
var body: some View {
VStack {
my code
}
.onTapGesture {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
I also had this exact same problem and it turns out a containing view had an .onTapGesture handler which was capturing the presses on the button effectively making it only work after the full swipe, but not when tapped.

SwiftUI: How to pass an argument from one view to the next with dynamically generated buttons?

Problem:
I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title
Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?
Background:
I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.
For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.
I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.
Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.
Thanks for your help in advance! :)
Testing.swift
import SwiftUI
#main
struct Testing: App {
#StateObject var buttonsEnvironmentObject = ButtonOnOff()
var body: some Scene {
WindowGroup {
Landing().environmentObject(buttonsEnvironmentObject)
}
}
}
Landing.swift
import SwiftUI
struct Landing: View {
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
EmptyView()
}
Button(action: {
self.tag = "alpha"
}) {
HStack {
Text("alpha")
}
}
Button(action: {
self.tag = "beta"
}) {
HStack {
Text("beta")
}
}
Button(action: {
self.tag = "gamma"
}) {
HStack {
Text("gamma")
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var btnName:String
#EnvironmentObject var buttonEnvObj:ButtonOnOff
init(landingChoice:String){
self.btnName = landingChoice
print("\(self.btnName) is the input string")
}
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach(0..<buttonEnvObj.buttonNames.count) { index in
BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(buttonEnvObj.buttonNames[index]) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var label: String
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: self.label)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(label)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
.cornerRadius(15)
.overlay(buttonBrandButtons.buttonBrand[self.label]! ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
ButtonOnOff.swift
import Foundation
class ButtonOnOff:ObservableObject{
var buttonNames = ["alpha","beta","gamma"]
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
For a short answer just add
.onAppear(){
buttonEnvObj.changeState(buttonName: self.btnName)
}
to ContentView that will highlight the button that was selected.
As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.
struct Landing: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
EmptyView()
}
//Put your buttons here
HStack{
//Use the keys of the dictionary to create the buttons
ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <), id: \.self){ key in
//Have the button set the value when pressed
Button(action: {
self.tag = key
buttonEnvObj.changeState(buttonName: key)
}) {
Text(key)
}
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct ContentView: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
//Change this to use the dictionary
ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
BubbleButton(key: key, value: value)
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(value) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var key: String
var value: Bool
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: key)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(key)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(value ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(value ? Color.blue : .clear)
.cornerRadius(15)
.overlay(value ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
class ButtonOnOff:ObservableObject{
//Get rid of this so you can keep the single source
//var buttonNames = ["alpha","beta","gamma"]
//When you want to add buttons just add them here it will all adjust
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}

Navigating to different views within SwiftUI

In my example code below, I have two tabs and within the main tab (Tab A) there are two "buttons" on the front page to allow the user to navigate to two views (View 1 or View 2) using navigationlinks. Within each of view 1 and view 2 there are further navigation links so my code (purposefully) resets the navigation stack for each view when you switch tabs and then return to the tab. Also, if you navigate to view 1 or view 2 (while still on the main tab (tab A)), tapping the main tab button (tab A) again brings you back to the front page (which presents the two buttons). This behaviour is what I need. The problem I now have is that the navigation links (to view 1 and view 2) don't work as intended. Sometimes clicking the view 1 button takes you to view 2 and sometimes it takes you to view 1. The same happens with the view 2 button. This must have something to do with how I reset the navigation stack for my other needs, but I'm not sure how to solve this. I include some minimal code to show this. Does anyone have an idea how to solve this? Thanks
struct ContentView: View {
#State var activeView: Int = 0
#State var showNavigation: Bool = false
let items = ["View1", "View2"]
var body: some View {
TabView(selection: Binding<Int> (
get: {
activeView
}, set: {
activeView = $0
showNavigation = false
}))
{
NavigationView {
HStack {
VStack {
NavigationLink(
destination: View1(), isActive: $showNavigation, label: {
Rectangle()
.fill(Color.red)
.cornerRadius(12)
.frame(width: 70, height: 70)
}).isDetailLink(false)
Text(items[0])
}
VStack{
NavigationLink(
destination: View2(), isActive: $showNavigation, label: {
Rectangle()
.fill(Color.red)
.cornerRadius(12)
.frame(width: 70, height: 70)
}).isDetailLink(false)
Text(items[1])
}
}.navigationTitle("")
.navigationBarHidden(true)
}
.tabItem {
Image(systemName: "a.circle")
Text("Main")
}
.tag(0)
View3()
.padding()
.tabItem {
Image(systemName: "b.circle")
Text("View 3")
}
.tag(1)
}
}
}
The problem seems to be that you're using the same showNavigation variable for both NavigationLinks.
I've changed the variable around a bit to hold an id and added a custom Binding to keep track of which NavigationLink should be active:
struct ContentView: View {
#State var activeView: Int = 0
#State var activeNavigationLink: Int = 0 //<-- Here
let items = ["View1", "View2"]
func navigationLinkBinding(id: Int) -> Binding<Bool> { //<-- Here
.init { () -> Bool in
activeNavigationLink == id
} set: { (newValue) in
if newValue {
activeNavigationLink = id
} else {
activeNavigationLink = 0
}
}
}
var body: some View {
TabView(selection: Binding<Int> (
get: {
activeView
}, set: {
activeView = $0
activeNavigationLink = 0 //<-- Here
}))
{
NavigationView {
HStack {
VStack {
NavigationLink(
destination: Text("View 1"), isActive: navigationLinkBinding(id: 1), label: { //<-- Here
Rectangle()
.fill(Color.red)
.cornerRadius(12)
.frame(width: 70, height: 70)
}).isDetailLink(false)
Text(items[0])
}
VStack{
NavigationLink(
destination: Text("View 2"), isActive: navigationLinkBinding(id: 2), label: { //<-- Here
Rectangle()
.fill(Color.red)
.cornerRadius(12)
.frame(width: 70, height: 70)
}).isDetailLink(false)
Text(items[1])
}
}.navigationTitle("")
.navigationBarHidden(true)
}
.tabItem {
Image(systemName: "a.circle")
Text("Main")
}
.tag(0)
Text("View 3")
.padding()
.tabItem {
Image(systemName: "b.circle")
Text("View 3")
}
.tag(1)
}
}
}

SwiftUI - Custom NavigationBar back button disables sliding gesture

I made my own back button in SwiftUI without using the navigation bar. Everything works very well. But when I switch from the first view to the second view, I cannot slide back. I shared my codes with you, my avalanche for your solution suggestions.
First View:
struct LogInView: View {
#State var showSignUpScreen = false
var body: some View {
NavigationView{
NavigationLink(
destination: SignUpView(),
isActive: self.$showSignUpScreen,
label: {
Text("")
})
Button(action: {
self.showSignUpScreen.toggle()
}, label: {
Text("Sign Up")
.fontWeight(.semibold)
.foregroundColor(.blue)
.frame(width: UIScreen.main.bounds.width * 0.3, height: UIScreen.main.bounds.height * 0.06)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.blue, lineWidth: 1.5))
.padding(.bottom, 15)
})
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
Second View:
struct SignUpView: View {
#Environment(\.presentationMode) var present
var body: some View {
NavigationView{
Button(action: {
self.present.wrappedValue.dismiss()
}, label: {
ZStack {
Image(systemName: "chevron.backward")
.font(.system(size: 25, weight: .semibold))
.foregroundColor(.blue)
.padding(.leading, 10)
}
})
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
Add this to your SignUpView:
init() {
UINavigationBar.appearance().isUserInteractionEnabled = false
UINavigationBar.appearance().backgroundColor = .clear
UINavigationBar.appearance().barTintColor = .clear
UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
UINavigationBar.appearance().shadowImage = UIImage()
UINavigationBar.appearance().tintColor = .clear
}
And delete:
.navigationBarHidden(true)
The idea:
.navigationBarHidden(true) seems to not only hide the bar but also disable sliding. So, in order to be able to slide, we can't do that. Fourtunately, we can also just hide it globally using UINavigationBar.appearance(), without the need of .navigationBarHidden(true).
Note that if anywhere else inside your app you want the default UINavigationBar back, you would have to init() again and unhide it, since this sets the appearance globally.
I would recommend having a globally accessible function that contains the code to hide and unhide it if that's the case for you.

SwiftUI NavigationLink not behaving correctly when buttons are close together

I'm trying to implement a programmable NavigationLink to go to another view, triggered by a simple button. But when that button is next to another button, instead of triggering the NavigationLink, it triggers the action of the button next to it.
struct NavLinkView: View {
#State var showPasswordStr = true
#State var showCheckView = false
#State var password = "Abc"
var body: some View {
Form {
VStack {
NavigationLink(destination: CheckView(), isActive: $showCheckView ) {
EmptyView()
}
Text("Password")
.font(.system(size: 12, weight: .light, design: .default))
.foregroundColor(.gray)
HStack {
if showPasswordStr {
TextField("", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
} else {
SecureField("", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Button(action: {
showPasswordStr.toggle()
} ) {
Image(systemName: showPasswordStr ? "eye.slash" : "eye" )
}
.padding(.leading)
Button(action: { showCheckView.toggle() } ) {
Image(systemName: "checkmark.circle" )
}
.padding(.leading)
}
}
}
}
}
What am I mising? If I move the NavigationLink to be after the VStack, then both buttons trigger both the action of the first button and the NavigationLink.
As your buttons are in a Form you need to change their style to PlainButtonStyle:
Button(action: {
self.showPasswordStr.toggle()
}) {
Image(systemName: showPasswordStr ? "eye.slash" : "eye")
}
.buttonStyle(PlainButtonStyle()) // <- add style
Button(action: { self.showCheckView.toggle() }) {
Image(systemName: "checkmark.circle")
}
.buttonStyle(PlainButtonStyle()) // <- add style