# 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
ObservableObject
는 뷰에서 인스턴스 값의 변화를 감시 가능하게 하는 프로토콜이다. 클래스에서 해당 프로토콜을 채택하여 구현하고, 주로 뷰모델 구현, 공유데이터 구현시 사용된다.ObservedObject
는 프로퍼티 래퍼이고ObservableObject
를 뷰에서 감시하다가 값이 업데이트 되는 경우 UI를 변경해준다.Published
는ObservableObject
내에서 속성 정의 시 사용되는 프로퍼티 래퍼이다. 해당 프로퍼티 래퍼로 선언된 속성이 뷰 업데이트를 위한 감시 대상이 된다.
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
의 경우 객체 소유 권한이 외부 뷰에게 있다는 것을 알리기 위한 목적이다. 단순 값의 변화 감시만 이루어지기 때문에 객체 생성과 소멸에 대한 책임을 모두 외부에 위임하기에, 상위 뷰 상태값 변화에 따른 리로드가 이루어지면 객체도 새로 초기화된다.