Disabling user interaction in a SwiftUI view without changing its appearance - ios

I have a custom view InteractiveView that allows user interaction. I want to show a thumbnail of that view on an overview page (inside a NavigationLink) so the user can tap it to navigate to the the fullscreen view.
For that reason, I need the InteractiveView to be non-interactive (i.e. disabled) when it's displayed as a thumbnail. I implemented this as follows:
NavigationLink {
InteractiveView(viewModel)
} label: {
InteractiveView(viewModel)
.disabled(true)
}
This works as intended (i.e. tapping the view does not interact with the view but performs the navigation to the fullscreen interactive view instead).
However, the disabled(true) modifier also changes the InteractiveView's appearance: All its subviews are faded out, i.e. their opacity is reduced and they appear semi-transparent. I understand that this is usually what I want as it signals to the user that the view is disabled and I cannot interact with it. But in my case, the user can interact with it as they can tap on it in order to show the fullscreen view.
Question:
How can I disable the InteractiveView while keeping its original appearance (without the fade-out effect)?
Or: Is there a better way to disable all controls in a view without changing their appearance?
Update (Additional Information)
Many answers to this question suggest using hit testing with .allowsHitTesting(false) instead of .disabled(true) in the code above. This works indeed in terms of navigation, but it violates another requirement specified in the question: namely, the "without changing its appearance" part.
Why hit testing doesn't work
A NavigationLink always changes the foreground color of its label view to blue, thus it modifies the label view's appearance. I solved this problem by using a PlainButtonStyle on the NavigationLink:
NavigationLink {
InteractiveView(viewModel)
} label: {
InteractiveView(viewModel)
.allowsHitTesting(false)
}
.buttonStyle(PlainButtonStyle()) // prevent change of foreground color
With this button style, the navigation link breaks when I add the .allowsHitTesting(false) modifier: The navigation link's content view doesn't intercept touches anymore, but the navigation link itself also doesn't receive (or handle) those touches. And that's the problem:
I need the correct (normal) navigation link behavior without the typical navigation link highlighting.

Seems like allowsHitTesting is what you may be after.
https://stackoverflow.com/a/58912816/1066424

NavigationLink {
InteractiveView()
} label: {
InteractiveView()
.allowsHitTesting(false)
}

Related

Button images which are part of a Container View appear before that view is supposed to animate in

I've implemented a custom dialog box in a game (showing options when the game is paused) by using a Container View in the main game's ViewController.
That Container View has a constraint to be centered vertically and I'm using that constraint to animate this custom dialog box.
The dialog box itself is an image of a wooden board on a pole with 4 buttons, each being an image I prepared. These buttons are arranged in a vertical Stack View which contains 2 horizontal Stack Views, each with 2 buttons, so they will be laid out nicely symmetrical.
All of the above is done in Interface Builder. So a segue was automatically added from the game's main ViewController to the new Pause Dialog View Controller.
In my game's main ViewController I move the Container View out of view by adding the following to my viewDidLoad():
dialogBoxYConstraint.constant -= (view.bounds.height)
Then when the user clicks on a PAUSE button which should show this dialog, the following code is running:
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseInOut) {
self.dialogBoxYConstraint.constant += (self.view.bounds.height)
self.view.layoutIfNeeded()
}
}
So this code will bring the constraint's constant back to its original location and it will show the dialog box that I put inside the Container View.
When the user clicks on the PAUSE button all of this indeed happens and there's a nice animation with the wooden board and all 4 buttons fall into place and all buttons are clickable. Here's an image (disregard the small white buttons, these are temporary work-in-progress):
But, before that, before I click on the PAUSE button, I always see part of the buttons all the way on top. I see the lower 2 buttons completely and a bit of those above as in the following image:
As you can see, the wooden board isn't here, only the buttons, and when I do press the PAUSE button everything together correctly animates to the right place as in the 1st image.
(It's an AR app so you basically see my walls in the background, but that's irrelevant for this question).
Moreover, when the buttons are on top, they are not clickable.
Also, it doesn't matter if I change the constraint's constant to be even higher, say I do this:
dialogBoxYConstraint.constant -= (view.bounds.height + 500)
the buttons will always show at the same place.
And if I try to put this line in viewDidAppear then I can see the whole board with the buttons as in the first image for a second on a black background and then I get what you see in the 2nd image, which makes sense.
The above happens whether or not I've implemented the prepare(for segue: )
The segue itself is actually happening immediately as the main view is loaded, which is why I had to initially move it out of view.
As a test, I tried to set one button's isHidden to true in the Pause Dialog View Controller and then set it to false in the prepare(for segue: ), thinking that maybe that would do something, but the button remained hidden all the time.
(Side question: how should I perform such changes in this child View Controller only after the user presses the PAUSE button? Since the segue happens already from the start, I don't understand how to control such changes only later on by user action?)
I'm not sure what I'm doing wrong. Looks like a glitch, but, as always, I guess it's something I did.
I had assumed that moving the view's constraint should always move all of its contents together.
Anyone has any idea why the buttons are always there at the top?
I saw that there is a present(_:animated:completion:) method to present VC's. Should I be looking into this instead of animating the constraint as I did???
Posting this as an answer, as per the OP's comments...
It can be very difficult to debug layouts when UI element are "clear." Giving them background colors allows you to easily see the framing at run-time.
Also, Debug View Hierarchy can be very helpful, because you can inspect constraints (and even see hidden or out-of-bounds elements). Note that you may need to temporarily disable certain features - such as playing video or an active AR scene.

SwiftUI custom tab bar: how to switch views when user drags finger across tab bar buttons?

I have developed a custom tab bar (ready to paste playground source), but now, in addition to user tapping the buttons to switch views, I would like to be able to swipe finger over the tab bar, and have views activated as I'm dragging over the tab bar buttons.
I tried listening on DragGesture(minimumDistance: 0) on individual buttons.
This helps in activating the view on touch down instead of touch up (which is how default Button works), but will only activate the button where user started dragging.
I assume I would somehow need to add .simultaneousGesture(DragGesture(minimumDistance: 0) to the whole tab bar, and I could then probably interpret the touch coordinates against individual button hit tests.
However, this doesn't feel like the SwiftUI way - is there an easier way to let the SwiftUI do the heavy lifting?
(Please note - in playground, I now get AttributeGraph: cycle detected through attribute 2584 notices, and the actual selection lags one tap behind the last tapped button for some reason, but it works okay in Xcode project.)
I don't know if it's a best solution, but I:
Saved a frame in global coordinates in each button
Used DragGesture(minimumDistance: 0, coordinateSpace: .global) on a tab bar, to manually hit test each button
And disabled hit testing on individual buttons with .allowsHitTesting(false), otherwise they would 'steal' the gesture.
Here's a diff of changes

Direct interaction (when VoiceOver is active) inside "allowsDirectInteraction" marked View does not work

Edit: Just noticed, that sometimes, it works, and sometimes not. And I do not know why.
I am making an App made mostly for blind, VoiceOver will probably be active. In one view I need to make my interactions and gestures myself. In there I am trying to make a zone that is directly interactable, so that the functionallity behind it works like there is no Voiceover active, even when it is. But when I do this, instead of printing text on double tap, VoiceOver always tells: "Zone direct interaction", or something similar (The testdevice is not set to english).
Does anyone has an idea what the problem could be?
This is my View:
struct MyView: View {
var body: some View {
TestView()
.accessibilityAddTraits(.allowsDirectInteraction)
}
}
And that is the TestView:
struct TestView: View {
var body: some View {
Rectangle()
.onTapGesture(count: 2) { print("A View was tapped") }
.onAppear { print("A View was created") }
}
}
When having a view that uses up all the space it cannot be made directly interactable as default as it seems. The user needs to do this himself using the rotor or the settings:
Settings App/Accessibility/VoiceOver/Rotor Actions/Direct Touch Apps/
When the App is checked in that setting the view marked with the trait .allowDirectInteraction does work as if VoiceOver is not active, though this should only be used for elements that are accessible on their own, and not as easy way to avoid making the app accessible for those parts!

SwiftUI: .fileImporter modifier not updating binding when dismissed by tapping "outside" the file picker

I am using the ".fileImporter" modifier in a SwiftUI view; it works great, except for one very annoying issue that I have not been able to find a solution for anywhere.
The "picker", which is bound to a state variable showFileImporter as required by the documentation (initially set to false), and configured to allow multiple selection, is presented by "toggling" the state variable when the user taps a button. Up to this point, all is good: the picker presents, I can effectively pick files (and get the corresponding URLs collection onCompletion), which in turn dismisses the picker view and sets the bound state variable showFileImporter back to false, as it should.
Also, if after presenting the picker the user taps the Cancel button on the view UI, the picker is appropriately dismissed and the bound variable set to false- again, as it should.
The issue comes when you dismiss the picker by tapping outside the picker view.
The picker gets dismissed, alright, BUT the bound variable DOES NOT get updated - thus, in my case, the user has to tap the "pick files" button twice in order for the picker to preset itself, obviously because when dismissed in this way the value of the showFileImporter state variable is not updated and the toggle action on it triggered by tapping the button will set it to false "first", and then to true on the second tap, which then triggers the picker presentation.
Anyone has any idea if this is a bug? Am I doing something inherently wrong, or missing something? Just for completion, I am including a sample code that exhibits this behaviour:
struct ContentView: View {
#State private var showFileImporter: Bool = false
var body: some View {
VStack {
Button("Pick Some Files...") {
showFileImporter.toggle()
}
}
.fileImporter(
isPresented: $showFileImporter,
allowedContentTypes: [.video, .audio, .image],
allowsMultipleSelection: true,
onCompletion: { result in
/* ...some code here */
})
}
}
Any pointers, hints or help at all is greatly appreciated, so thanks in advance.
This is a certified bug (confirmed by Apple). Hopefully will be solved soon.

Swift UI List: Open external URL when cell is tapped

I am trying to open a URL when one of the List's cells is tapped. I tried adding the modifier onTapGesture to the cell itself and then calling UIApplication.shared.open(url), but this only works if the tap is right on the cell view's elements (and not on the cell's background).
I also tried to add a background view (Rectangle) to the cell with opacity 0.01, but although this works the Rectangle is quite visible despite its low opacity.
Is there any workaround to make the whole row tappable?
Found a solution, here it is in case it helps anyone in the future:
// edit:
Used a single Button, with the required action (i.e. openURL in my case) in the action closure, and with my custom view returned in the label closure.
#stakri, What you're looking for is ".contentShape()"
Rectangle()
.stroke()
.onTapGesture() {
UIApplication.shared.open(URL(string: "https://stackoverflow.com")!)
}
above code will only work if you tap on the 'stroke' outline of the rectangle, but .contentShape() will make the entire area tappable without the need to nest it inside of a Stack:
Rectangle()
.stroke()
.contentShape( Rectangle() )
.onTapGesture() {
UIApplication.shared.open(URL(string: "https://stackoverflow.com")!)
}
Others coming to this question will likely be looking for how to specifically open a URL, you'll notice I force unwrapped my URL. Open to feedback on that, but barring any issues brought up, this works well.

Resources