PreferenceKey는 SwiftUI에서 뷰 간에 값을 전달하거나 공유할 때 사용하는 프로토콜
PreferenceKey를 사용하는 이유
PreferenceKey를 사용하는 이유는 이 프로토콜을 사용하면 하위 뷰에서 상위 뷰로 데이터를 전달하거나 뷰 간의 상태를 편리하게 관리할 수 있기 때문입니다.
PreferenceKey 사용하는법
PreferenceKey를 사용하기 위해서는 defaultValue와 reduce 메서드를 구현해야 합니다.
defaultValue는 저장하고 싶은 값을 의미하며 타입을 지정해줘야합니다.
reduce 메서드는 값을 저장하고 반환하는데 사용됩니다.
struct MyPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
PrefereneceKey 사용할 수 있는 상황
PreferenceKey 사용하기 전
다음과 같이 특정 컨텐츠의 크기를 가지고 모든 뷰에서 해당 크기를 사용해 frame을 설정하고 싶을 경우가 있습니다. 하지만 PreferenceKey를 알기 전까지 그렇기 위해서는 GeometryReader를 사용하여 다음과 같이 설정하는 방법을 생각할 수 있습니다.
struct GeometryPreferenceKeyView: View {
@State private var RectangleHeight: CGFloat = 0.0
@State private var RectangleWidth: CGFloat = 0.0
var body: some View {
VStack {
Text("Hello")
.background(.blue)
.frame(maxWidth: .infinity)
Spacer()
HStack {
GeometryReader(content: { geometry in
Rectangle()
.onAppear(perform: {
RectangleHeight = geometry.size.height
RectangleWidth = geometry.size.width
})
})
Rectangle()
Rectangle()
}
.frame(height: 55)
}
}
}
PreferenceKey 사용 후
다음으로 PreferenceKey를 정의해줍니다.
defaultValue는 PreferenceKey의 기본값을 설정합니다.
reduce 메서드에서 value는 inout String 타입을 의미하며 String 타입의 인자값을 받으면서 String 타입을 반환한다는 의미입니다.
그리고 reduce 메서드는 현재 값을 업데이트하는 것을 의미합니다.
struct RectangleGeometrySizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
다음은 사용하기 쉽게 View 확장자를 통해 새로운 메서드를 정의해 새로운 PreferenceKey값을 업데이트할 수 있도록 만들어줍니다.
extension View {
func updateRectangleGeoSize(_ size: CGSize) -> some View {
preference(key: RectangleGeometrySizePreferenceKey.self, value: size)
}
}
다음은 updateRectangleGeoSize 메서드를 통해 PreferenceKey값을 업데이트하고 onPreferenceChange를 사용하여 키값이 변화했을 때 프로퍼티에 값을 변경해 뷰에 적용하도록 하는 코드를 작성하였습니다.
struct GeometryPreferenceKeyView: View {
@State private var RectangleSize: CGSize = .zero
var body: some View {
VStack {
Text("Hello")
.frame(width: RectangleSize.width, height: RectangleSize.height)
.background(.blue)
Spacer()
HStack {
GeometryReader(content: { geometry in
Rectangle()
.updateRectangleGeoSize(geometry.size)
})
Rectangle()
Rectangle()
}
.frame(height: 55)
}
.onPreferenceChange(RectangleGeometrySizePreferenceKey.self, perform: { value in
self.RectangleSize = value
})
}
}
위에 코드를 통해 알 수 있는 점
PreferenceKey를 사용하기 전에 코드에서는 해당 뷰를 통해서만 또는 @Binding을 통해 하위 뷰 간의 데이터를 주고받을 수 있지만 PreferenceKey를 사용하게 된다면 바인딩을 통한 데이터 교환 없이 PreferenceKey으로 모든 뷰에서 쉽게 접근 가능하도록 만들 수 있습니다.
ScrollView에서 사용하는 PreferenceKey
다른 예시를 들어보겠습니다. ScrollView에서 화면을 내리게 되면 NavigationTitle이 왼쪽에서 오른쪽처럼 변하게 되는 것을 확인할 수 있습니다. 이것은 뷰가 얼만큼 스크린을 내렸는지 알고있기 때문에 가능한 것입니다.
이 개념과 PreferenceKey 개념을 알고 있다면 NavigationTitle을 위에서 좀 더 커스터마이징해서 만들 수 있게 됩니다.
위처럼 밑으로 내릴수록 New Title이 희미해지다가 40 밑으로 내려가게 되면 background(.blue)인 New Title를 확인할 수 있습니다.
이것을 더 잘 활용하면 Spotify에서 사용하는 뷰를 만들 수 있을 것 같습니다.
import SwiftUI
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
extension View {
func onScrollViewOffsetChanged(action: @escaping (_ offset: CGFloat) -> Void) -> some View {
self
.background(
GeometryReader(content: { geometry in
Text("")
.preference(key: ScrollViewOffsetPreferenceKey.self, value: geometry.frame(in: .global).minY) // minY는 가장 맨 위
})
)
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self, perform: { value in
action(value)
})
}
}
struct ScrollViewOffsetPreferenceKeyView: View {
let title: String = "New title"
@State private var scrollViewOffset: CGFloat = 0
var body: some View {
ScrollView {
VStack {
titleLayer
.opacity(Double(scrollViewOffset) / 63.0)
.onScrollViewOffsetChanged { value in
self.scrollViewOffset = value
}
contentLayer
}
.padding()
}
.overlay(Text("\(scrollViewOffset)"))
.overlay (navBarLayer
.opacity(scrollViewOffset < 40 ? 1.0 : 0.0)
, alignment: .top)
}
}
#Preview {
ScrollViewOffsetPreferenceKeyView()
}
extension ScrollViewOffsetPreferenceKeyView {
private var titleLayer: some View {
Text(title)
.font(.largeTitle)
.fontWeight(.semibold)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var contentLayer: some View {
ForEach(0..<30) { _ in
RoundedRectangle(cornerRadius: 10)
.fill(Color.red.opacity(0.3))
.frame(width: 300, height: 200)
}
}
private var navBarLayer: some View {
Text(title)
.font(.headline)
.frame(maxWidth: .infinity)
.frame(height: 55)
.background(Color.blue)
}
}
'SwiftUI' 카테고리의 다른 글
Protocols 프로토콜 (0) | 2024.04.27 |
---|---|
UIViewControllerRepresentable (0) | 2024.04.27 |
UIViewRepresentable SwiftUI에서 UIKit 사용하기 (0) | 2024.04.24 |
사용자한테 보여줄 색깔 선택하기 (0) | 2024.04.23 |
Custom tab bar (0) | 2024.04.22 |