Open11

WWDC2021

saharasahara

Platforms State of the Union

https://developer.apple.com/wwdc21/102

Structured Concurrency

(from Platforms State of the Union, 23: 53)

func prepareForShow() async throws -> Scene {
    async let dancers = danceCompany.warmUp(duration: .minutes(45))
    async let scenery = crew.fetchStageScenery()
    let openingScene = setStage(with: await scenery)
    return try await dancers.moveToPosition(in: openingScene)
}

Actors

Definition

actor StageManager { 
    var stage: Stage

    func setStage(with scenery: Scenery) -> Scene {
        stage.backdrop = scenery.backdrop
        for prop in scenery.props {
            stage.addProp(prop)
        }
        return stage.currentScene
    }
}

Usage from external context

it needs await keyword to use an actor's method.

let scene = await stageManager.setStage(with: scenery)

DispatchQueue.main

To indicate the execution in the main thread, now we use DispatchQueue.main.async { }. It enables us to use actors in main thread to add @MainActor keyword.

@MainActor
func display(scene: Scene
await display(scene: scene)

Async Sequence

for try await line in url.lines {
   //process each line
}
saharasahara

What's new in Swift

冒頭はコミュニティ関係のトピック、ビルド時間の短縮、ドキュメンテーション。

DocC

DocCのセッションが4つも用意されている:

DocC Sessions

このセッションで予告されていた、オープンソース化の結果が
DocC is open

https://github.com/apple/swift-docc

らしい。

Ergonomic improvements

続いて、Swift Evolutionで議論の結果追加された機能。

Flexible static member lookup for Generic parameter

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

// Flexible static member lookup
// SE-0297 SE-0299
protocol Coffee {}
struct RegularCoffee: Coffee {
    var tall: RegularCoffee { self }
}

struct Cappuccino: Coffee {
    var large: Cappuccino { self }
}

extension Coffee where Self == RegularCoffee {
    static var regular: RegularCoffee { RegularCoffee() }
}

extension Coffee where Self == Cappuccino {
    static var cappucino: Cappuccino { Cappuccino() }//動画では=があるが、コンパイルエラーになる
}


func brew<CoffeeType: Coffee>(_: CoffeeType) { }

brew(.cappucino.large)
brew(.regular.tall)
saharasahara

Meet async/await in Swift

https://developer.apple.com/videos/play/wwdc2021/10132/

https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html

asyncになるのはメソッドだけとは限らない。プロパティがasyncになることもできる。

Async properties

extension UIImage {
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}

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

Async Sequence

Async sequence

Async await facts

facts

Testing async code

class MockViewModelSpec: XCTestCase {
    func testFetchThumbnails() async throws {
        XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
    }
}

Bridging from Sync to async

続いて、SwiftUIのコードでどのようにasyncなメソッドを取り入れていくかの説明。

既存の同期処理を前提としたクロージャー内でasyncなメソッドを使うためには、Taskで囲う必要がある。

struct ThumbnailView: View {
    @ObservedObject var viewModel: ViewModel
    var post: Post
    @State private var image: UIImage?

    var body: some View {
        Image(uiImage: self.image ?? placeholder)
            .onAppear {
                Task {
                    self.image = try? await self.viewModel.fetchThumbnail(for: post.id)
                }
            }
    }
}

Async APIs in the SDK

既存のSDKのAPIにもasyncなものが追加された。Objective-Cのブロックを引数に取るメソッドもasyncなものに変換される。

デリゲートメソッドも、asyncなものが用意される。

import ClockKit

extension ComplicationController: CLKComplicationDataSource {
    func currentTimelineEntry(for complication: CLKComplication) async -> CLKComplicationTimelineEntry? {
        let date = Date()
        let thumbnail = try? await self.viewModel.fetchThumbnail(for: post.id)
        guard let thumbnail = thumbnail else {
            return nil
        }

        let entry = self.createTimelineEntry(for: thumbnail, date: date)
        return entry
    }
}

引数にとっているcomplicationハンドラーをメソッド内では明示的には実行していないことに注意。

Async alternatives and continuation

既存の完了ハンドラを使ったAPIをasyncなものにラップする

// Existing function
func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) {       
    do {
        let req = Post.fetchRequest()
        req.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
        let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in
            completion(result.finalResult ?? [], nil)
        }
        try self.managedObjectContext.execute(asyncRequest)
    } catch {
        completion([], error)
    }
}

// Async alternative
func persistentPosts() async throws -> [Post] {       //非同期で取得する場合、getを落とす
    typealias PostContinuation = CheckedContinuation<[Post], Error>
    return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
        // 既存の実装の完了ハンドラ内で、Continuationに非同期処理の結果を渡す
        self.getPersistentPosts { posts, error in
            if let error = error { 
                continuation.resume(throwing: error) 
            } else {
                continuation.resume(returning: posts)
            }
        }
    }
}

既存のデリゲートメソッドを使ったAPIをasyncなものでラップする

class ViewController: UIViewController {
    private var activeContinuation: CheckedContinuation<[Post], Error>?
    func sharedPostsFromPeer() async throws -> [Post] {
        try await withCheckedThrowingContinuation { continuation in
            self.activeContinuation = continuation
            self.peerManager.syncSharedPosts()
        }
    }
}

extension ViewController: PeerSyncDelegate {
    func peerManager(_ manager: PeerManager, received posts: [Post]) {
        self.activeContinuation?.resume(returning: posts)
        self.activeContinuation = nil // guard against multiple calls to resume
    }

    func peerManager(_ manager: PeerManager, hadError error: Error) {
        self.activeContinuation?.resume(throwing: error)
        self.activeContinuation = nil // guard against multiple calls to resume
    }
}
saharasahara

Explore structured concurrency in Swift

async let

asyncな関数の結果を待たずに後続の処理を行い、改めて結果が必要になったら待機するために、async let を使うことができる。

Concurrent Binding

async let result = someAsyncFunc(..)
...
try await result

詳しくは

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

を参照のこと。

Group tasks

複数の非同期処理を並行に実行するために、Task のGroupを使うことができる。

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
        for id in ids {
            group.async {
                return (id, try await fetchOneThumbnail(withID: id))
            }
        }
        // Obtain results from the child tasks, sequentially, in order of completion.
        for try await (id, thumbnail) in group {
            thumbnails[id] = thumbnail
        }
    }
    return thumbnails
}

上のコード例で、子タスクの中でthumbnailsに直接代入していないのは、データ競合に関するコンパイルエラーが出るのに対応するため。

Unstructured tasks

Taskオブジェクトを使うことで、既存のUIKitやAppKitのデリゲートメソッド内で非同期処理を行う際に、asyncな関数を呼び出すことができるようになる。

@MainActor
class MyDelegate: UICollectionViewDelegate {
    func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        Task {
            let thumbnails = await fetchThumbnails(for: ids)
            display(thumbnails, in: cell)
        }
    }
}

この場合、Task内に書かれた非同期処理はメインスレッド内で実行されるものの、非同期処理は完了するまでスレッドをブロックすることはなく、一旦呼び出し元に処理が戻る。

このTaskは、処理が完了するまでは、スコープに縛られずに残り続けることができる。
また、asyncでマーキングされていない関数内でも実行することができる。
結果を待ったりキャンセル処理をするには、以下のように処理を書き足す必要がある:

@MainActor
class MyDelegate: UICollectionViewDelegate {
    var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]//タスクを保持しておくDictionary
    
    func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        thumbnailTasks[item] = Task {
            defer { thumbnailTasks[item] = nil }
            let thumbnails = await fetchThumbnails(for: ids)
            display(thumbnails, in: cell)
        }
    }
    //セルの表示が終わる際に、表示のための非同期処理のタスクをキャンセルする
    func collectionView(_ view: UICollectionView, didEndDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
        thumbnailTasks[item]?.cancel()
    }
}

Detached tasks

@MainActor
class MyDelegate: UICollectionViewDelegate {
    var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
    
    func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        thumbnailTasks[item] = Task {
            defer { thumbnailTasks[item] = nil }
            let thumbnails = await fetchThumbnails(for: ids)
            Task.detached(priority: .background) {//メインスレッドで実行されるとは限らない
                writeToLocalCache(thumbnails)
            }
            display(thumbnails, in: cell)
        }
    }
}
@MainActor
class MyDelegate: UICollectionViewDelegate {
    var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
    
    func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
        let ids = getThumbnailIDs(for: item)
        thumbnailTasks[item] = Task {
            defer { thumbnailTasks[item] = nil }
            let thumbnails = await fetchThumbnails(for: ids)
            Task.detached(priority: .background) {
                withTaskGroup(of: Void.self) { g in//親タスクがキャンセルされると、このスコープ内の子タスクもキャンセルされる
                    g.async { writeToLocalCache(thumbnails) }
                    g.async { log(thumbnails) }
                    g.async { ... }
                }
            }
            display(thumbnails, in: cell)
        }
    }
}

Flavors of tasks

saharasahara

Protect mutable state with Swift actors

Syncronize shared mutable state

変わりうる状態を持つオブジェクトを複数のスレッド間で共有することは難しい。

structを使うと、race conditionを防ぐことはできるものの、オブジェクトが複製され、複数の変数は状態が共有されていないため、期待した挙動とは異なってしまう:

struct Counter {
    var value = 0

    mutating func increment() -> Int {
        value = value + 1
        return value
    }
}

let counter = Counter()

Task.detached {
    var counter = counter
    print(counter.increment()) // always prints 1
}

Task.detached {
    var counter = counter
    print(counter.increment()) // always prints 1
}

このように、Race Conditionを防ぎつつShared mutable state の同期が必要な局面がある。

Actorは、shared mutable stateの同期を実現する。

actor isolation

Actorは、プログラムの他の部分から自身の状態を隔離する:

  • 自身の状態へのアクセスは、アクターの機能を経由しなければならない
  • Actorは、自身の状態へのアクセスはお互いに排他的であることを保証する仕組みを持っている

こうした性質に違反するコードは、コンパイル時の検査でエラーが発生するため、実行できない。

nonisolatedキーワードがついたメソッドは、actorのミュータブルなプロパティにはアクセスできない。

actor LibraryAccount {
    let idNumber: Int
    var booksOnLoan: [Book] = []
}

extension LibraryAccount: Hashable {
    nonisolated func hash(into hasher: inout Hasher) {//booksOnLoanプロパティにはアクセスできない
        hasher.combine(idNumber)
    }
}
saharasahara

Swift concurrency: Update a sample app

func someAsyncFunc() async {...}

// 同期関数の内部でasyncな関数を呼び出す際

func someSyncFunc() {
...
    Task { await someAsyncFunc() }
...
}

Xcodeに、既存の関数の非同期版を追加するオプションが用意されている:

add async alternative

DispatchQueue.main.asyncの置き換え

クロージャの手前の DispatchQueue.main.asyncawait MainActor.runに置き換える。

クロージャ内の変数に関しては、race conditionを防ぐためにキャプチャリストを書くか定数に置き換える必要がある。