Button remains tappable after removing from view hierarchy - SwiftUI - swift

I'm trying to show and hide a view based on a certain state. But even after that view is removed from the hierarchy, it still remains tappable for a few moments, leading to phantom button presses. This is occurring only in iOS 16 to my knowledge.
Note that this only occurs when using the .zIndex modifier, which I need in order to transition the view out smoothly. The bug occurs with or without a transition modifier, however.
Minimum working example (tap the show button, then tap the hide button multiple times. If it worked correctly, the hide button handler should only trigger once, since it is removed from the hierarchy. In reality it can be triggered many times)
struct ContentView: View {
#State var show = false
var body: some View {
ZStack {
Button {
print("show")
show = true
} label: {
Text("show")
.foregroundColor(.white)
.padding()
.background(Color.blue.cornerRadius(8))
}
if show {
Button {
// this can be triggered multiple times if you tap fast
print("hide")
show = false
} label: {
Text("hide")
.foregroundColor(.white)
.padding(64)
.background(Color.red.cornerRadius(8))
}
.zIndex(1) // if we remove the zindex, it won't happen. but then we lose the ability to transition this view out.
}
}
}
}
Has anyone else experience this bug? I don't know a workaround besides removing zIndex, is there a way to fix it without losing transitions?
I filed a feedback for this FB11753719

Related

The EditButton for NavigationView is not working properly for iPad ( But correct for iPhone)

I am new in SwiftUI developing, In a SwiftUI project, I created a list of items then I followed the tips in this link to enabled edit button for this,
https://developer.apple.com/documentation/swiftui/editbutton
It works properly for iPhone interface, But in iPad, It has a very strange behaviour. Look at this video below to see how it works in iPad.
https://imgur.com/a/0CLkqiz
If you check this, the way it shows in iPad, when the Edit button text wants to turn to Done, the text steps forward and also creates a transparency with done and edit while it works properly and smooth and fixed in iPhone. I wonder if there might be any special settings for iPad interface in SwiftUI that would fix this problem.
This is my code for this:
struct QRCreator: View {
#State var showingCreateView = false
#State public var fruits = [
"Apple",
"Banana",
"Papaya",
"Mango"
]
var body: some View {
NavigationView {
List {
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
}
.onDelete { fruits.remove(atOffsets: $0) }
.onMove { fruits.move(fromOffsets: $0, toOffset: $1) }
}
.navigationTitle("Fruits2")
.navigationBarItems(trailing:
Button(action: {
showingCreateView = true
}){
Image(systemName: "plus.viewfinder")
.font(.largeTitle)
}
)
.toolbar {
EditButton()
}
}
}
and here is ContentView which I define three tabs in it like this
TabView
{
NavigationView{
QRScanner()
}
.tabItem
{
Image(systemName: "qrcode.viewfinder")
Text("Scanner")
}
NavigationView {
QRCreator()
}
.tabItem
{
Image(systemName: "doc.fill.badge.plus")
Text("Creator")
}
NavigationView
{
QRSetting()
}
.tabItem
{
Image(systemName: "gear")
Text("Setting")
}
}
There seem to be two layers at play:
From the screen recording, it would appear that your NavigationView is actually wrapped by another NavigationView (or NavigationStack/SplitView) somewhere further up the view hierarchy in your implementation. Besides the odd layout, this also creates a tricky situation in regards to toolbar items like your buttons, and the EditMode environment value that EditButton manipulates.
There is an iPad-specific animation bug in SwiftUI's implementation of EditButton. When clicked with a mouse/trackpad as in your screen recording, the button briefly shows both labels ("Edit" & "Done") at the same time. This doesn't happen when you tap the button directly.
It is only when issues 1 & 2 collide, that I actually run into the more problematic behavior that you've captured: the button jumps and the list also jumps.
If I keep everything as you have shown it (including the doubled-up NavigationViews), but tap the button instead of clicking it with a cursor, things seem fine (although I would expect other possible issues down the road).
If I get rid of the outer NavigationView, but click the button, the button itself still exhibits a slightly odd animation, but it is nowhere near as bad as before. And most importantly, the list animates and behaves correctly.
I tried a couple of approaches to work around the button's remaining animation bug, but nothing short of re-implementing a custom edit button worked.
PS: I know you might've already come across this, but since you said that you're just starting out with iOS 16 introduced new views and APIs for navigation (and in typical fashion for SwiftUI's documentation, older pages like the one for EditButton have not been updated). Depending on how complex your app is, switching later on can be a bit of a pain, so here's a good WWDC video introducing the new API: The SwiftUI cookbook for navigation as well as some blog posts.

Using NavigationLink in ForEach loop in SwiftUI

I have a view that has a grid of buttons. These buttons lead to another view, but depending on which button is pressed, passes in different information that will populate the view. This is all already in a NavigationView.
ForEach(userData) { user in
NavigationLink(destination: DestinationView(userData: user.data)) {
Button(action: {}) {
//button styling
}
}
}
The buttons display as intended and are clickable but nothing happens. The subview is a giant black rectangle for testing and it does not appear. I tried adding to each NavigationLink the isDetailLink(false) modifier, but that did not do anything.

Only show horizontal scroll indicator on hover

I'm developing a macOS application. I want to hide the horizontal scroll indicators when the mouse is not hovering over the view.
I got it working with the .onHover modifier. The problem is that the scroll indicator takes up space, so when it appears the view gets bigger and everything is moved below it. Is there a better way to do this, or at least a way to not change the views height when it appears?
Here is some stripped down code:
struct Test: View {
#State private var mouseHover = false
var body: some View {
ScrollView(.horizontal, showsIndicators: mouseHover) {
RowOfItems()
.padding(.bottom)
}
.onHover { hover in
mouseHover = hover
}
}
}

SwiftUI: popover + sheet in different hierarchies problem

I experience a problem of presenting a popover and then trying to present a sheet. The sheet is unable to be presented.
I have prepared a short code that displays two buttons
The first one presents a popover over itself ("Click this button")
The second one presents a sheet ("Then this button")
Steps to reproduce
--- Reproducible on an iPad ---
Click the first button, a popover is presented
Directly click the second button while the popover is being visible.
(without dismissing the popover any other way)
State: The popover is dismissed, but the sheet is not being presented. And it is impossible to present it using the second button. The popover button still works though.
Error
The following message is being printed to the console:
[Presentation] Attempt to present <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x10bc13cf0>
on <_TtGC7SwiftUI19UIHostingControllerV10AppBuilder8RootView_: 0x105a093f0>
(from <_TtGC7SwiftUI19UIHostingControllerV10AppBuilder8RootView_: 0x105a093f0>)
which is already presenting <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x10ba170a0>.
Code
import SwiftUI
struct MyView: View {
#State var showSheet: Bool = false
struct SomeDeepView: View {
#State var showPopover: Bool = false
var body: some View {
Button {
showPopover = true
} label: {
Text("Click this button")
}
.popover(isPresented: $showPopover) {
Text("Popover content")
}
}
}
var body: some View {
VStack(spacing: 64) {
SomeDeepView()
Button {
showSheet = true
} label: {
Text("Then this button")
}
}
.sheet(isPresented: $showSheet) {
Text("Sheet content")
}
.frame(width: 500, height: 500, alignment: .center)
}
}
My thoughts
MyView shoudn't care about the internal stuff of SomeDeepView.
Also, SomeDeepView shouldn't care much about its exterior/parents.
Yet, we can't show a popover and a sheet at the same time. I would accept this knowing that the framework would handle this and wouldn't break. However, it does break.
Unexpected side effect: by changing showSheet is not able to display the sheet anymore.
Any thoughts, ideas are very welcome.
Thank you
Edit1: I don't consider toggle() as an effective sulution as it introduces another bug. You would need to press the button multiple times before it would react.
If you change showSheet = true to showSheet.toggle(), the sheet will show up on the 3rd time passing the 2nd button.
Other than that I guess you would have to manually check, that the sheet can't be opened while the popover is open...

SwiftUI EditButton action on Done

How do you set an action to perform when the user hits the EditButton when it appears as "Done"? Is it even possible to do so?
Note that this is not the same as performing an action at each of the individual edits the user might do (like onDelete or onMove). How do you set an action to perform when the user is finished making all changes and ready to commit them?
It's necessary because I'm making all changes on a temporary copy of the model and will not commit the changes to the actual model until the user hits "Done". I am also providing a "Cancel" Button to discard the changes.
struct MyView: View {
#Environment(\.editMode) var mode
var body: some View {
VStack {
HStack {
if self.mode?.value == .active {
Button(action: {
// discard changes here
self.mode?.value = .inactive
}) {
Text("Cancel")
}
}
Spacer()
EditButton()
// how to commit changes??
}
// controls to change the model
}
}
}
Is it even possible to set an action for "Done" on the EditButton or would I have to provide my own button to act like an EditButton? I could certainly do that, but it seems like it should be easy to do with the EditButton.
New answer for XCode 12/iOS 14 using the onChange modifier.
This approach uses the SwiftUI editMode key path available via #Environment along with the new onChange modifier to determine when the view enters and exits editing mode.
In your view do the following:
#Environment(\.editMode) private var editMode
...
.onChange(of: editMode!.wrappedValue, perform: { value in
if value.isEditing {
// Entering edit mode (e.g. 'Edit' tapped)
} else {
// Leaving edit mode (e.g. 'Done' tapped)
}
})
You can use the onDisappear() modifier to perform the action, on a view that you show only on edit mode. There is an example on how to do it in the tutorial of SwiftUI "Working with UI Controls":
if self.mode?.wrappedValue == .inactive {
ProfileSummary(profile: profile)
} else {
ProfileEditor(profile: $draftProfile)
.onDisappear {
self.draftProfile = self.profile
}
}
In your sample code above, since you do not have a view shown in edit mode, you can add state to check if you have clicked on "Cancel" or "Done" as below:
struct MyView: View {
#Environment(\.editMode) var mode
#State var isCancelled = false
var body: some View {
VStack {
HStack {
if self.mode?.wrappedValue == .active {
Button(action: {
// discard changes here
self.mode?.wrappedValue = .inactive
self.isCancelled = true
}) {
Text("Cancel")
}
.onAppear() {
self.isCancelled = false
}
.onDisappear() {
if (self.isCancelled) {
print("Cancel")
} else {
print("Done")
}
}
}
Spacer()
EditButton()
// how to commit changes??
}
// controls to change the model
}
}
}
From what I understand, the EditButton is meant to put the entire environment in edit mode. That means going into a mode where, for example, you rearrange or delete items in a list. Or where text fields become editable. Those sorts of things. "Done" means "I'm done being in edit mode", not "I want to apply my changes." You would want a different "Apply changes," "Save," "Confirm" or whatever button to do those things.
Your last comment contains a subtle change to things. Until then it sounded like you wanted both a "Done" and "Cancel" button - something very few iOS apps do.
But if you want a "Done" button with a "double-checking things, are you sure" you should be able to code such a thing by (possibly) manually going into edit mode and detecting when it's no longer in it. Save a "before" state, add a popup to when it is about to be changed, and you might give the user a rollback option.
FYI - I'm very old school. Not just PC, but mainframe. Not just Microsoft, but SAP. I certainly understand wanting to give the user one last chance before a destructive change - but not too many iOS apps do that. :-)