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

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
}
}
}
}

Related

SwiftUI Modal Inherits SearchBar during Sheet Presentation

Consider the following example with a list and a button wrapped in a HStack that opens up a sheet:
struct ContentView: View {
#State var text: String = ""
#State var showSheet = false
var body: some View {
NavigationView {
List {
HStack {
button
}
Text("Hello World")
}
.searchable(text: $text)
}
}
var button: some View {
Button("Press", action: { showSheet = true })
.sheet(isPresented: $showSheet) {
modalView
}
}
var modalView: some View {
NavigationView {
List {
Text("Test")
}
}
}
}
On press of the button, a modal is presented to the user. However, the searchable modifier gets passed to the modal, see this video.
Now if the HStack is removed, everything works fine:
List {
button
Text("Hello World")
}
In addition, everything works also fine if the modal is not a NavigationView:
var modalView: some View {
List {
Text("Test")
}
}
Does somebody know what the problem here might be or is it once again one of those weird SwiftUI bugs?
putting the sheet, outside of the button and the List, works for me. I think .sheet is not meant to be inside a List, especially where searchable is operating.
struct ContentView: View {
#State var text: String = ""
#State var showSheet = false
var body: some View {
NavigationView {
List {
HStack {
button
}
Text("Hello World")
}
.searchable(text: $text)
}
.sheet(isPresented: $showSheet) {
modalView
}
}
var button: some View {
Button("Press", action: { showSheet = true })
}
var modalView: some View {
NavigationView {
List {
Text("Test")
}
}
}
}
Another workaround is to use navigationBarHidden = true, but then you must live without the navigation bar in the sheet view.
var modalView: some View {
NavigationView {
List {
Text("Test")
}
.navigationBarHidden(true)
}
}
Btw, on iPadOS it helps to use .searchable(text: $text, placement: .sidebar)

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 Show navigation bar title on the back button but not in the previous View

I have two views, one leads to the other. I want that the second view uses the title of the first view for the back button, which should then be: "<View1".
I don't want to show the title in the first view.
Problem: I can't hide navigation bar because it will also hide a custom button which is within it. Setting .navigationTitle("") hides the title in the first view, but also hides it from the back button in the second view.
What I have now:
What I would like to have:
Code:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack {
NavigationLink("go to the second view", destination: SecondView(), isActive: $isLinkActive).navigationTitle("View1")
.navigationBarItems(leading: Button(action: {
()
}, label: {
Text("custom button")
}))
}
}.navigationViewStyle(StackNavigationViewStyle())
}
private func btnPressed() {
isLinkActive = true
}
}
struct SecondView: View {
var body: some View {
Color.blue
}
}
You need to create custom back button for destination view as well,and you shouldn’t set navigation title for navigationLink, that’s why you are not able to hide “View1” correctly.
Check below code.
import SwiftUI
struct Test: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack {
NavigationLink("go to the second view", destination: SecondView()
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
isLinkActive = false
}, label: {
HStack{
Image(systemName: "backward.frame.fill")
Text("View1")
}
})) ,
isActive: $isLinkActive)
}.navigationBarItems(leading: Button(action: {
()
}, label: {
Text("custom button")
}))
}.navigationViewStyle(StackNavigationViewStyle())
}
private func btnPressed() {
isLinkActive = true
}
}
struct SecondView: View {
var body: some View {
Color.blue
}
}
You can try and make navigationBar code as reusable component, because you might need to do this at multiple places.
Output-:
I achieved this by using two modifiers on my main view. Similar to your case, I didn't want a title on the first view, but I wanted the back button on the pushed view to read < Home, not < Back.
.navigationTitle("Home")
.toolbar {
ToolbarItem(placement: .principal) {
Text("")
}
}

SwitUI - Two navigationLink in a list

I have two NavigationLink in a cell of my List
I want to go to destination1 when I tap once,and go to destination2 when I tap twice.
So I added two tap gesture to control the navigation.
But when I tap,there are two questions:
1 The tap gesture block won't be called.
2 The two navigation link will be both activated automatically even if they are behind a TextView.
The real effect is: Tap the cell -> go to Destination1-> back to home -> go to Destination2 -> back to home
Here is my code :
struct MultiNavLink: View {
#State var mb_isActive1 = false;
#State var mb_isActive2 = false;
var body: some View {
return
NavigationView {
List {
ZStack {
NavigationLink("", destination: Text("Destination1"), isActive: $mb_isActive1)
NavigationLink("", destination: Text("Destination2"), isActive: $mb_isActive2)
Text("Single tap::go to destination1\nDouble tap,go to destination2")
}
.onTapGesture(count: 2, perform: {()->Void in
NSLog("Double tap::to destination2")
self.mb_isActive2 = true
}).onTapGesture(count: 1, perform: {()->Void in
NSLog("Single tap::to destination1")
self.mb_isActive1 = true
})
}.navigationBarTitle("MultiNavLink",displayMode: .inline)
}
}
}
I have tried remove the List element,then everything goes as I expected.
It seems to be the List element that makes everything strange.
I found this question:SwiftUI - Two buttons in a List,but the situation is different from me.
I am expecting for your answer,thank you very much...
Try the following approach - the idea is to hide links in background of visible content and make them inactive for UI, but activated programmatically.
Tested with Xcode 12 / iOS 14.
struct MultiNavLink: View {
var body: some View {
return
NavigationView {
List {
OneRowView()
}.navigationBarTitle("MultiNavLink", displayMode: .inline)
}
}
}
struct OneRowView: View {
#State var mb_isActive1 = false
#State var mb_isActive2 = false
var body: some View {
ZStack {
Text("Single tap::go to destination1\nDouble tap,go to destination2")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.background(Group {
NavigationLink(destination: Text("Destination1"), isActive: $mb_isActive1) {
EmptyView() }
.buttonStyle(PlainButtonStyle())
NavigationLink(destination: Text("Destination2"), isActive: $mb_isActive2) {
EmptyView() }
.buttonStyle(PlainButtonStyle())
}.disabled(true))
.highPriorityGesture(TapGesture(count: 2).onEnded {
self.mb_isActive2 = true
})
.onTapGesture(count: 1) {
self.mb_isActive1 = true
}
}
}
Navigation link has a initializer that takes a binding selection and whenever that selection is set to the value of the NavigationLink tag, the navigation link will trigger.
As a tip, if the app can't differentiate and identify your taps, and even with two taps, still the action for one-tap will be triggered, then you can use a simultaneous gesture(.simultaneousGesture()) modifier instead of a normal gesture(.gesture()) modifier.
struct someViewName: View {
#State var navigationLinkTriggererForTheFirstOne: Bool? = nil
#State var navigationLinkTriggererForTheSecondOne: Bool? = nil
var body: some View {
VStack {
NavigationLink(destination: SomeDestinationView(),
tag: true,
selection: $navigationLinkTriggererForTheFirstOne) {
EmptyView()
}
NavigationLink(destination: AnotherDestinationView(),
tag: true,
selection: $navigationLinkTriggererForTheSecondOne) {
EmptyView()
}
NavigationView {
Button("tap once to trigger the first navigation link.\ntap twice to trigger the second navigation link.") {
// tap once
self.navigationLinkTriggererForTheFirstOne = true
}
.simultaneousGesture(
TapGesture(count: 2)
.onEnded { _ in
self.navigationLinkTriggererForTheSecondOne = true
}
)
}
}
}
}

Dismiss a parent modal in SwiftUI from a NavigationView

I am aware of how to dismiss a modal from a child view using #Environment (\.presentationMode) var presentationMode / self.presentationMode.wrappedValue.dismiss() but this is a different issue.
When you present a multi-page NavigationView in a modal window, and have navigated through a couple of pages, the reference to presentationMode changes to be the NavigationView, so using self.presentationMode.wrappedValue.dismiss() simply pops the last NavigationView rather than dismissing the containing modal.
Is it possible - and if so how - to dismiss the containing modal from a page in a NavigationView tree?
Here's a simple example showing the problem. If you create an Xcode Single View app project using SwiftUI and replace the default ContentView code with this, it should work with no further changes.
import SwiftUI
struct ContentView: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
self.showModal = false
}) {
PageOneContent()
}
}
}
struct PageOneContent: View {
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
trailing: NavigationLink(destination: PageTwoContent()) {
Text("Next")
})
}
}
}
struct PageTwoContent: View {
#Environment (\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
// How to dismiss parent modal here instead
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
Here is possible approach based on usage own explicitly created environment key (actually I have feeling that it is not correct to use presentationMode for this use-case.. anyway).
Proposed approach is generic and works from any view in modal view hierarchy. Tested & works with Xcode 11.2 / iOS 13.2.
// define env key to store our modal mode values
struct ModalModeKey: EnvironmentKey {
static let defaultValue = Binding<Bool>.constant(false) // < required
}
// define modalMode value
extension EnvironmentValues {
var modalMode: Binding<Bool> {
get {
return self[ModalModeKey.self]
}
set {
self[ModalModeKey.self] = newValue
}
}
}
struct ParentModalTest: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
}) {
PageOneContent()
.environment(\.modalMode, self.$showModal) // < bind modalMode
}
}
}
struct PageOneContent: View {
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
trailing: NavigationLink(destination: PageTwoContent()) {
Text("Next")
})
}
}
}
struct PageTwoContent: View {
#Environment (\.modalMode) var modalMode // << extract modalMode
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
self.modalMode.wrappedValue = false // << close modal
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
Another Approach would be to simply use a notification for this case and just reset the triggering flag for your modal.
It is not the most beautiful solution for me but it is the solution I am most likely to still understand in a few months.
import SwiftUI
struct ContentView: View {
#State var showModalNav: Bool = false
var body: some View {
Text("Present Modal")
.padding()
.onTapGesture {
showModalNav.toggle()
}.sheet(isPresented: $showModalNav, content: {
ModalNavView()
}).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
showModalNav = false
}
}
}
struct ModalNavView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: PushedView(),
label: {
Text("Show Another View")
}
)
}
}
}
struct PushedView: View {
var body: some View {
Text("Pushed View").onTapGesture {
NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "PushedViewNotifciation")))
}
}
}
If you don't want to loosely couple the views through a notification you could also just use a binding for this like so:
struct ContentView: View {
#State var showModalNav: Bool = false
var body: some View {
Text("Present Modal")
.padding()
.onTapGesture {
showModalNav.toggle()
}.sheet(isPresented: $showModalNav, content: {
ModalNavView(parentShowModal: $showModalNav)
}).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
showModalNav = false
}
}
}
struct ModalNavView: View {
#Binding var parentShowModal: Bool
var body: some View {
NavigationView {
NavigationLink(
destination: PushedView(parentShowModal: $parentShowModal),
label: {
Text("Show Another View")
}
)
}
}
}
struct PushedView: View {
#Binding var parentShowModal: Bool
var body: some View {
Text("Pushed View").onTapGesture {
parentShowModal = false
}
}
}
If it's only two levels, and especially if you can dismiss the sheet at multiple levels, you could include showModal as a binding variable in your navigation views below, and then toggling it anywhere would dismiss the entire sheet.
I would assume you could do something similar with showModal as an EnvironmentObject as Wei mentioned above - which might be better if there are more than two levels and you only want to dismiss the sheet at the most specific level.
I can't remember if there's some reason to move away from doing this as a binding variable, but it seems to be working for me.
import SwiftUI
struct ContentView: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
self.showModal = false
}) {
// Bind showModal to the corresponding property in PageOneContent
PageOneContent(showModal: $showModal)
}
}
}
Then you add showModal as a binding variable in PageOneContent, and it is bound to the state variable in ContentView.
struct PageOneContent: View {
// add a binding showModal var in here
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
// bind showModal again to PageTwoContent
trailing: NavigationLink(destination: PageTwoContent(showModal: $showModal)) {
Text("Next")
})
}
}
}
Finally, in PageTwoContent, you can add showModal here (and in the NavigationLink in PageOneContent, you have bound PageTwoContent's showModal to PageOneContent). Then in your button, all you have to do is toggle it, and it will dismiss the sheet.
struct PageTwoContent: View {
// Add showModal as a binding var here too.
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
// This will set the showModal var back to false in all three views, and will dismiss the current sheet.
self.showModal.toggle()
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
I found out you can actually make showModal into an EnvironmentObject, then simplify toggle the showModal to false on PageTwoContent to dismiss both PageOneContent and PageTwoContent.

Resources