在上一节的基础上,我们继续,在这节中,我们会一起布局一个简单的View。同时也会提到一些小Tip,例如环境变量的使用(这里的环境变量可不是命令行里的export哟)。
Views & Modifiers
Xcode 11 将控件库进行了改版,主要分为了两类,Views 和 Modifiers。顾名思义,Views即使原先的控件,Button,Label等等;Modifiers就是一些提供美化效果的组件。打个可能不太恰当的比喻,Views就是构图,Modifiers就是PS调色,精修。
布局
我们想想原来iOS的布局怎么做?两种方式,frame和autolayout,两者有什么共同点呢?要想把一个UI元素放在想要的位置,必须给这个元素设置点什么(宽,高,起点坐标或是autolayout的相对位置之类的),虽说有点繁琐,但很灵活,带来高度灵活性的同时必将引入复杂度,有过布局复杂控件的小伙伴一定有过这样的感受,无论是使用storyboard系,或是代码流。
而SwiftUI呢?我们发现我们没有设置任何东西,UI元素就好好的呆在那里。代码量少了,但转念一想,高度自动化是否会影响灵活性呢?我们来一探究竟。先画一个cell吧,绘制一个简化的微信聊天列表页的cell。
- 头像,先来个头像,一个Image,参数为拖到Xcode里的图片名字。
Tip: 预览代码里的
previewLayout(.fixed(width: 320, height: 60))
是为了在canvas中显示指定宽高的一个view。
struct DemoView: View {
var body: some View {
Image("avatar")
}
}
struct DemoView_Previews: PreviewProvider {
static var previews: some View {
Group {
DemoView().previewLayout(.fixed(width: 320, height: 60))
}
}
}
此处,我们没有设置图片的位置、大小以及contentMode。它的显示效果在说明系统给了我们一些默认值比如:图片原尺寸展示,多出部分被裁掉了,水平居中了(其实垂直方向也只居中的,只要更改
previewLayout
的高度就可以看出来)。
先来说说尺寸问题,不知道大家还记不记得UIKit时代的intrinsic属性,比如UILabel,当给它设置Text值,它就可以自己撑出宽高。OK,图片此时大小就是它图片的原本大小,就是intrinsic size。再来看看居中的问题,SwiftUI给元素了一个默认位置就是居中。
小结一下,将一个元素放在View里,默认居中,如果有intrinsic size,则按这个size来绘制宽高。
如果没有intrinsic size呢?我们更改一下代码,用Color来替换Image。
struct DemoView: View {
var body: some View {
Color(red: 0.5, green: 0.5, blue: 0.5)
}
}
它会撑满这个view,它的效果等同于flex:1
。属性flexbox布局的小伙伴一定有这样的感受。了解了默认的布局属性后,我们来完成目标布局:
基础布局 -- 元素放置
先用一个水平布局将cell一分为二,左侧的头像使用一个图片,利用它具有固有属性,固定的宽高,右侧有一个白色的Color,利用它充满剩余空间的。
struct DemoView: View {
var body: some View {
HStack() {
Image("avatar").resizable().aspectRatio(contentMode: .fit)
Color(red: 0.5, green: 0.5, blue: 0.5)
}
}
}
Tips:
resizable
将图片框定在父视图里,aspectRatio
不用多解释了吧。
此处的Color用了一个灰色,看一下效果:
中间怎么莫名的多了一条白色,苹果爸爸默认在视图间加入了一个美感间距。可是我们在做布局呀,如何剔除它呢?
HStack(spacing: 0) {...}
不错哟,有兴趣的小伙伴可以看看HStack的定义:
public struct HStack<Content> : View where Content : View {
/// Creates an instance with the given `spacing` and Y axis `alignment`.
///
/// - Parameters:
/// - alignment: the guide that will have the same horizontal screen
/// coordinate for all children.
/// - spacing: the distance between adjacent children, or nil if the
/// stack should choose a default distance for each pair of children.
@inlinable public init(alignment: VerticalAlignment = .center,
spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
...
}
spacing属性就是干这个的,同时我们看到了另一个默认参数VerticalAlignment = .center
,这个就解释了默认的居中。
接下来,我们看一下文字部分,我们要把文字放在背景上方,这里就要引入另一个布局方式ZStack
(Z -> z轴)
struct DemoView: View {
var body: some View {
HStack(spacing: 0) {
Image("avatar").resizable().aspectRatio(contentMode: .fit)
ZStack {
Color(red: 0.5, green: 0.5, blue: 0.5)
Text("Title")
}
}
}
}
发现文字被居中了,还记得刚刚看到的HStack构造函数的第一个参数么,ZStack也有这样的一个参数,改为leading
,靠左咯。由于我们要显示两行文字,这里就需要嵌套一个VStack,同时设置横向布局方式以及相应的padding:
struct DemoView: View {
var body: some View {
HStack(spacing: 0) {
Image("avatar").resizable().aspectRatio(contentMode: .fit).padding()
ZStack(alignment: .leading) {
Color(red: 0.5, green: 0.5, blue: 0.5)
VStack(alignment: .leading, spacing: 10) {
Text("Title")
Text("This is the latest message")
}.padding()
}
}
}
}
Tips: 在canvas上显示的有点不对,不过当render在正常屏幕上是OK哒
精修一下 -- 勾勒细节
勾勒细节就要用到modifier
了。
我们先给图片加一个圆角,会用到cornerRadius
,代码如下:
Image("avatar").resizable().aspectRatio(contentMode: .fit).cornerRadius(10).padding()
接着,给Title
加粗:
Text("Title").bold()
还有很多modifier,大家可以慢慢探索
番外篇
环境变量的使用
环境变量的使用,感受一下iOS里的黑暗模式(dark mode)。基于SwiftUI开发的App,不用做任何改变,在设置里更改为黑暗模式,App自动进行了切换。在开发过程中呢?只需要添加一句.environment(\.colorScheme, .dark)
。就可以在canvas里看到。
那么如果同时向看到两种模式呢?加个Group
。代码如下所示:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView(rGuess: 0.7, gGuess: 0.3, bGuess: 0.6, showAlert: false).environment(\.colorScheme, .dark)
ContentView(rGuess: 0.7, gGuess: 0.3, bGuess: 0.6, showAlert: false).environment(\.colorScheme, .light)
}
}
}
按照这个代码敲一遍 => "说好的黑暗模式呢?" WTF。
这个应该是Xcode的Bug,若想看到需要将ContentView的根视图包裹在NavigationView
里 => Awesome,黑暗模式如约而至。
NavigationView {
VStack { ... }
}
Modifiers的调用顺序将影响最终效果
Text("This is the latest message").background(Color.red).cornerRadius(10)
改变background
和cornerRadius
的调用顺序会看到不一样的结果哟。如果background
在后,将看不到圆角。如何理解,我想大家心里已经有了答案。
我们用一个cornerRadius
来解释一下,这个函数是View的一个extension,
返回值为一个View,那么很明显这是一个链式调用。但有一点需要注意,View是一个struct,对于一个struct,如果修改的是自己那么这个函数签名需要添加一个mutating
,而cornerRadius
并没有这个标示,所以通过cornerRadius
将创建出一个新的View。
/// Clips this view to its bounding frame, with the specified corner radius.
///
/// By default, a view's bounding frame only affects its layout, so any
/// content that extends beyond the edges of the frame remains visible.
/// Use the `cornerRadius()` modifier to hide any content that extends
/// beyond these edges while applying a corner radius.
///
/// The following code applies a corner radius of 20 to a square image:
///
/// Image(name: "walnuts")
/// .cornerRadius(20)
///
/// - Parameter antialiased: A Boolean value that indicates whether
/// smoothing is applied to the edges of the clipping rectangle.
/// - Returns: A view that clips this view to its bounding frame.
@inlinable public func cornerRadius(_ radius: CGFloat, antialiased: Bool = true) -> some View
这是系列教程的第二弹了,下一篇我解释state,binding等和数据绑定相关的内容。如何大家有任何反馈,请给我留言吧。