系统: Mac OS 10.15.1, XCode 11.2.1,swift 5.0
写作时间:2019-11-22
Swift 5以后 ABI 稳定了。(参考: ABI Stability and More 和 Evolving Swift On Apple Platforms After ABI Stability )
就是 binary 接口稳定,也就是在运行的时候只要是用 Swift 5 (或以上) 的编译器编译出来的 binary,就可以跑在任意的 Swift 5 (或以上) 的 runtime 上。这样,我们就不需要像以往那样在 app 里放一个 Swift runtime 了,Apple 会把它弄到 iOS 和 macOS 系统里。
是的,但是这是 Apple 通过 App Thinning 帮我们完成的,不需要你操心。在提交 app 时,Apple 将会按照 iOS 系统创建不同的下载包。对于 iOS 12.2 的系统,因为它们预装了 Swift 5 的 runtime,所以不再需要 Swift 的库,它们会被从 app bundle 中删掉。对于 iOS 12.2 以下的系统,外甥打灯笼,照旧。
一个新创建的空 app,针对 iOS 12.2 打包出来压缩后的下载大小是 26KB,而对 iOS 12.0 则是 2.4MB。如果你使用了很多标准库里的东西,那这个差距会更大 (因为没有用到的标准库的符号会被 strip 掉),对于一个比较有规模的 app 来说,一般可以减小 10M 左右的体积。
因为系统集成了 Swift,所以大家都用同一个 Swift 了,app 启动的时候也就不需要额外加载 Swift,所以在新系统上会更快更省内存。当然啦,只是针对新系统。
下面就从0开始,打造上面的效果的例子。
实战的例子来源于:SwiftUI: Getting Started
创建Xcode project (Shift-Command-N), 选择 iOS ▸ Single View App, 名字叫RGBullsEye, User Interface选择 SwiftUI。
打开分组RGBullsEye
,将看到: 以前的AppDelegate.swift
,现在分为AppDelegate.swift
和 SceneDelegate.swift
, SceneDelegate
有window
设置:
SceneDelegate
没有明确配置SwiftUI
, 但是下面代码有:
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
// ...
window.rootViewController = UIHostingController(rootView: contentView)
// ...
当app启动, window
显示实例ContentView
, 它定义在文件ContentView.swift
. 它是一个结构体struct
遵循协议View protocol
(老逻辑是View对象):
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
SwiftUI
定义了ContentView
的body
, body
包含了一个Text view
显示文字Hello World
.
预览结构体ContentView_Previews
生产一个view
包含了ContentView
实例.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
如何显示预览效果?点击右边视图的Resume
按钮
预览如下:
如果没有看到Resume
按钮,选择editor options, 选择Canvas,或者快捷键command+option+enter
把Text("Hello World")
替换为如下:
Text("Target Color Block")
点击Resume
按钮,看看文字是否已经改变。
Command+点击在右侧预览界面的Text view
, 选择 Embed in HStack
:
看到左侧代码已经更新(实际上手动写代码,点击Resume视图也跟着更新)
HStack {
Text("Target Color Block")
}
拷贝粘贴Text
代码, 更新HStack
如下.
HStack {
Text("Target Color Block")
Text("Guess Color Block")
}
发现不需要逗号,效果是两个Text View在同一行:
Notice you don’t separate the two statements with a comma — just write each on its own line:
准备添加滑动块视图,在横向容器HStack
里加入纵向容器VStack
. — 在代码里Command-点击HStack
, 选择Embed in VStack
:
在HStack
闭包下面换行,点击右上角**+**按钮,打开控件库Library
, 拖拽一个Vertical Stack
在换行的位置:
添加Text View
代码如下,预览如下:
完成以上UI布局,代码如下。(实际上,直接写代码就好,跟拖拽控件一样的效果)
VStack {
HStack {
Text("Target Color Block")
Text("Guess Color Block")
}
Text("Hit me button")
VStack {
Text("Red slider")
Text("Green slider")
Text("Blue slider")
}
}
在横向容器里,添加两个纵向容器VStack
, 里面都有长方形Rectangle
。 左边的纵向容器表示目标颜色,右边的纵向容器表示用户可以改变的颜色。
HStack {
// Target color block
VStack {
Rectangle()
Text("Match this color")
}
// Guess color block
VStack {
Rectangle()
HStack {
Text("R: xxx")
Text("G: xxx")
Text("B: xxx")
}
}
}
@State
修饰的属性,表示属性更新,界面也同步更新。
在结构体ContentView里面,在body闭包的上面添加如下属性:
let rTarget = Double.random(in: 0..<1)
let gTarget = Double.random(in: 0..<1)
let bTarget = Double.random(in: 0..<1)
@State var rGuess: Double
@State var gGuess: Double
@State var bGuess: Double
R, G, B 的值在区间[0,1). 初始化target的值. 需要初始化Guess的值为0.5.
向下滚动到结构体ContentView_Previews
, 实例化ContentView
需要改为带参数,修改如下:
ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5)
同样需要修改在类SceneDelegate
, 的方法scene(_:willConnectTo:options:)
— 替换 ContentView()
为:
let contentView = ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5)
给目标矩形添加颜色
Rectangle()
.foregroundColor(Color(red: rTarget, green: gTarget, blue: bTarget, opacity: 1.0))
给猜测矩形添加颜色
Rectangle()
.foregroundColor(Color(red: rGuess, green: gGuess, blue: bGuess, opacity: 1.0))
在没有重用之前, 在滑块在VStack
, 替换代码 Text("Red slider")
如下:
HStack {
Text("0")
.foregroundColor(.red)
Slider(value: $rGuess)
Text("255")
.foregroundColor(.red)
}
Text views
的颜色为红色. 初始化滑块的值(0.5) . 滑块的值范围为[0, 1].$
修饰变量rGuess
,表示可读可写绑定 — 当修改滑块的值的时候,需要同步更新猜测矩形的颜色.为了看出是否用修饰符$
的区别,把如下代码添加到猜测矩形的下面
HStack {
Text("R: \(Int(rGuess * 255.0))")
Text("G: \(Int(gGuess * 255.0))")
Text("B: \(Int(bGuess * 255.0))")
}
预览效果如下:
上面的滑块有点挤,添加一点空间,用.padding()
如下
HStack {
Text("0")
.foregroundColor(.red)
Slider(value: $rGuess)
Text("255")
.foregroundColor(.red)
}
.padding()
预览效果会宽一些
上面实现了红色滑块,实际上绿色、蓝色模块都是相同的逻辑,这里就要重构为公用方法。
Command-点击 红色滑块的HStack
, 选择 Extract Subview
:
右键Refactor ▸ Extract to Function一样可以达到上面重构的效果.
把ExtractedView
命名为ColorSlider
, 在body闭包的上面添加代码:
@Binding var value: Double
var textColor: Color
替换 $rGuess
为 $value
, 替换 .red
为 textColor
:
Text("0")
.foregroundColor(textColor)
Slider(value: $value)
Text("255")
.foregroundColor(textColor)
回到VStack
调用方法ColorSlider()
的地方, 添加参数:
ColorSlider(value: $rGuess, textColor: .red)
就是上面代码的下面,添加绿色滑块,蓝色滑块。
ColorSlider(value: $gGuess, textColor: .green)
ColorSlider(value: $bGuess, textColor: .blue)
点击Resume按钮,或者command + option + p 是静态预览,也就是没法拖动滑块。如果想要动态预览,需要点击预览界面,右下角的播放按钮。
当两边矩形颜色差不多的时候,点击Hit Me
按钮,弹出分数。
在body
的上面添加处理分数的方法
func computeScore() -> Int {
let rDiff = rGuess - rTarget
let gDiff = gGuess - gTarget
let bDiff = bGuess - bTarget
let diff = sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff)
return Int((1.0 - diff) * 100.0 + 0.5)
}
diff
值表示两个点之间的差距,求3种颜色的空间距离。最后用1 - diff
就是相同比例.
接着,替换Text("Hit me button")
为如下:
Button(action: {
}) {
Text("Hit Me!")
}
按钮有事件和文字。如果在Button
点击事件里面加Alert View
, 将会没有响应.
反而, 创建Alert
作为ContentView
的subview
, 并添加@State
修饰的Bool
类型变量. 在Button
点击事件里把这个变量值修改为true
, Alert
将会显示. 当用户点击Alert里面的按钮,Alert消失后,这个变量就自动变成false
.
增加@State
修饰的遍历,初始化为false
:
@State var showAlert = false
在Button的事件里添加
self.showAlert = true
最后添加Alert
的实现方法,最终代码如下:
Button(action: {
self.showAlert = true
}) {
Text("Hit Me!")
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Your Score"), message: Text("\(computeScore())"))
}
恭喜你!已经学会SwiftUI
的应用。
想要入门 SwiftUI
的使用,那 Apple 这次给出的官方教程绝对给力。
https://github.com/zgpeace/RGBullsEye
https://www.raywenderlich.com/3715234-swiftui-getting-started
https://onevcat.com/2019/02/swift-abi/
https://developer.apple.com/tutorials/swiftui/tutorials