[SwiftUI] ์ํ ๊ด๋ฆฌ
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"))
}
}