오랜만에 SwiftUI로 프로젝트를 진행하게 되면서 간단하게 개념을 정리해둘 기회가 생겼다.
State와 Binding을 알아보기에 앞서, 선행되어야 할 개념인 Source of Truth와 Property wrapper에 대해 짚고 넘어가자.
Source of Truth
개요
Source of Truth는 데이터 원본이라 생각하면 되는데, State로 선언된 Property는 모두 Source of Truth를 생성하고 참조할 수 있게 된다.
Source of Truth와 State, Binding의 흐름
SwiftUI에서는 property의 Storage를 관리하고, 데이터가 변경되면 이에 의존하는 View 계층의 일부를 업데이트한다.
property를 SoT 개념을 활용해 한 곳에서 통합 관리함을써, 데이터의 일관성과 정확성 유지가 유리해진 것이다.
당연하게도 데이터의 일관성과 정확성을 유지하기 위해서는 단일 데이터(Single Source of Truth, SSoT)로써 존재해야 하고,
이를 복사하면 혼란을 초래할 수 있다.
또한 최상위 뷰(parent view)에서 하위 뷰(subview)로 State값을 Binding하여 하나의 Data를 여러 View에 전달해서 사용함으로써 여러 Data를 관리하지 않아 Side-effect를 예방할 수 있게 되었다.
Property Wrapper
@State 이전에 알아야 할 선행 개념으로 Property Wrapper도 있다.
Property Wrapper는 data flow를 컨트롤 하기 위한 역할로, View와 의존 관계를 가진 데이터를 표시하기 위해 사용한다.
property wrapper로 감싼 데이터가 변경되면 view가 업데이트 된다.
@State, @Binding, @StateObject, @ObservedObject, ... 등이 있다.
@State
SwiftUI에 의해 관리되는 값
앞서 `SwiftUI에서는 property의 Storage를 관리한다.`는 말이 모호하게 다가왔을 수 있는데, @State로 선언된 데이터는 메모리의 어느 공간에 저장해둔 데이터 원본이 있는 것이고, @State를 이용해 해당 데이터를 가리키는 포인터 역할을 한다고 이해하면 된다.
즉, @State값은 진짜 값이 아니라 포인터라고 볼 수 있다.
State 값 관리
- @State로 선언된 property는 property의 storage와 struct의 Memberwise initializer와 충돌할 수 있어 private 사용이 권장된다.
- @State로 선언된 값은 subview에 binding해서 상태를 공유할 수 있고, subview에서는 값을 수정하기 위해 @Binding 키워드를 사용해야 한다.
// MARK: - 상위 뷰
struct PlayerView: View {
@State private var isPlaying: Bool = false // Create the state here now.
var body: some View {
VStack {
PlayButton(isPlaying: $isPlaying) // Pass a binding
// ...
}
}
}
// MARK: - 하위 뷰
struct PlayButton: View {
@Binding var isPlaying: Bool // Play button now receives a binding.
var body: some View {
Button(isPlaying ? "Pause" : "Play") {
isPlaying.toggle()
}
}
}
근데 View는 Struct인데 어떻게 업데이트 하는 걸까?
여기까지 공부하니 View도 Struct이고, State도 Struct이면 immutable한데, 어떻게 업데이트하는지가 가장 궁금했다. 나만 그랬어?
이 또한 @State에 해답이 있다.
앞서 @State는 데이터 원본을 가리키는 포인터의 역할을 한다고 했다.
이 때 SwiftUI에 의해 관리되는 property storage에 저장된 실제 값을 wrappedValue라고 하며,
wrappedValue를 @State로 참조해 사용함으로써, property가 변하는 것처럼 보이는 것이다.
@State 사용 간 주의사항
@Observable
class Library {
var name = "My library of books"
// ...
}
struct ContentView: View {
@State private var library = Library()
var body: some View {
LibraryView(library: library)
}
}
State property는 SwiftUI가 view를 인스턴스화할 때 항상 기본 값을 인스턴스화하기 때문에, State property의 기본값을 초기화할 때, side-effect와 performance-intensive(성능 집약, 무거운) 작업은 지양해야 한다.
이럴 때, view가 처음 표시될 때 한 번만 호출되는 task(priority: ) modifier를 사용해 객체 생성을 지연시킬 수 있다.
네트워크 호출, 파일 엑세스 등의 view 초기 상태를 만드는 데 필요한 작업을 지연시킬 때도 유용함.
struct ContentView: View {
@State private var library: Library?
var body: some View {
LibraryView(library: library)
.task {
library = Library()
}
}
}
Observable Object 객체의 생성을 지연시킴으로써, SwiftUI가 view를 초기화할 때마다 불필요한 객체 할당을 하지 않아도 된다.
State property는 ObservableObject 프로토콜을 준수하는 객체를 저장할 수 있다. 하지만 view는 객체에 대한 참조값이 변경될 때만 업데이트된다.
즉, 객체의 published property가 변경되는 경우는, view가 업데이트 되지 않는다.
참조 및 객체의 published property 모두에 대한 변경 사항을 추적하기 위해서는, 객체를 저장할 때 State 대신 StateObject를 사용해야 한다.
@Binding
앞서 데이터의 통합 관리를 위해 @State로 단일 데이터 원본(SSoT)를 생성한다고 했다. 그래서 새로운 State를 생성하지 않고 SoT를 참조해 뷰 간의 명확한 의존관계를 성립하도록 하기 위해 @Binding을 사용한다.
Binding은 데이터를 저장하는 property와 데이터를 표시하고 변경하는 view 간의 양방향 연결(2-way connection)을 만든다. 데이터를 직접 저장하지 않고 참조하여 데이터의 일관성과 정확성을 지킬 수있다.
Parent view는 State property wrapper를 사용해 이 프로퍼티 값이 value’s source of truth라는 것을 나타내기 위한 property를 선언해야 한다. 또한 Parent View에서 State값을 전달할 때, prefix에 $기호를 붙여 projectedValue를 반환하도록 한다.
struct PlayerView: View {
var episode: Episode
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Text(episode.title)
.foregroundStyle(isPlaying ? .primary : .secondary)
PlayButton(isPlaying: $isPlaying) // Pass a binding.
}
}
}
'🍎 iOS > 문서읽기' 카테고리의 다른 글
[UIKit] UITableViewDiffableDataSource ( iOS 13.0+ ) (0) | 2023.07.30 |
---|---|
[Article - UITableView] Filling a table with data (0) | 2023.07.28 |
[UIKit] UITableView의 구조 (0) | 2023.07.28 |
[UIKit] UITableViewCell의 구조 (0) | 2023.07.28 |
[H.I.G] Designing for iOS 번역 (0) | 2023.07.01 |
댓글