1

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.

6
  • 1
    This line seems a little suspect: Button(action: viewModel.start ) { did you mean Button(action: viewModel.startTest) { ? Commented May 8, 2021 at 14:36
  • 1
    perhaps even viewModel.model.start since viewModel has no start element? Commented May 8, 2021 at 14:53
  • Sorry about that. I accidentally left our the start method from viewModel when copying the code over. Edited it back in. @Baglan Commented May 8, 2021 at 15:30
  • @Jelumar that was my bad. The method exists but I forgot to copy it over when I was making the post. Added it back in. Commented May 8, 2021 at 15:30
  • Isn't it nonsensical to have a property that's both published and private, like this -> @Published private var model: ReactionLightsModel? Shouldn't that be @Published var model: ReactionLightsModel, or am I mistaken? Commented May 8, 2021 at 19:23

1 Answer 1

1

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()
        }
    }
}
Sign up to request clarification or add additional context in comments.

7 Comments

I have tried using Timer except now I get Escaping closure captures mutating 'self' parameter for the timer because of the line lights[I].turnON(). Additionally, my issue has to do with the fact that it is not recognizing that the variable in Light is being changed.
I did some testing and it appears that sleep does not have an impact. The issue is that it is not changing the color until after the startTest() method is done running.
I've updated with the beginnings of a Timer example (you'd need to finish the logic). You can't use it inside your struct model (generally you wouldn't want to be managing state changes from within your model anyway) because mutating functions like that can't store references to self (thus the error you got). So, move the logic to your view model (where state management should be done anyway).
Regarding your second comment, if you removed sleep and the calls to turn of the lights, yes, you'd see them all turn on at the same time because there's nothing spacing out the calls to turn them on.
Had a typo in my code but once fixed that worked. Thank you very much!
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.