Actionsheet in NavigationView seems have bug - swiftui

I do some tests. If I just make a button like this:
Button(action: {
self.showActionSheet = true
}) {
Text("Click")
}.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("This is a test"))
}
It works!
But if I put it in NavigationView, the bug appears! The ActionSheet will pop up again when I clicked the Cancel.

In SwiftUI the view is a function of the state, so if your flag that is linked to a modal, an action sheet or an alert appearing is not set back to false the overlay will keep on being presented. When the user dismisses the action sheet, your ContentView gets redrawn and because showActionSheet is still true it gets shown again.
ActionSheet
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("This is a test"),
buttons: [ActionSheet.Button.cancel({
self.showActionSheet = false })])
}
Modal
.sheet(item: $showModal,
onDismiss: {
self.showModal = false
}) { Text("Modal") }

I post my question on Swift Forum and receive an answer.
https://forums.swift.org/t/actionsheet-and-modal-seems-have-bug-in-navigationview/29590
If I put the ActionSheet or Modal outside NavigationView, it works perfectly!

Related

Dismiss Completion SwiftUI

I'm showing a fullScreenCover with SwiftUI. Under certain flows, I want to dismiss the cover and show another from the Root ContentView. When I call
#Environment(\.dismiss) var dismiss
Button {
dismiss()
NotificationCenter.default.post(name: NSNotification.showNewScreen, object: nil, userInfo: nil)
} label: {
Text("This is a test")
}
I get the error:
[Presentation] Attempt to present xxx from yyy which is already presenting zzz
UIKit had the helpful completion handler so I could wait until the ViewController was dismissed before adding logic:
func dismiss(
animated flag: Bool,
completion: (() -> Void)? = nil
)
Is there something similar with SwiftUI? My current workaround is to add a delay to avoid presenting on the same view... which feels wrong.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
NotificationCenter.default.post(name: NSNotification.showNewScreen, object: nil, userInfo: nil)
}
It's not pretty to mess with modals in this way, but if you want to present the next modal from the presenting view then you can pass a binding isPresented and use the onDismiss argument for the fullScreenCover view modifier:
.fullScreenCover(isPresented: $isPresented, onDismiss: DispatchQueue.main.async { presentNextThingy }) { firstThingyContent }
or my preference use a binding that when changed will dismiss and load the next modal view, if needed. This allows the first view to determine what is presented when it is dismissed by changing the binding directly.
.fullScreenCover(item: $thingyBeingPresented) { thingy in
content(for: Thingy)
}

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.

Lingering popover causes a problem with alerts

The code below has 2 buttons, one for an alert, and one for a popup. The problem is that if you bring up the popup, then click the alert, you don't see an alert. Even after "clearing" the popup, you will never see an alert. However, if you bring up the popup, and clear it, then click the alert, that works fine. This is all on an ipad simulator as that is where a popup is sized according to content (so it can share the UI with many elements).
Of course, this is a simplified example. In my real code the button that brings up the alert is nowhere near the one for bringing up the popup (both UI-wise, and code-wise).
It's like i need some sort of global function that says something like "clear all popups" or something? Or maybe use something other than popover? I don't even know if this is a "bug" or a "feature"..
struct ContentView: View {
#State private var showPopover = false
#State private var showingAlert = false
var body: some View {
VStack {
Spacer()
Button {
showingAlert = true
} label: {
Text("Click to show alert")
}
.alert("Alerted!", isPresented: $showingAlert) {
Button("OK", role: .cancel) {}
}.padding()
Spacer()
Button("Click to show popover", action: {
self.showPopover = true
})
.popover(isPresented: $showPopover) {
Text("Popup Text")
}
Spacer()
}
}
}
I also see these messages in the console while all this is going on.
2022-04-06 12:43:28.494902-0700 TestBee[39557:16837171] [UIFocus] Failed to update focus with context <UIFocusUpdateContext: 0x600001440b40: previouslyFocusedItem=(null), nextFocusedItem=(null), focusHeading=None>. No additional info available.

SwiftUI Mac: Adding buttonStyle Makes Button Click Propagate to Parent

I am trying to create a situation in a SwiftUI Mac app where when I click a Button inside a parent view, only the Button's action is trigger—not any of the tap gestures attached to its parent.
Here is a simple example that reproduces the problem:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 30){
Button("Button"){
print("button clicked")
}
.padding(5)
.background(Color.blue)
.buttonStyle(PlainButtonStyle())
}
.frame(width: 500, height: 500)
.background(Color.gray)
.padding(100)
.gesture(TapGesture(count: 2).onEnded {
//Double click (open message in popup)
print("double click")
})
.simultaneousGesture(TapGesture().onEnded {
if NSEvent.modifierFlags.contains(.command){
print("command click")
}else{
print("single click")
}
})
}
}
Clicking the button triggers both button clicked and single click.
If you comment out the buttonStyle...
//.buttonStyle(PlainButtonStyle())
It works how I want it to. Only button clicked is fired.
It doesn't seem to matter which button style I use, the behavior persists. I really need a custom button style on the child button in my situation, so how do I get around this?
If you replace your .simultaneousGesture with a regular .gesture it works for me – and still recognizes the outer single and double taps.

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.