๐Ÿšฅ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.

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) }
            }
        }
    }
}

Last updated