NavigationLink match any enum associated value - swiftui

How do I create a NavigationLink that matches any enum associated value?
I have this navigation enum for my app
enum NavRoute {
case loggedOut
case onboarding(OnboardingRoute)
enum OnboardingRoute {
case page1
case page2
}
}
#State var route = NavRoute.onboarding(.page1)
I want to write a NavigationLink like this
NavigationLink(
tag: NavRoute.onboarding(any), // What do I put here??
selection: $route
) {
OnboardingView()
} label: {
Text("Create account")
}

I don't think that is how you suppose to use this: NavigationLink(tag:selection:destination:label:) are suppose to work when a binding selection are the same of tag, so not when this case have some associated value. Like the example bellow: (It automatically navigate to two because of .onAppear modifier.
//
// ContentView.swift
// Navigation
//
// Created by Allan Garcia on 04/11/22.
//
import SwiftUI
enum NavigationRouteTag {
case one, two, three
}
struct ContentView: View {
#State var route: NavigationRouteTag?
var body: some View {
NavigationView {
List {
NavigationLink(
tag: .one,
selection: $route) {
Text("Hello World 1!")
} label: {
Text("Create Account 1!")
}
NavigationLink(
tag: .two,
selection: $route) {
Text("Hello World 2!")
} label: {
Text("Create Account 2!")
}
NavigationLink(
tag: .three,
selection: $route) {
Text("Hello World 3!")
} label: {
Text("Create Account 3!")
}
}
.onAppear {
route = .two
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PS: This API is also deprecated in iOS16, maybe a different approach will be better for you like .navigationDestination(for:destination:) modifier.

Related

In SwiftUI, iOS15, 2nd level NavigationLink, isActive is not working

in iOS15, it is not working:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1().navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}
}
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(
destination: Button {
dest2Active = false // not working!!
} label: {Text("dismiss")} .navigationTitle("Dest2"),
isActive: $dest2Active
) {Text("to Destination 2")}
}
}
The dismiss button in Dest2 is not working!
I remember that in iOS14, this code works well.
How to resolve this?
Adding .isDetailLink(false) to the top level NavigationLink seems to solve the issue. Note that this works on iPhone iOS -- for iPad, you will need to use a StackNavigationStyle as #workingdog suggests in their answer.
The documentation is not clear on why this works (in fact, it refers specifically to multi-column navigation), but it seems to solve a number of NavigationLink-related issues. See, for example: https://developer.apple.com/forums/thread/667460
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1()
.navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}.isDetailLink(false)
}
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(isActive: $dest2Active) {
Dest2(dest2Active: $dest2Active)
} label: {
Text("to Destination 2")
}
}
}
struct Dest2: View {
#Binding var dest2Active : Bool
var body: some View {
Button {
dest2Active = false
} label: {
Text("Dismiss")
}.navigationTitle("Dest2")
}
}
You need to add .navigationViewStyle(.stack) to make it work.
Here is the test code that works for me.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
Dest1().navigationTitle("Dest1")
} label: {
Text("to Destination 1")
}
}.navigationViewStyle(.stack) // <-- here the important bit
}
}
struct Dest1: View {
#State var dest2Active: Bool = false
var body: some View {
NavigationLink(
destination: Button {
dest2Active = false // now working!!
} label: {Text("dismiss")} .navigationTitle("Dest2"),
isActive: $dest2Active
) {Text("to Destination 2")}
}
}

NavigationLink dismisses after TextField changes

I have a navigation stack that's not quite working as desired.
From my main view, I want to switch over to a list view which for the sake of this example represents an array of strings.
I want to then navigate to a detail view, where I want to be able to change the value of the selected string.
I have 2 issues with below code:
on the very first keystroke within the TextField, the detail view is being dismissed
the value itself is not being changed
Also, I suppose there must be a more convenient way to do the binding in the detail view ...
Here's the code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
TestMainView()
}
}
}
struct TestMainView: View {
var body: some View {
NavigationView {
List {
NavigationLink("List View", destination: TestListView())
}
.navigationTitle("Test App")
}
}
}
struct TestListView: View {
#State var strings = [
"Foo",
"Bar",
"Buzz"
]
#State var selectedString: String? = nil
var body: some View {
List(strings.indices) { index in
NavigationLink(
destination: TestDetailView(selectedString: $selectedString),
tag: strings[index],
selection: $selectedString) {
Text(strings[index])
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("List")
}
}
}
struct TestDetailView: View {
#Binding var selectedString: String?
var body: some View {
VStack {
if let _ = selectedString {
TextField("Placeholder",
text: Binding<String>( //what's a better solution here?
get: { selectedString! },
set: { selectedString = $0 }
)
)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Spacer()
}
.navigationTitle("Detail")
}
}
struct TestMainView_Previews: PreviewProvider {
static var previews: some View {
TestMainView()
}
}
I am quite obviously doing it wrong, but I cannot figure out what to do differently...
You're changing the NavigationLink's selection from inside the NavigationLink which forces the TestListView to reload.
You can try the following instead:
struct TestListView: View {
#State var strings = [
"Foo",
"Bar",
"Buzz",
]
var body: some View {
List(strings.indices) { index in
NavigationLink(destination: TestDetailView(selectedString: self.$strings[index])) {
Text(self.strings[index])
}
}
}
}
struct TestDetailView: View {
#Binding var selectedString: String // remove optional
var body: some View {
VStack {
TextField("Placeholder", text: $selectedString)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
}
}
}

Sidebar with different columns in SwiftUI

I'm playing with the new Sidebar that has come with SwiftUI 2 and the possibility to navigate in large screens with three columns. An example about how it works can be found here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-sidebar-for-ipados
It works fine, but I would like to go one step forward and make some options of my main menu that show the three columns but other options just two.
Here an example of some demo code.
import SwiftUI
struct ContentView: View {
var body: some View{
NavigationView{
List{
Section(header: Text("Three columns")){
NavigationLink(
destination: ItemsView(),
label: {
Label("Animals",systemImage: "tortoise")
})
NavigationLink(
destination: ItemsView(),
label: {
Label("Animals 2",systemImage: "hare")
})
}
Section(header: Text("Two columns")){
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label("Settings",systemImage: "gear")
})
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label("Settings 2",systemImage: "gearshape")
})
}
}
.listStyle(SidebarListStyle())
.navigationBarTitle("App Menu")
ItemsView()
DetailView(animal: "default")
}
}
}
struct ItemsView: View{
let animals = ["Dog", "Cat", "Lion", "Squirrel"]
var body: some View{
List{
ForEach(animals, id: \.self){ animal in
NavigationLink(
destination: DetailView(animal: animal)){
Text(animal)
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Animals")
}
}
struct DetailView: View{
var animal: String
var body: some View{
VStack{
Text("🐕")
.font(.title)
.padding()
Text(animal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
If you run the code in, for example, an iPad Pro (12,9-inch) in landscape mode, you can see the tree columns. First the App Menu (sidebar). If you click on one of the first two options (animals, and animals 2), you can see a list of animals (second column) and when you click on some animal, you reach the third column (detail view).
However, I want to have only two columns when I click on the last two options of the menu (Settings and Settings 2). Any clue how to achieve it?
I've tried to hide that section if some of the first options in menu are not selected (with the selected parameter in NavigationLink), but without luck. It seems it is not possible (or I don't know) to know which option is selected in the sidebar.
Any idea is welcome!
It took me a few days to figure it out.
Test on different iPad device & multiple tasking mode, all works as expected.(iOS14+, haven't test on iOS13)
Minimal Example:
extension UISplitViewController {
open override func viewDidLoad() {
super.viewDidLoad()
self.show(.primary) // show sidebar, this is the key, toke me days to find this...
self.showsSecondaryOnlyButton = true
}
}
struct ContentView: View {
#State var column: Int = 3
var body: some View {
switch column {
case 3: // Triple Column
NavigationView {
List {
HStack {
Text("Triple")
}
.onTapGesture {
column = 3
}
HStack {
Text("Double")
}
.onTapGesture {
column = 2
}
}
Text("Supplementary View")
Text("Detail View")
}
default: // Double Column
NavigationView {
List {
HStack {
Text("Triple")
}
.onTapGesture {
column = 3
}
HStack {
Text("Double")
}
.onTapGesture {
column = 2
}
}
Text("Supplementary View")
}
}
}
}
My another answer: set sidebar default selected item.
Combine with this two solution, I have built an 2&3 column co-exist style's app.
SwiftUI, selecting a row in a List programmatically
Here is a solution which uses a custom ViewModifier. It's working on iOS 14.2, 15.0 and 15.2. Since you are using SidebarListStyle and Label I didn't test for prior versions.
Testproject:
enum Item: Hashable {
case animals, animals2, settings, settings2
static var threeColumns = [Item.animals, .animals2]
static var twoColumns = [Item.settings, .settings2]
var title: String {
switch self {
case .animals:
return "animals"
case .animals2:
return "animals2"
case .settings:
return "settings"
case .settings2:
return "settings2"
}
}
var systemImage: String {
switch self {
case .animals:
return "tortoise"
case .animals2:
return "hare"
case .settings:
return "gear"
case .settings2:
return "gearshape"
}
}
}
struct ContentView: View {
#State var selectedItem: Item?
var body: some View {
NavigationView {
List {
Section(header: Text("Three columns")) {
ForEach(Item.threeColumns, id: \.self) { item in
NavigationLink(tag: item, selection: $selectedItem) {
ItemsView()
} label: {
Label(item.title.capitalized, systemImage: item.systemImage)
}
}
}
Section(header: Text("Two columns")) {
ForEach(Item.twoColumns, id: \.self) { item in
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label(item.title, systemImage: item.systemImage)
})
}
}
}
.listStyle(SidebarListStyle())
.navigationBarTitle("App Menu")
}
}
}
struct ItemsView: View {
let animals = ["Dog", "Cat", "Lion", "Squirrel"]
var body: some View {
List {
ForEach(animals, id: \.self) { animal in
NavigationLink(
destination: DetailView(animal: animal)) {
Text(animal)
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Animals")
}
}
struct DetailView: View {
var animal: String
var body: some View {
VStack {
Text("🐕")
.font(.title)
.padding()
Text(animal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
private struct ColumnModifier: ViewModifier {
let item: Item?
func body(content: Content) -> some View {
if item == .settings || item == .settings2 {
content
EmptyView()
} else {
content
EmptyView()
DetailView(animal: "default")
}
}
}
Suppose you want to create a triple-column view (with two sidebars and a detail view). Here is an example to show Projects in the first column, Files in second column and File Content in the last column.
The core step is to add three View()s in NavagationView { ... }.
import SwiftUI
struct MainView: View {
var body: some View {
ProjectsSidebar()
}
}
struct ProjectsSidebar: View {
var body: some View {
NavigationView {
List {
ForEach([1, 2, 3], id: \.self) { project_id in
VStack {
NavigationLink {
FilesSidebar(project_id: project_id)
.navigationTitle("Project \(project_id)")
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.columns)
} label: {
Text("Project \(project_id)")
}
}
}
}
.listStyle(.sidebar)
.navigationTitle("Projects")
.navigationBarTitleDisplayMode(.inline)
FilesSidebar.DefaultView()
DetailView.DefaultView()
}
.navigationViewStyle(.columns)
}
}
struct FilesSidebar: View {
var project_id: Int
var body: some View {
List {
ForEach([1, 2, 3, 4], id: \.self) { file_id in
NavigationLink {
DetailView(project_id: project_id, file_id: file_id)
.navigationTitle("File")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("File \(file_id)")
}
}
}
.listStyle(.sidebar)
}
struct DefaultView: View {
var body: some View {
VStack {
Text("Please select a project.")
}
}
}
}
struct DetailView: View {
var project_id: Int
var file_id: Int
var body: some View {
VStack {
Text("Project \(project_id) - File \(file_id)")
}
}
struct DefaultView: View {
var body: some View {
VStack {
Text("Please select a file.")
}
}
}
}
first launch:
triple columns:
select project and file:

How can you switch views without having a navigationView or an popover?

I am trying to change a view without having something over it like when you used segue in swift. But the only solution I came up with is to have a navigation bar navigationBar or a popover.
struct view1: View {
var body: some View{
Button(action: {
// go to view2``
}) {
Text("press")
}
}
}
struct view2: View {
var body: some View{
Text("yeay")
}
}
If you just want to hide the navigation bar it's easy:
import SwiftUI
struct View2: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("POP")
}
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: View2()) {
Text("PUSH")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you, instead, want to get rid of the NavigationView and NavigationLink views you have to implement your own custom navigation. It's a little more complicated. The following is just a simple example of a push/pop transition between two views.
import SwiftUI
struct View2: View {
#Binding var push: Bool
var body: some View {
ZStack {
Color.yellow
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.push.toggle()
}
}) {
Text("POP")
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct View1: View {
#Binding var push: Bool
var body: some View {
ZStack {
Color.green
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.push.toggle()
}
}) {
Text("PUSH")
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView: View {
#State private var push = false
var body: some View {
ZStack {
if !push {
View1(push: $push)
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading)))
}
if push {
View2(push: $push)
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .trailing)))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Anyone coming to this later might find this to be of interest; in short, shove a hunk of data into #environment, tickle that with a button push in whatever view you want, and since it's at the very top of the overall application stack, it forces a redraw, which acts like a full view navigation, without the potential lost memory and orphaned or lost objects of the whole push/pop navigation view silliness.
It's still a little more "single page app"-ey than I'd like, but since SwiftUI is so crippled in its navigation thoroughness, it'll do nicely.
Not my site, not my link, not my tutorial, and it's buried way down in the list of hits when searching, which is a shame; this is the closest to what many are looking for. IMO, this should be baked into SwiftUI as a first class operation, and made less workaround-ey.
https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
You can also do this completely without NavigationView. Take a look at the following example:
struct MainView: View
{
#State private var showView = "LoginView"
var body: some View
{
switch showView
{
case "LoginView":
Text("Please login.")
Button("Login")
{
showView = "NormalView"
}
case "NormalView":
Text("This is youre NormalView!")
Button("Next view")
{
showView = "NextView"
}
case "NextView":
Text("This is the NextView")
Button("Back")
{
showView = "NormalView"
}
default:
Text("Default") // you should never reach this
}
}
}
Perhaps not the best code practice, but it solves your problem.
I think this not best way but it's easy
struct ContentView: View {
#State var gotoDetail3:Bool = false
var body: some View {
NavigationView{
ZStack{
VStack {
// Normal NavigationLink
NavigationLink {
Text("Detail......")
} label: {
Text("goto..")
}
//use for change state
Button {
gotoDetail3.toggle()
} label: {
Text("goto33333")
}
}// End VStack
// Hide Navigation Link
NavigationLink(
LocalizedStringKey("123"), destination: Text("Subsequent View"),
isActive: $gotoDetail3)
.hidden()
}
}
}
}

How to go to another view with button click

I have a button in my code and I have a file called LogindView.swift
I cannot get the code to open another view file when clicking on the button.
Can anybody give me an example on how to do it.
In my button action I have tried to write LogindView() but i just gives me a warning.
"Result of 'LogindView' initializer is unused"
Button(action: {
// Do action
LogindView()
}, label: {
//** Label text
Text("Logind")
.font(.headline)
.padding(.all)
.foregroundColor(Color.white)
})
.background(Color.blue)
You essentially have 3 options to transition between views depending on your needs.
First, you can use a NavigationView. This will provide a back button and will allow the user to go back. Note that there are some bugs currently when you don't put the NavigationLink inside of a List as per https://stackoverflow.com/a/57122621/3179416
import SwiftUI
struct MasterView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: LoginView()) {
Text("Login")
}
}
.navigationBarTitle(Text("Master"))
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
Second, you can present a modal using .sheet. This will present a modal that appears on top of the current view but it can be dismissed by the user by dragging it down.
import SwiftUI
struct MasterView: View {
#State var isModal: Bool = false
var body: some View {
Button("Login") {
self.isModal = true
}.sheet(isPresented: $isModal, content: {
LoginView()
})
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
Third, you can just use an if statement to change the current view to your Login View like so
import SwiftUI
struct MasterView: View {
#State var showLoginView: Bool = false
var body: some View {
VStack {
if showLoginView {
LoginView()
} else {
Button("Login") {
self.showLoginView = true
}
}
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
If you would like to animate this, so that the transition doesn't appear so abruptly, you can also do this:
import SwiftUI
struct MasterView: View {
#State var showLoginView: Bool = false
var body: some View {
VStack {
if showLoginView {
LoginView()
.animation(.spring())
.transition(.slide)
} else {
Button("Login") {
withAnimation {
self.showLoginView = true
}
}.animation(.none)
}
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
You can use navigation link instead button
var body: some View {
VStack {
Text("Title")
.font(.headline)
Image("myimage").clipShape(Circle())
Text("mytext").font(.title)
NavigationLink(destination: AnotherView()) {
Image(systemName: "person.circle").imageScale(.large)
}
}
}