본문 바로가기
🍎 iOS/문서읽기

[SwiftUI] @State, @Binding

by @Eddy 2024. 1. 7.
728x90

오랜만에 SwiftUI로 프로젝트를 진행하게 되면서 간단하게 개념을 정리해둘 기회가 생겼다.

State와 Binding을 알아보기에 앞서, 선행되어야 할 개념인 Source of Truth와 Property wrapper에 대해 짚고 넘어가자.

 

Source of Truth

개요

Source of Truth는 데이터 원본이라 생각하면 되는데, State로 선언된 Property는 모두 Source of Truth를 생성하고 참조할 수 있게 된다. 

 

Source of Truth와 State, Binding의 흐름

SwiftUI에서는 property의 Storage를 관리하고, 데이터가 변경되면 이에 의존하는 View 계층의 일부를 업데이트한다.

property를 SoT 개념을 활용해 한 곳에서 통합 관리함을써, 데이터의 일관성과 정확성 유지가 유리해진 것이다.

당연하게도 데이터의 일관성과 정확성을 유지하기 위해서는 단일 데이터(Single Source of Truth, SSoT)로써 존재해야 하고,

이를 복사하면 혼란을 초래할 수 있다. 

또한 최상위 뷰(parent view)에서 하위 뷰(subview)로 State값을 Binding하여 하나의 Data를 여러 View에 전달해서 사용함으로써 여러 Data를 관리하지 않아 Side-effect를 예방할 수 있게 되었다.


Property Wrapper

@State 이전에 알아야 할 선행 개념으로 Property Wrapper도 있다.

Property Wrapper는 data flow를 컨트롤 하기 위한 역할로, View와 의존 관계를 가진 데이터를 표시하기 위해 사용한다.

property wrapper로 감싼 데이터가 변경되면 view가 업데이트 된다.

@State, @Binding, @StateObject, @ObservedObject, ... 등이 있다.


@State

SwiftUI에 의해 관리되는 읽고 쓸 수 있는 Property wrapper 타입

SwiftUI에 의해 관리되는 값

앞서 `SwiftUI에서는 property의 Storage를 관리한다.`는 말이 모호하게 다가왔을 수 있는데, @State로 선언된 데이터는 메모리의 어느 공간에 저장해둔 데이터 원본이 있는 것이고, @State를 이용해 해당 데이터를 가리키는 포인터 역할을 한다고 이해하면 된다.

즉, @State값은 진짜 값이 아니라 포인터라고 볼 수 있다.

 

 

State 값 관리

- @State로 선언된 property는 property의 storage와 struct의 Memberwise initializer와 충돌할 수 있어 private 사용이 권장된다.

- @State로 선언된 값은 subview에 binding해서 상태를 공유할 수 있고, subview에서는 값을 수정하기 위해 @Binding 키워드를 사용해야 한다.

// MARK: - 상위 뷰

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
            // ...
        }
    }
}

// MARK: - 하위 뷰

struct PlayButton: View {
    @Binding var isPlaying: Bool // Play button now receives a binding.


    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            isPlaying.toggle()
        }
    }
}

 

근데 View는 Struct인데 어떻게 업데이트 하는 걸까?

여기까지 공부하니 View도 Struct이고, State도 Struct이면 immutable한데, 어떻게 업데이트하는지가 가장 궁금했다. 나만 그랬어?

이 또한 @State에 해답이 있다.

 

앞서 @State는 데이터 원본을 가리키는 포인터의 역할을 한다고 했다.

이 때 SwiftUI에 의해 관리되는 property storage에 저장된 실제 값을 wrappedValue라고 하며,

wrappedValue를 @State로 참조해 사용함으로써, property가 변하는 것처럼 보이는 것이다.

 

@State 사용 간 주의사항

@Observable
class Library {
    var name = "My library of books"
    // ...
}


struct ContentView: View {
    @State private var library = Library()


    var body: some View {
        LibraryView(library: library)
    }
}

 

State property는 SwiftUI가 view를 인스턴스화할 때 항상 기본 값을 인스턴스화하기 때문에, State property의 기본값을 초기화할 때, side-effect와 performance-intensive(성능 집약, 무거운) 작업은 지양해야 한다.

이럴 때, view가 처음 표시될 때 한 번만 호출되는 task(priority: ) modifier를 사용해 객체 생성을 지연시킬 수 있다.

네트워크 호출, 파일 엑세스 등의 view 초기 상태를 만드는 데 필요한 작업을 지연시킬 때도 유용함.

struct ContentView: View {
    @State private var library: Library?


    var body: some View {
        LibraryView(library: library)
            .task {
                library = Library()
            }
    }
}

 

Observable Object 객체의 생성을 지연시킴으로써, SwiftUI가 view를 초기화할 때마다 불필요한 객체 할당을 하지 않아도 된다.

State property는 ObservableObject 프로토콜을 준수하는 객체를 저장할 수 있다. 하지만 view는 객체에 대한 참조값이 변경될 때만 업데이트된다.

즉, 객체의 published property가 변경되는 경우는, view가 업데이트 되지 않는다.

참조 및 객체의 published property 모두에 대한 변경 사항을 추적하기 위해서는, 객체를 저장할 때 State 대신 StateObject를 사용해야 한다.

 


@Binding

앞서 데이터의 통합 관리를 위해 @State로 단일 데이터 원본(SSoT)를 생성한다고 했다. 그래서 새로운 State를 생성하지 않고 SoT를 참조해 뷰 간의 명확한 의존관계를 성립하도록 하기 위해 @Binding을 사용한다.

 

Binding은 데이터를 저장하는 property와 데이터를 표시하고 변경하는 view 간의 양방향 연결(2-way connection)을 만든다. 데이터를 직접 저장하지 않고 참조하여 데이터의 일관성과 정확성을 지킬 수있다.

 

Parent view는 State property wrapper를 사용해 이 프로퍼티 값이 value’s source of truth라는 것을 나타내기 위한 property를 선언해야 한다. 또한 Parent View에서 State값을 전달할 때, prefix에 $기호를 붙여 projectedValue를 반환하도록 한다.

struct PlayerView: View {
    var episode: Episode
    @State private var isPlaying: Bool = false


    var body: some View {
        VStack {
            Text(episode.title)
                .foregroundStyle(isPlaying ? .primary : .secondary)
            PlayButton(isPlaying: $isPlaying) // Pass a binding.
        }
    }
}
반응형

댓글