SwiftUI

PreferenceKey

Phililip 2022. 5. 30.
728x90

안녕하세요.

 

이번에는 SwiftUI의 PreferenceKey에 대해 알아볼게요.

 


## PreferenceKey

A view with multiple children automatically combines its values for a given preference into a single value visible to its ancestors.

 

 

PreferenceKey는 자식 뷰에서 상위 뷰 계층으로 데이터를 전달하고 싶을 때 사용합니다.

 

즉, 하위에서 값을 수정하고 이를 상위 계층에 알려줘야 할 때 사용되는 게 PreferenceKey입니다.

 

 

하위 -> 상위 계층으로 데이터를 전달하는 개념도 생각보다 간단해요.

1. PreferenceKey 프로토콜을 준수한 Key를 하나 만들고,
2. 상위 뷰에서는 값이 변할 때 동작하는 action을 등록해주고,
3. 하위 뷰에서 해당 key에 대한 값을 설정 또는 변경

 

 

직접 해볼까요?

 

뷰는 아래처럼 구현해줄게요.

 

struct ContentView: View {
@State private var text: String = "Hello world"
var body: some View {
VStack {
Text(text)
.frame(width: 150, height: 100)
.border(.gray, width: 5)
ChildView()
}
}
}
struct ChildView: View {
@State private var loading: Bool = true
var body: some View {
VStack {
Text("Fetching")
}
.frame(width: 150, height: 100)
.background(.yellow)
.overlay(loadingView)
}
@ViewBuilder private var loadingView: some View {
if loading {
ProgressView()
}
}
}

 

 

 

 

ChildView가 출력되고 2초 뒤에 상위 뷰(ContentView)의 text를 바꾸도록 구현해볼게요.

 

 

우선 PreferenceKey를 상속받은 커스텀 key를 하나 만들어줍니다.

 

struct CustomTitlePreferenceKey: PreferenceKey {
static var defaultValue: String = "Default Title"
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}

 

그다음에는 PreferenceKey에 값을 저장해볼게요.

 

ChildView 출력하고 2초 뒤에 newValue라는 @State 프로퍼티에 text를 저장하고, 이 값을 CustomTitlePreferenceKey에 저장해줍니다.

 

struct ChildView: View {
@State private var newValue: String = ""
@State private var loading: Bool = true
var body: some View {
VStack {
Text("Fetching")
.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
loading = false
newValue = "New title"
}
})
}
.frame(width: 150, height: 100)
.background(.yellow)
.overlay(loadingView)
.preference(key: CustomTitlePreferenceKey.self, value: newValue) // ✅
}
@ViewBuilder private var loadingView: some View {
if loading {
ProgressView()
}
}
}
view raw ChildView.swift hosted with ❤ by GitHub

 

 

값이 변경되었다는 걸 상위 계층에서 알아야겠죠?

 

ContentView에서 CustomTitlePreferenceKey 값이 변경되었을 때 이벤트를 받아서 text 값을 수정해줍니다.

 

struct ContentView: View {
@State private var text: String = "Hello world"
var body: some View {
VStack {
Text(text)
.frame(width: 150, height: 100)
.border(.gray, width: 5)
ChildView()
}
.onPreferenceChange(CustomTitlePreferenceKey.self) { // ✅
text = $0
}
}
}

 

전체 코드를 한번 볼게요.

 

struct ContentView: View {
@State private var text: String = "Hello world"
var body: some View {
VStack {
Text(text)
.frame(width: 150, height: 100)
.border(.gray, width: 5)
ChildView()
}
.onPreferenceChange(CustomTitlePreferenceKey.self) {
text = $0
}
}
}
struct ChildView: View {
@State private var newValue: String = ""
@State private var loading: Bool = true
var body: some View {
VStack {
Text("Fetching")
.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
loading = false
newValue = "New title"
}
})
}
.frame(width: 150, height: 100)
.background(.yellow)
.overlay(loadingView)
.preference(key: CustomTitlePreferenceKey.self, value: newValue)
}
@ViewBuilder private var loadingView: some View {
if loading {
ProgressView()
}
}
}
view raw ChildView.swift hosted with ❤ by GitHub

 

 

ChildView에서 값을 변경했을 때 ContentView에서 이벤트를 받아서 처리를 했죠? 

 

이렇게 하위 뷰에서 값을 변경하고 상위 계층에서 이벤트를 받아서 처리할 때 PreferenceKey를 사용합니다.

 

 

추가로, 상위 계층에서 하위 뷰의 크기를 알고 싶을 때 GeometryReader와 PreferenceKey를 활용해서 많이들 사용합니다.

 

요것도 한번 예시로 알아볼게요.

 

 

아래 같은 뷰가 있다고 해볼게요.

 

struct ContentView: View {
var body: some View {
VStack {
Text("Hello world")
.background(.blue)
Spacer()
HStack {
Rectangle()
Rectangle()
Rectangle()
}
.frame(height: 55)
}
.frame(height: 200)
}
}

 

 

그런데 만약 위에 있는 Text 뷰의 width와 HStack 안에 있는 Rectangle 뷰의 width를 같게 설정하고 싶다면 어떻게 해야 할까요?

 

우선 GeometryReader로 Rectangle의 width를 알 수 있겠죠?

 

struct ContentView: View {
var body: some View {
VStack {
Text("Hello world")
.background(.blue)
Spacer()
HStack {
Rectangle()
GeometryReader { proxy in // ✅
Rectangle()
.overlay(
Text("\(proxy.size.width)").foregroundColor(.white)
)
}
Rectangle()
}
.frame(height: 55)
}
.frame(height: 200)
}
}

 

 

 

132.6666 이란 값을 상위 계층에 전달하기 위해서 PreferenceKey를 사용해야 합니다.

 

PreferenceKey를 준수하는 GeometryPreferenceKey를 추가해줄게요.

 

struct GeometryPreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}

 

그다음 GeometryPreferenceKey에 값을 변경해줄게요.

 

struct ContentView: View {
var body: some View {
VStack {
Text("Hello world")
.background(.blue)
Spacer()
HStack {
Rectangle()
GeometryReader { proxy in
Rectangle()
.overlay(
Text("\(proxy.size.width)").foregroundColor(.white)
)
.preference(key: GeometryPreferenceKey.self, value: proxy.size) // ✅
}
Rectangle()
}
.frame(height: 55)
}
.frame(height: 200)
}
}

 

GeometryPreferenceKey에 값을 설정했으니, 이 값을 가지고 Text 뷰의 width를 변경해주면 되겠죠?

 

@State 프로퍼티를 하나 추가하고, onPreferenceChange method를 통해 GeometryPreferenceKey 값이 변경되었을 때의 action을 넣어줍시다.

 

struct ContentView: View {
@State private var size: CGSize = .zero // ✅
var body: some View {
VStack {
Text("Hello world")
.frame(width: size.width, height: size.height) // ✅
.background(.blue)
Spacer()
HStack {
Rectangle()
GeometryReader { proxy in
Rectangle()
.overlay(
Text("\(proxy.size.width)").foregroundColor(.white)
)
.preference(key: GeometryPreferenceKey.self, value: proxy.size)
}
Rectangle()
}
.frame(height: 55)
}
.frame(height: 200)
.onPreferenceChange(GeometryPreferenceKey.self) { ✅
size = $0
}
}
}

 

 

 

👍 👍 👍

 

 

## 참고

- https://developer.apple.com/documentation/swiftui/state-and-data-flow

- https://developer.apple.com/documentation/swiftui/preferencekey

- https://developer.apple.com/documentation/swiftui/form/preference(key:value:)

- https://developer.apple.com/documentation/swiftui/link/onpreferencechange(_:perform:)

- https://www.youtube.com/watch?v=OnbBc00lqWU&ab_channel=SwiftfulThinking

- https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/

 


이번 글은 여기서 마무리.

 

 

 

반응형

'SwiftUI' 카테고리의 다른 글

TimelineView  (0) 2022.06.17
trim  (0) 2022.06.06
Environment  (0) 2022.05.29
SwiftUI에서 ViewController를?  (0) 2022.05.11
복잡한 Navigation Flow 처리 (feat. Combine)  (0) 2022.05.10