Change Notification from Observable Object as a Nested Object in SwiftUI
Changes aren't detected because Foo
, being a reference-type, doesn't actually change - it's the same reference, so @Published
doesn't help here.
AppState
would need to manually subscribe to changes and call its own objectWillChange.send
:
class AppState: ObservableObject {
var foo: Foo = Foo() {
didSet {
cancellables = []
foo.$counter
.map { _ in } // ignore actual values
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}
private var cancellables: Set<AnyCancellable> = []
}
How to tell SwiftUI views to bind to nested ObservableObjects
Nested models does not work yet in SwiftUI, but you could do something like this
class SubModel: ObservableObject {
@Published var count = 0
}
class AppModel: ObservableObject {
@Published var submodel: SubModel = SubModel()
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}
Basically your AppModel
catches the event from SubModel
and send it further to the View
.
Edit:
If you do not need SubModel
to be class, then you could try something like this either:
struct SubModel{
var count = 0
}
class AppModel: ObservableObject {
@Published var submodel: SubModel = SubModel()
}
How to post a notification when an observable object has changed
If I correctly understood your intention the solution is to subscribe to default publisher which posts event on any change, i.e.
class MyClass: ObservableObject {
@Published var date: Date = Date() // default values just for demo simplicity
@Published var a: Int = 0
@Published var b: Int = 0
private var subscriber: AnyCancellable!
init() {
subscriber = self.objectWillChange.sink { [weak self] in
guard let object = self else { return }
NotificationCenter.default.post(name: .modified, object: object)
}
}
}
SwiftUI Combine: Nested Observed-Objects
When a Ball
in the balls
array changes, you can call objectWillChange.send()
to update the ObservableObject
.
The follow should work for you:
class BallManager: ObservableObject {
@Published var balls: [Ball] {
didSet { setCancellables() }
}
let ballPublisher = PassthroughSubject<Ball, Never>()
private var cancellables = [AnyCancellable]()
init() {
self.balls = []
}
private func setCancellables() {
cancellables = balls.map { ball in
ball.objectWillChange.sink { [weak self] in
guard let self = self else { return }
self.objectWillChange.send()
self.ballPublisher.send(ball)
}
}
}
}
And get changes with:
.onReceive(bm.ballPublisher) { ball in
print("ball update:", ball.id, ball.color)
}
Note: If the initial value of balls
was passed in and not always an empty array, you should also call setCancellables()
in the init.
SwiftUI - changes in nested View Model classes not detected using onChange method
Figure it out. Just had to create another AnyCancellable
variable to call objectWillChange
publisher.
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
@Published var workoutModel = WorkoutModel()
var cancellable: AnyCancellable?
init() {
cancellable = workoutModel.objectWillChange
.sink { _ in
self.objectWillChange.send()
}
}
}
While I have provided my answer, that worksaround with viewmodels, I would love to see/get advice on other alternatives.
Updates changes for nested models
In your current code, menu
is only ever set once (on init
). You want to set menu
every time the object updates.
To do this, change the menu
after the settings
object has changed one of the values. Here we receive on the main
thread, then update the menu with setMenu()
.
You could also change the UserSettings
to use didSet
instead of willSet
, and then you will no longer require the .receive(on: DispatchQueue.main)
. However, this may be a bit counter-intuitive to the meaning of objectWillChange
.
Code:
class ViewModel: ObservableObject {
let settings = UserSettings()
@Published private(set) var menu: [Item] = []
private var cancellables: [AnyCancellable] = []
init() {
setMenu()
settings.objectWillChange.receive(on: DispatchQueue.main).sink { [unowned self] in
setMenu()
}
.store(in: &cancellables)
}
private func setMenu() {
menu = [
.item(Language(id: "a", enable: settings.itemA)),
.item(Language(id: "b", enable: settings.itemB)),
.item(Language(id: "c", enable: settings.itemC))
]
}
}
I did change some things slightly which would be unrelated, such as the access levels. You don't want the user to accidentally edit menu
directly.
Related Topics
Integrate Existing Aws Cognito User Pool into iOS Project with Amplify
How to Set Priority on Constraints in Swift
Mfmailcomposeviewcontroller Error [Mc] Filtering Mail Sheet Accounts for Bundle Id
Background Upload with Share Extension
Set Uitextfield Placeholder Color Programmatically
Is There an Kotlin Equivalent 'With' Function in Swift
Why Can't I Use .Reduce() in a One-Liner Swift Closure with a Variadic, Anonymous Argument
Command Failed Due to Signal: Segmentation Fault: 11 While Emitting Ir Sil Function
Swift: Differencebetween a Typealias and an Associatedtype with a Value in a Protocol
Draw a Straight Line in Swift 3 and Core Graphics
Swift 3 - Pass Struct by Reference via Unsafemutablerawpointer
No Such Module Jsqmessagesviewcontroller
Uibutton Font Size Isn't Changing