Search code examples
xcodetimerswiftuiapple-watch

Sound when timer stops swiftUI Apple Watch


I'm working currently on a timer app for the Apple Watch. I'm managed to get a sound when the timer reaches 5 seconds. Only, when I'm pressing "done" the timer sounds still plays and I don't now how to stop the sound. I'm using WKInterfaceDevice.current().play(.success).

I want to stop the sound when the "cancel" button is pressed within the 5 seconds and when the "done" button is pressed.

I cannot find anything on the internet. I think that WKInterfaceDevice does not have an stop function.

struct softView: View {

@State var timerVal = 10

var body: some View {

    VStack {

        if timerVal > 0 {
            Text("Time Remaining")
                .font(.system(size: 14))
            Text("\(timerVal)")
                .font(.system(size: 40))

                .onAppear(){
                    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                        if self.timerVal > 0 {
                            self.timerVal -= 1
                        }
                        if self.timerVal < 6 {
                            WKInterfaceDevice.current().play(.success)
                        }
                    }
            }
            Text("Seconds")
                .font(.system(size: 14))
            Divider()
            Spacer()

            NavigationLink(destination: ContentView(), label: {Text("Cancel")})
            //.cornerRadius(20)
            .foregroundColor(Color.red)
            .background(
                RoundedRectangle(cornerRadius: 20)
                    .stroke(Color.red, lineWidth: 2)

            )



        }
        else {

            NavigationLink(destination: ContentView(), label: {Text("Done")})
                .foregroundColor(Color.green)
                .background(
                    RoundedRectangle(cornerRadius: 20)
                        .stroke(Color.green, lineWidth: 2)

                )
        }

    }         .navigationBarHidden(true)

}

Solution

  • When does your timer know when to stop?
    You have to define the event when the timer is to be stopped. That's where.invalidate will come handy.

    Basic Example:

    Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] (timer) in
      guard let _weakSelf = self else { timer.invalidate(); return }
    
      _weakSelf.timerVal -= 1
    
      if _weakSelf.timerVal < 0 { //if timer count goes negative then stop timer
        timer.invalidate()
      } else if _weakSelf.timerVal < 6 {
        WKInterfaceDevice.current().play(.success)
      }
    }
    

    For more control, i.e. If you want to stop the timer from, say, a button tap then we will have to make this timer object global.
    Furthermore, if you want to pop the View after the timer is completed/cancelled then we need to make more changes.
    This all gets a bit complicated but it's simple to understand.
    I would suggest you to break out your timer related logic into an ObservableObject class and use it within your View.

    Example:

    struct ContentView: View {
      @State var isShowingTimer: Bool = false
    
      var body: some View {
        NavigationView {
          NavigationLink(destination: TimerView(isShowing: $isShowingTimer),
                         isActive: $isShowingTimer) {
                          Text("Start Timer")
          }
        }
      }
    }
    
    • isShowingTimer controls the push/pop event for TimerView
      • It is sent as a binding to TimerView so it can be updated from inside TimerView in order to pop.

    struct TimerView: View {
      //Trigger for popping this view
      @Binding var isShowing: Bool
    
      @ObservedObject var timerControl = TimerControl()
    
      var body: some View {
        VStack {
          Text("\(timerControl.count)")
            .onAppear(perform: {
              //start timer event
              self.timerControl.startTimer(from: 10)
            })
            .onDisappear(perform: {
              //stop timer if user taps on `Back` from the Navigation Bar
              self.timerControl.stopTimer()
            })
            .onReceive(timerControl.$isComplete, //observe timer completion trigger
                       perform: { (success) in
                        //hide this view
                        self.isShowing = !success
            })
            Text("Cancel")
              .onTapGesture(perform: {
                //stop timer event
                self.timerControl.stopTimer()
            })
        }
      }
    }
    

    class TimerControl: ObservableObject {
      @Published var count: Int = 0
      @Published var isComplete: Bool = false
    
      private var timer: Timer?
    
      init(){}
    
      func startTimer(from count: Int) {
        self.count = count
    
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] (timer) in
          guard let _weakSelf = self else { timer.invalidate(); return }
    
          print(_weakSelf.count)
          _weakSelf.count -= 1
    
          if _weakSelf.count <= 0 {
            _weakSelf.stopTimer()
          } else if _weakSelf.count < 6 {
            print(">>make some noise here<<")
          }
        }
      }
    
      func stopTimer() {
        guard isComplete == false else { return }
        timer?.invalidate()
        isComplete = true
      }
    }
    

    • ObservableObject class can emit changes
    • @Published variables emit signals of change
    • @ObservedObject listens for changes on the ObservableObject
    • .onReceive handles Publisher events. In this case listens for changes by timerControl.$isComplete