How to disable Flashing when tapping PresentationLink (SwiftUI)? - ios

How can I disable the highlighted color when a button is tapped?
Now when I tap it, it gets gray and the action gets called, but I want to disable it.
Is it possible at the moment?
PresentationLink(destination: NextView()) {
....
}

PresentationView does not seem to have a way of styling the button, and I doubt it'll ever will. However, there are other methods to present a view. Below you have an example that will avoid the effect. It is a little more verbose, but it will serve your purpose.
As of beta3, modals seem to have a bug, and the onDismiss method is never called. So it is hard to reset the isPresented variable properly. In the meantime, I use a workaround for that. Check this answer for that: https://stackoverflow.com/a/56939555/7786555
struct ContentView : View {
#State var isPresented = false
var body: some View {
VStack(spacing: 30) {
// Option #1, with blink
PresentationLink(destination: NextView(), label: {
Text("Click to show")
})
// Option #2, without blink
Text("Click to show").color(.blue).tapAction { self.isPresented = true }
.presentation(isPresented ? Modal(NextView()) : nil)
}
}
}
struct NextView: View {
var body: some View {
Text("aloha!")
}
}

Related

How to make toggle switch always ON in swift?

I want to make the state of toggle switch to ON always, even if user tries to make it OFF, it should not change. I tried to use .isUserInterstionEnabled = .false, but that didn't work. Can somebody help me on this? Thank you in advance
Try using toggle.isEnabled = false
1.Make the switch outlet in the view controller.
2.Create IBAction of switch and set-:
self.swithCtrl.setOn(true, animated: false)
User will try to disable it but it will remain enable.
There are two ways that I see you can achieve what you want.
Disable it, using the modifier .disabled() (the user will see it slightly faded):
struct Example: View {
#State private var isOn = true
var body: some View {
VStack {
Toggle("Text of toggle", isOn: $isOn)
.disabled(true)
}
}
}
Force it to go back to on, using the modifier .onChange(of:):
struct Example: View {
#State private var isOn = true
var body: some View {
VStack {
Toggle("Text of toggle", isOn: $isOn)
}
.onChange(of: isOn) { _ in
isOn = true
}
}
}

How to use UIAccessibility.post(notification: .layoutChanged, argument: nil) in SwiftUI to move focus to specific view

I have one button on screen and on that button tap it opens one modal (view). but after closing that view, the focus of accessibility voice over goes on top of screen. In UIKit we can use UIAccessibility.post(notification: .layoutChanged, argument: nil) and pass reference as an argument. But how can we achieve this same behaviour in SwiftUI.
How I managed this was using a .accessibilityHidden wrapper on the very top level of the parent view and then used a #State variable as the value to pass into accessibilityHidden. This way the parent view is ignored while the modal is showing. And then reintroduced into the view once the modal is closed again.
struct MainView: View {
#State var showingModal = false
var body: some View {
VStack {
Button(action: {
showingModal = true
}, label: {
Text("Open Modal")
})
.fullScreenCover(isPresented: $showingModal, onDismiss: {
print("Focus coming back to main view")
} content: {
Modal()
})
}
.accessibilityHidden(self.showingModal)
}
}
struct Modal: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("Focus will move here")
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Close Modal to Refocus Back")
}
}
}
}
You can also chain multiple modal / alerts as long at you have #State values to handle the changes to them so the focus moves properly
.accessibilityHidden(self.showingModel1 || self.showingModel2 || self.showingAlert1 || self.showingAlert2)
I know this question is really old, but I literally just was handling this and thought if someone else stumbled onto this question there would be an answer here.

SwiftUI: How to ignore taps on background when menu is open?

I am currently struggling to resolve a SwiftUI issue:
In a very abstract way, this is how the code of my application looks like (not the actual code to simply things for the discussion here):
struct SwiftUIView: View {
#State private var toggle: Bool = true
var body: some View {
VStack {
Spacer()
if toggle {
Text("on")
} else {
Text("off")
}
Spacer()
Rectangle()
.frame(height: 200)
.onTapGesture { toggle.toggle() }
Spacer()
Menu("Actions") {
Button("Duplicate", action: { toggle.toggle() })
Button("Rename", action: { toggle.toggle() })
Button("Delete", action: { toggle.toggle() })
}
Spacer()
}
}
}
So what's the essence here?
There is an element (rectangle) in the background that reacts to tap input from the user
There is a menu that contains items that also carry out some action when tapped
Now, I am facing the following issue:
When opening the menu by tapping on "Actions" the menu opens up - so far so good. However, when I now decide that I don't want to trigger any of the actions contained in the menu, and tap somewhere on the background to close it, it can happen that I tap on the rectangle in the background. If I do so, the tap on the rectangle directly triggers the action defined in onTapGesture.
However, the desired behavior would be that when the menu is open, I can tap anywhere outside the menu to close it without triggering any other element.
Any idea how I could achieve this? Thanks!
(Let me know in the comments if further clarification is needed.)
You can implement an .overlay which is tappable and appears when you tap on the menu.
Make it cover the whole screen, it gets ignored by the Menu.
When tapping on the menu icon you can set a propertie to true.
When tapping on the overlay or a menu item, set it back to false.
You can use place it in your root view and use a viewmodel with #Environment to access it from everywhere.
The only downside is, that you need to place isMenuOpen = false in every menu button.
Apple is using the unexpected behaviour itself, a.ex in the Wether app.
However, I still think it's a bug and filed a report. (FB10033181)
#State var isMenuOpen: Bool = false
var body: some View {
NavigationView{
NavigationLink{
ChildView()
} label: {
Text("Some NavigationLink")
.padding()
}
.toolbar{
ToolbarItem(placement: .navigationBarTrailing){
Menu{
Button{
isMenuOpen = false
} label: {
Text("Some Action")
}
} label: {
Image(systemName: "ellipsis.circle")
}
.onTapGesture {
isMenuOpen = true
}
}
}
}
.overlay{
if isMenuOpen {
Color.white.opacity(0.001)
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
isMenuOpen = false
}
}
}
}
It's not amazing, but you can manually track the menu's state with a #State var and set this to true in the .onTap for the Menu.
You can then apply .disabled(inMenu) to background elements as needed. But you need to ensure all exits out of the menu properly set the variable back to false. So that means a) any menu items' actions should set it back to false and b) taps outside the menu, incl. on areas that technically are "disabled" also need to switch it back to false.
There are a bunch of ways to achieve this, depending on your view hierarchy. The most aggressive approach (in terms of not missing a menu exit) might be to conditionally overlay a clear blocking view with an .onTap that sets inMenu back to false. This could however have Accessibility downsides. Optimally, of course, there would just be a way to directly bind to the menu's presentationMode or the treatment of surrounding taps could be configured on the Menu. In the meantime, the approach above has worked ok for me.
I think I have a solution, but it’s a hack… and it won’t work with the SwiftUI “App” lifecycle.
In your SceneDelegate, instead of creating a UIWindow use this HackedUIWindow subclass instead:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = HackedWindow(windowScene: windowScene) // <-- here!
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
}
class HackedUIWindow: UIWindow {
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if type(of: subview) == NSClassFromString("_UIContextMenuContainerView") {
if let rootView = self.rootViewController?.view {
rootView.isUserInteractionEnabled = false
}
}
}
override func willRemoveSubview(_ subview: UIView) {
super.willRemoveSubview(subview)
if type(of: subview) == NSClassFromString("_UIContextMenuContainerView") {
if let rootView = self.rootViewController?.view {
rootView.isUserInteractionEnabled = true
}
}
}
}
The subclass watches for subviews being added/removed, looking for one of type _UIContextMenuContainerView that’s used by context menus. When it sees one being added, it grabs the window’s root view and disables user interaction; when the context menu is removed, it re-enables user interaction.
This has worked in my testing but YMMV. It may also be wise to obfuscate the "_UIContextMenuContainerView" string so App Review doesn’t notice you referencing a private class.
You can have the behavior you want by using a Form or a List instead of a "plain view". All buttons will then be disabled by default when the menu is on screen, but they need to be buttons, and only one per cell, it won't work with a tapGesture because what you are actually doing is tapping on the cell, and SwiftUI is disabling TableView taps for you.
The key elements to achieve this are:
Use a Form or a List
Use an actual Button. In your example you use a Rectangle with a tapGesture.
I modified the code you provided and if you open the menu you can't hit the button:
struct SwiftUIView: View {
#State private var toggle: Bool = true
var body: some View {
VStack {
Spacer()
if toggle {
Text("on")
} else {
Text("off")
}
Spacer()
/// We add a `List` (this could go at whole screen level)
List {
/// We use a `Button` that has a `Rectangle`
/// rather than a tapGesture
Button {
toggle.toggle()
} label: {
Rectangle()
.frame(height: 200)
}
/// Important: Never use `buttonStyle` or the
/// default behavior for buttons will stop working
}
.listStyle(.plain)
.frame(height: 200)
Spacer()
Menu("Actions") {
Button("Duplicate", action: { toggle.toggle() })
Button("Rename", action: { toggle.toggle() })
Button("Delete", action: { toggle.toggle() })
}
Spacer()
}
}
}
Bonus:
Bonus: Don't use a buttonStyle. I lost so many hours of code because of this and I want to share it here too. In my app all buttons have a buttonStyle. It turns out that by using a style, you remove some of the behaviors you get by default (like the one we are discussing).
Instead of using a buttonStyle use an extension like this:
extension Button {
func withRedButtonStyle() -> some View {
self.foregroundColor(Color(UIColor.primary.excessiveRed))
.font(Font(MontserratFont.regular.fontWithSize(14)))
}
}
And add the withRedButtonStyle() at the end of the button.
In my case an alert was prevented from showing in a similar scenario, conflicting with the Menu as well as Datepicker. My workaround was using a slight delay with DispatchQueue.
Rectangle()
.frame(height: 200)
.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05){
toggle.toggle()
}
}
The only real solution will happen when Apple fixes/refines SwiftUI regarding Menu (and Datepicker) behaviours.

SwiftUI: How to make NavigationLink not go anywhere and stay on current view

I am doing a bit of form validation in my app, and so I dont want the navigation link to go to the specified destination unless certain conditions are met. So is it possible to somehow set the destination to nil or similar so that it stays on the current view if conditions are not met when the navigation link is pressed?
Use Button instead and activate hidden NavigationLink programmatically if validated. Here is an example
struct DemoView: View {
#State private var isValid = false
var body: some View {
NavigationView {
Button("Validate") {
// some validate code here like
self.isValid = self.validate()
}
.background(
NavigationLink(destination: Text("Destination"), isActive: $isValid) { EmptyView() }
)
}
}
private func validate() -> Bool {
return true
}
}

SwiftUI: Button in NavigationBar won't fire after modal dismissal

I'm running into some weird behavior, trying to get a simple modal to pop after it has been dismissed.
I have an Add button in the NavigationBar that pops a modal. The modal has a button that will dismiss it, which works. However, I cannot interact with the Add button in the NavigationBar again until I interact with something else on the screen, such as scrolling the List below.
I have also placed another Add button, just for kicks, in the List itself, which always works.
Here's the code for the main view:
import SwiftUI
struct ContentView: View {
#State var displayModal: Bool = false
var body: some View {
NavigationView {
List {
Text("Hello again.")
Button(action: { self.displayModal = true }) {
Text("Add")
}
}
.sheet(isPresented: $displayModal) {
Modal(isPresented: self.$displayModal)
}
.navigationBarTitle("The Title")
.navigationBarItems(trailing: Button(action: { self.displayModal = true }) {
Text("Add")
})
}
}
}
And the modal, for completeness:
import SwiftUI
struct Modal: View {
#Binding var isPresented: Bool
var body: some View {
VStack {
HStack {
Button(action: {
self.isPresented = false
}) {
Text("Cancel")
}
.padding()
Spacer()
}
Text("I am the modal")
Spacer()
}
}
}
The only thing I can think of is that something invisible is preventing me from working with the NavigationBar button. So I fired up the UI Debugger, and here's what the ContentView looks like. Note the NavigationBar button.
Now, after I tap the button and display the modal, and then use the UI Debugger to see the ContentView again, all the same elements are in place, but the Button parent views are offset a bit, like this:
Once I drag the List up and down, the UI Debugger shows a view hierarchy identical to the first image.
Does anyone have any idea what's going on here?
I'm using Xcode 11.2.1 and iOS 13 on an iPhone 11 Pro simulator, but have also observed this on my iPhone.
It is really a bug. The interesting thing is that after 'drag to dismiss' the issue is not observed, so it is a kind of 'sync/async' state changing or something.
Workaround (temporary of course, decreases visibility almost completely)
.navigationBarItems(trailing: Button(action: { self.displayModal = true }) {
Text("Add").padding([.leading, .vertical], 4)
})
I ran into the same issue, and for me the workaround was to use an inline-style navigation bar title on the presenter.
.navigationBarTitle(Text("The Title"), displayMode: .inline)
HOWEVER, if you use a custom accent color on your ContentView (like .accentColor(Color.green)), this workaround no longer works.
Edit: the bug seems fixed in 13.4, and no workarounds are needed anymore.

Resources