헷갈리는 부분이 있어 다시보기위해 정리해보자..
View
, App
, Scene
에 변경 가능한 값을 저장하고 공유하기 위해서 사용하는
뷰 계층 구조 내에서 단일 진실 소스를 확립하며 swiftUI 내부적으로는 사용하는 View
의 외부 저장소에 데이터를 저장함
-> 선언형 UI인 SwiftUI에서 View
는 struct, 값 타입인 뷰가 재렌더링 되더라도 값을 잃지 않고 사용할 수 있다..
값이 변경되면 해당 속성을 사용하고 있는 View
들을 업데이트 한다.
-> delegate나 데이터 소스 메서드없이 데이터에 반응가능
struct PlayButton: View {
@State private var isPlaying: Bool = false // Create the state.
var body: some View {
Button(isPlaying ? "Pause" : "Play") { // Read the state.
isPlaying.toggle() // Write the state.
}
}
}
래핑하려는 인스턴스는 값 유형이여야 한다.
(인스턴스 객체에 대한 변경을 감지하기 때문에 참조 타입일 경우 객체의 게시된 published 속성이 변경되더라도 업데이트 하지 않음, 참조 값 자체가 변경 될 때 업데이트 함)
데이터를 저장하는 속성과 데이터를 표시하고 변경하는 뷰 간에 양방향 연결 즉 읽기와 쓰기 모두 할 때 사용
외부에 저장된 단일 데이터 소스에 연결함.
즉 SwiftUI의 저장소에 데이터를 저장하는 것이 아닌 이미 저장소에 존재하는 단일 진실 소스에 연결하는 것!
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.
}
}
}
struct PlayButton: View {
@Binding var isPlaying: Bool
var body: some View {
Button(isPlaying ? "Pause" : "Play") {
isPlaying.toggle()
}
}
}
Binding을 넘겨 줄 때는 $
달러사인을 접두사로 사용.
(단일 진실 소스의 참조를 넘겨주는 표시, 없다면 해당 값을 복사해서 넘겨주기 때문에 수정이 불가능)
래핑된 속성을 소유하지 않기 때문에 뷰가 제거되면 binding도 사라짐.(뷰가 삭제되어도 외부 저장소에 남아있는 @state과 달리)
값 타입이 아닌 참조타입의 상태 데이터를 사용할 때를 알아보자
먼저 ObserableObject
프로토콜을 알아야 한다.
protocol ObservableObject : AnyObject
ObserableObsect
프로토콜을 채택하면 해당 객체가 변경되기 전 변경을 알리는 publisher
로 간주 한다.
즉 해당 인스턴스를 subscriber
가 구독해 변경을 처리할 수 있다.
변경을 처리하는 방법은 @Published 프로퍼티 래퍼로 선언한다.
선언된 값이 변경 된다면 willset의 시점에서 objectWillChange
메서드가 실행되며
구독하는 쪽은 objectWillChange.sink
를 사용해 변경에 대한 이벤트를 처리한다.
@Published를 사용하지 않고 직접 objectWillChnage.send()
를 willset에서 사용해 알릴 수도 있다.
@propertyWrapper public struct Published<Value>
내부적으로 데이터를 방출하는 역할로
내부적으로 willset에서 데이터의 변경을 알린다고 한다
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"
다시 @StateObject
로 돌아오면
ObserableObject
를 View
외부에 있는 swiftUI의 저장소에 저장해 단일 진실 소스로 사용하기 위해서 사용한다.
해당 객체 내부의 @Published
인스턴스의 변경을 알리고 사용할 수 있는데
@StateObject
는 딱 한번 생성된다(저장소와의 연결이 최초 한번 이루어지며 변경되지 않음)
즉 View
내부에서 ObserableObject
를 채택한 객체를 소유하고 데이터를 사용할 때 사용하고 View
가 업데이트되어도 데이터가 남아있다.
@ObservedObject
와 비교하면 이해가 쉽다
@StateObject
와 같이 ObservableObject
프로토콜을 채택한 객체를 사용하는 View
의 외부인 swiftUI의 특정 저장소에 저장하고 사용이 가능하다.
다만 차이점은View
가 파괴되면 해당 인스턴스가 초기화 된다.
즉 View
를 담고있는 컨테이너의 변경에도 데이터 일관성을 유지하고 싶다면 @StateObject
를 사용하자.
struct EnvironmentObject<ObjectType> where ObjectType : ObservableObject
마찬가지로 ObserableObject
프로토콜을 채택한 객체를 할당 할 때 사용하는데
선언한 View
의 하위 계층 View
에서도 사용할 수 있도록 해준다.
사용 범위의 최상단 부모 뷰에 .environmentObject(Object)
를 선언해 컴파일 시 접근 주소를 알수 있도록 해야한다.
자식 뷰에서 해당 객체의 게시된 프로퍼티에 대해 변경을 할 경우 부모뷰에도 변경을 알린다.
-> 뷰 계층 구조간 공유 데이터 사용이 용이
iOS 17.0+ 업데이트된 내용으로 데이터 모델을 관찰 가능하도록 선언하게 미리 정의된 매크로이다.
// MARK: Old Observable Class
class WeatherViewModel: ObservableObject {
@Published var currentTemperature: Double = -5.2
@Published var city: String = "Sivas"
@Published var isSnowing: Bool = false
}
// MARK: New Observable Class
@Observable class WeatherViewModel {
var currentTemperature: Double = -5.2
var city: String = "Sivas"
var isSnowing: Bool = false
}
기존의 ObserableObject
를 대신해서 @Obserable
매크로를 데이터 모델앞에 추가하는 것으로
@Published
없이 모든 변수들에 @ObservationTracked
가 디폴트로 추가된다.
(게시하고 싶지 않다면 @ObservationIgnored
매크로를 추가한다.)
이때 매크로가 실행되며 해당 객체는 Obserable
프로토콜을 채택한 것이 된다.
(직접 Obserable 프로토콜을 채택하면 적용 안됨, @Obseralbe
매크로를 통해 채택해야 함)
Obserable
프로토콜을 채택한 객체들은 StateObject
나 ObservedObject
가 아닌 다른 방식으로 사용한다.
Obserable 매크로를 통해 Observable 프로토콜은 채택한 경우 @State를 통해 참조타입의 단일 진실 소스 인스턴스를 생성해 변경을 알릴 수 있다.
이 경우 해당 View
가 인스턴스를 소유하는 형태가 되어 View
가 재랜더링 되더라도 데이터 상태가 유지된다.(@StateObject
처럼)
@Observable
class WeatherViewModel {
...
}
struct WeatherView: View {
@State var weatherVM = WeatherViewModel()
var body: some View {
...
}
}
위의 코드와 같이 @State
로 인스턴스를 생성한다.
@Obserable
을 선언한 데이터 모델의 인스턴스는 참조 타입인 클래스만 가능하다.
⭐️ ObservableObject
에 비한 장점
element
에 해당하는 인스턴스 내부의 프로퍼티를 추적하고 싶다면 해당 element
도 @Obserable
을 적용해야 한다.)@Published
프로퍼티가 변경되면 해당 프로퍼티를 소유하지 않더라도 View
에서 ObserableObject
를 관찰하고 있다면 뷰가 업데이트 되었지만, Obserable
매크로는 View
에서 사용중인 프로퍼티가 변경되었을 때만 업데이트 -> 효율적인 드로잉 매커니즘final class Book: ObservableObject {
...
}
struct LibraryView: View {
@ObservedObject var books = [Book(), Book(), Book()]
var body: some View {
...
}
}
위는 불가능하지만 아래는 가능
@Observable final class Book {
...
}
struct LibraryView: View {
var books = [Book(), Book(), Book()] // 가능
var body: some View {
...
}
}
역시 마찬가지로 @Binding
을 사용할 수 있는데 하위 뷰에서 class의 프로퍼티에 대해 바인딩을 생성 할 수 있다.
@Obserable
매크로가 추가되면서 @State
를 사용해 참조 타입의 인스턴스를 View
가 소유하도록 생성할 수 있게 되었다.
@State
를 사용한다면 단일 진실 소스를 인스턴스화 해서 뷰가 해당 인스턴스를 소유하게 되는데, @ObservedObject
를 사용할 때 처럼 뷰가 소유하지 않을 경우에 사용하는 것이 @Bindable
이다.
(위의 카운터 예제 앱을 Obserable을 사용하는 것으로 마이그레이션,
CounterViewModel
은@Obserable
매크로를 채택)
위와 같이 viewModel
을 @State
로 선언하지 않아야 할 경우(뷰가 소유하지 않을 경우)
viewModel
에 대한 바인딩을 Stepper
에 연결하려면 아래와 같이 바인딩을 직접 만들어야 한다.
이를 간편하게 만들기 위해서 코드상에서 주석처리된 부분처럼 @Bindable
프로퍼티 래퍼를 사용하면 된다.
사용하게 된다면 $
을 통해서 바인딩을 전달 가능하다.
코드로 정리하면
struct ContentView: View {
@State private var book = Book() // 소유(단일 진실 소스 인스턴스화)
var body: some View {
BookView(book: book)
}
}
struct BookView: View {
let book: Book // 단일 진실 소스를 생성하지 않음(소유하지 않고 해당 소스에 대한 주소값만 가지고 있음)
var body: some View {
BookEditorView(book: book)
}
}
struct BookEditorView: View {
@Bindable var book: Book // 소유하지 않음, 프로퍼티에 대한 바인딩 전달 필요
var body: some View {
TextField("Title", text: $book.title)
}
}
뷰 계층 내에서 사용하는 데이터 모델을 공유하고 변경을 알릴 때 @Observalbe
이 적용된 객체에 대해서는 @EnvironmentObject
가 아닌 기존의Environmet
로 사용하라고 한다.
EnvironmentValues
는 colorScheme, timeZone, locale 등과 같이 뷰의 환경에 저장되어 하위 뷰에게 상속하게 되어 해당 변수를 읽고 다르게 처리할 수 있었는데
Observable
프로토콜을 준수하는 객체에 대해서 환경 변수로 설정할 수 있다.
@Observable
class Library {
var books: [Book] = [Book(), Book(), Book()]
var availableBooksCount: Int {
books.filter(\.isAvailable).count
}
}
@main
struct BookReaderApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(library)
// .environment(\.library, library) key-path로 객체 가져오기
}
}
}
struct LibraryView: View {
@Environment(Library.self) private var library: Library? // 옵셔널 가능
//@Environment(\.library) private var library key-path로 가져올 때
var body: some View {
// ...
}
}
프로퍼티 래퍼 선택의 핵심은 데이터 모델의 타입과 뷰가 소유해야하는지 여부이다.
17.0 이전에는
@State
를 사용해 값 타입의 데이터 모델의 단일 진실 소스를 인스턴스화 해 뷰가 소유하도록 하고 변경에 대해 알릴 수 있으며 @Binding
프로퍼티 래퍼를 통해 자식뷰에서 전달 받은 바인딩을 사용할 수 있었다.
클래스와 같은 참조타입의 경우엔 데이터 모델에 ObservedObject
프로토콜을 채택함으로 @Published
프로퍼티 래퍼로 감싸진 프로퍼티에 대한 변경을 알릴 수 있었는데
데이터 모델의 인스턴스를 생성할 때 뷰가 소유하도록 해 뷰가 파괴되더라도 값을 유지하도록 하려면 @StateObject
를, 그렇지 않다면 @ObservedObject
를 사용했다.
뷰 계층 구조내에서 참조타입의 동일한 데이터 모델의 변경을 알려야 한다면 차례차례 모델의 참조를 전달하는 대신 @EnvironmentObject
를 사용할 수 있는데
사용하는 데이터 모델을 소유한 최상단의 뷰 계층에 데이터를 저장하고 하위 계층의 뷰에서 @EnvironmentObejct
프로퍼티 래퍼를 사용해 인스턴스화 하고 모델을 사용하는 context
내에 변경을 알릴 수 있다.
17.0+ 에서 추가된 Observation에서는
참조 타입의 데이터 모델의 변경에 반응하기 위해서 해당 모델에 @Observable
매크로를 구현함으로써 코드가 간결해지고 컬렉션이나 옵셔널타입의 데이터 모델에 대해서도 인스턴스화가 가능했는데, 변경되는 프로퍼티를 사용하는 뷰에서만 변경이 일어나 효율적인 뷰 드로잉 매커니즘을 갖게 되었다.
뷰가 소유하도록 해 상태를 유지해야 할 때는 @State
프로퍼티 래퍼를 사용해 인스턴스화 해서 단일 진실 소스를 생성할 수 있다.
데이터 모델을 인스턴화 할 때 해당 모델을 뷰가 소유하지 않아야 하면서 바인딩을 전달해야 할 땐 @Bindable
프로퍼티 래퍼를 사용해 인스턴스화를 해 바인딩을 쉽게 전달할 수 있다.
뷰 계층내에서 전역적으로 Observable
객체를 사용할 경우에는 기존의 Environment
를 사용할 수 있다.