Search code examples
swiftswiftuiswift5

Modifying user input in Swift


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.


Solution

  • 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.