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...
Related
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
In SwiftUI, if you embed a NavigationLink inside a button, then clicking the button will trigger the button's action and the navigation as well.
struct LoginView: View {
#StateObject private var viewModel = LoginViewModel()
Button(action: viewModel.doLogin, label: {
NavigationLink(value: viewModel.userInfo) {
Text("Log in")
}
.buttonStyle(.plain)
})
.buttonStyle(.plain)
}
However the reverse only triggers the button's action. Why is that?
struct LoginView: View {
#StateObject private var viewModel = LoginViewModel()
NavigationLink(value: viewModel.userInfo) {
Button(action: viewModel.doLogin, label: {
Text("Log in")
})
.buttonStyle(.plain)
}
.buttonStyle(.plain)
}
When the Button is created, it takes a label: parameter in its initialiser that is, in this case, of type view.
So what gets dispalyed on the screen is the label.
In the first case, the label is a NavigationLink and clicking on a it performs the navigation.
In the second case, the NavigationLink has the label parameter, that displays a Button and because of the style, it completely hides the navigation link area, as opposed to, if you had used a Text view or so.
So the answer is, the NavigationLink simply does not get clicked on because it hides entirely beneath the Button.
I am developing an app which uses UIKit. I have integrated a UIKit UIViewController inside SwiftUI and everything works as expected. I am still wondering if there is a way to 'know' when a SwiftUI View is completely gone.
My understanding is that a #StateObject knows this information. I now have some code in the deinit block of the corresponding class of the StateObject. There is some code running which unsubscribes the user of that screen.
The problem is that it is a fragile solution. In some scenario's the deinit block isn't called.
Is there any recommended way to know if the user pressed the back button in a SwiftUI View (or swiped the view away)? I don't want to get notified with the .onDisppear modifier because that is also called when the user taps somewhere on the screen which adds another view to the navigation stack. I want to run some code once when the screen is completely gone.
Is there any recommended way to know if the user pressed the back button in a SwiftUI View (or swiped the view away)?
This implies you're using a NavigationView and presenting your view with a NavigationLink.
You can be notified when the user goes “back” from your view by using one of the NavigationLink initializers that takes a Binding. Create a custom binding and in its set function, check whether the old value is true (meaning the child view was presented) and the new value is false (meaning the child view is now being popped from the stack). Example:
struct ContentView: View {
#State var childIsPresented = false
#State var childPopCount = 0
var body: some View {
NavigationView {
VStack {
Text("Child has been popped \(childPopCount) times")
NavigationLink(
"Push Child",
isActive: Binding(
get: { childIsPresented },
set: {
if childIsPresented && !$0 {
childPopCount += 1
}
childIsPresented = $0
}
)
) {
ChildView()
}
}
}
}
}
struct ChildView: View {
var body: some View {
VStack {
Text("Sweet child o' mine")
NavigationLink("Push Grandchild") {
GrandchildView()
}
}
}
}
struct GrandchildView: View {
var body: some View {
VStack {
Text("👶")
.font(.system(size: 100))
}
}
}
Note that these initializers, and NavigationView, are deprecated if your deployment target is iOS 16. In that case, you'll want to use a NavigationStack and give it a custom Binding that performs the pop-detection.
SwiftUI helpfully gives you NavigationView which easily lets you define a sidebar and main content for iPad apps that automatically collapse for iPhones.
I have an app and everything works as expected except on iPad, in portrait mode, the sidebar is hidden by default and you are forced click a button to show it.
All I want is to force the sidebar to always be visible, even in portrait mode. And make it work the same way as the settings app.
I’m even willing to use a UIKit view wrapped for SwiftUI, but wrapping NavigationController seems very very challenging.
Is there a better way?
Without your specific code I can't be sure but I think you are describing the initial screen that shows on iPad. That is actually a "ThirdView". You can see it with the code below.
And I am answering the "better way" portion of your question. Which for me is a way of just "graciously" dealing with it.
struct VisibleSideBar2: View {
var body: some View {
NavigationView{
List(0..<10){ idx in
NavigationLink("SideBar \(idx)", destination: Text("Secondary \(idx)"))
}
Text("Welcome Screen")
}
}
}
It is even more apparent if you have a default selection in your "Secondary View" because now you have to click "Back" twice to get to the SideBar
struct VisibleSideBar1: View {
#State var selection: Int? = 1
var body: some View {
NavigationView{
List(0..<10){ idx in
NavigationLink(
destination: Text("Secondary \(idx)"),
tag: idx,
selection: $selection,
label: {Text("SideBar \(idx)")})
}
Text("Third View")
}
}
}
A lot of the "solutions" out there for this just turn the NavigationView into a Stack but then you can't get the double column.
One way of dealing with it is the what is depicted in VisibleSideBar2. You can make/embrace a nice "Welcome Screen" so the user isn't greeted with a blank screen and then the natural navigation instincts kick in. You only see the "Welcome Screen" on iPad Portrait and on Catalyst/MacOS where Stack is unavailable.
Or you can bypass the third screen by using isActive in a NavigationLink and using the Sidebar as a menu like View
struct VisibleSidebar3: View{
#State var mainIsPresented = true
var body: some View {
NavigationView {
ScrollView{
NavigationLink(
destination: Text("Main View").navigationTitle("Main"),
isActive: $mainIsPresented,
label: {
Text("Main View")
})
NavigationLink("List View", destination: ListView())
}.navigationTitle("Sidebar")
//Not visible anymore
Text("Welcome Screen")
}
}
}
struct ListView: View{
var body: some View {
List(0..<10){ idx in
NavigationLink("SideBar \(idx)", destination: Text("Secondary \(idx)"))
}.navigationTitle("List")
}
}
Like I said my answer isn't really a way of "fixing" the issue. Just dealing with it. To fix it we would have to somehow dismiss the "Third Screen/Welcome Screen". Then manipulate the remaining UISplitViewController (Several SO questions on this) to show both the SideBar/Master and the Detail View.
In UIKit it seems to have been done a lot, if you search SO, you will find a way to create a UISplitViewController that behaves like Settings.
I don't know how to navigate between views with buttons.
The only thing I've found online is detail view, but I don't want a back button in the top left corner. I want two independent views connected via two buttons one on the first and one on the second.
In addition, if I were to delete the button on the second view, I should be stuck there, with the only option to going back to the first view being crashing the app.
In storyboard I would just create a button with the action TouchUpInSide() and point to the preferred view controller.
Also do you think getting into SwiftUI is worth it when you are used to storyboard?
One of the solutions is to have a #Statevariable in the main view. This view will display one of the child views depending on the value of the #Statevariable:
struct ContentView: View {
#State var showView1 = false
var body: some View {
VStack {
if showView1 {
SomeView(showView: $showView1)
.background(Color.red)
} else {
SomeView(showView: $showView1)
.background(Color.green)
}
}
}
}
And you pass this variable to its child views where you can modify it:
struct SomeView: View {
#Binding var showView: Bool
var body: some View {
Button(action: {
self.showView.toggle()
}) {
Text("Switch View")
}
}
}
If you want to have more than two views you can make #State var showView1 to be an enum instead of a Bool.