[Swift] 타이머 앱 ViewModel

팔랑이·2024년 8월 16일
0

iOS/Swift

목록 보기
58/71
post-thumbnail

내배캠 토이 프로젝트로 ios 시계앱을 만들게 되었다.
4개의 탭 중 내가 타이머 부분을 맡게 되었다.
이하는 만들었던 타이머 객체와 메서드를 기록


timers를 BehaviorRelay로 선언

var timers = BehaviorRelay<[TimerModel]>(value: [])

이 코드에서 BehaviorRelay<[TimerModel]> 타입인 timers는 타이머의 상태를 관리하고, 여러 뷰 컨트롤러와의 데이터 바인딩을 위해 활용된다.

현재 설정된 모든 타이머의 리스트를 관리하고 있다. BehaviorRelay는 항상 최신 값을 유지하며, 새로운 구독자가 생길 때 마지막으로 방출된 값을 즉시 전달한다. 초기값으로는 빈 배열 []이 설정되어 있으며, 타이머가 추가되거나 삭제될 때마다 이 배열이 업데이트된다.

TimerModel 선언

timer의 id, remainingTime, isRunning을 관리한다.

struct TimerModel {
  let id: UUID
  let remainingTime: BehaviorRelay<TimeInterval>
  let isRunning: BehaviorRelay<Bool>
}

모델 타입을 저렇게 지정하는 이유는 여기에서 다루기로.

TimeInterval: 시간을 초 단위로 표현하는 Double 타입의 타입 별칭으로, 실수 값을 사용해 시간의 길이를 나타낸다.

  • 단위: TimeInterval의 단위는 초(Seconds) 로,1분은 60.0, 1시간은 3600.0으로 표현된다.
  • 타이머, 지연(딜레이) 처리, 날짜 및 시간 간의 차이 계산 등에서 편리하게 사용할 수 있다.

setNewTimer(time:)

새로운 타이머를 설정할 때 setNewTimer(time:) 메서드를 통해 timers에 새로운 TimerModel이 추가된다. 이때 accept 메서드를 사용하여 기존 배열에 새로운 타이머를 추가하고, 업데이트된 배열을 방출한다.

func setNewTimer(time: TimeInterval) {
    let newTimer = TimerModel(id: UUID(),
                              remainingTime: BehaviorRelay<TimeInterval>(value: time),
                              isRunning: BehaviorRelay<Bool>(value: true))
    timers.accept(timers.value + [newTimer])
    startTimer(id: newTimer.id)
}
  • timers.accept(timers.value + [newTimer]): 기존의 타이머 리스트에 새로운 타이머를 추가한 후, timers에 새로운 값을 전달하여 모든 구독자에게 업데이트된 타이머 리스트를 방출한다.

accept 메서드
: RxCocoa의 BehaviorRelay와 PublishRelay에서 값을 업데이트하고, 그 값을 구독자들에게 방출하는 데 사용됨

getTimer

타이머 id를 통해 해당 타이머를 가져온다.

func getTimer(id: UUID) -> TimerModel? {
    return timers.value.first(where: { $0.id == id })
  }

startTimer(id:)

특정 id의 타이머를 시작한다.

func startTimer(id: UUID) {
    guard let timer = getTimer(id: id), timer.isRunning.value else { return }

    if let previousSubscription = timerSubscription[id] {
      previousSubscription.dispose()
      timerSubscription.removeValue(forKey: id)
    }
    
    let subscription = Observable<Int>.interval(.milliseconds(100), scheduler: MainScheduler.instance)
      .subscribe(onNext: { [weak self] _ in
        guard let self = self else { return }
        let currentTime = max(timer.remainingTime.value - 0.1, 0)
        timer.remainingTime.accept(currentTime)
        
        if currentTime <= 0 {
          self.endTimer(id: id)
          self.timerSubscription[id]?.dispose()
          self.timerSubscription.removeValue(forKey: id)
        }
      })
    timerSubscription[id] = subscription
}
  • getTimer(id:) 메서드를 통해 해당 타이머를 가져오고, 타이머가 이미 실행 중인지 확인한다(timer.isRunning.value). 타이머가 실행 중이 아니면 메서드를 종료한다.

  • 타이머가 이미 구독 중이라면(timerSubscription[id]), 기존 구독을 해제하고(dispose), 메모리에서 제거한다. 일시정지 후 다시 시작할 때 중복 처리되는 것을 방지할 수 있음.

  • Observable<Int>.interval을 사용하여 100밀리초 간격으로 타이머를 업데이트한다. 남은 시간을 계산하고(timer.remainingTime.accept(currentTime))에 저장한다.

  • 남은 시간이 0 이하가 되면 endTimer(id:) 메서드를 호출하여 타이머를 종료하고, 구독을 해제한다.

pauseTimer(id:)

pauseTimer 메서드는 특정 타이머를 일시정지하는 역할을 한다.

func pauseTimer(id: UUID) {
    guard let timer = getTimer(id: id) else { return }
    timer.isRunning.accept(false)
    
    let remainingTime = timer.remainingTime.value
    NotificationManager.shared.cancelNotification(identifier: id.uuidString)
    timerSubscription[id]?.dispose()
    timerSubscription.removeValue(forKey: id)
}
  • getTimer(id:) 메서드를 통해 해당 타이머를 가져오고, 타이머의 실행 상태를 false로 설정한다. (timer.isRunning.accept(false))

  • 알림을 취소한다. (NotificationManager.shared.cancelNotification)

  • 타이머의 구독을 해제하고, 구독 정보를 메모리에서 제거한다.

resumeTimer(id:)

특정 타이머를 일시정지 상태에서 재개하는 역할을 한다.

func resumeTimer(id: UUID) {
    guard let timer = getTimer(id: id) else { return }
    
    timer.remainingTime.accept(remainingTime)
    timer.isRunning.accept(true)
      
    let newEndTime = Date().addingTimeInterval(remainingTime)
    notification(id: id, endTime: newEndTime)
      
    startTimer(id: timer.id)
}
  • 불러온 남은 시간을 timer.remainingTime.accept(remainingTime)으로 설정하고, 타이머의 실행 상태를 true로 변경한다(timer.isRunning.accept(true)).

  • 새로운 종료 시간을 계산하고(newEndTime), 알림을 다시 설정한다.

  • startTimer(id:) 메서드를 호출하여 타이머를 재시작한다.

cancelTimer(id:)

특정 타이머를 취소하는 역할을 한다.

func cancelTimer(id: UUID) {
    guard let timer = getTimer(id: id) else { return }
    pauseTimer(id: id)
    timer.remainingTime.accept(0)
    
    NotificationManager.shared.cancelNotification(identifier: id.uuidString)
    removeTimer(id: id)
}
  • getTimer(id:) 메서드를 통해 해당 타이머를 가져오고, pauseTimer(id:)를 호출하여 타이머를 일시정지한다.

  • 타이머의 남은 시간을 0으로 설정한다. (timer.remainingTime.accept(0))

  • 알림을 취소하고(NotificationManager.shared.cancelNotification), 타이머 리스트에서 해당 타이머를 제거한다(removeTimer(id:)).

removeTimer(id:)

타이머가 삭제되거나 종료될 때도 timers의 값을 업데이트한다. 이 경우 removeTimer(id:) 메서드를 통해 해당 타이머를 리스트에서 제거하고, 변경된 타이머 리스트를 timers에 전달한다.

func removeTimer(id: UUID) {
    timers.accept(timers.value.filter { $0.id != id })
}
  • timers.accept(timers.value.filter { $0.id != id }): 특정 타이머를 제거한 새로운 타이머 리스트를 timers에 전달하여, 구독자에게 최신 타이머 상태를 알린다.
profile
정체되지 않는 성장

0개의 댓글