SwiftUI

布局调整

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)
}

safeAreaPaddingpadding 的区别: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))
        }
    }
}

offsetposition 的区别:

  • 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)
    }
}

获取坐标信息

使用 GeometryProxyframe(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 控制视图尺寸,支持固定和灵活尺寸
  • 使用 positionoffset 调整视图位置
  • 使用 alignment 控制视图对齐,支持自定义对齐指南
  • 使用 safeAreaInsetignoresSafeArea 处理安全区域
  • 使用 GeometryReader 获取布局信息,实现响应式设计
  • 使用 环境值 获取设备特性,适配不同屏幕

掌握这些布局调整技巧,可以精确控制视图的呈现效果,构建出适应各种场景的用户界面。

在 GitHub 上编辑

上次更新于