Upgrade to Pro — share decks privately, control downloads, hide ads and more …

async/awaitやactorでiOSアプリ開発がどう変わるか Before & Afterの具体例で学ぶ

Yuta Koshizawa
September 19, 2021

async/awaitやactorでiOSアプリ開発がどう変わるか Before & Afterの具体例で学ぶ

今秋リリース予定のSwift 5.5には、async/awaitやactorなどの並行処理関連の新機能が多く含まれます。これによって、iOSアプリ開発における非同期処理や並行処理のコードは劇的に変化します。本トークでは、iOSアプリ開発でよく見かけるようなケースを取り上げ、Before & Afterのコードを示し、何がどのように変わるのかを説明します。

async/awaitやactorは使いこなせばとても便利ですが、新しい概念を初めから抽象的に学ぶのは大変です。しかし、それらが解決しようとしている問題自体はiOSアプリ開発者にとって馴染み深いものです。async/awaitやactorで書くコードが既存コードのどのような処理に対応するのか、既存コードにはどんな問題があり、async/awaitやactorはそれをどう解決してくれるのか、具体的なケースで学べば入口のハードルはぐっと下がります。

Swiftの並行処理関連の新機能は、async/await、async let、Task、CheckedContinuation、actor、Sendable、MainActorなど多岐に渡ります。それらを一つ一つ取り上げ、具体例を題材に、iOSアプリ開発のコードがどのように改善されるのかを説明します。

https://fortee.jp/iosdc-japan-2021/speaker/proposal/view/19c076c3-18cb-4d04-9e9d-59cc02220538

Yuta Koshizawa

September 19, 2021
Tweet

More Decks by Yuta Koshizawa

Other Decks in Programming

Transcript

  1. Before func downloadData(from url: URL, completion: @escaping (Data) -> Void)

    downloadData(from: url) { data in // data Λ࢖͏ॲཧ }
  2. Before func downloadData(from url: URL, completion: @escaping (Data) -> Void)

    downloadData(from: url) { data in // data Λ࢖͏ॲཧ }
  3. A"er func downloadData(from url: URL) async -> Data let data

    = await downloadData(from: url) // data Λ࢖͏ॲཧ
  4. A ͱ B ͕ಉ͡εϨουͰ࣮ߦ͞ΕΔͱ͸ݶΒͳ͍ func downloadData(from url: URL) async ->

    Data print("A") let data = await downloadData(from: url) print("B")
  5. A ͱ B ͕ಉ͡εϨουͰ࣮ߦ͞ΕΔͱ͸ݶΒͳ͍ func downloadData(from url: URL, completion: @escaping

    (Data) -> Void) print("A") downloadData(from: url) { data in print("B") }
  6. Case 1 (Before) func downloadData(from url: URL, completion: @escaping (Data)

    -> Void) downloadData(from: url) { data in // data Λ࢖͏ॲཧ }
  7. Before func downloadData(from url: URL, completion: @escaping (Result<Data, Error>) ->

    Void) downloadData(from: url) { result in do { let data = try result.get() // data Λ࢖͏ॲཧ } catch { // ΤϥʔϋϯυϦϯά } }
  8. A"er func downloadData(from url: URL) async throws -> Data do

    { let data = try await downloadData(from: url) // data Λ࢖͏ॲཧ } catch { // ΤϥʔϋϯυϦϯά }
  9. Before func fetchUser(for id: User.ID, completion: @escaping (Result<User, Error>) ->

    Void) { let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! }
  10. Before func fetchUser(for id: User.ID, completion: @escaping (Result<User, Error>) ->

    Void) { let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! downloadData(from: url) { result in } }
  11. Before func fetchUser(for id: User.ID, completion: @escaping (Result<User, Error>) ->

    Void) { let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! downloadData(from: url) { result in let data = try result.get() let user = try JSONDecoder().decode(User.self, from: data) completion(.success(user)) } }
  12. Before func fetchUser(for id: User.ID, completion: @escaping (Result<User, Error>) ->

    Void) { let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! downloadData(from: url) { result in do { let data = try result.get() let user = try JSONDecoder().decode(User.self, from: data) completion(.success(user)) } catch { completion(.failure(error)) } } }
  13. Before func fetchUser(for id: User.ID, completion: @escaping (Result<User, Error>) ->

    Void) { let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! downloadData(from: url) { result in do { let data = try result.get() let user = try JSONDecoder().decode(User.self, from: data) completion(.success(user)) } catch { completion(.failure(error)) } } }
  14. A"er func fetchUser(for id: User.ID) async throws -> User {

    let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! let data = try await downloadData(from: url) let user = try JSONDecoder().decode(User.self, from: data) return user }
  15. Before func fetchUserIcon(for id: User.ID, completion: @escaping (Result<Data, Error>) ->

    Void) { let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! downloadData(from: url) { data in do { let data = try data.get() let user = try JSONDecoder().decode(User.self, from: data) downloadData(from: user.iconURL) { icon in do { let icon = try icon.get() completion(.success(icon)) } catch { completion(.failure(error)) } } } catch { completion(.failure(error)) } } }
  16. A"er func fetchUserIcon(for id: User.ID) async throws -> Data {

    let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! let data = try await downloadData(from: url) let user = try JSONDecoder().decode(User.self, from: data) let icon = try await downloadData(from: user.iconURL) return icon }
  17. // ίʔϧόοΫ func downloadData(from url: URL, completion: @escaping (Result<Data, Error>)

    -> Void) // async func downloadData(from url: URL) async throws -> Data { }
  18. // ίʔϧόοΫ func downloadData(from url: URL, completion: @escaping (Result<Data, Error>)

    -> Void) // async func downloadData(from url: URL) async throws -> Data { downloadData(from: url) { result in do { let data = try result.get() } catch { } } }
  19. // ίʔϧόοΫ func downloadData(from url: URL, completion: @escaping (Result<Data, Error>)

    -> Void) // async func downloadData(from url: URL) async throws -> Data { downloadData(from: url) { result in do { let data = try result.get() } catch { } } }
  20. // ίʔϧόοΫ func downloadData(from url: URL, completion: @escaping (Result<Data, Error>)

    -> Void) // async func downloadData(from url: URL) async throws -> Data { try await withCheckedThrowingContinuation { continuation in downloadData(from: url) { result in do { let data = try result.get() continuation.resume(returning: data) } catch { continuation.resume(throwing: error) } } } }
  21. // ίʔϧόοΫ func downloadData(from url: URL, completion: @escaping (Result<Data, Error>)

    -> Void) // async func downloadData(from url: URL) async throws -> Data { try await withCheckedThrowingContinuation { continuation in downloadData(from: url) { result in do { let data = try result.get() continuation.resume(returning: data) } catch { continuation.resume(throwing: error) } } } }
  22. // ίʔϧόοΫ func downloadData(from url: URL, completion: @escaping (Result<Data, Error>)

    -> Void) // async func downloadData(from url: URL) async throws -> Data { try await withCheckedThrowingContinuation { continuation in downloadData(from: url) { result in continuation.resume(with: result) } } }
  23. async ؔ਺͸ async ؔ਺ͷதͰ͔͠ݺ΂ͳ͍ func foo() async { ... }

    func bar() async { await foo() } func baz() async { await bar() } ...
  24. ඇಉظॲཧͷ։࢝ func main() { Task { await foo() } }

    ͢΂ͯͷ async ؔ਺͸ Task ্Ͱ࣮ߦ͞ΕΔɻ
  25. iOS ΞϓϦ։ൃͰͲͷΑ͏ʹ࢖ΘΕΔ͔ • viewDidAppear • View Controller ͕දࣔ͞ΕͨΒ fetchUser Ͱ

    User Λऔಘ ͯ͠දࣔ • @IBAction • μ΢ϯϩʔυϘλϯ͕ԡ͞ΕͨΒ downloadData Ͱμ΢ϯ ϩʔυ • ...
  26. A"er extension UserViewController { override func viewDidAppear(_ animated: Bool) {

    super.viewDidAppear(animated) do { let user = try await fetchUser(for: userID) self.user = user } catch { // ΤϥʔϋϯυϦϯά } } }
  27. A"er extension UserViewController { override func viewDidAppear(_ animated: Bool) {

    super.viewDidAppear(animated) Task { do { let user = try await fetchUser(for: userID) self.user = user } catch { // ΤϥʔϋϯυϦϯά } } } }
  28. A"er func fetchUserIcons(for id: User.ID) async throws -> (small: Data,

    large: Data) { let smallURL: URL = ... let largeURL: URL = ... return icons }
  29. ͜ΕͰ͸ฒߦʹͳΒͳ͍ func fetchUserIcons(for id: User.ID) async throws -> (small: Data,

    large: Data) { let smallURL: URL = ... let largeURL: URL = ... let smallIcon = try await downloadData(from: smallURL) let largeIcon = try await downloadData(from: largeURL) let icons = (small: smallIcon, large: largeIcon) return icons }
  30. ͜ΕͰ͸ฒߦʹͳΒͳ͍ func fetchUserIcons(for id: User.ID) async throws -> (small: Data,

    large: Data) { let smallURL: URL = ... let largeURL: URL = ... let smallIcon = try await downloadData(from: smallURL) let largeIcon = try await downloadData(from: largeURL) let icons = (small: smallIcon, large: largeIcon) return icons }
  31. A"er func fetchUserIcons(for id: User.ID) async throws -> (small: Data,

    large: Data) { let smallURL: URL = ... let largeURL: URL = ... async let smallIcon = downloadData(from: smallURL) async let largeIcon = downloadData(from: largeURL) let icons = try await (small: smallIcon, large: largeIcon) return icons }
  32. A"er func fetchUserIcons(for id: User.ID) async throws -> (small: Data,

    large: Data) { let smallURL: URL = ... let largeURL: URL = ... async let smallIcon = downloadData(from: smallURL) async let largeIcon = downloadData(from: largeURL) let icons = try await (small: smallIcon, large: largeIcon) return icons } async let ͸ඞͣείʔϓ಺Ͱ await ͞Εͳ͚Ε͹ͳΒͳ͍ɻ
  33. A"er func fetchUserIcons(for id: User.ID) async throws -> (small: Data,

    large: Data) { let smallURL: URL = ... let largeURL: URL = ... async let smallIcon = downloadData(from: smallURL) async let largeIcon = downloadData(from: largeURL) let icons = try await (small: smallIcon, large: largeIcon) return icons } async let ͸ඞͣείʔϓ಺Ͱ await ͞Εͳ͚Ε͹ͳΒͳ͍ɻ
  34. async let ͱ Task Task { async let a =

    foo() async let b = bar() print(await a + b) }
  35. Before func downloadData( from url: URL, cancellation: @escaping () ->

    Void, completion: @escaping (Result<Data, Error>) -> Void ) -> DownloadCanceller
  36. Before extension ViewController { @IBAction func downloadButtonPressed(_ sender: UIButton) {

    canceller = downloadData(from: url, cancellation: { // Ωϟϯηϧ࣌ͷॲཧ }, completion: { data in do { let data = try data.get() // data Λ࢖͏ॲཧ } catch { // ΤϥʔϋϯυϦϯά } }) } }
  37. A"er extension ViewController { @IBAction func downloadButtonPressed(_ sender: UIButton) {

    task = Task { do { let data = try await downloadData(from: url) // data Λ࢖͏ॲཧ } catch { if Task.isCancelled { // Ωϟϯηϧ࣌ͷॲཧ } else { // ΤϥʔϋϯυϦϯά } } } } }
  38. σʔλڝ߹ͷྫ final class Counter { private var count: Int =

    0 func increment() -> Int { count += 1 return count } }
  39. σʔλڝ߹ͷྫ final class Counter { private var count: Int =

    0 func increment() -> Int { count += 1 return count } }
  40. Before final class Counter { private let queue: DispatchQueue =

    .init(label: UUID().uuidString) private var count: Int = 0 func increment(completion: @escaping (Int) -> Void) { queue.async { [self] in count += 1 completion(count) } } }
  41. Before let counter: Counter = .init() counter.increment { count in

    print(count) // 1 or 2 } counter.increment { count in print(count) // 2 or 1 }
  42. A"er actor Counter { private var count: Int = 0

    func increment() -> Int { count += 1 return count } }
  43. σʔλڝ߹Λى͜͢ Counter final class Counter { private var count: Int

    = 0 func increment() -> Int { count += 1 return count } }
  44. A"er actor Counter { private var count: Int = 0

    func increment() -> Int { count += 1 return count } }
  45. A"er actor Counter { private var count: Int = 0

    func increment() async -> Int }
  46. A"er let counter: Counter = .init() Task.detached { print(await counter.increment())

    // 1 or 2 } Task.detached { print(await counter.increment()) // 2 or 1 }
  47. Counter ͷத͔Β Counter ͷϝιουΛ࢖͏ final class Counter { ... func

    increment(completion: @escaping (Int) -> Void) func incrementTwice(completion: @escaping (Int) -> Void) { } }
  48. Counter ͷத͔Β Counter ͷϝιουΛ࢖͏ final class Counter { ... func

    increment(completion: @escaping (Int) -> Void) func incrementTwice(completion: @escaping (Int) -> Void) { increment { [self] _ in increment { count in completion(count) } } } }
  49. ڝ߹ঢ়ଶͷྫ let counter: Counter = .init() counter.incrementTwice { count in

    print(count) // ? } counter.incrementTwice { count in print(count) // ? }
  50. ڝ߹ঢ়ଶͷྫ let counter: Counter = .init() counter.incrementTwice { count in

    print(count) // 2 } counter.incrementTwice { count in print(count) // 4 }
  51. ڝ߹ঢ়ଶͷྫ let counter: Counter = .init() counter.incrementTwice { count in

    print(count) // 3 } counter.incrementTwice { count in print(count) // 4 }
  52. ڝ߹ঢ়ଶͷྫ final class Counter { ... func increment(completion: @escaping (Int)

    -> Void) func incrementTwice(completion: @escaping (Int) -> Void) { increment { [self] _ in increment { count in completion(count) } } } }
  53. Before final class Counter { ... private func _increment() ->

    Int { count += 1 return count } func increment(completion: @escaping (Int) -> Void) { queue.async { [self] in _increment() completion(count) } } ... }
  54. Before final class Counter { ... func incrementTwice(completion: @escaping (Int)

    -> Void) { queue.async { [self] in _increment() _increment() completion(count) } } }
  55. A"er actor Counter { ... @discardableResult func increment() -> Int

    { count += 1 return count } func incrementTwice() -> Int { increment() increment() return count } }
  56. Case 14 (A*er) actor Counter { private var count: Int

    = 0 @discardableResult func increment() -> Int { count += 1 return count } }
  57. A"er actor Counter { var count: Int = 0 @discardableResult

    func increment() -> Int { count += 1 return count } }
  58. Effec%ul Read-only ϓϩύςΟSE-0310 class Foo { var value : Int

    { get { ... } } } SE-0310 h)ps:/ /github.com/apple/swi;-evolu=on/blob/main/proposals/0310-effecAul-readonly-proper=es.md
  59. Effec%ul Read-only ϓϩύςΟSE-0310 class Foo { var value : Int

    { get async throws { ... } } } SE-0310 h)ps:/ /github.com/apple/swi;-evolu=on/blob/main/proposals/0310-effecAul-readonly-proper=es.md
  60. ObservableObject ͷಋೖ final class UserViewController: UIViewController { private let state:

    UserViewState ... override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) state.loadUser() } }
  61. ObservableObject ͷಋೖ final class UserViewController: UIViewController { ... override func

    viewDidLoad() { ... state .objectWillChange .receive(on: DispatchQueue.main) .sink { [self] _ in // state Λ View ʹ൓ө͢Δॲཧ } .store(in: &cancellables) } ... }
  62. A"er actor UserViewState: ObservableObject { let userID: User.ID @Published var

    user: User? ... func loadUser() async { do { user = try await fetchUser(for: userID) } catch { // ΤϥʔϋϯυϦϯά } } }
  63. A"er final class UserViewController: UIViewController { ... override func viewDidAppear(_

    animated: Bool) { super.viewDidAppear(animated) state.loadUser() // } }
  64. A"er final class UserViewController: UIViewController { ... override func viewDidAppear(_

    animated: Bool) { super.viewDidAppear(animated) Task { await state.loadUser() // } } }
  65. A"er state .objectWillChange .receive(on: DispatchQueue.main) .sink { [self] _ in

    // state Λ View ʹ൓ө͢Δॲཧ } .store(in: &cancellables)
  66. A"er state .objectWillChange .receive(on: DispatchQueue.main) .sink { [self] _ in

    // state Λ View ʹ൓ө͢Δॲཧ let user = await state.user nameLabel.text = user?.name } .store(in: &cancellables)
  67. A"er state .objectWillChange .receive(on: DispatchQueue.main) .sink { [self] _ in

    // state Λ View ʹ൓ө͢Δॲཧ Task { let user = await state.user nameLabel.text = user?.name } } .store(in: &cancellables)
  68. final class User: Sendable { let id: ID var name:

    String var age: Int ... } ⛔ var ϓϩύςΟΛ࣋ͭͷͰίϯύΠϧΤϥʔ
  69. final class User: Sendable { let id: ID let name:

    String let age: Int ... } ✅ ΠϛϡʔλϒϧͳͷͰ OK
  70. final class User: Sendable { let id: ID let name:

    NSString let age: Int ... } ⛔ Sendable Ͱͳ͍ܕͷϓϩύςΟΛ࣋ͭͷͰίϯύΠϧΤϥʔ
  71. final class User: Sendable { let id: ID let firstName:

    String let familyName: String let age: Int var name: String { "\(firstName) \(familyName)" } ... } ✅ Computed Property Λ͍࣋ͬͯͯ΋ΠϛϡʔλϒϧͳͷͰ OK
  72. final class User: Sendable { let id: ID let firstName:

    String let familyName: String let age: Int private var _name: String? // var name: String { if _name == nil { _name = "\(firstName) \(familyName)" } return _name! } ... }
  73. final class User: @unchecked Sendable { let id: ID let

    firstName: String let familyName: String let age: Int private var _name: String? // var name: String { if _name == nil { _name = "\(firstName) \(familyName)" } return _name! } ... }
  74. actor Foo { func bar(_ f: @Sendable (String) -> Int)

    { // // ... } } Ϋϩʔδϟ͸ϓϩτίϧʹద߹Ͱ͖ͳ͍ͷͰ @Sendable Λ࢖͏
  75. Case 18 (A*er) state .objectWillChange .receive(on: DispatchQueue.main) .sink { [self]

    _ in // state Λ View ʹ൓ө͢Δॲཧ Task { let user = await state.user nameLabel.text = user?.name } } .store(in: &cancellables)
  76. A"er state .objectWillChange .receive(on: DispatchQueue.main) .sink { [self] _ in

    // state Λ View ʹ൓ө͢Δॲཧ nameLabel.text = state.user?.name } .store(in: &cancellables)
  77. @MainActor ͸ @StateObject ʹ΋ඞਢ struct UserView: View { @StateObject private

    var state: UserViewState ... var body: some View { VStack { if let user = state.user { Text(user.name) } ... } .onAppear { state.loadUser() } } }