๐ฅAnchor Preferences
SwiftUI โฉ Data Flow โฉ View Preferences โฉ
An anchor is a wrapper around a value (e.g., a point) that can be resolved inside the coordinate system of a di๏ฌerent view somewhere else in the view hierarchy.
.onPreferenceChange has the requirement that the value conforms to Equatable.
Anchor does not conform to Equatable, use .overlayPreference or .backgroundPreference instead. ๐ Thinking in SwiftUI
The main goal of anchor preferences is to pass layout data like bounds, center coordinates, etc. to its parent view. ๐ Anchor<T>
When using the Anchor<Value> as an index to the GeometryProxy, you get the represented CGRect or CGPoint value. And as a plus, you get it already translated to the coordinate space of the GeometryReader view.
// .anchorPreference(key:value:transform:)
func anchorPreference<A, K: PreferenceKey>(
key _: K.Type = K.self,
value : Anchor<A>.Source,
transform: @escaping (Anchor<A>) -> K.Value
) -> some View
SwiftUI โฉ
View Layout & Presentation โฉ OutlineGroup โฉ ViewModifiers โฉ
use GeometryReader to solve an Anchor<T> in the coordinate system of another view.
selected button with underline - get bounds of selected button.
Examples
import SwiftUI
/// ๐
ฟ๏ธ HasDefaultValue
public protocol HasDefaultValue {
static var defaultValue: Self { get }
}
// ------------------------------------------
// `HasDefaultValue` conforming types
// ------------------------------------------
/// ๐ CGSize
extension CGSize: HasDefaultValue {
public static var defaultValue: Self { .zero }
}
/// ๐ CGRect
extension CGRect: HasDefaultValue {
public static var defaultValue: Self { .zero }
}
// -----------------------
// ViewPreference
// -----------------------
/// ๐ธ ViewPreference
public enum ViewPreference {
// ๅฐๆ้ bounds anchor ็่ณ่จๆพๅฐ view extension methods ็ๅๆธไธญ๏ผ
// ็ก้่ฎ method name ไธ่ฆๅคช้ทใ
public enum BoundsAnchorType {
case first
case last
}
/// โญ ViewPreference type aliases
public typealias Size = ViewPreference.First<CGSize>
public typealias FirstBoundsAnchor = ViewPreference.FirstNonNil<Anchor<CGRect>>
public typealias LastBoundsAnchor = ViewPreference.LastNonNil<Anchor<CGRect>>
/// ๐ธ ViewPreference.First
public enum First<T: HasDefaultValue>: PreferenceKey {
public typealias Value = T
public static var defaultValue: Value { Value.defaultValue }
public static func reduce(value: inout Value, nextValue: () -> Value) {
// โญ ignore all values other than the first
}
}
/// ๐ธ ViewPreference.FirstNonNil<T>
public enum FirstNonNil<T>: PreferenceKey {
public typealias Value = T?
public static var defaultValue: Value { nil }
public static func reduce(value: inout Value, nextValue: () -> Value) {
value = value ?? nextValue() // nil or first non-nil value
}
}
/// ๐ธ ViewPreference.LastNonNil<T>
public enum LastNonNil<T>: PreferenceKey {
public typealias Value = T?
public static var defaultValue: Value { nil }
public static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue() ?? value // nil or last non-nil value
}
}
}
// -----------------------
// View extensions
// -----------------------
/// ๐ CGRect + ViewPreference
extension View {
/// โญ usage: `view.actOnSelfSize { size in ... }`
public func actOnSelfSize(
action: @escaping (CGSize) -> Void
) -> some View
{
// ไธ่ฌ็ view ไธฆไธ็ฅ้่ชๅทฑ็ size ่ณ่จ๏ผไฝไฝฟ็จ๏ผ
// `.background(GeometryReader{ geo in Color.clear })`
// ๅฐฑๅฏไปฅๅจไธๅฝฑ้ฟ่ชๅทฑ็ไฝๅฑ่ๅค่งไธ๏ผ้้ `geo` (GeometryProxy) ็ฉไปถ
// ๅพ็ฅ่ชๅทฑ็ size (geo.size)๏ผ็ถๅพ็จ .preference() ๅๅ ฑไธๅฑคใ
self.background( GeometryReader { geo in
Color.clear
// โญ ไธๅฑคๅๅ ฑ่ชๅทฑ็ size
.preference(key: ViewPreference.Size.self, value: geo.size)
})
// โญ ไธๅฑคๅพ็ฅ size๏ผไธฆ็จ `action` ่็ใ
.onPreferenceChange(ViewPreference.Size.self, perform: action)
}
// โญ local abbreviations
typealias FBA = ViewPreference.FirstBoundsAnchor
typealias LBA = ViewPreference.LastBoundsAnchor
// ---------------------------------
// ไธๅฑคๅๅ ฑ่ณๆ็ใๆนๆณๅฝๅๅๅใ๏ผ
// 1. ๅฆๆๅชๆถ้ๅ
ถไธญไธๅ๏ผ็จ๏ผreport...()
// 2. ๅฆๆๆถ้ๅพๅคๅ๏ผๅฐฑ็จ๏ผcollect...()
// ----------------------------------
/// โญ report bounds anchor (with anchor type)
@ViewBuilder public func report(
bounds anchorType: ViewPreference.BoundsAnchorType
) -> some View {
switch anchorType {
case .first:
// โญ ๅๅ ฑ anchor preference
anchorPreference(key: FBA.self, value: .bounds) { $0 }
// โฐโ 1 โโโฏ โฐโโ2โโโฏ โฐโ3โโโฏ
// 1. ่ฆๅๅ ฑ็ anchor preference (K = FirstBoundsAnchor)
// 2. ่ฆๅๅ ฑ็ anchor ็จฎ้ก (.bounds)
// 3. ๅฐ Anchor<A> ่ฝ็บ่ฆๅๅ ฑ็่ณๆ๏ผ @escaping (Anchor<A>) -> K.Value
// ๅฆๆ Anchor<A> ๆฌ่บซๅฐฑๆฏ่ฆๅๅ ฑ็่ณๆ (K.Value)๏ผ็ดๆฅไฝฟ็จ `$0` ๅฐฑๅฏไปฅใ
case .last:
anchorPreference(key: LBA.self, value: .bounds) { $0 }
}
}
/// โญ report bounds anchor
public func reportBounds() -> some View {
// โญ ๅๅ ฑ anchor preference
anchorPreference(key: FBA.self, value: .bounds) { $0 }
// โฐโ 1 โโโฏ โฐโโ2โโโฏ โฐโ3โโโฏ
// 1. ่ฆๅๅ ฑ็ anchor preference (K = FirstBoundsAnchor)
// 2. ่ฆๅๅ ฑ็ anchor ็จฎ้ก (.bounds)
// 3. ๅฐ Anchor<A> ่ฝ็บ่ฆๅๅ ฑ็่ณๆ๏ผ @escaping (Anchor<A>) -> K.Value
// ๅฆๆ Anchor<A> ๆฌ่บซๅฐฑๆฏ่ฆๅๅ ฑ็่ณๆ (K.Value)๏ผ็ดๆฅไฝฟ็จ `$0` ๅฐฑๅฏไปฅใ
}
/// โญ background by reported bounds (with anchor type)
@ViewBuilder public func background<V: View>(
bounds anchorType: ViewPreference.BoundsAnchorType,
@ViewBuilder content: @escaping (CGRect) -> V
) -> some View
{
switch anchorType {
case .first:
backgroundPreferenceValue(FBA.self) { anchor in
GeometryReader { geo in
if let anchor = anchor { content(geo[anchor]) }
}
}
case .last:
backgroundPreferenceValue(LBA.self) { anchor in
GeometryReader { geo in
if let anchor = anchor { content(geo[anchor]) }
}
}
}
}
/// โญ background by reported bounds
public func backgroundByBounds<V: View>(
@ViewBuilder content: @escaping (CGRect) -> V
) -> some View
{
backgroundPreferenceValue(FBA.self) { anchor in
GeometryReader { geo in
if let anchor = anchor { content(geo[anchor]) }
}
}
}
/// โญ act on bounds reported
@ViewBuilder public func actOnBounds(
_ anchorType: ViewPreference.BoundsAnchorType,
action: @escaping (Anchor<CGRect>) -> Void
) -> some View
{
switch anchorType {
case .first:
onPreferenceChange(FBA.self) { anchor in
if let anchor = anchor { action(anchor) }
}
case .last:
onPreferenceChange(LBA.self) { anchor in
if let anchor = anchor { action(anchor) }
}
}
}
}
struct ContentView: View {
@State private var isFirst = true
// โญ bounds anchor type
var anchorType: ViewPreference.BoundsAnchorType {
isFirst ? .first : .last
}
var body: some View {
// Toggle
Toggle("First/Last Bounds Anchor", isOn: $isFirst)
.padding()
.background(Color.gray)
// ZStack
ZStack {
Color.red
VStack {
Text("Hello")
.padding()
.border(Color.black)
.report(bounds: anchorType) // โญ report bounds anchor
Text("Wonderful")
Text("World")
.report(bounds: anchorType) // โญ report bounds anchor
}.background(bounds: anchorType) { bounds in // โญ background with bounds
ZStack(alignment: .leading) {
Rectangle()
.fill(Color.orange)
.frame(
width: bounds.width/3,
height: bounds.height
)
.offset(x: bounds.minX, y: bounds.minY)
Rectangle()
.fill(Color.purple)
.frame(
width: bounds.width * 2 / 3,
height: bounds.height)
.offset(
x: bounds.width/3 + bounds.minX,
y: bounds.minY)
}
}.actOnBounds(anchorType){ anchor in // โญ act on reported bounds
print(anchor)
}
}// ZStack
}// body
}// ContentView
ๅฆๆๅญ่ฆไปถ childView ่ฆๅ ฌ้ๅฎ็ frame ็ตฆๅฅ็่ฆไปถไฝฟ็จ๏ผๅฏไปฅ็จไธๅ็่ชๆณไพๅฎฃๅ๏ผ
ไปฅไธๅ่จญ typealias FrameAnchors = AllValues<Anchor<CGRect>>
๐ ้ๆผ AllValues<T>๏ผ่ซๅ้ฑ๏ผ PreferenceKeyใ
// โญ๏ธ 1. ๅฐ่ชๅทฑ็ frame (็จ value: .bounds ๆๅฎ) ็ป่จๅฐ FrameAnchors ไธญ
// 2. $0 ็้กๅๆฏ Anchor<CGRect>
// 3. [$0] ๆฏ็บไบ้
ๅ FrameAnchors.Value ็้กๅ๏ผ[Anchor<CGRect>]
childView.anchorPreference(key: FrameAnchors.self, value: .bounds) { [$0] }
็ถๆฏ่ฆไปถ่ฆ็จ้ไบ FrameAnchors ไพๅไฝๅฑๆ๏ผๅฏไปฅ็จ๏ผ
parentView
// โญ๏ธ ๆๅ
ฌ้็ FrameAnchors ๆฟไพ็จ (้
ๅ๏ผGeometryProxy)
.overlayPreferenceValue(FrameAnchors.self) { (anchors) in
GeometryReader { geo in
// ็จ GeometryProxy ็ subscript ่ชๆณ๏ผ
// geo[anchors[i]]
// ๅๅบ็ฌฌ i ๅ Anchor<CGRect>
// ๅจ้ๅๅบงๆจ็ณป็ตฑไธญ็ frame (CGRect)๏ผ
// ็ถๅพๅ็ธ้็ไฝๅฑใ
}
}
้ๆจฃๅ็ๅฅฝ่ๆฏ๏ผๆๅไธ้่ฆๅจ parentView ่บซไธๅฆ่กๅฎ็พฉ CoordinateSpaceใ
Last updated