Use PreferenceKey to extract values from child views in SwiftUI | Advanced Learning #10
PreferenceKey
.onPreferenceChange(CustomTitlePreferenceKey.self) { value in
self.navTitle = value
}
PreferenceKey
를 변경하는 메소드. @State
값을 변경할 수 있는 클로저로, 특정 값의 변화를 자식 뷰에서 감지해서 부모 뷰로 이동시키는 방법으로 사용됨struct CustomTitlePreferenceKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
defaultValue
, reduce
두 개의 변수, 함수를 선언한 뒤 사용 가능. reduce
함수를 통해 새롭게 들어온 inout
변수를 새로운 값으로 변경 가능. nextValue
는 해당 값을 리턴하는 일종의 클로저, 함수임에 주의extension View {
func customTitle(text: String) -> some View {
self
.preference(key: CustomTitlePreferenceKey.self, value: text)
}
}
preference
키 사용 방법preference
를 걸고, 이후 onPreferenceChange
와 같은 방법으로 내부 값 변경 가능import SwiftUI
struct PreferenceKeyBootCamp: View {
@State private var navTitle: String = "HELLO WOLRD"
var body: some View {
NavigationView {
VStack {
SecondaryScreen(text: navTitle)
.navigationTitle("FIRST NAV TITLE")
}
}
.onPreferenceChange(CustomTitlePreferenceKey.self) { value in
self.navTitle = value
}
}
}
extension View {
func customTitle(text: String) -> some View {
self
.preference(key: CustomTitlePreferenceKey.self, value: text)
}
}
struct SecondaryScreen: View {
let text: String
@State private var newValue = ""
var body: some View {
Text(text)
.onAppear(perform: getDataFromDatabase)
.customTitle(text: newValue)
}
private func getDataFromDatabase() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.newValue = "NEW VALUE FROM DATABASE"
}
}
}
struct CustomTitlePreferenceKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
SecondaryScreen
이 나타나는 시점에서 DB에서 값을 받아오는 것처럼 행동하는 getDataFromDatabase()
import SwiftUI
struct GeometryPreferenceBootCamp: View {
@State private var rectSize: CGSize = .zero
var body: some View {
VStack {
Spacer()
Text("Hello, World!")
.frame(width: rectSize.width, height: rectSize.height)
.background(Color.blue)
Spacer()
HStack {
Rectangle()
GeometryReader { geometry in
Rectangle()
.updateRectangleGeoSize(geometry.size)
}
Rectangle()
}
.frame(height: 55)
}
.onPreferenceChange(RectangleGeometryPreferenceKey.self) { value in
self.rectSize = value
}
}
}
extension View {
func updateRectangleGeoSize(_ size: CGSize) -> some View {
preference(key: RectangleGeometryPreferenceKey.self, value: size)
}
}
struct RectangleGeometryPreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
Spacer
, padding
등으로 그려진 뷰의 정확한 크기를 안 뒤 다른 뷰에 적용할 때 사용하는 방법rectSize
프레임 사이즈의 첫 번째 타이틀이 HStack
에 넣어놓은 사각형 뷰 크기 프레임을 입력받아 값 변경RectangleGeometryPreferenceKey
를 updateRectangleGeoSize
뷰 익스텐션 함수를 통해 사각형 뷰에 등록, onPreferenceChange
를 통해 해당 값(CGSize
)을 rectSize
에 할당import SwiftUI
struct ScrollViewOffsetPreferenceBootCamp: View {
let title = "SCROLL VIEW"
@State private var scrollViewOffset: CGFloat = 0
var body: some View {
ScrollView {
VStack {
titleLayer
.opacity(Double(scrollViewOffset) / 63.0)
.onScrollViewOffsetChange { offset in
scrollViewOffset = offset
}
contentLayer
}
.padding()
}
.overlay(Text("\(scrollViewOffset)"))
.overlay(navBarLayer.opacity(scrollViewOffset < 40 ? 1.0 : 0.0), alignment: .top)
}
}
extension View {
func onScrollViewOffsetChange(action:@escaping (_ offset: CGFloat) -> ()) -> some View {
self
.background(
GeometryReader { geometry in
Text("")
.preference(key: ScrollViewOffsetPreferenceKey.self, value: geometry.frame(in: .global).minY)
}
)
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
action(value)
}
}
}
extension ScrollViewOffsetPreferenceBootCamp {
private var titleLayer: some View {
Text(title)
.font(.largeTitle)
.fontWeight(.semibold)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var contentLayer: some View {
ForEach(0..<100) { _ in
RoundedRectangle(cornerRadius: 10)
.fill(Color.red.opacity(0.3))
.frame(width: 300, height: 200)
}
}
private var navBarLayer: some View {
Text("OVERLAY TITLE")
.font(.headline)
.fontWeight(.bold)
.frame(maxWidth: .infinity)
.frame(height: 55)
.background(.white)
}
}
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
titleLayer
navBarLayer
contentLayer
titleLayer
에 onScrollViewOffsetChange
를 적용, 보이지 않는 Text("")
뷰의 현재 위치값을 GeometryReader
로 읽어 scrollViewOffset
에 동기화하는 역할Text("")
는 스크롤이 이동 방향에 따라 값 변경 → 현재 스크롤 바의 상대적 위치 파악 가능 → 네비게이션 라지/인라인 타이틀 여부 결정 가능opacity
작게, 특정 값 40 이하라면 곧바로 인라인 타이틀 나타나도록 구현부모 뷰에서 자식 뷰의 특정 값을
PreferenceKey
로 등록해 캐치할 수 있는 것은State
,Binding
뿐만이 아님을 알 수 있었다.
Binding
보다 위의 프레임, 위치 정보 등을 얻는 데에는 가장 적합한 방법인 것 같다.