WKExtendedRuntimeSession stopping after about 30 seconds?

Viewed 187

This is TRULY maddening me. I’ve tried all sorts of combinations here to even get to this point. It works sometimes and not others. And when it does work it stops by itself while in the background. Please help. I’m a total noob to coding in general and been trying many different methods so I apologize about the jankiness.

    if inspectionTime == true {
        if stopWatchManager.mode == .stopped {
            ZStack{
            HStack {
                if stopWatchManager.minutesElapsed > 0 {
                    if stopWatchManager.secondsElapsed < 10 {
                    Text("\(stopWatchManager.minutesElapsed) :0")
                        .font(.largeTitle)
                        .contentShape(Rectangle())
                        .onTapGesture(count: 2, perform: {
                            stopWatchManager.resetToZero()
                        })
                        .foregroundColor(isPressingDown ? .green : .white)
                        
                        .gesture(LongPressGesture(minimumDuration: 0.5)
                            .sequenced(before: LongPressGesture(minimumDuration: .infinity))
                            .updating($isPressingDown) { value, state, transaction in
                                
                                switch value {
                                    case .second(true, nil): //This means the first Gesture completed
                                        state = true
                                        
                                    default: break
                                }
                            })
                }
                 else   {
                    Text("\(stopWatchManager.minutesElapsed) :")
                        .font(.largeTitle)
                        .contentShape(Rectangle())
                        .onTapGesture(count: 2, perform: {
                            stopWatchManager.resetToZero()
                        })
                        .foregroundColor(isPressingDown ? .green : .white)
                        
                        .gesture(LongPressGesture(minimumDuration: 0.5)
                            .sequenced(before: LongPressGesture(minimumDuration: .infinity))
                            .updating($isPressingDown) { value, state, transaction in
                                
                                switch value {
                                    case .second(true, nil): //This means the first Gesture completed
                                        state = true
                                        
                                    default: break
                                }
                            })
                }
                }
                Text("\(stopWatchManager.secondsElapsed, specifier: "%.02f")")
            .font(.largeTitle)
            .contentShape(Rectangle())
            .onTapGesture(count: 2, perform: {
                stopWatchManager.resetToZero()
                
                
            })
            
            .foregroundColor(isPressingDown ? .green : .white)
            
            .gesture(LongPressGesture(minimumDuration: 0.5)
                .sequenced(before: LongPressGesture(minimumDuration: .infinity))
                .updating($isPressingDown) { value, state, transaction in
                    
                    switch value {
                        case .second(true, nil): //This means the first Gesture completed
                            state = true
                            
                        default: break
                    }
                })
            .onChange(of: isPressingDown, perform: { value in
                if isPressingDown == false {
                    if session.state == WKExtendedRuntimeSessionState.notStarted{
                        session.start()}
                    if showCountdown == false {

                        showCountdown = true
                        DispatchQueue.main.asyncAfter(deadline: .now() + 13.5){
                            if showCountdown == true {
                                stopWatchManager.start()
                                showCountdown = false}
                        }
                    }
                    else if showCountdown == true{
                    stopWatchManager.start()
                    showCountdown = false
                    
                    }
                    turnGreen = false
                    
                }
                else if isPressingDown == true{
                //    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
                        WKInterfaceDevice.current().play(.click)

                //    }
                    

                }
                if showCountdown == true && isPressingDown == true {
                   turnGreen = true
                }
            })

        }
                if showCountdown == true {
                    CountdownView()}
                Rectangle()
                    .contentShape(Rectangle())
                    .onTapGesture(count: 2, perform: {
                        stopWatchManager.resetToZero()
                        showCountdown = false
                    })
                    .gesture(LongPressGesture(minimumDuration: 0.5)
                        .sequenced(before: LongPressGesture(minimumDuration: .infinity))
                        .updating($isPressingDown) { value, state, transaction in
                            
                            switch value {
                                case .second(true, nil): //This means the first Gesture completed
                                    state = true
                                    
                                default: break
                            }
                        })
                    .opacity(0.1)
                    .foregroundColor(.black)
                    .gesture(LongPressGesture(minimumDuration: 0.5)
                        .sequenced(before: LongPressGesture(minimumDuration: .infinity))
                                .updating($testing) { value, state, transaction in
                            
                            switch value {
                                case .second(true, nil): //This means the first Gesture completed
                                    state = true
                                    
                                default: break
                            }
                        })
        }
        }
       else if stopWatchManager.mode == .running {
        ZStack{
        HStack{
        if stopWatchManager.minutesElapsed > 0 {
            if stopWatchManager.secondsElapsed < 10 {
            Text("\(stopWatchManager.minutesElapsed) :0")
                .font(.largeTitle)
                .contentShape(Rectangle())
                .onTapGesture(count: 2, perform: {
                    stopWatchManager.resetToZero()
                })
                .foregroundColor(isPressingDown ? .green : .white)
                
                .gesture(LongPressGesture(minimumDuration: 0.5)
                    .sequenced(before: LongPressGesture(minimumDuration: .infinity))
                    .updating($isPressingDown) { value, state, transaction in
                        
                        switch value {
                            case .second(true, nil): //This means the first Gesture completed
                                state = true
                                
                            default: break
                        }
                    })
        }
         else   {
            Text("\(stopWatchManager.minutesElapsed) :")
                .font(.largeTitle)
              //  .frame(width: 500.0, height: 500.0)
                .contentShape(Rectangle())
                .onTapGesture(count: 2, perform: {
                    stopWatchManager.resetToZero()
                })
                .foregroundColor(isPressingDown ? .green : .white)
                
                .gesture(LongPressGesture(minimumDuration: 0.5)
                    .sequenced(before: LongPressGesture(minimumDuration: .infinity))
                    .updating($isPressingDown) { value, state, transaction in
                        
                        switch value {
                            case .second(true, nil): //This means the first Gesture completed
                                state = true
                                
                            default: break
                        }
                    })
        }
        }
            Text(String(format: "%.02f", stopWatchManager.secondsElapsed))


                .font(.largeTitle)
                //.frame(width: 500.0, height: 500.0)
                .contentShape(Rectangle())
                .onTapGesture {
                    if stopWatchManager.mode == .running {
                        stopWatchManager.stop()
                    }
                }
                .onChange(of: stopWatchManager.secondsElapsed, perform: {value in
                        if stopWatchManager.secondsElapsed >= 60{
                            stopWatchManager.secondsElapsed = 0
                            stopWatchManager.minutesElapsed = stopWatchManager.minutesElapsed + 1

                        }})
            
    }
        Rectangle()
            .contentShape(Rectangle())
            .onTapGesture{
            if stopWatchManager.mode == .running {
                stopWatchManager.stop()
            }
            }
            .gesture(LongPressGesture(minimumDuration: 0.5)
                .sequenced(before: LongPressGesture(minimumDuration: .infinity))
                .updating($isPressingDown) { value, state, transaction in
                    
                    switch value {
                        case .second(true, nil): //This means the first Gesture completed
                            state = true
                            
                        default: break
                    }
                })
            .opacity(0.01)
            .foregroundColor(.black)}}}

    class StopWatchManager: ObservableObject {
    var session = WKExtendedRuntimeSession()
    let scoreList = ScoreList()
    @AppStorage("countdown") var countdown = "15"

    @AppStorage("bigDNF") var bigDNF: Bool = false

    @Published var secondsElapsed = 0.00
    @Published var minutesElapsed = 0

    @Published var mode: stopWatchMode = .stopped
    @State private var timeRemaining = 100
    let countdownTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @ObservedObject var scoreStore = ScoreStore()
    @AppStorage("newScore") var newScore = ""
    @AppStorage("save") var save = false
    @AppStorage("plus2") var plus2 = false

    var timer = Timer()
    
    

    func start() {
        session = WKExtendedRuntimeSession()
        if session.state == WKExtendedRuntimeSessionState.notStarted{
            session.start()}

        mode = .running
        WKInterfaceDevice.current().play(.start)
        secondsElapsed = 0
        minutesElapsed = 0

        timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
            self.secondsElapsed += 0.01
        }
        
    }
    func stop() {
        let formattedSeconds = String(format: "%.02f", secondsElapsed)
        
        timer.invalidate()
        
        mode = .stopped
        WKInterfaceDevice.current().play(.start)
        
        if plus2 == true {
        secondsElapsed = secondsElapsed + 2
        }
        //DispatchQueue.main.asyncAfter(deadline: .now() + 1.0)

        if minutesElapsed > 0{
            
            newScore = "\(minutesElapsed) :\(formattedSeconds)"
            save = true
        }
        else {
            newScore = "\(formattedSeconds)"

        }
        save = true
        
       // session.invalidate()


    }
    
    func resetToZero() {
            
            secondsElapsed = 0
        minutesElapsed = 0
        WKInterfaceDevice.current().play(.click)
        bigDNF = false

    }
    
    
}
1 Answers

WatchOS deliberately limits the opportunities to execute in the background as it can drain the battery. There is no need for a background task to implement a stopwatch.

You need a timer running when your app is on screen in order to periodically update the display. To track elapsed time, you can simply record the Date at which the stopwatch was started. Since time always moves forward you don't need a background task when the app isn't on screen. When the user returns to your app, you can simply calculate and display the elapsed time based on that start date.

I have included a minimal stopwatch view below. Note that UserDefaults can't store Date objects, so I have stored the start date as a Double - The time since the reference date.

There is also no need for gesture recognisers - You can simply use a Button

struct StopwatchView: View {
    
    @AppStorage("running") var running = false
    @AppStorage("started") var started = Date.distantPast.timeIntervalSinceReferenceDate
    let timer = Timer.publish(every: 0.2, on: .main, in: .common).autoconnect()
    
    @AppStorage("minutes") var elapsedMinutes = 0
    @AppStorage("seconds") var elapsedSeconds = 0
    @State var elapsed = "00:00"
    
    var body: some View {
        VStack {
            Text(elapsed).onReceive(timer) { input in
                self.elapsed = elapsedTime()
            }.font(.system(.headline, design: .monospaced))
            Spacer()
            Button( action: {
                self.running.toggle()
                if self.running {
                    self.started = Date().timeIntervalSinceReferenceDate
                }
            }) {
                Text(self.running ? "Stop":"Start")
            }.buttonStyle(BorderedButtonStyle(tint: self.running ? Color.red : Color.green))
        }
        .padding()
        
    }
    
    func elapsedTime() -> String {
        if self.running {
            
            let startDate = Date(timeIntervalSinceReferenceDate: self.started)
            let elapsedTime = max( Date().timeIntervalSince(startDate),0)
            elapsedMinutes = Int(elapsedTime / 60)
            elapsedSeconds = Int(elapsedTime - Double(elapsedMinutes * 60))
        }
        
        return String(format:"%02d:%02d",elapsedMinutes,elapsedSeconds)
        
    }
}
Related