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

Swift Zoomin' #9 報告会

とち🐹
February 12, 2022

Swift Zoomin' #9 報告会

https://swift-tweets.connpass.com/event/237293/
第2回リファクタリングチャレンジの報告会で発表したスライドです。

実装したコードはこちら。
https://github.com/tochi86/login-challenge

Android公式ドキュメントの「アプリ アーキテクチャ ガイド」を、ひたすらオススメする内容となっています。
https://developer.android.com/jetpack/guide?hl=ja

とち🐹

February 12, 2022
Tweet

More Decks by とち🐹

Other Decks in Programming

Transcript

  1. 自己紹介 @tochi86_ iOS / Android / Flutter アプリエンジニアをやっています 個人開発アプリ「アイテムリスト for

    あつ森」を各ストアで公開中 ドメイン駆動設計とクリーンアーキテクチャが大好きな設計オタク ソフトウェア設計を考えるときに重視しているポイント3 選 機能を変更しやすい 不具合を修正しやすい テストを追加しやすい 2
  2. UI レイヤ UI 状態(UiState ) UI を構築する元となるデータ群 状態ホルダー(ViewModel ) UI

    状態を保持して、更新する 入力はイベント、出力は状態 (単方向データフロー:UDF ) UI 要素(ViewController ) データの表現方法を決める イベントを状態ホルダーに伝える 6
  3. Repository の実装 モックライブラリにはMockolo を使用 protocol に @mockable を付与すると、モッククラスを自動生成 /// @mockable

    protocol AuthRepository: AnyObject { func login(id: String, password: String) async throws } final class AuthRepositoryImpl: AuthRepository { func login(id: String, password: String) async throws { try await AuthService.logInWith(id: id, password: password) } } 7
  4. UiState の実装 不変性が求められるので、Swift では struct で定義する struct LoginUiState: Equatable {

    var id: String = "" var password: String = "" var isLoading: Bool = false var showHomeView: Bool = false var showErrorAlert: ErrorAlert? var isLoginButtonEnabled: Bool { return !isLoading && !id.isEmpty && !password.isEmpty } } 8
  5. ViewModel の実装 final class LoginViewModel: ObservableObject { @Published private(set) var

    state: LoginUiState private let authRepository: AuthRepository func onLoginButtonDidTap() async { do { state.isLoading = true try await authRepository.login(id: state.id, password: state.password) state.isLoading = false state.showHomeView = true } catch { state.isLoading = false state.showErrorAlert = ErrorAlert(error: error) } } } 9
  6. ViewController の実装 final class LoginViewController: UIViewController { private let viewModel

    = LoginViewModel() private var cancellables = Set<AnyCancellable>() @IBOutlet private var loginButton: UIButton! override func viewDidLoad() { super.viewDidLoad() viewModel.$state.map(\.isLoginButtonEnabled) .removeDuplicates() .receive(on: DispatchQueue.main) .assign(to: \.isEnabled, on: loginButton) .store(in: &cancellables) } } 10
  7. ViewController の実装 viewModel.$state.map(\.showErrorAlert) .removeDuplicates() .receive(on: DispatchQueue.main) .compactMap { $0 }

    .sink { [weak self] errorAlert in guard let self = self else { return } let alertController: UIAlertController = .init( title: errorAlert.title, message: errorAlert.message, preferredStyle: .alert ) alertController.addAction(.init(title: errorAlert.buttonTitle, style: .default, handler: nil)) self.present(alertController, animated: true) self.viewModel.onErrorAlertDidShow() } .store(in: &cancellables) 11
  8. ViewController の実装 @IBAction private func loginButtonPressed(_ sender: UIButton) { Task

    { await viewModel.onLoginButtonDidTap() } } 詳細な実装は、GitHub のコードをご覧ください ホーム画面はSwiftUI で実装しています UIKit と全く同じアーキテクチャを適用できました UiState およびViewModel のテストも頑張って書きました こちらも合わせて読んでいただけると嬉しいです 12
  9. まとめ 今回は、Android 公式のアーキテクチャガイドの素晴らしさを、 ぜひ皆さんにも知っていただきたくて発表しました Android アプリだけでなく、iOS やFlutter も含め、モバイルアプリ の開発なら汎用的に採用できるアーキテクチャです 以下の理由から、今後皆さんがアーキテクチャを考える際のベース

    として、活用していくことをオススメします! iOS やFlutter には、公式が推奨するアーキテクチャがない 比較的低い学習コストで、堅牢なアプリを構築できる RxSwift / Combine / Swift Concurrency / SwiftUI などの 採用する技術によらず、統一的な考え方で実装できる 13