티스토리 뷰

반응형

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

 

Xcode 15 beta 5 External macro imp… | Apple Developer Forums

This seems to be caused by having a space in the name of the Xcode app (e.g. to delineate betas, I had renamed the app "Xcode 15 Beta 5"). Removing the spaces solved this issue for me.

developer.apple.com

 

 


 

[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
댓글