오늘은 런닝앱을 만들고 있는 부분에 대한 고민과 과정에 대한 기록을 하려고 한다!
현재 런닝앱을 만들고 있는데 내가 뛴 기록을 공유하고 기록을 차곡차곡 쌓아나갈 수 있는 앱을 만들어 보려고한다.
현재 런닝을 기록하는 부분은 데이터를 저장하는 로직을 제외하고 거의 구현이 완료되었고 현재 주, 월 별로 저장된 기록을 확인하는 Record를 구현하고 있는 중이다.
먼저 런닝을 기록하는 부분을 살펴보면
struct RunningSession {
let id: UUID
let date: Date
let distance: BehaviorRelay<Double>
let duration: BehaviorRelay<TimeInterval>
let locations: BehaviorRelay<[CLLocation]>
init(id: UUID = UUID (), date: Date = Date(), distance: Double = 0.0, duration: TimeInterval = 0.0, locations: [CLLocation] = []) {
self.id = id
self.date = date
self.distance = BehaviorRelay<Double>(value: distance)
self.duration = BehaviorRelay<TimeInterval>(value: duration)
self.locations = BehaviorRelay<[CLLocation]>(value: locations)
}
}
하나의 세션에서 런닝 정보를 저장할 수 있고 활용하기 위해 데이터 모델은 이렇게 구성하였다. 이전 Relay의 개념을 알아봤듯이 UI업데이트가 끊임없이 진행되기 위해 BehaviorRelay를 활용해 변형되는 거리 , 시간 , 장소에 활용했다.
class RunningManager {
static let shared = RunningManager()
private init() {}
let currentSession = BehaviorRelay<RunningSession?>(value: nil)
let isRunning = BehaviorRelay<Bool>(value: false)
func startNewSession() {
let newSession = RunningSession()
currentSession.accept(newSession)
isRunning.accept(true)
}
func endCurrentSession() {
currentSession.accept(nil)
isRunning.accept(false)
}
func updateCurrentSession(newLocation: CLLocation) {
guard let session = currentSession.value else { return }
//update location
var locations = session.locations.value
locations.append(newLocation)
session.locations.accept(locations)
//update distance
if let lastLocation = locations.dropLast().last {
let newDistance = lastLocation.distance(from: newLocation) / 1000.0
let currentDistance = session.distance.value
session.distance.accept(currentDistance + newDistance)
}
//update duration
let newDuration = Date().timeIntervalSince(session.date)
session.duration.accept(newDuration)
currentSession.accept(session)
}
}
다음은 데이터 모델의 세션에 런닝 데이터를 관리하는 런닝 매니저를 구현했다. 여기서 RxCocoa를 임포트해 현재의 런닝 상태와 세션을 Relay로 구현하였고 지속적으로 위치 데이터를 전송받아 거리 정보를 받을 수 있도록 구현했다.
그 다음 RunningViewModel을 살펴보자!
enum SessionState {
case stopped
case running
case paused
}
각각의 상태를 enum으로 정의하고 각 상황별로 값을 처리할 수 있도록 지정해 둔다.
private func startSession() {
runningManager.startNewSession()
sessionStateRelay.accept(.running)
startTimer()
}
private func pauseSession() {
sessionStateRelay.accept(.paused)
stopTimer()
speak(text: "Pause")
}
private func resumeSession() {
sessionStateRelay.accept(.running)
startTimer()
speak(text: "Running Start")
}
private func stopSession() {
runningManager.endCurrentSession()
sessionStateRelay.accept(.stopped)
stopTimer()
resetTimer()
}
private func finishSession() {
stopSession()
speak(text: "Running Finish")
}
그리고 각각 세션에서 사용될 메서드를 설정해 정지, 시작, 재시작, 초기화 등 각 상황별 실행되어야 할 메서드를 넣어주었다.
private func setupBindings() {
startStopTrigger
.withLatestFrom(sessionStateRelay)
.subscribe(onNext: { [weak self] state in
guard let self = self else { return }
if state == .stopped {
self.startCountdown()
} else {
self.stopSession()
}
})
.disposed(by: disposeBag)
pauseResumeTrigger
.withLatestFrom(sessionStateRelay)
.subscribe(onNext: { [weak self] state in
guard let self = self else { return }
if state == .running {
self.pauseSession()
} else if state == .paused {
self.resumeSession()
}
})
.disposed(by: disposeBag)
locationUpdate
.withLatestFrom(sessionStateRelay) { ($0, $1) }
.filter { _, state in state == .running }
.map { location, _ in location }
.subscribe(onNext: { [weak self] location in
self?.runningManager.updateCurrentSession(newLocation: location)
})
.disposed(by: disposeBag)
stopButtonLongPressTrigger
.throttle(.seconds(2), scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] in
self?.finishSession()
})
.disposed(by: disposeBag)
}
그 다음 RxSwift를 사용해서 버튼이 트리거가 됐을때 데이터가 전달될 수 있도록 구현하였는데,
sessionStateRelay, startStopTrigger, pauseResumeTrigger, stopButtonLongPressTrigger를 활용해 이벤트가 발생할 때 값을 방출하는 역할을 담당해주도록 구현하였다.
startStopTrigger
.withLatestFrom(sessionStateRelay)
.subscribe(onNext: { [weak self] state in
guard let self = self else { return }
if state == .stopped {
self.startCountdown()
} else {
self.stopSession()
}
})
.disposed(by: disposeBag)
sessionStateRelay에서 최신 상태를 받아와 현재 상태가 .stopped이면 세션을 시작하고 stopped이 아니라면 세션을 멈추도록 구현하였다.
pauseResumeTrigger
.withLatestFrom(sessionStateRelay)
.subscribe(onNext: { [weak self] state in
guard let self = self else { return }
if state == .running {
self.pauseSession()
} else if state == .paused {
self.resumeSession()
}
})
.disposed(by: disposeBag)
위와 같은 형태로 sessionStateRelay를 최신 상태로 받아오고 그 값이 .running 이라면 멈추도록 하고 그렇지 않다면 다시 진행되도록 구현하였다.
locationUpdate
.withLatestFrom(sessionStateRelay) { ($0, $1) }
.filter { _, state in state == .running }
.map { location, _ in location }
.subscribe(onNext: { [weak self] location in
self?.runningManager.updateCurrentSession(newLocation: location)
})
.disposed(by: disposeBag)
stopButtonLongPressTrigger
.throttle(.seconds(2), scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] in
self?.finishSession()
})
.disposed(by: disposeBag)
locationUpdate는 세션이 실행중일때만 실행되도록 설정하고 위치 데이터를 업데이트해 지속적으로 업데이트 되도록 구현하였다.
또한 스탑 버튼은 2초 이상 눌러야 실행될 수 있도록 스로틀을 활용해 2초이후 값을 방출할 수 있도록 구현하였다.
오늘은 첫번째 기록 시간이니 런닝 기록을 저장하는 과정만 기록으로 남겨두기위해 데이터 모델, 런닝 매니저, 런닝 뷰모델에서 데이터를 바인딩하는 부분만 알아보았다 🙂
다음 글에서는 현재 구현하면서 고민하고 있는 내용과 어떤걸 채택해 사용할지 고민하는 부분에 대해서 글로 남겨두려고 한다 ㅎㅎ
오늘은 여기까지!!!!
'◽️ Programming > T I L' 카테고리의 다른 글
Firebase RealtimeDatabase 내 데이터 저장하기 (0) | 2024.10.25 |
---|---|
Equatable를 사용하면서 Extension 해야하는 상황 (0) | 2024.09.25 |
Swift의 async/await에 대해서 알아보자 (0) | 2024.08.19 |
CropBox를 활용해 이미지 자르기 (0) | 2024.08.16 |
Core Image의 개념과 이미지 내 필터 적용하는 방법 (0) | 2024.08.14 |