简介

本年 SwiftUI 新增最好的功用之一必须是布局协议。它不但让咱们参与到布局过程中,而且也给了咱们一个很好的机会去更好的了解布局在 SwiftUI 中的效果。

早在2019年,我写了一篇文章 SwiftUI 中 frame 的体现,其间,我论述了父视图和子视图怎么和谐构成终究视图效果。那里描绘的许多状况需求经过调查不同测验的成果去猜想。整个过程就像是发现外星行星,天文学家发现太阳亮度微小的减少,然后推断出这一定是行星过境(了解行星过境)。

现在,有了布局协议,就像用自己的眼睛在遥远的太阳系周游,令人振奋。

创立一个根底布局并不难,只需求完成两个办法。尽管如此,咱们依然有许多选择去完成一个杂乱的容器。咱们将会探究常规布局案例之外的内容。有许多风趣的论题到现在为止我还没有在任何当地看到过解说,所以我将在这儿介绍它们。可是,在深化这些领域之前,咱们需求先打下扎实的根底。

由于涉及到许多内容,我将分红两个部分:

Part 1 – 根底:

  • 什么是布局协议
  • 视图层次结构的族动态
  • 咱们的榜首个布局完成
  • 容器对齐
  • 自界说值:LayoutValueKey
  • 默许距离
  • 布局特点和 Spacer()
  • 布局缓存
  • 高超的伪装者
  • 运用AnyLayout切换布局
  • 结语

Part 2 – 高档布局:

  • 敞开风趣的旅程
  • 自界说动画
  • 双向自界说值
  • 避免布局循环和崩溃
  • 递归布局
  • 布局组合
  • 另一个组合案例:刺进两个布局
  • 运用绑定参数
  • 一个有用的调试东西
  • 终究的考虑

假如你现已熟悉布局协议,你或许想直接跳到第二部分。这是能够的,尽管我依然推荐你浏览榜首部分,至少浅读一下。这将确保咱们在开端探究第二部分中描绘的更多高档特性时,咱们在同一进展。

假如在阅览本文的任何时分,你认为布局协议不合适你(至少现在来说),我依然主张你查看 Part2 的这一小节—一个有用的调试东西,这个东西能够协助你运用 SwiftUI ,且不需求了解布局协议就能够运用。我将它放在第二部分结束是有原因的,这个东西是运用本文的常识构建的。不过,你能够直接仿制代码运用它。

什么是布局协议

选用布局协议类型的使命,是告知 SwiftUI 怎么放置一组视图,需求多少空间。这类型常常被作为视图容器,尽管布局协议是本年新推出的(至少公开来说),可是咱们在榜首天运用 SwiftUI 的时分就在运用了,当每次运用 HStack 或许 VStack 放置视图时都是如此。

请留意至少到现在,布局协议不能创立懒加载容器,比如 LazyHStackLazyVStack。懒加载容器是指那些只在滚入屏幕时烘托,滚出到屏幕外就停止烘托的视图。

一个重要的常识点,Layout 类型不是视图 。例如,它们没有视图具有的 body 特点。可是不用忧虑,现在为止你能够认为它们便是视图而且像视图相同运用它们。这个结构运用了漂亮的 Swift 言语技巧使你的布局代码在向 SwiftUI 中刺进时发生一个透明视图 。我将在后边-高超的伪装者部分阐明。

视图层次结构的族动态

在咱们开端布局代码之前,让咱们从头审视一下 SwiftUI 结构的中心。就像我在曾经的文章 SwiftUI 中 frame 的体现 所描绘的的那样,在布局过程中,父视图给子视图供给一个尺度,但终究仍是由子视图决议怎么制作自己。然后,它将此传达给父视图,以便采纳相应的动作。有三个或许的状况,咱们将专注评论于横轴(宽度),但纵轴(高度)同理:

状况一:假如子视图需求小于供给的视图

在这个比如中考虑文本视图,供给了比需求制作文字更多的空间

SwiftUI 布局协议 - Part1

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {
            Rectangle().fill(.green)
            Text("Hello World!")
            Rectangle().fill(.green)
        }
        .padding(20)
    }
}

在这个比如中,屏幕宽度是 400pt。因而,文本供给 HStack 宽度的三分之一 ((400 – 40) / 3 = 120)。在这 120pt 中,文本只需求 74,并传达给父视图,父视图现在能够拿走剩余的 46pt 给其他的子视图用。由于其他子视图是图形,所以它们能够接纳给它们的一切东西。在这种状况下,120+46/2=143。

状况二:假如子视图完全接纳供给的视图

图形便是视图中的一个比如,不论你供给了什么他都能接纳。在上一个比如中,绿色矩形占据了供给的一切空间,但没有一个剩余的像素。

状况三:假如子视图需求超出供给的视图

考虑下面这个比如,图片视图特别严厉(除非他们修正了 resizable 办法),它们需求多少空间就要占用多少空间,在下面这个比如中,图片是 300300,这也是它们需求制作自己需求的空间,可是,经过调用 frame(width:100) 子视图只得到了 100pt,父视图就没有办法只能听从子视图的做法吗?并非如此,子视图依然会运用 300pt 制作,可是父视图将会布局其他视图,就好像子视图只有 100pt 宽度相同。成果呢,咱们将会有一个超出鸿沟的子视图,可是周围的视图不会被图片额定运用的空间影响。在下面这个比如中,黑色边框展现的空间是供给给图片的。

SwiftUI 布局协议 - Part1

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {
            Rectangle().fill(.yellow)
            Image("peach")
                .frame(width: 100)
                .border(.black, width: 3)
                .zIndex(1)
            Rectangle().fill(.yellow)
        }
        .padding(20)
    }
}

视图的行为办法有许多差异。例如,咱们看见文本获取需求空间后怎么处置剩余的不需求的空间,可是,假如需求的空间大于供给,就或许会发生一些事情,详细取决于你怎么装备你的视图。例如,或许会依据供给的尺度截取文本,或许在供给的宽度内垂直的展现文本,假如你运用 fixedSize 修正乃至或许超出屏幕就像比如中的图片相同。请记住, fixedSize 告知视图运用其抱负尺度,无论供给的是多少。

假如你想了解更多这些行为以及怎么改动它们,请查看我曾经的文章 SwiftUI 中 frame 的体现

咱们的榜首个布局完成

创立一个布局类型需求咱们完成至少两个办法, sizeThatFitsplaceSubviews 。这些办法接纳一些新类型作为参数: ProposedViewSizeLayoutSubview 。在咱们开端写办法之前,先看看这些参数长什么样:

ProposedViewSize

ProposedViewSize 被父视图用来告知子视图怎么核算自己的尺度。这是一个简略的类型,但很强大。它仅仅一对可选的 CGFloat ,用于主张宽度和高度。可是,正是咱们怎么解说这些值才使它们变得风趣。

这些特点能够有详细的值(例如35,74等),但当它们等于0.0 ,nil 或许 .infinity 时是有特其他意义。

  • 关于一个详细的宽度,例如 45,父视图供给的也是 45pt,这个视图应该由供给的宽度来决议本身的尺度
  • 关于宽度为 0.0,子视图应该响应为最小尺度
  • 关于宽度为 .infinity ,子视图应该响应为最大尺度
  • 关于 nil,父视图应该响应为抱负尺度

ProposedViewSize 也能够有一些预界说值:

ProposedViewSize.zero = ProposedViewSize(width: 0, height: 0)
ProposedViewSize.infinity = ProposedViewSize(width: .infinity, height: .infinity)
ProposedViewSize.unspecified = ProposedViewSize(width: nil, height: nil)

LayoutSubview

sizeTheFitsplaceSubviews 办法也接纳一个 Layout.Subviews 参数,它是一个 LayoutSubview 元素的合集。每个视图都有一个,作为父视图的直接子孙。尽管有这个称号,但它的类型不是视图,而是一个署理。咱们能够查询这些署理去了解咱们正在布局的各个视图的布局信息。例如,自 SwiftUI 推出以来,咱们榜首次能够直接查询到视图最小,抱负和最大的尺度,或许咱们能够取得每个视图的布局优先级以及其他风趣的值。

sizeThatFits 办法

func sizeThatFits(proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache) -> CGSize

SwiftUI 将会调用 sizeThatFits 办法决议咱们布局容器的尺度,当咱们写这个办法咱们应该认为咱们既是父视图又是子视图:当作为父视图时需求询问子视图的尺度,当咱们是子视图时,要基于咱们子视图的回复告知父视图需求的尺度,

这个办法将会收到主张尺度,一个子视图署理的合集和一个缓存。终究一个参数或许用以进步咱们的布局和一些其他高档应用的功能,但现在咱们不会运用它,咱们会在后边一点再去看它。

sizeThatFits 办法在给定维度中(即宽度或高度)收到的主张尺度为 nil 时,咱们应该回来容器的抱负尺度。当收到的主张尺度为0.0时,咱们应该回来容器的最小尺度。当收到的主张尺度为 .infinity 时,咱们应该回来容器的最大尺度。

留意 sizeThatFits 或许经过不同提案多次调用来测验容器的灵活性,提案能够是上述每个维度案例的任意组合。例如,你或许会得到一个带有 ProposedViewSize(width: 0.0, height: .infinity)的调用。

在咱们掌握了这些信息后,让咱们开端榜首个布局。咱们经过创立一个根底的 HStack 开端。咱们把它命名为 SimpleHStack 。为了比较两者,咱们创立一个规范的 HStack (蓝色)视图放置在SimpleHStack (绿色)上方。在咱们的榜首次测验中,咱们将会完成 sizeThatFits ,可是一起咱们将会使其他需求的办法(placeSunviews)为空。

SwiftUI 布局协议 - Part1

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            HStack(spacing: 5)  { 
                contents() 
            }
            .border(.blue)
            SimpleHStack(spacing: 5) {
                contents() 
            }
            .border(.blue)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.white)
    }
    @ViewBuilder func contents() -> some View {
        Image(systemName: "globe.americas.fill")
        Text("Hello, World!")
        Image(systemName: "globe.europe.africa.fill")
    }
}
struct SimpleHStack: Layout {
    let spacing: CGFloat
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let idealViewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let spacing = spacing * CGFloat(subviews.count - 1)
        let width = spacing + idealViewSizes.reduce(0) { $0 + $1.width }
        let height = idealViewSizes.reduce(0) { max($0, $1.height) }
        return CGSize(width: width, height: height)
    }
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        // ...
    }
}

你能够调查到,这两个图形的尺度是相同的。可是,这是由于咱们没有在 placeSubviews 办法中编写任何代码,一切的视图都放置在容器中心。假如你没有清晰的放置方位,这便是容器的默许视图。

在咱们的 sizeThatFits 办法中,咱们首要要核算每个视图的一切抱负尺度。咱们能够很简略的完成,由于子视图署理中有回来主张尺度的办法。

一旦咱们核算好一切抱负尺度,咱们能够经过添加子视图宽度和视图距离来核算容器尺度。从高度上来说,咱们的视图将会和最高子视图相同高。

你或许现已发觉到了咱们完全忽视了供给的尺度,咱们立刻回到这儿,现在,让咱们完成 placeSubviews

placeSubviews 办法

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache)

SwiftUI 经过不同的提案值重复调用 sizeThatFits 来测验过容器视图后,总算能够调用 placeSubviews 。在这儿咱们的方针是遍历子视图,确认它们的方位并放置。

除了 sizeThatFits 收到相同的参数外,placeSubviews 还得到一个 CGRect 参数 。bounds rect 具有咱们在 sizeThatFits 办法中要求的尺度。通常,矩形的原点是(0,0),可是你不应该这样假定,假如咱们正在组合布局,这个原点或许会有不同的值,咱们将在后边看到。

放置视图很简略,这多亏了具有放置办法的子视图署理。咱们必须供给视图的坐标,锚点(默许为中心)和主张尺度,以便子视图能够相应地制作自己。

struct SimpleHStack: Layout {
    // ...
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        for v in subviews {
            v.place(at: pt, anchor: .topLeading, proposal: .unspecified)
            pt.x += v.sizeThatFits(.unspecified).width + spacing
        }
    }
}

现在,还记得我之条件到的咱们疏忽了从父容器收到的主张了吗?这意味着 SimpleHStack 容器将会一向具有相同的巨细。不论供给什么,容器都会运用 .unspecified 核算尺度和放置,意味着容器一直具有抱负的尺度。在这个比如中容器的抱负尺度便是答应它以自己的抱负尺度放置一切子视图的尺度。假如咱们改动供给尺度看看会发生什么,在这个动画中红框代表供给的宽度。

SwiftUI 布局协议 - Part1

调查 SimpleHStack 是怎么忽视供给的尺度而且总是以抱负尺度制作自己,该尺度合适一切子视图的抱负尺度。

容器对齐

布局协议让咱们也为容器界说对齐指南。留意,这表明容器是作为一个全体怎么与其他视图对齐的。它对容器内的视图没有任何影响。

在下面这个比如中,咱们让 SimpleHStack 对齐第二个视图,但条件是容器与头部对齐(假如把 VStack 的对齐办法改为尾部对齐,你将不会看到任何特其他对齐办法)。

有赤色边框的视图是 SimpleHStack ,黑色边框的视图是规范的 HStack 容器,绿色边框的表示关闭的 VStack

SwiftUI 布局协议 - Part1

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            HStack(spacing: 5) {
                contents()
            }
            .border(.black)
            SimpleHStack(spacing: 5) {
                contents()
            }
            .border(.red)
            HStack(spacing: 5) {
                contents()
            }
            .border(.black)
        }
        .background { Rectangle().stroke(.green) }
        .padding()
        .font(.largeTitle)
    }
    @ViewBuilder func contents() -> some View {
        Image(systemName: "globe")
            .imageScale(.large)
            .foregroundColor(.accentColor)
        Text("Hello, world!")
    }
}
struct SimpleHStack: Layout {
    // ...
    func explicitAlignment(of guide: HorizontalAlignment, in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGFloat? {
        if guide == .leading {
            return subviews[0].sizeThatFits(proposal).width + spacing
        } else {
            return nil
        }
    }
}

优先布局

当咱们运用 HStack 时,咱们知道一切视图都在相等的竞赛宽度,除非它们有不同的布局优先级。一切的视图默许优先级都是0.0,可是,你能够经过调用 layoutPriority() 来修正布局优先级。

履行布局优先级是容器布局的责任,所以假如咱们创立一个新布局,假如相关的话,咱们需求添加一些逻辑去考虑布局优先级。咱们怎么做到这一点,这取决于咱们自己。尽管有更好的办法(咱们将在一分钟内解决它们),但你能够运用视图布局优先级的值赋予它们任何意义。例如,在上一个比如中,咱们将会依据视图优先级的值从左往右放置视图。

为了完成效果,无需对子视图集合进行迭代,只需求简略的经过优先级排序。

truct SimpleHStack: Layout {
    // ...
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        for v in subviews.sorted(by: { $0.priority > $1.priority }) {
            v.place(at: pt, anchor: .topLeading, proposal: .unspecified)
            pt.x += v.sizeThatFits(.unspecified).width + spacing
        }
    }
}

在下面这个比如中,蓝色圆圈将会首要出现,由于它比起其他视图具有较高的优先级

SwiftUI 布局协议 - Part1

SimpleHStack(spacing: 5) {
    Circle().fill(.yellow)
         .frame(width: 30, height: 30)
    Circle().fill(.green)
        .frame(width: 30, height: 30)
    Circle().fill(.blue)
        .frame(width: 30, height: 30)
        .layoutPriority(1)
}

LayoutValueKey

自界说值:LayoutValueKey

不主张将布局优先级用于优先级以外的内容,这或许使其他的用户不了解你的容器,乃至将来的你也不了解。走运的是,咱们有其他办法在视图中添加新值。这个值并不约束于 CGFloat ,它们能够具有任何类型(后边咱们将在其他比如中看到)。

咱们将重写前面的比如,运用一个新值,咱们把它称为 PreferredPosition。榜首件事便是创立一个符合LayoutValueKey 的类型,咱们只需求一个带有静态默许值的结构体。这个默许值用于没有指明详细值的时分。

struct PreferredPosition: LayoutValueKey {
    static let defaultValue: CGFloat = 0.0
}

这样,咱们的视图就具有了新的特点。为了设置这个值,咱们需求用到 layoutValue() ,为了读取这个值,咱们运用 LayoutValueKey 类型作为视图署理的下标:

SimpleHStack(spacing: 5) {
    Circle().fill(.yellow)
         .frame(width: 30, height: 30)
    Circle().fill(.green)
        .frame(width: 30, height: 30)
    Circle().fill(.blue)
        .frame(width: 30, height: 30)
        .layoutValue(key: PreferredPosition.self, value: 1.0)
}
struct SimpleHStack: Layout {
    // ...
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        let sortedViews = subviews.sorted { v1, v2 in
            v1[PreferredPosition.self] > v2[PreferredPosition.self]
        }
        for v in sortedViews {
            v.place(at: pt, anchor: .topLeading, proposal: .unspecified)
            pt.x += v.sizeThatFits(.unspecified).width + spacing
        }
    }
}

这段代码不像榜首段咱们写的 layoutPriority 那样整齐,可是用这两个扩展很简略解决:

extension View {
    func preferredPosition(_ order: CGFloat) -> some View {
        self.layoutValue(key: PreferredPosition.self, value: order)
    }
}
extension LayoutSubview {
    var preferredPosition: CGFloat {
        self[PreferredPosition.self]
    }
}

现在咱们能够像这样重写:

SimpleHStack(spacing: 5) {
    Circle().fill(.yellow)
         .frame(width: 30, height: 30)
    Circle().fill(.green)
        .frame(width: 30, height: 30)
    Circle().fill(.blue)
        .frame(width: 30, height: 30)
        .preferredPosition(1)
}
struct SimpleHStack: Layout {
    // ...
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        for v in subviews.sorted(by: { $0.preferredPosition > $1.preferredPosition }) {
            v.place(at: pt, anchor: .topLeading, proposal: .unspecified)
            pt.x += v.sizeThatFits(.unspecified).width + spacing
        }
    }
}

默许距离

到现在为止,咱们在初始化布局的时分 SimpleHStack 运用的都是咱们供给的距离值,可是,在你运用了 HStack 一阵子,你就会知道假如没有指明距离,视图将会依据不同的渠道和内容供给默许的距离。一个视图能够具有不同距离,假如周围是文本视图和周围是图画距离是不相同的。除此之外,每个边际都会有自己的偏好。

所以咱们应该怎么用 SimpleHStack 让它们行为共同?我曾提到过子视图署理是布局常识的宝藏,而且它们不会让人失望。它们有能够查询它们空间偏好的办法。

struct SimpleHStack: Layout {
    var spacing: CGFloat? = nil
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let idealViewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let accumulatedWidths = idealViewSizes.reduce(0) { $0 + $1.width }
        let maxHeight = idealViewSizes.reduce(0) { max($0, $1.height) }
        let spaces = computeSpaces(subviews: subviews)
        let accumulatedSpaces = spaces.reduce(0) { $0 + $1 }
        return CGSize(width: accumulatedSpaces + accumulatedWidths,
                      height: maxHeight)
    }
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        let spaces = computeSpaces(subviews: subviews)
        for idx in subviews.indices {
            subviews[idx].place(at: pt, anchor: .topLeading, proposal: .unspecified)
            if idx < subviews.count - 1 {
                pt.x += subviews[idx].sizeThatFits(.unspecified).width + spaces[idx]
            }
        }
    }
    func computeSpaces(subviews: LayoutSubviews) -> [CGFloat] {
        if let spacing {
            return Array<CGFloat>(repeating: spacing, count: subviews.count - 1)
        } else {
            return subviews.indices.map { idx in
                guard idx < subviews.count - 1 else { return CGFloat(0) }
                return subviews[idx].spacing.distance(to: subviews[idx+1].spacing, along: .horizontal)
            }
        }
    }
}

请留意,除了运用空间偏好外,你还能够告知系统容器视图的空间偏好。这样, SwiftUI 就会知道怎么将其与周围的视图分开,为此,你需求完成布局办法 spacing(subviews:cache:)。

布局特点和 Spacer()

布局协议有一个你能够完成的名为 layoutProperties 的静态特点。依据文档, LayoutProperties 包括布局容器的特定布局特点。在写这篇文章时,只界说了一个特点:stackOrientation

struct MyLayout: Layout {
    static var layoutProperties: LayoutProperties {
        var properties = LayoutProperties()
        properties.stackOrientation = .vertical
        return properties
    }
    // ...
}

stackOrientation 告知是像 Spacer 这样的视图是否应该在横轴或纵轴上打开。例如,假如你查看 Spacer 视图署理的最小,抱负和最大尺度,这便是它在不同容器回来的成果,每个容器都有不同的stackOrientation

stackOrientation minimum ideal maximum
.horizontal 8.0 0.0 8.0 0.0 .infinity 0.0
.vertical 0.0 8.0 0.0 8.0 0.0 .infinity
.none or nil 8.0 8.0 8.0 8.0 .infinity .infinity

布局缓存

布局缓存是常被用来进步咱们布局功能的一种办法。可是,它还有其他用途。只需求把它看作是一个存储数据的当地,咱们需求在 sizeThatFitsplaceSubviews 调用中持久保存。首要想到的是进步功能,可是,它关于和其他子视图布局同享信息也是非常有用的。当咱们讲到组合布局的比如时,咱们将对此进行评论,但让咱们从了解怎么运用缓存进步功能开端。

SwiftUI 的布局过程中会多次调用 sizeThatFitsplaceSubviews 办法。这个结构测验咱们的容器的灵活性,以确认全体视图层级结构的终究布局。为了进步布局容器功能, SwiftUI 让咱们完成了一个缓存, 只有当容器内的至少一个视图改动时才更新缓存。由于 sizeThatFitsplaceSubviews 都能够为单个视图更改时多次调用,所以保存不需求为每次调用而从头核算的数据缓存是有意义的。

运用缓存不是必须的。事实上,许多时分你不需求。无论怎么,在没有缓存的状况下编写咱们的布局更简略一点,当咱们今后需求时再添加。 SwiftUI 现已做了一些缓存。例如,从子视图署理取得的值会主动存储在缓存中。相同的参数的重复调用将会运用缓存成果。在 makeCache(subviews:) 文档页面,有一个很好的评论关于你或许想要完成自己的缓存的原因。

一起也要留意, sizeThatFitsplaceSubviews 中的缓存参数有一个是 inout 参数,这意味着你也能够用这个函数更新缓存存储,咱们将会看到它在 RecursiveWheel 比如中特别有协助。

例如,这儿是运用更新缓存的 SimpleHStack 。下面是咱们需求做的:

  • 创立一个将包括缓存数据的类型。在本例中,我把它叫做 CacheData ,它将会核算视图间的最大高度和空间。
  • 完成 makeCache(subviews:) 创立缓存。
  • 可选的完成 updateCache(subviews:),这个办法会在检测到更改时调用。它供给了默许完成,基本上经过调用 makeCache 从头创立缓存。
  • 记住要更新 sizeThatFitsplaceSubviews中的缓存参数类型。
struct SimpleHStack: Layout {
    struct CacheData {
        var maxHeight: CGFloat
        var spaces: [CGFloat]
    }
    var spacing: CGFloat? = nil
    func makeCache(subviews: Subviews) -> CacheData {
        return CacheData(maxHeight: computeMaxHeight(subviews: subviews),
                         spaces: computeSpaces(subviews: subviews))
    }
    func updateCache(_ cache: inout CacheData, subviews: Subviews) {
        cache.maxHeight = computeMaxHeight(subviews: subviews)
        cache.spaces = computeSpaces(subviews: subviews)
    }
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize {
        let idealViewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let accumulatedWidths = idealViewSizes.reduce(0) { $0 + $1.width }
        let accumulatedSpaces = cache.spaces.reduce(0) { $0 + $1 }
        return CGSize(width: accumulatedSpaces + accumulatedWidths,
                      height: cache.maxHeight)
    }
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        for idx in subviews.indices {
            subviews[idx].place(at: pt, anchor: .topLeading, proposal: .unspecified)
            if idx < subviews.count - 1 {
                pt.x += subviews[idx].sizeThatFits(.unspecified).width + cache.spaces[idx]
            }
        }
    }
    func computeSpaces(subviews: LayoutSubviews) -> [CGFloat] {
        if let spacing {
            return Array<CGFloat>(repeating: spacing, count: subviews.count - 1)
        } else {
            return subviews.indices.map { idx in
                guard idx < subviews.count - 1 else { return CGFloat(0) }
                return subviews[idx].spacing.distance(to: subviews[idx+1].spacing, along: .horizontal)
            }
        }
    }
    func computeMaxHeight(subviews: LayoutSubviews) -> CGFloat {
        return subviews.map { $0.sizeThatFits(.unspecified) }.reduce(0) { max($0, $1.height) }
    }
}

假如咱们每次调用其间一个布局函数都打印出一条信息,咱们将会取得的下面的成果。如你所见,缓存将会核算两次,可是其他办法将会被调用25次!

makeCache called <<<<<<<<
sizeThatFits called
sizeThatFits called
sizeThatFits called
sizeThatFits called
placeSubiews called
placeSubiews called
updateCache called <<<<<<<<
sizeThatFits called
sizeThatFits called
sizeThatFits called
sizeThatFits called
placeSubiews called
placeSubiews called
sizeThatFits called
sizeThatFits called
placeSubiews called
sizeThatFits called
placeSubiews called
placeSubiews called
sizeThatFits called
placeSubiews called
placeSubiews called
sizeThatFits called
sizeThatFits called
sizeThatFits called
placeSubiews called

留意除了运用缓存参数进步功能,它们也有其他用途。咱们将会在第二部分的 RecursiveWheel比如中再议论。

高超的伪装者

正如我现已提到的,布局协议没有选用视图协议。那么咱们为什么一向在 ViewBuilder中运用布局容器,就好像它们是视图相同?事实证明,当你用代码放置你的布局时,会有一个系统函数调用来发生视图。那这个函数叫什么呢?你或许现已猜到了:

func callAsFunction<V>(@ViewBuilder _ content: () -> V) -> some View where V : View

由于言语的添加(在 SE-0253中有描绘和解说),被命名为 callAsFunction 的办法是特其他。当咱们运用一个类型实例时,这些办法会像一个函数相同被调用。在这种状况下,咱们或许会感到困惑,由于咱们似乎仅仅在初始化类型,而实际上,咱们做的更多。咱们初始化类型然后调用 callAsFunction,由于 callAsFunction的回来值是一个视图,所以咱们能够把它放到咱们的 SwiftUI 代码中。

SimpleHStack(spacing: 10).callAsFunction({
    Text("Hello World!")
})
// Thanks to SE-0253 we can abbreviate it by removing the .callAsFunction
SimpleHStack(spacing: 10)({
    Text("Hello World!")
})
// And thanks to trailing closures, we end up with:
SimpleHStack(spacing: 10) {
    Text("Hello World!")
}

假如布局没有初始化参数,代码乃至能够更简略:

SimpleHStack().callAsFunction({
    Text("Hello World!")
})
// Thanks to SE-0253 we can abbreviate it by removing the .callAsFunction
SimpleHStack()({
    Text("Hello World!")
})
// And thanks to single trailing closures, we end up with:
SimpleHStack {
    Text("Hello World!")
}

所以你理解了,布局类型并不是视图,可是当你在 SwiftUI 中运用它们的时分它们就会发生一个视图。这个技巧(callAsFunction)还能够切换到不同布局,一起保持视图的标识,就像接下来的部分描绘的那样。

运用 AnyLayout 切换布局

布局容器的另一个风趣的当地,咱们能够修正容器的布局, SwiftUI 会友爱地用动画处理两者的切换。不需求额定的代码!那是由于视图会识别标识而且保护, SwiftUI 将这个行为认为是视图的改动,而不是两个独自的视图。

SwiftUI 布局协议 - Part1

struct ContentView: View {
    @State var isVertical = false
    var body: some View {
        let layout = isVertical ? AnyLayout(VStackLayout(spacing: 5)) : AnyLayout(HStackLayout(spacing: 10))
        layout {
            Group {
                Image(systemName: "globe")
                Text("Hello World!")
            }
            .font(.largeTitle)
        }
        Button("Toggle Stack") {
            withAnimation(.easeInOut(duration: 1.0)) {                
                isVertical.toggle()
            }
        }
    }
}

三元运算符(条件?成果1:成果2)要求两个表达式回来同一类型。AnyLayout 在这儿发挥了效果。

留意:假如你观看过 2022 WWDC Layout session,你或许看见过苹果工程师运用的比如,但运用的是 VStack 替代 VStackLayoutHStack 替代 HStackLayout 。那现已过期了。在 beta3 过后, HStackVStack 不再选用布局协议,而且他们添加了 VStackLayoutHStackLayout 布局(分别由HStackVStack 运用),他们还添加了 ZStackLayoutGridLayout

结语

假如咱们停下来考虑每一种或许的状况,编写布局容器或许会让咱们寸步难行。有的视图运用尽或许多的空间,有的视图会尽量适应,还有的将会运用的更少,等等。当然还有布局优先级,当多个视图需求竞赛同一个空间会变得愈加艰难。可是,这项使命或许并不像看起来艰巨。咱们或许会运用自己的布局,而且或许会提早知道咱们的容器会有什么类型的视图。例如,假如你计划只用方形图片或许文本视图来运用自己的容器,或许你知道你的容器会有详细尺度,或许你确认你一切的视图都具有相同的优先级,等等。这些信息都能够大大的简化使命。即使你不能有这种奢求来做这种假定,它也或许是开端编码的好当地,让你的布局在一些状况下作业,然后开端为更杂乱的状况添加代码。

在本文的第二部分,咱们将开端探究一些风趣的论题,比如自界说动画,双向自界说值,递归布局或布局组合。我还会介绍一个非常有用的调试东西,即使你没有创立自己的布局也能够运用。

本文正在参与「金石计划 . 分割6万现金大奖」