SwiftUI의 데이터 흐름
- SwiftUI에서의 데이터 흐름과 라이프사이클을 알아보고자 합니다.
- SwiftUI 데이터 흐름에 대해서는 애플의 WWDC19와 WWDC20 영상이 있습니다.
- WWDC23 이후 완전히 개편된 간편해진 데이터 사용 방법에 공식 문서도 있습니다.
SwiftUI 라이프사이클
- SwiftUI에는 View의 상태를 나타내는 라이프사이클이 아래와 같이 단 두가지 밖에 없습니다.
- 대신 상태를 나타내는 다양한 Property Wrapper가 존재해 Data 흐름에 대한 여러 상태에 대응할 수 있습니다.
.onAppear {
print("View appeared")
}
.onDisappear {
print("View disappeared")
}
WWDC 23 이후
Xcode 15와 Swift 5.9 이후, Observation의 Observable Macro를 통해 더 쉽게 SwiftUI를 다룰 수 있게 되었습니다
Observable
프로토콜을 준수하는 대신,@Observable
Macro를 표시합니다.- 이로 인해,
@Published
프로퍼티 래퍼도 이제 필요하지 않게 되었습니다.
// BEFORE
class Library: ObservableObject {
@Published var books: [Book] = [Book(), Book(), Book()]
}
// AFTER
@Observable class Library {
var books: [Book] = [Book(), Book(), Book()]
}
@State
,@Binding
,@StateObject
,@ObservedObject
,@EnvironmentObject
와 같은 프로퍼티 래퍼들이 있었습니다.- 그러나 이제는 훨씬 간단하게, 값 타입과 참조 타입 모두
@State
프로퍼티 래퍼만을 사용하면 됩니다.
// BEFORE
@main
struct BookReaderApp: App {
@StateObject private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
// AFTER
@main
struct BookReaderApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(library)
}
}
}
@EnvironmentObject
역시,@Environment
로 통일할수 있게 되었습니다.
// BEFORE
struct LibraryView: View {
@EnvironmentObject var library: Library
var body: some View {
List(library.books) { book in
BookView(book: book)
}
}
}
// AFTER
struct LibraryView: View {
@Environment(Library.self) private var library
var body: some View {
List(library.books) { book in
BookView(book: book)
}
}
}
@ObservedObject
역시, 필요가 없어졌습니다.
// BEFORE
struct BookView: View {
@ObservedObject var book: Book
var body: some View {
Text(book.title)
}
}
// AFTER
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
Observable
을 준수하는 오브젝트에 대해@Binding
이 필요한 경우에만@Bindable
을 사용하면 됩니다.
// BEFORE
struct BookEditView: View {
@ObservedObject var book: Book
var body: some View {
TextField("Title", text: $book.title)
}
}
// AFTER
struct BookEditView: View {
@Bindable var book: Book
var body: some View {
TextField("Title", text: $book.title)
}
}
WWDC 23 이전
@State
- State 프로퍼티 래퍼는 크게 2가지 역할을 합니다.
- View를 준수하는 struct 변수의 값을 변경할수 있게합니다.
- 변수의 값이 변경되면 View의 body가 다시 계산될수 있게합니다.
- State 변수는 주로 Source of Truth 역할을 하기에 private으로 선언됩니다.
- 다른 view와 이를 공유하고 싶다면, 아래에서 소개될
@Binding
이나@ObservedObject
를 사용합니다.
struct ContentView: View {
@State private var number = 0
}
@Binding
- Binding 프로퍼티 래퍼는 부모 view의 State 변수와 같은 값을 양방향으로 연결되도록 합니다.
- 아래 코드에서
isPresented
는showAddView
를 바인딩 시켜줘서 값을 변경합니다.
struct ContentView: View {
@State private var showAddView = false
var body: some View {
VStack {
Button("Trigger") {
showAddView = true
}
}
.sheet(isPresented: $showAddView) {
AddView(isPresented: self.$showAddView)
}
}
}
struct AddView: View {
@Binding var isPresented: Bool
var body: some View {
Button("Dismiss") {
self.isPresented = false
}
}
}
ObservableObject
- ObservableObject Protocol은 Combine 프레임워크의 일부로, 객체를 옵저빙 할 수 있게 도와줍니다.
- 클래스가 ObservableObject Protocol을 준수하도록 해주고, Published 프로퍼티 래퍼를 사용하면 됩니다.
- Published 변수를 사용하면 변수의 값이 변경 되었다는 것을 View가 알 수 있게 해줍니다.
class MyViewModel: ObservableObject {
@Published var dataSource: MyModel
init(dataSource: MyModel) {
self.dataSource = dataSource
}
}
@StateObject
- StateObject 프로퍼티 래퍼를 WWDC 2020에서 애플이 추가로 공개했습니다.
- ObservedObject 프로퍼티 래퍼와 비슷한 방식으로 작동하지만, View가 다시 랜더링 될 때 릴리즈 되는것을 방지해줍니다.
- State와 마찬가지로, 일반적으로 Source of Truth 역할을 하기에 주로 private으로 선언됩니다.
class User: ObservableObject {
@Published var name = "Hohyeon Moon"
}
struct ContentView: View {
@StateObject private var user = User()
}
@ObservedObject
- ObservedObject 프로퍼티 래퍼는 view가 객체를 옵저빙 할 수 있게합니다.
- 아래 코드에서 User class는 ObservableObject를 준수하고
@Published
변수를 갖고 있습니다. @ObservedObject
user 변수는 이러한 User class 객체를 담고 있습니다.- SwiftUI는 이러한 user 객체의
@Published
변수 값이 변경될 때 view를 refresh합니다.
struct ContentView: View {
@StateObject private var user = User()
var body: some View {
ChildView(user: user)
}
}
struct ChildView: View {
@ObservedObject var user: User
var body: some View {
Text(user.name)
}
}
@EnvironmentObject
@EnvironmentObject
는 보통 앱 전반에 걸쳐 공유되는 데이터에 사용됩니다.@EnvironmentObject
는.environmentObject()
를 통해 값을 전달할 수 있습니다.- 전달하는 object는 ObservableObject 프로토콜을 준수해야 합니다.
- 아래 코드와 같이 root view를 제공하면, 어떠한 view에서도 사용이 가능합니다.
class Settings: ObservableObject {
@Published var version = "0"
}
struct ContentView: View {
@StateObject var settings = Settings()
var body: some View {
MainView()
.environmentObject(settings)
}
}
struct MainView: View {
@EnvironmentObject var settings: Settings
var body: some View {
Text(settings.version)
}
}
Property Wrapper
- DynamicProperty를 준수하면, 값이 변경됐을때 View의 body가 다시 계산됩니다.
- State, Binding, StateObject, ObservedObject, EnvironmentObject 등은 DynamicProperty를 준수합니다.
- SwiftUI에는 이와 같이 여러가지 프로퍼티 래퍼가 있고 이를 접근하는 방식이 여러가지 존재합니다.
- 아래와 같이 State 변수 number는
number
,_number
,$number
로 접근 가능합니다.
@State private var number = 0
_number // Binding<Int>
number // Int (= _number.wrappedValue)
$number // Binding<Int>: (= _number.projectedValue)
총 정리
- 이렇게 해서 SwiftUI에서는 라이프사이클과 데이터 흐름을 어떻게 처리하는지 알아봤습니다.
- swiftuipropertywrappers.com의 이미지를 빌려와 총 정리를 해보면 이렇습니다.
참고
본문에 링크되어 있는 링크는 제외했습니다