# State

SwiftUI는 속성 동기화를 자동으로 처리한다. UIKit 기반에서는 인스턴스에 직접 접근하여 속성을 변경하고 UI 동기화까지 처리해야 하는데, SwiftUI는 그럴 필요가 없다.

struct OneWayBinding: View {
    @State private var text: String = "Hello, SwiftUI"

    var body: some View {
       VStack(spacing: 70) {
          Text(text)
             .font(.largeTitle)

           Button {
               text = "TEST"
           } label: {
               Text("Update")
           }
           .padding()
       }
    }
}

위 코드의 text속성을 @State프로퍼티 래퍼로 선언하지 않으면 immutable 기반으로 동작하는 구조체 특성상 self 속성 업데이트가 불가능하다는 컴파일 에러가 발생한다.

@State 키워드로 선언된 속성은 구조체에서 분리되어 메모리상에서 관리된다. 또한 외부에서 해당 속성을 업데이트 할때는 @State로 선언된 속성을 사용하는 것이 아니라 @Binding을 사용해야 한다.

# Binding

Binding은 자식 뷰에 바인딩 속성을 전달할때 사용한다. 자식 뷰 기준으로 @Binding 프로퍼티 래퍼로 속성을 선언하며, 외부에서 해당 속성에 접근하여 바인딩을 해야하므로 private으로 선언하면 안된다.

struct SubView: View {
    @Binding var value: String

    var body: some View {
        VStack {
            TextField("Value", text: $value)
        }
    }
}

#Preview {
    SubView(value: .constant(""))
}

서브뷰 기준으로 프리뷰에서는 화면 표시를 해야하기 때문에 정적 바인딩을 전달하면 된다. 이후 상위 뷰에서는 아래와 같이 @State 속성을 전달한다.

@State private var value: String = "Hello"

var body: some View {
    VStack(spacing: 70) {
        Text(value)
            .font(.largeTitle)

        SubView(value: $value)
            .padding()
    }
}

# ObservableObject, @ObservedObject, @Published

  1. ObservableObject는 뷰에서 인스턴스 값의 변화를 감시 가능하게 하는 프로토콜이다. 클래스에서 해당 프로토콜을 채택하여 구현하고, 주로 뷰모델 구현, 공유데이터 구현시 사용된다.
  2. ObservedObject는 프로퍼티 래퍼이고 ObservableObject를 뷰에서 감시하다가 값이 업데이트 되는 경우 UI를 변경해준다.
  3. PublishedObservableObject내에서 속성 정의 시 사용되는 프로퍼티 래퍼이다. 해당 프로퍼티 래퍼로 선언된 속성이 뷰 업데이트를 위한 감시 대상이 된다.
class ViewModel: ObservableObject {
    var title = "Hello"
    @Published var list = [String]()
}

클래스에 ObservableObject를 채택하고 감시 대상 속성을 @Published로 선언한다.

@ObservedObject var viewModel = ViewModel()

//...
List(viewModel.list, id:\.self) { item in
    Text(item)
}

리스트에 속성을 연결하여 새로운 배열 방출과 동시에 UI에 동기화가 이루어진다. 이때 클래스 인스턴스는 @ObservedObject 프로퍼티 래퍼로 선언해야 한다.

# EnvironmentObject

EnvironmentObject를 사용하면 뷰 계층이 복잡한 경우에 공유데이터를 전달하기 쉬워진다. 이는 시스템 공유데이터와 커스텀 공유데이터로 쪼개진다. 시스템 공유데이터는 @Environment로, 커스텀 공유데이터는 @EnvironmentObject로 선언한다.

@Environment(\.colorScheme) var currentColorScheme

시스템 공유 데이터는 키패스로 접근한다. 시스템 공유 데이터 역시 언제든 바뀔 수 있기 때문에 var로 선언해야 한다.

커스텀 공유데이터의 경우 @EnvironmentObject로 선언한다. @Binding과 유사하게 외부에서 주입되는 형태로 동작하기 때문에 타입만 선언하고 기본값은 설정 불가능하다. 공유데이터를 주입할 상위 뷰 계층에 .environmentObject(ViewModel()) 모디파이어를 추가하여 객체를 전달한다.

@EnvironmentObject var viewModel: ViewModel

@EnvironmentObject로 전달하는 클래스 역시 ObservableObject를 채택해야 하고, 내부 속성에 @Published속성이 있어야 의미가 있다.

@main
struct StatesApp: App {
    var body: some Scene {
        WindowGroup {
            MainList()
                .environmentObject(ViewModel())
        }
    }
}

# StateObject

ObservedObject와 같이 객체를 보유하는 뷰에서 다른 상태값에 의해 뷰 초기화가 여러번 이루어지는 경우, 클래스 초기화도 동시에 여러번 이루어지게 된다.

클래스 초기화가 여러번 이루어져도 괜찮은 경우라면 상관없지만 기존 데이터를 유지해야 하는 경우 StateObject 프로퍼티 래퍼로 선언해야한다.

struct NumberView: View {
    @ObservedObject var generator = RandomNumberGenerator()

    var body: some View {
        HStack {
            Text("\(generator.number)")
                .font(.largeTitle)
        }
    }
}

난수를 생성해주는 객체를 ObservedObject로 선언해둔 뒤 NumberView를 상위 뷰에서 추가한다.

@State private var value = ""

var body: some View {
    VStack {
        NumberView()
            .frame(width: 200, height: 200)
            .background(color)
            .clipShape(Circle())

        TextField("Input", text: $value)
    }
}

위와 같이 value상태값에 따라 NumberView를 호출하는 상위 뷰가 새로 초기화되는데, 이렇게 되면 NumberView초기화가 다시 이루어지고 내부속성 중 generator 역시 새로 초기화되어 기존 생성된 난수의 데이터를 잃어버린다.

이를 유지하기 위해서는 generator 속성을 @StateObject로 선언해야 한다. @StateObject로 선언된 객체는 객체 소유권한이 해당 뷰에게 직접 속하기 때문에 상위 뷰 리로드에도 초기화되지 않고 특정 메모리 공간에 유지된다.

반면 @ObservedObject의 경우 객체 소유 권한이 외부 뷰에게 있다는 것을 알리기 위한 목적이다. 단순 값의 변화 감시만 이루어지기 때문에 객체 생성과 소멸에 대한 책임을 모두 외부에 위임하기에, 상위 뷰 상태값 변화에 따른 리로드가 이루어지면 객체도 새로 초기화된다.