Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI Combine: Nested Observed-Objects

What is the best approach to have swiftUI still update based on nested observed objects?

The following example shows what I mean with nested observed objects. The balls array of the ball manager is a published property that contains an array of observable objects, each with a published property itself (the color string).

Unfortunately, when tapping one of the balls it dos not update the balls name, nor does it receive an update. So I might have messed up how combine was ment to work in that case?

import SwiftUI

class Ball: Identifiable, ObservableObject {
    let id: UUID
    @Published var color: String
    init(ofColor color: String) {
        self.id = UUID()
       self.color = color
    }
}

class BallManager: ObservableObject {
    @Published var balls: [Ball]
    init() {
        self.balls = []
    }
}

struct Arena: View {
   @StateObject var bm = BallManager()

    var body: some View {
        VStack(spacing: 20) {
            ForEach(bm.balls) { ball in
                Text(ball.color)
                    .onTapGesture {
                        changeBall(ball)
                    }
            }
        }
        .onAppear(perform: createBalls)
        .onReceive(bm.$balls, perform: {
            print("ball update: \($0)")
        })
    }
    
    func createBalls() {
        for i in 1..<4 {
            bm.balls.append(Ball(ofColor: "c\(i)"))
        }
    }
    
    func changeBall(_ ball: Ball) {
        ball.color = "cx"
    }
}
like image 477
lenny Avatar asked Jul 19 '26 19:07

lenny


1 Answers

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.

like image 142
George Avatar answered Jul 21 '26 16:07

George



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!