-
[SwiftUI] ์ํ ๊ด๋ฆฌiOS/TCA 2023. 7. 5. 10:21
PointFree ๊ฐ์ Composable Architecture
SwiftUI and State Management Part. 1 ~ Part. 3 ์ ๋ฆฌ๐ ์ด๋ฒ ๊ฐ์ ๋ชฉํ
- How to manage state across an entire application
- How to model the architecture with simple units, such as value types
- How to modularize each feature of the application
- How to model side effects in the application
- How to easily write comprehensive tests for each feature
์ ์ฒด ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ
๊ฐ ์ ํ๊ณผ ๊ฐ์ ๊ฐ๋จํ ๋จ์๋ก ์ํคํ ์ฒ๋ฅผ ๋ชจ๋ธ๋งํ๋ ๋ฐฉ๋ฒ
์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ ๊ธฐ๋ฅ์ ๋ชจ๋ํํ๋ ๋ฐฉ๋ฒ
์ ํ๋ฆฌ์ผ์ด์ ์ ๋ถ์์ฉ์ ๋ชจ๋ธ๋งํ๋ ๋ฐฉ๋ฒ
๊ฐ ๊ธฐ๋ฅ์ ๋ํ ํฌ๊ด์ ์ธ ํ ์คํธ๋ฅผ ์ฝ๊ฒ ์์ฑํ๋ ๋ฐฉ๋ฒ๐ ๊ฐ์
ํฌ๊ฒ 3๊ฐ์ ํ๋ฉด์ด ์๊ณ , Counter demo(๋ ๋ฒ์งธ) ํ๋ฉด์ ์ํ ๋ณํ๋ฅผ ๋ค๋ฅธ ํ๋ฉด์์๋ ๊ด๋ฆฌํ ์ ์๋ ์ฝ๋๋ฅผ ๊ตฌํํ๋ค.
ํต์ฌ ํค์๋
@ObservableObject
@ObservedObject
@Binding๐ ์ฝ๋
import Combine class AppState: ObservableObject { @Published var count = 0 @Published var favoritePrimes: [Int] = [] @Published var loggedInUser: User? @Published var activityFeed: [Activity] = [] var didChange = PassthroughSubject<Void, Never>() struct Activity { let timestamp: Date let type: ActivityType enum ActivityType { case addedFavoritePrime(Int) case removedFavoritePrime(Int) } } struct User { let id: Int let name: String let bio: String } }
Combine
์ด๋ฒคํธ ์ฒ๋ฆฌ ์ฐ์ฐ์๋ฅผ ๊ฒฐํฉํ์ฌ ๋น๋๊ธฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฅผ ์ฌ์ฉ์ ์ง์ ํ ์ ์๋ค.
Combine ํ๋ ์์ํฌ๋ ์๊ฐ ๊ฒฝ๊ณผ์ ๋ฐ๋ฅธ ๊ฐ ์ฒ๋ฆฌ๋ฅผ ์ํ ์ ์ธ์ Swift API๋ฅผ ์ ๊ณตํ๋ค.
ObservableObject๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ Combine์ import ํด์ค์ผ ํ๋ค.
ObservableObject๋ฅผ ์์ํ๋ ๊ฒฝ์ฐ objectWillChange๋ผ๋ ํ๋กํผํฐ๊ฐ ์๋ ์์๋๊ณ , objectWillChange.send()๋ผ๋ ๋ฉ์๋๋ฅผ ์ด์ฉํ์ฌ ์ํ์ ๋ณํ๊ฐ ์์์ ์๋ฆด ์ ์๋ค.
@Published๋ก ๋ณ์๋ฅผ ์ ์ธํ๋ ๊ฒฝ์ฐ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋ ์๋์ผ๋ก objectWillChange.send() ๋ฉ์๋๋ฅผ ํธ์ถํ๋ค.
struct ContentView: View { @ObservedObject var state: AppState var body: some View { NavigationView { List{ NavigationLink(destination: CounterView(state: self.state)) { Text("Counter demo") } NavigationLink( destination: FavoritePrimesView( state: self.state, favoritePrimes: self.$state.favoritePrimes, activityFeed: self.$state.activityFeed ) ) { Text("Favorite primes") } } .navigationTitle("State Management") } } }
๋ทฐ ์ ์ญ์์ ์ํ๊ด๋ฆฌ๋ฅผ ํ๊ธฐ ์ํด @ObservedObject๋ฅผ ์ ์ธํด์ค๋ค.
struct CounterView: View { // @State var count: Int = 0 // @State: ์ํ๊ฐ ์ ๋ฐ์ดํธ ๋ ๋๋ง๋ค ๋ทฐ ๋๋๋ง // ํ๋ฉด์ด ๋ฐ๋๋ฉด(๋ค๋ก ๊ฐ๊ฑฐ๋ ๋ค๋ฅธ ํ๋ฉด์ผ๋ก ๋์ด๊ฐ ๋)์ํ๊ฐ ์ ์ฅ๋์ง ์์ // @ObservedObject(AppState ํด๋์ค์์ ObservableObject ํ๋กํ ์ฝ ์ฑํํด์ผ ํจ) @ObservedObject var state: AppState // ๋ชจ๋ฌ ์ํ ์ถ์ (๋ก์ปฌ) @State var isPrimeModalShown: Bool = false @State var alertNthPrime: Int? @State var isAlertShown = false @State var isNthPrimeButtonDisabled = false var body: some View { VStack { HStack { Button(action: {self.state.count -= 1}) { Text("-") } Text("\(self.state.count)") Button(action: {self.state.count += 1}) { Text("+") } } Button (action: {self.isPrimeModalShown = true}) { Text("Is this prime?") } Button (action: self.nthPrimeButtonAction) { // { self.isNthPrimeButtonDisabled = true // nthPrime(self.state.count) { prime in // self.alertNthPrime = prime // self.isAlertShown = true // self.isNthPrimeButtonDisabled = false }} Text("What is the \(ordinal(self.state.count)) prime?") } .disabled(self.isNthPrimeButtonDisabled) } .font(.title) .navigationTitle("Counter demo") .sheet(isPresented: $isPrimeModalShown, onDismiss: { self.isPrimeModalShown = false }) { IsPrimeModalView(state: self.state) } .alert(isPresented: $isAlertShown) { Alert(title: Text("The \(ordinal(self.state.count)) prime is API ๋์ฒด")) } } func nthPrimeButtonAction() { self.isNthPrimeButtonDisabled = true nthPrime(self.state.count) { prime in self.alertNthPrime = prime self.isAlertShown = true self.isNthPrimeButtonDisabled = false } } }
๋ทฐ ์ ์ญ์์ ์ํ ๊ด๋ฆฌ๋ฅผ ํ ํ์๊ฐ ์๋ ๊ฒฝ์ฐ์๋ @State(๋ก์ปฌ ์ํ๊ด๋ฆฌ)๋ก ์ ์ธํด์ค๋ค.
is this prime? ๋ฒํผ์ ๋๋ฅด๋ ๊ฒฝ์ฐ
1. isPrimeModalShown = true
2. ๋ชจ๋ฌ์ ๋ด๋ฆฌ๋ ๊ฒฝ์ฐ(onDismiss) self.isPrimeModalsShown = false
3. IsPrimeModalView(state: self.state) ํธ์ถ
struct IsPrimeModalView: View { @ObservedObject var state: AppState var body: some View { VStack { if isPrime(self.state.count) { Text("\(self.state.count) is prime๐ป") if self.state.favoritePrimes.contains(self.state.count) { Button(action: { self.state.favoritePrimes.removeAll(where: { $0 == self.state.count}) self.state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(self.state.count))) }) { Text("Remove to/from favorite primes") } } else { Button(action: {self.state.favoritePrimes.append(self.state.count) self.state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(self.state.count)))}) { Text("Save to favorite primes") } } } else { Text("\(self.state.count) is not prime๐ป") } } } }
ํด๋น ๋ณ์๊ฐ ์์์ผ ๋
favoritePrimes ๋ฐฐ์ด์ ์ด๋ฏธ ๊ฐ์ด ์๋ ๊ฒฝ์ฐ -> ํด๋น ๊ฐ ์ญ์
favoritePrimes ๋ฐฐ์ด์ ๊ฐ์ด ์๋ ๊ฒฝ์ฐ -> ํด๋น ๊ฐ ์ถ๊ฐ
struct FavoritePrimesView: View { @ObservedObject var state: AppState @Binding var favoritePrimes: [Int] @Binding var activityFeed: [AppState.Activity] var body: some View { List { ForEach(self.state.favoritePrimes, id: \.self) { prime in Text("\(prime)") }.onDelete { indexSet in for index in indexSet { let prime = self.state.favoritePrimes[index] self.state.favoritePrimes.remove(at: index) self.state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(prime))) } } } .navigationBarTitle(Text("Favorite Primes")) } }
'iOS > TCA' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TCA] TCA ์๋ ๋ฐฉ์ (0) 2023.08.31 [SwiftUI] ์ํ๊ด๋ฆฌ(Higher-Order Reducers) (0) 2023.08.02 [SwiftUI] ์ํ๊ด๋ฆฌ(Action Pullbacks) (0) 2023.07.18 [SwiftUI] ์ํ๊ด๋ฆฌ(State Pullbacks) (0) 2023.07.07 [SwiftUI] ์ํ๊ด๋ฆฌ(redux) (0) 2023.07.06