SwiftUI(Combine)、Laravel初心者。完全自分用の学習備忘録として残します。
詳細な解説はコード内に記述してあります。
前回の続きとして、Combineを使用して、ViewModelでAPIリクエスト呼び出し処理とレスポンスハンドリング処理を実装していきます。
今回は、サインイン、サインアップ、アカウント削除の3つの処理を例に実装していきます。(具体的な処理は実装せず、必要最低限の処理のみ実装していますので、必要に応じて補ってください。)
前回の記事 ↓
「SwiftUI(Combine)✖️LaravelでAPI通信②(Combineで統一したインターフェースでAPIクライアントを作成)」
ViewModel(AuthViewModel)を定義
-
AuthViewModel.swift
class AuthViewModel { // httpErrorMsgに値が代入されると、それをView側に通知 // View側でhttpErrorMsgを用いて、例えばエラーアラートを表示させるようにすれば良い @Published var httpErrorMsg: String = "" private let myAppErrorSubject = PassthroughSubject<MyAppError, Never>() private var cancellableBag = Set<AnyCancellable>() // MARK: - Dependencies private let apiService: APIServiceType init(apiService: APIServiceType) { // 任意の処理..... myAppErrorSubject .sink(receiveValue: { [weak self] (error) in guard let self = self else { return } // ローディングフラグのトグル処理やボタンの非活性処理、 // emial, passwordテキストフィールドのリセット処理など // リクエスト成功後に@Publishedを通して、View側にさせる処理(その他の処理でも良い)を記述 // ここでMyAppError型のエラーメッセージを代入 . self.httpErrorMsg = error.errorDescription }) .store(in: &cancellableBag) } } // MARK: - HTTPリクエスト&レスポンスハンドリング extension AuthViewModel { // リクエスト private func handleRequest<T, R>(request: R) -> AnyPublisher<T, Never> where R: CommonHttpRouter, T: Decodable { // APIリクエストを実行 apiService.request(with: request) .receive(on: RunLoop.main) // エラー(MyAppError)が流れてきた場合はキャッチ .catch { [weak self] error -> Empty<Decodable, Never> in guard let self = self else { return Empty(completeImmediately: true) } // MyAppErrorを流す(後にエラーダイアログへの文言表示に使用) self.myAppErrorSubject.send(error) // MyAppErrorが流れてきた場合は、即座にストリームを終了し、不要な後続の処理の実行を防ぐ return Empty(completeImmediately: true) } // 成功時のデータ変換を行う // value → APIServiceクラスにて、レスポンスが指定の型(Model)にデコードされたデータ // T → レスポンスをデコードしたい指定の型(Model) // 戻り値AnyPublisher<T, Never>のため、handleRequest(request:).sink(receiveValue: ((Decodable) -> Void)となり、 // このsinkクロージャ内で受け取る(Decodable)引数に「T」(具体的なモデルの型)を指定する .flatMap { value -> AnyPublisher<T, Never> in // valueが要求された型Tにキャストできるか確認 guard let castedValue = value as? T else { return Empty().eraseToAnyPublisher() } return Just(castedValue).eraseToAnyPublisher() } .eraseToAnyPublisher() } // サインアップ/サインイン private func authenticate(with request: any CommonHttpRouter, authModel: AuthModel) { // リクエスト handleRequest(request: request) .sink(receiveValue: { [weak self] (value: AuthModel) in guard let self = self else { return } // ローディングフラグのトグル処理やボタンの非活性処理、 // emial, passwordテキストフィールドのリセット処理など // リクエスト成功後に@Publishedを通して、View側にさせる処理(その他の処理でも良い)を記述 }) .store(in: &cancellableBag) } // サインアップ private func signUp() { let authModel = AuthModel( name: userName, email: email, password: password, apiToken: "", isDuplicatedEmail: nil ) // サインアップリクエスト組み立て let signUpRequest = SignUpRequest(model: authModel) authenticate(with: signUpRequest, authModel: authModel) } // サインイン private func signIn(trialUserInfo trialAuthModel: AuthModel? = nil) { let authModel = AuthModel( name: "", email: email, password: password, apiToken: "", isDuplicatedEmail: nil ) // サインインリクエスト組み立て let signInRequest = SignInRequest(model: trialAuthModel ?? authModel) authenticate(with: signInRequest, authModel: authModel) } // アカウント削除 private func deleteAccount(email: String, password: String) { let authModel = AuthModel( name: "", email: email, password: password, apiToken: "", isDuplicatedEmail: nil ) // リクエスト組み立て let deleteAccountRequest = DeleteAccountRequest(model: authModel) // リクエスト handleRequest(request: deleteAccountRequest) .sink(receiveValue: { [weak self] (value: EmptyModel) in guard let self = self else { return } // ローディングフラグのトグル処理やボタンの非活性処理、 // emial, passwordテキストフィールドのリセット処理など // リクエスト成功後に@Publishedを通して、View側にさせる処理(その他の処理でも良い)を記述 }) .store(in: &cancellableBag) } }
リクエストする情報を組み立てるクラスを定義していきます。
-
SignUpRequest
import Alamofire struct SignUpRequest: CommonHttpRouter { typealias Response = AuthModel var path: String { return ApiUrl.signUpUrl } var method: HTTPMethod { return .post } func body() throws -> Data? { try JSONEncoder().encode(model) } private let model: AuthModel init(model: AuthModel) { self.model = model } }
-
SignInRequest
import Alamofire struct SignInRequest: CommonHttpRouter { typealias Response = AuthModel var path: String { return ApiUrl.signInUrl } var method: HTTPMethod { return .post } func body() throws -> Data? { try JSONEncoder().encode(model) } private let model: AuthModel init(model: AuthModel) { self.model = model } }
-
DeleteAccountRequest
import Alamofire struct DeleteAccountRequest: CommonHttpRouter { typealias Response = EmptyModel var path: String { return ApiUrl.deleteAccout } var method: HTTPMethod { return .delete } func body() throws -> Data? { try JSONEncoder().encode(model) } private let model: AuthModel init(model: AuthModel) { self.model = model } }