1

I'm trying to learn Swift and have gone through several tutorials, however, I seem to be going in circles and need some direction on how to solve this problem.

My goal is to create a decrementing timer, given a user-input (in seconds), in this case I've chosen a Stepper to -/+ the value. Then begin decrementing the timer on a button press, in this case "Decrement". The counter is displayed on the label.

This problem is super easy if I hard code the starting value, but what purpose would that serve for a UI Test. So, this was the "challenging" task I was able to think of to help understand how SwiftUI works.

The problem I'm encountering is the variable passed by the user is immutable. I have tried making copies of it or assigning it to other variables to manipulate but seem to be going in circles. A nudge in the right direction or a potential solution would go a long way.

struct ContentView: View {
    @State private var timeInput: Int = 0
    var timer = Timer()
    var timeInputCopy: Int {
        timeInput
    }
    var body: some View {
        Stepper("Input", value: $timeInput, in: 0...150)
        Button("Decrement", action: decrementFunction)
        Label(String(timeInputCopy), image: "")
            .labelStyle(TitleOnlyLabelStyle())
    }
func decrementFunction() {
    timer.invalidate()
    timer = Timer.schedulerTimer(timeInterval: 1, 
                  target: self, 
                  selector: #selector(ContentView.timerClass), 
                  userInfo: nil, 
                  repeats: true)
}
func timerClass() {
    timeInputCopy -= timeInputCopy
    if (timeInputCopy == 0) {
        timer.invalidate()
    }
}
> Cannot assign to property: 'self' is immutable
> Mark method 'mutating' to make 'self' mutable

Attempting to auto-fix as Xcode recommends does not lead to a productive solution. I feel I am missing a core principle here.

2
  • Definitely a few things that could be adjusted here. 1) timeInputCopy doesn't have a point -- it's not really a copy, it's just a computed property that returns timeInput. 2) You won't have much luck with that form of Timer in SwiftUI with a selector. Instead, look at the Timer publisher. I'm happy to give a full-code answer, but I'm honestly not entirely sure what the code should be doing. The timer should be running and then the stepper can decrease the amount of remaining time? Commented Jan 27, 2022 at 20:09
  • @jnpdx Thanks for the response. From reading the link, I don't think Timer_publisher will work for this situation. Perhaps I didn't make my explanation clear. I am essentially trying to create the Timer iOS application, using a "Stepper". Where the user defines the initial number of seconds. Once they submit (click Decrement), the number they inputted will be decremented by 1 until 0 at a rate of 1 per second. Commented Jan 27, 2022 at 20:14

1 Answer 1

1

As mentioned in my comments above:

  1. timeInputCopy doesn't have a point -- it's not really a copy, it's just a computed property that returns timeInput

  2. You won't have much luck with that form of Timer in SwiftUI with a selector. Instead, look at the Timer publisher.

Here's one solution:

import Combine
import SwiftUI

class TimerManager : ObservableObject {
    @Published var timeRemaining = 0
    
    private var cancellable : AnyCancellable?
    
    func startTimer(initial: Int) {
        timeRemaining = initial
        cancellable = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { _ in
                self.timeRemaining -= 1
                if self.timeRemaining == 0 {
                    self.cancellable?.cancel()
                }
            }
    }
}

struct ContentView: View {
    @StateObject private var timerManager = TimerManager()
    @State private var stepperValue = 60
    
    
    var body: some View {
        Stepper("Input \(stepperValue)", value: $stepperValue, in: 0...150)
        Button("Start") {
            timerManager.startTimer(initial: stepperValue)
        }
        Label("\(timerManager.timeRemaining)", image: "")
            .labelStyle(TitleOnlyLabelStyle())
    }
}

This could all be done in the View, but using the ObservableObject gives a nice separation of managing the state of the timer vs the state of the UI.

Sign up to request clarification or add additional context in comments.

7 Comments

Thank you so much for the solution. If you are still available I have some questions as some of this code I have not encountered before.
Sure, happy to answer what I can, although I may direct you to ask another question if it's too lengthy to answer in comments.
1) I don't really understand the AnyCancellable? on line 5. 2) This is an elegant solution but does not seem too far off from my implementation. Why does schedulerTimer not work in this case? 3) To validate my understanding, You are passing stepperValue to startTimer as "initial", and assigning timeRemaining (local to the function) to the initial value of initial. Then decrementing that local copy?
2) Your use of scheduleTimer relies on keeping a reference to an object (in this case, your View) to use with a selector. This is something that works well in UIKit, but doesn't really work with SwiftUI, where Views are transitive -- they get recreated often, so keeping a reference is never a viable solution.
Amazing, thanks so much for nudging in my the right path (by providing solution) but I've actually learned quite a few different things from this brief discussion. Truly saved me tons of time and now I have constructive docs to read through that are relevant to my understanding! Again, thanks for your time! Marked as solution incase someone else has this oddly specific question.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.