使用 SwiftUI 实现多层画布布局
添加多图层下的安全边距
在构建类似多图层的应用时,我们可能需要确保所有内容均在背景层的安全区域内部,因此需要为背景层添加安全边距 —— 类似于 Canva 或 Figma 中的效果。
类似于下面这样的效果:
- 设置背景层横向、纵向的 Padding
- 确保子组件内容不超出 Padding
使用 SwiftUI 实现的方式非常简单:
- 创建一个 VStack(或者使用 Group),在 VStack 上添加 Padding。不使用 ZStack。
- 如果要修改背景颜色,在 VStack 上使用
.background
进行填充。
需要特别注意的是,在 iOS 平台,可以使用固定的 Padding。但是在 Mac 平台上,因为窗口是动态变化的,因此推荐基于窗口宽度、高度动态计算 Padding,例如:
.padding(
.vertical, canvas.height * PageConfig.Layout.verticalPadding
)
.padding(
.horizontal, canvas.width * PageConfig.Layout.horizontalPadding
)
让画布中心对齐
// 添加 ZStack 作为主容器,确保内容居中
ZStack {
// 背景色(可选)
Color(uiColor: .systemGroupedBackground)
.ignoresSafeArea()
VStack(这是画布)
}
使用 AnyLayout 动态切换布局样式
如果需要根据条件判断,动态切换 HStack 和 VStack。
一种方式是使用 if/else
来判断,但这样有两个问题:
- 造成代码冗余。
- if else 会涉及视图的销毁和重建,这会导致不必要的性能开销,而且在动画过渡上也会有影响。
从 iOS16 开始,SwiftUI 新推出了 AnyLayout 组件。
注意:使用HStackLayout
和VStackLayout
,而不是HStack
和VStack
。否则 Xcode 无法编译通过。
let layout =
isHorizontal
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
使用 Layout 协议实现复杂的自定义布局
Layout 协议基础
Layout
协议是 SwiftUI 提供的一个强大的自定义布局协议,它允许我们创建完全自定义的布局行为。
主要包含两个必须实现的方法:
func sizeThatFits()
sizeThatFits 方法负责计算子视图所需的大小(宽度和高度),并返回该大小。它根据父视图传入的约束来决定如何调整子视图的大小。
func placeSubviews()
placeSubviews 方法负责在布局中根据给定的边界和计算的大小来放置子视图。它会使用 sizeThatFits 返回的大小,将每个子视图放置到适当的位置。
protocol Layout {
// 1. 计算布局大小
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Self.Cache
) -> CGSize
// 2. 放置子视图
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Self.Cache
)
}
sizeThatFits 和 placeSubviews 的执行顺序和关系
sizeThatFits 先执行
- 主要职责:确定整个布局的大小
- 返回:布局所需的总体尺寸(CGSize)
- 时机:在布局过程的计算阶段
placeSubviews 后执行
- 主要职责:确定每个子视图的具体位置
- 使用:使用 sizeThatFits 返回的尺寸作为可用空间
- 时机:在实际渲染阶段
sizeThatFits()
sizeThatFits()
是 Layout 协议的一个必需方法,用于确定布局所需的尺寸。
下面这个 sizeThatFits 方法,实际上实现了
func sizeThatFits(
proposal: ProposedViewSize, subviews: Subviews, cache: inout ()
) -> CGSize {
// 如果没有子视图(subviews 为空),直接返回 CGSize.zero(宽度和高度都为0)。这是一个安全检查,避免对空视图进行不必要的计算。
guard !subviews.isEmpty else { return .zero }
return proposal.replacingUnspecifiedDimensions()
}
placeSubviews()
placeSubviews()
方法,主要通过
-
subviews[0].place()
方法来设置每个子视图的位置和大小。 - 设置锚点(anchor)来控制对齐方式。
使用示例:指定比例分配空间
实现此目的的核心代码如下:
通过 bounds 获取父组件的尺寸和位置:
- bounds.width: 父组件提供的总宽度
- bounds.height: 父组件提供的总高度
- bounds.minX: 起始 X 坐标(通常是 0)
- bounds.minY: 起始 Y 坐标(通常是 0)
- bounds.midX: 水平中心点
- bounds.midY: 垂直中心点
然后,通过 proposal 参数为每个 subview 分配 width
和 height
。
使用示例:指定对齐方式
可以通过结合使用 at
和 anchor
参数设置对齐方式:
使用示例:填满 subview 最大空间
在 Layout 中,实际上不存在所谓的——填充最大空间。
在下面这个代码中,我们通用设置 proposal 的 width 和 height 显式设置了容器的尺寸,但仍然需要在子容器中通过使用 .scaleToFit 这样的修饰器来让子组件填充。
动态调整字体大小
minimumScaleFactor
最小 Scale Factor (缩放因子) 为 0.5 的标签
以实际字体的一半的字体大小绘制其文本(如果需要),以适合文本输入字段旁边的空间。
HStack {
Text("This is a very long label:")
.lineLimit(1)
.minimumScaleFactor(0.5)
TextField("My Long Text Field", text: $myTextField)
.frame(width: 250, height: 50, alignment: .center)
}