SwiftUI, weird NavigationLink behavior when working with actionSheet - ios

I want to detect if the user meets the prerequisite first before I let him/her in. If the prerequisite is not met, the app will pop an actionSheet and show the user some ways to unlock the feature.
It works perfectly fine when I tap on the text. But when I tap on the blank place on the list. It just skip the Binding. And the weird thing is that in my actually project, the Binding becomes "true" even if I only set it to false.
Here's the question. Am I using the correct approach or did I miss anything? Or is this a bug?
Thank you.
struct ContentView: View {
#State var linkOne = false
#State var linkTwo = false
#State var linkThree = false
#State var actionOne = false
#State var actionTwo = false
#State var actionThree = false
var body: some View {
NavigationView{
List{
NavigationLink("Destination View One", destination: DestOneView(), isActive: self.$linkOne)
.actionSheet(isPresented: self.$actionOne) { () -> ActionSheet in
ActionSheet(title: Text("Hello"), message:Text("This is weird"), buttons: [ActionSheet.Button.cancel()])
}.onTapGesture {
self.actionOne = true
// self.linkOne = true
}
NavigationLink("Destination View Two", destination: DestTwoView(), isActive: self.$linkTwo)
.actionSheet(isPresented: self.$actionTwo) { () -> ActionSheet in
ActionSheet(title: Text("Hello"), message:Text("This is weird"), buttons: [ActionSheet.Button.cancel()])
}
.onTapGesture {
self.actionTwo = true
// self.linkTwo = true
}
NavigationLink("Destination View Three", destination: DestThreeView(), isActive: self.$linkThree)
.actionSheet(isPresented: self.$actionThree) { () -> ActionSheet in
ActionSheet(title: Text("Hello"), message:Text("This is weird"), buttons: [ActionSheet.Button.cancel()])
}
.onTapGesture {
self.actionThree = true
// self.linkThree = true
}
}
}
}
}
Three other views.
struct DestOneView: View {
var body: some View {
Text("First View")
}
}
struct DestTwoView: View {
var body: some View {
Text("Second View")
}
}
struct DestThreeView: View {
var body: some View {
Text("Third View")
}
}

Generally overriding gestures does not work well within the List. One of the solutions can be to use a Button to present a NavigationLink:
import SwiftUI
struct ContentView: View {
#State private var linkOne = false
...
var body: some View {
NavigationView {
List {
NavigationLink(destination: SomeView(), isActive: $linkOne) {
EmptyView()
}
Button(action: {
// here you can perform actions
self.linkOne = true
}, label: {
Text("Some text!")
})
Spacer()
}
}
}
}

When I came back and tested my the code. The solution didn't really work. May be because of the List's bug. People on another post said that the List in the new view only show once if the sheet is inside the List. So I only got an empty List in the new view when I tap the button in Xcode Version 11.5. For some reasion, if I use NavigationView, all contents are shrunk into the middle of the view instead of aligning to the top.
My work around is to set the Binding in .onAppear. It pops the actionSheet when the view loads. And then use the presentationMode method to return to the previous view.
#Environment(\.presentationMode) var presentationMode
.
.
.
ActionSheet.Button.default(Text("Dismiss"), action: {
self.presentationMode.wrappedValue.dismiss()}

Related

SwiftUI dismissing .alert pops NavigationView

I've got the following sample code running on watchos 8+:
struct TestView: View {
#State private var showingAlert = false
var body: some View {
NavigationView {
NavigationLink {
Button(action: {
showingAlert = true
}, label: {
Text("Show dialog")
})
.alert("", isPresented: $showingAlert, actions: {
Button("refresh") {
}
})
} label: {
Text("Show detail")
}
}
}
}
It's a simple screen with a NavigationLinks that pushes into navigation a new view with a simple Button.
On button press I'm displaying an alert with a simple button. When the alert is dismissed, the detail view is also dismissed and the interface returns back to it's original state.
It looks like this:
I am wondering what am I missing here. How can I dismiss the alert but remain on the second view that shows the "Show dialog" button.
View separations and not needed NavigationView (watchOS only!).
So here is fixed code. Tested with Xcode 13.4 / watchOS 8.5
struct TestView: View {
var body: some View {
NavigationLink {
DestinationView()
} label: {
Text("Show detail")
}
}
struct DestinationView: View {
#State private var showingAlert = false
var body: some View {
Button(action: {
showingAlert = true
}, label: {
Text("Show dialog")
})
.alert("", isPresented: $showingAlert, actions: {
Button("refresh") {
}
})
}
}
}

Why is NavigationLink not working inside of a SwiftUI alert?

I have an alert that upon tapping I would like to return the user back to another view. The alert is showing, but why does it not navigate upon tapping?
VStack{
.alert("End of available content", isPresented: $model.alertIsPresented) {
NavigationLink(destination: SearchView()) {
Button("OK", role:.cancel) {}
}
}
}
Because NavigationLink needs to be inside of a hierarchy using NavigationView. An alert is a modal presented outside of that structure.
If you would like to programmatically navigate, you can use the isActive property of a NavigationLink within the NavigationView hierarchy.
struct ContentView : View {
#State private var alertIsPresented = false
#State private var navLinkActive = false
var body: some View {
NavigationView {
VStack{
Button("Present alert") {
alertIsPresented = true
}
.alert("End of available content", isPresented: $alertIsPresented) {
Button("Navigate") {
navLinkActive = true
}
}
NavigationLink(isActive: $navLinkActive, destination: { SearchView() }, label: {
EmptyView()
})
}
}
}
}
struct SearchView : View {
var body: some View {
Text("Search")
}
}

SwiftUI. Present a detail view pushed from the root view as the initial application view

I have two SwiftUI views, the first view has a navigation link to the second view and I want to show the second view that is "pushed" out of the first view, as the initial application view.
This is the behavior of the iOS Notes app, where users see a list of notes as the initial view controller and can return to the folder list with the back navigation button.
Can I implement this with SwiftUI and how?
Here is a simple demo. Prepared & tested with Xcode 11.7 / iOS 13.7
struct ContentView: View {
#State private var isActive = false
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View"), isActive: $isActive) {
Text("First View")
}
}
.onAppear { self.isActive = true }
}
}
you can add another state variable to hide the first view until the second view appears on the screen.
struct ContentView1: View {
#State private var isActive = false
#State private var showView = false
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View")
.onAppear {
self.showView = true
},
isActive: $isActive) {
if self.showView {
Text("First View")
} else {
EmptyView()
}
}
}
.onAppear {
self.isActive = true
}
}
}
As mentioned in my comments to another answer, by setting an initial state for a variable that controls the presentation of the second view to true, your ContentView presents this second view as the initial view.
I've tested this using the simulator and on device. This appears to solve your problem and does not present the transition from the first view to the second view to the user - app opens to the second view.
struct ContentView: View {
#State private var isActive = true
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View"), isActive: $isActive) {
Text("First View")
}
}
}
}
I made my own implementation based on #Asperi and #Mohammad Rahchamani answers.
This implementation allows you to navigate even from a list with multiple navigation links. Tested on Xcode 12 with SwiftUI 2.0.
struct IOSFolderListView: View {
#State var isActive = false
#State var wasViewShown = false
var body: some View {
let list = List {
NavigationLink(destination: Text("SecondView").onAppear {
self.wasViewShown = true
}, isActive: $isActive) {
Text("SecondView")
}
NavigationLink(destination: Text("ThirdView")) {
Text("ThirdView")
}
.onAppear {
self.isActive = false
}
}
if wasViewShown {
list.listStyle(GroupedListStyle())
.navigationBarTitle("FirstView")
.navigationBarItems(leading: Image(systemName: "folder.badge.plus"), trailing: Image(systemName: "square.and.pencil"))
} else {
list.opacity(0)
.onAppear {
self.isActive = true
}
}
}
}

SwiftUI transition from modal sheet to regular view with Navigation Link

I'm working with SwiftUI and I have a starting page. When a user presses a button on this page, a modal sheet pops up.
In side the modal sheet, I have some code like this:
NavigationLink(destination: NextView(), tag: 2, selection: $tag) {
EmptyView()
}
and my modal sheet view is wrapped inside of a Navigation View.
When the value of tag becomes 2, the view does indeed go to NextView(), but it's also presented as a modal sheet that the user can swipe down from, and I don't want this.
I'd like to transition from a modal sheet to a regular view.
Is this possible? I've tried hiding the navigation bar, etc. but it doesn't seem to make a difference.
Any help with this matter would be appreciated.
You can do this by creating an environmentObject and bind the navigationLink destination value to the environmentObject's value then change the value of the environmentObject in the modal view.
Here is a code explaining what I mean
import SwiftUI
class NavigationManager: ObservableObject{
#Published private(set) var dest: AnyView? = nil
#Published var isActive: Bool = false
func move(to: AnyView) {
self.dest = to
self.isActive = true
}
}
struct StackOverflow6: View {
#State var showModal: Bool = false
#EnvironmentObject var navigationManager: NavigationManager
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: self.navigationManager.dest, isActive: self.$navigationManager.isActive) {
EmptyView()
}
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}
}
}
.sheet(isPresented: self.$showModal) {
secondView(isPresented: self.$showModal).environmentObject(self.navigationManager)
}
}
}
struct StackOverflow6_Previews: PreviewProvider {
static var previews: some View {
StackOverflow6().environmentObject(NavigationManager())
}
}
struct secondView: View {
#EnvironmentObject var navigationManager: NavigationManager
#Binding var isPresented: Bool
#State var dest: AnyView? = nil
var body: some View {
VStack {
Text("Modal view")
Button(action: {
self.isPresented = false
self.dest = AnyView(thirdView())
}) {
Text("Press me to navigate")
}
}
.onDisappear {
// This code can run any where but I placed it in `.onDisappear` so you can see the animation
if let dest = self.dest {
self.navigationManager.move(to: dest)
}
}
}
}
struct thirdView: View {
var body: some View {
Text("3rd")
.navigationBarTitle(Text("3rd View"))
}
}
Hope this helps, if you have any questions regarding this code, please let me know.

SwiftUI Modal Sheet Re-Opening After Dismiss

I have a list in a Navigation View, with a trailing navigation button to add a list item. The button opens a modal sheet. When I dismiss the sheet (by pulling it down), the sheet pops right back up again automatically and I can't get back to the first screen. Here's my code.
struct ListView: View {
#ObservedObject var listVM: ListViewModel
#State var showNewItemView: Bool = false
init() {
self.listVM = ListViewModel()
}
var body: some View {
NavigationView {
List {
ForEach(listVM.items, id: \.dateCreated) { item in
HStack {
Text(item.name)
Spacer()
Image(systemName: "arrow.right")
}
}
}
.navigationBarTitle("List Name")
.navigationBarItems(trailing: AddNewItemBtn(isOn: $showNewItemView))
}
}
}
struct AddNewItemBtn: View {
#Binding var isOn: Bool
var body: some View {
Button(
action: { self.isOn.toggle() },
label: { Image(systemName: "plus.app") })
.sheet(
isPresented: self.$isOn,
content: { NewItemView() })
}
}
I am getting this error:
Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c603b7c22SheetHostingControllerVS_7AnyView_: 0x7fc5e0c1f8f0> on which is already presenting (null)
I've tried toggling the bool within "onDismiss" on the button itself, but that doesn't work either. Any ideas?
Turns out putting the button in the navigationBarItems(trailing:) modifier is the problem. I just put the button in the list itself instead of in the nav bar and it works perfectly fine. Must be some kind of bug.

Resources