티스토리 뷰

반응형

[SwiftUI] @Observable 매크로 (1) 에서 이어집니다. 

 

[ 요약 ]

 

# 1. 

예전에 뷰에서 썼던 프로퍼티 래퍼
@State, @Binding, @ObservedObject, @StateObject, @EnvironmentObject, @Enviroment 

iOS 17+ 부터는 4개만 쓰면 됨 
@State, @Binding,  @Bindable (NEW),  @Enviroment

 

# 2. 

ObservableObject 가 Observale 매크로로 대체되고 

뷰에서 프로퍼티 래퍼로 쓰던 것은 다음과 같이 매핑됨 

 

✓ @ObservedObject ----> 안써도 됨 

ㄴ 하지만 텍스트 필드처럼 Binding을 넘겨줘야하는 경우는 @Bindable 

 

@StateObject   ---->  @State

@EnvironmentObject  ---->  @Environment

 

 


[1] @Bindable 

 

Bindable은 이번에 새로나온 프로퍼티 래퍼입니다!! (iOS 17+)

 

mutable property 가 변화하기 전에 view가 binding을 기대하는 경우가 있습니다. 

예를들어 TextField 같은 케이스 입니다. (초기화할때 Binding 타입을 넘겨줘야함)

 

아래의 코드에서는 컴파일 에러가 납니다. 

@Observable final class Book {
    var title = "Sample Book Title"
}

struct BookEditView: View {
    var book: Book
    
    var body: some View {
        /*
         컴파일 에러 발생!
         Cannot convert value of type 'String' to expected argument type 'Binding<String>'
        */
        TextField("Title", text: book.title)
            .textFieldStyle(.roundedBorder)
    }
}

 

model data 를 Bindable property wrapper 로 래핑한 후,  binding 전달이 필요한 프로퍼티에

$ syntax 를 붙여주면 됩니다. 

@Observable final class Book {
    var title = "Sample Book Title"
}

struct BookEditView: View {
    @Bindable var book: Book
    
    var body: some View {
        TextField("Title", text: $book.title)
            .textFieldStyle(.roundedBorder)
    }
}

 

또한 아래의 예제처럼 @Bindable variable 을 만들 수도 있습니다. 

local, global variable 모두 가능합니다. 

struct LibraryView: View {
    @State private var books = [Book(), Book(), Book()]


    var body: some View {
        List(books) { book in 
            @Bindable var book = book
            TextField("Title", text: $book.title)
        }
    }
}

 

 

[2] Observable 매크로  +  State  Property Wrapper 

model data 에 대해 source of truth 를 만들려면

observable data model 을 private 으로 선언 후, State property wrapper 로 래핑합니다. 

 

이렇게 하면  SwiftUI는 model instance 저장을 관리해줍니다.

SwiftUI가 BookView를 re-create 할때마다 book 을 연결해줍니다. (뷰에 모델 데이터에 대한 single source of truth 를 제공하는 셈) 

struct BookView: View {
    @State private var book = Book()
    
    var body: some View {
        Text(book.title)
    }
}

 

위의 내용은 

Observable 매크로 + State =  @StateObject  라고 이해하면 됩니다~ 

 

 

또한 App 이나 Scene 같은 더 top-level 에서도 동일하게 사용할 수 있습니다. 

@main
struct BookReaderApp: App {
    @State private var library = Library()
    
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library)
        }
    }
}

 

 

[3] Observable 매크로 +  Enviroment  Property Wrapper 

view hierarchy 를 거쳐 모델을 share 하고 싶은 경우가 있습니다. 

이때 view hierarchy 가 깊으면 전달 하기 힘드니까 view's environment 에 등록해두고 사용합니다.

 

기존에는  environmentObject  modifier 를 사용했습니다.  (ObservableObject, @EnvironmentObject 와 함께)

이제는 environment(_:_:) 또는 environment(_:) modifier 를 사용할 수 있습니다.  (Observable 매크로, @Environment 와 함께) 

 

 

 

3.1  key path를 이용한 방법

 

environment(_:_:)  modifier 를 사용하기 전에

custom EnvironmentKey  를 만들어주고 EnvironmentValues  를 확장해줘야합니다. 

 

예를들어 library 에 대한 코드 입니다. 

extension EnvironmentValues {
    var library: Library {
        get { self[LibraryKey.self] }
        set { self[LibraryKey.self] = newValue }
    }
}


private struct LibraryKey: EnvironmentKey {
    static var defaultValue: Library = Library()
}

 

그리고 LibaryView에 Library instance 에 대한  source of truth 를 추가해줍니다.  

@main
struct BookReaderApp: App {
    @State private var library = Library()
    
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(\.library, library)
        }
    }
}

 

library instance 를 서브뷰에서 접근하려면

각 뷰는 local variable 을 선언해줘야합니다. Environment property wrapper 로 감싸고 key path 를 전달합니다. 

struct LibraryView: View {
    @Environment(\.library) private var library


    var body: some View {
        // ...
    }
}

 

 

# 3.2  data type을 이용한 방법

 

또 다른 방법으로 

environment(_:) modifier 를 사용해서 model data 를 직접적으로 enviroment 에 저장하는 방법이 있습니다. 
(3.1 처럼 custom environment value 를 별도로 정의하지 않아도 됩니다.)

@main
struct BookReaderApp: App {
    @State private var library = Library()
    
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library)
        }
    }
}

 

서브뷰에서 접근을 하려면 동일하게 local variable 을 선언해주고 Environment property wrapper 로 감싸줍니다.

KeyPath 대신 data type 을 전달합니다. 

struct LibraryView: View {
    @Environment(Library.self) private var library
    
    var body: some View {
        // ...
    }
}

 

# 3.3  옵셔널

 

environment 에서 object를 읽으면 non-optional object 를 리턴하는 것이 디폴트입니다. 

이는 현재 뷰 hierarchy 에서 non-optional instance 가 저장되어있다는 전제에 의합니다. 

만약 가져오려는 object가 environment 에 없다면, SwiftUI 는 exception을 던집니다.

 

object가 environment에 있다는 보장이 없으면 optional 버전으로 불러올 수 있습니다. 

그러면 SwiftUI는 exception 대신 nil을 리턴해줍니다. 

@Environment(Library.self) private var library: Library?

 

 

반응형
댓글