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

Swift Concurrency時代のiOSアプリの作り方

Yuta Koshizawa
September 12, 2022

Swift Concurrency時代のiOSアプリの作り方

昨年、Swift Concurrencyが導入されました。当初はiOS 15のみでサポートされていましたが、Concurrencyのback deploymentが実現されたため、iOS 13以降であれば今すぐにでもConcurrencyを取り入れることができます。

しかし、実際にConcurencyを取り入れようとすると、参考となる情報はまだまだ少ないのではないでしょうか。Conccurency自体の情報は豊富でも、iOSアプリ開発での活用、特にactorや単体テストなどについてはほとんど語られていないように思います。

本トークでは、iOSアプリ開発におけるConcurrency活用の一つのベースラインとなることを目指して、async/awaitやTask、actor、MainActorなどを、アプリやテストのコードにどのように取り入れるか、具体例を用いて紹介します。

Yuta Koshizawa

September 12, 2022
Tweet

More Decks by Yuta Koshizawa

Other Decks in Programming

Transcript

  1. Swi$ Concurrency ࣌୅ͷ iOS ΞϓϦͷ࡞Γํ Yuta Koshizawa @koher

  2. ͜ͷ਺೥ͷ iOS ΞϓϦ։ൃͷมԽ • Swi%UI (2019) • Combine (2019) •

    Swi$ Concurrency (2021)
  3. Swi$ Concurrency ࣌୅ͷ iOS ΞϓϦ։ൃͷ ϕʔεϥΠϯΛߟ͑Δࢀߟʹ

  4. None
  5. None
  6. None
  7. None
  8. ViewController ʹϕλॻ͖ ↓ ViewModel Λಋೖ ↓ DI ͱςετ

  9. ViewController ʹϕλॻ͖

  10. LoginViewController final class LoginViewController: UIViewController { @IBOutlet private var idField:

    UITextField! @IBOutlet private var passwordField: UITextField! @IBOutlet private var loginButton: UIButton! ...
  11. LoginViewController final class LoginViewController: UIViewController { @IBOutlet private var idField:

    UITextField! @IBOutlet private var passwordField: UITextField! @IBOutlet private var loginButton: UIButton! @IBAction private func loginButtonPressed( sender: UIButton) { ... } ...
  12. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // ͜͜ͰϩάΠϯͷ

    API Λୟ͘ }
  13. AuthAPI.logIn enum AuthAPI { static func logIn( id: String, password:

    String ) async throws -> String { // ID token ... } }
  14. AuthAPI.logIn enum AuthAPI { static func logIn( for id: String,

    with password: String ) async throws -> String { // ID token ... } }
  15. AuthAPI.logIn enum AuthAPI { static func logIn( for id: User.ID,

    with password: String ) async throws -> IDToken { ... } }
  16. User.ID struct User: Identifiable { let id: String var nickname:

    String var birthday: Date }
  17. User.ID struct User: Identifiable { let id: ID var nickname:

    String var birthday: Date }
  18. User.ID struct User: Identifiable { let id: ID var nickname:

    String var birthday: Date struct ID: Hashable { let rawValue: String init(rawValue: String) { self.rawValue = rawValue } } }
  19. User.ID struct User: Identifiable, Sendable { let id: ID var

    nickname: String var birthday: Date struct ID: Hashable { let rawValue: String init(rawValue: String) { self.rawValue = rawValue } } }
  20. Sendable ฒߦʹѻͬͯ΋҆શͳܕͰ͋Δ͜ͱΛࣔ͢ • actor ڥքΛ௒͑Δͷʹඞཁ • Task.init ͳͲ @Sendable ΫϩʔδϟʹΩϟϓνϟ͢Δͷ

    ʹඞཁ
  21. User.ID struct User: Identifiable, Sendable { let id: ID var

    nickname: String var birthday: Date struct ID: Hashable { let rawValue: String init(rawValue: String) { self.rawValue = rawValue } } }
  22. User.ID struct User: Identifiable, Sendable { let id: ID var

    nickname: String var birthday: Date struct ID: Hashable, Sendable { let rawValue: String init(rawValue: String) { self.rawValue = rawValue } } }
  23. ! ฒߦʹѻ͏ܕ͸ Sendable ʹ४ڌͤ͞Δ

  24. User.ID struct User: Identifiable, Sendable { let id: ID var

    nickname: String var birthday: Date struct ID: Hashable, Sendable { let rawValue: String init(rawValue: String) { self.rawValue = rawValue } } }
  25. @unchecked Sendable // Xcode 13 ·Ͱͷ৔߹ extension Date: @unchecked Sendable

    {}
  26. ! Sendable ରԠ͍ͯ͠ͳ͍ܕ͸ @unchecked Sendable Ͱ ແཧ΍Γ४ڌͤ͞ΒΕΔ ※ Sendable ʹͯ͠΋҆શͳ৔߹͚ͩ

  27. Sendable ʹͯ͠΋҆શͳܕͷྫ • ७ਮͳ struct • ΠϛϡʔλϒϧΫϥε • ϛϡʔλϒϧ͚ͩͲϩοΫ౳Ͱอޢ͞ΕͨΫϥε •

    actor • ɾɾɾ
  28. AuthAPI.logIn enum AuthAPI { static func logIn( for id: User.ID,

    with password: String ) async throws -> IDToken { ... } }
  29. AuthAPI.logIn static func logIn(...) async throws -> IDToken { let

    url: URL = ... let request: URLRequest = ... // Concurrency Ҏલ let dataTask = URLSession.shared .dataTask(with: request) { // ίʔϧόοΫͰ݁ՌΛड͚औΔ } ... }
  30. AuthAPI.logIn static func logIn(...) async throws -> IDToken { let

    url: URL = ... let request: URLRequest = ... // Concurrency Ҏ߱ let (data, response) = try await URLSession.shared .data(for: request) // ໭Γ஋Ͱ݁ՌΛड͚औΔ ... }
  31. AuthAPI.logIn static func logIn(...) async throws -> IDToken { let

    (data, response) = ... let idToken: IDToken = ... return idToken }
  32. ͢ͰʹίʔϧόοΫ൛͕͋Δ৔߹ enum AuthAPI { static func logIn( for id: User.ID,

    with password: String, completion: @escaping (Result<IDToken, Error>) -> Void ) { ... } }
  33. ͢ͰʹίʔϧόοΫ൛͕͋Δ৔߹ extension AuthAPI { static func logIn( for id: User.ID,

    with password: String ) async throws -> IDToken { try await withCheckedThrowingContinuation { continuation in logIn(for: id, with: password) { result in continuation.resume(with: result) } } }
  34. ! Con%nua%on Λ࢖͑͹ίʔϧόοΫΛ async ʹม׵Ͱ͖Δ

  35. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // ͜͜ͰϩάΠϯͷ

    API Λୟ͘ }
  36. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { let idToken

    = try await AuthAPI.logIn( // for: .init(rawValue: idField.text ?? ""), with: passwordField.text ?? "" ) ... }
  37. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    // async throws let idToken = try await AuthAPI.logIn( // for: .init(rawValue: idField.text ?? ""), with: passwordField.text ?? "" ) ... } }
  38. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task.init {

    // async throws let idToken = try await AuthAPI.logIn( // for: .init(rawValue: idField.text ?? ""), with: passwordField.text ?? "" ) ... } }
  39. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    // async throws let idToken = try await AuthAPI.logIn( // for: .init(rawValue: idField.text ?? ""), with: passwordField.text ?? "" ) ... } }
  40. ! ಉظϝιου͔ΒඇಉظॲཧΛ ࢝ΊΔʹ͸ Task.init Λ࢖͏

  41. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    // async throws let idToken = try await AuthAPI.logIn( // for: .init(rawValue: idField.text ?? ""), with: passwordField.text ?? "" ) ... } }
  42. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    // async throws do { let idToken = try await AuthAPI.logIn(...) ... } catch { // ΤϥʔϋϯυϦϯά } } }
  43. ! Task.init ʹ౉͢ΫϩʔδϟͰ͸ do/catch Λ๨Εͳ͍Α͏ʹ͢Δ

  44. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    loginButton.isEnabled = false defer { loginButton.isEnabled = true } ... } }
  45. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // main

    εϨου Task { // ??? loginButton.isEnabled = false defer { loginButton.isEnabled = true } ... } }
  46. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // main

    εϨου Task { // main εϨου loginButton.isEnabled = false defer { loginButton.isEnabled = true } ... } }
  47. UIViewController ͱ @MainActor @MainActor class UIViewController : UIResponder

  48. loginButtonPressed @IBAction private func loginButtonPressed(...) { // main εϨου Task

    { // main εϨου loginButton.isEnabled = false defer { loginButton.isEnabled = true } do { let idToken = try await AuthAPI.logIn(...) // main εϨου
  49. ! Task.init ͸ actor context ΛҾ͖ܧ͙

  50. ! UIViewController ͸ MainActor Ͱ อޢ͞Ε͍ͯΔ

  51. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // main

    εϨου Task { // main εϨου loginButton.isEnabled = false defer { loginButton.isEnabled = true } ... } }
  52. Swi$ ͷόά @MainActor ͳϝιουͰɺ Task.init ʹ౉͢Ϋϩʔδϟͷத Ͱ defer ͢Δͱɺ MainActor

    Ͱอޢ͞Ε͍ͯͳ͍ͱ൑அ͞Εͯ ͠·͏ɻ 2021/08/01 ʹϑΥʔϥϜͰใࠂ Swi* Forums #50796 ͞Ε௕Β͘ղܾ͞ Ε͍ͯͳ͔͕ͬͨɺ 2022/08/22 ʹ PR apple/swi* #60688 ͕࡞ΒΕɺ 8/31 ʹϚʔδɻ apple/swi) #60688 h0ps:/ /github.com/apple/swi)/pull/60688 Swi$ Forums #50796 h3ps:/ /forums.swi$.org/t/a-bug-cant-defer-actor-isolated-variable-access/50796
  53. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // main

    εϨου Task { // main εϨου loginButton.isEnabled = false defer { loginButton.isEnabled = true } ... } }
  54. MainActor Ͱ࣮ߦ͢Δํ๏ • await MainActor.run { } • Task {

    @MainActor in ... }
  55. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // main

    εϨου Task { // main εϨου loginButton.isEnabled = false defer { loginButton.isEnabled = true } ... } }
  56. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // main

    εϨου Task { // main εϨου loginButton.isEnabled = false defer { Task { await MainActor.run { loginButton.isEnabled = true } }
  57. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // main

    εϨου Task { // main εϨου loginButton.isEnabled = false defer { Task { @MainActor in loginButton.isEnabled = true } }
  58. MainActor.run ͱ Task { @MainActor in } func foo() async

    { await MainActor.run { // ϝΠϯεϨουͰ࣮ߦ͍ͨ͠ॲཧ } } func bar() { Task { @MainActor in // ϝΠϯεϨουͰ࣮ߦ͍ͨ͠ॲཧ } }
  59. ! async ϝιου͔ΒҰ෦ͷॲཧΛ ϝΠϯεϨουͰ࣮ߦ͍ͨ͠৔߹͸ await MainActor.run { }

  60. ! ಉظϝιου͔ΒϝΠϯεϨουʹ ॲཧ౤͍͛ͨ৔߹ʹ͸ Task { @MainActor in }

  61. DispatchQueue.main.async ͸ʁ @Sendable ͕෇͍͍ͯͳ͍ͷͰආ͚ͨํ͕ྑ͍ɻ func async( group: DispatchGroup? = nil,

    qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping () -> Void )
  62. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    loginButton.isEnabled = false defer { Task { @MainActor in loginButton.isEnabled = true } } ... }
  63. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    loginButton.isEnabled = false defer { loginButton.isEnabled = true } ... } }
  64. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    loginButton.isEnabled = false ... loginButton.isEnabled = true }
  65. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { let idToken = try await AuthAPI.logIn(...) ... } catch { // ΤϥʔϋϯυϦϯά } } }
  66. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { let idToken = try await AuthAPI.logIn(...) let data: Data = idToken.rawValue .data(using: .utf8)! let url: URL = .libraryDirectory .appendingPathComponent("IDToken") try data.write(to: url, options: .atomic) ...
  67. Task.detached Task.detached { let data: Data = idToken.rawValue .data(using: .utf8)!

    let url: URL = .libraryDirectory .appendingPathComponent("IDToken") try data.write(to: url, options: .atomic) }
  68. ! ॏΊͷॲཧ͸ Task.detached Ͱ ผεϨουʹಀ͢

  69. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { let idToken = try await AuthAPI.logIn(...) Task.detached { ... try data.write(to: url, options: .atomic) } ...
  70. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { let idToken = try await AuthAPI.logIn(...) _ = try await Task.detached { ... try data.write(to: url, options: .atomic) }.value ...
  71. ! ޙଓॲཧ͕͋Δ৔߹͸ Task.detached ͔Β value Λऔಘͯ͠ ॲཧ͕ऴΘΔͷΛ଴ͭ

  72. Task.detached ΑΓ ΋ͬͱྑ͍ํ๏͕͋Δ

  73. actor actor IDTokenStore { func update(_ value: IDToken) throws {

    let data: Data = value.rawValue .data(using: .utf8)! let url: URL = .libraryDirectory .appendingPathComponent("IDToken") try data.write(to: url, options: .atomic) } }
  74. actor actor IDTokenStore { var value: IDToken { get throws

    { let url: URL = .libraryDirectory .appendingPathComponent("IDToken") let data: Data = try .init(contentsOf: url) let rawValue: String = .init(data: data, encoding: .utf8)! return IDToken(rawValue: rawValue) } } ...
  75. ! ഉଞతʹ࣮ߦ͍ͨ͠ॲཧ͸ actor Ͱอޢ

  76. actor actor IDTokenStore { static let shared: IDTokenStore = .init()

    private init() {} ... }
  77. ! actor ͸Πϯελϯε͝ͱʹ Serial Executor Λ࣋ͪอޢ͞ΕΔ ※ άϩʔόϧʹอޢ͍ͨ͠ͳΒγϯάϧτϯʹ͢Δ

  78. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { let idToken = try await AuthAPI.logIn(...) try await IDTokenStore.shared .update(idToken) ...
  79. actor ͷϝιου͸֎͔ΒݟΔͱ async ʹͳΔ actor IDTokenStore { func update(_ value:

    IDToken) throws { let data: Data = value.rawValue .data(using: .utf8)! let url: URL = .libraryDirectory .appendingPathComponent("IDToken") try data.write(to: url, options: .atomic) } }
  80. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { let idToken = try await AuthAPI.logIn(...) try await IDTokenStore.shared .update(idToken) ...
  81. AuthService actor AuthService { func logIn( for id: User.ID, with

    password: String ) async throws { ... } }
  82. AuthService actor AuthService { func logIn( for id: User.ID, with

    password: String ) async throws { let idToken = try await AuthAPI .logIn(for: id, with: password) try await IDTokenStore.shared .update(idToken) } }
  83. A B AuthAPI.login (1) (2) IDTokenStore.sh ared.update (4) (3)

  84. AuthService actor AuthService { func logIn( for id: User.ID, with

    password: String ) async throws { let idToken = try await AuthAPI .logIn(for: id, with: password) try await IDTokenStore.shared .update(idToken) } }
  85. AuthService actor AuthService { private var isLoggingIn: Bool = false

    func logIn( for id: User.ID, with password: String ) async throws { let idToken = try await AuthAPI .logIn(for: id, with: password) try await IDTokenStore.shared .update(idToken) }
  86. AuthService actor AuthService { private var isLoggingIn: Bool = false

    func logIn( for id: User.ID, with password: String ) async throws { if isLoggingIn { ... } isLoggingIn = true defer { isLoggingIn = false} ... }
  87. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { let idToken = try await AuthAPI.logIn(...) try await IDTokenStore.shared .update(idToken) ...
  88. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { try await AuthService.shared.logIn(...) ...
  89. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { try await AuthService.shared.logIn(...) parent?.dismiss(animated: true) } catch { ...
  90. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { ... } catch { logger.warning("\(error)") ...
  91. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { ... } catch { ... let title: String let message: String if error is LoginError { title = "ϩάΠϯΤϥʔ" message = "ID·ͨ͸ύεϫʔυ͕ਖ਼͋͘͠Γ·ͤΜɻ" } else ...
  92. loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { Task {

    ... do { ... } catch { ... let alertController: UIAlertController = .init(title: title, message: message, preferredStyle: .alert) alertController .addAction(.init(title: "ด͡Δ", style: .default, handler: nil)) present(alertController, animated: true) }
  93. ViewModel Λಋೖ

  94. UIKit Λར༻͢ΔίʔυͱͦΕҎ֎Λ෼཭

  95. None
  96. LoginViewModel import Combine final class LoginViewModel { @Published var id:

    String = "" @Published var password: String = "" @Published private(set) var isLoginButtonEnabled: Bool = true }
  97. LoginViewModel import Combine final class LoginViewModel: ObservableObject { @Published var

    id: String = "" @Published var password: String = "" @Published private(set) var isLoginButtonEnabled: Bool = true }
  98. LoginViewModel import Combine @MainActor final class LoginViewModel: ObservableObject { @Published

    var id: String = "" @Published var password: String = "" @Published private(set) var isLoginButtonEnabled: Bool = true }
  99. ! ViewModel Λ MainActor Ͱอޢ͢Δ

  100. LoginViewModel import Combine @MainActor final class LoginViewModel: ObservableObject { ...

    func loginButtonPressed() async { ... } }
  101. LoginViewController.loginButtonPressed // LoginViewController @IBAction private func loginButtonPressed(...) { Task {

    loginButton.isEnabled = false defer { loginButton.isEnabled = true } do { ... } catch { ... } } }
  102. LoginViewController.loginButtonPressed // LoginViewController @IBAction private func loginButtonPressed(...) { Task {

    } }
  103. LoginViewController.loginButtonPressed // LoginViewController @IBAction private func loginButtonPressed(...) { Task {

    await viewModel.loginButtonPressed() } }
  104. LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { }

  105. LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { isLoginButtonEnabled = false

    defer { isLoginButtonEnabled = true } do { ... } catch { ... } }
  106. ! Task.init Λ ViewModel Ͱ͸ͳ͘ View ଆʹॻ͘ ※ ViewModel ͷςετ͕ॻ͖΍͘͢ͳΔ

    ※ ͨͩ͠ɺ ViewModel ͰΩϟϯηϧΛѻ͍ͮΒ͘ͳΔ
  107. WWDC2021 10019 h*ps:/ /developer.apple.com/videos/play/wwdc2021/10019/

  108. LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { isLoginButtonEnabled = false

    defer { isLoginButtonEnabled = true } ... }
  109. isLoginButtonEnabled ͷ View ΁ͷ൓ө // LoginViewController override func viewDidLoad() {

    super.viewDidLoad() ... viewModel.$isLoginButtonEnabled .sink { [weak self] isEnabled in guard let self else { return } self.loginButton.isEnabled = isEnabled } .store(in: &cancellables)
  110. LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { isLoginButtonEnabled = false

    defer { isLoginButtonEnabled = true } ... }
  111. LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { isLoginButtonEnabled = false

    defer { isLoginButtonEnabled = true } do { try await AuthService.shared.logIn(...) dismiss.send() } catch { ... } }
  112. LoginViewModel.dismiss @MainActor final class LoginViewModel: ObservableObject { ... let dismiss:

    PassthroughSubject<Void, Never> = .init() ... }
  113. dismiss ͷ View Ͱͷ࣮ߦ // LoginViewController override func viewDidLoad() {

    super.viewDidLoad() ... viewModel.dismiss .sink { [weak self] _ in self?.parent?.dismiss(animated: true) } .store(in: &cancellables)
  114. LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { ... do {

    ... } catch { logger.warning("\(error)") if error is LoginError { ... } else ... } }
  115. LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { ... do {

    ... } catch { logger.warning("\(error)") if error is LoginError { presentLoginErrorAlert.send(...) } else ... } }
  116. LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { ... do {

    ... } catch { logger.warning("\(error)") if error is LoginError { loginErrorMessage = .login } else ... } }
  117. LoginViewModel.loginErrorMessage @MainActor final class LoginViewModel: ObservableObject { ... @Published var

    loginErrorMessage: LoginErrorMessage? ... }
  118. LoginErrorMessage enum LoginErrorMessage: Hashable { case login case network case

    server case system }
  119. Swi$UI struct LoginView: View { @StateObject private var viewModel: LoginViewModel

    = .init() ... }
  120. Swi$UI struct LoginView: View { ... var body: some View

    { VStack(alignment: .center) { Spacer() TextField("ID", text: $viewModel.id) .textInputAutocapitalization(.never) .textFieldStyle(.roundedBorder) .frame(width: 200) ...
  121. Swi$UI struct LoginView: View { ... var body: some View

    { VStack(alignment: .center) { ... Button("Log in") { Task { await viewModel .loginButtonPressed() } } ...
  122. Swi$UI struct LoginView: View { ... var body: some View

    { VStack(alignment: .center) { ... } .onReceive(viewModel.dismiss) { _ in dismiss() } ... } }
  123. Swi$UI struct LoginView: View { ... @Environment(\.dismiss) private var dismiss

    var body: some View { VStack(alignment: .center) { ... } .onReceive(viewModel.dismiss) { _ in dismiss() } ... } }
  124. DI ͱςετ

  125. LoginViewModel ͷςετ͕ॻ͖ͮΒ͍ func loginButtonPressed() async { isLoginButtonEnabled = false defer

    { isLoginButtonEnabled = true } do { // αʔόʔʹΞΫηεɺϑΝΠϧ I/O try await AuthService.shared.logIn(...) dismiss.send() } catch { ... } }
  126. ςετ͔Βαʔόʔ΍ϑΝΠϧʹΞΫηεͨ͘͠ͳ͍ • ωοτϫʔΫ΍αʔόʔͷঢ়ଶͰ݁Ռ͕ෆఆ • ੒ޭɾࣦഊέʔεͳͲΛ੍ޚͰ͖ͳ͍ • ςετͷ౓ʹσʔλΛॻ͖ࠐΈͨ͘ͳ͍

  127. ಠཱͨ͠ίΞϨΠϠʔύλʔϯ Core Layer Pa*ern h*ps:/ /blog.shin1x1.com/entry/independent-core-layer-pa*ern

  128. None
  129. None
  130. None
  131. AuthServiceProtocol protocol AuthServiceProtocol { static var shared: Self { get

    } func logIn( for id: User.ID, with password: String ) async throws ... }
  132. AuthService actor AuthService: AuthServiceProtocol { ... }

  133. LoginViewModel @MainActor final class LoginViewModel: ObservableObject { ... }

  134. LoginViewModel @MainActor final class LoginViewModel<AuthService>: ObservableObject where AuthService: AuthServiceProtocol {

    ... }
  135. ܕύϥϝʔλΠϯδΣΫγϣϯ

  136. AuthService ͷར༻Օॴͷίʔυ͸มΘΒͳ͍ func loginButtonPressed() async { isLoginButtonEnabled = false defer

    { isLoginButtonEnabled = true } do { try await AuthService.shared.logIn(...) dismiss.send() } catch { ... } }
  137. ! DI Λ׆༻ͯ͠ ViewModel Λ UI ΍ωοτϫʔΫ౳͔Β ಠཱͤ͞Δ

  138. LoginViewModelTests @MainActor final class LoginViewModelTests: XCTestCase { ... }

  139. loginErrorMessage ͷςετ func testLoginErrorMessageByLoginError() async { let viewModel: LoginViewModel<AuthService> =

    .init() ... }
  140. loginErrorMessage ͷςετ func testLoginErrorMessageByLoginError() async { let viewModel: LoginViewModel<AuthService> =

    .init() await viewModel.loginButtonPressed() // LoginError ... }
  141. loginErrorMessage ͷςετ func testLoginErrorMessageByLoginError() async { let viewModel: LoginViewModel<AuthService> =

    .init() await viewModel.loginButtonPressed() // LoginError XCTAssertEqual(viewModel.loginErrorMessage, .login) }
  142. ! async/await Λ࢖͑͹ ඇಉظϝιουͷςετΛ γϯϓϧʹॻ͚Δ ※ Task.init Λ ViewModel ଆʹॻ͔ͳ͔͔ͬͨΒͦ͜

  143. loginErrorMessage ͷςετ func testLoginErrorMessageByLoginError() async { let viewModel: LoginViewModel<AuthService> =

    .init() await viewModel.loginButtonPressed() // LoginError XCTAssertEqual(viewModel.loginErrorMessage, .login) }
  144. ςετ༻ͷ AuthService private final class AuthService: AuthServiceProtocol { static var

    shared: AuthService = .init() private init() {} ... }
  145. ςετ༻ͷ AuthService private final class AuthService: AuthServiceProtocol { ... func

    logIn(for id: User.ID, with password: String) async throws { throw LoginError() } }
  146. ςετ༻ͷ AuthService private final class AuthService: AuthServiceProtocol { ... var

    logInResult: Result<Void, Error>? func logIn(for id: User.ID, with password: String) async throws { try logInResult!.get() } }
  147. loginErrorMessage ͷςετ func testLoginErrorMessageByLoginError() async { let viewModel: LoginViewModel<AuthService> =

    .init() await viewModel.loginButtonPressed() XCTAssertEqual(viewModel.loginErrorMessage, .login) }
  148. loginErrorMessage ͷςετ func testLoginErrorMessageByLoginError() async { let viewModel: LoginViewModel<AuthService> =

    .init() AuthService.shared.logInResult = .failure(LoginError()) await viewModel.loginButtonPressed() XCTAssertEqual(viewModel.loginErrorMessage, .login) }
  149. loginErrorMessage ͷςετ func testLoginErrorMessageByServerError() async { let viewModel: LoginViewModel<AuthService> =

    .init() AuthService.shared.logInResult = .failure(ServerError.response(...)) await viewModel.loginButtonPressed() XCTAssertEqual(viewModel.loginErrorMessage, .server) }
  150. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { let viewModel: LoginViewModel<AuthService> =

    .init() ... }
  151. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { let viewModel: LoginViewModel<AuthService> =

    .init() XCTAssertTrue(viewModel.isLoginButtonEnabled) ... }
  152. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { let viewModel: LoginViewModel<AuthService> =

    .init() XCTAssertTrue(viewModel.isLoginButtonEnabled) viewModel.loginButtonPressed() ... }
  153. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { let viewModel: LoginViewModel<AuthService> =

    .init() XCTAssertTrue(viewModel.isLoginButtonEnabled) viewModel.loginButtonPressed() XCTAssertFalse(viewModel.isLoginButtonEnabled) ... }
  154. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { let viewModel: LoginViewModel<AuthService> =

    .init() XCTAssertTrue(viewModel.isLoginButtonEnabled) viewModel.loginButtonPressed() XCTAssertFalse(viewModel.isLoginButtonEnabled) ... XCTAssertTrue(viewModel.isLoginButtonEnabled) }
  155. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { let viewModel: LoginViewModel<AuthService> =

    .init() XCTAssertTrue(viewModel.isLoginButtonEnabled) await viewModel.loginButtonPressed() XCTAssertFalse(viewModel.isLoginButtonEnabled) ... XCTAssertTrue(viewModel.isLoginButtonEnabled) }
  156. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { let viewModel: LoginViewModel<AuthService> =

    .init() XCTAssertTrue(viewModel.isLoginButtonEnabled) async let logIn: Void = viewModel.loginButtonPressed() XCTAssertFalse(viewModel.isLoginButtonEnabled) await logIn XCTAssertTrue(viewModel.isLoginButtonEnabled) }
  157. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { let viewModel: LoginViewModel<AuthService> =

    .init() XCTAssertTrue(viewModel.isLoginButtonEnabled) async let logIn: Void = viewModel.loginButtonPressed() XCTAssertFalse(viewModel.isLoginButtonEnabled) AuthService.shared.??? await logIn XCTAssertTrue(viewModel.isLoginButtonEnabled)
  158. ςετ༻ͷ AuthService private final class AuthService: AuthServiceProtocol { ... var

    logInResult: Result<Void, Error>? func logIn(for id: User.ID, with password: String) async throws { try logInResult!.get() } }
  159. ςετ༻ͷ AuthService private final class AuthService: AuthServiceProtocol { ... var

    logInContinuation: CheckedContinuation<Void, Error>? func logIn(for id: User.ID, with password: String) async throws { try await withCheckedThrowingContinuation { continuation in logInContinuation = continuation } } }
  160. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { ... async let logIn:

    Void = viewModel.loginButtonPressed() XCTAssertFalse(viewModel.isLoginButtonEnabled) AuthService.shared.??? await logIn
  161. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { ... async let logIn:

    Void = viewModel.loginButtonPressed() XCTAssertFalse(viewModel.isLoginButtonEnabled) AuthService.shared.logInContinuation? .resume(returning: ()) await logIn
  162. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { ... async let logIn:

    Void = viewModel.loginButtonPressed() await Task.yield() XCTAssertFalse(viewModel.isLoginButtonEnabled) AuthService.shared.logInContinuation? .resume(returning: ()) await logIn
  163. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { ... async let logIn:

    Void = viewModel.loginButtonPressed() while AuthService.shared.logInContinuation == nil { await Task.yield() } XCTAssertFalse(viewModel.isLoginButtonEnabled) AuthService.shared.logInContinuation? .resume(returning: ()) await logIn
  164. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { ... async let logIn:

    Void = viewModel.loginButtonPressed() while AuthService.shared.logInContinuation == nil { await Task.yield() } XCTAssertFalse(viewModel.isLoginButtonEnabled) AuthService.shared.logInContinuation? .resume(returning: ()) AuthService.shared.logInContinuation = nil await logIn
  165. isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { ... await logIn XCTAssertTrue(viewModel.isLoginButtonEnabled)

    }
  166. ·ͱΊ • async/await, Sendable, Con'nua'on, Task, actor, MainActor • ViewModel

    ͷಋೖͱඇಉظॲཧͷςετ
  167. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ