布局调整
SwiftUI 的布局系统提供了丰富的修饰符,用于对视图的尺寸、位置、对齐、内边距等布局参数进行精细调整。本文详细总结了布局调整的核心概念和 API。
添加内边距
内边距(Padding)在视图周围添加空白空间,是最常用的布局调整手段之一。
padding(_:)
添加默认内边距,在所有边缘添加系统标准间距。
Text("Hello, World!")
.padding()
.background(Color.gray.opacity(0.2))padding(::)
为指定边缘添加自定义内边距。
struct PaddingExample: View {
var body: some View {
VStack(spacing: 20) {
Text("顶部内边距")
.padding(.top, 40)
.background(Color.blue.opacity(0.2))
Text("水平内边距")
.padding(.horizontal, 30)
.background(Color.green.opacity(0.2))
Text("所有边 20pt")
.padding(.all, 20)
.background(Color.orange.opacity(0.2))
}
}
}可用的边缘选项:
.top,.bottom,.leading,.trailing.horizontal=.leading+.trailing.vertical=.top+.bottom.all= 所有边缘
scenePadding
为视图添加场景级别的内边距,适应不同平台的标准间距。
VStack {
Text("内容区域")
}
.scenePadding()ScenePadding 提供了预定义的内边距类型:
.minimum:最小场景内边距.navigationBar:导航栏级别的内边距
safeAreaPadding
在安全区域内添加内边距,不会被安全区域裁剪。
ScrollView {
VStack {
ForEach(0..<20) { index in
Text("Item \(index)")
}
}
.safeAreaPadding(.horizontal, 20)
.safeAreaPadding(.bottom, 30)
}safeAreaPadding 与 padding 的区别:safeAreaPadding 在安全区域内部添加内边距,而 padding 可能会被安全区域覆盖。
影响视图尺寸
Frame 修饰符用于控制视图的尺寸,提供了固定尺寸和灵活尺寸两种模式。
frame(width:height:alignment:)
设置视图的固定宽度和高度。
struct FixedFrameExample: View {
var body: some View {
VStack(spacing: 20) {
Text("固定尺寸")
.frame(width: 200, height: 100)
.background(Color.blue)
Text("仅固定宽度")
.frame(width: 150)
.background(Color.green)
Text("右对齐")
.frame(width: 200, height: 50, alignment: .trailing)
.background(Color.orange.opacity(0.3))
}
}
}frame(minWidth:idealWidth:maxWidth:...)
设置灵活尺寸,允许视图在指定范围内自适应。
struct FlexibleFrameExample: View {
var body: some View {
VStack(spacing: 20) {
Text("短文本")
.frame(minWidth: 100, maxWidth: 300)
.background(Color.blue.opacity(0.2))
Text("这是一段很长的文本,会在最大宽度限制下自动换行")
.frame(minWidth: 100, maxWidth: 300)
.background(Color.green.opacity(0.2))
Text("全宽")
.frame(maxWidth: .infinity)
.background(Color.orange.opacity(0.2))
}
.padding()
}
}常用模式:
maxWidth: .infinity:占满可用宽度minWidth: 0, maxWidth: .infinity:灵活宽度,可以收缩到 0
containerRelativeFrame
相对于容器的尺寸设置视图大小,常用于 ScrollView 和 TabView。
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(0..<5) { index in
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue.opacity(Double(index) * 0.2 + 0.2))
.containerRelativeFrame(.horizontal)
.overlay(Text("Page \(index + 1)"))
}
}
}
.scrollTargetBehavior(.paging)也可以使用自定义计算:
Text("1/3 宽度")
.containerRelativeFrame(.horizontal) { length, axis in
length / 3
}
.background(Color.blue.opacity(0.2))fixedSize
让视图使用其理想尺寸,而不是父视图提供的尺寸。
struct FixedSizeExample: View {
var body: some View {
VStack(spacing: 20) {
Text("这段文本会被截断...")
.frame(width: 100)
.background(Color.red.opacity(0.2))
Text("这段文本使用理想尺寸")
.fixedSize()
.frame(width: 100)
.background(Color.green.opacity(0.2))
Text("仅水平方向使用理想尺寸,垂直方向可以换行")
.fixedSize(horizontal: true, vertical: false)
.frame(width: 100)
.background(Color.blue.opacity(0.2))
}
}
}fixedSize() 常用于防止文本被截断,或让视图保持其内容的自然尺寸。
layoutPriority
设置视图在布局时的优先级,数值越大优先级越高。
HStack {
Text("高优先级文本不会被压缩")
.layoutPriority(1)
.background(Color.blue.opacity(0.2))
Text("低优先级文本会被优先压缩")
.background(Color.green.opacity(0.2))
}
.frame(width: 200)调整视图位置
位置调整修饰符用于改变视图在其父容器中的位置。
position
设置视图中心点的绝对位置。
ZStack {
Color.gray.opacity(0.1)
Circle()
.fill(Color.blue)
.frame(width: 50, height: 50)
.position(x: 100, y: 100)
Circle()
.fill(Color.red)
.frame(width: 50, height: 50)
.position(CGPoint(x: 200, y: 150))
}
.frame(height: 300)position 使用绝对坐标,原点在父视图的左上角。使用 position 后,视图会占据整个父容器空间。
offset
相对于视图原始位置进行偏移,不影响布局空间。
struct OffsetExample: View {
var body: some View {
VStack(spacing: 40) {
HStack {
Text("原始")
.background(Color.blue.opacity(0.2))
Text("偏移")
.offset(x: 20, y: -10)
.background(Color.red.opacity(0.2))
Text("后续")
.background(Color.green.opacity(0.2))
}
Text("向下偏移")
.offset(y: 20)
.background(Color.orange.opacity(0.2))
}
}
}offset 与 position 的区别:
offset:相对偏移,不改变布局空间position:绝对定位,占据整个父容器
coordinateSpace
为视图定义命名坐标空间,用于在不同视图间进行坐标转换。
struct CoordinateSpaceExample: View {
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0..<20) { index in
GeometryReader { geometry in
let offset = geometry.frame(in: .named("scroll")).minY
Text("Item \(index)")
.frame(maxWidth: .infinity)
.background(
Color.blue.opacity(
Double(1 - abs(offset) / 500)
)
)
}
.frame(height: 50)
}
}
}
.coordinateSpace(name: "scroll")
}
}对齐视图
对齐系统控制视图在容器中的对齐方式,包括内置对齐和自定义对齐。
Alignment
SwiftUI 提供了多种内置对齐选项:
struct AlignmentExample: View {
var body: some View {
VStack(spacing: 20) {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.gray.opacity(0.2))
.frame(width: 200, height: 100)
Text("左上")
}
ZStack(alignment: .bottomTrailing) {
Rectangle()
.fill(Color.gray.opacity(0.2))
.frame(width: 200, height: 100)
Text("右下")
}
HStack(alignment: .top, spacing: 20) {
Text("顶部对齐")
.background(Color.blue.opacity(0.2))
Text("顶部\n对齐\n多行")
.background(Color.green.opacity(0.2))
}
}
}
}常用对齐选项:
- 水平:
.leading,.center,.trailing - 垂直:
.top,.center,.bottom - 组合:
.topLeading,.topTrailing,.bottomLeading,.bottomTrailing
自定义对齐指南
通过 alignmentGuide 创建自定义对齐行为。
extension VerticalAlignment {
private struct CustomAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.center]
}
}
static let custom = VerticalAlignment(CustomAlignment.self)
}
struct CustomAlignmentExample: View {
var body: some View {
HStack(alignment: .custom, spacing: 20) {
VStack {
Text("顶部")
Text("中间")
.alignmentGuide(.custom) { d in d[VerticalAlignment.center] }
Text("底部")
}
.background(Color.blue.opacity(0.2))
Text("对齐到中间")
.alignmentGuide(.custom) { d in d[VerticalAlignment.center] }
.background(Color.green.opacity(0.2))
}
}
}自定义对齐的典型应用场景:
- 对齐文本基线
- 对齐图标和文本
- 复杂布局中的精确对齐
调整间距
lineSpacing
设置多行文本的行间距。
struct LineSpacingExample: View {
let text = "这是一段很长的文本,用于演示行间距的效果。行间距可以提高文本的可读性。"
var body: some View {
VStack(spacing: 30) {
Text(text)
.frame(width: 200)
.background(Color.blue.opacity(0.1))
Text(text)
.lineSpacing(10)
.frame(width: 200)
.background(Color.green.opacity(0.1))
}
}
}ViewSpacing
ViewSpacing 用于在自定义布局中控制视图间距,这是一个高级特性。
// 在自定义 Layout 中使用
struct CustomLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
// 实现布局逻辑
return .zero
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var point = bounds.origin
for subview in subviews {
subview.place(at: point, proposal: proposal)
point.x += subview.sizeThatFits(proposal).width
point.x += subview.spacing.distance(to: subviews.first?.spacing ?? .zero, along: .horizontal)
}
}
}与安全区域交互
安全区域是系统定义的不会被系统 UI(如刘海、状态栏)遮挡的区域。
ignoresSafeArea
让视图扩展到安全区域之外。
struct IgnoreSafeAreaExample: View {
var body: some View {
VStack(spacing: 0) {
Color.blue
.ignoresSafeArea()
.frame(height: 200)
Text("内容区域")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
}可以指定忽略特定边缘:
Color.blue
.ignoresSafeArea(.container, edges: .top)安全区域类型:
.container:容器安全区域.keyboard:键盘安全区域.all:所有安全区域
safeAreaInset
在安全区域内添加固定内容,其他内容会自动避开。
struct SafeAreaInsetExample: View {
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0..<30) { index in
Text("Item \(index)")
.frame(maxWidth: .infinity)
.padding()
.background(Color.gray.opacity(0.2))
}
}
}
.safeAreaInset(edge: .bottom) {
HStack {
Button("取消") { }
Spacer()
Button("确认") { }
}
.padding()
.background(.ultraThinMaterial)
}
}
}读取布局信息
GeometryReader
GeometryReader 是一个容器视图,提供父视图的尺寸和坐标信息。
struct GeometryReaderExample: View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("宽度: \(Int(geometry.size.width))")
Text("高度: \(Int(geometry.size.height))")
HStack(spacing: 0) {
Color.blue
.frame(width: geometry.size.width * 0.3)
Color.green
.frame(width: geometry.size.width * 0.7)
}
}
}
.frame(height: 200)
}
}获取坐标信息
使用 GeometryProxy 的 frame(in:) 方法获取不同坐标空间中的位置。
struct CoordinateExample: View {
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0..<20) { index in
GeometryReader { geometry in
let globalFrame = geometry.frame(in: .global)
let localFrame = geometry.frame(in: .local)
VStack(alignment: .leading, spacing: 5) {
Text("Item \(index)")
.font(.headline)
Text("Global Y: \(Int(globalFrame.minY))")
.font(.caption)
Text("Local Y: \(Int(localFrame.minY))")
.font(.caption)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.blue.opacity(0.2))
}
.frame(height: 80)
}
}
}
}
}坐标空间选项:
.global:屏幕坐标空间.local:视图自身坐标空间.named("name"):自定义命名坐标空间
GeometryReader 会尽可能占用所有可用空间,可能影响布局。使用时需要注意设置 frame 限制其尺寸。
获取布局属性
SwiftUI 提供了环境值来获取设备和显示相关的布局属性。
displayScale 和 pixelLength
struct DisplayScaleExample: View {
@Environment(\.displayScale) var displayScale
@Environment(\.pixelLength) var pixelLength
var body: some View {
VStack(spacing: 10) {
Text("显示缩放: \(displayScale, specifier: "%.1f")x")
Text("像素长度: \(pixelLength, specifier: "%.4f")")
Rectangle()
.fill(Color.blue)
.frame(height: pixelLength)
}
.padding()
}
}horizontalSizeClass 和 verticalSizeClass
尺寸类别用于适配不同屏幕尺寸的设备。
struct SizeClassExample: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
Group {
if horizontalSizeClass == .compact {
VStack {
content
}
} else {
HStack {
content
}
}
}
}
var content: some View {
Group {
Text("内容 1")
Text("内容 2")
Text("内容 3")
}
}
}UserInterfaceSizeClass 的可能值:
.compact:紧凑尺寸(如 iPhone 竖屏).regular:常规尺寸(如 iPad)
边缘和插入
Edge
Edge 枚举定义了视图的四个边缘。
enum Edge {
case top
case bottom
case leading
case trailing
}Edge.Set 可以组合多个边缘:
.all:所有边缘.horizontal:水平边缘(leading + trailing).vertical:垂直边缘(top + bottom)[.top, .leading]:自定义组合
EdgeInsets
EdgeInsets 表示四个边缘的插入值。
struct EdgeInsetsExample: View {
let insets = EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)
var body: some View {
Text("自定义插入")
.padding(insets)
.background(Color.blue.opacity(0.2))
}
}HorizontalEdge 和 VerticalEdge
用于指定单一方向的边缘。
// HorizontalEdge
enum HorizontalEdge {
case leading
case trailing
}
// VerticalEdge
enum VerticalEdge {
case top
case bottom
}常用于需要明确指定单边的场景:
.safeAreaInset(edge: .bottom) {
// 底部插入内容
}总结
SwiftUI 的布局调整系统提供了丰富而灵活的工具:
- 使用 padding 添加内边距,控制视图周围的空白
- 使用 frame 控制视图尺寸,支持固定和灵活尺寸
- 使用 position 和 offset 调整视图位置
- 使用 alignment 控制视图对齐,支持自定义对齐指南
- 使用 safeAreaInset 和 ignoresSafeArea 处理安全区域
- 使用 GeometryReader 获取布局信息,实现响应式设计
- 使用 环境值 获取设备特性,适配不同屏幕
掌握这些布局调整技巧,可以精确控制视图的呈现效果,构建出适应各种场景的用户界面。
上次更新于