Pushing multiple navigation links from a parent view in SwiftUI - swiftui

I want to implement a wizard whereby the user has to go through multiple screens in order to complete a signup process.
In SwiftUI the easiest way to do this is to have each view when it's finished push the next view on the navigation stack, but this codes the entire navigation between views in the views themselves, and I would like to avoid it.
What I want to do is have a parent view show the navigation view and then push the different steps on that navigation view.
I have something working already that looks like this:
struct AddVehicleView: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
NavigationView {
switch viewModel.state {
case .description:
AddDescriptionView(addDescriptionViewModel: AddVehicleDescriptionViewModel(), addVehicleViewModel: viewModel)
case .users:
AddUsersView(viewModel: AddUsersViewModel(viewModel.vehicle), addVehicleViewModel: viewModel)
}
}
}
}
This works fine. In the first step the AddVehicleViewModel is updated with the necessary info, the AddVehicleView is re-evaluated, the switch case jumps to the next option and the next view is presented to complete the wizard.
The issue with this however is that there are no navigation stack animations. Views simply get replaced. How can I change this to a system whereby the views are pushed, without implementing the push inside the AddDescriptionView object?
Should I write wrapper views that do the navigation stack handling on top of those views, and get rid of the switch case?

Ok so if you want to go from view a to b you should implement this not in your NavigationView but the view after the NavigationView, this way you wont break the animations. Why? Good question, I really don't know. When possible I keep my NavigationView always in the App struct under WindowGroup.
To get back to the point. Basically there should be an intermediate view between your steps and NavigationView. This view (StepperView) will contain the navigation logic of your steps. This way you keep the animations intact.
import SwiftUI
class AddVehicleViewModel: ObservableObject {
enum StateType {
case description
case users1
case users2
}
#Published var state: StateType? = nil
}
struct AddDescriptionView: View {
#ObservedObject var viewModel: AddVehicleViewModel
#State var text: String = ""
var body: some View {
GeometryReader {proxy in
VStack {
TextField("test", text: self.$text).background(RoundedRectangle(cornerRadius: 10).fill(Color.white).frame(width: 150, height: 40)).padding()
Button("1") {
viewModel.state = .users1
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center).background(Color.orange)
}
}
}
struct AddUsersView: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
GeometryReader {proxy in
ZStack {
Button("2") {
viewModel.state = .users2
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/).background(Color.orange)
}
}
}
struct AddUsersView2: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
GeometryReader {proxy in
ZStack {
Button("3") {
viewModel.state = .description
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/).background(Color.orange)
}
}
}
struct StepperView: View {
#ObservedObject var viewModel: AddVehicleViewModel = AddVehicleViewModel()
var body: some View {
VStack {
NavigationLink(
destination: AddDescriptionView(viewModel: viewModel),
isActive: .constant(viewModel.state == .description),
label: {EmptyView()})
if viewModel.state == .users1 {
NavigationLink(
destination: AddUsersView(viewModel: viewModel),
isActive: .constant(true),
label: {EmptyView()})
}
if viewModel.state == .users2 {
NavigationLink(
destination: AddUsersView2(viewModel: viewModel),
isActive: .constant(true),
label: {EmptyView()})
}
}.onAppear {
viewModel.state = .description
}
}
}
class BackBarButtonItem: UIBarButtonItem {
#available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// Don't set the menu here
// super.menu = menu
}
get {
return super.menu
}
}
}
struct AddVehicleView: View {
#ObservedObject var viewModel: AddVehicleViewModel = AddVehicleViewModel()
var body: some View {
NavigationView {
NavigationLink(
destination: StepperView(),
isActive: .constant(true),
label: {EmptyView()})
}
}
}

Related

SwiftUI - Navigation title background becomes transparent when VStack is visible

I'm running into an issue with the navigation title header in SwiftUI. It's a combination of a couple of things, as far as I can tell...
The main problem is that I'm trying to change the default background color of a view that contains a list. But when I use the tag .background(), the navigation title background becomes transparent. This only happens when there is a VStack on the view.
I have a simplify example code that shows the problem I'm facing:
ContentView:
import SwiftUI
struct ContentView: View {
#State var showButton: Bool
var body: some View {
VStack {
NavigationStack {
NavigationLink(
destination: SecondView(showButton: showButton),
label: {
Text("Take me to second view")
})
Toggle("VStack Visibile", isOn: $showButton)
.padding()
}
}
}
}
SecondView:
import SwiftUI
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#State var showButton: Bool = true
var body: some View {
VStack {
List(0..<10) { _ in
Text("Hello World")
}
if showButton {
button
}
}
.navigationTitle("This is a title")
.background(Color(.systemCyan))
}
var button: some View {
Text("Something")
}
}
Please see below the resulting problem:
Issues / Suggestions:
ContentView
Have the NavigationStack outside the VStack
SecondView
Don't embed List inside a VStack
List is special and has special characteristics
Don't initialise #State property from outside, pass a binding instead
Code:
ContentView:
struct ContentView: View {
#State var showButton = true
var body: some View {
NavigationStack {
VStack {
NavigationLink(
destination: SecondView(showButton: $showButton),
label: {
Text("Take me to second view")
})
Toggle("VStack Visibile", isOn: $showButton)
.padding()
}
}
}
}
SecondView
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#Binding var showButton: Bool
var body: some View {
List {
ForEach(0..<100) { _ in
Text("Hello World")
}
}
.safeAreaInset(edge: .bottom) {
if showButton {
HStack {
Spacer()
button
Spacer()
}
//I have added transparency, you can make it opaque if you want
.background(.cyan.opacity(0.8))
}
}
}
var button: some View {
Text("Something")
}
}
Try this if you don't want your list go under nav bar.
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#State var showButton: Bool = true
var body: some View {
VStack {
List(0..<10) { _ in
Text("Hello World")
}
.padding(.top, 1)
if showButton {
button
}
}
.background(Color(.systemCyan))
.navigationTitle("This is a title")
}
var button: some View {
Text("Something")
}
}

SwiftUI Observable object doesn't update UI on first attempt

Issue
The #ObservableObject ViewModel only updates the UI after closing and re-opening the view. Once this update has happened, every subsequent change in the #Published var inside the #ObservableObject triggers an automatic update.
Context
I have a model called 'Stack' in my project.
struct Stack: Identifiable, Equatable {
var id = UUID()
var title: String
var isPinned = false
}
I have a view (NavigationMenuView) which presents all of the stacks in a scroll view
import SwiftUI
struct NavigationMenuView: View {
#ObservedObject var viewModel = NavigationMenuViewModel()
#EnvironmentObject var environmentObjects: EnvironmentObjects
var body: some View {
ZStack{
NavigationView {
VStack(alignment: .leading){
ScrollView {
ForEach(viewModel.stacks){ stack in
StackRow(
stack: stack,
lftButtonAction: {
self.viewModel.toggleIsPinned(for: stack)
},
rhtButtonAction: {
self.viewModel.deleteStack(stack: stack)
}
)
}
}
}
.navigationBarTitle(Text("Home"))
.navigationBarItems(trailing: addNewStackButton)
.background(Color(UIColor.systemGray6).edgesIgnoringSafeArea(.all))
}.zIndex(1)
if environmentObjects.newStackViewIsShown {
VStack{
Spacer()
NewStackView(viewModel: NewStackViewModel())
}
.onDisappear{
self.viewModel.fetchStacks()
}
.zIndex(2)
}
}
.onAppear{
self.viewModel.fetchStacks()
}
}
}
The view model for this view has a #Published var stacks = [Stack]() array which gets updates by fetchStacks().
The NavigationMenuView has a subview (NewStackView) which creates a new 'stack' (using the ViewModel) using this button:
Button(action: {
if !self.isAddButtonDisabled{
self.viewModel.addNewStack(title: self.title)
self.environmentObjects.newStackViewIsShown = false
}
}) {
Text("Add")
.padding(.vertical, 16.0)
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(Color(UIColor.systemBackground))
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(addButtonColor)
)
}
.disabled(isAddButtonDisabled)
My own progress
I have found out that this only occurs when the NewStackView is presented as a subview of the NavigationMenuView. When presenting it as a modal view using .sheet() the NewStackView updates straight away.

SwiftUI, setting title to child views of TabView inside of NavigationView does not work

Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.
But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:
import SwiftUI
enum Gender: String {
case female, male
}
let members: [Gender: [String]] = [
Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]
struct TabItem: View {
let image: String
let label: String
var body: some View {
VStack {
Image(systemName: image).imageScale(.large)
Text(label)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
ListView(gender: .female).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
ListView(gender: .male).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
}
}
}
struct ListView: View {
let gender: Gender
var body: some View {
let names = members[gender]!
return List {
ForEach(0..<names.count, id: \.self) { index in
NavigationLink(destination: DetailView(name: names[index])) {
Text(names[index])
}
}
}.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
}
}
struct DetailView: View {
let name: String
var body: some View {
ZStack {
VStack {
Text("profile views")
}
VStack {
Spacer()
HStack {
Spacer()
TabItem(image: "pencil.circle", label: "Edit")
Spacer()
TabItem(image: "minus.circle", label: "Delete")
Spacer()
}
}
}
.navigationBarTitle(Text(name), displayMode: .inline)
}
}
What I could do is to have a #State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.
The idea is to join TabView selection with NavigationView content dynamically.
Demo:
Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.
struct TestTabsOverNavigation: View {
#State private var tabVisible = true
#State private var selectedTab: Int = 0
var body: some View {
ZStack(alignment: .bottom) {
contentView
tabBar
}
}
var contentView: some View {
NavigationView {
ListView(gender: selectedTab == 0 ? .female : .male)
.onAppear {
withAnimation {
self.tabVisible = true
}
}
.onDisappear {
withAnimation {
self.tabVisible = false
}
}
}
}
var tabBar: some View {
TabView(selection: $selectedTab) {
Rectangle().fill(Color.clear).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
Rectangle().fill(Color.clear).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
.frame(height: 50) // << !! might be platform dependent
.opacity(tabVisible ? 1.0 : 0.0)
}
}
This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:
#State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
NavigationView{
TabView(selection: $selectedTab, content:{
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text(pageTitles[0])
}.tag(0)
CustomerListView()
.tabItem {
Image(systemName: "rectangle.stack.person.crop.fill")
Text(pageTitles[1])
}.tag(1)
SaleView()
.tabItem {
Image(systemName: "tag.fill")
Text(pageTitles[2])
}.tag(2)
MoreView()
.tabItem {
Image(systemName: "ellipsis.circle.fill")
Text(pageTitles[3])
}.tag(3)
})
.navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)
.font(.headline)
}
}

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

Show a new View from Button press Swift UI

I would like to be able to show a new view when a button is pressed on one of my views.
From the tutorials I have looked at and other answered questions here it seems like everyone is using navigation button within a navigation view, unless im mistaken navigation view is the one that gives me a menu bar right arrows the top of my app so I don't want that. when I put the navigation button in my view that wasn't a child of NavigationView it was just disabled on the UI and I couldn't click it, so I guess I cant use that.
The other examples I have seen seem to use presentation links / buttons which seem to show a sort of pop over view.
Im just looking for how to click a regular button and show another a view full screen just like performing a segue used to in the old way of doing things.
Possible solutions
1.if you want to present on top of current view(ex: presentation style in UIKit)
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
2.if you want to reset current window scene stack(ex:after login show home screen)
Button(action: goHome) {
HStack(alignment: .center) {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
func goHome() {
if let window = UIApplication.shared.windows.first {
window.rootViewController = UIHostingController(rootView: HomeScreen())
window.makeKeyAndVisible()
}
}
3.push new view (ex: list->detail, navigation controller of UIKit)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}.navigationBarTitle("Navigation")
}
}
}
}
4.update the current view based on #state property, (ex:show error message on login failure)
struct ContentView: View {
#State var error = true
var body: some View {
...
... //login email
.. //login password
if error {
Text("Failed to login")
}
}
}
For simple example you can use something like below
import SwiftUI
struct ExampleFlag : View {
#State var flag = true
var body: some View {
ZStack {
if flag {
ExampleView().tapAction {
self.flag.toggle()
}
} else {
OtherExampleView().tapAction {
self.flag.toggle()
}
}
}
}
}
struct ExampleView: View {
var body: some View {
Text("some text")
}
}
struct OtherExampleView: View {
var body: some View {
Text("other text")
}
}
but if you want to present more view this way looks nasty
You can use stack to control view state without NavigationView
For Example:
class NavigationStack: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var list: [AuthState] = []
public func push(state: AuthState) {
list.append(state)
didChange.send()
}
public func pop() {
list.removeLast()
didChange.send()
}
}
enum AuthState {
case mainScreenState
case userNameScreen
case logginScreen
case emailScreen
case passwordScreen
}
struct NavigationRoot : View {
#EnvironmentObject var state: NavigationStack
#State private var aligment = Alignment.leading
fileprivate func CurrentView() -> some View {
switch state.list.last {
case .mainScreenState:
return AnyView(GalleryState())
case .none:
return AnyView(LoginScreen().environmentObject(state))
default:
return AnyView(AuthenticationView().environmentObject(state))
}
}
var body: some View {
GeometryReader { geometry in
self.CurrentView()
.background(Image("background")
.animation(.fluidSpring())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height,
alignment: self.aligment))
.edgesIgnoringSafeArea(.all)
.onAppear {
withAnimation() {
switch self.state.list.last {
case .none:
self.aligment = Alignment.leading
case .passwordScreen:
self.aligment = Alignment.trailing
default:
self.aligment = Alignment.center
}
}
}
}
.background(Color.black)
}
}
struct ExampleOfAddingNewView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.push(state: .emailScreen) }){
Text("Tap me")
}
}
}
}
struct ExampleOfRemovingView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.pop() }){
Text("Tap me")
}
}
}
}
In my opinion this bad way, but navigation in SwiftUI much worse