天天看點

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

原創 巴格 淘系技術  7月6日

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

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
Effectful read-only properties https://github.com/apple/swift-evolution/blob/main/proposals/0310-effectful-readonly-properties.md
Structured Concurrency https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md
async let https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md
Continuations for interfacing async tasks with synchronous code https://github.com/apple/swift-evolution/blob/main/proposals/0300-continuation.md
https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md
Global Actors https://github.com/apple/swift-evolution/blob/main/proposals/0316-global-actors.md
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注釋編寫

開發者可以通過類似下面代碼中的方式編寫注釋:

ovided 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 {      

最終生成的文檔如下圖

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

詳細的DocC注釋編寫規範可以參考文檔: 

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

▐  從代碼注釋建構DocC文檔

為了讓 DocC 編譯文檔,Xcode 首先建構 Swift 工程,并在編譯的同時存儲有關其 API 的資訊。DocC 使用該資訊将注釋編譯為 DocC 檔案, 流程如下圖:

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

要為 Swift 工程建構文檔,請選擇Product > Build Documentation。DocC 編譯工程的文檔并可以在 Xcode 的文檔檢視器中打開它。

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

Notifications 

在WWDC2021中,系統通知也發生了較大的變化,具體反映在如下幾個方面:

▐  視覺更新

比如使用者收到如下通知:

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

在iOS15系統中開發者可以自定義點選效果,如下圖

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

為了實作上述App icon、内容擴充、動作icon等視覺效果,我們隻需要按照下面的方式進行開發:

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

▐  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 通知權限彈框更新

為了支援上面新的功能,權限提示也在發生變化。

現在,當應用程式請求推送權限時,使用者将能夠指定他們是要立即從應用程式接收通知,還是将通知組合在一起作為通知摘要的一部分。

Swift5.5、DocC、Notifications,蘋果WWDC21帶來的最大技術變化

▐  通信通知

新系統添加了将應用程式的通知區分為通信通知的功能。

通信通知将包含發送它們的聯系人的頭像,并且可以與 SiriKit 內建,以便 Siri 可以根據常用聯系人智能地提供快捷方式和通信操作建議。

例如,當使用者為焦點模式設定允許的聯系人或從您的應用撥打電話時,Siri 将根據您的應用程式提供的意圖資料智能地推薦聯系人。 

要使用通信通知,開發者需要在 Xcode 配置中添加通信通知功能,并實作新 UNNotificationContentProviding 協定的 Intent 對象更新應用程式通知服務擴充中通知的内容。 

參考資料

繼續閱讀