-
[SwiftUI] ์ํ๊ด๋ฆฌ(State Pullbacks)iOS/TCA 2023. 7. 7. 17:44
PointFree ๊ฐ์ Composable Architecture
Composable State Management: State Pullbacks ์ ๋ฆฌ๐ ์ด๋ฒ ๊ฐ์ ๋ชฉํ
์ด์ ์ ์์ฑํ๋ reducer ์ฝ๋๊ฐ ๋ฌด๊ฑฐ์๋ณด์ -> ํ๋์ ํฐ reducer๋ฅผ ํ ๊ฐ์ง ํน์ ์์ ์ ์ํํ๋ ์ฌ๋ฌ ๊ฐ์ reducer๋ก ์ชผ๊ฐ ๋ค์ ์๋ก ์ฐ๊ฒฐํด์ master reducer๋ฅผ ๋ง๋ค์ด๋ณด์
๐ ์ฝ๋
- ๊ธฐ์กด์ ์ฌ๋ฌ ๊ฐ์ง ์ญํ ์ ํ๋ ํจ์๊ฐ ์ ์๋์ด ์๋ reducer ์ฝ๋
func appReducer(value: inout AppState, action: AppAction) -> Void { switch action { case .counter(.decrTapped): state.count -= 1 case .counter(.incrTapped): state.count += 1 case .primeModal(.saveFavoritePrimeTapped): state.favoritePrimes.append(state.count) state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count))) case .primeModal(.removeFavoritePrimeTapped): state.favoritePrimes.removeAll(where: { $0 == state.count }) state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count))) case let .favoritePrimes(.deleteFavoritePrimes(indexSet)): for index in indexSet { let prime = state.favoritePrimes[index] state.favoritePrimes.remove(at: index) state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(prime))) } } }
- ๋ ๊ฐ์ reducer๊ฐ ์์ ๋ ํ๋์ reducer๋ก ๊ฒฐํฉํ๋ ๋ฐฉ๋ฒ
: ์ฒซ ๋ฒ์งธ reducer๋ฅผ ์คํํ ๋ค ๋ ๋ฒ์งธ reducer ์คํํ๋ค!
func combine<Value, Action>( _ first: @escaping (inout Value, Action) -> Void, _ second: @escaping (inout Value, Action) -> Void ) -> (inout Value, Action) -> Void { return { value, action in first(&value, action) second(&value, action) } }
- ๊ธฐ์กด ์ฝ๋๋ฅผ ์ฌ๋ฌ ๊ฐ์ reducer๋ก ๋ถํดํด๋ณด์
func counterReducer(value: inout AppState, action: AppAction) -> Void { switch action { case .counter(.decrTapped): state.count -= 1 case .counter(.incrTapped): state.count += 1 default: break } } func primeModalReducer(state: inout AppState, action: AppAction) -> Void { switch action { case .primeModal(.addFavoritePrime): state.favoritePrimes.append(state.count) state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count))) case .primeModal(.removeFavoritePrime): state.favoritePrimes.removeAll(where: { $0 == state.count }) state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count))) default: break } } func favoritePrimesReducer(state: inout AppState, action: AppAction) -> Void { switch action { case let .favoritePrimes(.removeFavoritePrimes(indexSet)): for index in indexSet { state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index]))) state.favoritePrimes.remove(at: index) } default: break } }
let appReducer = combine(combine(counterReducer, primeModalReducer), favoritePrimesReducer)
app reducer๋ฅผ ์๊น ๋ง๋ combine ํจ์๋ฅผ ์ด์ฉํ์ฌ ์๋กญ๊ฒ ์์ฑํ๋ค
ํ์ฌ combine ํจ์๋ ์ฌ๋ฌ ๊ฐ๋ฅผ ์คํํ ์๋ก ๊ณ์ํด์ ์ค์ฒฉํด์ผ ํ๊ธฐ ๋๋ฌธ์ ๋ณต์กํด์ง๋ค.
๋ฐ๋ผ์ ์ฌ๋ฌ ๊ฐ์ ๊ฐ๋ณ reducer๋ฅผ ๋ฐ์ ์ ์๋ ํจ์๋ก ์๋กญ๊ฒ ์ ์ํ๋ค
func combine<Value, Action>( _ reducers: (inout Value, Action) -> Void... ) -> (inout Value, Action) -> Void { return { value, action in for reducer in reducers { reducer(&value, action) } } }
์์ ๊ฐ์ด ํจ์๋ฅผ ๋ณ๊ฒฝํ๋ฉด appReducer๋ฅผ ์ข ๋ ๊ฐ๋จํ๊ณ ์ฝ๊ธฐ ์ฝ๊ฒ ์ ์ํ ์ ์๋ค.
let appReducer = combine( counterReducer, primeModalReducer, favoritePrimesReducer )
๊ธฐ์กด counterReducer ํจ์๋ฅผ ์ดํด๋ณด๋ฉด ์ค์ ๋ก count์ ๊ฐ๋ง ํ์ํจ์๋ ๋ถ๊ตฌํ๊ณ , ์ ์ฒด appState๋ฅผ ๋งค๊ฐ๋ก ๋ฐ๋๋ค.
๋ฐ๋ผ์ state: inout Int๋ก ๋ณ๊ฒฝํ์ฌ ํ์ํ ๊ฐ๋ง AppState์์ ๊ฐ์ ธ์ฌ ์ ์๋๋ก ํ๋ค.
//func counterReducer(state: inout AppState, action: AppAction) -> Void { func counterReducer(state: inout Int, action: AppAction) -> Void { switch action { case .counter(.decrTapped): // state.count -= 1 state -= 1 case .counter(.incrTapped): // state.count += 1 state += 1 default: break } }
ํ์ง๋ง ์ด์ ๊ฐ์ด ๋งค๊ฐ๋ณ์๋ฅผ ๋ณ๊ฒฝํ๋ ๊ฒฝ์ฐ reducer๊ฐ ๊ฐ์ state๋ฅผ ๊ฐ๋ฆฌํค์ง ์๊ธฐ ๋๋ฌธ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
counterReducer๋ integer๊ฐ๋ง ํ์๋ก ํ์ง๋ง, ๋ค๋ฅธ reducer๋ค์ ์ ์ฒด appState๊ฐ ํ์ํ๋ค.
โจ ์ด๋ฌํ ์ํฉ์์ ์ฌ์ฉํ๋ ๊ฒ์ด pullback!
pullback์ ์์ ๋ฐ์ดํฐ์์ ํฐ ๋ฐ์ดํฐ๋ก ๋ณํํ๋๋ฐ ์ ๋ฆฌํ๋ค!
์๋ฅผ ๋ค์ด integer ๊ฐ์ด ์ฃผ์ด์ก์ ๋ ์ฌ์ฉ์ ID ํ๋ ๊ฐ์ ํฌ์ํ์ฌ ์์์ ๋ชจ๋ธ์ ๋ํ ์ ์ด๊ฐ ๋๋๋ก ๋ค์ ๊ฐ์ ธ์ฌ ์ ์๋ค.
Local ์ํ์ reducer๋ฅผ Global ์ํ์ reducer๋ก ๋ณํํ ์ ์๋ ํจ์๋ฅผ ๋ง๋ค์ด๋ณด์.
func pullback<LocalValue, GlobalValue, Action>( _ reducer: @escaping (inout LocalValue, Action) -> Void ) -> (inout GlobalValue, Action) -> Void { }
์๋ฐ basic ํจ์์๋ค๊ฐ LocalValue์ GlobalValue ์ ๋ค๋ฆญ์ ์ฐ๊ฒฐํ์
func pullback<LocalValue, GlobalValue, Action>( _ reducer: @escaping (inout LocalValue, Action) -> Void, get: @escaping (GlobalValue) -> LocalValue, set: @escaping (inout GlobalValue, LocalValue) -> Void ) -> (inout GlobalValue, Action) -> Void { return { globalValue, action in var localValue = get(globalValue) reducer(&localValue, action) set(&globalValue, localValue) } }
pullback(counterReducer, get: { $0.count }, set: { $0.count = $1 }),
pullback ํจ์๋ฅผ ํตํด ๋ค์๊ณผ ๊ฐ์ด ํธ์ถํ ์ ์๋ค.
get, set ์ธ์๋ฅผ WritableKeyPath๋ฅผ ํตํด ์ข ๋ ๊ฐ๋จํ๊ฒ ์์ฑํ ์ ์๋ค.
func pullback<LocalValue, GlobalValue, Action>( _ reducer: @escaping (inout LocalValue, Action) -> Void, value: WritableKeyPath<GlobalValue, LocalValue> ) -> (inout GlobalValue, Action) -> Void { return { globalValue, action in reducer(&globalValue[keyPath: value], action) } }
key path๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ ์ ์๋ค.
let _appReducer = combine( pullback(counterReducer, value: \.count), primeModalReducer, favoritePrimesReducer ) let appReducer = pullback(_appReducer, value: \.self)
\.self๋ AppState์์ AppState๋ก ์ด๋ํ๋ ํต์ฌ ๊ฒฝ๋ก์ด๋ค.
์ด์ favoritePrimesReducer ํจ์๋ฅผ pullbackํด๋ณด์!
// ์ํ๋ ๋ฐ์ดํฐ๋ง AppState์์ ๊ฐ์ ธ์ฌ ์ ์๋๋ก ๊ตฌ์กฐ์ฒด ์๋ก ์ ์ธ struct FavoritePrimesState { var favoritePrimes: [Int] var activityFeed: [AppState.Activity] } func favoritePrimesReducer(value: inout FavoritePrimesState, action: AppAction) -> Void { switch action { case let .favoritePrimes(.removeFavoritePrimes(indexSet)): for index in indexSet { state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index]))) state.favoritePrimes.remove(at: index) } default: break }
๋ฐ์ํ๋ ์๋ฌ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด AppState์ ์๋ favoritePrimesReducer๋ฅผ ์์ ํด์ค๋ค.
(ํ์ํ ์ํ ๊ฐ์ด ๋จ์ผ AppState๊ฐ ์๋๋ผ ๋ ๊ฐ์ AppState์์ ๋์ค๊ธฐ ๋๋ฌธ)
extension AppState { var favoritePrimesState: FavoritePrimesState { get { return FavoritePrimesState( favoritePrimes: self.favoritePrimes, activityFeed: self.activityFeed ) } set { self.activityFeed = newValue.activityFeed self.favoritePrimes = newValue.favoritePrimes } } }
appReducer๊ฐ์ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝํด์ค๋ค.
let _appReducer = combine( pullback(counterReducer, value: \.count), primeModalReducer, pullback(favoritePrimesReducer, value: \.favoritePrimesState) )
์์ ์ ํ์ํ ์ต์ํ์ state์์๋ง ์๋ํ๋๋ก ์ ์ฒด reducer๋ฅผ ์ธ๋ถํํ๊ณ , ์ ์ฒด reducer์ ๊ฐ์ ธ์ฌ ์ ์๋๋ก ์ฝ๋๋ฅผ ์์ ํด๋ดค๋ค.
'iOS > TCA' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TCA] TCA ์๋ ๋ฐฉ์ (0) 2023.08.31 [SwiftUI] ์ํ๊ด๋ฆฌ(Higher-Order Reducers) (0) 2023.08.02 [SwiftUI] ์ํ๊ด๋ฆฌ(Action Pullbacks) (0) 2023.07.18 [SwiftUI] ์ํ๊ด๋ฆฌ(redux) (0) 2023.07.06 [SwiftUI] ์ํ ๊ด๋ฆฌ (0) 2023.07.05