转自链接
知道 SwiftUI 的人,都会很想使用这个框架,但是由于只能在 iOS 13以及以后的系统使用,所以会很痛心。像我就觉得为什么 SwiftUI 不开源啊!开源多好,将底层渲染替换一下,就能用 SwiftUI 在 Android 上编程了!对 Flutter 也有冲击。但是就像我在文中说的 Swift5.1 引入的二进制库稳定导致,苹果可以不开源我们就能使用 SwiftUI.framework。(好痛心,宁愿不要二进制库稳定!)
幸运的是,SwiftUI 之所以能够出现,很大程度上得益于 Swift5.1 引入的新特性属性包装器(文中有介绍,但是坑爹的貌似不支持 Markdown 文内链接)以及到目前为止官方都没有披露的 @_functionBuilder
。虽然由于需要支持 iOS 13 以前系统的原因不能使用 SwiftUI,但是只要我们使用 Swift5.1 就可以使用 Swift 的这两个新特性。这样一来,在 iOS 13 以前我们可以开发另一套界面描述 DSL,同时也能够很容易支持数据的双向绑定,如此一来,从 Swift5.1 开始 iOS 开发人员编写界面的方式会有很大的变化,从以前的命令式过渡到现在的声明式。毕竟 SwiftUI 的最大讨好点就是声明式的界面编写。(从这一点来说我反而比较担心 SwiftUI 可能并不会被开发者青睐。因为毕竟不支持 iOS 13 以前的系统,而且使用 Swift5.1 也很容易的实现声明式的 UI 框架,不过考虑到 SwiftUI 是跨 iOS 和 macOS 平台的,所以,可能并不是我想的那么不被青睐)
最后一句总结,由于属性包装器以及 @_functionBuilder
的存在,从 Swift5.1 开始,Swift 成为一门真正的支持高效 UI 编程的语言,一门高效的用于开发 DSL 的语言,一门更加易用的语言!
前言
视频地址:What's New in Swift
本文介绍视频中的主要内容。(以及夹带一些私货)
二进制库稳定
ABI 稳定
Swift 5 带来了 ABI 的稳定。但是用 Swift 写的二进制框架并没有稳定。
先说一下什么是 ABI(Application Binary Interface),打个比方,代码中调用一个函数,需要传入几个参数,调用结束需要返回结果,编译器如何将参数传给被调用者,以及如何将结果返回给调用方,就是 ABI 涉及的内容。ABI 不稳定意味则,如何传参数和返回参数还没有最终确定(当然这只是举例,实际的编译器实现很复杂,ABI 涉及很多方面)。ABI 不稳定导致的结果就是,使用 Swift 5 以前的版本编写 iOS app,Xcode 需要同时将 ABI 相关的 Swift 核心库编译到你的 app 中,直观的感受就是你的 ipa 包变大了。
⚠️ iOS 12.2 系统内置 Swift 核心库所以 Swift5 编写的 app 不需要内置 Swift 核心库了,但是 Xcode 在打包的时候仍然会将 Swift5 核心库集成到 ipa 里面,这样 Swift5 写的 app 就能运行在 iOS12.2 之前系统。只是对于 iOS 12.2 及以后的系统安装该 app 的时候,系统不需要下载 app 中 Swift 的核心库了,减少了下载体积和安装大小。(同时 app 启动也变快了,因为系统 dyld3 缓存了对应的 Swift 核心库,官方说能带来 5% 的启动速度提升)
️ 问题来了,如果你用 Swift5.1 编写的 app,安装在 iOS12.2 上面,需要下载并且安装 Swift5.1 对应的核心库吗?
ABI 稳定之后就是模块稳定了。
模块稳定
模块不稳定指的是,用 Swift5.1 以前编写的模块,如果想要提供给第三方的话,需要把这个模块的代码也提供第三方,这样他们才能使用这个模块。当然这对于某些模块的开发者来说是不可接受的(毕竟是把模块的源代码给别人了)。模块库稳定指的是,用 Swift5.1,库开发者不需要提供源代码给第三方了,只需要提供一个类似头文件的东西给第三方,第三方就可以使用了。
顺便简单说一下 Swift 是如何使用模块。如下图所示:
可以知道 Swift5.1 以前,在 Foo.swift 中 import MyFramework 的话,Swift 会自动在 Foo.swift 中导入 MyFramework 的“头文件”亦即 swiftmodule 文件,这个文件是编译器在编译 MyFramework 的时候生成的。如此一来,就可以在 Foo.swift 中任意使用 MyFramework 中的类、结构体、函数等了,比如图中的 doSomething 方法。
Swift5.1 也差不多,如下图:
只是将 swiftmodule 文件改成了 swiftinterface 文件。当然 swiftinterface 对于库开发者来说应该是可以编辑的。库开发者需要将这个文件和二进制文件提供给第三方。
ABI 和模块的稳定意味者二进制库的稳定。
对于更多关于 Swift 库稳定相关的内容可以查看本文引用中的链接。
️ 问题来了, 如果第三方用的是 Swift5.1 以前的版本开发的话,那么库开发者(使用 Swift5.1)是否需要源码呢?
Swift5.1 的优化
- 编译出来的二进制更小了,通常是 10% 的减少,如果开启 ‘Optimize for Size‘ 就是 15% 的减少。
- Swift 和 ObjC 的桥接更快了
- NSString 和 String 桥接有15x 的提升
- NSDictionary 和 Dictionary 有 1.6x 的提升
- 大小比较小的 String 的优化
这些对 String 和 Dictionary 的优化使得 SwiftNIO 请求处理速度提升了 20%。
SwiftNIO 是 Swift 官方出的跨平台异步事件驱动的网络应用框架
Swift 开源相关
- Swift 官方 docker 镜像
- Swift 的 SourcKit (用于代码补全、高亮、重构以及符号跳转到定义等编写 Swift 代码相关的编辑器功能)的压力测试(为什么压力测试,用过 Xcode 的都知道 Xcode 的 Swift 编辑器有多烂 )
- LSP(Language Server Protocol)
LSP 的作用如下所示,简单的说就是代码编辑器有很多比如 Xcode 的 Source Editor、VSCode、Vim 等等,每个编辑器都要支持各种类型代码(比如 Java、Python、Swift 等)编辑相关的操作(高亮、补全等)。
由于 SourceKit 定义的内容被 Xcode 使用,不够通用,没法直接给 VSCode 等第三方编辑器使用,所以需要使用 LSP 协议来做桥接(计算机领域有句名言,所有复杂问题都可以引入另一个中间层来解决)。
Swift 新特性
所有的新特性都可以在 Swift Evolution 中找到。
这里介绍比较重要的一些内容。
单行表达式隐式返回
这个很简单。在以前需要写这种代码:
// Implicit return from single expressions
// Swift Evolution: SE-0255
struct Rectangle {
var width = 0.0, height = 0.0
var area: Double { width * height }.
}.
现在 Swift5.1 可以这样写了
// Implicit return from single expressions
// Swift Evolution: SE-0255
struct Rectangle {
var width = 0.0, height = 0.0
var area: Double { width * height }.
}.
结构体初始化
以前需要这样初始化结构体
// Synthesized default values for the memberwise initializer
// Proposal and implementation by an open source contributor Alejandro Alonso
// Swift Evolution: SE-0242
struct Dog {
var name = "Generic dog name"
var age = 0
}
let boltNewborn = Dog()
let daisyNewborn = Dog(name: "Daisy", age: 0)
let benjiNewborn = Dog(name: "Benji") // 编译错误❌
现在最后一句也能编译通过了。
字符串插值
字符串插值指的是:
let quantity = 10
label.text = "You have \(quantity) apples"
Swift 5.1 以前,不能这样写
let quantity = 10
label.text = NSLocalizedString(
"You have \(quantity) apples"
,
comment: "Number of apples"
) // 编译错误❌
需要改成这样子:
let quantity = 10
let formatString = NSLocalizedString(
"You have %lld apples",
comment: "Number of apples"
)
label.text = String(format: formatString, quantity)
Swift5.1 可以这样写了:
let quantity = 10
return Text(
"You have \(quantity) apples"
,
comment: "Number of apples"
)
其中 Text 来自 SwiftUI 框架,定义如下:
// In SwiftUI.framework
public struct Text {
public init(
_ key: LocalizedStringKey,
tableName: String? = nil,
bundle: Bundle? = nil,
comment: StaticString? = nil
)
}
Swift 会将上述代码转换成这样:
let quantity = 10
return Text(
"You have \(quantity) apples",
comment: "Number of apples"
)
// Generated by the Swift compiler
var builder = LocalizedStringKey.StringInterpolation(
literalCapacity: 16, interpolationCount: 1
).
builder.appendLiteral("You have ")
builder.appendInterpolation(quantity)
builder.appendLiteral(" apples")
LocalizedStringKey(stringInterpolation: builder)
这些得益于 ExpressibleByStringInterpolation 这个新的协议
返回值类型抽象化
为什么要抽象化返回值类型呢?因为不想暴露实现细节,但是在 Swift5.1 以前某些情况下有问题。
如下类型:
// Shapes Example
protocol Shape { /* ... */ }
struct Square: Shape { /* ... */ }
struct Circle: Shape { /* ... */ }
struct Oval: Shape { /* ... */ }
struct Union: Shape { /* ... */ }
struct Transformed: Shape { /* ... */ }
下面这种情况还好:
// API returns different types conforming to the same protocol
// Use Protocol
struct FaceShape {
…
var shape: Shape {
switch faceType {
case .round:
return Circle()
case .square:
return Square()
case .diamond:
return Transformed(Square(), by: .fortyFiveDegrees))
default:
return Oval()
}
}
}
这种情况:
// API returns the same type but is leaking implementation details
struct EightPointedStar {
…
var shape: Union> {
return Union(Square(), Transformed(Square(), by: .fortyFiveDegrees))
}
}
就不能写成下面这样了:
// API returns the same type but is leaking implementation details
// Protocol type?
struct EightPointedStar {
…
var shape: Shape {
return Union(Square(), Transformed(Square(), by: .fortyFiveDegrees))
}
}
为什么呢?因为缺少了类型信息。而且 Swift 的泛型系统支持的不够好,同时阻碍了 Swift 编译器的优化。
在 Swift5.1 中可以这样写了(引入了 some 关键字)
// API returns the same type but is leaking implementation details
// Use Opaque Result Types
struct EightPointedStar {
…
var shape: some Shape {
return Union(Square(), Transformed(Square(), by: .fortyFiveDegrees))
}
}
但是还是不能这样写:
// Opaque Result Types
// Compiler enforces that the same type is returned from the implementation
struct EightPointedStar {
…
var shape: some Shape {
if symmetrical {
return Union(Square(), Transformed(Square(), by: .fortyFiveDegrees))
} else {
return Transformed(Square(), by: .twentyDegrees)
}
}
}
属性包装
很重要!!!
直接上代码,在以前,会有这样的代码:
static var usesTouchID: Bool {
get {
return UserDefaults.standard.bool(forKey: "USES_TOUCH_ID")
}
set {
UserDefaults.standard.set(newValue, forKey: "USES_TOUCH_ID")
}
}
static var isLoggedIn: Bool {
get {
return UserDefaults.standard.bool(forKey: "LOGGED_IN")
}
set {
UserDefaults.standard.set(newValue, forKey: "LOGGED_IN")
}
}
现在,使用 Swift5.1 可以这样了:
// Using UserDefault property wrapper to declare and access properties
@UserDefault("USES_TOUCH_ID", defaultValue: false)
static var usesTouchID: Bool
@UserDefault("LOGGED_IN", defaultValue: false)
static var isLoggedIn: Bool
if !isLoggedIn && usesTouchID {
!authenticateWithTouchID()
}
问题来了:@UserDefault 哪里来的?UserDefault 其实是一个泛型结构体,如下所示:
// The purpose of property wrappers is to wrap a property, specify its access patterns
@propertyWrapper
struct UserDefault {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
…
UserDefaults.standard.register(defaults: [key: defaultValue])
}
var value: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
定义以上内容之后,我们就可以使用 @UserDefault 这种方式了。
熟悉 Java 的人会说这是注解,熟悉 Python 或者 JavaScript 的会说这是装饰器,不管如何,两者有异曲同工的效果!
我以前写过一篇文章介绍这个,感兴趣的可以去看看:Swift 中类似 Java 的注解:Attribute
DSL(Domain Specific Languages,领域特定语言)
视频中 Swift 开发者介绍了 Swift 就有了一项新的能力: 很容易就能支持 DSL!
DSL 是一种描述语言,描述一种特定的内容,比如界面的结构等等。这种描述语言需要编程语言作为载体(或者其他,但是编程语言是其中最好的一个载体)。对 DSL 不熟悉的可以查看文末详细链接。
DSL 简单的比喻就是,将原本字符串描述的内容用编程语言描述出来。比如 XML 这种格式的字符串可以描述很多,比如界面和数据结构等,但是为什么一定要编程语言描述出来呢?
- 因为编程语言有编译器的帮助可以帮助你写出合法、有效的描述,并且支持编译优化,
- 编程语言可以被执行,所以if-else 语句,while、for 语句,等等各种表达式以及对应编程语言的所有特性都能使用,帮助你构建灵活的描述
- 编辑器在编译器的帮助下可以支持代码高亮、补全等等,加快你的开发。
- 编程语言是严谨的精确的,而且开发人员喜欢写代码!而不是写各类配置或者字符串描述,想象一下,你写字会有错别字这种情况。
接下来介绍 Swift 具体是如何支持 DSL 的。
我们现在可以这样子用 Swift5.1 写代码描述一个 HTML 文档了:
html {
head {
title("\(name)'s WWDC19 Blog")
}
body {
h2 { "Welcome to \(name)'s WWDC19 Blog!" }
br()
"Check out the talk schedule and latest news from "
a {
"the source"
}.href("https://developer.apple.com/wwdc19/")
if !loggedIn {
button {
"Log in for comments"
}
.onclick("triggerLogin()")
}
}..
}..
如何实现的呢?
如下所示:
// Functions that construct the HTML objects
public func html(@HTMLBuilder content: () -> HTML) -> HTML { … }
public func head(@HTMLBuilder content: () -> HTML) -> HTML { … }
public func body(@HTMLBuilder content: () -> HTML) -> HTML { … }
可见,这些函数定义使用了上一节说的属性包装器,也就是 HTMLBuilder,有了这个包装器,下面的代码:
head {
meta().charset("UTF-8")
if cond {
title("Title 1")
} else {
title("Title 2")
}
}
在运行时候的效果类型下面这样子:
head {
let a: HTML = meta().charset("UTF-8")
let d: HTML
if cond {
let b: HTML = title("Title 1")
d = HTMLBuilder.buildEither(first: b)
} else {
let c: HTML = title("Title 2")
d = HTMLBuilder.buildEither(second: c)
}
return HTMLBuilder.buildBlock(a, d)
}
嗯,如果关注 WWDC19 的话你会发现,iOS13 引入的新的 UI 编程框架 SwiftUI.framework 这个框架就是像上面这样被实现的!!
不过可惜的是 Swift 支持的 @_functionBuilder
在会上并没有透露出来,这个东西也是笔者在网上找到的,可以查看本文引用中的 Swift functionBuilder
引用
- Apple 介绍 dyld3
- Swift 官方博客关于 ABI 稳定的介绍
- Swift UTF8 String
- SwiftNIO
- Swift SourcKit 压力测试
- LSP
- sourcekit-lsp
- swift-evolution/
- ExpressibleByStringInterpolation
- Opaque Result Types
- New Design for String Interpolation
- SIMD — A Better API for Vector Programming
- Property Wrapper Types
- Domain Specific Languages
- Swift 中类似 Java 的注解:Attribute
- Swift functionBuilder