[Project 일지] Running App (1)

오늘은 런닝앱을 만들고 있는 부분에 대한 고민과 과정에 대한 기록을 하려고 한다!

 

현재 런닝앱을 만들고 있는데 내가 뛴 기록을 공유하고 기록을 차곡차곡 쌓아나갈 수 있는 앱을 만들어 보려고한다.

 

현재 런닝을 기록하는 부분은 데이터를 저장하는 로직을 제외하고 거의 구현이 완료되었고 현재 주, 월 별로 저장된 기록을 확인하는 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초이후 값을 방출할 수 있도록 구현하였다.

 

오늘은 첫번째 기록 시간이니 런닝 기록을 저장하는 과정만 기록으로 남겨두기위해 데이터 모델, 런닝 매니저, 런닝 뷰모델에서 데이터를 바인딩하는 부분만 알아보았다 🙂

 

다음 글에서는 현재 구현하면서 고민하고 있는 내용과 어떤걸 채택해 사용할지 고민하는 부분에 대해서 글로 남겨두려고 한다 ㅎㅎ

 

오늘은 여기까지!!!!