티스토리 뷰
Swift 5.9 부터 Observable macro 를 사용할 수 있습니다.
✓ WWDC 23 > Discover Observation in SwiftUI
✓ Managing model data in your app / Migrating from the Observable Object protocol to the Observable macro
✓ Observation
✓ 구현 코드
WWDC 만 보면 채워지지 않는 구멍이 정말 많습니다 .. 🙄
문서를 같이 보길 추천드립니다.
또한 Xcode 15 beta 5 에서 @Observable 을 사용하면 컴파일 에러가 납니다
저는 이거 보고 Xcode 앱 이름에 공백제거하고 컴파일 성공했음!
https://developer.apple.com/forums/thread/734638
[1] Observable 매크로
# Observable 매크로로 Observation 구축하기
Observation을 통해 SwiftUI는 observable data model 에 의존성을 형성하고 data 가 변할때마다 UI 를 업데이트 합니다.
하지만 SwiftUI 에서의 Observation 지원은 iOS 17 이상 부터... (하위 버전에서는 컴파일 에러 납니다)
Observable 매크로 를 data model에 적용하면 컴파일 타임에 data model 안에 observataion 코드가 자동으로 만들어집니다.
auto generated 된 코드를 보고 싶으면 매크로 우클릭 > Expand Macro 또는 Xcode > Editor > Expand Macro 를 해주면 되고
해당 코드에 브레이크포인트를 걸고 디버깅도 가능하다고 하네요
(freestanding 매크로에만 해당 메뉴가 활성화되는데 원래 그런건지 엑코 베타 버그 인지 모르겠음 @_@)
Observable 프로토콜을 직접 사용하지 말고 매크로를 쓰라고 가이드합니다.
# Observation 지원
Observation 은 reference type (아래 예제에서 Author) 와 value type을 모두 지원합니다.
@Observable final class Book {
var title = "Sample Book Title"
var author = Author()
var isAvailable = true
}
final class Author {
var name = "Sample Author"
}
위의 예제처럼 Author 에 @Observable 를 안붙이면 오직 author reference 가 변할때만 뷰가 업데이트 됩니다.
struct BookView: View {
var book: Book
var body: some View {
Text(book.author.name)
.onAppear {
let newAuthor = Author()
newAuthor.name = "New Author"
book.author = newAuthor // 뷰가 업데이트 됨!
}
}
}
struct BookView: View {
var book: Book
var body: some View {
Text(book.author.name)
.onAppear {
book.author.name = "New Author" // 뷰가 업데이트 안됨!
}
}
}
Author 가 들고 있는 프로퍼티들의 변화도 추적하고 싶으면 Author에 @Observable 를 같이 붙여줘야합니다.
@Observable final class Author {
var name = "Sample Author"
}
struct BookView: View {
var book: Book
var body: some View {
Text(book.author.name)
.onAppear {
book.author.name = "New Author" // 뷰가 업데이트 됨!
}
}
}
[2] Observable 매크로와 뷰 업데이트 메커니즘
# 뷰 업데이트 매커니즘
SwiftUI 에서 뷰를 업데이트 하는 메커니즘은 기존 @Published 를 썼을 때와 유사합니다.
(살짝 다른 점이 있는데 3번에서 다루겠음)
뷰의 body가 observable data model object 의 특정 프로퍼티를 읽을 때만 뷰는 의존성을 트래킹 합니다.
예를 들어 아래 뷰는 book의 title 변화에만 업데이트 되고 author 나 isAvailable이 바뀔 때는 업데이트 되지 않습니다.
이로 인해 불필요한 뷰 업데이트를 피할 수 있습니다.
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
SwiftUI 는 global property 나 singleton 을 사용할때도 동일한 dependency tracking 을 형성합니다.
var globalBook: Book = Book()
struct BookView: View {
var body: some View {
Text(globalBook.title)
}
}
computed property 에도 트래킹이 지원됩니다.
@Observable class Library {
var books: [Book] = [Book(), Book(), Book()]
var availableBooksCount: Int {
books.filter(\.isAvailable).count
}
}
struct LibraryView: View {
@Environment(Library.self) private var library
var body: some View {
NavigationStack {
List(library.books) { book in
// ...
}
.navigationTitle("Books available: \(library.availableBooksCount)")
}
}
}
또한 컬렉션 자체에 대한 변경사항도 추적가능합니다. books 컬렉션이 변할때 SwiftUI는 뷰를 업데이트 합니다.
struct LibraryView: View {
@State private var books = [Book(), Book(), Book()]
var body: some View {
List(books) { book in
Text(book.title)
}
}
}
그러나 LibraryView 은 book의 title 프로퍼티에 대해 dependency를 형성하지 않습니다.
왜냐하면 뷰의 body가 직접적으로 title을 읽고 있지 않기 때문입니다.
List의 content closure 는 @escaping closure 로
SwiftUI는 화면에 아이템들이 나오기 직전 아이템들을 lazy 하게 만들면서 이 클로저를 호출합니다.
즉 LibraryView 가 아니라 각각의 Text 아이템이 book의 title 프로퍼티에 대해 의존성을 가지고 있는 것입니다.
그래서 title 관련 변화는 오직 각각의 Text 만 업데이트 시킵니다.
또한 다른 뷰와 observable model data object 를 share 할 수 있습니다.
아래의 예제에서 LibraryView 는 book 인스턴스를 BookView 와 공유하고 있습니다.
book의 title이 변하면 SwiftUI는 오직 BookView만 업데이트 합니다 (LibaryView는 업데이트 하지 X)
왜냐면 BookView만 title 프로퍼티를 읽고 있기 때문입니다.
struct LibraryView: View {
@State private var books = [Book(), Book(), Book()]
var body: some View {
List(books) { book in
BookView(book: book)
}
}
}
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
위에서 쭉 확인한 것처럼 뷰가 의존성을 가지고 있지 않다면, SwiftUI는 data가 변할 때 뷰를 업데이트 시키지 않습니다.
이 접근 덕분에 view hierarchy 가 여러 겹 있어도 중간 layer가 의존성을 안가져도 됩니다.
// Will not update when any property of `book` changes.
struct LibraryView: View {
@State private var books = [Book(), Book(), Book()]
var body: some View {
LibraryItemView(book: book)
}
}
// Will not update when any property of `book` changes.
struct LibraryItemView: View {
var book: Book
var body: some View {
BookView(book: book)
}
}
// Will update when `book.title` changes.
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
또한 뷰가 들고 있는 observable data model 에 대한 reference 가 변경 되어도 뷰는 업데이트 됩니다.
stored reference 는 뷰의 일부이기 때문입니다.
아래의 예제에서 book reference 가 변하면 SwiftUI 는 뷰를 업데이트 합니다.
struct BookView: View {
var book: Book
var body: some View {
// ...
}
}
# ObservationIgnored() 매크로
위에서 쭉 본 것처럼 body 에서 직접 read 를 하지 않으면 해당 데이터의 변경은 뷰 업데이트에 영향을 주지 않습니다.
그럼에도 불구하고 커스텀하고 싶다면 ObservationIgnored() 매크로를 사용할 수 있습니다.
body에서 직접 해당 프로퍼티를 읽어도 뷰 업데이트가 되지 않습니다.
@Observable final class Book {
@ObservationIgnored() var title = "Sample Book Title"
}
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
.onAppear {
book.title = "~~!~~" // 뷰 업데이트 안됨
}
}
}
언제 사용되면 좋을 지 감이 잘 안오네요.. 🤔
[3] 기존 ObservableObject 와 달라지는 것들
첫번째. 코드가 간결해짐
@Observable 매크로를 쓰면
✓ 모델
- ObservableObject 채택 안해도 됨
- @Published Property Wrapper안써도 됨
✓ 뷰
- @ObservedObject Property Wrapper 안써도 됨
AS IS
final class Book: ObservableObject {
@Published var title = "Sample Book Title"
var isAvailable = true
}
struct BookView: View {
@ObservedObject var book: Book
var body: some View {
Text(book.title)
}
}
TO BE
@Observable final class Book {
var title = "Sample Book Title"
var isAvailable = true
}
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
두번째. 뷰 업데이트 메커니즘이 좀 더 효율적으로 바뀜
@Observable 매크로를 쓰면
뷰의 body 에서 프로퍼티를 직접 읽어야지만 뷰 업데이트가 된다.
AS IS
ObservableObject 에서는 published property 가 변하면
뷰가 해당 프로퍼티를 읽고 있지 않아도 다시 그려진다.
대충 이렇게 코드를 짜서 확인을 해보면
뷰에서 isAvailable 을 읽고 있지 않아도 published property 이기 때문에
0.1초 마다 이 값이 변할 때 뷰가 다시 그려진다.
final class Book: ObservableObject {
@Published var title = "Sample Book Title"
@Published var isAvailable = true
var cancellable: Cancellable?
func connectTimer() {
cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { [weak self] _ in
self?.isAvailable.toggle()
})
}
}
struct BookView: View {
@ObservedObject var book: Book
var body: some View {
Text(book.title)
.background(.random)
.onAppear {
book.connectTimer()
}
}
}
TO BE
반면 @Observable 매크로 를 쓴 경우는
isAvailable 을 뷰가 읽고 있지 않기 때문에 뷰 업데이트가 없다.
@Observable final class Book {
var title = "Sample Book Title"
var isAvailable = true
var cancellable: Cancellable?
func connectTimer() {
cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { [weak self] _ in
self?.isAvailable.toggle()
})
}
}
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
.background(.random)
.onAppear {
book.connectTimer()
}
}
}
세번째. data model object optional 로 들고 있을 수 있게 됨
AS IS
final class Book: ObservableObject {
...
}
struct BookView: View {
@ObservedObject var book: Book? // 컴파일 에러남
var body: some View {
...
}
}
TO BE
@Observable final class Book {
...
}
struct BookView: View {
var book: Book? // 가능
var body: some View {
...
}
}
네번째. data model objects collection 도 가능
AS IS
final class Book: ObservableObject {
...
}
struct LibraryView: View {
@ObservedObject var books = [Book(), Book(), Book()] // 컴파일 에러남
var body: some View {
...
}
}
TO BE
@Observable final class Book {
...
}
struct LibraryView: View {
var books = [Book(), Book(), Book()] // 가능
var body: some View {
...
}
}
'🍏 > SwiftUI + Combine' 카테고리의 다른 글
[SwiftUI] Drawing Curved Path with Animation (2) | 2023.10.29 |
---|---|
[SwiftUI] @Observable 매크로 (2) (0) | 2023.07.31 |
[SwiftUI] GridItem - adaptive vs flexible (0) | 2023.06.09 |
[SwiftUI] task modifier (0) | 2023.03.08 |
[Combine] 네트워킹 (0) | 2023.01.13 |
- Total
- Today
- Yesterday
- Django Heroku Scheduler
- 플러터 싱글톤
- 장고 Custom Management Command
- Flutter Spacer
- Watch App for iOS App vs Watch App
- cocoapod
- flutter 앱 출시
- METAL
- flutter deep link
- Dart Factory
- Django FCM
- 구글 Geocoding API
- drf custom error
- Sketch 누끼
- ipad multitasking
- Flutter Clipboard
- flutter dynamic link
- 장고 URL querystring
- flutter build mode
- DRF APIException
- SerializerMethodField
- Python Type Hint
- PencilKit
- 플러터 얼럿
- ribs
- Flutter 로딩
- Flutter Text Gradient
- Flutter getter setter
- github actions
- Django Firebase Cloud Messaging
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |