【WWDC2019 之 SwiftUI】04 - SwiftUI的 View 如何布局

基本布局

在刚创建好一个 SwiftUI 项目时,Xcode 给我们准备了模板代码:

struct ContentView : View {
    var body: some View {
        Text("Hello World")
    }
}

预览如下:

View 层级关系如下:

本质上来说,这个例子有三个 View:1)最底层的 Root View,也就是整个手机屏幕除去留海屏的部分;2)处于中间的 ContentView,预览图体现不出来,因为它和 Text 一样大;2)最顶层的 Text。但是因为 ContentView 的大小是有它的 Child View Text 决定的,所以这里我们可以把这个 View 层级简化成只有 Root View 和 Text

我们就以这个为例子,讲解一下 SwiftUI 的基本布局步骤:

  1. Parent View 给 Child View 提供一个 size,这个 size 是 Parent View 所能提供的最大 size。在本例中就相当于 Root View 对 Text 说:“Hey, Text,我可以把整个屏幕大小(除了留海)的空间给你”。
  1. Child View 选择自己的 size。在本例中,Text 说我只需要这么大就够了。
  1. Parent View 把 Child View 放在自己的坐标空间里。在本例中,Root View 把 Text 放在中间。

这就是一个完整的 SwiftUI 布局流程。在 SwiftUI 中所有的 Parent View 与 Child View 之间的布局逻辑都是跟这里讲解的一样的。

HStack 和 VStack

借用官方的 Demo,UI代码如下:

HStack {
    VStack {
        Text("★★★★★")
        Text("5 stars")
    }.font(.caption)
    VStack(alignment: .leading) {
        HStack {
            Text("Avocado Toast").font(.title)
            Spacer()
            Image("20x20_avocado")
        }
        Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
            .font(.caption).lineLimit(1)
    }
}

预览效果:

Stack 可以自动为它的 Child Views 之间添加 spacing。如果用户使用的是阿拉伯语,SwiftUI 可以自动切换文字的方向,如下图:

我们拿 HStack 作为例子,讲解一下 Stack 是如果工作的。假设有以下代码:

HStack {
    Text("Delicious")
    Image("20x20_avocado")
    Text("Avocado Toast")
}
.lineLimit(1)

当水平方向上有足够空间的情况下,预览图如下:

  1. 首先 HStack 先预留合适的宽度给 spacing,所以 HStack 的可用空间为所有可用宽度减去 S0 + S1
  1. 因为总共有三个 Child Views,所以 HStack 先把剩余可用宽度平均分配给三个 Child Views:
  1. 因为 Image 的大小是固定的,所以 Image 先确定它的所需宽度,然后从可用宽度减去 Image 的宽度:

  1. 剩下两个 Child Views,所以 HStack 又把剩余可用宽度平均分配给两个 Child Views:
  1. Delicious 确定它的所需宽度,HStack 从可用宽度减去 Delicious 的宽度:
  1. 剩下的可用宽度全部分配给 Avocado Toast
  1. 所有 Child Views 水平方向的位置都确定了,那么剩下垂直方向。HStack垂直方向的对齐方式默认是 center,所以完整的代码如下:
// alignment 默认是 center
HStack(alignment: .center) {
    Text("Delicious")
    Image("20x20_avocado")
    Text("Avocado Toast")
}
.lineLimit(1)

Layout Priority

如果水平方向的空间不够,那么就会造成下面这种效果:

这时我们觉得后面的文字比较重要,想让它优先显示完整,可以使用 Layout Priority。代码如下:

HStack {
    Text("Delicious")
    Image("20x20_avocado")
    Text("Avocado Toast").layoutPriority(1)
}
.lineLimit(1)

结果为:

使用了 Layout Priority 后,HStack 就会优先满足除了最大优先级外的所有 Child Views 的最小所需宽度。前面文字的所需最小宽度是 ... 的宽度,图片所需最小宽度就是它的宽度,所以 HStack 把所有可用宽度减去前面文字和图片的所需最小宽度,剩下的可用宽度全部分配给 Avocado Toast。如果还有更多较低优先级的 Child Views,那么会重复使用同样的逻辑去进行宽度的分配。

Alignment

上面的例子中,我们使用的是 center,我们可以改为 bottom,代码如下:

HStack(alignment: .bottom) {
    Text("Delicious")
    Image("20x20_avocado")
    Text("Avocado Toast")
}
.lineLimit(1)

结果如下:

如果我们把前面文字的字体改小,代码和结果如下:

HStack(alignment: .bottom) {
    Text("Delicious").font(.caption)
    Image("20x20_avocado")
    Text("Avocado Toast")
}
.lineLimit(1)

这样看起来三个 Child Views 的最低部不是在同一条线上。这时我们可以把 alignment 改为 lastTextBaseline。代码和结果如下:

HStack(alignment: .lastTextBaseline) {
    Text("Delicious").font(.caption)
    Image("20x20_avocado")
    Text("Avocado Toast")
}
.lineLimit(1)

现在我们把注意力放到图片里,我们可以看到这个图片里没有文字,但是 lastTextBaseline 默认是 View 的最底部,所以我们才能达到我们想要的对齐效果。但是我们的产品经理觉得图片的位置太靠上了,这样不好看,想要把图片往下移一点点,想要的效果如下:

这时我们可以自定义 lastTextBaseline 的位置,代码如下:

HStack(alignment: .lastTextBaseline) {
    Text("Delicious")
        .font(.caption)
    Image("20x20_avocado")
        .alignmentGuide(.lastTextBaseline) { d in
            d[.bottom] * 0.927
    }
    Text("Avocado Toast")
}
.lineLimit(1)

d[.bottom] 这种写法是使用了 Swift 的 subscript 的特性。

自定义 VerticalAlignment

回到这一部分开头的例子,假设我们想让星星与 Avocado Toast 垂直方向上居中对齐,只设置 alignmentcenter 是不行的。

要达到想要的效果,我们需要自定义 VerticalAlignment。代码如下:

extension VerticalAlignment {
    private enum MidStarAndTitle: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> Length {
            return d[.bottom]
        }
    }
    static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self)
}

先定义一个遵循 AlignmentID 协议的 MidStarAndTitle,然后再用 MidStarAndTitle 实例化 VerticalAlignment。这样我们就可以把原有代码修改为:

HStack(alignment: .midStarAndTitle) {
    VStack {
        Text("★★★★★")
            .alignmentGuide(.midStarAndTitle) { d in d[.bottom] / 2 }
        Text("5 stars")
    }.font(.caption)
    
    VStack(alignment: .leading) {
        HStack {
            Text("Avocado Toast").font(.title)
                .alignmentGuide(.midStarAndTitle) { d in d[.bottom] / 2 }
            Spacer()
            Image("money")
        }
        Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
    }
    .font(.caption).lineLimit(1)
}

最终得到我们想要的效果:

至于代码中的 alignmentGuide() 是如果工作的,你把代码运行起来,修改 closure 里的值,看看具体效果。

想要更详细了解文章的内容,可以点击查看下面的视频。想及时看到我的新文章的,可以关注我。

参考资料

Building Custom Views with SwiftUI - WWDC 2019 - Videos - Apple Developer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357