SwiftUI - Declarative UI

上一节的基础上,我们继续,在这节中,我们会一起布局一个简单的View。同时也会提到一些小Tip,例如环境变量的使用(这里的环境变量可不是命令行里的export哟)。

Views & Modifiers

Xcode 11 将控件库进行了改版,主要分为了两类,Views 和 Modifiers。顾名思义,Views即使原先的控件,Button,Label等等;Modifiers就是一些提供美化效果的组件。打个可能不太恰当的比喻,Views就是构图,Modifiers就是PS调色,精修。

library-views.png
library-modifiers.png

布局

我们想想原来iOS的布局怎么做?两种方式,frame和autolayout,两者有什么共同点呢?要想把一个UI元素放在想要的位置,必须给这个元素设置点什么(宽,高,起点坐标或是autolayout的相对位置之类的),虽说有点繁琐,但很灵活,带来高度灵活性的同时必将引入复杂度,有过布局复杂控件的小伙伴一定有过这样的感受,无论是使用storyboard系,或是代码流。

而SwiftUI呢?我们发现我们没有设置任何东西,UI元素就好好的呆在那里。代码量少了,但转念一想,高度自动化是否会影响灵活性呢?我们来一探究竟。先画一个cell吧,绘制一个简化的微信聊天列表页的cell。

  1. 头像,先来个头像,一个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))
        }
    }
}

image1.png

此处,我们没有设置图片的位置、大小以及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布局的小伙伴一定有这样的感受。了解了默认的布局属性后,我们来完成目标布局:

frame.png

基础布局 -- 元素放置

先用一个水平布局将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用了一个灰色,看一下效果:

frame2.png

中间怎么莫名的多了一条白色,苹果爸爸默认在视图间加入了一个美感间距。可是我们在做布局呀,如何剔除它呢?

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()
frame3.png

还有很多modifier,大家可以慢慢探索


modifier.png

番外篇

环境变量的使用

环境变量的使用,感受一下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 { ... }
}
dark mode.png

Modifiers的调用顺序将影响最终效果

Text("This is the latest message").background(Color.red).cornerRadius(10)

改变backgroundcornerRadius的调用顺序会看到不一样的结果哟。如果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等和数据绑定相关的内容。如何大家有任何反馈,请给我留言吧。

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

推荐阅读更多精彩内容