Thomas Braun
Thomas Braun

Reputation: 1349

SwiftUI view not updating with data changes?

I am trying to make a view where once a user pressed a button, 5 circles (lights) will turn on one after another. However, when the model changes the status of the light, the view doesn't update to represent each lights status.

View

struct ReactionLightsView: View {
    
    @ObservedObject var viewModel: ReactionLightsViewModel
    
    
    var body: some View {
        VStack {
            VStack(spacing: 0) {
                HStack {
                    ForEach(viewModel.lights) { light in
                        Circle()
                            .foregroundColor(light.color)
                    }
                }
            } .padding()
            Button(action: viewModel.start ) {
                Text("Start")
            }
        }
    }
}

ViewModel

class ReactionLightsViewModel: ObservableObject {
    
    @Published private var model: ReactionLightsModel
    
    init(){
        model = ReactionLightsModel()
    }
    
    var lights: [Light] {
        model.lights
    }

    func start() {
        model.startTest()
    }
}

Model

struct ReactionLightsModel {
    private(set) var lights: [Light] = [Light(), Light(), Light(), Light(), Light()]
    private(set) var start: UInt64?
    private(set) var stop: UInt64?
    private(set) var reactionTimeNanoseconds: UInt64?
    
    mutating func startTest() {
        print("start reaction test")
        for i in 0..<5 {
            lights[i].turnOn();
            sleep(1)
        }
        for i in 0..<5 {
            lights[i].turnOff()
        }
        start = DispatchTime.now().rawValue
        print("done with reaction test")
    }
    
    mutating func stopTest() {
        stop = DispatchTime.now().rawValue
        reactionTimeNanoseconds = (stop! - start!)
        print(reactionTimeNanoseconds)
    }
}

Initially, I had the lights as an array of Booleans, with this implementation the lights would turn red (on) but only all at once once the 5 seconds had elapsed. However, not I changed each light to be its own object and the view does not update at all.

Lights

struct Light: Identifiable {
    private(set) var isLit: Bool = false
    private(set) var color: Color = .gray
    let id = UUID()
    
    mutating func turnOn() {
        isLit = true
        color = .red
    }
    
    mutating func turnOff() {
        isLit = false
        color = .gray
    }
}

Would appreciate any advice on how to fix this and any other recommendations on how I can improve my code.

Upvotes: 1

Views: 1479

Answers (1)

jnpdx
jnpdx

Reputation: 52312

Right now, you're trying to sleep on the main thread -- my suspicion is that the system won't even let you do that. Generally, sleep should probably be avoided in general, since it halts execution of the thread and especially not on the UI thread. Judging by your variable names, it looks like you're getting ready to test someone's reaction time to the lights all going on or off, which also wouldn't work with the sleep strategy, because any button press would be blocked by the sleep strategy. I suppose you could still measure after the sleep calls, but you'd lose the ability to measure anything that happened early.

If you remove the sleep and the code to turn the lights off, you'll see that the lights do in fact get turned on, so it's not that the UI isn't updating with data changes.

I'd suggest rewriting your code to either use a Timer or DispatchQueue asyncAfter. You'll need to keep track of the state of the lights and which should be lit next.


Update, showing the beginnings of a Timer example:


struct ReactionLightsModel {
    private(set) var lights: [Light] = [Light(), Light(), Light(), Light(), Light()]
    private var currentLight = 0
    private(set) var start: UInt64?
    private(set) var stop: UInt64?
    private(set) var reactionTimeNanoseconds: UInt64?
    
    mutating func turnOnNextLight() {
        lights[currentLight].turnOn()
        currentLight += 1
        if currentLight == lights.count {
            currentLight = 0
        }
    }
}

class ReactionLightsViewModel: ObservableObject {
    
    @Published private var model: ReactionLightsModel
    var timer : Timer?
    
    init(){
        model = ReactionLightsModel()
    }
    
    var lights: [Light] {
        model.lights
    }

    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            self.model.turnOnNextLight()
        }
    }
}

Upvotes: 1

Related Questions