Building a complex UI may require data exchange between different parts of the view hierarchy. Often we may require an exact position or some coordinate point during various animation processes or even just a point.

Back in UIKit, everything is quite simple - we may ask UIView to convert values (such as frame (CGRect) or point (CGPoint)) from one coordinate system to another and send data using various mechanisms (for example delegate or closure).

check Converting Between View Coordinate Systems part of this official documentation

If we think about SwiftUI - things are a bit different: we can’t use any of these mechanisms. So what we have instead? The answer is Anchor.

Anchor

Anchor is just a struct, that can wrap some value - value that u want to manage and pass between view’s. The official description of this - An opaque value derived from an anchor source and a particular view.

The declaration of this:

@frozen public struct Anchor<Value> {
	@frozen public struct Source { }
}

Inside we also see a nested struct that represents Source - something that generates the value that we want to pass and manipulate later. Source can be used within GeometryProxy object (using subscript mechanism).

GeometryProxy can be obtained from GeometryReader.

Source is managed using PreferenceKey via special viewModifier - anchorPreference:

@inlinable public func anchorPreference<A, K>(key _: K.Type = K.self, value: Anchor<A>.Source, transform: @escaping (Anchor<A>) -> K.Value) -> some View where K : PreferenceKey

I wrote separate article about PreferenceKey, check it out for more.

Removing generic parameters and constraints for simplification we can see that this view modifier requires from us a few things:

  • key to which value will be temporarily stored
  • source - where to find the value and
  • transform block - how to transform and represent obtained value.

I would like to mention that in theory, we can pass any Value in the Anchor struct - as u already saw, there are no requirements to it. So we may want to pass our type not related to geometry, but this probably a bad coding style.

In case if u would like to use if for passing CGRect or CGPoint, Apple has prepared for us predefined set of possible options and operations similar to one existing in UIKit:

extension Anchor.Source where Value == CGRect {
    public static func rect(_ r: CGRect) -> Anchor<Value>.Source
    public static var bounds: Anchor<CGRect>.Source { get }
}

extension Anchor.Source where Value == CGPoint {
    public static func point(_ p: CGPoint) -> Anchor<Value>.Source
    public static func unitPoint(_ p: UnitPoint) -> Anchor<Value>.Source
    public static var topLeading: Anchor<CGPoint>.Source { get }
    public static var top: Anchor<CGPoint>.Source { get }
    
    ...
}

Example

The best way to understand something is by trying it.

As for me, I prefer to have a bit of understanding of an idea before actual coding, that’s why I put a description of how it should be used above.

We may create a simple view with a few nested views and pass geometry between them. Code for the view will be very simple:

struct AnchorView: View {
        
    var body: some View {
        VStack {
            HStack {
                Button(action: {
						// action
                }, label: {
                    Text("Hi")
                })
                .padding()
                Spacer()
            }
            HStack {
                Spacer()
                Button(action: {
						// action
                }, label: {
                    Text("Hello")
                })
                Spacer()
            }
            HStack {
                Spacer()
                Button(action: {
						// action
                }, label: {
                    Text("Aloha")
                })
                .padding()
            }
        }
        .padding(32)
	}
}

I added extra .padding on few buttons just to make changes more obvious - u will see it later.

initialView


To test Anchor firstly, as was mention above, we should create a PreferenceKey - value that can hold data for us:

struct TextBoundsKey: PreferenceKey {
    static var defaultValue: Anchor<CGRect>? = nil
    
    static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) {
        value = value ?? nextValue()
    }
}

Here, we declared TextBoundsKey that will be used to manage CGRect (Anchor<CGRect>) - frame of each button.

Next - we can already use the preference key using the modifier described above - anchorPreference. To make things a bit better, we may create a View extension for this. Adding any input parameters that can be used for the transform block is also an option.

So let’s define extension with additional input - flag that can help us to decide whenever we should capture value. Also - pay attenstion to a value - .bounds used - one of predefined options from Anchor<CGRec>:

extension View {
    func captureBounds(_ shouldCaptureAnchor: Bool) -> some View {
        anchorPreference(
            key: TextBoundsKey.self,
            value: .bounds, // <-- var bounds: Anchor<CGRect>.Source
            transform: { anchor in
                shouldCaptureAnchor ? anchor : nil
            }
        )
    }
}

Now, we may add logic to the view that allows us to capture coordinates of the Button only if we press it. To do so we need a @State variable for holding the index of the button and our captureBounds function:

struct AnchorView: View {
    
    @State private var selectedItemIndex: Int = 0
    
    var body: some View {
        VStack {
            HStack {
                Button(action: {
                    selectedItemIndex = 0
                }, label: {
                    Text("Hi")
                })
                .padding()
                .captureBounds(selectedItemIndex == 0)
                Spacer()
            }
            HStack {
                Spacer()
                Button(action: {
                    selectedItemIndex = 1
                }, label: {
                    Text("Hello")
                })
                .captureBounds(selectedItemIndex == 1)
                Spacer()
            }
            HStack {
                Spacer()
                Button(action: {
                    selectedItemIndex = 2
                }, label: {
                    Text("Aloha")
                })
                .padding()
                .captureBounds(selectedItemIndex == 2)
            }
        }
        .padding(32)
	}
}

Here u can see that whenever we press a button, the index of a button is stored and captureBounds receive a true flag, that enables Anchor capturing inside the transform block.

Now we may add action on this preference key change. Unfortunately we can’t use existing function for this onPreferenceChange(perform:) because its has Equitable requirements. To solve this, Apple has added overlayPreferenceValue(::).

Thus we capture CGRect, let’s draw a rectangle using transferred value - rect around a selected button. But before adding code for this, I would like to remember how to obtain transferred value - we should use GeometryReader and available subscript from GeometryProxy:

subscript<T>(anchor: Anchor<T>) -> T { get }

doc

Usage within our example may be next:

// at the end of body
.overlayPreferenceValue(TextBoundsKey.self, { value in
    GeometryReader { geometryProxy in
        if let value = value { // <-- optional, so unwrap and use if exist
            RoundedRectangle(cornerRadius: 3)
                .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, dash: [1, 2], dashPhase: 0.5))
                .frame(width: geometryProxy[value].size.width, height: geometryProxy[value].size.height)
                .offset(x: geometryProxy[value].minX, y: geometryProxy[value].minY)
                .animation(.easeIn)
        }
    }
})

Here the magic is in this code - geometryProxy[value].

simple_demo


This is quite a simple example, and we use everything within one View. An example is good only for initial testing, but in real life we never gonna have such a case - instead, a more complex view tree will be used.

So let’s make this example a bit more realistic and so complex. To do so we may extract Button to separate View and add some additional parameter that can be moved within a CGRect - the color of the rectangle that is drawn around a selected button.

Let’s start by defining our data type for transferring CGRect and Color:

struct AnchorValue {
    
    let rectAnchor: Anchor<CGRect>
    let color: Color
}

Than - modified version of preferenseKey extension that now use our data type AnchorValue:

struct ColoredTextBoundsKey: PreferenceKey {
    static var defaultValue: AnchorValue? = nil
    
    static func reduce(value: inout AnchorValue?, nextValue: () -> AnchorValue?) {
        value = value ?? nextValue()
    }
}

extension View {
    func captureColoredTextBounds(_ shouldCaptureAnchor: Bool, color: Color) -> some View {
        anchorPreference(key: ColoredTextBoundsKey.self, value: .bounds, transform: { anchor in
            shouldCaptureAnchor ? AnchorValue(rectAnchor: anchor, color: color) : nil
        })
    }
}

Extracted ButtonView:

struct ButtonView: View {
    
    // for simplicity this type is not handled RTL and LTR
    enum Alignment { 
        
        case left
        case center
        case right
    }
    
    let text: String
    let rectColor: Color
    let position: Int
    let alignment: ButtonView.Alignment
    @Binding var selectedIndex: Int
    
    var body: some View {
        HStack {
            
            if alignment != .left {
                Spacer()
            }
            
            Button(action: {
                selectedIndex = position
            }, label: {
                Text(text)
            })
            .padding()
            .captureColoredTextBounds(selectedIndex == position, color: rectColor)
            if alignment != .right {
                Spacer()
            }
        }
    }
}

And finally, our view that uses anchorPreference:

struct AnchorView: View {
    
    @State private var selectedItemIndex: Int = 0
    
    var body: some View {
        VStack {
            ButtonView(
                text: "Hi",
                rectColor: .red,
                position: 0,
                alignment: .left,
                selectedIndex: $selectedItemIndex
            )
            ButtonView(
                text: "Hello",
                rectColor: .green,
                position: 1,
                alignment: .center,
                selectedIndex: $selectedItemIndex
            )
            ButtonView(
                text: "Aloha",
                rectColor: .blue,
                position: 2,
                alignment: .right,
                selectedIndex: $selectedItemIndex
            )
        }
        .padding(32)
        .overlayPreferenceValue(ColoredTextBoundsKey.self, { value in
            GeometryReader { geometryProxy in
                if let value = value {
                        RoundedRectangle(cornerRadius: 3)
                            .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, dash: [1, 2], dashPhase: 0.5))
                            .foregroundColor(value.color)
                            .frame(width: geometryProxy[value.rectAnchor].size.width, height: geometryProxy[value.rectAnchor].size.height)
                            .offset(x: geometryProxy[value.rectAnchor].minX, y: geometryProxy[value.rectAnchor].minY)
                            .animation(.easeOut)

                }
            }
        })
    }
}

Result:

demo_advanced


This example demonstrates a bit more complex viewTree and data.

Consclusion

Using Anchor, we easily can manage the view’s, its geometry and transfer data between viewTree components. This is a very useful technique that helps us build a more complex layout.

download source