SwiftUI之深入解析布局协议

一、什么是布局协议?

  • 采用布局协议类型的任务,是告诉 SwiftUI 如何放置一组视图,需要多少空间。这类型常常被作为视图容器,虽然布局协议是 2022 年新推出的(至少公开来说),但是我们在第一天使用 SwiftUI 的时候就在使用了,当每次使用 HStack 或者 VStack 放置视图时都是如此。
  • 请注意至少到现在,布局协议不能创建懒加载容器,比如 LazyHStack 或 LazyVStack,懒加载容器是指那些只在滚入屏幕时渲染,滚出到屏幕外就停止渲染的视图。
  • Layout 类型不是视图,例如,它们没有视图拥有的 body 属性。但是不用担心,目前为止你可以认为它们就是视图并且像视图一样使用它们,这个框架使用了漂亮的 Swift 语言技巧使你的布局代码在向 SwiftUI 中插入时产生一个透明视图 。

二、视图层次结构的族动态

  • 在开始布局代码之前,重新审视一下 SwiftUI 框架的核心,就像我在以前的文章 SwiftUI之深入解析Frame Behaviors 所描述的的那样,在布局过程中,父视图给子视图提供一个尺寸,但最终还是由子视图决定如何绘制自己。然后,它将此传达给父视图,以便采取相应的动作。

① 如果子视图需求小于提供的视图

  • 如下所示,考虑文本视图,提供比需要绘制文字更多的空间:

在这里插入图片描述

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 方法),它们需要多少空间就要占用多少空间,下面示例的图片是 300×300,这也是它们需要绘制自己需要的空间,然而,通过调用 frame(width:100) 子视图只得到了 100pt,父视图就没有办法只能听从子视图的做法吗?并非如此,子视图仍然会使用 300pt 绘制,但是父视图将会布局其他视图,就好像子视图只有 100pt 宽度一样。结果呢,我们将会有一个超出边界的子视图,但是周围的视图不会被图片额外使用的空间影响,黑色边框展示的空间是提供给图片的:

在这里插入图片描述

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 告诉视图使用其理想尺寸,无论提供的是多少。

三、布局实现

① ProposedViewSize

  • 创建一个布局类型需要实现至少两个方法,sizeThatFits 和 placeSubviews,这些方法接收一些新类型作为参数:ProposedViewSize 和 LayoutSubview。
  • 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

  • sizeTheFits 和 placeSubviews 方法也接收一个 Layout.Subviews 参数,它是一个 LayoutSubview 元素的合集。每个视图都有一个,作为父视图的直接后代。
  • 尽管有这个名称,但它的类型不是视图,而是一个代理,可以查询这些代理去了解正在布局的各个视图的布局信息。例如,自 SwiftUI 推出以来,我们第一次可以直接查询到视图最小,理想和最大的尺寸,或者可以获得每个视图的布局优先级以及其他有趣的值。

③ sizeThatFits 方法

  • SwiftUI 将会调用 sizeThatFits 方法决定布局容器的尺寸,当写这个方法我们应该认为既是父视图又是子视图:当作为父视图时需要询问子视图的尺寸,当是子视图时,要基于子视图的回复告诉父视图需要的尺寸,这个方法将会收到建议尺寸,一个子视图代理的合集和一个缓存,最后一个参数可能用以提高布局和一些其他高级应用的性能。
  • 当 sizeThatFits 方法在给定维度中(即宽度或高度)收到的建议尺寸为 nil 时,应该返回容器的理想尺寸。当收到的建议尺寸为 0.0 时,应该返回容器的最小尺寸,当收到的建议尺寸为 .infinity 时,应该返回容器的最大尺寸。
  • 注意 sizeThatFits 可能通过不同提案多次调用来测试容器的灵活性,提案可以是上述每个维度案例的任意组合。例如,可能会得到一个带有 ProposedViewSize(width: 0.0, height: .infinity) 的调用。
func sizeThatFits(proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache) -> CGSize
  • 通过创建一个基础的 HStack 开始,命名为 SimpleHStack 。为了比较两者,创建一个标准的 HStack (蓝色)视图放置在SimpleHStack (绿色)上方,第一次尝试,我们将会实现 sizeThatFits ,但是同时将会使其他需要的方法(placeSunviews)为空。
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: CGFloatfunc 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 方法

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 计算尺寸和放置,意味着容器始终拥有理想的尺寸。在这个例子中容器的理想尺寸就是允许它以自己的理想尺寸放置所有子视图的尺寸,如果改变提供尺寸看看会发生什么,在这个动画中红框代表提供的宽度。

在这里插入图片描述

  • 观察 SimpleHStack 是如何忽视提供的尺寸并且总是以理想尺寸绘制自己,该尺寸适合所有子视图的理想尺寸。

四、容器对齐

  • 布局协议也为容器定义对齐指南,这表明容器是作为一个整体如何与其余视图对齐的,它对容器内的视图没有任何影响。
  • 如下所示的例子,让 SimpleHStack 对齐第二个视图,但前提是容器与头部对齐(如果把 VStack 的对齐方式改为尾部对齐,将不会看到任何特殊的对齐方式)。有红色边框的视图是 SimpleHStack ,黑色边框的视图是标准的 HStack 容器,绿色边框的表示封闭的 VStack。

在这里插入图片描述

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}}
}
  • 在如下所示的例子中,蓝色圆圈将会首先出现,因为它比起其他视图拥有较高的优先级:

在这里插入图片描述

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

  • 不建议将布局优先级用于优先级以外的内容,这可能使其他的用户不理解你的容器,甚至将来的你也不理解。幸运的是,我们有别的方法在视图中添加新值,这个值并不限制于 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 inv1[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? = nilfunc 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 inguard 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:
stackOrientationminimumidealmaximum
.horizontal8.0 × 0.08.0 × 0.0.infinity × 0.0
.vertical0.0 × 8.00.0 × 8.00.0 × .infinity
.none or nil8.0 × 8.08.0 × 8.0.infinity × .infinity

九、布局缓存

  • 布局缓存是常被用来提高布局性能的一种方式。然而,它还有别的用途,只需要把它看作是一个存储数据的地方,需要在 sizeThatFits 和 placeSubviews 调用中持久保存。首先想到的是提高性能,但是它对于和其他子视图布局共享信息也是非常有用的。
  • 在 SwiftUI 的布局过程中会多次调用 sizeThatFits 和 placeSubviews 方法,这个框架测试我们的容器的灵活性,以确定整体视图层级结构的最终布局。为了提高布局容器性能, SwiftUI 实现了一个缓存, 只有当容器内的至少一个视图改变时才更新缓存。因为 sizeThatFits 和 placeSubviews 都可以为单个视图更改时多次调用,因此保留不需要为每次调用而重新计算的数据缓存是有意义的。
  • 使用缓存不是必须的。事实上,很多时候你不需要。无论如何,在没有缓存的情况下编写布局更简单一点,当以后需要时再添加。SwiftUI 已经做了一些缓存。例如,从子视图代理获得的值会自动存储在缓存中,相同的参数的反复调用将会使用缓存结果,在 makeCache(subviews:) 文档页面,有一个很好的讨论关于可能想要实现自己的缓存的原因。
  • 同时也要注意, sizeThatFits 和 placeSubviews 中的缓存参数有一个是 inout 参数,这意味着也可以用这个函数更新缓存存储,将会看到它在 RecursiveWheel 例子中特别有帮助。例如,这里是使用更新缓存的 SimpleHStack 。下面是需要做的:
    • 创建一个将包含缓存数据的类型,在本例中,我把它叫做 CacheData,它将会计算视图间的最大高度和空间。
    • 实现 makeCache(subviews:) 创建缓存。
    • 可选的实现 updateCache(subviews:),这个方法会在检测到更改时调用,它提供了默认实现,基本上通过调用 makeCache 重新创建缓存。
    • 记住要更新 sizeThatFits 和 placeSubviews 中的缓存参数类型。
struct SimpleHStack: Layout {struct CacheData {var maxHeight: CGFloatvar spaces: [CGFloat]}var spacing: CGFloat? = nilfunc 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 inguard 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

十、高明的伪装者

  • 正如已经提到的,布局协议没有采用视图协议,那么为什么一直在 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 将这个行为认为是视图的改变,而不是两个单独的视图。

在这里插入图片描述

struct ContentView: View {@State var isVertical = falsevar 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 代替 VStackLayout 和 HStack 代替 HStackLayout,那已经过时了。在 beta3 过后, HStack 和 VStack 不再采用布局协议,并且它们添加了 VStackLayout 和 HStackLayout 布局(分别由 HStack 和 VStack 使用),它们还添加了 ZStackLayout 和 GridLayout。

十二、总结

  • 如果我们停下来考虑每一种可能的情况,编写布局容器可能会让我们举步维艰。有的视图使用尽可能多的空间,有的视图会尽量适应,还有的将会使用的更少等等。当然还有布局优先级,当多个视图需要竞争同一个空间会变得更加艰难。
  • 然而,这项任务可能并不像看起来艰巨,我们可能会使用自己的布局,并且可能会提前知道我们的容器会有什么类型的视图。例如,如果你打算只用方形图片或者文本视图来使用自己的容器,或者你知道你的容器会有具体尺寸,或者你确定你所有的视图都拥有一样的优先级等,这些信息都可以大大的简化任务。即使你不能有这种奢望来做这种假设,它也可能是开始编码的好地方,让你的布局在一些情况下工作,然后开始为更复杂的情况添加代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/236385.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot项目如何防止反编译?

SpringBoot项目如何防止反编译&#xff1f; 场景方案项目操作启动方式反编译效果绑定机器启动 场景 最近项目要求部署到其他公司的服务器上&#xff0c;但是又不想将源码泄露出去。要求对正式环境的启动包进行安全性处理&#xff0c;防止客户直接通过反编译工具将代码反编译出…

影视仓最新配置接口2024tvbox源配置地址

影视仓是在TVBox开源代码基础上开发的优质版本&#xff0c;安装后需要配置接口才能正常使用。影视仓"内置版"是开发者做的资源内置化修改版本&#xff0c;不用自行设置接口&#xff0c;安装后即可使用。 影视仓的接口配置方法与TVBOX一样&#xff0c;区别在于影视仓…

TCP/IP 网络模型

TCP/IP 网络通常是由上到下分成 4 层&#xff0c;分别是应用层&#xff0c;传输层&#xff0c;网络层和网络接口层。 应用层 应用层专注于为用户提供应用功能&#xff0c;比如 HTTP、FTP、Telnet、DNS、SMTP等。我们电脑或手机使用的应用软件都是在应用层实现。应用层是不用去关…

YOLOv6s,map值打印成两位小数(原本是显示0.538,变成显示为53.79)

显示结果 更改前&#xff1a; 更改后&#xff1a; 方法 将tools/eval.py中的--do_pr_metric后面改为defaultTrue即可打印出map值原本是显示0.538&#xff0c;变成显示为53.79&#xff0c;方法为&#x1f447; 在YOLOv6-main/yolov6/core/evaler.py中做如下更改&#xff1a…

揭秘HTTP协议:深入了解互联网通信的核心!

文章目录 HTTPHTTP的消息结构HTTP 常用请求方法HTTP 状态码 HTTP HTTP 是超文本传输协议&#xff0c;HTTP是缩写&#xff0c;全称是 HyperText Transfer Protocol 超文本指的是 HTML、css、JavaScript和图片等&#xff0c;HTTP的出现就是为方便接收和发布超HTML页面&#xff0c…

20240113斐波那切数列

代码 def fibonacci(n):fib_list [0, 1] # 初始的斐波那契数列&#xff0c;包含0和1while len(fib_list) < n:next_number fib_list[-1] fib_list[-2]fib_list.append(next_number)return fib_list[:n]# 示例&#xff1a;计算前10个斐波那契数 n 10 result fibonacci…

预训练中文GPT2(包括重新训练tokenizer)

训练数据 1.json后缀的文件 2.数据是json line格式&#xff0c;一行一条json 3. json结构如下 {"content": "①北京和上海户籍的游客可获得韩国多次签证&#xff1b;②“整容客”可以不经由韩国使领馆、直接在网上申请签证&#xff1b;③中泰免签的实施日期…

在加载第三方库过程中,无法加载到库的问题(使用readelf, patchelf命令)

无法加载到库问题 问题及分析过程readelf 命令patchelf命令 问题及分析过程 在开发一个程序过程中&#xff0c;需要加载第三方库iTapTradeAPI, 在CMakeList.txt中已经设置了CMAKE_INSTALL_RPATH&#xff0c;但是发布到生产之后由于目录问题无法加载到libiTapTradeAPI库了 下面…

韩语发音干货,零基础韩语学习,柯桥韩语知识点之发音规律

01.连音化 当收音遇到以元音为首音的音节时&#xff0c;收音要和该元音相连发音。 예: 독일[도길] 밥을 [바블] 우산이[우사니] 읽어요[일거요] 02.送气 ㄱ/ㄷ/ㅂ/ㅈ遇到ㅎ,送气化读成ㅋ/ㅌ/ㅍ/ㅊ 예: 어떻게[어떠케] 좋다[조타] 많지만[만치만] 백화점[배콰…

1.12 力扣中等图论

797. 所有可能的路径 - 力扣&#xff08;LeetCode&#xff09; 给你一个有 n 个节点的 有向无环图&#xff08;DAG&#xff09;&#xff0c;请你找出所有从节点 0 到节点 n-1 的路径并输出&#xff08;不要求按特定顺序&#xff09; graph[i] 是一个从节点 i 可以访问的所有节…

vscode+opencv基础用法学习1

案例1&#xff1a;读取图片信息 如果是使用云服务器的话&#xff0c;由于图形界面的问题&#xff0c;使用cv::show来显示图片会报错 // 图片的读取和显示 // 导入opencv头文件 #include "opencv2/opencv.hpp" #include <iostream>int main(int argc, char** …

【Java 设计模式】设计原则

文章目录 ✨单一职责原则&#xff08;SRP&#xff09;✨开放/封闭原则&#xff08;OCP&#xff09;✨里氏替换原则&#xff08;LSP&#xff09;✨依赖倒置原则&#xff08;DIP&#xff09;✨接口隔离原则&#xff08;ISP&#xff09;✨合成/聚合复用原则&#xff08;CARP&#…

动态pv策略和组件

pv和pvc&#xff0c;存储卷&#xff1a; 存储卷&#xff1a; emptyDir 容器内部&#xff0c;随着pod销毁&#xff0c;emptyDir也会消失 不能做数据持久化 hostPath&#xff1a;持久化存储数据 可以和节点上的目录做挂载。pod被销毁了数据还在 NFS&#xff1a;一台机器&am…

Java常用类---日期时间类

日期时间类 Date类 简介 在Java中&#xff0c;Date类用来封装当前的日期和时间。Date类提供两个构造函数来初始化对象&#xff0c;如下所示。 通过Date() 使用当前日期和时间来初始化对象。 通过Date(long millisec) 来初始化对象&#xff0c;其中的参数是从1970年1月1日起…

学习笔记——C++中的循环结构 while语句

while循环语句 作用&#xff1a;满足循环条件&#xff0c;执行循环语句 语法&#xff1a;while&#xff08;循环条件&#xff09;{循环语句} 解释&#xff1a;只要循环条件的结果为真&#xff0c;就执行循环语句 以打印0-9这十个数字为例&#xff0c;特别需要注意的是&…

【python】爬取豆瓣电影排行榜Top250存储到Excel文件中【附源码】

英杰社区https://bbs.csdn.net/topics/617804998 一、背景 近年来&#xff0c;Python在数据爬取和处理方面的应用越来越广泛。本文将介绍一个基于Python的爬虫程 序&#xff0c;用于抓取豆瓣电影Top250的相关信息&#xff0c;并将其保存为Excel文件。 程序包含以下几个部…

大模型学习产品,一个月顶一年 | 对话网易有道周枫

OpenAI CEO奥特曼曾表示&#xff1a;“AI女友只不过是一个美丽的陷阱&#xff0c;AI教育才是最应该去发力的一个领域。” 场景的确定性&#xff0c;是OpenAI等一众公司尤为重视教育领域的原因所在。教与学是教育场景中的核心&#xff0c;但再将两个字进行拆解&#xff0c;教学…

OpenAI推出GPT商店和ChatGPT Team服务

&#x1f989; AI新闻 &#x1f680; OpenAI推出GPT商店和ChatGPT Team服务 摘要&#xff1a;OpenAI正式推出了其GPT商店和ChatGPT Team服务。用户已经创建了超过300万个ChatGPT自定义版本&#xff0c;并分享给其他人使用。GPT商店集结了用户为各种任务创建的定制化ChatGPT&a…

Ubuntu 卸载重装 Nvidia 显卡驱动

问题描述 我使用 airsim 的时候&#xff0c;发现 UE4 没法使用显卡&#xff0c;导致非常卡顿 输入 nvidia-smi 有显卡型号等信息的输出&#xff0c;但是进程 process 里面没有显示 airsim 和其他软件占用显卡情况 因此&#xff0c;我选择了卸载重装 一.卸载旧版本的驱动 …

error: undefined reference to ‘cv::imread(std::__ndk1::basic_string<char

使用android studio编译项目时&#xff0c;由于用到了 cv::imread&#xff08;&#xff09;函数&#xff0c;编译时却报错找不到该函数的定义。 cv::imread一般是在highgui.hpp中定义&#xff0c;因此我加上了该头文件&#xff1a; #include “opencv2/highgui/highgui.hpp” 但…