Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
巨大モノリシックアプリ モダン化大作戦
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Ryomm
September 12, 2026
Programming
83
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
巨大モノリシックアプリ モダン化大作戦
2026.09.12 iOSDC Japan 2026
Ryomm
September 12, 2026
More Decks by Ryomm
See All by Ryomm
TipKitTips
ktcryomm
0
230
なあ兄弟、 余白の意味を考えてから UI実装してくれ!
ktcryomm
11
11k
iOSでQRコード生成奮闘記
ktcryomm
2
360
iOS18でQRコードが表示されなくなった🤷
ktcryomm
0
170
リョムキャットのパーフェクトSwiftネーミング教室
ktcryomm
0
2.4k
Slackを使いこなせ!Slack効率3000倍
ktcryomm
0
1.5k
クソアプリ作って煙突詰めた♪
ktcryomm
0
120
Other Decks in Programming
See All in Programming
Press start. Python's next generation.
willingc
PRO
3
320
Snowflakeで業務アプリを作ろう。 Snowflakeのアプリ機能解説&実践ガイド
ayumu_yamaguchi
1
160
AWS Step Functions 大規模並列の壁を越える / jaws-sonic-2026-niigata-step-functions
kasacchiful
PRO
0
400
【デモ】Kiroで体験する仕様駆動開発|設計からコーディングまでAIと進める開発フロー
cmkudo
0
600
Claude Codeを組織的に動かして月400PRを実現した話
happy_ryo
0
260
Kiroで創り、AgentCoreで繋ぐ!AWSで実践する「AI-DLC」から「AIエージェント統合」までの最新地図
licux
3
360
レビュー履歴をAIに食わせて、 Compose移行を加速するs
shihochan
0
270
RSSとCodexを使ってX投稿自動化してみた
ochtum
0
110
[GoCon2026] When Goroutines Are Not Enough: Runtime Locality in High-Throughput Go
takehaya
4
620
LL言語やWebフレームワークのPostgreSQL対応 〜DBの機能がユーザーに届くまで〜
kentaroutakeda
0
140
ソフトウェアラスタライザ
fadis
1
790
GraphRAGのKnowledge Graphを 直接!見る/View-GraphRAG's-KnowledgeGraph-directly!
tyumugi1113
0
250
Featured
See All Featured
The Power of CSS Pseudo Elements
geoffreycrofte
82
6.5k
Groundhog Day: Seeking Process in Gaming for Health
codingconduct
0
340
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
67
58k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
128
56k
What does AI have to do with Human Rights?
axbom
PRO
1
2.4k
It's Worth the Effort
3n
188
29k
Future Trends and Review - Lecture 12 - Web Technologies (1019888BNR)
signer
PRO
0
3.7k
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
410
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
3k
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
The Mindset for Success: Future Career Progression
greggifford
PRO
0
490
How to Align SEO within the Product Triangle To Get Buy-In & Support - #RIMC
aleyda
2
1.8k
Transcript
巨大モノリシックアプリ モダン化大作戦 Ryomm / Ryoko Matsusaka 2026.09.12 iOSDC 2026
Ryomm / Ryoko Matsusaka KINTOテクノロジーズ株式会社 iOS App Developer
None
None
140 画面 2,400 ファイル 234,000 行 巨大モノリシックアプリ
使える状態を維持したまま モダンに整理していく
None
Overview カオスができるまで 心がざわざわしてきたら… #iosdc #b カオス爆誕 カオス収束 エピローグ をつけてXでポスト!
カオス紀元前
初期アーキテクチャ View Storyboard) ViewController View ViewModel Model
Storyboardのメンテ大変すぎる問題 〜 ね か 開 全然 ぎ す 重 ぎ
す く に し 作 操 こいつTa bleViewか 囲 範 差分わか 響 影 らん!! ? ! い な ど よ
StoryboardからSwiftUIへの移行 UIHostingController
StoryboardからSwiftUIへの移行 View A Storyboard) View B SwiftUI ViewController A ViewController
B ナビゲーション( UIKit )
StoryboardからSwiftUIへの移行 View ObservedObject HostingController ViewController View ViewModel
StoryboardからSwiftUIへの移行 View ObservedObject HostingController import SwiftUI struct HogeView: View {
var body: some View { Text(”hoge") ViewController View ViewModel } }
StoryboardからSwiftUIへの移行 class ViewController: UIViewController { let hostingController: UIHostingController<HogeView> View init()
{ let view = HogeView() self.hostingController = UIHostingController(rootView: view) ObservedObject } HostingController super.init(nibName: nil, bundle: nil) override func viewDidLoad() { super.viewDidLoad() addChild(hostingController) hostingController.view.translatesAutoresizingMaskIntoConstraints = false ViewController view.addSubview(hostingController.view) NSLayoutConstraint.activate([ hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), View /" viewを全体に広げる ]) ViewModel } } hostingController.didMove(toParent: self)
StoryboardからSwiftUIへの移行 View ObservedObject class ViewStateModel: ObservableObject { HostingController @Published var
item: Item? @Published var actionEvent: Void = () ViewController View ViewModel }
StoryboardからSwiftUIへの移行 View import SwiftUI ObservedObject struct HogeView: View { @ObservedObject
var model: ViewStateModel HostingController var body: some View { Button(model.title) { model.actionEvent = () ViewController View ViewModel } } }
StoryboardからSwiftUIへの移行 View class HostingController: UIHostingController<HogeView> { private let model: ViewStateModel
init(model: ViewStateModel) { ObservedObject self.model = model super.init(rootView: .init(model: model)) } HostingController var actionPublisher: AnyPublisher<Void, Never> { model.$actionEvent.dropFirst().eraseToAnyPublisher() } ViewController func update(item: Item?) { View model.item = item } ViewModel }
StoryboardからSwiftUIへの移行 View ObservedObject extension ViewController { func bind(hostingController: HostingController) {
hostingController.actionPublisher .receive(on: DispatchQueue.main) HostingController .sink { /" Do something } .store(in: &cancellables) ViewController View ViewModel } }
StoryboardからSwiftUIへの移行 View Storyboard ObservedObject HostingController ViewController ViewController View View ViewModel
ViewModel
StoryboardからSwiftUIへの移行 View Storyboard ObservedObject HostingController 本当に同じに なってるか…? ViewController ViewController View
View ViewModel ViewModel
SnapshotTestを導入する SnapshotTesting
SnapshotTestを導入する https://blog.kinto-technologies.com/posts/20240417SnapshotTest/
SwiftUI化 めでたしめでたし…? 見た目が同じだけで 良いのか…?
一方 その頃────
Domainモデルの導入 View @IBOutlet weak var ViewModel typealias Hoge = label:
UILabel! Response Response.Hoge func setLabel() { class ViewModel { label.text = let hoge: Hoge viewModel.hoge.p1 APIClient struct Response { let hoge: Hoge } struct Hoge { } let p1: String } }
Domainモデルの導入 View @IBOutlet weak var ViewModel typealias Hoge = label:
UILabel! Response Response.Hoge func setLabel() { class ViewModel { label.text = let hoge: Hoge viewModel.hoge.p2 APIClient struct Response { let hoge: Hoge } struct Hoge { } let p1: String let p2: String } }
Domainモデルの導入 View ViewModel Response @IBOutlet weak var typealias Hoge =
struct Response { Response.Hoge let hoge: Hoge label: UILabel! func setLabel() { label.text = viewModel.hoge.p2 } class ViewModel { struct Hoge { let hoge: Hoge let p1: String } let p2: String } } APIClient
Domainモデルの導入 View @IBOutlet weak var Response label: UILabel! ViewModel typealias
Hoge = Response.Hoge struct Response { func setLabel() { let hoge: Hoge label.text = class ViewModel { viewModel.hoge.p2 struct Hoge { } } let p1: String let p2: String } } let hoge: Hoge APIClient
Domainモデルの導入
Domainモデルの導入 DomainModel struct Domain { View ViewModel @IBOutlet weak var
class ViewModel { label: UILabel! func setLabel() { label.text = let p: String APIClient } let d: Domain Response } struct Response { let hoge: Hoge viewModel.d.p } struct Hoge { let p1: String } }
Domainモデルの導入 DomainModel struct Domain { View ViewModel @IBOutlet weak var
class ViewModel { label: UILabel! func setLabel() { label.text = let p: String APIClient } let d: Domain Response } struct Response { let hoge: Hoge viewModel.d.p } struct Hoge { let p1: String let p2: String } }
Domainモデルの導入 View Response View DomainModel
一方 その頃────
APIクライアントの移行 (背景説明割愛)
APIクライアントの移行 static func getHoge(p: String, completion: @escaping (Result<Response, Error>) ->
Void) { let parameters = RequestParameters(p: p) AF.request(path, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: APIBase.apiHeader(), requestModifier: { $0.timeoutInterval = timeoutInterval }) .responseDecodable { (response: AFDataResponse<Response>) in switch response.result { case .success(let item): if let error = ServerResponseDecoder().decodeMaintenanceError(response) { completion(.failure(error)) } else { completion(.success(item)) } case .failure(let error): completion(.failure(CustomError(serverError: .network, error: error, response: response))) } } } func getHoge(p: String) async throws -> Response { let networkService = NetworkServiceImpl() let response = try await networkService.request( PostHogeTarget( parameters: .init(p: p) ) ) return response } completion handler concurrency
APIクライアントの移行 View class ViewModel { let apiService: APIService func transform(_
input: TimetableInput) -> TimetableOutput { let onGetHoge = input.getHoge .flatMap { self.getHoge() } .eraseToAnyPublisher() ObservedObject return .init(onGetHoge: onGetHoge) } HostingController func getHoge() -> Future<Result<Domain, Error>, Never> { .init { promise in Task { do { let domain = try await apiService.getHoge(p: p) promise(.success(.success(domain))) } catch { promise(.success(.failure(error)) } } } } ViewController View ViewModel }
APIクライアントの移行 View class ViewModel { let apiService: APIService func transform(_
input: TimetableInput) -> TimetableOutput { let onGetHoge = input.getHoge .flatMap { self.getHoge() } .eraseToAnyPublisher() ObservedObject return .init(onGetHoge: onGetHoge) } HostingController func getHoge() -> Future<Result<Domain, Error>, Never> { .init { promise in Task { do { let domain = try await apiService.getHoge(p: p) promise(.success(.success(domain))) } catch { promise(.success(.failure(error)) } } } } ViewController View ViewModel }
APIクライアントの移行 View ObservedObject extension ViewController { func bind(hostingController: HostingController) {
let output = viewModel.transform( .init( getHoge: getHogeSubject.eraseToAnyPublisher() ) ) output.onGetHoge .receive(on: DispatchQueue.main) .sink { [weak self, weak hostingController] result in guard let self, let hostingController else { return } switch result { case .success(let hoge): updateView(hoge) case .failure(let error): showError(error: error, resumeHandler: { [weak self] in getHogeSubject.send() }) } } .store(in: &cancellables) HostingController ViewController View } ViewModel }
カオス爆誕前夜 Storyboard UIKit ViewController HostingController ViewModel ObservedObject View SwiftUI APIService
Domain Mapper Response) APIManager Alamofire Apollo Completion Handler Model Singleton) Moya Concurrency
SwiftUI化 めでたしめでたし…? 見た目が同じだけで 良いのか…?
SwiftUI化 めでたしめでたし…? 見た目が同じだけで 良いのか…? 生 発 レ デグ
SwiftUI化 めでたしめでたし…? や ル ク イ サ フ ライ の
作 操 ザ 見た目が同じだけで ユー れ 漏 良いのか…? 装 実 生 発 レ デグ スクロー ル や 分析ツー キーボー ルの ドの 実装漏れ 挙動の違 い
SwiftUI化 めでたしめでたし…? や ル ク イ サ フ ライ の
作 操 ザ 見た目が同じだけで ユー れ 漏 良いのか…? 装 実 生 発 レ デグ スクロー ル や 分析ツー キーボー ルの ドの 実装漏れ 挙動の違 い 化 須 必 A Q
SwiftUI化 めでたしめでたし…? や ル ク イ サ フ ライ の
作 操 ザ 見た目が同じだけで ユー れ 漏 良いのか…? 装 実 生 発 レ デグ スクロー ル や 分析ツー キーボー ルの ドの 実装漏れ 挙動の違 い ! 化 須 必 A Q
リファクタすること
安全にリファクタすること
一方 その頃────
Swift6に移行したい Swift6 にした い!
Swift6に移行したい class ViewModel { @Published var status: LoadingType? var updateStatus:
AnyObject? メインスレッド class ViewController { viewModel.setStatus { [weakを更新 self] status in guard let self, let status else { return } switch status { case .loading: break case .finish: updateView() case .error(let error): showError(error: error) } func setStatus( process: @escaping (_ status: LoadingType?) -> Void ) { updateStatus = $status.sink { status in process(status) } } 非メインスレッド で更新 func callAPI() { status = .loading apiService.callAPI { [weak self] status in guard let self = self else { return } switch status { case .loading: self.guestLoginStatus = .loading case .finish: self.guestLoginStatus = .finish case .error(let error): self.guestLoginStatus = .error(error) } } } } } } ら な 6 t f Swi で 点 時 ビルド ! ず は る け 付 気
Swift6に移行したい https://www.swift.org/migration/documentation/migrationguide/
Swift6に移行したい
Swift6に移行したい 全体に共通の問題 ・構造体がSendableじゃない ・ViewModel↔ViewController間のCombine購読 ・APIクライアントの完了ハンドラが非Sendable ・etc… + Upcoming Features 全部を1度のQAで
動作保証してもらうのは 無理…
Swift6に移行したい 全部QAするなら 凍結中のリファクタも 一緒に進めたい…
2ブランチ作戦
2ブランチ作戦 feature develop cherry-pick modern refactor
2ブランチ作戦 modern develop develop
2ブランチ作戦 feature エグ差分 develop cherry-pick modern refactor
カオス爆誕
2ブランチ作戦 develop modern みえない運用コスト
恐怖!誰も知らない進捗
恐怖!誰も知らない進捗 年内に終わる …ということに 無理じゃね? なっている マ?
恐怖!誰も知らない進捗 は SwiftUI化 面 画 の ど ? み 済
応 対 が 何 Domainモデル導入 いつ終わる見込み? い な ら 知 も 誰 全体で APIクライアント移行 ? た っ わ 終 が 割 何 どの画面は Strict Concurrency Checking対応 何がで きてない?
恐怖!誰も知らない進捗 画面A 画面B SwiftUI化 APIクライアント移行 Domainモデル導入 Strict Concurrency Checking対応 移行単位を定義
画面C
一方 その頃────
Repository導入? Storyboard UIKit ViewController HostingController ViewModel ObservedObject View SwiftUI APIService
Mapper APIManager Alamofire Apollo Completion Handler Model Singleton) Moya Concurrency
Repository導入? Storyboard UIKit ViewController HostingController ViewModel ObservedObject View SwiftUI APIService
Mapper APIManager Alamofire Apollo Completion Handler Model Singleton) Moya Concurrency
Repository導入? 長期的な理想像 + 実現可能な計画
カオス収束に向けて
モダン化大作戦
モダン化大作戦 SwiftUI化 Domainモデル導入 APIクライアント移行 Strict Concurrency Checking対応 アーキテクチャ刷新(整理) モダン化
モダン化大作戦 「妥協の心」が 大事!
モダン化大作戦 - アーキテクチャ刷新 Storyboard UIKit ViewController HostingController ViewModel ObservedObject View
SwiftUI APIService Mapper APIManager Alamofire Apollo Completion Handler Model Singleton) Moya Concurrency
モダン化大作戦 - アーキテクチャ刷新 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya EntityModel
モダン化大作戦 - アーキテクチャ刷新 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel 端末に依存する ロジック UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya import SwiftUI import UIKit EntityModel が必要なロジック
モダン化大作戦 - アーキテクチャ刷新 Storyboard UIKit ViewController HostingController iPhoneでもiPadでも Macでも動くロジック ViewModel
UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… ObservedObject View アプリ内で使う 構造体 SwiftUI DomainModel Moya EntityModel
モダン化大作戦 - アーキテクチャ刷新 Storyboard UIKit ViewController HostingController ObservedObject ViewModel何をどこに Mapper
Repository APIClient DataStore Singleton, Cache, etc… SwiftUI アプリ外で使う 構造体 保存しているかを知る UseCase View DomainModel Moya EntityModel
モダン化大作戦 - アーキテクチャ刷新 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya EntityModel
モダン化大作戦 - 安全な移行 feature ちゃんとQAしてから develop リリースされるように コントロールしたい cherry-pick modern
refactor
モダン化大作戦 - モジュール化 Swift Package Manager → モジュール単位でSwift6ビルドを有効化できる
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject ここだけ View
SwiftUI Swift6ビルド ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya SPM1 EntityModel SPM2
モダン化大作戦 - モジュール化 var package = Package( /* … */
) package.targets.forEach { target in target.swiftSettings = (target.swiftSettings ?& []) + [ .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("MemberImportVisibility"), .enableUpcomingFeature("InferIsolatedConformances"), .enableUpcomingFeature("NonisolatedNonsendingByDefault"), .enableUpcomingFeature("ImmutableWeakCaptures") ] }
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController レ イヤードアーキ テ
ObservedObject SPMの相性良す ViewModel 件について UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… View クチャと SwiftUI ぎる アプリ本体 DomainModel Moya SPM1 EntityModel SPM2
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya SPM1 EntityModel SPM2
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya SPM1 EntityModel SPM2
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya SPM1 EntityModel SPM2
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
リソース ViewModel UseCase Mapper Repository APIClient どうせ全部 メインスレッドで 動くし、そのままで… DataStore Singleton, Cache, etc… Moya アプリ本体 DomainModel SPM1 EntityModel SPM2
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya EntityModel SPM
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya EntityModel SPM
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya EntityModel SPM
モダン化大作戦 - モジュール化 Storyboard ViewController UIKit これをレビュー HostingController ObservedObject View
SwiftUI し続けるのは無理… ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya EntityModel SPM
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI
ViewModel アプリ本体 UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya SPM1 EntityModel SPM2
モダン化大作戦 - モジュール化 var package = Package( name: "DomainLayer", products:
[ .library(name: "UseCase", targets: ["UseCase", "UseCaseImplementation"]), ], ) .library(name: "Domain", targets: ["DomainModule"]) /" ..) var package = Package( name: "DataLayer", products: [ .library(name: "Entity", targets: ["Entity"]), ], ) .library(name: "Repository", targets: ["Repository", "RepositoryImplementation"]) /" ..)
一方 その頃────
モダン化大作戦 - モジュール化 KMPモジュール 入れるわ Android ロボットは、Google が作成および提供している作品から複製または変更したものであり、クリエイティブ・コモンズ表示 3.0 ライセンスに記載された条件に従って使用しています
マ?
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ViewModel UseCase Repository
DataStore ObservedObject プロトコルを 独立したモジュール として定義 Mapper APIClientProtocol APIClientImpl KMPAPIClientImpl View SwiftUI アプリ本体 DomainModel SPM1 Moya EntityModel SPM2
モダン化大作戦 - モジュール化 人がルールを守る
モダン化大作戦 - モジュール化 構造がルールを守らせる
モダン化大作戦 開発 QA 画面A モダン化 画面B モダン化 画面A QA
モダン化大作戦 開発 QA
Tips 1
APIトークンリフレッシュ&リトライ HogeRepository TokenRepository TokenAPIClient Keychain
APIトークンリフレッシュ&リトライ HogeRepository public protocol HogeRepository { } func getHoge() async
throws-+ Hoge TokenRepository TokenAPIClient Keychain public struct HogeRepositoryImpl: HogeRepository { let apiClient: HogeAPIClient let tokenRepository: TokenRepository public func getHoge() async throws -+ Hoge { do { let token = try await tokenRepository.getToken() try await apiClient.getHoge(token: token) } catch where error.serverError =- .tokenInvalid { let token = try await tokenRepository.refreshToken() } } } try await apiClient.getHoge(token: token) 面倒だ…
APIトークンリフレッシュ&リトライ Macro
APIトークンリフレッシュ&リトライ public struct TokenAuthenticatedMacro: BodyMacro { public static func expansion(
of node: AttributeSyntax, providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, in context: some MacroExpansionContext ) throws -+ [CodeBlockItemSyntax] { /" ..) let statements = body.statements.trimmed return [ """ do { let token = try await tokenRepository.getToken() \(statements) } catch where error.serverError =- .tokenInvalid { let token = try await tokenRepository.refreshToken() } } } ] \(statements) """
APIトークンリフレッシュ&リトライ public protocol HogeRepository { } func getHoge() async throws-+
Hoge public struct HogeRepositoryImpl: HogeRepository { let apiClient: HogeAPIClient let tokenRepository: TokenRepository @TokenAuthenticated public func getHoge() async throws -+ Hoge { } } try await apiClient.getHoge(token: token)
Tips 2
UseCaseパターンがSwiftっぽくない Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI ViewModel UseCase
Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel Moya EntityModel
UseCaseパターンがSwiftっぽくない public struct HogeUseCase { private let hogeRepository: any HogeRepository
public func getHoge() async throws -+ Domain.Hoge { let response = try await hogeRepository.getHoge() /" ..) } return HogeMapper.map(response) public func register(hoge: Domain.Hoge) async throws { /" ..) } } try await hogeRepository.register(hoge: hoge) 本来UseCaseは ビジネスロジック単位
UseCaseパターンがSwiftっぽくない public struct GetHogeUseCase { private let hogeRepository: any HogeRepository
public struct HogeUseCase { public func execute() async throws -+ Domain.Hoge { private let hogeRepository: any HogeRepository let response = try await hogeRepository.getHoge() /" ..) public func getHoge() async throws -+ Domain.Hoge { let response = try await hogeRepository.getHoge() /" ..) } return HogeMapper.map(response) public func register(hoge: Domain.Hoge) async throws { /" ..) } } } } return HogeMapper.map(response) public struct RegisterHogeUseCase { private let hogeRepository: any HogeRepository try await hogeRepository.register(hoge: hoge) public func execute(hoge: Domain.Hoge) async throws { /" ..) } } try await hogeRepository.register(hoge: hoge)
UseCaseパターンがSwiftっぽくない user.change(phoneNumber: “080-0000-0000”) changeUserPhoneNumberUseCase.execute(“080-0000-0000”) https://www.swift.org/documentation/api-design-guidelines/
UseCaseパターンがSwiftっぽくない callAsFunction
UseCaseパターンがSwiftっぽくない public protocol ChangePhoneNumberUseCase { func callAsFunction(_ phoneNumber: String) async
throws } public struct ChangePhoneNumberUseCaseImpl: ChangePhoneNumberUseCase { private let phoneNumberRepository: any PhoneNumberRepository public func callAsFunction(to newPhoneNumber: String) async throws { try await phoneNumberRepository.update(phoneNumber: newPhoneNumber) } } let changePhoneNumber: ChangePhoneNumberUseCase = ChangePhoneNumberUseCaseImpl() /" ..) try await changePhoneNumber(to: “080-0000-0000“)
UseCaseパターンがSwiftっぽくない public protocol ChangePhoneNumberUseCase { func callAsFunction(_ phoneNumber: String) async
throws } /" -+ changePhoneNumber(“080-0000-0000”) public protocol ChangePhoneNumberUseCase { func execute(_ phoneNumber: String) async throws } /" -+ changePhoneNumberUseCase.execute(“080-0000-0000”)
UseCaseパターンがSwiftっぽくない public protocol ChangePhoneNumberUseCase { func callAsFunction(_ phoneNumber: String) async
throws } /" -+ changePhoneNumber(“080-0000-0000”) 違法建築 しやすい public protocol ChangePhoneNumberUseCase { func execute(_ phoneNumber: String) async throws func auth() async throws } /" -+ changePhoneNumberUseCase.execute(“080-0000-0000”)
Tips 3
テストを維持したい public struct HogeUseCase: HogeUseCase { private let repository: any
Repository public init(fuga: String) { self.init( repository: RepositoryImpl(fuga: fuga) ) } init(repository: any Repository) { self.repository = repository } public func getHoge(key: String) async throws -+ Hoge { /" hoge } }
テストを維持したい 本体 テストA SPM1 テストB SPM2 テストC
テストを維持したい 本体 テストA SPM1 テストB SPM2 テストC
テストを維持したい TestPlan
テストを維持したい
まとめ
モダン化作戦で得た大規模リファクタのコツ ・影響範囲を限定し、コントロール可能にする ソフトスキルの 見せ所だぜ! ・理想像の共有 ・「妥協の心」を持つ ! な る れ
忘 を 目的
エピローグ
未解決問題 1
Observationへの移行 Observation https://developer.apple.com/documentation/swiftui/ migrating-from-the-observable-object-protocol-to-the-observable-macro
Observationへの移行 /" BEFORE final class ViewStateModel: ObservableObject { @Published var
hoge: Hoge? /" AFTER @Observable final class ViewModel { var hoge: Hoge? @Published var submitEvent: String? } func submit(p: String) {} } struct View: View { } struct View: View { @ObservedObject var model: ViewStateModel var viewModel: ViewModel /" ..) /" ..) }
Observationへの移行 View ObservedObject HostingController ViewController View Observable ViewModel
Observationへの移行 View View ObservedObject HostingController ViewController ViewController View View ViewModel
Observable ViewModel
Observationへの移行 enum ActionEvent { actionTask = Task { [weak self]
in case presentFuga guard let self else { break } case dismiss } @Observable final class HogeViewModel { private let stream: AsyncStream<ActionEvent> for await action in viewModel.action { switch action { case .presentFuga: presentFugaVC() case .dismiss: CombineもAsyncStreamも dismiss(animated: true) private let continuation: AsyncStream<ActionEvent>.Continuation nonisolated func makeAsyncIterator() -+ AsyncStream<ActionEvent>.Iterator { } stream.makeAsyncIterator() func dismiss() { } continuation.yield(.dismiss) func submit() { } } continuation.yield(.presentFuga) コードが辿りにくい } んだよなぁ } }
未解決問題 2
エラーの構造化 CustomError
エラーの構造化 Storyboard UIKit ViewController HostingController ObservedObject View CustomError ViewModel UseCase
Mapper Repository APIClient DataStore Singleton, Cache, etc… SwiftUI アプリ本体 DomainModel SPM1 CustomError Moya EntityModel CustomError SPM2
エラーの構造化 Storyboard UIKit ViewController HostingController View ObservedObject SwiftUI ViewModel アプリ本体
UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… 出所がわかる エラー Moya DomainModel SPM1 EntityModel NetworkError KeychainError SPM2
エラーの構造化 Storyboard UIKit ViewController HostingController View ObservedObject SwiftUI その後の振る舞いが わかるエラー
ViewModel UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… アプリ本体 DomainModel SPM1 LogoutError TicketError Moya EntityModel NetworkError KeychainError SPM2
エラーの構造化 Storyboard UIKit ViewController HostingController ObservedObject View SwiftUI ViewModel アプリ本体
UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… DomainModel SPM1 LogoutError TicketError Moya EntityModel NetworkError KeychainError SPM2
未解決問題 3
分析ログの構造化 Storyboard UIKit ログ送信… ViewController HostingController ViewModel ログ送信… ObservedObject View
APIService Domain Mapper ログ送信… Response) APIManager Alamofire Apollo Model Singleton) ログ送信… SwiftUI Completion Handler ログ送信… Moya Concurrency
分析ログの構造化 Storyboard ViewController UIKit 分析ログの送信って HostingController ObservedObject ビジネスロジック? View SwiftUI
ViewModel UseCase Repository アプリ本体 ログ送信… Mapper APIClient DataStore Singleton, Cache, etc… DomainModel Moya SPM1 EntityModel SPM2
分析ログの構造化 「削除」のログを送る場合 ・「スワイプして削除」「削除ボタンが押された」ことを知りたい? → UI層 ・「削除」という操作がされたことを知りたい? → Domain層
分析ログの構造化 操作を 知りたい Storyboard ViewController HostingController ObservedObject View ユーザ行動を 知りたい
アプリ本体 ViewModel UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… データ状況を DomainModel SPM1 知りたい ログ送信… Moya EntityModel SPM2 Shared Package
None