Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
巨大モノリシックアプリ モダン化大作戦
Search
Ryomm
September 12, 2026
Programming
1.2k
1
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
240
なあ兄弟、 余白の意味を考えてから UI実装してくれ!
ktcryomm
11
11k
iOSでQRコード生成奮闘記
ktcryomm
2
370
iOS18でQRコードが表示されなくなった🤷
ktcryomm
0
170
リョムキャットのパーフェクトSwiftネーミング教室
ktcryomm
0
2.5k
Slackを使いこなせ!Slack効率3000倍
ktcryomm
0
1.5k
クソアプリ作って煙突詰めた♪
ktcryomm
0
130
Other Decks in Programming
See All in Programming
世界の中心で、AI(App Intents)をさけぶ ー App Intents中心設計の実践ガイド
touyou
0
740
JAWS-UG 東京支部が始める、JAWS-UG支部コラボ / JAWS-UG lunchtime LT Collaboration
y0hgi
0
170
re:Inventに行く前に知っておきたい現地参加のノウハウ
nokomoro3
0
280
Everything will be SERVERLESS — 信じて運用した10年の経験値 / Everything Will be Serverless — Lessons Learned from 10 Years of Operational Experience
seike460
PRO
1
630
手動確認はもう限界 〜XCUITestでCustom URL Schemeの遷移を起動種別ごとに自動テストする〜 / Testing Custom URL Schemes with XCUITest
otouto
0
360
パズルゲームの作り方 / how to make puzzle games
kaityo256
PRO
2
260
標準パッケージに uuid が追加された 背景から見る Go らしい意思決定 / go_127_uuid_decision
convto
5
9.2k
モバイル交通系ICへのチャージ実例から考える、クロスプラットフォーム開発におけるiOS実機テスト設計とCI運用
yusuga
1
570
Verilogで学ぶCPU自作入門.pdf
uyuki234
7
3.9k
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
370
SREの越境 / SRE Collaboration
y0hgi
2
290
MVNOの申込からeSIM開通までをiOSアプリでつなぐ- 本人確認・MNP・通信事業者基盤をまたぐ実装
satotakeshi
0
510
Featured
See All Featured
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.9k
Marketing Yourself as an Engineer | Alaka | Gurzu
gurzu
0
320
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.6k
Git: the NoSQL Database
bkeepers
PRO
432
67k
Avoiding the “Bad Training, Faster” Trap in the Age of AI
tmiket
0
260
We Have a Design System, Now What?
morganepeng
55
8.3k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
133
20k
4 Signs Your Business is Dying
shpigford
187
23k
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
2
510
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
710
The Organizational Zoo: Understanding Human Behavior Agility Through Metaphoric Constructive Conversations (based on the works of Arthur Shelley, Ph.D)
kimpetersen
PRO
0
480
Prompt Engineering for Job Search
mfonobong
0
470
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