WWDC Quick Look 💓 By SwiftGGTeam
What's new in Swift

What's new in Swift

观看原视频

Highlight

Swift 5.5 引入了 async/await、结构化并发和 actor,并通过 Swift Collections、Swift Algorithms、Swift System、DocC 等工具补齐包生态、文档、构建性能和日常语法的开发体验。

核心内容

很多 Swift 项目的痛点不在业务代码,而在周边代码。

找一个第三方包,要先搜索网页,再复制 Git URL 到 Xcode。标准库没有双端队列、有序集合、有序字典,团队只能自己写,或者引入来源不一的实现。做系统调用时,低层接口接近 C,类型安全和跨平台体验都不够自然。

异步代码更典型。一个网络请求会拆成 dataTask、completion handler、错误参数和 resume()。真正的业务流程藏在闭包里。你想写 try/catch,却要把每个错误都塞进回调。

Swift 5.5 做了两件事。

第一,Apple 扩展了 Swift 生态。Xcode 13 支持 Swift Package Collections(包集合),Swift 官方开源包新增 Collections、Algorithms、System、Numerics 等能力。DocC 进入 Xcode,用 Markdown 注释生成框架文档。Swift Driver 成为默认编译驱动,增量导入减少了改动模块后的重编译范围。

第二,Swift 语言加入了新的并发模型。async 表示函数可以挂起,await 标记挂起点,async let 让相关任务并行运行,actor 用编译器和运行时协调共享可变状态的访问。

结果很直接。写网络请求时,代码按从上到下的顺序执行。写并行渲染时,子任务不会逃出父函数。写统计计数器时,actor 把并发访问排队,避免多个线程同时修改同一个值。

详细内容

Swift Collections:标准库之外的常用数据结构

06:16)Swift Collections 是新的开源包,补充标准库缺少的数据结构。首版包含 DequeOrderedSetOrderedDictionary

Deque 类似数组,但支持在两端高效插入和删除。

import Collections

var colors: Deque = ["red", "yellow", "blue"]

colors.prepend("green")
colors.append("orange")
// `colors` is now ["green", "red", "yellow", "blue", “orange"]

colors.popFirst() // "green"
colors.popLast() // "orange"
// `colors` is back to ["red", "yellow", "blue"]

关键点:

  • import Collections 引入 Swift Collections 包。
  • var colors: Deque = [...] 创建一个双端队列。
  • colors.prepend("green") 在队首插入元素。
  • colors.append("orange") 在队尾插入元素。
  • colors.popFirst() 从队首删除并返回元素。
  • colors.popLast() 从队尾删除并返回元素。

06:25OrderedSet 同时保留顺序和唯一性。它像数组一样支持按下标访问,像集合一样保证元素只出现一次。

import Collections

var buildingMaterials: OrderedSet = ["straw", "sticks", "bricks"]

for i in 0 ..< buildingMaterials.count {
    print("Little piggie #\(i) built a house of \(buildingMaterials[i])")
}
// Little piggie #0 built a house of straw
// Little piggie #1 built a house of sticks
// Little piggie #2 built a house of bricks

buildingMaterials.append("straw") // (inserted: false, index: 0)

关键点:

  • OrderedSet 保存插入顺序。
  • buildingMaterials.count 返回元素数量。
  • buildingMaterials[i] 按顺序读取元素。
  • append("straw") 尝试插入已有元素。
  • 返回值里的 inserted: false 表示没有插入新元素,index: 0 表示已有元素位置。

06:42OrderedDictionary 适合键值对需要稳定顺序的场景,比如展示 HTTP 状态码列表。

import Collections

var responses: OrderedDictionary = [200: "OK", 403: "Forbidden", 404: "Not Found"]

for (code, phrase) in responses {
    print("\(code) (\(phrase))")
}
// 200 (OK)
// 403 (Forbidden)
// 404 (Not Found)

关键点:

  • OrderedDictionary 保存键值对的顺序。
  • 字面量 [200: "OK", ...] 创建初始内容。
  • for (code, phrase) in responses 按保存顺序遍历键和值。
  • print 输出状态码和原因短语。

Swift Algorithms 与 Swift System:把常见算法和系统调用做成 Swift API

07:39)Swift Algorithms 是新的开源包,提供 Sequence 和 Collection 算法。演讲提到已加入 40 多个算法,包括组合、排列、分组、取最小值、取最大值、随机采样。

import Algorithms

let testAccounts = [ ... ]

for testGroup in testAccounts.uniquePermutations(ofCount: 0...) {
    try validate(testGroup)
}

let randomGroup = testAccounts.randomSample(count: 5)

关键点:

  • import Algorithms 引入 Swift Algorithms 包。
  • testAccounts.uniquePermutations(ofCount: 0...) 生成指定数量范围的唯一排列。
  • for testGroup in ... 逐个验证生成的测试组合。
  • try validate(testGroup) 表示验证过程可能抛出错误。
  • randomSample(count: 5) 从集合中随机取 5 个元素。

07:52)Swift System 提供更符合 Swift 习惯的低层系统调用接口。它支持 Apple 平台、Linux 和 Windows。

import System

let fd: FileDescriptor = try .open(
    "/tmp/a.txt", .writeOnly,
    options: [.create, .truncate], permissions: .ownerReadWrite)
try fd.closeAfter {
    try fd.writeAll("Hello, WWDC!\n".utf8)
}

关键点:

  • import System 引入 Swift System 包。
  • FileDescriptor 用类型表示文件描述符。
  • .open("/tmp/a.txt", .writeOnly, ...) 以只写方式打开文件。
  • options: [.create, .truncate] 表示不存在就创建,已存在就截断。
  • permissions: .ownerReadWrite 设置所有者读写权限。
  • fd.closeAfter { ... } 在闭包执行后关闭文件描述符。
  • fd.writeAll("Hello, WWDC!\n".utf8) 写入 UTF-8 字节。

08:06FilePath 新增路径操作 API,可以查询和设置扩展名、添加组件、删除组件、执行路径规范化。

import System

var path: FilePath = "/tmp/WWDC2021.txt"
print(path.lastComponent)         // "WWDC2021.txt"

print(path.extension)             // "txt"
path.extension = "pdf"            // path == "/tmp/WWDC2021.pdf"
path.extension = nil              // path == "/tmp/WWDC2021"
print(path.extension)             // nil

path.push("../foo/bar/./")        // path == "/tmp/wwdc2021/../foo/bar/."
path.lexicallyNormalize()         // path == "/tmp/foo/bar"
print(path.ends(with: "foo/bar")) // true!

关键点:

  • FilePath 用 Swift 类型表示文件路径。
  • path.lastComponent 读取最后一个路径组件。
  • path.extension 读取扩展名。
  • path.extension = "pdf" 修改扩展名。
  • path.extension = nil 删除扩展名。
  • path.push("../foo/bar/./") 追加路径组件。
  • path.lexicallyNormalize() 规范化 ...
  • path.ends(with: "foo/bar") 检查路径结尾。

Swift 5.5 语法改进:Codable、静态成员查找、参数属性包装器

17:04)过去带关联值的枚举要实现 Codable,需要手写 init(from:)encode(to:) 和多组 CodingKey。Swift 5.5 可以由编译器合成。

enum Command: Codable {
    case load(key: String)
    case store(key: String, value: Int)
}

关键点:

  • enum Command 定义命令类型。
  • : Codable 声明它支持编码和解码。
  • case load(key: String) 带一个关联值。
  • case store(key: String, value: Int) 带两个关联值。
  • 编译器为这些 case 生成编码和解码逻辑。

17:26)类型推断让调用枚举值时可以省略类型名。Swift 5.5 扩展了静态成员查找能力,让协议上的静态属性也能形成类似枚举的 API。

enum Coffee {
    case regular
    case decaf
}

func brew(_ coffee: Coffee) { ... }

brew(.regular)

关键点:

  • enum Coffee 定义咖啡种类。
  • case regularcase decaf 是枚举成员。
  • func brew(_ coffee: Coffee) 的参数类型明确是 Coffee
  • brew(.regular) 借助类型推断省略 Coffee.regular

18:25)属性包装器(Property Wrapper)原本常用于属性。Swift 5.5 通过 SE-0293 允许它用于函数参数和闭包参数。

@propertyWrapper
struct NonEmpty<Value: Collection> {
    init(wrappedValue: Value) {
        precondition(!wrappedValue.isEmpty)
        self.wrappedValue = wrappedValue
    }

    var wrappedValue: Value {
        willSet { precondition(!newValue.isEmpty) }
    }
}

func logIn(@NonEmpty _ username: String) {
    print("Logging in: \(username)")
}

关键点:

  • @propertyWrapper 声明 NonEmpty 是属性包装器。
  • Value: Collection 限制被包装的值必须是集合类型。
  • init(wrappedValue:) 在初始化时检查集合不为空。
  • precondition(!wrappedValue.isEmpty) 在传入空值时触发运行时检查失败。
  • wrappedValue 保存真实值。
  • willSet 在修改前检查新值不为空。
  • func logIn(@NonEmpty _ username: String) 把同一套约束用在函数参数上。

SwiftUI 代码变短:绑定数组、点语法、CGFloat 转换

19:37)演讲用一个设置列表展示 Swift 5.5 对日常 SwiftUI 代码的影响。以前要用下标访问数组里的 binding,还要显式写 CheckboxToggleStyle()CGFloat(padding)。现在可以直接传入绑定数组。

// You can now write this.

import SwiftUI

struct SettingsView: View {
    @State var settings: [Setting]

    private let padding = 10.0

    var body: some View {
        List($settings) { $setting in
            Toggle(setting.displayName, isOn: $setting.isOn)
              #if os(macOS)
              .toggleStyle(.checkbox)
              #else
              .toggleStyle(.switch)
              #endif
        }
        .padding(padding)
    }
}

关键点:

  • @State var settings: [Setting] 保存可变设置数组。
  • List($settings) 把设置数组的投影绑定传给列表。
  • { $setting in ... } 闭包直接获得每个元素的绑定。
  • Toggle(setting.displayName, isOn: $setting.isOn) 同时读取显示名并绑定开关状态。
  • #if os(macOS) 让同一段代码按平台选择样式。
  • .toggleStyle(.checkbox) 使用简化的静态成员点语法。
  • .padding(padding) 依赖编译器在 DoubleCGFloat 之间转换。

async/await:把回调式网络请求改成顺序代码

22:20)以前用 URLSession.dataTask 发请求,函数会先返回,真正的数据处理发生在闭包里。错误也要通过额外参数传递。

// Instead of writing this...

func fetchImage(id: String, completion: (UIImage?, Error?) -> Void) {
    let request = self.imageURLRequest(for: id)
    let task = URLSession.shared.dataTask(with: request) {
        data, urlResponse, error in
        if let error = error {
            completion(nil, error)
        } else if let httpResponse = urlResponse as? HTTPURLResponse,
                httpResponse.statusCode != 200 {
            completion(nil, MyTransferError())
        } else if let data = data, let image = UIImage(data: data) {
            completion(image, nil)
        } else {
            completion(nil, MyOtherError())
        }
    }
   task.resume()
}

关键点:

  • completion: (UIImage?, Error?) -> Void 用回调返回成功值或错误。
  • URLSession.shared.dataTask(with: request) 创建异步任务。
  • 闭包在请求完成后接收 dataurlResponseerror
  • 每个错误分支都要调用 completion(nil, ...)
  • 成功分支调用 completion(image, nil)
  • task.resume() 启动任务。

24:40)Swift 5.5 的 URLSession.shared.data(for:) 可以和 try await 一起使用。函数签名声明 async throws,错误进入 Swift 原有的抛出机制。

// You can now write this.

func fetchImage(id: String) async throws -> UIImage {
    let request = self.imageURLRequest(for: id)
    let (data, response) = try await URLSession.shared.data(for: request)
    if let httpResponse = response as? HTTPURLResponse,
       httpResponse.statusCode != 200 {
        throw TransferFailure()
    }
    guard let image = UIImage(data: data) else {
        throw ImageDecodingFailure()
    }
    return image
}

关键点:

  • async throws -> UIImage 表示函数会挂起,也可能抛错,成功时返回图片。
  • let request = self.imageURLRequest(for: id) 构造请求。
  • try await URLSession.shared.data(for: request) 发起请求并等待结果。
  • let (data, response) 解构返回的数据和响应元数据。
  • HTTPURLResponse 分支检查状态码。
  • throw TransferFailure() 把传输错误交给调用方处理。
  • guard let image = UIImage(data: data) 解码图片。
  • return image 返回最终结果。

结构化并发:让并行任务受父函数约束

27:06)有些任务可以并行执行。例子里,背景图和前景图可以同时渲染,标题图单独渲染,最后合并三个结果。

func titleImage() async throws -> Image {
    async let background = renderBackground()
    async let foreground = renderForeground()
    let title = try renderTitle()
    return try await merge(background,
                           foreground,
                           title)
}

关键点:

  • func titleImage() async throws -> Image 声明函数支持挂起和抛错。
  • async let background = renderBackground() 启动背景图渲染任务。
  • async let foreground = renderForeground() 启动前景图渲染任务。
  • let title = try renderTitle() 在当前流程中生成标题图。
  • try await merge(...) 等待并行结果就绪后合并。
  • 两个 async let 子任务不能超出 titleImage() 的生命周期。
  • 函数内抛错时,运行时会等待未完成任务,并向它们发出提前完成的信号。

Actor:把共享可变状态放进隔离区

29:26)多个线程同时修改同一个计数器,结果可能损坏。Swift 5.5 的 actor 让这类状态由一个并发安全的引用类型管理。

actor Statistics {
    private var counter: Int = 0
    func increment() {
        counter += 1
    }
    func publish() async {
        await sendResults(counter)
    }
}

var statistics = Statistics()
await statistics.increment()

关键点:

  • actor Statistics 定义一个 actor。
  • private var counter: Int = 0 保存被保护的可变状态。
  • func increment() 在 actor 内部修改计数器。
  • func publish() async 声明发布结果时可能挂起。
  • await sendResults(counter) 等待网络等异步操作完成。
  • var statistics = Statistics() 创建 actor 实例。
  • await statistics.increment() 从 actor 外部调用方法时使用 await
  • actor 在可能造成数据损坏的操作前挂起调用,等安全时再执行。

核心启发

  • 做什么:给图片列表做 async/await 网络加载层。 为什么值得做URLSession.shared.data(for:) 让请求、状态码检查、图片解码按顺序写,错误走 throws怎么开始:把旧的 completion handler 改成 func fetchImage(id:) async throws -> UIImage,在视图模型里用 try await 调用。

  • 做什么:为聊天、通知或日志页面维护有序去重列表。 为什么值得做OrderedSet 保留顺序并保证元素唯一,适合“按到达顺序展示,但不重复”的数据。 怎么开始:引入 Collections,用 OrderedSet 存消息 ID 或通知 ID,再用下标顺序渲染列表。

  • 做什么:给测试系统生成更系统的账号组合。 为什么值得做:Swift Algorithms 提供排列、组合、随机采样等集合算法,减少手写循环。 怎么开始:引入 Algorithms,用 uniquePermutations(ofCount:) 枚举测试账号组合,用 randomSample(count:) 做抽样验证。

  • 做什么:把埋点计数、下载进度、同步状态封装进 actor。 为什么值得做:actor 适合多个任务同时访问的可变状态,演讲中的 Statistics 就是计数器示例。 怎么开始:把原来的 class Statistics 改成 actor Statistics,把可变属性设为 private,从外部调用时加 await

  • 做什么:为内部工具或 Swift 包生成可发布文档。 为什么值得做:DocC 集成在 Xcode 13,使用 Swift 源码里的 Markdown 注释生成文档。 怎么开始:先给公开类型补 Markdown 注释,再用 Xcode 的 DocC 工作流生成文档归档。

关联 Session

评论

GitHub Issues · utterances