Anchor preferences in SwiftUI

栏目: IT技术 · 发布时间: 5年前

内容简介:Today we will continue mastering view preferences inFirst of all, I want to ask you to check the post about view preferences if you are not familiar with these API. Anchor preferences use a very similar API. The only difference is that it is tuned to pass

Today we will continue mastering view preferences in SwiftUI that we touched a few weeks ago. Anchor preferences are another type of view preferences provided by SwiftUI . The main goal of anchor preferences is to pass layout data like bounds, center coordinates, etc. to its parent view.

Basics

First of all, I want to ask you to check the post about view preferences if you are not familiar with these API. Anchor preferences use a very similar API. The only difference is that it is tuned to pass layout-specific data.

To learn more about the benefits of preferences in SwiftUI, take a look at my “The magic of view preferences in SwiftUI” post.

Let’s build a simple view that shows text and passes its bounds to the ancestor. The parent view will draw an overlaying rectangle in that position.

struct BoundsPreferenceKey: PreferenceKey {
    typealias Value = Anchor<CGRect>?

    static var defaultValue: Value = nil

    static func reduce(
        value: inout Value,
        nextValue: () -> Value
    ) {
        value = nextValue()
    }
}

struct ExampleView: View {
    var body: some View {
        ZStack {
            Color.yellow
            Text("Hello World !!!")
                .anchorPreference(
                    key: BoundsPreferenceKey.self,
                    value: .bounds
                ) { $0 }
        }
        .overlayPreferenceValue(BoundsPreferenceKey.self) { preferences in
            GeometryReader { geometry in
                preferences.map {
                    Rectangle()
                        .stroke()
                        .frame(
                            width: geometry[$0].width,
                            height: geometry[$0].height
                        )
                        .offset(
                            x: geometry[$0].minX,
                            y: geometry[$0].minY
                        )
                }
            }
        }
    }
}

As you can see in the example above, we still use the PreferenceKey protocol to create an anchor preference key. It has two requirements: default value and reduce function. Reduce function allows us to merge multiple values that appear from different views. We can replace the current value with the new one for now. We will see more advanced usage of reduce function later in the post.

Anchor preferences use opaque Anchor type. You can’t merely use Anchor type anywhere in the app. You have to use it in pair with GeometryProxy provided by GeometryReader . You can use the subscript of GeometryProxy to resolve anchor and access wrapped CGRect value. As a bonus, SwiftUI will convert a coordinate space between views while solving anchor, and you don’t need to do it manually.

We use the anchorPreference modifier to define the type of PreferenceKey and the value we want to gather. It can be bounds, center, leading, trailing, top, bottom. We also pass a closure that transforms provided anchor value. In this case, we don’t need any transformation and return the CGRect value as is.

In the end, we use overlayPreferenceValue on ancestor view to access gathered preference values and return overlay view. As I mentioned before, we need a GeometryProxy to resolve an anchor. That’s why we use here GeometryReader .

We can easily use border modifier on the text view to achieve the same result, but I’ve done it to show you the basics of anchor preferences.

Advanced usage

Now we can move to more advanced usage of anchor preferences. As an example, we will build a grid view. We will need to gather the size of every view inside the grid to calculate its positions. Let’s start by defining the PreferenceKey for our grid view.

struct SizePreferences<Item: Hashable>: PreferenceKey {
    typealias Value = [Item: CGSize]

    static var defaultValue: Value { [:] }

    static func reduce(
        value: inout Value,
        nextValue: () -> Value
    ) {
        value.merge(nextValue()) { $1 }
    }
}

As you can see here, we will store the dictionary that represents an item and its size. In the reduce function, we merge old and new dictionaries by overriding new values. Now we can define our grid view.

struct Grid<Data: RandomAccessCollection, ElementView: View>: View where Data.Element: Hashable {
    private let data: Data
    private let itemView: (Data.Element) -> ElementView

    @State private var preferences: [Data.Element: CGRect] = [:]

    init(_ data: Data, @ViewBuilder itemView: @escaping (Data.Element) -> ElementView) {
        self.data = data
        self.itemView = itemView
    }

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .topLeading) {
                ForEach(self.data, id: \.self) { item in
                    self.itemView(item)
                        .alignmentGuide(.leading) { _ in
                            -self.preferences[item, default: .zero].origin.x
                    }.alignmentGuide(.top) { _ in
                        -self.preferences[item, default: .zero].origin.y
                    }.anchorPreference(
                        key: SizePreferences<Data.Element>.self,
                        value: .bounds
                    ) {
                        [item: geometry[$0].size]
                    }
                }
            }
        }
    }
}

We use ZStack with top leading alignment. It allows us to position items inside in an effortless way. Instead of using the offset modifier which doesn’t affect the layout, we use overridden alignment guides to position our child views. We also resolve our anchors here, because we already have access to the instance of GeometryProxy .

To learn more about the benefits of alignment guides in SwiftUI, take a look at my “Alignment guides in SwiftUI” post.

var body: some View {
    GeometryReader { geometry in
        ZStack(alignment: .topLeading) {
            ...
        }
        .onPreferenceChange(SizePreferences<Data.Element>.self) { sizes in
            var newPreferences: [Data.Element: CGRect] = [:]
            var bounds: [CGRect] = []
            for item in self.data {
                let size = sizes[item, default: .zero]
                let rect: CGRect
                if let lastBounds = bounds.last {
                    if lastBounds.maxX + size.width > geometry.size.width {
                        let origin = CGPoint(x: 0, y: lastBounds.maxY)
                        rect = CGRect(origin: origin, size: size)
                    } else {
                        let origin = CGPoint(x: lastBounds.maxX, y: lastBounds.minY)
                        rect = CGRect(origin: origin, size: size)
                    }
                } else {
                    rect = CGRect(origin: .zero, size: size)
                }
                bounds.append(rect)
                newPreferences[item] = rect
            }
            self.preferences = newPreferences
        }
    }
}

As the last step, we calculate bounds for every item using GeometryProxy and gathered sizes. Let’s take a look at the final result.

struct RootView: View {
    @State private var cards: [String] = [
        "Lorem", "ipsum", "is", "placeholder", "text", "!!!"
    ]

    var body: some View {
        Grid(cards) { card in
            Text(card)
                .frame(width: 120, height: 120)
                .background(Color.orange)
                .cornerRadius(8)
                .padding(4)
        }
    }
}

Anchor preferences in SwiftUI

Conclusion

SwiftUI provides us so many great tools that we can use to build impressive views. Anchor preferences feature is one of the powerful hidden gems of SwiftUI . I hope you enjoy the post. Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading, and see you next week!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

思考的技术

思考的技术

[日]大前研一 / 刘锦秀、谢育容 / 中信出版社 / 2010-11 / 32.00元

思路决定出路,没有了思路,也就没有了出路。 在充满危机与冒险的当下,我们缺乏的不是技巧而是揭发事务本质的动力和好奇心,缺少怀疑一切的心态和对固有模式的怠惰。 大前研一凭借他30多年的管理咨询经验,为我们提供了一种全新的可借鉴的思考方式。 企业和个人惟有改变既有的思考模式,放弃对过去成功经验的迷恋,学习有创意的思考方法,方能找到正确的经营思路。一起来看看 《思考的技术》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具