Observable, State, Environment 객체
SwiftUI의 데이터 주도 방식의 앱 개발: 기본 데이터의 변경에 따른 처리 코드를 작성하지 않아도 뷰가 업데이트된다고 설명했다. 이것은 데이터와 사용자 인터페이스 내의 뷰 사이에 게시자(publisher)와 구독자(subscriber)를 구축하여 할 수 있다.
이를 위하여 SwiftUI는 상태 프로퍼티, Observable 객체, State 객체, 그리고 Environment 객체를 제공하며, 이들 모두는 사용자 인터페이스의 모양과 동작을 결정하는 상태를 제공한다. SwiftUI에서 사용자 인터페이스 레이아웃을 구성하는 뷰는 코드 내에서 직접 업데이트하지 않는다. 그 대신, 뷰와 바인딩된 상태 객체가 시간이 지남에 따라 변하면 그 상태에 따라 자동으로 뷰가 업데이트된다.
상태 프로퍼티
상태 프로퍼티는 상태에 대한 가장 기본적인 형태이며, 뷰 레이아웃의 현재 상태(예를 들어, 토글 버튼이 활성화되었는지 여부, 텍스트 필드에 입력된 텍스트, 또는 피커 뷰에서의 현재 선택)를 저장하기 위해서만 사용된다. 상태 프로퍼티는 String이나 Int 값처럼 간단한 데이터 타입을 저장하기 위해 사용되며, @State 프로퍼티 래퍼를 사용하여 선언된다.
struct ContentView: View { @State private var wifiEnabled = true @State private var userName = "" var body: some View } - 상태 값은 해당 뷰에 속한 것이기 때문에 private 프로퍼티로 선언되어야 한다.
상태 프로퍼티 값이 변경되었다는 건 그 프로퍼티가 선언된 뷰 계층 구조를 다시 렌더링해야 한다는 SwiftUI 신호다. 즉, 그 계층구조 안에 있는 모든 뷰를 빠르게 재생성하고 표시해야 한다. 결국 그 프로퍼티에 의존하는 모든 뷰는 어떤 식으로든 최신 값이 반영되도록 업데이트된다.
상태 프로퍼티를 선언했다면 레이아웃에 있는 뷰와 바인딩을 할 수 있다. 바인딩이 되어있는 뷰에서 어떤 변경이 일어나면 해당 상태 프로퍼티에 자동으로 반영된다. 예를 들어, 토글 뷰와 위에서 선언한 불리언 타입의 wifiEnabled 프로퍼티가 바인딩되어서 사용자가 토글 뷰를 조작하면 SwiftUI는 새로운 토글 설정값으로 상태 프로퍼티를 자동으로 업데이트 할 것이다.
상태 프로퍼티와의 바인딩은 프로퍼티 이름 앞에 $를 붙이면 된다. 다음의 예제에서 TextField 뷰는 사용자가 입력한 텍스트를 저장하는 데 사용하기 위해 userName이라는 상태 프로퍼티와 바인딩된다.
struct ContentView: View { @State private var wifiEnabled = true @State private var userName = "" var body: some View { VStack { TextField("Enter user name", text: $userName) } } } - 사용자가 TextField에 입력하게 되면 바인딩은 현재의 텍스트를 userName 프로퍼티에 저장할 것이다.
- 이 상태 프로퍼티에 변화가 생길 때마다 뷰 계층 구조는 SwiftUI에 의해 다시 렌더링된다.
상태 프로퍼티에 값을 저장하는 것은 단방향 프로세스다. 상태가 변하면 레이아웃에 있는 다른 뷰들도 변겨오딘다. 다음의 예제에서 Text 뷰는 입력된 사용자의 이름을 반영하기 위해 업데이트되어야 한다. 이 작업은 Text 뷰를 위한 콘텐트로 userName 상태 프로퍼티 값을 선언하면 된다.
var body: some View { VStack { TextField("Enter user name", text: $userName) Text(userName) } } - 사용자가 텍스트를 입력하면 Text 뷰는 사용자의 입력을 반영하기 위해 자동으로 업데이트될 것이다.
- 여기서 주목해야 할 점은 userName 프로퍼티가 $ 표시 없이 사용되었다는 것이다.
- 왜냐하면 이제는 상태 프로퍼티에 할당된 값, 즉 사용자에 의해 입력된 스트링을 참조하는 것이기 때문이다.
- 마찬가지로, 앞서 공부한 Toggle 뷰와 wifiEnabled 상태 프로퍼티 간의 바인딩은 다음과 같이 구현된다.
struct ContentView: View { @State private var wifiEnabled = true @State private var userName = "" var body: some View { VStack { Toggle(isOn: $wifiEnabled) { Text("Enable Wi-Fi") } TextField("Enter user name", text: $userName) Text(userName) Image(systemName: wifiEnabled ? "wifi" : "wifi.slash") } } } State 바인딩
상태 프로퍼티는 선언된 뷰와 그 하위 뷰에 대한 현재 값이다. 하지만, 어떤 뷰가 하나 이상의 하위 뷰를 가지고 있으며 동일한 상태 프로퍼티에 대해 접근해야 하는 경우가 발생한다. 예를 들어, 앞의 예제의 와이파이 이미지 뷰가 하위 뷰로 분리되는 상황을 살펴보자.
... VStack { Toggle(isOn: $wifiEnabled) { Text("Enable Wi-Fi") } TextField("Enter user name", text: $userName) WifiImageView() } struct WifiImageView: View { var body: some View { Image(systemName: wifiEnabled ? "wifi" : "wifi.slash") } } WifiImageView 하위 뷰는 여전히 wifiEnabled 상태 프로퍼티에 접근해야 한다. 하지만, 분리된 하위 뷰의 요소인 Image 뷰는 이제 메인 뷰의 범위 밖이다. 다시 말해, WifiImageView 입장에서 보면 이 뷰에 있는 wifiEnabled 프로퍼티는 정의되지 않은 변수인 것이다.
- 이 문제는 다음과 같이 @Binding 프로퍼티 래퍼를 이용하여 프로퍼티를 선언하면 해결된다.
struct WifiImageView: View { @Binding var wifiEnabled : Bool var body: some View { Image(systemName: wifiEnabled ? "wifi" : "wifi.slash") } } - 이제 하위 뷰가 호출될 때 상태 프로퍼티에 대한 바인딩을 전달하면 된다.
WifiImageView(wifiEnabled: $wifiEnabled)