Share
Embed
Transcript
143 slides
-
Storyboardのメンテ大変すぎる問題 〜 ね か 開 全然 ぎ す 重 ぎ
す く に し 作 操 こいつTa bleViewか 囲 範 差分わか 響 影 らん!! ? ! い な ど よ -
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 } } -
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モデルの導入 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 } } -
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化 めでたしめでたし…? や ル ク イ サ フ ライ の
作 操 ザ 見た目が同じだけで ユー れ 漏 良いのか…? 装 実 生 発 レ デグ スクロー ル や 分析ツー キーボー ルの ドの 実装漏れ 挙動の違 い 化 須 必 A Q -
SwiftUI化 めでたしめでたし…? や ル ク イ サ フ ライ の
作 操 ザ 見た目が同じだけで ユー れ 漏 良いのか…? 装 実 生 発 レ デグ スクロー ル や 分析ツー キーボー ルの ドの 実装漏れ 挙動の違 い ! 化 須 必 A Q -
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 で 点 時 ビルド ! ず は る け 付 気 -
恐怖!誰も知らない進捗 は SwiftUI化 面 画 の ど ? み 済
応 対 が 何 Domainモデル導入 いつ終わる見込み? い な ら 知 も 誰 全体で APIクライアント移行 ? た っ わ 終 が 割 何 どの画面は Strict Concurrency Checking対応 何がで きてない? -
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 -
モダン化大作戦 - アーキテクチャ刷新 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 -
モダン化大作戦 - モジュール化 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"]) /" ..) -
モダン化大作戦 - モジュール化 Storyboard UIKit ViewController HostingController ViewModel UseCase Repository
DataStore ObservedObject プロトコルを 独立したモジュール として定義 Mapper APIClientProtocol APIClientImpl KMPAPIClientImpl View SwiftUI アプリ本体 DomainModel SPM1 Moya EntityModel SPM2 -
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トークンリフレッシュ&リトライ 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) -
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っぽくない 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”) -
テストを維持したい 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 } } -
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への移行 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) コードが辿りにくい } んだよなぁ } } -
エラーの構造化 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 -
分析ログの構造化 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 -
分析ログの構造化 操作を 知りたい Storyboard ViewController HostingController ObservedObject View ユーザ行動を 知りたい
アプリ本体 ViewModel UseCase Mapper Repository APIClient DataStore Singleton, Cache, etc… データ状況を DomainModel SPM1 知りたい ログ送信… Moya EntityModel SPM2 Shared Package