I am trying to have two NavigationLinks inside a struct that I use as a NavigationLink inside a List with ForEach but the code does not work (the navigation does not happen to either View) , here is the code:
struct mainView : View {
#State var people = [Person(name: "one", family: "1", type: "men"),Person(name: "two", family: "2", type: "women")]
var body : some View {
List{
ForEach(self.people, id:\.self){person in
NavigationLink(destination: Text("hi")) {
PersonView(person : person)
}.buttonStyle(BorderlessButtonStyle())
}
}
}
struct Person {
var name : String
var family : String
var type : String
}
struct PersonView : View {
#State var person : Person?
var body : some View {
HStack{
NavigationLink(destination: Text(self.person.name)) {
Text("this is \(self.person.name) \(self.person.family)")
}
NavigationLink(destination: Text(self.person.type)) {
Text(self.person.type)
}
}
}
}
I have added the .buttonStyle after reading it might help but it did nothing. I also tried the following PersonView :
struct PersonView : View {
#State var person : Person?
#State var goToFirst = false
#State var goToType = false
var body : some View {
VStack{
Button(action:{
self.goToFirst.toggle()
}){
Text("this is \(self.person.name) \(self.person.family)")
}.buttonStyle(BorderlessButtonStyle())
Button(action:{
self.goToType.toggle()
}){
Text(self.person.type)
}.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: Text(self.person.name), isActive : self.$goToFirst) {
Text("").frame(width: 0.01, height: 0.01)
}
NavigationLink(destination: Text(self.person.type), isActive : self.$goToType) {
Text("").frame(width: 0.01, height: 0.01)
}
}
}
This did not work as well, what am I supposed to do so when I click anywhere on the cell it will navigate to Text("hi") and when I click on the respective areas it will navigate to the proper View .
Important: This view is getting navigated to from a view that is wrapped with a NavigationView
You have not added a navigationView, that is the problem.
struct MainView: View {
#State private var peoples = [Person(name: "one", family: "1", type: "men"),Person(name: "two", family: "2", type: "women")]
var body: some View {
NavigationView { //This is important
List {
ForEach(self.peoples, id: \.name) { people in
NavigationLink(destination: PersonView(person: people)) {
Text(people.name)
}
}
}
.navigationTitle("Demo")
}
}
}
struct Person {
var name : String
var family : String
var type : String
}
struct PersonView : View {
#State var person : Person?
var body : some View {
HStack{
NavigationLink(destination: Text(self.person.name)) {
Text("this is \(self.person.name) \(self.person.family)")
}
NavigationLink(destination: Text(self.person.type)) {
Text(self.person.type)
}
}
}
}
I think the following does what you want. The code can be pasted into an empty project.
Some clarifications:
The List seems to make things more difficult, so I replaced it with a VStack. You may need to surround it with a ScrollView.
Also, like user Asperi hinted, this works best when you create multiple NavigationLink but around an EmptyView that is then later activated.
struct MainView: View {
#State var people = [Person(name: "one", family: "1", type: "men"), Person(name: "two", family: "2", type: "women")]
var body: some View {
VStack {
ForEach(self.people, id: \.self) { person in
PersonView(person: person)
}
}
}
}
struct Person: Hashable {
var name: String
var family: String
var type: String
}
struct PersonView: View {
let person: Person
#State private var selection: Int?
var body: some View {
HStack {
NavigationLink(
destination: Text(self.person.name),
tag: self.person.hashValue + 1,
selection: self.$selection) {
EmptyView()
}
NavigationLink(
destination: Text(self.person.type),
tag: self.person.hashValue + 2,
selection: self.$selection) {
EmptyView()
}
Button("this is \(self.person.name) \(self.person.family)") {
self.selection = self.person.hashValue + 1
}
Spacer()
Button(self.person.type) {
self.selection = self.person.hashValue + 2
}.padding()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MainView()
}
}
}
Related
I have 3 views. The issue I have is that the 1st one (a parent view of 2) is not changing when the 3rd one (a children of view 2) has updated a property in the array.
Let's use some code so it is easier to understand:
public struct Item {
public let id: String
public var name: String?
public var inStock: Bool
import SwiftUI
#main
struct ThisIsMessedUpApp: App {
var body: some Scene {
WindowGroup {
ItemsMainView()
}
}
}
import Foundation
import SwiftUI
struct ItemsMainView: View {
#State var items = [Item]()
var body: some View {
VStack {
Text("Item count is \(items.count)")
Divider()
VStack {
Text("ItemsMainView has:")
HStack {
ForEach(self.items, id: \.id) { item in
Text(item.name ?? "nothing found")
Spacer()
Text(item.inStock.description)
}
}
}
ItemsView(items: $items)
}
}
}
struct ItemsView: View {
#Binding var items: [Item]
var body: some View {
Button("Add new item (call made from ItemsView", action: {
self.items.append(Item(id: UUID().uuidString,
name: "Test #1",
inStock: false))
})
VStack {
ForEach($items, id: \.id) { $item in
ItemView(item: $item)
}
}
}
}
struct ItemView: View {
#Binding var item: Item
#State var draftItemName: String = ""
var body: some View {
Text("ItemView has")
HStack {
TextField("TextField", text: $draftItemName)
.onSubmit {
item.name = draftItemName
}
Spacer()
Text(item.inStock.description)
}
.onAppear {
draftItemName = item.name ?? ""
}
}
}
Some of the Text are for debugging purposes.
If you run this code and change the second TextField's value to, say, "Test #2", you will see that you end up with an inconsistent UI state: ItemsMainView has "Test #1", whereas ItemView has "Test #2"
In Xcode 14.2, create a new project for ios or macos. Copy and paste the following code, replacing
the original ContentView.
Tell us if this code, tested on real ios 16.3 devices (not Previews), macCatalyst and MacOS 13.2 only, works for you.
struct ContentView: View {
var body: some View {
ItemsMainView()
}
}
public struct Item {
public let id: String
public var name: String?
public var inStock: Bool
}
struct ItemsMainView: View {
#State var items = [Item]()
var body: some View {
VStack {
Text("Item count is \(items.count)")
Divider()
VStack {
Text("ItemsMainView has:")
ForEach(items, id: \.id) { item in
HStack {
Text(item.name ?? "nothing found").foregroundColor(.red)
Spacer()
Text(item.inStock.description).foregroundColor(.red)
}
}
}
ItemsView(items: $items)
}
}
}
struct ItemsView: View {
#Binding var items: [Item]
var body: some View {
Button("Add new item (call made from ItemsView", action: {
self.items.append(Item(id: UUID().uuidString, name: "Test #1", inStock: false))
}).buttonStyle(.bordered)
VStack {
ForEach($items, id: \.id) { $item in
ItemView(item: $item)
}
}
}
}
struct ItemView: View {
#Binding var item: Item
#State var draftItemName: String = ""
var body: some View {
Text("ItemView has")
HStack {
TextField("TextField", text: $draftItemName)
.onSubmit {
item.name = draftItemName
}
Spacer()
Text(item.inStock.description)
}
.onAppear {
draftItemName = item.name ?? ""
}
}
}
Supposing I have a simple view with a list:
struct ContentView: View {
var names = ["Bob","John","Lisa"]
var body: some View {
List {
ForEach (names, id: \.self) {name in
Detail(name: name)
}
}
}
}
How would I achieve for the list item to change on press? The Detail would presumably look something like:
struct Detail: View {
var name: String
var body: some View {
if !inFocus {
Text(name)
} else {
Text("This is a detail view")
Text(name)
Text("---------------------")
}
}
}
ou can try a few things like this with #State variable
struct Detail: View {
var name: String
#State var inFocus = false
var body: some View {
if !inFocus {
Text(name)
.onTapGesture{
self.inFocus.toggle()
}
} else {
VStack{
Text("This is a detail view")
Text(name)
Text("---------------------")
}
.onTapGesture {
self.inFocus.toggle()
}
}
}
}
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 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()
}
}
}
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