iOS/TCA

[SwiftUI] ์ƒํƒœ ๊ด€๋ฆฌ

year.number 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๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

 

 

Combine | Apple Developer Documentation

Customize handling of asynchronous events by combining event-processing operators.

developer.apple.com

 

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"))
    }
}