기존 코드
Timer View의 하위 View Components를 Enum으로 구분해 Interaction이 발생하면 해당 Enum 값을 방출함
타이머 뷰 컴포넌트 Interaction Enum 값으로 분리
extension TimerFeature{
enum ActionType:Equatable{
case timerFieldTapped
case catTapped
case resetTapped
case triggerTapped
case triggerWillTap
}
enum Action:Equatable{
case viewAction(ActionType)
...
}
}
View 컴포넌트에서 사용
enum TimerViewComponents{
...
struct ResetButton: View{
let store: StoreOf<TimerFeature>
var body: some View{
Button("Reset"){
**store.send(.viewAction(.resetTapped))**
}.resetStyle()
}
}
}
TimerFeature에서 유저 Interaction이 일어난 뷰 Action 타입에 맞는 하위 메서드 제작 및 호출
extension TimerFeature{
func viewAction(_ state:inout State,_ act: ViewAction) -> Effect<Action>{
switch act{
case .timerFieldTapped:
return self.timerFieldTapped(state: &state)
case .catTapped:
return self.catTapped(state: &state)
case .resetTapped:
return self.resetTapped(state: &state)
case .triggerTapped:
return self.triggerTapped(state: &state)
case .triggerWillTap: return self.triggerWillTap(state: &state)
}
}
//MARK: -- TimerFieldTapped Reducer 실제 구현 부
func timerFieldTapped(state:inout TimerFeature.State) -> Effect<TimerFeature.Action>{ ... }
//MARK: -- Cat Tapped Reducer
func catTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>{ ... }
func resetTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>{ ... }
func triggerTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>{ ... }
func triggerWillTap(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>{ ... }
}
기존 코드의 문제점
1. Dependency 변화 대처의 어려움
탭 액션에 따른 처리해야하는 Dependency가 현재 3가지, 추후에 더 추가될 예정…
- 유저 가이드 처리
- 타이머 상태 변경
- 햅택 처리
➕ 사운드 처리
⇒ 하나의 View Component Interaction에 혼재하는 로직 처리…
Trigger 버튼 Tap 시 처리해야하는 로직 기존 코드
fileprivate extension TimerFeature{
...
func triggerTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>{
let hapticEffect:Effect<Action> = .run { _ in await haptic.impact(style: .light) }
switch state.timerStatus{
case .standBy:
guard state.count != 0 else {return .none }
state.startDate = Date()
state.guideInformation.standByGuide = true
var effects:[Effect<Action>] = [hapticEffect,.run { send in
await send(.setStatus(.focus))
},.run {[guide = state.guideInformation] send in
await send(.setGuideState(guide))
}]
if !state.guideInformation.startGuide{
var guide = state.guideInformation
guide.startGuide = true
effects.append(.run {[guide] send in
try await Task.sleep(for: .seconds(3))
await send(.setGuideState(guide),animation: .easeInOut)
})
}
return Effect.concatenate(effects)
case .focus: return .run { send in
await send(.setStatus(.pause))
}.merge(with: hapticEffect)
case .pause:
return .run {[count = state.count] send in
await send(.setStatus(.focus,count: count))
}.merge(with: hapticEffect)
case .completed: return .run{ send in
await send(.setStatus(.standBy))
}.merge(with: hapticEffect)
case .breakStandBy:
state.startDate = Date()
return .run { send in
await send(.setStatus(.breakTime))
}.merge(with: hapticEffect)
case .breakTime: return .concatenate([.cancel(id: CancelID.timer),
.run { await $0(.setStatus(.standBy)) }]).merge(with: hapticEffect)
case .sleep: return .none
}
}
2. 기획 변경에 따른 대처
- 개발 도중 기획에 변경으로 사라진 Circle Timer Interaction
- 기획에서 Haptic 세기 정도의 변경
Controller 객체로 분화 및 처리
⚠️ Presenter대신, Controller라는 의미를 부여한 이유
- Controller의 정확한 의미를 발견하지 못 했음... 그러나...
1. View Action에 대한 처리
2. TimerFeature State에 참조로 접근하는 경우가 존재함 → State 값에 직접 접근한다.는 점에서 Controller라 이름 붙임
1. Protocol 정의
protocol TimerControllerProtocol{
func makeReducer(state: inout TimerFeature.State,
act:TimerFeature.ControllType) -> Effect<TimerFeature.Action>
func timerFieldTapped(state:inout TimerFeature.State) -> Effect<TimerFeature.Action>
func catTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>
func resetTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>
func triggerTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>
func triggerWillTap(state: inout TimerFeature.State) -> Effect<TimerFeature.Action>
}
Protocol Extension을 통해 기본 채택 메서드를 정의
extension TimerControllerProtocol{
func makeReducer(state: inout TimerFeature.State,act: TimerFeature.ControllType)->Effect<TimerFeature.Action>{
switch act{
case .catTapped: catTapped(state: &state)
case .resetTapped: resetTapped(state: &state)
case .timerFieldTapped: timerFieldTapped(state: &state)
case .triggerTapped: triggerTapped(state: &state)
case .triggerWillTap: triggerWillTap(state: &state)
}
}
}
2. 구현부 Controller를 모두 포함하고 TimerFeature에서 접근 가능한 Controller 인스턴스
enum 타입으로 구현부 Controller를 구분함
extension TimerFeature{
enum ControllerReducers:CaseIterable{
case haptic,guide,action
private var myReducer: TimerControllerProtocol{
switch self{
case .haptic: HapticReducer()
case .guide: GuideReducer()
case .action: ActionReducer()
}
}
static func makeAllReducers(state:inout TimerFeature.State,act:ControllType) -> [Effect<Action>]{
Self.allCases.map { reducer in
reducer.myReducer.makeReducer(state: &state, act: act)
}
}
}
}
구현부 Controller
Trigger Tap시 처리하는 메서드만 표시함
프로토콜 Extension 기본 메서드를 통해 실제 구현부는 상위 통합 Controller에서 호출하는 makeReducer을 구현 사항을 알 필요가 없음
1. Haptic Controller
extension TimerFeature.ControllerReducers{
struct HapticReducer: TimerControllerProtocol{
@Dependency(\.haptic) var haptic
...
func triggerTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action> {
let hapticEffect: Effect<TimerFeature.Action> = .run { send in
await haptic.impact(style: .light)
}
return hapticEffect
}
...
}
}
2. Guide Controller
extension TimerFeature.ControllerReducers{
struct GuideReducer: TimerControllerProtocol{
@Dependency(\.guideDefaults) var guideDefaults
...
func triggerTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action> {
if !state.guideInformation.onBoarding{
state.guideInformation.onBoarding = true
var guide = state.guideInformation
guide.startGuide = true
return .run {[guide] send in
try await Task.sleep(for: .seconds(3))
await send(.setGuideState(guide),animation: .easeInOut)
}
}
return .none
}
...
}
}
3. ActionController
💡 Interaction에 따른 타이머 상태를 바꾸는 요청
extension TimerFeature.ControllerReducers{
struct ActionReducer: TimerControllerProtocol{
typealias Action = TimerFeature.Action
...
func triggerTapped(state: inout TimerFeature.State) -> Effect<TimerFeature.Action> {
switch state.timerStatus{
case .standBy:
guard state.count != 0 else {return .none}
return .run { send in
await send(.setStatus(.focus,startDate: Date()))
}
case .focus: return .run { send in
await send(.setStatus(.pause))
}
case .pause:
return .run {[count = state.count] send in
await send(.setStatus(.focus,count: count))
}
case .completed: return .run{ send in
await send(.setStatus(.standBy))
}
case .breakStandBy:
return .run { send in
await send(.setStatus(.breakTime,startDate: Date()))
}
case .breakTime: return .run { await $0(.setStatus(.standBy)) }
case .sleep: return .none
}
}
...
}
}
'이모저모 > SwiftUI' 카테고리의 다른 글
iPad ConfirmationDialog 에러 대응 (0) | 2024.06.23 |
---|---|
SwiftUI View Nested Type 사용 컨벤션 (0) | 2024.05.30 |
TCA에서 Stream 생성 및 주입 (0) | 2024.04.13 |
Using @globalActor RealmSwift in TCA (0) | 2024.03.25 |
TCA - Pagination TabView 사용 (0) | 2024.03.17 |
댓글