ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

     

     

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

     

     

     

Designed by Tistory.