Swift5.5、DocC、Notifications,苹果WWDC21带来的最大技术变化
巴格2021-07-06


WWDC (苹果开发者大会) 2021已经落下帷幕,今年的WWDC 提供了 200 多个深度课程,以帮助开发者了解WWDC2021 引入的新技术,本文会帮国内开发者梳理部分WWDC 2021带来的技术上的变化。

Swift5.5

WWDC2021 给我们带来了Swift 5.5,这是Swift 语言最新的版本,在这个版本中有许多重大的更新,下面会大家详细介绍一下Swift 5.5的一些重要更新。

▐ Swift Concurrency


Swift 5.5 中最大的更新就是引入了全新的并发编程方式,包括async/await语法、结构化并发、Actor等,新的并发编程方式解决了我们以往使用回调的种种缺陷 (嵌套地狱、回调错误处理麻烦、回调分支编写困难等),为开发者带来了极大的便利。

async/await


过去我们编写异步代码都是通过回调的方式,如下:

func processImageData2a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            completionBlock(nil, error)
            return
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                completionBlock(nil, error)
                return
            }
            decodeImage(dataResource, imageResource) { imageTmp, error in
                guard let imageTmp = imageTmp else {
                    completionBlock(nil, error)
                    return
                }
                dewarpAndCleanupImage(imageTmp) { imageResult, error in
                    guard let imageResult = imageResult else {
                        completionBlock(nil, error)
                        return
                    }
                    completionBlock(imageResult)
                }
            }
        }
    }
}

processImageData2a { image, error in
    guard let image = image else {
        display("No image today", error)
        return
    }
    display(image)
}

通过回调的方式编写异步代码有以下缺点:

  • 阅读不直观
  • 嵌套逻辑复杂
  • 错误处理麻烦
  • 分支逻辑难以处理
  • 经常会忘了回调或者返回

在Swift 5.5中为了解决上述回调方式的缺点,引入了async/await语法,可以帮助我们快速的编写异步代码,通过async/await上述代码可以变成如下同步代码:

func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image

func processImageData() async throws -> Image {
  let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  let imageTmp      = try await decodeImage(dataResource, imageResource)
  let imageResult   = try await dewarpAndCleanupImage(imageTmp)
  return imageResult
}


正如上述代码所展现的,所有的闭包和缩进都消失了,你可以按顺序依次书写代码——除了 await 关键字,它看起来就和同步代码一样。

关于async函数的工作方式,有一些规则需要注意:


  • 同步函数不能简单地直接调用异步函数, Swift 编译器会抛出错误。
  • 异步函数可以调用其他异步函数,但如果需要,它们也可以调用常规的同步函数。
  • 如果有可以以相同方式调用的异步和同步函数,Swift 将优先选择与当前上下文匹配的任何一个, 如果当前执行上下文是异步的,那么 Swift 将调用异步函数,否则它将调用同步函数。


Structured concurrency


在介绍结构化并发之前,我们先来看一个案例:

func chopVegetables() async throws -> [Vegetable] { ... }
func marinateMeat() async -> Meat { ... }
func preheatOven(temperature: Double) async throws -> Oven { ... }

// ...

func makeDinner() async throws -> Meal {
  let veggies = try await chopVegetables()  // 处理蔬菜
  let meat = await marinateMeat()           // 腌制肉
  let oven = try await preheatOven(temperature: 350) //预热烤箱

  let dish = Dish(ingredients: [veggies, meat])   // 把蔬菜和肉装盘
  return try await oven.cook(dish, duration: .hours(3))  // 用烤箱做出晚餐
}

上面处理蔬菜、腌制肉、预热烤箱等都是异步执行的,但是上述三个步骤仍然是串行执行的,这使得做晚餐的时间变长了,为了让晚餐准备时间变短,我们需要让处理蔬菜、腌制肉、预热烤箱几个步骤并发执行

为了解决上述问题,Swift 5.5中引入了Structured concurrency(结构化并发),下面是维基百科中的解释:

结构化并发是一种编程范式,旨在通过使用结构化的并发编程方法来提高计算机程序的清晰度、质量和研发效能。

核心理念是通过具有明确入口和出口点并确保所有生成的子任务在退出前完成的控制流构造来封装并发执行任务(这里包括内核和用户线程和进程)。这种封装允许并发任务中的错误传播到控制结构的父作用域,并由每种特定计算机语言的本机错误处理机制进行管理。尽管存在并发性,但它允许控制流通过源代码的结构保持显而易见。为了有效,这个模型必须在程序的所有级别一致地应用——否则并发任务可能会泄漏、成为孤立的或无法正确传播运行时错误。(来自维基百科)

使用结构化并发,上述制作晚餐的过程可以通过下面的方式进行:

func makeDinner() async throws -> Meal {
  // Prepare some variables to receive results from our concurrent child tasks
  var veggies: [Vegetable]?
  var meat: Meat?
  var oven: Oven?

  enum CookingStep { 
    case veggies([Vegetable])
    case meat(Meat)
    case oven(Oven)
  }

  // Create a task group to scope the lifetime of our three child tasks
  try await withThrowingTaskGroup(of: CookingStep.self) { group in
    group.async {
      try await .veggies(chopVegetables())
    }
    group.async {
      await .meat(marinateMeat())
    }
    group.async {
      try await .oven(preheatOven(temperature: 350))
    }

    for try await finishedStep in group {
      switch finishedStep {
        case .veggies(let v): veggies = v
        case .meat(let m): meat = m
        case .oven(let o): oven = o
      }
    }
  }

  // If execution resumes normally after `withTaskGroup`, then we can assume
  // that all child tasks added to the group completed successfully. That means
  // we can confidently force-unwrap the variables containing the child task
  // results here.
  let dish = Dish(ingredients: [veggies!, meat!])
  return try await oven!.cook(dish, duration: .hours(3))
}


上述代码中chopVegetables、marinateMeat 和preheatOven 将并发运行,并且可能以任何顺序进行。

无论哪种情况,任务组都会自然地将状态从子任务传播到父任务;在这个例子中,如果菜刀发生了事故,chopVegetables() 函数可能会抛出一个错误。

抛出的错误完成了切菜的子任务。正如预期的那样,该错误随后将传播到 makeDinner() 函数之外。在出现此错误退出 makeDinner() 函数的主体时,任何尚未完成的子任务(腌肉或预热烤箱,可能两者)将自动取消。

结构化并发意味着我们不必手动传播错误和管理取消;如果在调用 withTaskGroup 后继续正常执行,我们可以假设它的所有子任务都成功完成。


Actors


Swift 5.5引入了Actor,它在概念上类似于在并发环境中可以安全使用的类。Swift 确保在任何给定时间只能由单个线程访问 Actor 内的可变状态,这有助于在编译器级别消除各种严重的错误。

我们可以先一起看一个Swift中的Class,如下:

class RiskyCollector {
    var deck: Set<String>

    init(deck: Set<String>) {
        self.deck = deck
    }

    func send(card selected: String, to person: RiskyCollector) -> Bool {
        guard deck.contains(selected) else { return false }

        deck.remove(selected)
        person.transfer(card: selected)
        return true
    }

    func transfer(card: String) {
        deck.insert(card)
    }
}


RiskyCollector 在单线程环境中,代码是安全的。然而,在多线程环境中,我们的代码存在潜在的多线程竞争未作处理。

Actor 通过引入 Actor 隔离解决了这个问题:除非异步执行,否则无法从 Actor 对象外部读取属性和方法,并且根本无法从 Actor 对象外部写入属性。异步行为不是为了性能;相反,这是因为 Swift 会自动将这些请求放入一个按顺序处理的队列中,以避免出现多线程竞争。

我们可以使用Actor重新实现一个SafeCollector,如下:

actor SafeCollector {
    var deck: Set<String>

    init(deck: Set<String>) {
        self.deck = deck
    }

    func send(card selected: String, to person: SafeCollector) async -> Bool {
        guard deck.contains(selected) else { return false }

        deck.remove(selected)
        await person.transfer(card: selected)
        return true
    }

    func transfer(card: String) {
        deck.insert(card)
    }
}


在这个例子中有几件事情需要注意:

  • Actor 是使用新的 actor 关键字创建的
  • send() 方法被标记为 async,因为它需要异步执行
  • 尽管 transfer(card:) 方法没有用 async 标记,但我们仍然需要用 await 调用它,因为它会等到另一个 SafeCollector actor 能够处理请求。

需要明确的是,actor 可以自由地、异步或以其他方式使用自己的属性和方法,但是当与不同的 actor 交互时,它必须始终异步完成。通过这些特性,Swift 可以确保永远不会同时访问所有与 actor 隔离的状态,更重要的是,这是在编译时完成的,以保证线程安全。

Actor 和 Class 有一些相似之处:

  • 两者都是引用类型,因此它们可用于共享状态。
  • 它们可以有方法、属性、初始值设定项和下标。
  • 它们可以实现协议。任何静态属性和方法在这两种类型中的行为都相同,因为它们没有 self 的概念,因此不会被隔离。

除了 Actor 隔离之外,Actor 和 Class之间还有另外两个重要的区别:

  • Actor 目前不支持继承,这在未来可能会改变
  • 所有 Actor 都隐式遵守一个新的 Actor Protocol

除了上述特性外,Swift 5.5 还增加了不少新特性,但是Cocurrency部分的新特性只有iOS15及以上系统可以运行,下面整理了一个表格,列出了Swift 5.5的重要更新以及适用的系统:

更新项

描述链接

适用系统

async/awai

https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md

iOS15及以上

async sequences

https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md

iOS15及以上

Effectful read-only properties

https://github.com/apple/swift-evolution/blob/main/proposals/0310-effectful-readonly-properties.md

iOS15及以上

Structured Concurrency

https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md

iOS15及以上

async let

https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md

iOS15及以上

Continuations for interfacing async tasks with synchronous code

https://github.com/apple/swift-evolution/blob/main/proposals/0300-continuation.md

iOS15及以上

Actors

https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md

iOS15及以上

Global Actors

https://github.com/apple/swift-evolution/blob/main/proposals/0316-global-actors.md

iOS15及以上

Sendable and @Sendable closures

https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md

不限制

#if for postfix member expressions

https://github.com/apple/swift-evolution/blob/main/proposals/0308-postfix-if-config-expressions.md

不限制

Codable synthesis for enums with associated values

https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md

不限制

lazy now works in local contexts


不限制

Extend Property Wrappers to Function and Closure Parameters

https://github.com/apple/swift-evolution/blob/main/proposals/0293-extend-property-wrappers-to-function-and-closure-parameters.md

不限制

Extending Static Member Lookup in Generic Contexts

https://github.com/apple/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md

不限制

DocC

DocC是Xcode13包含的文档编译器,它可以帮助开发者轻松地为 Swift 代码生成文档。

编译器通过将您在源代码中编写的注释与在 Xcode 项目中一起存在的扩展文件、文章和其他资源相结合来构建文档,从而可以为开发人员创建丰富且引人入胜的文档。

使用 DocC,开发者可以提供技术参考和示例的组合,并使用强大的组织和链接功能将它们连接在一起。

编译器直接与 Xcode 集成以整合现有的工作流程,包括代码提示、快速帮助等。

而且因为直接在源代码中编写文档,所以可以使用现有的工具(例如 Git)来跟踪所有的变更。

DocC注释编写

开发者可以通过类似下面代码中的方式编写注释:

/// Eat the provided specialty sloth food.
///
/// Sloths love to eat while they move very slowly through their rainforest 
/// habitats. They're especially happy to consume leaves and twigs, which they 
/// digest over long periods of time, mostly while they sleep.
///
/// When they eat food, a sloth's `energyLevel` increases by the food's `energy`.
///
/// - Parameters:
///   - food: The food for the sloth to eat.
///   - quantity: The quantity of the food for the sloth to eat.
///
/// - Returns: The sloth's energy level after eating.
///
/// - Throws: `SlothError.tooMuchFood` if the quantity is more than 100.
mutating public func eat(_ food: Food, quantity: Int) throws -> Int {

最终生成的文档如下图


详细的DocC注释编写规范可以参考文档:

https://developer.apple.com/documentation/xcode/writing-symbol-documentation-in-your-source-files

从代码注释构建DocC文档


为了让 DocC 编译文档,Xcode 首先构建 Swift 工程,并在编译的同时存储有关其 API 的信息。DocC 使用该信息将注释编译为 DocC 档案, 流程如下图:



要为 Swift 工程构建文档,请选择Product > Build Documentation。DocC 编译工程的文档并可以在 Xcode 的文档查看器中打开它。



Notifications


在WWDC2021中,系统通知也发生了较大的变化,具体反映在如下几个方面:

视觉升级


比如用户收到如下通知:


在iOS15系统中开发者可以自定义点击效果,如下图



为了实现上述App icon、内容扩展、动作icon等视觉效果,我们只需要按照下面的方式进行开发:



Focus Mode


Apple 新增了Focus Mode,这个模式可以更好地使通知体验与用户偏好保持一致。

新的专注模式非常适合减少对用户的干扰。iPhone用户可以自定义他们希望收到通知的方式和时间。以前,用户可以通过启用“请勿打扰”模式来选择将所有来电和通知静音。现在,用户将能够通过设置工作、睡眠和个人通知模式来完善他们的通知偏好以适应不同的场景。

对于每个配置文件,用户可以选择要接收通知的应用和联系人、要阻止的应用和联系人,以及要暂停的特定应用功能。用户还可以创建一个主屏幕页面以匹配他们当前的焦点模式并仅显示相关的应用程序。例如,在工作模式下,用户可以选择仅查看与工作相关的应用程序。

焦点配置文件将同步到所有其他苹果设备。 焦点设置也可以由其他设置确定,例如一天中的时间、地理位置或日历事件。

Apple 将使用 AI 自动预测要设置的配置文件。例如,当用户到达工作地点时,iPhone 可以使用地理位置数据来触发工作模式,或者在用户接近就寝时间时使用睡眠时间偏好来触发睡眠模式。

还将有两个与焦点模式相关的新 API。 Status API 告诉应用设备是否处于焦点模式。时间敏感 API 允许应用指定对时间敏感的通知以覆盖设置。

// 返回焦点系统的状态,包括有关当前焦点项目的信息。
class func status() -> UIFocusDebuggerOutput

// 返回系统通知时间敏感的设置
var timeSensitiveSetting: UNNotificationSetting { get }


通知摘要


用户可以设置对通知进行批处理和优先处理,并选择在一天中的特定时间接收应用程序通知作为摘要。

例如,用户可以将通知分组显示,而不是在整个早上一个接一个地接收通知。

iOS系统将根据用户如何使用不同应用程序而不是应用程序名称和时间来优先处理这些通知。

来自朋友的通知将更接近顶部。带有媒体附件的通知更有可能在摘要中突出显示。

开发人员可以使用新的 relatedScore API 来指示应在此摘要中突出显示应用程序的哪些通知。

/// 系统用于对应用的通知进行排序的权重值
var relevanceScore: Double { get }


iOS 通知权限弹框更新


为了支持上面新的功能,权限提示也在发生变化。

现在,当应用程序请求推送权限时,用户将能够指定他们是要立即从应用程序接收通知,还是将通知组合在一起作为通知摘要的一部分。



通信通知


新系统添加了将应用程序的通知区分为通信通知的功能。

通信通知将包含发送它们的联系人的头像,并且可以与 SiriKit 集成,以便 Siri 可以根据常用联系人智能地提供快捷方式和通信操作建议。

例如,当用户为焦点模式设置允许的联系人或从您的应用拨打电话时,Siri 将根据您的应用程序提供的意图数据智能地推荐联系人。

要使用通信通知,开发者需要在 Xcode 配置中添加通信通知功能,并实现新 UNNotificationContentProviding 协议的 Intent 对象更新应用程序通知服务扩展中通知的内容。

参考资料


https://onesignal.com/blog/ios-notification-changes-updates-from-apples-wwdc-21/

https://developer.apple.com/documentation/Xcode/documenting-a-swift-framework-or-package

https://developer.apple.com/documentation/xcode/writing-symbol-documentation-in-your-source-files