This looks like a very simple thing, but I can't figure out how to do this:
I have a List embedded in a NavigationView, containing a NavigationLink to view the detail of the item.
I have a save bar button where I would like to save the selected item. But how can I access the selected item?
It isn't visible in the button's action closure.
struct ItemList : View {
#EnvironmentObject var items: ItemsModel
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(destination: ItemDetail(item: item)) {
Text(item.name)
}
}
.navigationBarTitle(Text("Item"))
.navigationBarItems(trailing: Button(action: {
self.save(/*item: item */) // How can I access item here?
}, label: {
Text("Save")
}))
}
}
func save(item: Item) {
print("Saving...")
}
}
Navigation links are not obligatory to accomplish this.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
var id: Self { self }
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var selectedOceans = [Ocean]()
#State private var multiSelection = Set<Ocean.ID>()
var body: some View {
VStack {
Text("Oceans")
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// Walkaround: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
selectedOceans = Array(multiSelection)
print(selectedOceans)
})
}
Divider()
Text("Selected oceans")
List(selectedOceans, selection: $multiSelection) {
Text($0.name)
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Related
There is a list in on the Main View that has navigation links that bring you to a an Edit Birthday View where the textFieldName is saved with the onAppear method. I need help in allowing the user to change the text in the text field on the Edit Birthday View and having it save when the user dismisses and returns to that particular item in the foreach list. I have tried onEditingChanged and on change method but they don't seem to work. (Also, in my view model i append birthday items when they are created in the Add Birthday View). If you would like to see more code i will make updates. Thank you.
/// MAIN VIEW
import SwiftUI
struct MainView: View {
#EnvironmentObject var vm: BirthdayViewModel
#State var nameTextField: String = ""
var body: some View {
VStack(spacing: 20) {
List {
ForEach(vm.searchableUsers, id: \.self) { birthday in
NavigationLink(destination: EditBirthdayView(birthday: birthday)) {
BirthdayRowView(birthday: birthday)
}
.listRowSeparator(.hidden)
}
.onDelete(perform: vm.deleteBirthday)
}
}
.toolbar {
ToolbarItem {
NavigationLink(destination: AddBirthdayView(textfieldName: $nameTextField)) {
Image(systemName: "plus.circle")
}
}
}
}
}
/// EDIT BIRTHDAY VIEW
import SwiftUI
import Combine
struct EditBirthdayView: View {
#EnvironmentObject var vm: BirthdayViewModel
#State var textfieldName: String = ""
#Environment(\.presentationMode) var presentationMode
var birthday: BirthdayModel
var body: some View {
NavigationView {
VStack {
TextField("Name...", text: $textfieldName)
}
Button {
saveButtonPressed()
} label: {
Text("Save")
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()) {
textfieldName = birthday.name
}
}
}
}
func saveButtonPressed() {
vm.updateItem(birthday: birthday)
presentationMode.wrappedValue.dismiss()
}
func updateTextField() {
textfieldName = birthday.name
}
}
struct MainView: View {
#EnvironmentObject var store: BirthdayStore
var body: some View {
List {
ForEach($store.birthdays) { $birthday in
NavigationLink(destination: EditBirthdayView(birthday: $birthday)) {
BirthdayRowView(birthday: birthday)
}
}
.onDelete(perform: deleteBirthday)
}
.listRowSeparator(.hidden)
.toolbar {
ToolbarItem {
NavigationLink(destination: AddBirthdayView() {
Image(systemName: "plus.circle")
}
}
}
}
}
struct EditBirthdayView: View {
#EnvironmentObject var store: BirthdayStore
#Binding var birthday: Birthday
...
TextField("Name", text: $birthday.name)
My environmentObject isn't working.I tap on navigationLink and see nothing in there.
I change note but it does not get updated.I made viewModel and share data from it everywhere I need it
I made the second TextEditor to do changes to my notes, but I cannot see changes.I just want to write smith and data should be updated
So how can I fix that?
import SwiftUI
#main
struct WhatToDoAppApp: App {
#StateObject private var vm = NoteViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vm)
}
}
}
//ContentView.swift
import SwiftUI
struct ContentView: View {
#EnvironmentObject var vm: NoteViewModel
#State private var showSheet = false
#State private var searchText = ""
var body: some View {
NavigationView {
List {
ForEach(vm.notes) { item in
NavigationLink(destination: NoteDetailView()) {
Text(item.task)
.lineLimit(1)
}
}
.onDelete(perform: vm.deleteTask)
.onMove(perform: vm.moveTask)
}
.searchable(text: $searchText) {
if !searchResult.isEmpty {
ForEach(searchResult) { item in
NavigationLink(destination: NoteDetailView()) {
Text(item.task)
.lineLimit(1)
}
}
}
}
.navigationBarTitle("Notes")
.safeAreaInset(edge: .bottom) {
Color.clear
.frame(maxHeight: 40)
.background(.gray.opacity(0.7))
HStack {
Spacer(minLength: 160)
Text("\(vm.notes.count) notes")
.foregroundColor(.black.opacity(0.3))
Spacer()
Button {
showSheet = true
} label: {
Image(systemName: "square")
.font(.largeTitle)
.padding(.trailing)
}
}
}
.sheet(isPresented: $showSheet) {
NoteView()
}
}
}
var searchResult: [ToDoItem] {
guard !searchText.isEmpty else { return vm.notes }
return vm.notes.filter { $0.task.contains(searchText) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.preferredColorScheme(.dark)
ContentView()
.preferredColorScheme(.light)
}
.environmentObject(NoteViewModel())
}
}
//NoteDetailView.swift
import SwiftUI
struct NoteDetailView: View {
#EnvironmentObject var vm: NoteViewModel
var body: some View {
VStack {
TextEditor(text: $vm.text)
Spacer()
}
}
}
struct NotedetailView_Previews: PreviewProvider {
static var previews: some View {
NoteDetailView().environmentObject(NoteViewModel())
}
}
//NoteView.swift
import SwiftUI
struct NoteView: View {
// #State private var text = ""
#EnvironmentObject var vm: NoteViewModel
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
VStack {
TextEditor(text: $vm.text)
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
addTask()
dismiss()
vm.text = ""
}, label: {
Text("Done")
.font(.system(size: 25))
.foregroundColor(.accentColor)
})
}
}
}
}
func addTask() {
vm.add(ToDoItem(task: vm.text))
}
}
struct NoteView_Previews: PreviewProvider {
static var previews: some View {
NoteView()
.environmentObject(NoteViewModel())
}
}
import Foundation
struct ToDoItem: Identifiable, Codable {
var id = UUID()
var task : String
}
class NoteViewModel: ObservableObject {
#Published var notes = [ToDoItem]()
#Published var text = ""
let saveKey = "SavedKey"
init() {
if let data = UserDefaults.standard.data(forKey: saveKey) {
if let decoded = try? JSONDecoder().decode([ToDoItem].self, from: data) {
notes = decoded
return
}
}
notes = []
}
private func save() {
if let encoded = try? JSONEncoder().encode(notes) {
UserDefaults.standard.set(encoded, forKey: saveKey)
}
}
func add(_ note: ToDoItem) {
notes.append(note)
save()
}
func deleteTask(indexSet: IndexSet) {
indexSet.forEach { index in
self.notes.remove(at: index)
save()
}
}
}
The detail view should be a #Binding, and you can use the array that you have in the viewModel as an Bindable List here the fixes:
List {
ForEach($vm.notes) { $item in
NavigationLink(item.task, destination: NoteDetailView(note: $item))
}
The detail view should look like this:
struct NoteDetailView: View {
#Binding var note: ToDoItem
#EnvironmentObject var vm: NoteViewModel
var body: some View {
VStack {
TextEditor(text: $note.task)
Spacer()
}
.onDisappear {
vm.save()
}
}}
This way every time the user updates and closes the modal, the list will be saved.
When I press the Move button in the contextMenu, I change the isCopied and setOriginPath variables in the EnvironmentObject. When this change is made, the List view is cleared and I can't see anything on the screen. I don't have any problems when I don't use EnvironmentObject.
ContextMenu:
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
View:
struct DetailObjectView: View {
#ObservedObject var safeFileVM: SafeFileViewModel = SafeFileViewModel()
#EnvironmentObject var safeFileClipboard: SafeFileClipBoard
var currentFile: MyFile
var currentLocation = ""
var body: some View {
VStack {
.....
}
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
}
}
In the mini project below, when the EnvironmentObject value changes, navigation goes to the beginning. Why ? How can I fix this ?
Example Project:
Main:
#main
struct EnvironmentTestApp: App {
#StateObject var fooConfig: FooConfig = FooConfig()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.environmentObject(fooConfig)
}
}
}
}
ContentView:
struct ContentView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
struct Number: Identifiable {
var id = UUID()
var item: String
}
DetailView:
struct DetailView: View {
#EnvironmentObject var fooConfig: FooConfig
var itemNumber: String = ""
var body: some View {
NavigationLink(destination: ContentView().environmentObject(fooConfig)) {
Text("\(itemNumber) - \(fooConfig.fooBool == true ? "On" : "Off")")
.environmentObject(fooConfig)
.contextMenu {
Button {
fooConfig.fooBool.toggle()
} label: {
HStack {
Text(fooConfig.fooBool != true ? "On" : "Off")
}
}
}
}
}
}
ObservableObject:
class FooConfig: ObservableObject {
#Published var fooBool: Bool = false
}
Move that from scene into ContentView, because scene is a bad place to update view hierarchy, it is better to do inside view hierarchy, so here
struct EnvironmentTestApp: App {
var body: some Scene {
WindowGroup {
ContentView() // only root view here !!
}
}
}
everything else is inside views, like
struct ContentView: View {
#StateObject private var foo = FooConfig()
var body: some View {
NavigationView {
MainView()
.environmentObject(foo) // << here !!
}
}
}
struct MainView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
and so on...
Tested with Xcode 14 / iOS 16
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