SwiftUI中的Alignment Guides

Alignment guides是用一种功能强大但通常未被充分利用的布局工具。在很多情况下,它们可以帮助我们避免更复杂的选项,例如Anchor preferences。就如下图所示,改变alignment也可以自动并轻松的实现动画效果。

如果你发现Alignment guides确实有效,但是又没有达到预期的效果,很可能是没有意识到有一些隐式的Alignment guides在起作用。请记住:容器中的每一个视图都有一个Alignment guide。

什么是Alignment Guide

Alignment guide本质上是一个浮点型数值,它在视图中设置一个点,用来标识视图间对齐的依据。对齐可以是水平方式也可以是垂直方式,我们先来看下水平的对齐方式。

假设我们有3个视图(A,B和C),它们的水平基线分别为0,20和10。我们将对视图进行定位,使View A的起点(leading,水平偏移0)与View B的起始第20个点(leading,水平偏移20)和view C的起始第10个点(leading,水平偏移10)对齐:

同样的概念也适用于垂直对齐:

从这些例子可以看出,垂直容器(VStack)需要水平对齐,水平容器(HStack)则需要垂直对齐,可能觉得很奇怪,但稍微想一下确实如此,VStack里面的视图是垂直排列,充分占据容器的高,只能在水平方向移动来对齐,而HStack则正好相反。

另外,ZStack同时需要水平对齐和垂直对齐。

开始吧

先来一张图让你感受一下:

你可能有点慌,但是这没什么,这里是一些基本的描述,我们会在后面进一步讨论。

  • Container Alignment: 容器对齐,它有两个作用,它决定了哪些alignmentGuides()会被忽略,同时它也为其包含的没有指定Alignment guide的视图定义了默认的Alignment guide;
  • Alignment Guide: 如果该值和Container Alignment的对齐参数不匹配,布局时将会被忽略;
  • Implicit Alignment Value: 隐式对齐值,浮点型数值,有一些方便的预设值可以使用,如d.width、d[.leading]、d[.center]等等。这是一个与指定Guide相关的默认值;
  • Explicit Alignment Value: 显示对齐值,浮点型数值,表示修改视图Guide的位置。需要通过编码计算得到;
  • Frame Alignment: 框架对齐,表示容器内所有的视图(作为一个整体)的对齐方式;
  • Text Alignment: 多行文本在视图中的对齐方式。

Implicit vs. Explicit Alignments

容器中每个视图都有一个对齐方式。 为什么总是突出显示这句话,因为这是需要记住的最重要的概念之一。当我们通过.alignmentGuide()来定义对齐方式时,对齐方式是显示的(Explicit),当我们没有指定,对齐方式就是隐式的(Implicit)。隐式对齐方式的值将由容器视图中的对齐方式(Container Alignment)参数提供,如VStack(alignment: .leading)。

你可能会问,如果我们不为VStack、HStack或ZStack指定对齐方式参数会怎么样?很简单,它们都有一个默认值:.center。

ViewDimensions

到目前为止,我们看到在.alignmentGuide()修饰符的闭包中,我们需要返回一个CGFloat的值作为对齐指南的值,来看下.alignmentGuide()方法的定义:

Swift 复制代码
func alignmentGuide(_ g: HorizontalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
func alignmentGuide(_ g: VerticalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View

这是一个重载方法,有两个版本,一个用于水平对齐,一个用于垂直对齐。computeValue闭包提供了一个ViewDimensions类型的参数,该类型是一个struct,包含了一些我们可以用来创建对齐指南的关于视图的信息:

Swift 复制代码
public struct ViewDimensions {
    public var width: CGFloat { get } // The view's width
    public var height: CGFloat { get } // The view's height

    public subscript(guide: HorizontalAlignment) -> CGFloat { get }
    public subscript(guide: VerticalAlignment) -> CGFloat { get }
    public subscript(explicit guide: HorizontalAlignment) -> CGFloat? { get }
    public subscript(explicit guide: VerticalAlignment) -> CGFloat? { get }
}

高度和宽度没什么好说的,就是我们当前处理的视图的高度和宽度。但是有一些subscript方法就有点不好理解,我们先看看怎么访问它们:

Swift 复制代码
Text("Hello")
    .alignmentGuide(HorizontalAlignment.leading, computeValue: { d in                        
        return d[HorizontalAlignment.leading] + d.width / 3.0 - d[explicit: VerticalAlignment.top]
    })

稍后我们再探索HorizontalAlignment、VerticalAlignment和Alignment类型,看看这些是什么。

Alignment的不明确使用

通常我们不需要像这样指定对齐指南的全名:

Swift 复制代码
d[HorizontalAlignment.leading] + d.width / 3.0 - d[explicit: VerticalAlignment.top]

编译器可以推断出我们指的是HorizontalAlignment还是VerticalAlignment,因此我们只需要使用:

Swift 复制代码
d[.leading] + d.width / 3.0 - d[explicit: .top]

不过,在有些情况下,编译器可能会提示对齐方式不明确,尤其是在使用.center的时候,因为.center有两种类型:HorizontalAlignment.center和VerticalAlignment.center,这时候就需要指定全名。

HorizontalAlignment类型

使用HorizontalAlignment类型访问ViewDimensions时,我们可以获得视图的leading、视图的center和视图的trailing:

Swift 复制代码
extension HorizontalAlignment {
    public static let leading: HorizontalAlignment
    public static let center: HorizontalAlignment
    public static let trailing: HorizontalAlignment
}

注意,引用可以通过两种方式获取:

Swift 复制代码
d[.trailing]
d[explicit: .trailing]

第一个是隐式值,也就是对齐指南的默认值,通常情况下,.leading的默认值是0,.center的默认值是width/2,.trailing的默认值是width。不过,有时候也可以获取显示值,例如在ZStack中,当计算.leading时,可能会用到.top的显示值。

VerticalAlignment类型

VerticalAlignment和HorizontalAlignment类似,但它多了两个属性firstTextBaseline(最上面的文本基线)和lastTextBaseline(最下面的文本基线),在对齐一行不同字体大小的文本时很有用。

Swift 复制代码
extension VerticalAlignment {
    public static let top: VerticalAlignment
    public static let center: VerticalAlignment
    public static let bottom: VerticalAlignment
    public static let firstTextBaseline: VerticalAlignment
    public static let lastTextBaseline: VerticalAlignment
}

Alignment类型

之前说过,ZStack需要指定两种对齐方式(一种水平对齐,一种垂直对齐),所以有了Alignment类型,它将这两种对齐方式结合在一起。例如,如果我们想要一个顶部垂直对齐方式和前缘水平对齐方式(就是左上角),我们有两种做法:

Swift 复制代码
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) { ... }

或者最常见的:

Swift 复制代码
ZStack(alignment: .topLeading) { ... }

稍后当我们开始使用自定义的Alignment guides时,第一种方式就会很有用。

Container Alignment

容器视图(VStack、HStack和ZStack)中的对齐参数有两种作用:

  1. 确定哪些.alignmentGuides()与布局有关,所有与容器参数中的对齐方式不同的alignment guides都将被忽略;
  2. 为没有指定显示对齐方式的视图提供隐式对齐方式,即子视图的默认值。

在下面的动画中,你可以看到改变容器中的对齐方式将如何决定哪些alignment guides在布局时生效。

Frame Alignment

到目前为止,我们看到的所有对齐方式都是关于如何将一个视图相对于另外一个视图定位。一旦确定了这一点,布局系统就需要在容器内定位整个视图组,可以通过frame(alignment:)来修改整个视图组在容器中的对齐方式,如果不指定,视图组将在容器内居中。

通常情况下,修改frame的alignment不会产生任何影响,这是正常的,因为容器默认是紧凑的,容器的大小刚好容纳所有的视图,因此在frame()修饰符中使用.leading、.center或.trailing都不会有任何效果,视图组已经占用了所有的空间,无法再移动,除非指定frame的大小,让它有移动的空间。

多文本Alignment

这东西比较简单,指定多行文本的对齐方式。

自定义Alignment

既然我们已经知道了标准对齐的工作方式,那我们就来创建一个自定义的对齐方式。先看看第一个示例:

Swift 复制代码
extension HorizontalAlignment {
    private enum WeirdAlignment: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            return d.height
        }
    }
    
    static let weirdAlignment = HorizontalAlignment(WeirdAlignment.self)
}

自定义Alignment时,我们需要做两件事:

  1. 确定是水平对齐还是垂直对齐;
  2. 为隐式对齐方式(即没有明确调用.alignmentGuide()的视图)提供默认值。

我们将使用height作为默认值。这将会产生一种有趣的效果:

Swift 复制代码
struct CustomView: View {
    var body: some View {
        VStack(alignment: .weirdAlignment, spacing: 10) {
            
            Rectangle()
                .fill(Color.primary)
                .frame(width: 1)
                .alignmentGuide(.weirdAlignment, computeValue: { d in d[.leading] })
            
            ColorLabel(label: "Monday", color: .red, height: 50)
            ColorLabel(label: "Tuesday", color: .orange, height: 70)
            ColorLabel(label: "Wednesday", color: .yellow, height: 90)
            ColorLabel(label: "Thursday", color: .green, height: 40)
            ColorLabel(label: "Friday", color: .blue, height: 70)
            ColorLabel(label: "Saturday", color: .purple, height: 40)
            ColorLabel(label: "Sunday", color: .pink, height: 40)
            
            Rectangle()
                .fill(Color.primary)
                .frame(width: 1)
                .alignmentGuide(.weirdAlignment, computeValue: { d in d[.leading] })
        }
    }
}

struct ColorLabel: View {
    let label: String
    let color: Color
    let height: CGFloat
    
    var body: some View {
        Text(label).font(.title).foregroundColor(.primary).frame(height: height).padding(.horizontal, 20)
            .background(RoundedRectangle(cornerRadius: 8).fill(color))
    }
}

对齐不同层次结构中的视图

在前面的示例中,我们知道了如何创建自定义的对齐方式,但这有什么意义呢?不使用自定义对齐方式也能达到同样的效果。使用自定义对齐方式的真正作用在于,它可以对齐不同层次结构中的视图。

请看下一个示例:

如果我们分析一下该视图的组件,就会发现我们需要将图像与文本视图对齐,但是它们并不属于同一个容器:

图像和文本视图都有一个共同的容器(HStack),因此我们需要创建一个自定义对齐方式,以匹配它们的中心点。重要的是要记住适当设置共同容器的对齐参数。

Swift 复制代码
extension VerticalAlignment {
    private enum MyAlignment : AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            return d[.bottom]
        }
    }
    static let myAlignment = VerticalAlignment(MyAlignment.self)
}

struct CustomView: View {
    @State private var selectedIdx = 1
    
    let days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    
    var body: some View {
            HStack(alignment: .myAlignment) {
                Image(systemName: "arrow.right.circle.fill")
                    .alignmentGuide(.myAlignment, computeValue: { d in d[VerticalAlignment.center] })
                    .foregroundColor(.green)

                VStack(alignment: .leading) {
                    ForEach(days.indices, id: \.self) { idx in
                        Group {
                            if idx == self.selectedIdx {
                                Text(self.days[idx])
                                    .transition(AnyTransition.identity)
                                    .alignmentGuide(.myAlignment, computeValue: { d in d[VerticalAlignment.center] })
                            } else {
                                Text(self.days[idx])
                                    .transition(AnyTransition.identity)
                                    .onTapGesture {
                                        withAnimation {
                                            self.selectedIdx = idx
                                        }
                                }
                            }
                        }
                    }
                }
            }
            .padding(20)
            .font(.largeTitle)
    }
}

你可能会问,所有没有指定显示的垂直对齐方式的文本视图怎么办,难道它们不会使用隐式值(默认值)吗?如果是这样,它们不都是重叠在一起了?

这些问题都有道理。这是Alignment guides中另一个可能令人费解的问题。不过,在这种情形下,我们处理的是VStack,而不是ZStack,这意味着它内部的所有视图都必须垂直排列,Alignment guides不会破坏这一点。布局系统将使用所选视图中的显示对齐方式来对齐"→"图像,其他没有显示对齐方式的文本视图将相对于有显示对齐方式的视图进行定位。

自定义ZStack的Alignment

如果你需要为ZStack创建自定义对齐方式,这里有一个模板:

Swift 复制代码
extension VerticalAlignment {
    private enum MyVerticalAlignment : AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            return d[.bottom]
        }
    }
    
    static let myVerticalAlignment = VerticalAlignment(MyVerticalAlignment.self)
}

extension HorizontalAlignment {
    private enum MyHorizontalAlignment : AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            return d[.leading]
        }
    }
    
    static let myHorizontalAlignment = HorizontalAlignment(MyHorizontalAlignment.self)
}

extension Alignment {
    static let myAlignment = Alignment(horizontal: .myHorizontalAlignment, vertical: .myVerticalAlignment)
}

struct CustomView: View {
    var body: some View {
        ZStack(alignment: .myAlignment) {
            ...
        }
    }
}

总结

在这篇文章中,我们看到了Alignment guides的强大功能,一旦你能掌握它,将会变得更有意义,为此需要牢记以下几点:

  1. 容器中每个视图都有一个Alignment guide,如果没有显示指定,将使用容器的对齐参数;
  2. 在布局过程中,与容器对齐参数指定的类型不同的Alignment guide将被忽略;
  3. VStack使用HorizontalAlignment,而HStack使用VerticalAlignment;
  4. 如果容器很紧凑,.frame(alignment:)中的对齐参数可能不会产生视觉效果;
  5. 对齐不同层次结构中的视图时,就需要用到自定义对齐。

原文出处:swiftui-lab.com/alignment-g...

相关推荐
黑化旺仔14 小时前
【OC】Runtime:类元数据、加载初始化与消息派发
开发语言·macos·ios·objective-c
songgeb15 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
软泡芙15 小时前
【iOS】swift启动屏显示黑屏
ios
用户693717500138417 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer17 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
用户693717500138418 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
茶底世界之下19 小时前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
Marco_Marco13520 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记1 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机
2501_915921431 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview