SwiftUI의 데이터 흐름
- SwiftUI는 Apple 앱을 개발하는 완전히 새로운 방식입니다.
- 이번에는 SwiftUI에서의 데이터 흐름과 라이프사이클을 알아보고자 합니다.
- SwiftUI 데이터 흐름에 대해서는 애플의 WWDC19와 WWDC20 영상이 있습니다.
- SwiftUI 데이터 흐름에 대한 애플의 공식 문서도 존재합니다.
- WWDC23 이후 완전히 개편된 간편해진 데이터 사용 방법에 공식 문서도 있습니다.
WWDC 23 이후
Xcode 15와 Swift 5.9 이후, 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
,@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)
}
}
SwiftUI 라이프사이클
- SwiftUI에는 View의 상태를 나타내는 함수가 즉, 라이프사이클이 아래와 같이 단 두가지 밖에 없습니다.
- 대신 상태를 나타내는 다양한 Property Wrapper가 존재해 Data 흐름에 대한 여러 상태에 대응할 수 있습니다.
.onAppear {
print("View appeared")
}
.onDisappear {
print("View disappeared")
}
@State
- 일반적으로 struct는 값 타입이여서 struct내의 값을 변경할 수 없습니다.
- SwiftUI는
@State
를 제공해 struct내의 값을 변경할 수 있게 해줍니다.
struct ContentView: View {
@State private var number = 0
}
- SwiftUI의 view는 struct이고, 이는 언제든 소멸되거나 재생성됩니다.
- 그렇기 때문에
@State
를 사용해 지속적으로 변형 가능한 변수를 만드는 것입니다. - 단,
@State
는 String, Int, Bool과 같은 값 타입에만 사용되는 것이 좋습니다. - 일반적으로
@State
변수는 private으로 선언되고, 다른 view와 공유되지 않습니다. - 다른 view와 값을 공유하고 싶다면, @Binding이나 @ObservedObject를 사용하면 됩니다.
@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 프레임워크의 일부입니다.
- 이것을 사용하기 위해서는, Protocol을 준수하고
@Published
를 사용하면 됩니다. @Published
를 사용하면 변수의 값이 추가되거나 삭제 되었다는 것을 View가 알 수 있게 해줍니다.- ObservableObject는 MVVM 아키텍쳐의 ViewModel에 적용하기 좋은 프로토콜입니다.
class MyViewModel: ObservableObject {
@Published var dataSource: MyModel
init(dataSource: MyModel) {
self.dataSource = dataSource
}
}
@StateObject
- WWDC 2020에서 애플은
@StateObject
를 추가로 공개했습니다. @ObservedObject
와 거의 같은 방식으로 작동하는데요.- SwiftUI가 View를 다시 랜더링 할 때, 실수록 취소되는 것을 방지해줍니다.
struct ContentView: View {
@StateObject var user = User()
}
@ObservedObject
- 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
@EnvironmentObject
는 보통 앱 전반에 걸쳐 공유되는 데이터에 사용됩니다.@EnvironmentObject
는.environmentObject()
를 통해 값을 전달할 수 있습니다.- 전달하는 object는 ObservableObject 프로토콜을 준수해야 합니다.
- 아래 코드와 같이 root view를 제공하면, 어떠한 view에서도 사용이 가능합니다.
// MySettings.swift
class Settings: ObservableObject {
@Published var version = 0
}
// SceneDelegate.swift
var settings = UserSettings()
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(settings))
// MyViews.swift
struct ContentView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
NavigationView {
VStack {
Button(action: {
self.settings.version += 1
}) {
Text("Increase version")
}
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}
}
}
}
}
struct DetailView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
Text("Version: \(settings.version)")
}
}
총 정리
- objc.io의 Chris Eidhof 이미지를 빌려와 총 정리를 해보면 이렇습니다.

마무리
- 이렇게 해서 SwiftUI에서는 라이프사이클과 데이터 흐름을 어떻게 처리하는지 알아봤습니다.