https://developer.apple.com/documentation/swiftui/managing-user-interface-state
Encapsulate view-specific data within your app’s view hierarchy to make your views reusable.
뷰가 재사용 가능한 상태가 될 수 있도록 앱의 뷰 계층구조 내에서 뷰와 관련 있는 데이터를 캡슐화합니다.
뷰 사이에서 공유되는 데이터 원천을 설정하기 위해 데이터가 필요한 뷰의 최소 부모 뷰에 상태로써 데이터를 저장할 수 있습니다. 스위프트 속성을 사용해 데이터를 리드온리로 제공할 수 있고, 혹은 바인딩을 사용해 양방향 연결이 있는 상태를 생성할 수도 있습니다. SwiftUI는 데이터의 변경을 감시하고, 필요한 경우 영향을 받는 모든 뷰를 업데이트합니다.
하나의 뷰에 저장된 상태를 보여주고 있는 다이어그램은 양방향 연결을 제공하는 바인딩을 통해 다른 뷰에 상태를 공유하기도 하고, 딘방향 연결을 제공하는 속성으로 또 다른 뷰와 공유되기도 합니다.
영구 저장소에 상태 속성을 사용하지 않아야 합니다. 이유는 상태 변수의 생명주기가 뷰의 생명주기에 미러링되어 있기 때문입니다. 대신 오직 UI만 영향을 미치는 일시적인 상태만을 다뤄야 합니다. 예를 들어 버튼, 필터 설정, 혹은 현재 선택된 리스트 아이템의 하이라이트 상태와 같은 것이 있습니다. 앱의 데이터 모델을 변경할 준비 전에 프로토타입을 하는 동안 저장소를 위한 편의성 있는 기능들을 발견할 수 있을 것입니다.
뷰에서 수정 가능한 데이터 저장이 필요한 경우 State
속성 래퍼를 사용해서 변수를 선언할 수 있습니다. 예를 들어 팟캐스트가 실행중일 때 트랙을 추적하기 위해서 isPlaying
이라는 Boolean
타입을 팟캐스트 플레이어 뷰 내부에 생성할 수 있습니다.
struct PlayerView: View {
@State private var isPlaying: Bool = false
var body: some View {
// ...
}
}
상태를 속성에 마킹하는 것은 프레임워크에게 기번 저장소 관리를 요청하는 것과 같습니다. 뷰는 속성의 이름을 사용해 상태의 wrappedValue
속성에서 찾을 수 있는 데이터를 읽고 씁니다. 값을 변경하면 SwiftUI는 뷰의 영향을 받는 부분들을 업데이트합니다. 예를 들어 버튼이 눌릴 때 저장된 값을 토글하는 버튼을 PlayerView
에 추가할 수 있으며, 저장된 값에 따라 다른 이미지를 표시하도록 할 수 있습니다.
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
private
접근제어를 선언해 상태 변수의 범위를 제한할 수 있습니다. 이렇게 함으로써 변수는 해당 변수를 선언하고 있는 뷰 계층구조 내에서 캡슐화된 상태로 남아있을 수 있게 됩니다.
뷰가 수정할 수 없는 데이터를 사용해 뷰를 제공하려면 표준 스위프트 속성을 선언하는 것이 좋습니다. 예를 들어 에피소드 타이틀과 이름을 나타내는 스트링을 포함한 입력 구조체를 갖게 하기 위해 팟캐스트 플레이어를 확장할 수 있습니다.
struct PlayerView: View {
let episode: Episode // The queued episode.
@State private var isPlaying: Bool = false
var body: some View {
VStack {
// Display information about the episode.
Text(episode.title)
Text(episode.showTitle)
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
}
}
}
PlayerView
에서 에피소드 속성의 값은 상수이지만, 이 뷰의 부모뷰에서 상수일 필요는 없습니다. 사용자가 부모에서 다른 에피소드를 선택하면 SwiftUI는 상태 변화를 감지하고 PlayerView
를 새 입력과 함께 재생성합니다.
자식 뷰에서 상태의 제어가 공유될 필요가 있는 경우 자식 뷰에서 Binding
속성 래퍼를 사용해 속성을 선언할 수 있습니다. 바인딩은 존재하는 저장소에 대한 참조를 나타내며, 기반 데이터의 단일 원천을 유지합니다. 예를 들어 팟캐스트 플레이어 뷰의 버튼이 PlayButton
이라 명명된 자식뷰에 있도록 하는 경우 isPlaying
속성을 바인딩으로 제공할 수 있습니다.
struct PlayButton: View {
@Binding var isPlaying: Bool
var body: some View {
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
}
}
위에서 보이는 것처럼 바인딩이 감싸는 값을 속성에서 직접 참조해서 읽고 쓸 수 있습니다. 이는 상태와 유사하지만 다른 점이 있습니다. 바인딩은 자체 저장소를 갖지 못합니다. 대신 어딘가에 저장된 상태 속성을 참조하고, 해당 저장소에 양방향 연결을 제공합니다.
PlayButton
을 인스턴스화할 때 $
접두사를 사용해서 부모뷰에 선언되나 상태 변수에 상응하도록 바인딩을 제공할 수 있습니다.
struct PlayerView: View {
var episode: Episode
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Text(episode.title)
Text(episode.showTitle)
PlayButton(isPlaying: $isPlaying) // Pass a binding.
}
}
}
$
접두사는 기반 저장소의 바인딩인 projectedValue
에 대한 감싸있던 속성을 요청합니다. 유사하게 $
접두사를 사용해서 바인딩으로부터 바인딩을 얻는 방식으로 임의의 여러 뷰 계층 구조 레벨에 바인딩을 전달할 수 있습니다.
상태 변수 내부에 영역이 제한된 값에 대한 바인딩을 가져올 수도 있습니다. 예를 들어 플레이어의 부모뷰에 에피소드라는 상태 변수를 선언하고, 에피소드 구조체가 isFavorite
이라는 Boolean
을 포함하고 있는 경우 에피소드의 즐겨찾기 상태에 대한 바인딩을 가져오려면 $episode.isFavorite
으로 참조할 수 있습니다.
struct Podcaster: View {
@State private var episode = Episode(title: "Some Episode",
showTitle: "Great Show",
isFavorite: false)
var body: some View {
VStack {
Toggle("Favorite", isOn: $episode.isFavorite) // Bind to the Boolean.
PlayerView(episode: episode)
}
}
}
뷰 상태가 변경될 때 SwiftUI는 영향을 받는 뷰들을 즉시 업데이트합니다. 부드러운 시각적 전환을 원한다면 withAnimation(_:_:)
함수에서 상태 변경이 애니메이션을 가지도록 할 수 있습니다. 예를 들어 isPlaying
이라는 Boolean
타입 속성을 애니메이션을 통해 제어할 수 있습니다.
withAnimation(.easeInOut(duration: 1)) {
self.isPlaying.toggle()
}
애니메이션 함수의 후행 클로저 내부에서 isPlaying
을 변경함으로써 값에 따라 SwiftUI가 어떤 것이든 애니메이션을 갖게 할 수 있으며, 버튼의 이미지에 스케일 효과를 주는 것이 예시가 될 수 있습니다.
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
.scaleEffect(isPlaying ? 1 : 1.5)
SwiftUI는 개발자가 구체화한 커브 및 듀레이션을 사용해 주어진 값 1에서 1.5 사이에서 시간에 걸쳐 스케일 효과 입력을 전환하며, 값이 주어지지 않는 경우 합리적인 기본값을 사용합니다. 반면에 이미지 컨텐트는 애니메이션에 영향을 받지 않으며, 시스템이 표시하는 이미지가 같은 Boolean
을 지정해도 마찬가지입니다. 이는 SwiftUI가 pause.circle
, play.circle
사이에서 의미있는 방식으로 점진적 전환을 할 수 없기 때문입니다.
상태 속성에 애니메이션을 추가할 수 있고, 위 예시처럼 바인딩에 추가할 수도 있습니다. 어떤 방식이더라도 SwiftUI는 저장된 값이 변경될 때 발생하는 모든 뷰의 변경을 애니메이션으로 나타낼 수 있습니다. 예를 들어 애니메이션 블록의 위치 위에 있는 뷰 계층구조 수준에 PlayerView
가 배경 색상을 갖도록 한다면 SwiftUI는 이 역시 애니메이션 처리할 수 있습니다.
VStack {
Text(episode.title)
Text(episode.showTitle)
PlayButton(isPlaying: $isPlaying)
}
.background(isPlaying ? Color.green : Color.red) // Transitions with animation.
상태 변경에 있어 발생하는 애니메이션이 모든 뷰에 걸쳐 애니메이션을 갖도록 하는 것이 아니라 특정 뷰만 애니메이션을 갖도록 하려면 animation(_:value:)
을 사용할 수 있습니다.