ShareRun을 만들면서 TCA를 활용해 구현하는 방식 중 State를 변경하는 로직과 Action이 발생할 때 상태 변화 및 이팩트를 처리하는 메서드를 정의한다.
@Reducer
struct RunningFeature {
@ObservableState
struct RunningState: Equatable {
var record: RunningRecord? // 러닝 기록을 저장할 상태
var isRunning: Bool = false // 러닝 중인지 여부
var currentLocation: CLLocationCoordinate2D? // 현재 위치
var mapRegion: MKCoordinateRegion? // 지도에 표시할 영역
var authorizationStatus: CLAuthorizationStatus = .notDetermined // 위치 권한 상태
var heartRate: Int = 0 // 실시간 심박수
var cadence: Int = 0 // 실시간 케이던스
var locationHistory: [CLLocationCoordinate2D] = [] // 이동 기록(위치 기록)
var startTime: Date? // 러닝 시작 시간
var endTime: Date? // 러닝 종료 시간
var distance: Double = 0.0 // 누적 이동 거리
}
}
State는 현재 러닝 상태와 관련된 데이터를 관리하고 isRunning이 true일 때 사용자가 런닝 중임을 의미한다. 이와 동일하게 위치정보, 심박수, 케이던스, 지도 등 상태로 관리하도록 설정했다.
enum RunningAction {
case startRunning // 러닝 시작
case stopRunning // 러닝 종료
case locationUpdated(CLLocationCoordinate2D) // 위치 업데이트
case heartRateUpdated(Int) // 심박수 업데이트
case cadenceUpdated(Int) // 케이던스 업데이트
case authorizationStatusChanged(CLAuthorizationStatus) // 권한 변경
case mapRegionChanged(MKCoordinateRegion) // 지도 영역 변경
case saveRunRecord // 러닝 기록 저장
}
Action은 사용자와 시스템의 이벤트를 정의한다. startRunning은 러닝이 시작됨의 action을 설정하고 locationUpdated 는 새로운 위치 데이터가 들어왔을때 처리 되도록 정의했다.
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .startRunning:
// 러닝 시작 시 상태 초기화
state.isRunning = true
state.startTime = Date()
state.locationHistory = []
state.distance = 0.0
// 위치와 심박수 데이터를 받아오는 스트림 생성
let locationStream = locationManager.startUpdatingLocation()
.map { Action.locationUpdated($0.coordinate) }
.receive(on: mainQueue)
.eraseToEffect()
let heartRateStream = healthKitManager.startHeartRateUpdates()
.map(Action.heartRateUpdated)
.receive(on: mainQueue)
.eraseToEffect()
return .merge(locationStream, heartRateStream) // 위치, 심박수 스트림을 처리
}
}
}
Reducer는 액션이 발생하면 상태를 업데이트 하거나 이펙트를 트리거 하도록 설정했다. 예를 들어 startRunning 액션이 발생하면 런닝을 시작하고 위치와 심박수 데이터를 받아오는 스트림을 반환 한다. 이 데이터는 이후 locationUpdated, heartRateUpdated로 다시 처리 된다.
struct RunningEnvironment {
var locationManager: LocationManager
var healthKitManager: HealthKitManager
var mainQueue: AnySchedulerOf<DispatchQueue>
}
TCA는 Environment 를 활용해 외부 시스템 혹은 API 호출을 관리하도록 한다. 이 프로젝트를 진행하면서 RunningEnvironment를 활용해 CLLocationManager, HealthKit 등 데이터를 처리하고 mainQueue를 통해 메인스레드에서 UI업데이트를 수행할 수 있도록 구현했다.
class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private let subject = PassthroughSubject<CLLocation, Never>()
func startUpdatingLocation() -> AnyPublisher<CLLocation, Never> {
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
return subject.eraseToAnyPublisher()
}
func stopUpdatingLocation() {
manager.stopUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
subject.send(location)
}
}
}
LocationManager는 CLLocationManager를 래핑해 위치 데이터를 실시간으로 제공하고 이 내용을 Combine 스트림으로 변환해 리듀서에서 처리할 수 있도록 메서드를 구현하였다.
startUpdatingLocation 메서드를 호출하면 CLLocationManager가 위치 업데이트를 시작하고 이를 AnyPublisher로 변환해 리듀서에서 사용할 수 있게 한다.
이렇게해서 위치 업데이트가 발생하면 locationManager(_:didUpdateLocations:) 에서 subject를 통해 최신 위치를 방출하도록 구현했다.
이렇게 오늘은 Manager를 구현해 메서드를 만들고 State, Action을 정의해 Reducer를 활용해 이 둘의 변화를 담당하는 TCA 구조로 구성을 완료했다 🙂
아직은 TCA가 확 다가올 정도로 익숙치 않고 확실히 러닝커브가 좀 높은 것 같지만 핫한 아키텍처인 만큼 TCA 교육자료가 상당히 잘되어있는 것 같다. 꾸준히 공부해서 익숙해져보자!
오늘은 여기까지!
'◽️ Programming > T I L' 카테고리의 다른 글
Device 잠금 상태 추적해 백그라운드 데이터 업로드 하기 (0) | 2024.11.07 |
---|---|
Firebase RealtimeDatabase 내 데이터 저장하기 (0) | 2024.10.25 |
Equatable를 사용하면서 Extension 해야하는 상황 (0) | 2024.09.25 |
[Project 일지] Running App (1) (0) | 2024.08.30 |
Swift의 async/await에 대해서 알아보자 (0) | 2024.08.19 |