안녕하세요.
이번에는 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() | |
} | |
} | |
} |
값이 변경되었다는 걸 상위 계층에서 알아야겠죠?
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() | |
} | |
} | |
} |
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 |