Gladiator Boo
Gladiator Boo

Reputation: 189

SwiftUI instanced @State variable

I am quite new to SwiftUI. I have a following "Counter" view that counts up every second. I want to "reset" the counter when the colour is changed:

struct MyCounter : View {
  let color: Color
  @State private var count = 0

  init(color:Color) {
    self.color = color
    _count = State(initialValue: 0) 
  }
  var body: some View {
    Text("\(count)").foregroundColor(color)
    .onAppear(){
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
    }
  }
}

Here is my main view that uses counter:

struct ContentView: View {
  @State var black = true
  var body: some View {
    VStack {
        MyCounter(color: black ? Color.black : Color.yellow)
        Button(action:{self.black.toggle()}) { Text("Toggle") }
    }
  }
}

When i click "Toggle" button, i see MyCounter constructor being called, but @State counter persists and never resets. So my question is how do I reset this @State value? Please note that I do not wish to use counter as @Binding and manage that in the parent view, but rather MyCounter be a self-contained widget. (this is a simplified example. the real widget I am creating is a sprite animator that performs sprite animations, and when I swap the image, i want the animator to start from frame 0). Thanks!

Upvotes: 0

Views: 537

Answers (2)

E.Coms
E.Coms

Reputation: 11531

You need a binding var:

struct MyCounter : View {
      let color: Color
      @Binding var count: Int
      var body: some View {
      Text("\(count)").foregroundColor(color)
            .onAppear(){
                Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
            }
       }
}

struct ContentView: View {
    @State var black = true
    @State var count : Int  = 0
    var body: some View {
         VStack {
              MyCounter(color: black ? Color.black : Color.yellow , count: $count)
               Button(action:{self.black.toggle()
                    self.count = 0
                }) { Text("Toggle") }
         }
     }
}

Also you can just add one State Value innerColor to help you if you don't like binding.

struct MyCounter : View {
    let color: Color
    @State private var count: Int = 0
    @State private var innerColor: Color?

    init(color: Color) {
        self.color = color

    }

    var body: some View {

        return Text("\(self.count)")
            .onAppear(){
                Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
            }.foregroundColor(color).onReceive(Just(color), perform: { color in
                if self.innerColor !=  self.color {
                self.count = 0
                self.innerColor = color}
            })
    }
}

Upvotes: 1

Palle
Palle

Reputation: 12109

There are two way you can solve this issue. One is to use a binding, like E.Coms explained, which is the easiest way to solve your problem.

Alternatively, you could try using an ObservableObject as a view model for your timer. This is the more flexible solution. The timer can be passed around and it could also be injected as an environment object if you so desire.

class TimerModel: ObservableObject {
    // The @Published property wrapper ensures that objectWillChange signals are automatically emitted.
    @Published var count: Int = 0
    init() {}
    func start() {
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
    }
    func reset() {
        count = 0
    }
}

Your timer view then becomes

struct MyCounter : View {
    let color: Color
    @ObservedObject var timer: TimerModel

    init(color: Color, timer: TimerModel) {
        self.color = color
        self.timer = timer
    }
    var body: some View {
        Text("\(timer.count)").foregroundColor(color)
        .onAppear(){
            self.timer.start()
        }
    }
}

Your content view becomes

struct ContentView: View {
    @State var black = true
    @ObservedObject var timer = TimerModel()
    var body: some View {
        VStack {
            MyCounter(color: black ? Color.black : Color.yellow, timer: self.timer)
            Button(action: {
                self.black.toggle()
                self.timer.reset()
            }) { 
                Text("Toggle") 
            }
        }
    }
}

The advantage of using an observable object is that you can then keep track of your timer better. You could add a stop() method to your model, which invalidates the timer and you can call it in a onDisappear block of your view.

One thing that you have to be careful about this approach is that when you're using the timer in a standalone fashion, where you create it in a view builder closure with MyCounter(color: ..., timer: TimerModel()), every time the view is rerendered, the timer model is replaced, so you have to make sure to keep the model around somehow.

Upvotes: 1

Related Questions