SwiftUI 布局系统指南 - 第 1 部分

除了声明式 DSL 和强大的数据绑定外,SwiftUI 还具有全新的布局系统,该系统在许多方面结合了手动框架计算的明确性和自动布局的适应性。乍一看可能看起来很简单的系统,但是一旦我们开始将其各种构建块组合成越来越复杂的布局,它就会提供巨大的灵活性和强大的功能。

frame

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
    }
}
centered-calendar.png

默认情况下,SwiftUI 允许每个视图根据其渲染的容器选择自己的大小,然后居中放置在其父视图中。所以上面代码的结果是在屏幕中央呈现一个小图标——而不是像我们基于 UIKit 和 AppKit 的工作方式所预期的那样位于左上角或左下角。

接下来,让我们把图标放大一点,比如 50x50 pt。关于如何实现这一点的初步想法可能是使用.frame()告诉我们的视图采用该大小,如下所示:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .frame(width: 50, height: 50)
    }
}

然而,虽然上面的代码会产生一个50x50 pt的视图,但我们图标的大小将与之前完全一样——这乍一看可能有点奇怪。为了探究为什么会这样,让我们给我们的视图一个背景颜色,这样我们就可以很容易地看到它在屏幕上的frame:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .frame(width: 50, height: 50)
            .background(Color.red)
    }
}
framed-calendar.png

有了上面的内容,我们可以看到我们的视图确实是正确的大小 。只是我们的图标似乎完全不受我们的.frame()修饰符的影响。将修饰符应用于视图时,我们通常根本不是修饰视图,而是将其封装在一个新的、透明的视图中。因此,在上面调用.background()时,我们实际上是将该background modifier应用于包装Image的新视图上面,而不是Image本身。

所以,从布局的角度来看,我们的Image没有任何变化,它仍然在其父视图中居中,只是这次它的父视图是一个新的 50x50 透明包装视图,但渲染结果看起来是一样的。

在SwiftUI中 视图负责确定它们自己的大小,我们需要告诉我们的Image调整自身大小以占用所有可用空间,而不是坚持其默认大小。为了实现这一点,我们只需使用.resizable()修饰符:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .background(Color.red)
    }
}
resized-calendar.png

padding

接下来,我们来看看SwiftUI 中的padding。就像在其他布局系统中一样,如 CSS,padding 使我们能够在其自己的frame内偏移视图的内容。然而,我们在view modifiers中使用padding的位置,可以获得完全不同的渲染结果。例如,让我们通过在上述代码的末尾附加.padding()修饰符来应用paddding

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .background(Color.red)
            .padding()
    }
}

上面的结果可能不是我们所期望的,因为我们给了calendar图标外边距。 没有背景颜色的额外空白。想一下,这与我们之前在应用.frame()修饰符时遇到的情况完全相同。调用.padding()实际上并没有改变我们之前的视图和修饰符,它只是在前面表达式的结果周围添加了空白而已。

事实上,如果我们在调用.padding()之后添加第二个.background()修饰符,就会更加清晰的展示这种情况。因为第二个背景颜色将在padding内渲染出来:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .background(Color.red)
            .padding()
            .background(Color.blue)
    }
}
padded-calendar.png

因此,如果我们希望添加内边距inner padding,我们需要在添加背景之前使用padding()修饰符——就像这样:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .padding()
            .background(Color.red)
    }
}

每个修饰符modifier本质上都将它调用的视图包装在另一个视图中,如果我们在应用.frame()修饰符之前调用.padding() ,我们的图标将缩小,因为padding将应用在我们固定的50x50容器中——迫使我们调整图像尺寸以适应较小的尺寸:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .padding()
            .frame(width: 50, height: 50)
            .background(Color.red)
    }
}
smaller-calendar.png

为了完成我们的calendar图标视图,让我们对其应用cornerRadius并使其前景色为白色——最后将所有代码提取到一个名为CalendarView的新视图中,如下所示:

struct CalendarView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .padding()
            .background(Color.red)
            .cornerRadius(10)
            .foregroundColor(.white)
    }
}
finished-calendar.png

一般而言,每当我们完成一个独立的功能组件 UI 部分时,将代码提取到新View实现中通常是个好主意,以避免出现臃肿视图。

Stacks and spacers

SwiftUI 中的各种stacks和spacers乍一看可能非常简单和有限,实际上却可以用来表达几乎无限的布局组合:

struct ContentView: View {
    var body: some View {
        VStack {
            CalendarView()
        }
    }
}

上述内容VStack实际上根本不会影响我们的布局,因为 在SwiftUI中stacks不会拉伸自己以占据其父级 。相反,它们只是根据其子级的总大小调整自己的大小。

要移动我们的CalendarView,我们还必须将Spacer添加到我们的stacks中。在HStack或者VStack中放置spacers时,spacers总是占据尽可能多的空间,在下面代码中,渲染后我们的CalendarView会被推到屏幕顶部:

struct ContentView: View {
    var body: some View {
        VStack {
            CalendarView()
            Spacer()
        }
    }
}
calendar-at-top.png

stacks的酷炫之处在于它们可以通过嵌套来构建日益复杂的布局,而无需任何形式的手动frame计算。

struct ContentView: View {
    var body: some View {
        HStack {
            VStack {
                CalendarView()
                Spacer()
            }
            Spacer()
        }.padding()
    }
}
calendar-at-leading-top.png

接下来,让我们向视图中添加一个Text,以模拟calendar事件的一组详细信息。由于在本文中我们将坚持仅探索 SwiftUI 的布局系统,因此我们现在将硬编码我们的内容Text

struct ContentView: View {
    var body: some View {
        HStack {
            VStack {
                CalendarView()
                Spacer()
            }
            Text("Event title").font(.title)
            Spacer()
        }.padding()
    }
}
calendar-with-text.png

看看上面的代码,我们希望Text出现在CalendarView水平轴的右边。

struct ContentView: View {
    var body: some View {
        HStack(alignment: .top) {
            VStack {
                CalendarView()
                Spacer()
            }
            Text("Event title").font(.title)
            Spacer()
        }.padding()
    }
}
calendar-with-top-text.png

下面我们继续在文字视图下面添加一个用来描述的文字视图:

struct ContentView: View {
    var body: some View {
        HStack(alignment: .top) {
            VStack {
                CalendarView()
                Spacer()
            }
            VStack(alignment: .leading) {
                Text("Event title").font(.title)
                Text("Location")
            }
            Spacer()
        }.padding()
    }
}

然而,虽然上述布局有效,但可以被简化,以便更容易阅读代码。

因此,将ContentViewbody代码提取到一个专用组件中,同时对其进行重构并命名为EventHeader

struct EventHeader: View {
    var body: some View {
        HStack(spacing: 15) {
            CalendarView()
            VStack(alignment: .leading) {
                Text("Event title").font(.title)
                Text("Location")
            }
            Spacer()
        }
    }
}
event-header.png

回到我们的ContentView,我们现在可以将它的主体变成一个VStack包含我们的新EventHeader组件以及Spacer,使我们的布局代码更容易理解:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            Spacer()
        }.padding()
    }
}

只要有可能,最好不断地将ContenView中的代码提取到专用组件中。以这种方式编码通常可以让我们自然地将我们的 UI 分成原子部分,而无需我们预先进行大量的架构设计工作。

ZStacks 和 offset

最后,让我们快速了解一下 SwiftUI 的ZStack类型,它使我们能够使用从后到前的顺序,在深度方面堆叠一系列视图。

举个例子,假设在之前的calendar视图顶部添加显示一个小的“验证徽章”的支持。在 top-trailing 放置一个checkmark图标。为了以更通用的方式实现它,让我们对View进行扩展,将所有视图包装在ZStack内(它本身不会影响视图的布局),它还可以选择包含我们的checkmark图标 :

extension View {
    func addVerifiedBadge(_ isVerified: Bool) -> some View {
        ZStack(alignment: .topTrailing) {
            self

            if isVerified {
                Image(systemName: "checkmark.circle.fill")
                    .offset(x: 3, y: -3)
            }
        }
    }
}

请注意ZStack如何在alignment上为我们提供对其的完整二维控制,我们可以使用它来将我们的图标定位在父视图的top-trailing。然后我们还将.offset()modifier应用到我们的徽章上,这会将它稍微移动到其父视图的边界之外。

有了上述内容,我们现在可以有条件地将我们的新徽章添加到我们CalendarView上,在eventIsVerified属性设置为true时可以显示出来:

struct CalendarView: View {
    var eventIsVerified = true

    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .padding()
            .background(Color.red)
            .cornerRadius(10)
            .foregroundColor(.white)
            .addVerifiedBadge(eventIsVerified)
    }
}
verified-event.png

ZStack.offset()修饰符一起使用是向视图添加各种叠加层的好方法,而根本不会影响该视图自己的布局。我们可以使用该技术来实现进度加载、应用内通知以及我们希望在现有视图层次结构之上呈现的许多其他类型的视图。

结论

让我们总结一下到目前为止我们所涵盖的内容:

  • SwiftUI 的核心布局引擎的工作原理是要求每个子视图根据其父视图的边界确定自己的大小,然后要求每个父视图将其子视图定位在自己的边界内
  • 视图修饰符View modifiers通常将当前视图包装在另一个视图中,这就是为什么我们可以根据调用修饰符的顺序获得完全不同的布局结果。
  • 使用.frame().padding()修饰符可以让我们调整视图的大小和内部边距,只要该视图配置为相应地调整自身大小。
  • 使用HStackVStackZStack我们可以一起在水平方向,垂直或深度方向排列视图。
  • 使用offset()我们可以移动视图而不影响其周围环境,这在实现叠加和其他类型的视图重叠时非常有用。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容