0

In my Swift project, the data and the front-end buttons to trigger changes in the data are separated by several layers of objects. How do I make the front-end views reflect changes made several layers removed? I imagine that this is a fairly common situation in Swift development.

Here is a working example, with no separating layers between the view and the data.

import Foundation
import SwiftUI
import OrderedCollections

class Test: ObservableObject {
    @Published var array: [String] = []
    var num: Int = 0
    
    func add() {
        array.append(String(num))
        num += 1
    }
}


struct testView: View {
    @ObservedObject var test = Test()
    var body: some View {
        VStack {
            Button(action: { test.add() }) {
                Text("Add Item to Array")
            }
            Text("")
            Text("Items")
            Text(Array(test.array).description)
        }
    }
}

@main
struct app: App {
    var body: some Scene {
        WindowGroup {
            testView()
        }
    }
}

Here is an example with a "Middle" class that doesn't work.

class Test: ObservableObject {
    @Published var array: [String] = []
    var num: Int = 0
    
    func add() {
        array.append(String(num))
        num += 1
    }
}

class Middle: ObservableObject {
    @ObservedObject var test: Test = Test()
}


struct testView: View {
    @ObservedObject var m = Middle()
    var body: some View {
        VStack {
            Button(action: { m.test.add() }) {
                Text("Add Item to Array")
            }
            Text("")
            Text("Items")
            Text(Array(m.test.array).description)
        }
    }
}

@main
struct app: App {
    var body: some Scene {
        WindowGroup {
            testView()
        }
    }
}

Note: with other code, I seemingly have been able to get my views to update changing data far removed from the front-end. I am sure it is possible--what I am looking for is the systematic way to make this work (my current method is just adding @ObservableObject, @Published, and @ObservedObject to everything the data touches until it works--or doesn't).

aheze
  • 24,434
  • 8
  • 68
  • 125
John Sorensen
  • 710
  • 6
  • 29

1 Answers1

4

There are probably a few elements that would be good to understand. Most importantly:

  1. The @ObservedObject property wrapper is for use within a View -- it won't do anything if used inside another ObservableObject

  2. @Published will only work out-of-the box when using value types. In Swift, this means using structs rather that classes.

Regarding the second point, unless there's a really compelling reason to do otherwise, your models should be structs. That means that you could just use a @Published property to store them and everything would work:

struct Test {
    var array: [String] = []
    var num: Int = 0
}

class Middle: ObservableObject {
  @Published var test: Test = Test()

  func append() {
    test.array.append(String(test.num))
    test.num += 1
  }
}

If for some reason you have to used nested objects, you'll have to call objectWillChange.send() manually to tell your SwiftUI View that the @Published property has new data:

class Test : ObservableObject {
    var array: [String] = []
    var num: Int = 0
    
    func append() {
      array.append(String(num))
      num += 1
    }
}

class Middle: ObservableObject {
  @Published var test: Test = Test()

    func append() {
        test.append()
        self.objectWillChange.send()
    }
}

You can also search for "SwiftUI nested ObservableObject" which will give you plenty of existing conversation on this topic, including:

How to tell SwiftUI views to bind to nested ObservableObjects

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • How do I modify multiple instance variables in a struct method? I'm trying to convert my class-heavy logic code to structs. My struct methods are mutating and have inout parameters, but I'm getting an "overlapping accesses to self" error – John Sorensen Sep 07 '21 at 02:45
  • Can you provide an example? Probably in a new question, I’d imagine. – jnpdx Sep 07 '21 at 04:55
  • An unrelated question... how do I publish a struct to a view directly, without a class controller? – John Sorensen Sep 09 '21 at 04:55
  • I’m not sure what that means. Do you mean how do you use a struct as a parameter for a View? Just like any other parameter. – jnpdx Sep 09 '21 at 05:18
  • How do I pass a struct to a view but also have the view update with changes to the struct (or rather with the creation of a new struct)? Structs can't conform to the ObservableObject protocol? – John Sorensen Sep 09 '21 at 14:01
  • It sounds like you are looking for a Binding – jnpdx Sep 09 '21 at 14:15
  • I'll look into that – John Sorensen Sep 09 '21 at 16:14
  • I don't need two way editing though--actions that happen at the view level won't directly affect the struct. Rather, the struct will trigger class methods that (should) result in the creation of a new struct. So instead of doing struct - class - view (Model, ViewModel, View), I'm doing class - struct - view. – John Sorensen Sep 09 '21 at 16:24
  • The reason why I'm not using a class for the ViewModel is because I want to compute and store values using the init but it seems like that only happens once--I can't get a class to re-init itself with changes to the Model. At least not how I'm doing it now. I want the struct because its a value type and so a new one is initialized with changes to the Model. – John Sorensen Sep 09 '21 at 16:28
  • I think it would be helpful to see an example. If you don’t want two way communication and just want to display the struct, just pass the struct. It should all just work. – jnpdx Sep 09 '21 at 16:32
  • I created a new question with example code. The title is "Observing Changes to Struct Swift" – John Sorensen Sep 09 '21 at 16:55