[SwiftUI] State and Data Flow
State and Data Flow
✅ 아래의 문서 중 필요한 부분을 골라서 작성하였습니다.
https://developer.apple.com/documentation/swiftui/state-and-data-flow
SwiftUI는 user interface 디자인을 선언적으로 제공하고 있다. (선언형 프로그래밍) 따라서 view를 계층적으로 구성할 때 views를 위한 데이터도 종속성도 표시한다. 외부 이벤트 혹은 user가 취하는 액션에 의해 데이터의 변화가 있을 때, SwiftUI에 영향을 받는 부분을 자동으로 업데이트 합니다. 결과적으로 이 프레임워크(SwiftUI)는 전통적으로 viewController에서 하는 대부분의 일을 수행합니다.
쉽게 말해서 SwiftUI에서는 MVVM 패턴에서 ViewController의 역할을 대부분 가져간다고 봄.
✅ SwiftUI에는 @State 및 @Binding을 제공하여 user interface에 연결합니다. 이러한 도구들은 앱의 모든 데이터에 대한 단일 소스를 유지하는데 도움을 주며, 작성해야 하는 로직의 양을 줄여줍니다.
(클로저나 delegate 혹은 notification 등을 이용해서 처리하던 긴 로직을 RxSwfit를 사용하면서 크게 줄인 것처럼 이와 같은 이야기!)
✅ 애플 공식문서의 설명!
아래에 설명을 자세하게 하고 있습니다. 글의 흐름을 차례로 따라가보죠!
🟠 SwiftUI의 LifeCycle
- SwiftUI에는 View의 상태를 나타내는 함수가 단 두 개 존재합니다.
- 대신 상태를 나타내는 다양한 Property Wrapper가 존재해 Data 흐름에 대한 여러 상태에 대응할 수 있습니다.
.onAppear {
print("ViewAppeared") // viewDidAppear
}
.onDisappear {
print("ViewDisappeared")
}
🟠 @State
- struct는 value type이라서 값을 변경할 수 없으나, SwiftUI에서 @State를 제공해 strcut내의 값을 변경할 수 있게 해줍니다.
- mutating과 비슷합니다. swiftUI에서 일반적으로 View를 struct로 생성하는데 값을 저장해야 하는 경우에는 class로도 생성할 수 있습니다. 여기서 UIRepesentView를 사용하려면 final로 해야합니다. 이건 내가 해봐서 참고!
- 일반적으로 @State는 private으로 선언되고, 다른 view와 공유하지 않습니다.
- 다른 view와 공유하고 싶다면, @StateObject나 @ObservedObject를 사용하면 됩니다.
🟠 @Binding
- @Binding은 부모 view의 @State와 같은 같을 양방향으로 연결되도록 해줍니다.
- 아래 코드에서 isPresented는 showAddView를 바인딩 시켜줘서 값을 변경해줍니다.
- @Binding의 경우에는 init을 통해서 사용하는게 개인적으로 깔끔합니다. 또한 init 안에서 .constant()로 초기값을 지정할 수 있습니다.
struct ContentView: View {
@State private var showAddView = false
var body: some View {
VStack {
Text("Hello World.")
}
}
.sheet(isPresented: $showAddView) {
AddView(isPresented: self.$showAddView)
}
}
struct AddView: View {
@Binding var isPresented: Bool
var body: some View {
Button("Dismiss") {
self.isPresented = false
}
}
}
여기까지는 Combine의 개념을 모르더라도 쉽게 사용할 수 있다.
내가 이 글을 포스팅하는 이유도 여기부터인데, 프로젝트에서 SwiftUI에 MVVM을 적용해서 ViewModel을 붙이려고 할 때 한번 정리가 필요하다고 느껴서다.
🟠 ObservableObject: Property Wrapper가 아닌 protocol입니다!!
- ObservableObejct는 Protocol이며, Combine 프레임워크의 일부입니다.
- 이를 사용하기 위해서는 프로토콜을 준수하고, @Published를 사용하면 됩니다.
- @Published를 사용하면 변수의 값이 추가되거나 삭제되었다는 것을 view가 알 수 있게 해줍니다.
- ObservableObject는 MVVM 아키텍쳐의 viewModel에 적용하기 좋은 프로토콜입니다.
- ObservableObject protocol은 ObservedObeject property wrapper를 이용하여 외부 참조 모델에 연결합니다.
👉 예시코드 1 (애플 공식문서)
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func haveBirthday() -> Int {
age += 1
return age
}
}
let john = Contact(name: "John Appleseed", age: 24)
cancellable = john.objectWillChange // 🟠 객체가 변경되기 전에 방출합니다.
.sink { _ in
print("\(john.age) will change")
}
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"
👉 예시코드 2 (참고 링크)
이렇게 모델 자체를 걸어두면 더욱 편리합니다.
class MyViewModel: ObservableObject {
@Published var dataSource: MyModel
init(dataSource: MyModel) {
self.dataSource = dataSource
}
}
🟠 @StateObject
- WWDC2020에서 @StateObject를 추가적으로 공개했습니다.
- @ObservedObject와 거의 같은 방식으로 작동한다.
- SwiftUI가 View를 다시 렌더링 할 때, 실수로 취소되는 것을 방지한다.
- view에서 @StateObject를 이용하여 직접 관찰 가능한 객체를 인스턴스화 합니다.
해당 프로퍼티 래퍼에 대한 자세한 설명입니다.
2022.06.02 - [iOS/SwiftUI] - [SwiftUI] @StateObject
👉 예시코드
struct ContentView: View {
@StateObject var user = User()
}
🟠 @ObservedObject - ObservableObject랑 다릅니다!!
- SwiftUI는 @ObservedObject를 통해 view가 외부 객체를 감지할 수 있게 해줍니다.
- 아래 코드에서 User class는 ObservableObject를 준수하고 @Published 변수를 갖고 있습니다.
- @ObservedObject의 user 변수는 이러한 User class 객체를 담고 있습니다.
- SwiftUI는 이러한 user 객체의 @Published 변수 값이 변경될 때 view를 refresh합니다.
class User: ObservableObject {
@Published var name = "Hohyeon Moon"
}
struct ContentView: View {
@ObservedObject var user = User()
var body: some View {
VStack {
Text("Your name is \(user.name).")
}
}
}
🟠 @EnvironmentObject
- @EnvironObject는 보통 앱 전반에 걸쳐 공유되는 데이터에 사용됩니다.
- @EnvironObject는 .environmnetObejct()를 통해 값이 전달할 수 있습니다.
- 전달하는 Object는 ObservableObject 프로토콜을 준수해야 합니다.
- 아래 코드와 같이 rootView를 제공하면, 어떠한 view에서도 사용이 가능합니다.
- EnvironmentObject property wrapper를 통해서 environmnet에 저장된 관찰 가능한 객체에 액세스할 수 있습니다.
👉 예시코드
import SwiftUI
// MySettings.swift
class Settings: ObservableObject {
@Published var version = 0
}
@main
struct CombinePracticeApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(Settings())
}
}
}
struct ContentView: View {
@EnvironmentObject var settings: Settings
var body: some View {
Text("Hello, world!")
.padding()
Button {
settings.version += 1
} label: {
Text("version \(settings.version)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
여기서 잠깐! ObservableObject와 EnvironmentObject가 비슷한 것 같아서 차이를 짚어보자.
EnvironmentObject의 설명은 StateObject에 넣어두었고, 아니면 아래에서 나오니까 추후에 여기를 다시 보아도 좋습니다. 다만 저는 이미 알고 있기에 이쯤에서 짚고 가는게 맞다고 생각되어서 여기서 글을 정리합니다.
1. UI 밖에 위치하면서 앱 내의 SwiftUI 뷰 구조체의 하위 뷰에만 필요한 데이터에 ObservableObject사용
2. UI 밖에 위치하면서 여러 뷰에서 접근해야 하는 데이터를 Environment 오브젝트 활용
View1의 Timer의 시간은 View2A - View2B에서 공유하면서 보여준다고 하자.
ObservableObject사용하면 그냥 하면된다.
다만 EnvironmentObject를 사용하면 ContentView().environment( )를 작성하는 변경이 필요하다.
결론은 개발 플로우에 따라서 스스로 어떤게 더 편할지 선택해서 사용하면 된다.
여기부터는 조금 위와는 조금 다릅니다. 조금 더 가볍게 보셔도 좋습니다:)
🟠 PreferenceKey
- 하나의 뷰에 여러 하위항목(children)이 있는 경우 자동적으로 상위 항목에서 볼 수 있는 단일 값으로 결합한다.(통일한다.)
- key - value로 구성된 데이터 전달 수단입니다.
- 전달 방향은 하위뷰 -> 상위뷰입니다.
마지막으로 총정리 - 이건 다른 분의 자료를 참고하였습니다.
(참고)
https://www.hohyeonmoon.com/blog/swiftui-data-flow/
https://velog.io/@budlebee/SwiftUI-ObservableObject
https://velog.io/@kipsong/SwiftUI-Preference-Key-coordinatespace
https://protocorn93.github.io/tags/PreferenceKey/