SwiftUI: use Link with onTapGesture - swiftui

I want to specify a Link in my SwiftUI view, but I also want to register when/if that Link was actually tapped or not. What's the best way to do this? I tried adding an onTapGesture on the Link (which is a View I believe):
Link("Test", destination: URL(string: "testThreePanel://")!)
.onTapGesture {
print("Testing button link tapped")
}
But the onTapGesture doesn't get invoked on tapping the link.
Is there another way to do this?

you could try this, works for me, but only on iOS15 / macOS12:
// iOS15 / macOS12 only
Link("Test", destination: URL(string: "https://duckduckgo.com")!)
.environment(\.openURL, OpenURLAction { url in
print("---> testing link actioned")
return .systemAction
})

Just enable firing both embedded and added gestures simultaneously.
struct ContentView: View {
var body: some View {
Link("Test", destination: URL(string: "testThreePanel://")!)
.simultaneousGesture(
TapGesture()
.onEnded { val in
print("Testing button link tapped \(val)")
}
)
}
}

For iOS 15 it also works if you remove the 'url' parameter, just in case you don't want to use it.
Link("Test", destination: URL(string: "https://duckduckgo.com")!)
.environment(\.openURL, OpenURLAction { _ in
print("---> testing link actioned")
return .systemAction
})

Related

SwiftUI open ShareLink from ActionSheet (confirmationDialog) Not Working

I have a ShareLink inside an ActionSheet (confirmationDialog) but it's not opening. Can anyone confirm if this is possible or not? I imagine it's something to do with the action sheet being dismissed at the same time.
.confirmationDialog("", isPresented: $isShowingMoreActionSheet, titleVisibility: .hidden) {
if let url = URL(string: "https://www.google.com") {
ShareLink("Share", item: url)
}
}

Swiftui: How to Close one Tab in a TabView?

I'm learning SwiftUI and having trouble with closing Each Tab View Element.
My App shows photos from user's album by TabView with pageViewStyle one by one.
And What I want to make is user can click save button in each view, and when button is clicked, save that photo and close only that view while other photos are still displayed. So unless all photos are saved or discarded, if user clicks save button, TabView should automatically move to another one.
However, I don't know how to close only one Tab Element. I've tried to use dismiss() and dynamically changing vm.images element. Latter one actually works, but it displays awkward movement and it also requires quite messy code. How could I solve this issue?
Here is my code.
TabView {
ForEach(vm.images, id: \.self) { image in
TestView(image: image)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
struct TestView: View {
#ObservedObject var vm: TestviewModel
...
var body: some View {
VStack(spacing: 10) {
Image(...)
Spacer()
Button {
...
} label: {
Text("Save")
}
}
You need actually to remove saved image from the viewModel container, and UI will be updated automatically
literally
Button {
vm.images.removeAll { $0.id == image.id } // << here !!
} label: {
Text("Save")
}
You need to use the selection initializer of TabView in order to control what it displays. So replace TabView with:
TabView(selection: $selection)
Than add a new property: #State var selection: YourIdType = someDefaultValue, and in the Button action you set selection to whatever you want to display.
Also add .tag(TheIdTheViewWillUse) remember that whatever Id you use must be the same as your selection variable. I recommend you use Int for the simple use.

Swipe back when using a custom navigation bar and a TabView SwiftUI

I have a SwiftUI app which uses a custom navigation bar. Because of that, I need to handle the back navigation separately (both the back button and the swipe gesture). Everything went fine up until now, when I need to use a TabView to swipe between pages. The code below illustrates what I'm trying to achieve:
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("Go to second view")
}
}
}
}
SecondView.swift
import SwiftUI
// Being able to go back by swiping
// https://stackoverflow.com/questions/59921239/hide-navigation-bar-without-losing-swipe-back-gesture-in-swiftui
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack(alignment: .leading) {
Image(systemName: "chevron.left")
.foregroundColor(Color(.systemBlue))
.font(.title2)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
.padding()
TabView {
Text("Test 1")
.tag(1)
Text("Test 2")
.tag(1)
Text("Test 3")
.tag(3)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
.background(Color(.systemGray6))
.navigationBarHidden(true)
}
}
Note that I didn't add the custom navigation bar for the second view, I've just hidden the default navigation bar, as the custom bar is not needed to solve this problem.
When the user is inside the SecondView and presses the back button, everything works as expected. The problem appears when he tries to swipe back, as the swipe back gesture is captured by the TabView. I want to keep the 'swipe-between-pages' functionality of the TabView while being able to go back to ContentView when the user swipes right from the leftmost part of the screen.
This problem only appears when using TabViews, other types of content handle the swipe back gesture without problems.
To solve this problem, I could add a horizontal padding to the TabView like this:
TabView {
// content
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.padding(.horizontal)
and then the user would have some space to swipe back, but this solution is not so good when some views inside the TabView need to take the whole width of the screen.
Is there any way to handle the swipe back gesture in this particular case? Maybe another possible solution would be customizing the TabView to ignore the drag gesture when the first view is presented and a swipe right gesture is captured (I don't know how to implement that).
I ran into this same problem. I solved it by wrapping my first tabview in with a geometryReader. When the bounds of that view is more than a quarter off the screen, I dismiss the view.

NavigationLink doesn't fire after FullScreenCover is dismissed

I have a button in a view (inside a NavigationView) that opens a full screen cover - a loading screen while some data is processing. When the cover is dismissed, I want to automatically route to the next view programmatically. I'm using a NavigationLink with a tag and selection binding, and the binding value updates when the cover is dismissed, but the routing doesn't happen unless I tap that same "open modal" button again.
import SwiftUI
struct OpenerView: View {
#EnvironmentObject var viewModel: OpenerViewModel
#State private var selection: Int? = nil
#State private var presentLoadingScreen = false
var body: some View {
VStack {
NavigationLink(destination: SecondScreen(), tag: 1, selection: $selection) { EmptyView() }
Button(action: {
viewModel.frequency = 0
self.presentLoadingScreen.toggle()
}, label: {
Text("Open cover")
}).buttonStyle(PlainButtonStyle())
}
.navigationBarTitle("Nav title", displayMode: .inline)
.fullScreenCover(isPresented: $presentLoadingScreen, onDismiss: {
self.selection = 1
}, content: ModalView.init)
}
}
struct ModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Howdy")
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
presentationMode.wrappedValue.dismiss()
}
}
}
}
The first time I hit the Button, the cover opens. Inside the cover is just a DispatchQueue.main.asyncAfter which dismisses it after 2 seconds. When it's dismissed, the onDismiss fires, but then I have to hit the button again to route to SecondScreen.
Any ideas?
Edit: Added the modal's View
I got it working with some changes to the code, and I'm sharing here along with what I think is happening.
I believe the problem is a race condition using a #State boolean to toggle the cover and the navigation. When the cover is being dismissed, my main OpenerView is being recreated - to be expected with state changes. Because of this, I try to set the #State var selection to trigger the navigation change, but before it can do so, the view is recreated with selection = nil.
There seem to be two ways to solve it in my case:
Move the cover boolean to my view model - this worked, but I didn't want it there because it only applied to this view and it's a shared view model for this user flow. Plus, when the modal is dismissed, you see the current OpenerView for a brief flash and then get routed to the SecondScreen.
Keep the cover boolean in #State, but trigger the navigation change in the button immediately after setting the boolean to open the modal. This worked better for my use case because the modal opens, and when it closes, the user is already on the next screen.
I had a similar problem where I was trying to draw a view after dismissing a fullScreenCover. I kept getting an error that said that the view had been deallocated since it was trying to draw to the fullScreenCover.
I used Joe's hints above to make this work. Specifically:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
viewToShow()
}
I had previously tried onChange, onDisappear, onAppear - but none of those fit the use case I needed.

Is it possible to make a modal non-dismissible in SwiftUI?

I am creating an App where the login / register part is inside a modal, which is shown if the user is not logged in.
The problem is, that the user can dismiss the modal by swiping it down...
Is it possible to prevent this?
var body: some View {
TabView(selection: $selection) {
App()
}.sheet(isPresented: self.$showSheet) { // This needs to be non-dismissible
LoginRegister()
}
}
Second example:
I am using a modal to ask for information. The user should not be able to quit this process except by dismissing the modal with save button. The user has to input information before the button works. Unfortunately the modal can be dismissed by swiping it down.
Is it possible to prevent this?
iOS 15 and later:
Use .interactiveDismissDisabled(true) on the sheet, that's all.
Prev iOS 15:
You can try to do this by using a highPriorityGesture. Of course the blue Rectangle is only for demonstration but you would have to use a view which is covering the whole screen.
struct ModalViewNoClose : View {
#Environment(\.presentationMode) var presentationMode
let gesture = DragGesture()
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 300, height: 600)
.highPriorityGesture(gesture)
.overlay(
VStack{
Button("Close") {
self.presentationMode.value.dismiss()
}.accentColor(.white)
Text("Modal")
.highPriorityGesture(gesture)
TextField("as", text: .constant("sdf"))
.highPriorityGesture(gesture)
} .highPriorityGesture(gesture)
)
.border(Color.green)
}
}
This is a common problem and a "code smell"... well not really code but a "design pattern smell" anyway.
The problem is that you are making your login process part of the rest of the app.
Instead of presenting the LoginRegister over the App you should really be showing either App or LoginRegister.
i.e. you should have some state object like userLoggedIn: Bool or something and depending on that value you should show either App or LoginRegister.
Just don't have both in the view hierarchy at the same time. That way your user won't be able to dismiss the view.
If you dont mind using Introspect:
import Introspect
#available(iOS 13, *)
extension View {
/// A Boolean value indicating whether the view controller enforces a modal behavior.
///
/// The default value of this property is `false`. When you set it to `true`, UIKit ignores events
/// outside the view controller's bounds and prevents the interactive dismissal of the
/// view controller while it is onscreen.
public func isModalInPresentation(_ value: Bool) -> some View {
introspectViewController {
$0.isModalInPresentation = value
}
}
}
Usage:
.sheet {
VStack {
...
}.isModalInPresentation(true)
}
iOS 15+
Starting from iOS 15 you can use interactiveDismissDisabled.
You just need to attach it to the sheet:
var body: some View {
TabView(selection: $selection) {
App()
}.sheet(isPresented: self.$showSheet) {
LoginRegister()
.interactiveDismissDisabled(true)
}
}
Regarding your second example, you can pass a variable to control when the sheet is disabled:
.interactiveDismissDisabled(!isAllInformationProvided)
You can find more information in the documentation.
theoretically this may help you (I didn't tryed it)
private var isDisplayedBind: Binding<Bool>{ Binding(get: { true }, set: { _ = $0 }) }
and usage:
content
.sheet(isPresented: isDisplayedBind) { some sheet }