Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

ViewController ʹϕλॻ͖ ↓ ViewModel Λಋೖ ↓ DI ͱςετ

Slide 9

Slide 9 text

ViewController ʹϕλॻ͖

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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) { ... } ...

Slide 12

Slide 12 text

loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // ͜͜ͰϩάΠϯͷ API Λୟ͘ }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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 } } }

Slide 19

Slide 19 text

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 } } }

Slide 20

Slide 20 text

Sendable ฒߦʹѻͬͯ΋҆શͳܕͰ͋Δ͜ͱΛࣔ͢ • actor ڥքΛ௒͑Δͷʹඞཁ • Task.init ͳͲ @Sendable ΫϩʔδϟʹΩϟϓνϟ͢Δͷ ʹඞཁ

Slide 21

Slide 21 text

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 } } }

Slide 22

Slide 22 text

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 } } }

Slide 23

Slide 23 text

! ฒߦʹѻ͏ܕ͸ Sendable ʹ४ڌͤ͞Δ

Slide 24

Slide 24 text

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 } } }

Slide 25

Slide 25 text

@unchecked Sendable // Xcode 13 ·Ͱͷ৔߹ extension Date: @unchecked Sendable {}

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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) // ໭Γ஋Ͱ݁ՌΛड͚औΔ ... }

Slide 31

Slide 31 text

AuthAPI.logIn static func logIn(...) async throws -> IDToken { let (data, response) = ... let idToken: IDToken = ... return idToken }

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

͢ͰʹίʔϧόοΫ൛͕͋Δ৔߹ 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) } } }

Slide 34

Slide 34 text

! Con%nua%on Λ࢖͑͹ίʔϧόοΫΛ async ʹม׵Ͱ͖Δ

Slide 35

Slide 35 text

loginButtonPressed @IBAction private func loginButtonPressed( sender: UIButton) { // ͜͜ͰϩάΠϯͷ API Λୟ͘ }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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 ?? "" ) ... } }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

! ಉظϝιου͔ΒඇಉظॲཧΛ ࢝ΊΔʹ͸ Task.init Λ࢖͏

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

! Task.init ʹ౉͢ΫϩʔδϟͰ͸ do/catch Λ๨Εͳ͍Α͏ʹ͢Δ

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

UIViewController ͱ @MainActor @MainActor class UIViewController : UIResponder

Slide 48

Slide 48 text

loginButtonPressed @IBAction private func loginButtonPressed(...) { // main εϨου Task { // main εϨου loginButton.isEnabled = false defer { loginButton.isEnabled = true } do { let idToken = try await AuthAPI.logIn(...) // main εϨου

Slide 49

Slide 49 text

! Task.init ͸ actor context ΛҾ͖ܧ͙

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

MainActor Ͱ࣮ߦ͢Δํ๏ • await MainActor.run { } • Task { @MainActor in ... }

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

MainActor.run ͱ Task { @MainActor in } func foo() async { await MainActor.run { // ϝΠϯεϨουͰ࣮ߦ͍ͨ͠ॲཧ } } func bar() { Task { @MainActor in // ϝΠϯεϨουͰ࣮ߦ͍ͨ͠ॲཧ } }

Slide 59

Slide 59 text

! async ϝιου͔ΒҰ෦ͷॲཧΛ ϝΠϯεϨουͰ࣮ߦ͍ͨ͠৔߹͸ await MainActor.run { }

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

DispatchQueue.main.async ͸ʁ @Sendable ͕෇͍͍ͯͳ͍ͷͰආ͚ͨํ͕ྑ͍ɻ func async( group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping () -> Void )

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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) ...

Slide 67

Slide 67 text

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) }

Slide 68

Slide 68 text

! ॏΊͷॲཧ͸ Task.detached Ͱ ผεϨουʹಀ͢

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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 ...

Slide 71

Slide 71 text

! ޙଓॲཧ͕͋Δ৔߹͸ Task.detached ͔Β value Λऔಘͯ͠ ॲཧ͕ऴΘΔͷΛ଴ͭ

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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) } }

Slide 74

Slide 74 text

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) } } ...

Slide 75

Slide 75 text

! ഉଞతʹ࣮ߦ͍ͨ͠ॲཧ͸ actor Ͱอޢ

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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) } }

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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) } }

Slide 83

Slide 83 text

A B AuthAPI.login (1) (2) IDTokenStore.sh ared.update (4) (3)

Slide 84

Slide 84 text

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) } }

Slide 85

Slide 85 text

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) }

Slide 86

Slide 86 text

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} ... }

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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) }

Slide 93

Slide 93 text

ViewModel Λಋೖ

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

No content

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

! ViewModel Λ MainActor Ͱอޢ͢Δ

Slide 100

Slide 100 text

LoginViewModel import Combine @MainActor final class LoginViewModel: ObservableObject { ... func loginButtonPressed() async { ... } }

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { }

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

! Task.init Λ ViewModel Ͱ͸ͳ͘ View ଆʹॻ͘ ※ ViewModel ͷςετ͕ॻ͖΍͘͢ͳΔ ※ ͨͩ͠ɺ ViewModel ͰΩϟϯηϧΛѻ͍ͮΒ͘ͳΔ

Slide 107

Slide 107 text

WWDC2021 10019 h*ps:/ /developer.apple.com/videos/play/wwdc2021/10019/

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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)

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

LoginViewModel.loginButtonPressed // LoginViewModel func loginButtonPressed() async { isLoginButtonEnabled = false defer { isLoginButtonEnabled = true } do { try await AuthService.shared.logIn(...) dismiss.send() } catch { ... } }

Slide 112

Slide 112 text

LoginViewModel.dismiss @MainActor final class LoginViewModel: ObservableObject { ... let dismiss: PassthroughSubject = .init() ... }

Slide 113

Slide 113 text

dismiss ͷ View Ͱͷ࣮ߦ // LoginViewController override func viewDidLoad() { super.viewDidLoad() ... viewModel.dismiss .sink { [weak self] _ in self?.parent?.dismiss(animated: true) } .store(in: &cancellables)

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

LoginViewModel.loginErrorMessage @MainActor final class LoginViewModel: ObservableObject { ... @Published var loginErrorMessage: LoginErrorMessage? ... }

Slide 118

Slide 118 text

LoginErrorMessage enum LoginErrorMessage: Hashable { case login case network case server case system }

Slide 119

Slide 119 text

Swi$UI struct LoginView: View { @StateObject private var viewModel: LoginViewModel = .init() ... }

Slide 120

Slide 120 text

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) ...

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

Swi$UI struct LoginView: View { ... var body: some View { VStack(alignment: .center) { ... } .onReceive(viewModel.dismiss) { _ in dismiss() } ... } }

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

DI ͱςετ

Slide 125

Slide 125 text

LoginViewModel ͷςετ͕ॻ͖ͮΒ͍ func loginButtonPressed() async { isLoginButtonEnabled = false defer { isLoginButtonEnabled = true } do { // αʔόʔʹΞΫηεɺϑΝΠϧ I/O try await AuthService.shared.logIn(...) dismiss.send() } catch { ... } }

Slide 126

Slide 126 text

ςετ͔Βαʔόʔ΍ϑΝΠϧʹΞΫηεͨ͘͠ͳ͍ • ωοτϫʔΫ΍αʔόʔͷঢ়ଶͰ݁Ռ͕ෆఆ • ੒ޭɾࣦഊέʔεͳͲΛ੍ޚͰ͖ͳ͍ • ςετͷ౓ʹσʔλΛॻ͖ࠐΈͨ͘ͳ͍

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

No content

Slide 129

Slide 129 text

No content

Slide 130

Slide 130 text

No content

Slide 131

Slide 131 text

AuthServiceProtocol protocol AuthServiceProtocol { static var shared: Self { get } func logIn( for id: User.ID, with password: String ) async throws ... }

Slide 132

Slide 132 text

AuthService actor AuthService: AuthServiceProtocol { ... }

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

LoginViewModel @MainActor final class LoginViewModel: ObservableObject where AuthService: AuthServiceProtocol { ... }

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

AuthService ͷར༻Օॴͷίʔυ͸มΘΒͳ͍ func loginButtonPressed() async { isLoginButtonEnabled = false defer { isLoginButtonEnabled = true } do { try await AuthService.shared.logIn(...) dismiss.send() } catch { ... } }

Slide 137

Slide 137 text

! DI Λ׆༻ͯ͠ ViewModel Λ UI ΍ωοτϫʔΫ౳͔Β ಠཱͤ͞Δ

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

! async/await Λ࢖͑͹ ඇಉظϝιουͷςετΛ γϯϓϧʹॻ͚Δ ※ Task.init Λ ViewModel ଆʹॻ͔ͳ͔͔ͬͨΒͦ͜

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

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

Slide 159

Slide 159 text

ςετ༻ͷ AuthService private final class AuthService: AuthServiceProtocol { ... var logInContinuation: CheckedContinuation? func logIn(for id: User.ID, with password: String) async throws { try await withCheckedThrowingContinuation { continuation in logInContinuation = continuation } } }

Slide 160

Slide 160 text

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

Slide 161

Slide 161 text

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

Slide 162

Slide 162 text

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

Slide 163

Slide 163 text

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

Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

isLoginButtonEnabled ͷςετ func testLoginButtonEnabled() async { ... await logIn XCTAssertTrue(viewModel.isLoginButtonEnabled) }

Slide 166

Slide 166 text

·ͱΊ • async/await, Sendable, Con'nua'on, Task, actor, MainActor • ViewModel ͷಋೖͱඇಉظॲཧͷςετ

Slide 167

Slide 167 text

͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ