SwiftUI实战一:从入门到精通

系统: 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 )

什么是 ABI 稳定?

就是 binary 接口稳定,也就是在运行的时候只要是用 Swift 5 (或以上) 的编译器编译出来的 binary,就可以跑在任意的 Swift 5 (或以上) 的 runtime 上。这样,我们就不需要像以往那样在 app 里放一个 Swift runtime 了,Apple 会把它弄到 iOS 和 macOS 系统里。
SwiftUI实战一:从入门到精通_第1张图片

app 尺寸会变小?

是的,但是这是 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,所以在新系统上会更快更省内存。当然啦,只是针对新系统。

ABI稳定后Apple憋了个一统江湖的新框架SwiftUI.

  1. 这个框架相当简洁,拖拽控件跟写代码实现达到一致性,不需要额外的关联(去掉了@IBAction, @IBOutlet).
  2. 写一套代码在Apple所有产品通用(iPhone, iPad, AppleWatch, AppleTV).
  3. 可见即可得,写完代码,点击右边的Resume按钮,或者用快捷键command+option+p, 就可以看到运行起来的样子。
  4. 模拟运行:如果点击右下角的预览按钮,跟模拟器运行的效果是一样的。
    SwiftUI实战一:从入门到精通_第2张图片

下面就从0开始,打造上面的效果的例子。
实战的例子来源于:SwiftUI: Getting Started

创建工程

创建Xcode project (Shift-Command-N), 选择 iOS ▸ Single View App, 名字叫RGBullsEye, User Interface选择 SwiftUI。
SwiftUI实战一:从入门到精通_第3张图片

SwiftUI是怎么启动的

打开分组RGBullsEye,将看到: 以前的AppDelegate.swift,现在分为AppDelegate.swiftSceneDelegate.swiftSceneDelegatewindow设置:
SwiftUI实战一:从入门到精通_第4张图片
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 定义了ContentViewbodybody包含了一个Text view 显示文字Hello World.

预览结构体ContentView_Previews 生产一个view包含了ContentView实例.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

如何显示预览效果?点击右边视图的Resume按钮
在这里插入图片描述
预览如下:
SwiftUI实战一:从入门到精通_第5张图片
如果没有看到Resume按钮,选择editor options, 选择Canvas,或者快捷键command+option+enter
SwiftUI实战一:从入门到精通_第6张图片

编写UI占位结构

Text("Hello World") 替换为如下:

Text("Target Color Block")

点击Resume按钮,看看文字是否已经改变。

Command+点击在右侧预览界面的Text view, 选择 Embed in HStack:
SwiftUI实战一:从入门到精通_第7张图片
看到左侧代码已经更新(实际上手动写代码,点击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:
SwiftUI实战一:从入门到精通_第8张图片

HStack闭包下面换行,点击右上角**+**按钮,打开控件库Library, 拖拽一个Vertical Stack 在换行的位置:SwiftUI实战一:从入门到精通_第9张图片
添加Text View代码如下,预览如下:
SwiftUI实战一:从入门到精通_第10张图片
完成以上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修饰属性

@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))

点击Resume按钮预览如下:
SwiftUI实战一:从入门到精通_第11张图片

滑块控件复用View

在没有重用之前, 在滑块在VStack, 替换代码 Text("Red slider") 如下:

HStack {
  Text("0")
    .foregroundColor(.red)
  Slider(value: $rGuess)
  Text("255")
    .foregroundColor(.red)
}

  1. 更新Text views的颜色为红色. 初始化滑块的值(0.5) . 滑块的值范围为[0, 1].
  2. $修饰变量rGuess,表示可读可写绑定 — 当修改滑块的值的时候,需要同步更新猜测矩形的颜色.

为了看出是否用修饰符$的区别,把如下代码添加到猜测矩形的下面

HStack {
  Text("R: \(Int(rGuess * 255.0))")
  Text("G: \(Int(gGuess * 255.0))")
  Text("B: \(Int(bGuess * 255.0))")
}

预览效果如下:
SwiftUI实战一:从入门到精通_第12张图片
上面的滑块有点挤,添加一点空间,用.padding() 如下

HStack {
  Text("0")
    .foregroundColor(.red)
  Slider(value: $rGuess)
  Text("255")
    .foregroundColor(.red)
}
  .padding()

预览效果会宽一些
SwiftUI实战一:从入门到精通_第13张图片
上面实现了红色滑块,实际上绿色、蓝色模块都是相同的逻辑,这里就要重构为公用方法。

Command-点击 红色滑块的HStack, 选择 Extract Subview:
SwiftUI实战一:从入门到精通_第14张图片
右键Refactor ▸ Extract to Function一样可以达到上面重构的效果.
ExtractedView命名为ColorSlider, 在body闭包的上面添加代码:

@Binding var value: Double
var textColor: Color

替换 $rGuess$value, 替换 .redtextColor:

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)

预览效果如下:
SwiftUI实战一:从入门到精通_第15张图片

点击Resume按钮,或者command + option + p 是静态预览,也就是没法拖动滑块。如果想要动态预览,需要点击预览界面,右下角的播放按钮。
SwiftUI实战一:从入门到精通_第16张图片

动态预览成功如下:SwiftUI实战一:从入门到精通_第17张图片

提交数据,展示分数

当两边矩形颜色差不多的时候,点击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作为ContentViewsubview, 并添加@State修饰的Bool类型变量. 在Button点击事件里把这个变量值修改为trueAlert将会显示. 当用户点击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实战一:从入门到精通_第18张图片
如果在模拟器中运行,还可以翻转查看:
SwiftUI实战一:从入门到精通_第19张图片

总结

恭喜你!已经学会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

你可能感兴趣的:(iOS)