[SwiftUI] 모델 데이터

Hashswim·2024년 8월 2일
0

헷갈리는 부분이 있어 다시보기위해 정리해보자..

1. @State

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 속성이 변경되더라도 업데이트 하지 않음, 참조 값 자체가 변경 될 때 업데이트 함)

2. Binding

데이터를 저장하는 속성과 데이터를 표시하고 변경하는 뷰 간에 양방향 연결 즉 읽기와 쓰기 모두 할 때 사용
외부에 저장된 단일 데이터 소스에 연결함.
즉 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과 달리)

3. @StateObject

값 타입이 아닌 참조타입의 상태 데이터를 사용할 때를 알아보자

ObserableObject

먼저 ObserableObject 프로토콜을 알아야 한다.

protocol ObservableObject : AnyObject

ObserableObsect 프로토콜을 채택하면 해당 객체가 변경되기 전 변경을 알리는 publisher로 간주 한다.

즉 해당 인스턴스를 subscriber가 구독해 변경을 처리할 수 있다.

변경을 처리하는 방법은 @Published 프로퍼티 래퍼로 선언한다.
선언된 값이 변경 된다면 willset의 시점에서 objectWillChange메서드가 실행되며
구독하는 쪽은 objectWillChange.sink를 사용해 변경에 대한 이벤트를 처리한다.

@Published를 사용하지 않고 직접 objectWillChnage.send()를 willset에서 사용해 알릴 수도 있다.

@Published

@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로 돌아오면
ObserableObjectView 외부에 있는 swiftUI의 저장소에 저장해 단일 진실 소스로 사용하기 위해서 사용한다.

해당 객체 내부의 @Published 인스턴스의 변경을 알리고 사용할 수 있는데
@StateObject는 딱 한번 생성된다(저장소와의 연결이 최초 한번 이루어지며 변경되지 않음)

View 내부에서 ObserableObject를 채택한 객체를 소유하고 데이터를 사용할 때 사용하고 View가 업데이트되어도 데이터가 남아있다.

@ObservedObject와 비교하면 이해가 쉽다

4. @ObservedObject

@StateObject와 같이 ObservableObject 프로토콜을 채택한 객체를 사용하는 View의 외부인 swiftUI의 특정 저장소에 저장하고 사용이 가능하다.

다만 차이점은View가 파괴되면 해당 인스턴스가 초기화 된다.

유명한 카운터 예제

View를 담고있는 컨테이너의 변경에도 데이터 일관성을 유지하고 싶다면 @StateObject를 사용하자.

5. @EnvironmentObject

struct EnvironmentObject<ObjectType> where ObjectType : ObservableObject 

마찬가지로 ObserableObject 프로토콜을 채택한 객체를 할당 할 때 사용하는데
선언한 View의 하위 계층 View에서도 사용할 수 있도록 해준다.

사용 범위의 최상단 부모 뷰에 .environmentObject(Object)를 선언해 컴파일 시 접근 주소를 알수 있도록 해야한다.

자식 뷰에서 해당 객체의 게시된 프로퍼티에 대해 변경을 할 경우 부모뷰에도 변경을 알린다.
-> 뷰 계층 구조간 공유 데이터 사용이 용이

@Obserable 매크로

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 프로토콜을 채택한 객체들은 StateObjectObservedObject가 아닌 다른 방식으로 사용한다.

1+. @State(with Obserable macro)

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의 프로퍼티에 대해 바인딩을 생성 할 수 있다.

6. @Bindable

@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)
    }
}

7. @Environment

뷰 계층 내에서 사용하는 데이터 모델을 공유하고 변경을 알릴 때 @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를 사용할 수 있다.

0개의 댓글