美文网首页swiftUI
SwiftUI之AlignmentGuides

SwiftUI之AlignmentGuides

作者: 老马的春天 | 来源:发表于2020-06-06 17:30 被阅读0次

    本质上,Alignment Guides属于SwiftUI中布局的知识点,在某些特殊场景下,使用Alignment Guides能起到事半功倍的效果,比如我们平时经常用的下边的这样的效果:

    1

    上图显示了,当切换背景容器的最大宽度时,使用Alignment Guides能够自动执行动画效果,这正是我们想要的,核心代码如下:

    struct TestWrappedLayout: View {
        let w: CGFloat
        var texts: [String]
    
        var body: some View {
            self.generateContent(in: w)
        }
    
        private func generateContent(in w: CGFloat) -> some View {
            var width = CGFloat.zero
            var height = CGFloat.zero
    
            return ZStack(alignment: .topLeading) {
                ForEach(self.texts, id: \.self) { t in
                    self.item(for: t)
                        .padding([.trailing, .bottom], 4)
                        .alignmentGuide(.leading, computeValue: { d in
                         
                            if (abs(width - d.width) > w)
                            {
                                width = 0
                                height -= d.height
                            }
                            let result = width
                            if t == self.texts.last! {
                                width = 0 //last item
                            } else {
                                width -= d.width
                            }
                            return result
                        })
                        .alignmentGuide(.top, computeValue: {d in
                            let result = height
                            if t == self.texts.last! {
                                height = 0 // last item
                            }
                            return result
                        })
                }
            }
        }
    
        func item(for text: String) -> some View {
            Text(text)
                .padding([.leading, .trailing], 8)
                .frame(height: 30)
                .font(.subheadline)
                .background(Color.orange)
                .foregroundColor(.white)
                .cornerRadius(15)
                .onTapGesture {
                    print("你好啊")
            }
        }
    }
    

    在本篇文章中,最核心的思想就是容器container中的每个View都有它的alignment guide。

    Alignment Guide是什么?

    说到对齐,大家头脑中一定要有一个组的概念,也就是group,如果只有一个view,那对齐就失去了意义,我们在设计对齐相关的ui的时候,是对一组中的多个view进行考虑的,这也恰恰和容器的概念对应上了,我这里说的容器指的是VStack,HStack,ZStack

    换句话说,我们对容器内的Views使用Alignment guide。

    对齐共分为两种:水平对齐(horizontal),垂直对齐(vertical)

    我们先以水平对齐为例,先看下图:

    align-horizontal-1.png

    有3个view,分别为A,B,C,他们alignment guide返回的值分别为0, 20, 10,从上图可以看出,他们的偏移关系正好和值对应上了,当值为正的时候,往左偏移,为负的时候,往右偏移。这里有下边几个概念,大家一定要理解,如果不理解这几个概念,就无法真正明白对齐的奥义:

    • 我们把A,B,C放到了VStack中,VStack中使用的对齐方式是水平对齐,比如VStack(alignment: .center)`
    • alignment guide返回的值表达的是这3个view的位置关系,并不是说A的返回值为0,A就不偏移,我们需要把他们作为一个整体来看,通过偏移量来描述他们之间的位置关系,然后让他们3个view在VStack中整体居中

    上边的重点是,alignment guide描述的是views之间的位置关系,系统在布局的时候,会把他们看成一个整体,然后在使用frame alignment guide对整体进行布局。

    同样的道理,下边图片展示的是垂直对齐,我们就不再多做解释了:

    align-vertical.png

    通过上边这两个例子,我们得出一个结论:VStack需要水平对齐,HStack需要垂直对齐,虽然这听上去有点怪,但只需在头脑中想一想他们中view的排列方式,就不难理解。至于ZStack,即需要水平对齐,也需要垂直对齐,这个我们在下边的小节中,详细解释。

    Alignment Guide中的疑惑

    相信大家在代码中的很多地方会用到.leading,在SwiftUI中,用到对齐的地方一共有下边几种:

    alignment-confusion.png

    这张图片覆盖了对齐所有的使用方式,现在大家可能是一脸茫然,但读完剩下的文章后,再回过头来看这张图片,就会发现,这张图片实在是太经典了,毫不夸张的说,你以后在SwiftUI中使用alignment guide的时候,头脑中一定会浮现出这张图片。

    我们对上边的几个概念做个简单的介绍:

    • Container Alignment: 容器的对齐方式主要有2个目的,首先它定义了其内部views的隐式对齐方式,没有使用alignmentGuides()modifier的view都使用隐式对齐,然后定义了内部views中使用了alignmentGuides()的view,只有参数与容器对齐参数相同,容器才会根据返回值计算布局
    • Alignment Guide:如果该值和Container Alignment的参数不匹配,则不会生效
    • Implicit Alignment Value:通常来说,隐式对齐采用的值都是默认的值,系统通常会使用和对齐参数相匹配的值
    • Explicit Alignment Value:显式对齐跟隐式对齐相反,是我们自己用程序明确给出的返回值
    • Frame Alignment:表示容器中views的对齐方式,把views看作一个整体,整体偏左,居中,或居右
    • Text Alignment:控制多行文本的对齐方式

    隐式和显式对齐的区别

    每个view都有一个alignment,记住这一点非常重要,当我们使用.alignmentGuide()设置对齐方式时,我们称之为显式,相反则称之为隐式。隐式的情况下,.alignmentGuide()的返回值和它父类容器的对齐参数有关。

    如果我们没有为VStack, HStackZStack提供alignment参数,默认值为center。

    ViewDimensions

    func alignmentGuide(_ g: HorizontalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
    func alignmentGuide(_ g: VerticalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
    

    我们已经知道了computeValue函数的返回值是一个CGFloat类型,但我们不太清楚ViewDimensions是个什么东西?很简单,我们可以直接查看它的系统定义:

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

    很容易发现,通过widthheight,我们很容易获得该view的宽和高,这在我们返回对齐值的时候非常有用,我们不做过多解释,我们往下看,subscript表明我们可以像这样访问:d[HorizontalAlignment.leading]

    那么这有什么用呢? 我们先看段代码:

    struct Example6: View {
        var body: some View {
            ZStack(alignment: .topLeading) {
                Text("Hello")
                    .alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return 0 })
                    .alignmentGuide(.top, computeValue: { d in return 0 })
                    .background(Color.green)
                
                Text("World")
                    .alignmentGuide(.top, computeValue: { d in return 100 })
                    .alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return 0 })
                    .background(Color.purple)
                
            }
            .background(Color.orange)
        }
    }
    

    这段代码运行后的效果

    1

    由于我们给Text("World")设置了.alignmentGuide(.top, computeValue: { d in return 100 }),因此,它出现在hello的上边没什么问题,那么我如果把.alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return 0 })改成.alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return d[.top] })呢?

    在设置leading对齐的时候使用了top对齐的数据,运行效果:

    2

    可以看出,完全符合我们的预期,world又向左偏移了100的距离,这就是我们上边说的用法,不过,通常情况下我们基本不需要这样操作。

    类似d[HorizontalAlignment.leading]这样的参数,我们都可简写成d[.leading],Swift能够非常智能的识别这些类型,但是center除外,原因是HorizontalAlignmentVerticalAlignment都有center。

    对齐类型

    对于HorizontalAlignment来说,有下边几个参数:

    extension HorizontalAlignment {
        public static let leading: HorizontalAlignment
        public static let center: HorizontalAlignment
        public static let trailing: HorizontalAlignment
    }
    

    当我们使用下标访问数据的时候,有两种方式:

    d[.trailing]
    d[explicit: .trailing]
    

    d[.trailing]表示获取d的隐式leading,也就是默认值,通常情况下,.leading的值为0,.center的值为width的一半,.trailing的值为width。

    d[explicit: .trailing]表示获取d的显式的trailing,当没有通过.alignmentGuide()指定值的时候,它返回nil,就像上一小节讲的一样,在ZStack中,我们可以获取显式的对齐值

    对于VerticalAlignment来说,基本用法跟HorizontalAlignment差不多,但它多了几个参数:

    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
    }
    

    firstTextBaseline表示所有text的以各自最上边的那一行的base line对齐,lastTextBaseline表示所有text的以最下边的那一行的base line对齐。对于某个view而言,如果它不是多行文本,则firstTextBaselinelastTextBaseline是一样的。

    我们可以通过print(d[.lastTextBaseline])打印出这些值,他们都为正值。

    我们先看个firstTextBaseline的例子:

            HStack(alignment: .firstTextBaseline) {
                Text("床前明月光")
                    .font(.caption)
                    .frame(width: 50)
                    .background(Color.orange)
                   
                Text("疑是地上霜")
                    .font(.body)
                    .frame(width: 50)
                    .background(Color.green)
                
                Text("举头望明月")
                    .font(.largeTitle)
                    .frame(width: 50)
                    .background(Color.blue)
                 
            }
    
    企业微信截图_b87e6bd8-84b4-4e49-9f44-3a88d13c64dc.png

    可以看出来,这3个text都以他们各自的第一行的base line 对齐了,我们稍微改下代码:

            HStack(alignment: .lastTextBaseline) {
                ...
        }
    
    企业微信截图_b92bc7fa-3042-4e7c-9026-7c0322ac19e7.png

    他们以各自的最后一行的的base line对齐了,针对这3个text,上边的代码都使用了隐式的alignment guide,那么我们再进一步尝试,我们给第3个text一个显式的alignment guide会是怎么样的?

            HStack(alignment: .lastTextBaseline) {
                Text("床前明月光")
                    .font(.caption)
                    .frame(width: 50)
                    .background(Color.orange)
                   
                Text("疑是地上霜")
                    .font(.body)
                    .frame(width: 50)
                    .background(Color.green)
                
                Text("举头望明月")
                    .font(.largeTitle)
                    .alignmentGuide(.lastTextBaseline, computeValue: { (d) -> CGFloat in
                        d[.firstTextBaseline]
                    })
                    .frame(width: 50)
                    .background(Color.blue)
                 
            }
    
    企业微信截图_3aa000af-94ee-4224-9926-c6e220d4527a.png

    重点来了,对齐描述的是容器内view之间的布局关系,由于computeValue函数的返回值都是CGFloat,因此不管是哪种对齐方式,最终都是得到一个CGFloat。

    那么如果我们在text中间加入一个其他的view呢?

            HStack(alignment: .firstTextBaseline) {
                Text("床前明月光")
                    .font(.caption)
                    .frame(width: 50)
                    .background(Color.orange)
                
                RoundedRectangle(cornerRadius: 3)
                    .foregroundColor(.green)
                    .frame(width: 50, height: 40)
                   
                Text("疑是地上霜")
                    .font(.body)
                    .frame(width: 50)
                    .background(Color.green)
                
                Text("举头望明月")
                    .font(.largeTitle)
                    .alignmentGuide(.firstTextBaseline, computeValue: { (d) -> CGFloat in
                        return 0
                    })
                    .frame(width: 50)
                    .background(Color.blue)
                 
            }
    
    企业微信截图_afa2d94f-7faf-4f70-bfe4-1f5ed7c41e2c_副本.png
    • 除了text之外的其他view,都使用bottom对齐方式
    • 不管是lastTextBaseline还是firstTextBaseline,布局的算法都是.top + computeValue ,也就是说以它的顶部为布局的基线
    • alignment 描述的是view之间的关系,把他们作为一个整体或者一组来看待

    这一块可能有点绕,但并不难理解,如果大家有问题,可以留言。

    我们知道HStack使用VerticalAlignmentVStack使用HorizontalAlignment,他们只需要一种就行了,但是ZStack同时需要两种对齐方式,该如何呢?

    这里引入Alignment类型,用法如下:

    ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) { ... }
    

    本质上,它把horizontal和vertical封装在了一起,我们平时经常用的是下边这个写法,只是写法不同而已:

    ZStack(alignment: .topLeading) { ... }
    

    Container Alignment

    所谓的容器的对齐方式指的是下边这里:

    VStack(alignment: .leading)
    HStack(alignment: .top)
    ZStack(alignment: .topLeading)
    

    那么它主要有什么作用呢?

    • 我们知道,容器中的view都能够用.alignmentGuides()modifier来显式的返回对齐值,.alignmentGuides()的第一个参数如果与Container Alignment不一样,容器在布局的时候就会忽略这个view的.alignmentGuides()
    • 它提供了容器中view的隐式alignment guide

    大家看这段代码:

    struct Example3: View {
        @State private var alignment: HorizontalAlignment = .leading
        
        var body: some View {
            VStack {
                Spacer()
                
                 VStack(alignment: alignment) {
                   LabelView(title: "Athos", color: .green)
                    .alignmentGuide(.leading, computeValue: { _ in 30 } )
                       .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 30 } )
                       .alignmentGuide(.trailing, computeValue: { _ in 90 } )
                     
                   LabelView(title: "Porthos", color: .red)
                       .alignmentGuide(.leading, computeValue: { _ in 90 } )
                       .alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 30 } )
                       .alignmentGuide(.trailing, computeValue: { _ in 30 } )
                     
                    LabelView(title: "Aramis", color: .blue) // use implicit guide
                    
                 }
    
                Spacer()
                HStack {
                    Button("leading") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .leading }}
                    Button("center") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .center }}
                    Button("trailing") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .trailing }}
                }
            }
        }
    }
    
    

    它的运行效果如下图所示:

    Kapture 2020-06-01 at 17.11.25.gif

    可以很明显的看出当我们切换container alignment的参数时,它内部的view的alignment那些被忽略,那些被使用。

    Frame Alignment

    我一直在不断的强调,我们上边看到的对齐方式,都应该把容器中的所有view看作一个整体,alignment描述的是view之间的一种位置关系。

    大家思考一下,即使.alignmentGuide中的computeValue返回值为0,也不能说明该view保持不动。

    如果我们把容器内部的view看成一组,那么Frame Alignment就非常容易理解了:

    VStack(alignment: .leading) VStack(alignment: .center) VStack(alignment: .trailing)

    上边3张图片分别展示了VStack(alignment: .leading),VStack(alignment: .center)VStack(alignment: .trailing)的情况,可以看出,他们内部图形的布局发生了变化,但是他们3个整体都是居中对齐的。

    原因就是我们上一小节讲的,container alignment只影响容器内的布局,要让容器内的views整体左对齐或者居中,需要使用Frame Alignment.

    .frame(maxWidth: .infinity, alignment: .leading)
    
    Frame Alignment

    关于Frame Alignment有一点需要特别注意,有时候看上去我们的设置没有生效,只要原因就是,在SwiftUI中,大多数情况下View的布局政策基于收紧策略,也就是View的宽度只是自己需要的宽度,这种情况下设置frame对齐当然就没有意义了。

    Multiline Text Alignment()

    多行文本对齐就比较简单了,大家直接看图就行了。

    Multiline Text Alignment()

    Interacting with the Alignment Guides

    如果大家对上边讲的这些对齐方式还有疑惑,可以下载这里的代码https://gist.github.com/swiftui-lab/793ca53ad1f2f0d7eb07aa23b54d9cbf,自己动手做一些交互,就应该能够明白这些原理和用法了,放一张界面的截图:

    交互

    Custom Alignments

    大多数情况,我们是不要自定义对齐的,使用系统提供的.leading.center等等几乎可以实现所有的UI效果,在本小节中,大家应该重点关注第二个例子,基本上只有这种情况,我们优先考虑自定义对齐。

    自定义对齐的基本写法如下:

    extension HorizontalAlignment {
        private enum WeirdAlignment: AlignmentID {
            static func defaultValue(in d: ViewDimensions) -> CGFloat {
                return d.height
            }
        }
        
        static let weirdAlignment = HorizontalAlignment(WeirdAlignment.self)
    }
    
    • 决定是horizontal还是vertical
    • 提供一个隐式对齐的默认值

    我们小试牛刀,在上边代码中,我们自定义一个alignment,默认值返回view的高度,这样产生的效果如下:

    weird-alignment.png

    可以看出,每个view的偏移都是它自身的高度,这样的效果看上去还挺有意思。完整代码如下:

    struct Example4: View {
        var body: some View {
            VStack(alignment: .weirdAlignment, spacing: 10) {
                
                Rectangle()
                    .fill(Color.primary)
                    .frame(width: 1)
                    .alignmentGuide(.weirdAlignment, computeValue: { d in 0 })
                
                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 0 })
            }
        }
    }
    
    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))
        }
    }
    

    重点来了,我要说,使用自定义对齐最大的优势是:当在不同的view继承分支上使用自定义对齐,会产生理想的效果。

    custom-alignment.gif

    大家看上图,一个HStack包裹这一个Image和VStack,VStack中有一组Text,当点击某个Text的时候,Image可以和点击的Text对齐。

    这就是我们上边说的,对于属于不同层次的view进行对齐,SwiftUI非常智能的知道我们想要这样的效果。完整代码如下:

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

    如果要自定义ZStack的alignment,稍微麻烦一点,但原理都是一样的相信大家都能够理解,就直接上代码了:

    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 Guide有了一个全面的了解,它应该成为我们日常开发进行布局的强大工具,现在回过头来再看看这张图片,是不是有那么一点感觉了呢?

    alignment-confusion.png

    注:上边的内容参考了网站https://swiftui-lab.com/alignment-guides/,如有侵权,立即删除。

    相关文章

      网友评论

        本文标题:SwiftUI之AlignmentGuides

        本文链接:https://www.haomeiwen.com/subject/sumgzhtx.html