Why is the binding not updating the array and the PatientView title in the ContentView?
I use the ForEach that takes a binding and the navigationDestination then passes that binding to the child view.
import SwiftUI
struct PatientView: View {
#Binding var patient: Patient
var body: some View {
Section("Name") {
TextField("Name", text: $patient.name)
.textFieldStyle(.roundedBorder)
}
Spacer()
.navigationTitle(patient.name)
}
}
protocol BlankInit {
init()
}
extension View {
func binding<V>(id: V.ID, array: Binding<[V]>) -> Binding<V> where V: Identifiable {
if let ret = array.first(where: { $0.id == id}) {
return ret
} else {
fatalError()
}
}
}
extension Binding : Hashable, Equatable where Value: Identifiable, Value: Equatable, Value: Hashable {
public static func == (lhs: Binding<Value>, rhs: Binding<Value>) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
public func hash(into hasher: inout Hasher) {
hasher.combine("Binding")
hasher.combine(self.wrappedValue)
}
}
struct Patient: Hashable, Equatable, Identifiable, BlankInit {
var id: UUID = UUID()
var name: String = ""
var age: UInt8 = 0
init(_ name: String, age: UInt8 = 0) {
self.name = name
self.age = 0
self.id = UUID()
}
init() {
self.name = ""
self.age = 0
self.id = UUID()
}
static func == (lhs: Patient, rhs: Patient) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.age == rhs.age
}
}
struct ContentView: View {
#StateObject var appData: AppData = AppData()
var body: some View {
NavigationStack(path: $appData.path) {
List {
ForEach($appData.patients) { $pt in
NavigationLink(pt.name, value: $pt)
}
}
.navigationDestination(for: Binding<Patient>.self) { item in
PatientView(patient: item)
}
.navigationTitle("Patients")
}
.environmentObject(appData)
.padding()
}
}
class AppData: ObservableObject {
#Published var path = NavigationPath()
#Published var patients = [Patient]()
init() {
patients.append(Patient("Smith"))
patients.append(Patient("Jones"))
patients.append(Patient("DeCaro"))
}
}
#main
struct TestAppApp: App {
var body: some Scene {
WindowGroup {
ContentView() //.environmentObject(AppData())
}
}
}
It works fine if I do the following:
struct ContentView: View {
#StateObject var appData: AppData = AppData()
var body: some View {
NavigationStack(path: $appData.path) {
List {
ForEach(appData.patients) { pt in
NavigationLink(pt.name, value: pt)
}
}
.navigationDestination(for: Patient.self) { item in
let binding = binding(id: item.id, array: $appData.patients)
PatientView(patient: binding)
}
.navigationTitle("Patients")
}
.environmentObject(appData)
.padding()
}
}
It seems more 'elegant' and less kludgy to be able to do the Binding version????
Related
I have a struct called Activity which has an id (UUID), name (String), description (String) and timesCompleted (Int).
I also have a class called Activities that contains an array of Activity structs called activityList. Activities is marked with ObservableObject.
I have activities declared as a #StateObject in my ContentView and I pass it to my ActivityDetailView where it is declared as an #ObservedObject.
However I can only partially write to activities.activityList in the child view. I can append, but I can't overwrite, update or remove an element from the array. No error is thrown but the view immediately crashes and the app returns to the main ContentView.
How do you update/write to an #ObservedObject? As you can see from the comments in my updateTimesCompleted() function I've tried all kinds of things to update/overwrite an existing element. All crash silently and return to ContentView. Append does not fail, but isn't the behavior I want, I want to update/overwrite an array element, not append a new copy.
Activity Struct:
struct Activity : Codable, Identifiable, Equatable {
var id = UUID()
var name: String
var description: String
var timesCompleted: Int
}
Activities Class:
class Activities: ObservableObject {
#Published var activityList = [Activity]() {
didSet {
if let encoded = try? JSONEncoder().encode(activityList) {
UserDefaults.standard.set(encoded, forKey: "activityList")
}
}
}
init() {
if let savedList = UserDefaults.standard.data(forKey: "activityList") {
if let decodedList = try? JSONDecoder().decode([Activity].self, from: savedList) {
activityList = decodedList
return
}
}
activityList = []
}
init(activityList: [Activity]) {
self.activityList = activityList
}
subscript(index: Int) -> Activity {
get {
assert(index < activityList.count, "Index out of range")
return activityList[index]
}
set {
assert(index < activityList.count, "Index out of range")
activityList[index] = newValue
}
}
}
ContentView:
struct ContentView: View {
#StateObject var activities = Activities()
#State private var showingAddActivity = false
var body: some View {
NavigationView {
List {
ForEach(activities.activityList) { activity in
NavigationLink {
ActivityDetailView(activity: activity, activities: activities)
} label: {
Text(activity.name)
}
}
}
.navigationTitle("Habits")
.toolbar {
Button {
showingAddActivity = true
let _ = print("add activity")
}
label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddActivity) {
AddActivityView(activities: activities)
}
}
}
ActivityDetailView:
struct ActivityDetailView: View {
#State private var timesCompleted = 0
let activity: Activity
#ObservedObject var activities: Activities
var body: some View {
NavigationView {
Form {
Text("Activity: \(activity.name)")
Text("Description: \(activity.description)")
Stepper {
Text("Times Completed: \(timesCompleted)")
} onIncrement: {
timesCompleted += 1
updateTimesCompleted()
} onDecrement: {
if timesCompleted > 0 {
timesCompleted -= 1
updateTimesCompleted()
}
}
}
.navigationTitle("Activity Details")
}
}
func updateTimesCompleted() {
let newActivity = Activity(name: activity.name, description: activity.description, timesCompleted: timesCompleted)
let _ = print("count: \(activities.activityList.count)")
let index = activities.activityList.firstIndex(of: activity)
let _ = print(index ?? -666)
if let index = index {
activities.activityList[index] = Activity(name: activity.name, description: activity.description, timesCompleted: timesCompleted)
//activities.activityList.swapAt(index, activities.activityList.count - 1)
//activities.activityList[index].incrementTimesCompleted()
//activities.activityList.append(newActivity)
//activities.activityList.remove(at: index)
//activities.activityList.removeAll()
//activities.activityList.append(newActivity)
}
}
}
You could try this approach, where the activity is passed to the ActivityDetailView
as a binding.
In addition, #ObservedObject var activities: Activities is used directly in AddActivityView to add an Activity to the list.
struct Activity : Codable, Identifiable, Equatable {
let id = UUID() // <-- here
var name: String
var description: String
var timesCompleted: Int
enum CodingKeys: String, CodingKey { // <-- here
case name,description,timesCompleted
}
}
class Activities: ObservableObject {
#Published var activityList = [Activity]() {
didSet {
if let encoded = try? JSONEncoder().encode(activityList) {
UserDefaults.standard.set(encoded, forKey: "activityList")
}
}
}
init() {
if let savedList = UserDefaults.standard.data(forKey: "activityList") {
if let decodedList = try? JSONDecoder().decode([Activity].self, from: savedList) {
activityList = decodedList
return
}
}
activityList = []
}
init(activityList: [Activity]) {
self.activityList = activityList
}
subscript(index: Int) -> Activity {
get {
assert(index < activityList.count, "Index out of range")
return activityList[index]
}
set {
assert(index < activityList.count, "Index out of range")
activityList[index] = newValue
}
}
}
struct ContentView: View {
#StateObject var activities = Activities()
#State private var showingAddActivity = false
var body: some View {
NavigationView {
List {
ForEach($activities.activityList) { $activity in // <-- here
NavigationLink {
ActivityDetailView(activity: $activity) // <-- here
} label: {
Text(activity.name)
}
}
}
.navigationTitle("Habits")
.toolbar {
Button {
showingAddActivity = true
}
label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddActivity) {
AddActivityView(activities: activities)
}
.onAppear {
// for testing
if activities.activityList.isEmpty {
activities.activityList.append(Activity(name: "activity-1", description: "activity-1", timesCompleted: 1))
activities.activityList.append(Activity(name: "activity-2", description: "activity-2", timesCompleted: 2))
activities.activityList.append(Activity(name: "activity-3", description: "activity-3", timesCompleted: 3))
}
}
}
}
// -- here for testing
struct AddActivityView: View {
#ObservedObject var activities: Activities
var body: some View {
Text("AddActivityView")
Button("add activity") {
activities.activityList.append(Activity(name: "workingDog", description: "workingDog", timesCompleted: 5))
}
}
}
struct ActivityDetailView: View {
#Binding var activity: Activity // <-- here
var body: some View {
Form {
Text("Activity: \(activity.name)")
Text("Description: \(activity.description)")
Stepper {
Text("Times Completed: \(activity.timesCompleted)")
} onIncrement: {
activity.timesCompleted += 1 // <-- here
} onDecrement: {
if activity.timesCompleted > 0 {
activity.timesCompleted -= 1 // <-- here
}
}
}
}
}
On Xcode 13 Beta 3, I am trying to find a good solution for an edit detail view presented in a sheet that needs to explicitly be confirmed.
In the DetailEditView, I initialise a #State property (editingModel) which is initialised from a #Binding (model) that I hand down.
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
//...
When I tap/press the confirm button in my sheet, I want to assign the altered editingModel to the passed model.
Button {
#warning("My expectation (saving changes by assigning `editingModel` to `model`) fails here…")
model = editingModel
isEditing = false
} label: {
Text("Done")
}
//...
While I do not have any build errors, the code does not work as expected–and I don't understand why. Look out for my #warning: that's where my code does not work as expected.
For all I know this could be a bug in the Xcode 13 Beta–or am I misunderstanding something fundamentally?
Here's all the code:
import SwiftUI
//MARK: - Main
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelStore)
}
}
}
//MARK: - Views
struct ContentView: View {
#SceneStorage("selection") var selection: Model.ID?
var body: some View {
NavigationView {
SidebarView(selection: $selection)
DetailView(modelSelection: $selection)
}
}
}
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var selection: Model.ID?
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $selection)
} label: {
Text(modelItem.id)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var modelSelection: Model.ID?
#State private var isEditing = false
var body: some View {
Form {
Text(modelBinding.wrappedValue.id)
}
.sheet(isPresented: $isEditing) {
DetailEditView(model: modelBinding, isEditing: $isEditing)
}
.toolbar {
ToolbarItem {
Button {
isEditing = true
} label: {
Label("Edit", systemImage: "pencil")
}
}
}
}
var modelBinding: Binding<Model> {
$modelStore[modelSelection]
}
}
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.id)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
#warning("My expectation (saving changes by assigning `editingModel` to `model`) fails here…")
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
//MARK: - Store
class ModelStore: ObservableObject {
#Published var models: [Model] = Model.mockModelArray()
subscript(modelId: Model.ID?) -> Model {
get {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
return models[modelIndex]
}
}
if models.isEmpty {
return Model(id: UUID().uuidString)
} else {
return models[0]
}
}
set(newValue) {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
models[modelIndex] = newValue
}
}
}
}
}
//MARK: - Models
struct Model: Identifiable {
var id: String
static func mockModel() -> Model {
Model(id: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
At first, do not edit id of Model. Instead use a new property and edit it.
//MARK: - Models
struct Model: Identifiable {
let id = UUID()
var content: String
static func mockModel() -> Model {
Model(content: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
For the first time you are in DetailView, selected model is not among the $modelStore.models. You need to send the first object of `` to the DetailsView.
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView(selection: $modelStore.models.first!)
.environmentObject(modelStore)
}
}
}
When you choose a model from SidebarView, the model in DetailView does not get updated. Send $modelItem to DetailView instead.
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $modelItem)
} label: {
Text(modelItem.content)
}
}
}
}
}
In DetailView, remove modelBinding and send modelSelection to DetailEditView.
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.content)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
All the code
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView(selection: $modelStore.models.first!)
.environmentObject(modelStore)
}
}
}
//MARK: - Views
struct ContentView: View {
#Binding var selection: Model
var body: some View {
NavigationView {
SidebarView()
DetailView(modelSelection: $selection)
}
}
}
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $modelItem)
} label: {
Text(modelItem.content)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var modelSelection: Model
#State private var isEditing = false
var body: some View {
Form {
Text(modelSelection.content)
}
.sheet(isPresented: $isEditing) {
DetailEditView(model: $modelSelection, isEditing: $isEditing)
}
.toolbar {
ToolbarItem {
Button {
isEditing = true
} label: {
Label("Edit", systemImage: "pencil")
}
}
}
}
}
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.content)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
//MARK: - Store
class ModelStore: ObservableObject {
#Published var models: [Model] = Model.mockModelArray()
subscript(modelId: Model.ID?) -> Model {
get {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
return models[modelIndex]
}
}
if models.isEmpty {
return Model(content: UUID().uuidString)
} else {
return models[0]
}
}
set(newValue) {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
models[modelIndex] = newValue
}
}
}
}
}
//MARK: - Models
struct Model: Identifiable {
let id = UUID()
var content: String
static func mockModel() -> Model {
Model(content: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
Now upon confirmation the selected model is edited in all the views.
I have a list of items of different classes derived from the same class.
The goal: editing any object using a different view
The model:
class Paper: Hashable, Equatable {
var name: String
var length: Int
init() {
name = ""
length = 0
}
init(name: String, length: Int) {
self.name = name
self.length = length
}
static func == (lhs: Paper, rhs: Paper) -> Bool {
return lhs.length == rhs.length
}
func hash(into hasher: inout Hasher) {
hasher.combine(length)
}
}
class ScientificPaper: Paper {
var biology: Bool
override init(name: String, length: Int) {
biology = false
super.init(name: name, length: length)
}
}
class TechnicalPaper: Paper {
var electronics: Bool
override init(name: String, length: Int) {
electronics = false
super.init(name: name, length: length)
}
}
The main view containing the list.
struct TestView: View {
#Binding var papers: [Paper]
#State private var edit = false
#State private var selectedPaper = Paper()
var body: some View {
let scientificBinding = Binding<ScientificPaper>(
get: {selectedPaper as! ScientificPaper},
set: { selectedPaper = $0 }
)
VStack {
List {
ForEach(papers, id: \.self) { paper in
HStack {
Text(paper.name)
Text("\(paper.length)")
Spacer()
Button("Edit") {
selectedPaper = paper
edit = true
}
}
}
}
}
.sheet(isPresented: $edit) {
VStack {
if selectedPaper is ScientificPaper {
ScientificForm(paper: scientificBinding)
}
if selectedPaper is TechnicalPaper {
TechnicalForm(paper: technicalBinding)
}
}
}
}
}
The custom view for each class.
struct ScientificForm: View {
#Binding var paper: ScientificPaper
var body: some View {
Form {
Text("Scientific")
TextField("Name: ", text: $paper.name)
TextField("Length: ", value: $paper.length, formatter: NumberFormatter())
TextField("Biology: ", value: $paper.biology, formatter: NumberFormatter())
}
}
}
struct TechnicalForm: View {
#Binding var paper: TechnicalPaper
var body: some View {
Form {
Text("Technical")
TextField("Name: ", text: $paper.name)
TextField("Length: ", value: $paper.length, formatter: NumberFormatter())
TextField("Electronics: ", value: $paper.electronics, formatter: NumberFormatter())
}
}
}
Problem is that at run time I get the following:
Could not cast value of type 'Paper' to 'ScientificPaper'.
maybe because the selectedPaper is already initialized as Paper.
What is the right strategy to edit list items belonging to different classes?
The error is due to creating binding in body, which calculates on every refresh, so binding is invalid.
The solution is to make binding as computable property, so it is requested only after validation in correct flow.
Tested with Xcode 12.1 / iOS 14.1 (demo is for scientificBinding only for simplicity)
struct TestView: View {
#Binding var papers: [Paper]
#State private var edit = false
#State private var selectedPaper = Paper()
var scientificBinding: Binding<ScientificPaper> { // << here !!
return Binding<ScientificPaper>(
get: {selectedPaper as! ScientificPaper},
set: { selectedPaper = $0 }
)
}
var body: some View {
VStack {
List {
ForEach(papers, id: \.self) { paper in
HStack {
Text(paper.name)
Text("\(paper.length)")
Spacer()
Button("Edit") {
selectedPaper = paper
edit = true
}
}
}
}
}
.sheet(isPresented: $edit) {
VStack {
if selectedPaper is ScientificPaper {
ScientificForm(paper: scientificBinding)
}
// if selectedPaper is TechnicalPaper {
// TechnicalForm(paper: technicalBinding)
// }
}
}
}
}
I want
MapView change by condition
store current MapView
call method in current MapView
Is this possible?
MapViewEnvironment.swift
class MapViewEnvironment: ObservableObject {
#Published var value1 = "aaa"
#Published var currentMapCompany = "apple"
}
MapViewProtocol.swift
protocol MapViewProtocol {
func aaa()
func bbb()
}
AppleMapView.swift
struct AppleMapView: UIViewRepresentable, MapViewProtocol {
func aaa() { print("AppleMapView - aaa") }
func bbb() { print("AppleMapView - bbb") }
}
GoogleMapView.swift
struct GoogleMapView: UIViewRepresentable, MapViewProtocol {
func aaa() { print("GoogleMapView - aaa") }
func bbb() { print("GoogleMapView - bbb") }
}
CommonMapView.swift
struct CommonMapView: View {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
#State var cancellable = Set<AnyCancellable>()
#State var currentMapView: AnyView? // <-- correct?
func choiceView() -> some View {
switch mapViewEnvironment.currentMapCompany {
case "apple":
currentMapView = AnyView(AppleMapView()) // Modifying state during view update, this will cause undefined behavior.
default:
currentMapView = AnyView(GoogleMapView())
}
return currentMapView
}
var body: some View {
choiceView().onAppear {
self.mapViewEnvironment.$value1
.filter { $0 == "aaa" }
.sink { _ in currentMapView.aaa() } // error
.store(in: &self.cancellable)
self.mapViewEnvironment.$value1
.filter { $0 == "bbb" }
.sink { _ in currentMapView.bbb() } // error
.store(in: &self.cancellable)
}
}
}
ContentView.swift
struct ContentView: View {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
var body: some View {
VStack{
Button(action: { self.mapViewEnvironment.value1 = "aaa" }) { Text("set aaa") }
Button(action: { self.mapViewEnvironment.value1 = "bbb" }) { Text("set bbb") }
CommonMapView()
}
}
}
Self answer.
I found another way and change some class.
ContentView.swift
struct ContentView: View {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
var body: some View {
VStack {
Button(action: { self.mapViewEnvironment.value1 = "aaa" }) { Text("set aaa") }
Button(action: { self.mapViewEnvironment.value1 = "bbb" }) { Text("set bbb") }
Button(action: { self.mapViewEnvironment.currentMapCompany = "apple" }) { Text("set apple") }
Button(action: { self.mapViewEnvironment.currentMapCompany = "google" }) { Text("set google") }
CommonMapView()
}
}
}
CommonMapView.swift
struct CommonMapView: View {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
var body: some View {
ZStack {
if mapViewEnvironment.currentMapCompany == "apple" {
AppleMapView()
} else {
GoogleMapView()
}
}
}
}
AppleMapView.swift
struct AppleMapView: UIViewRepresentable {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView()
view.mapType = .standard
willAppear(context)
return view
}
...
func makeCoordinator() -> AppleMapView.Coordinator {
return Coordinator()
}
static func dismantleUIView(_ uiView: MKMapView, coordinator: AppleMapView.Coordinator) {
coordinator.cancellable.removeAll()
}
final class Coordinator {
var cancellable = Set<AnyCancellable>()
}
}
extension AppleMapView: MapViewProtocol {
func willAppear(_ context: Context) {
mapViewEnvironment.$value1.filter { $0 == "aaa" }.sink { _ in self.aaa() }.store(in: &context.coordinator.cancellable)
mapViewEnvironment.$value1.filter { $0 == "bbb" }.sink { _ in self.bbb() }.store(in: &context.coordinator.cancellable)
}
func aaa() { print("AppleMapView - aaa") }
func bbb() { print("AppleMapView - bbb") }
}
Im trying to create an environment object that is editable and putting it in a list.
The Variables are only refreshing when I switch the tab for example (so whenever I leave the NavigationView) and then come back.
The same worked with a ModalView before. Is it a bug maybe? Or am I doing something wrong?
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
try passing .environmentObject on your destination:
NavigationLink(destination: detailView(feed: self._feed, i: i).environmentObject(x))
You have to use willSet instead of didSet.
TestItem should be a value type: struct or enum. SwiftUI's observation system properly works only with value types.