
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 > TCA' 카테고리의 다른 글
TCA - Effect & Test 에 대해서 조금 더 알아보기 (0) | 2025.04.09 |
---|---|
SwiftUI TCA - Dependency, Reducer, Effect에 대해서 알아보자!! (0) | 2024.11.21 |
SwiftUI TCA (3) - Binding (0) | 2024.09.23 |
SwiftUI TCA (2) - Effect (0) | 2024.09.15 |
SwiftUI TCA (1) (0) | 2024.09.09 |