1

I need to create Array property of class witch is conform to ObservableObject protocol. Because I am going to use it with filter, sort and etc - to select records from table to show in List view

class SQS_Record: NSObject, ObservableObject {
    @Published final var __idx: Int = -1 
}

class Project: SQS_Record {
    @objc dynamic var name: String = ""         { willSet { objectWillChange.send() } }
}

class SQS_Table<RecordType: SQS_Record>: ObservableObject {
    @Published var records = [RecordType]()
    var rcount: Int {
        return records.count
    }
    subscript (index: Int) -> RecordType {
        get {
            return records[index]
        }
        set(item) {
            records[index] = item
        }
    }


Let projectsTbl = SQS_Table<Project>(...)

struct ProjectsListView: View {
    // This is ERROR line - below
    @StateObject private var itemsList = projectsTbl.records.sorted { $0.__idx < $1.__idx }        
        
    var body: some View {
        NavigationView {
            List() {
                // Список
                ForEach (itemsList, id: \.__id) { (proj: Project) in
                    ProjectElement(project: proj)
                }
                .onMove(perform: moveItem )
                .onDelete(perform: delItem )
            }
    …
}

ERROR: Generic struct 'StateObject' requires that '[Project]' conform to 'ObservableObject'

George
  • 25,988
  • 10
  • 79
  • 133
Alex778801
  • 23
  • 7

1 Answers1

1

If I understand your question correctly, the items of your array are classes. You could declare your array as @State, but only changes to the array itself (addition, sort, deletion) will cause the body to be recalculated.

Let's try :

struct ArrayOfObservableObjects: View {
    @State private var store = (1 ... 5).map(Record.init)
    var body: some View {
        NavigationView {
            List(store) { record in
                Text(record.score.description)
            }
            .toolbar {
                HStack {
                    Button("Shuffle") {
                        store.shuffle()
                    }
                    Button("Change First") {
                        store.first?.score = 666
                    }
                    Button("Add") {
                        store.insert(Record(store.count + 1), at: 0)
                    }
                }
            }
        }
    }
}

As you can see the "Change First" button actually changes the "score" of a "Record", but this modification is not displayed.

If you want the changes to each element of the array to be "published", you have to proceed in several steps:

  1. Make your Records observable:
class Record: ObservableObject, Identifiable {
    let id = UUID()
    @Published var score: Int
    init(_ score: Int) {
        self.score = score
    }
}
  1. wrap your array in an ObservableObject :
class RecordStore: ObservableObject {
    @Published var records: [Record] = (1 ... 5).map(Record.init)

  1. subscribe to the changes on each element of the array
    private var c: AnyCancellable?
    init() {
        subscribeToChanges()
    }

    func subscribeToChanges() {
        c = records
            .publisher
            .flatMap { record in record.objectWillChange }
            .sink { [weak self] in
                self?.objectWillChange.send()
            }
    }
}

It works. But something is missing (if we add a new element it will not be observed). So to finish:

class RecordStore: ObservableObject {
    @Published var records: [Record] = (1 ... 5).map(Record.init) {
        didSet {
            subscribeToChanges()    ///<<< HERE
        }
    }

    private var c: AnyCancellable?
    init() {
        subscribeToChanges()
    }

    func subscribeToChanges() {
        c = records
            .publisher
            .flatMap { record in record.objectWillChange }
            .sink { [weak self] in
                self?.objectWillChange.send()
            }
    }
}

And the View :

import Combine
import SwiftUI

struct ArrayOfObservableObjects: View {
    @StateObject var store = RecordStore()
    var body: some View {
        NavigationView {
            List(store.records) { record in
                Text(record.score.description)
            }
            .toolbar {
                HStack {
                    Button("Shuffle") {
                        store.records.shuffle()
                    }
                    Button("Change First") {
                        store.records.first?.score = 666
                    }
                    Button("Add") {
                        store.records.insert(Record(store.records.count + 1), at: 0)
                    }
                }
            }
        }
    }
}
Adrien
  • 1,579
  • 6
  • 25
  • Thank you. This is a right way. I am going to adapt it for my design. Add subscribs for emulate array and etc – Alex778801 Sep 02 '21 at 21:12
  • @Alex778801 : You're welcome. I see you're new to SO : if you feel an answer solved your problem, please mark it as 'accepted' [by clicking the green check mark](https://meta.stackexchange.com/a/5235). You can too [vote up answers that are helpful and well-researched](https://stackoverflow.com/help/someone-answers). – Adrien Sep 02 '21 at 22:29
  • @Published var records: [Record] = (1 ... 5).map(Record.init) { didSet { subscribeToChanges() ///<<< HERE } } can you explain this construction? i get records set from another source - for example - create record set in init(records: [Record]) section – Alex778801 Sep 03 '21 at 17:22
  • @Alex : does not provide a default value to `records` and initialize the value in the `init`. `@Published var records: [Record] { didSet { subscribeToChanges() } }` and `init(records [Record]) { self.records = records }` – Adrien Sep 03 '21 at 18:18
  • I want to debug RecordStore() class. how can i receive all changes objectWillChange.send() in [Record] array and in every Record property - and print it to console? And show property/changed value - if it possible. Thank you – Alex778801 Sep 05 '21 at 21:26
  • rather than subscribing to `objectWillChange` (which send Void) you can subscribe to a `Published` property of a `Record`. Change `record in record.objectWillChange` by `record in record.$nameOfProperty`. And : `.sink {newValue in` – Adrien Sep 06 '21 at 06:11
  • Ok. Is there any solution to receive ALL properties changes and its values? Not only $nameOfProperty? – Alex778801 Sep 06 '21 at 13:51
  • No because this is an `objectWILLChange` publisher. – Adrien Sep 06 '21 at 14:00
  • c2 = records .publisher .flatMap { record in record.$__deleted } .sink { [self] newValue in if newValue { records.removeAll(where: { $0.__deleted }) print("deleted: \(newValue)") objectWillChange.send() } } – Alex778801 Sep 06 '21 at 20:53
  • i want to delete records which have property __deleted set to true. it is work OK - deleted. But objectWillChange.send() in the end of this closure doeesnt work - the view didnt refreshed – Alex778801 Sep 06 '21 at 20:55