Using a data-structure for isActive bools for dynamic NavigationLinks to enable a programmatic pop-back doesn't work. Tapping the Pop Back in the Destination should pop the view back to the root DynamicList, but it doesn't work.
import SwiftUI
struct DynamicList: View {
#ObservedObject var listViewModel = ListViewModel()
var body: some View {
NavigationView {
List {
ForEach(0..<self.listViewModel.cities.count, id: \.self) { index in
NavigationLink(
destination: Destination(isActive: $listViewModel.isActiveItems[index], city: $listViewModel.cities[index]),
isActive: $listViewModel.isActiveItems[index],
label: {
Text(listViewModel.cities[index])
})
}
}
.navigationBarTitle(Text("Locations"))
.navigationBarItems(trailing: Button(action: {
self.addRow()
}) {
Image(systemName: "plus")
})
}
}
private func addRow() {
self.listViewModel.addCity(city: ["New York", "London", "Moskau", "Sydney"].randomElement() ?? "")
}
}
class ListViewModel: ObservableObject {
#Published var cities: [String] = []
#Published var isActiveItems: [Bool] = []
func addCity(city: String){
cities.append(city)
isActiveItems.append(false)
}
}
struct Destination: View {
#Binding var isActive: Bool
#Binding var city: String
var body: some View {
Text(city)
Button(action: {
isActive.toggle()
}, label: {
Text("Pop Back")
})
}
}
struct DynamicList_Previews: PreviewProvider {
static var previews: some View {
DynamicList()
}
}
Related
I noticed issue in SwiftUI when using NavigationStack
Once I swipe-back on a half and revert it -> it stops working
Also I attached sample code if you want to try it
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
ListView()
}
}
}
struct ListView: View {
var body: some View {
List {
NavigationLink(destination: ViewA(viewModel: .init()), label: {
Text("A")
})
NavigationLink(destination: ViewB(), label: {
Text("B")
})
}
}
}
struct ViewA: View {
#StateObject var viewModel: Observed
var body: some View {
ZStack {
List {
Button(action: {
viewModel.action()
}, label: {
Text("label")
})
}
NavigationLink(isActive: $viewModel.shouldShowViewB, destination: {
ViewB()
}, label: {EmptyView()})
}
.navigationTitle("view a")
}
}
struct ViewB: View {
var body: some View {
List {
Button(action: {
print("actionb")
}, label: {
Text("labelb")
})
}
.navigationTitle("view b")
}
}
class Observed: ObservableObject {
#Published var shouldShowViewB = false
func action() {
print("action from model")
shouldShowViewB = true
}
}
Expected: whatever I do it should work as expected - when I tap it should open new view
Anyone else found this issue? How to fix it?
Issue 1 is you create the ObservedObject inside the NavigationLink with .init and then have a #StateObject declaration in the Subview ViewA(). That doesn't feel right. Create the Object with #StateObject in the parent view and pass it down.
Issue 2 is the new SwiftUI Navigation model, with NavigationLink)destination: label:) being deprecated. I adapted your code to the new navigation logic:
struct ContentView: View {
var body: some View {
NavigationStack {
ListView()
}
}
}
struct ListView: View {
#StateObject var viewModel = Observed() // create ObservedObject here
var body: some View {
List {
NavigationLink("A") {
ViewA(viewModel: viewModel) // pass down
}
NavigationLink("B") {
ViewB()
}
}
}
}
struct ViewA: View {
#ObservedObject var viewModel: Observed // passed down Object
var body: some View {
ZStack {
List {
Button(action: {
viewModel.action()
print(viewModel.shouldShowViewB)
}, label: {
Text("label")
})
}
.navigationDestination(isPresented: $viewModel.shouldShowViewB, destination: { ViewB() })
}
.navigationTitle("view a")
}
}
struct ViewB: View {
var body: some View {
List {
Button(action: {
print("actionb")
}, label: {
Text("labelb")
})
}
.navigationTitle("view b")
}
}
class Observed: ObservableObject {
#Published var shouldShowViewB = false
func action() {
print("action from model")
shouldShowViewB = true
}
}
I am working on a project that is attempting to present and dismiss views in a NavigationView using state and binding. The reason I am doing this is there is a bug in the #Environment(.presentationMode) var presentaionMode: Binding
model. It's causing odd behavior. It's discussed in this post here.
The example below has three views that are progressively loaded on to the view. The first two ContentView to NavView1 present and dismiss perfectly. However, once NavView2 is loaded, the button that is used to toggle the state of presentNavView2 ends up adding another NavView2 view on the stack and does not dismiss it as expected. Any thoughts as to why this would be?
ContentView
struct ContentView: View {
#State private var presentNavView1 = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: NavView1(presentNavView1: self.$presentNavView1), isActive: self.$presentNavView1, label: {
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("To NavView1")
}) // Button
}) // NavigationLink
} // List
.navigationTitle("Home")
} // NavigationView
} // View
}
NavView1
struct NavView1: View {
#State private var presentNavView2 = false
#Binding var presentNavView1: Bool
var body: some View {
List {
NavigationLink(destination: NavView2(presentNavView2: self.$presentNavView2), isActive: self.$presentNavView2, label: {
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("To NavView2")
}) // Button
}) // NavigationLink
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("Back")
})
} // List
.navigationTitle("NavView1")
} // View
}
NavView2
struct NavView2: View {
#Binding var presentNavView2: Bool
var body: some View {
VStack {
Text("NavView2")
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("Back")
}) // Button
} // VStack
.navigationTitle("NavView2")
}
}
You can use DismissAction, because PresentationMode will be deprecated. I tried the code and it works perfectly! Here you go!
import SwiftUI
struct MContentView: View {
#State private var presentNavView1 = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: NavView1(), isActive: self.$presentNavView1, label: {
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("To NavView1")
})
})
}
.navigationTitle("Home")
}
}
}
struct NavView1: View {
#Environment(\.dismiss) private var dismissAction: DismissAction
#State private var presentNavView2 = false
var body: some View {
List {
NavigationLink(destination: NavView2(), isActive: self.$presentNavView2, label: {
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("To NavView2")
})
})
Button(action: {
self.dismissAction.callAsFunction()
}, label: {
Text("Back")
})
}
.navigationTitle("NavView1")
}
}
struct NavView2: View {
#Environment(\.dismiss) private var dismissAction: DismissAction
var body: some View {
VStack {
Text("NavView2")
Button(action: {
self.dismissAction.callAsFunction()
}, label: {
Text("Back")
})
}
.navigationTitle("NavView2")
}
}
struct MContentView_Previews: PreviewProvider {
static var previews: some View {
MContentView()
}
}
I'm trying to build out a simple navigation where you can click on items in a link and pop back to the root controller from a sheet view. As you can see from the video below, when I tap on an item in the list, the wrong item is loaded (there's an offset between the row I click and the one that gets highlighted and loaded).
I also get the error SwiftUI encountered an issue when pushing aNavigationLink. Please file a bug.
Here's all my code:
import SwiftUI
struct ContentView: View {
#State var rootIsActive:Bool = false
var body: some View {
NavigationView{
AllProjectView(rootIsActive: self.rootIsActive)
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
.environment(\.rootPresentationMode, self.$rootIsActive)
}
}
struct AllProjectView: View {
#State var rootIsActive:Bool = false
#State var projects: [String] = ["1", "2", "3"]
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
ProjectItem(name: self.$projects[idx], rootIsActive: self.$rootIsActive)
}
}.navigationBarTitle("All Projects")
}
}
struct ProjectItem: View{
#Binding var name: String
#Binding var rootIsActive: Bool
init(name: Binding<String>, rootIsActive: Binding<Bool>){
self._name = name
self._rootIsActive = rootIsActive
}
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name),
isActive: self.$rootIsActive){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#State var isShowingSheet: Bool = false
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
#Environment(\.rootPresentationMode) private var rootPresentationMode: Binding<RootPresentationMode>
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop view")
self.presentationMode.wrappedValue.dismiss()
print("pop root")
self.rootPresentationMode.wrappedValue.dismiss()
}
}
.navigationBarTitle("Project View")
}
}
// from https://stackoverflow.com/a/61926030/1720985
struct RootPresentationModeKey: EnvironmentKey {
static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}
extension EnvironmentValues {
var rootPresentationMode: Binding<RootPresentationMode> {
get { return self[RootPresentationModeKey.self] }
set { self[RootPresentationModeKey.self] = newValue }
}
}
typealias RootPresentationMode = Bool
extension RootPresentationMode {
public mutating func dismiss() {
self.toggle()
}
}
You only have one isRootActive variable that you're using. And, it's getting repeated for each item on the list. So, as soon as any item on the list is tapped, the isActive property for each NavigationLink turns to true.
Beyond that, your isRootActive isn't actually doing anything right now, since your "Return to root" button already does this:
self.isShowingSheet = false
self.presentationMode.wrappedValue.dismiss()
At that point, there's nothing more to dismiss -- it's already back at the root view.
My removing all of the root and isActive stuff, you get this:
struct ContentView: View {
var body: some View {
NavigationView{
AllProjectView()
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct AllProjectView: View {
#State var projects: [String] = ["1", "2", "3"]
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
ProjectItem(name: self.$projects[idx])
}
}.navigationBarTitle("All Projects")
}
}
struct ProjectItem: View{
#Binding var name: String
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name)
){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#State var isShowingSheet: Bool = false
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop view")
self.presentationMode.wrappedValue.dismiss()
}
}
.navigationBarTitle("Project View")
}
}
If you had an additional view in the stack, you would need a way to keep track of if the root were active. I've used a custom binding here that converts an optional String representing the project's name to a Bool value that gets passed down the view hierarchy:
struct ContentView: View {
var body: some View {
NavigationView{
AllProjectView()
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct AllProjectView: View {
#State var projects: [String] = ["1", "2", "3"]
#State var activeProject : String?
func activeBindingForProject(name : String) -> Binding<Bool> {
.init {
name == activeProject
} set: { newValue in
activeProject = newValue ? name : nil
}
}
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
InterimProjectView(name: self.$projects[idx],
isActive: activeBindingForProject(name: self.projects[idx]))
}
}.navigationBarTitle("All Projects")
}
}
struct InterimProjectView: View {
#Binding var name : String
#Binding var isActive : Bool
var body : some View {
NavigationLink(destination: ProjectItem(name: $name, isActive: $isActive),
isActive: $isActive) {
Text("Next : \(isActive ? "true" : "false")")
}
}
}
struct ProjectItem: View {
#Binding var name: String
#Binding var isActive: Bool
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name, isActive: $isActive)
){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#Binding var isActive : Bool
#State var isShowingSheet: Bool = false
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop root")
self.isActive.toggle()
}
}
.navigationBarTitle("Project View")
}
}
I have gut a simple project with a struct inside of a struct and want to add at first the names and hobbies of a single user and than want to add this user to a whole pool of users. The code is the following:
import SwiftUI
struct User: Identifiable, Hashable {
var id = UUID()
var firstName = ""
var lastName = ""
var hobbiesOfUser = [Hobbies]()
}
struct Hobbies: Identifiable, Hashable {
var id = UUID()
var nameOfHobby = ""
var nameClub = ""
}
class UsersStorage: ObservableObject {
#Published var users = [User]()
}
struct ContentView: View {
#EnvironmentObject var userStorage: UsersStorage
#State private var isPresented = false
var body: some View {
NavigationView {
List(userStorage.users) { singleUser in
VStack {
HStack {
Text(singleUser.firstName)
Text(singleUser.lastName)
}
}
}
.navigationBarItems(trailing:
Button(action: {
self.isPresented = true
}) {
Text("New User")
}.sheet(isPresented: $isPresented, onDismiss: {
self.isPresented = false
}) {
AddUserView(isPresented: self.$isPresented, user: User()).environmentObject(self.userStorage)
}
)
}
}
}
struct AddUserView: View {
#EnvironmentObject var userStorage: UsersStorage
#Binding var isPresented: Bool
#State var user: User
#State var hobbiesOfUser = [Hobbies]()
var body: some View {
NavigationView {
VStack {
Text("First Name")
TextField("First Name", text: $user.firstName)
Text("Last Name")
TextField("Last Name", text: $user.lastName)
NavigationLink(destination: AddHobbieView(hobbie: Hobbies())) {
Text("Add New Hobbie")
}
List(user.hobbiesOfUser) { singleHobbie in
VStack {
HStack {
Text(singleHobbie.nameOfHobby)
Text(singleHobbie.nameClub)
}
}
}
HStack {
Button(action: {
self.isPresented = false
}){
Text("Cancel")
}
Button(action: {
self.userStorage.users.append(self.user)
self.isPresented = false
}){
Text("Save")
}
}
}
}
}
}
struct AddHobbieView: View {
#EnvironmentObject var userStorage: UsersStorage
#State var hobbie: Hobbies
var body: some View {
VStack {
Text("Hobby")
TextField("First Name", text: $hobbie.nameOfHobby)
Text("Club")
TextField("Last Name", text: $hobbie.nameClub)
HStack {
Button(action: {
// self.userStorage.users.append(self.hobbie)
}){
Text("Cancel")
}
Button(action: {
}){
Text("Save")
}
}
}
}
}
My question is: how can I add hobbies to the list in the AddUserView and get the buttons in the AddHobbieView let me go back to the AddUserView.
You add #Environment(\.presentationMode) var presentationMode to AddHobbieView and call self.presentationMode.wrappedValue.dismiss() when you want to dismiss the view
I hope this helps!
The code for the basic app from Anlil's answer works fine. If I edit the datamodel to be more like mine, with a multidimensional String array, I get something like:
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var dm: DataManager
var body: some View {
NavigationView {
List {
NavigationLink(destination:AddView().environmentObject(self.dm)) {
Image(systemName: "plus.circle.fill").font(.system(size: 30))
}
ForEach(dm.array, id: \.self) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item[0])
}
}
}
}
}
}
struct DetailView: View {
var item : [String] = ["", "", ""]
var body: some View {
VStack {
Text(item[0])
Text(item[1])
Text(item[2])
}
}
}
struct AddView: View {
#EnvironmentObject var dm: DataManager
#State var item0 : String = "" // needed by TextField
#State var item1 : String = "" // needed by TextField
#State var item2 : String = "" // needed by TextField
#State var item : [String] = ["", "", ""]
var body: some View {
VStack {
TextField("Write something", text: $item0)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
TextField("Write something", text: $item1)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
TextField("Write something", text: $item2)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button(action: {
self.item = [self.item0, self.item1, self.item2]
print(self.item)
self.dm.array.append(self.item)
}) {
Text("Save")
}
}
}
}
class DataManager: BindableObject {
var willChange = PassthroughSubject<Void, Never>()
var array : [[String]] = [["Item 1","Item 2","Item 3"],["Item 4","Item 5","Item 6"],["Item 7","Item 8","Item 9"]] {
didSet {
willChange.send()
}
}
}
There are no errors and the code runs as expected. Before I'm going to rewrite my own code (with the lessons I've learned solar) it would be nice if the code could be checked.
I'm really impressed with SwiftUI!
If your "source of truth" is an array of some "model instances", and you just need to read values, you can pass those instance around like before:
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var dm: DataManager
var body: some View {
NavigationView {
List(dm.array, id: \.self) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item)
}
}
}
}
}
struct DetailView: View {
var item : String
var body: some View {
Text(item)
}
}
class DataManager: BindableObject {
var willChange = PassthroughSubject<Void, Never>()
let array = ["Item 1", "Item 2", "Item 3"]
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(DataManager())
}
}
#endif
You need to pass the EnvironmentObject only if some views are able to manipulate the data inside the instances... in this case you can easily update the EnvironmentObject's status and everything will auto-magically updated everywhere!
The code below shows a basic App with "list", "detail" and "add", so you can see 'environment' in action (the only caveat is that you have to manually tap < Back after tapped the Save button). Try it and you'll see the list that will magically update.
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var dm: DataManager
var body: some View {
NavigationView {
List {
NavigationLink(destination:AddView().environmentObject(self.dm)) {
Image(systemName: "plus.circle.fill").font(.system(size: 30))
}
ForEach(dm.array, id: \.self) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item)
}
}
}
}
}
}
struct DetailView: View {
var item : String
var body: some View {
Text(item)
}
}
struct AddView: View {
#EnvironmentObject var dm: DataManager
#State var item : String = "" // needed by TextField
var body: some View {
VStack {
TextField("Write something", text: $item)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button(action: {
self.dm.array.append(self.item)
}) {
Text("Save")
}
}
}
}
class DataManager: BindableObject {
var willChange = PassthroughSubject<Void, Never>()
var array : [String] = ["Item 1", "Item 2", "Item 3"] {
didSet {
willChange.send()
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(DataManager())
}
}
#endif