From d1eb3961039a09227c2488678c5053c169414715 Mon Sep 17 00:00:00 2001 From: kenji Date: Sun, 7 Sep 2025 10:51:12 +0900 Subject: [PATCH 1/3] =?UTF-8?q?UseCase=E3=81=AEForIos=E9=96=A2=E6=95=B0?= =?UTF-8?q?=E3=81=AE=E4=BD=9C=E3=82=8A=E3=81=A0=E3=81=A8=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=81=8C=E4=BC=9D=E6=90=AC=E3=81=A7=E3=81=8D=E3=81=9A?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB=E3=81=AA=E3=81=A3=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F=E3=81=AE=E3=81=A7=E5=85=83=E3=81=AE1?= =?UTF-8?q?=E3=81=A4=E3=81=AE=E9=96=A2=E6=95=B0=E3=81=AB=E7=B5=B1=E5=90=88?= =?UTF-8?q?=E3=81=97=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92=E3=83=8F=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iosApp/iosApp/Sources/util/Extension.swift | 52 +++++++++++++++ .../hotdrop/considercline/model/AppError.kt | 15 ----- .../considercline/usecase/HistoryUseCase.kt | 21 +++--- .../considercline/usecase/PointUseCase.kt | 65 ++++++++----------- .../considercline/usecase/UserUseCase.kt | 39 +++++------ 5 files changed, 104 insertions(+), 88 deletions(-) create mode 100644 iosApp/iosApp/Sources/util/Extension.swift diff --git a/iosApp/iosApp/Sources/util/Extension.swift b/iosApp/iosApp/Sources/util/Extension.swift new file mode 100644 index 0000000..95090dc --- /dev/null +++ b/iosApp/iosApp/Sources/util/Extension.swift @@ -0,0 +1,52 @@ +// +// Extension.swift +// iosApp +// +import Foundation +import shared + +// Sealed Classが使いづらいのでAppResultをEnumにする +enum AppResultEx { + case success(data: Any) + case error(error: AppError) +} + +func asEnumResult(result: AppResult) -> AppResultEx { + if let success = result as? AppResultSuccess { + return .success(data: success.data as Any) + } + if let failure = result as? AppResultError { + return .error(error: failure.error) + } + fatalError(#function + ": 想定外のAppResultです") +} + +// Sealed Classが使いづらいのでAppCompleteをEnumにする +enum AppCompleteEx { + case complete + case error(error: AppError) +} + +func asEnumComplete(complete: AppComplete) -> AppCompleteEx { + if complete is AppComplete.Complete { + return .complete + } + if let failure = complete as? AppComplete.Error { + return .error(error: failure.error) + } + fatalError(#function + ": 想定外のAppCompleteです") +} + +// KMPのBooleanをSwiftのBoolに変換する拡張関数(オプショナル型) +extension Optional where Wrapped == shared.KotlinBoolean { + var asBool: Bool { + return self?.boolValue ?? false + } +} + +// KMPのBooleanをSwiftのBoolに変換する拡張関数 +extension shared.KotlinBoolean { + var asBool: Bool { + return self.boolValue + } +} diff --git a/shared/src/commonMain/kotlin/jp/hotdrop/considercline/model/AppError.kt b/shared/src/commonMain/kotlin/jp/hotdrop/considercline/model/AppError.kt index a1d031c..9f1c0ad 100644 --- a/shared/src/commonMain/kotlin/jp/hotdrop/considercline/model/AppError.kt +++ b/shared/src/commonMain/kotlin/jp/hotdrop/considercline/model/AppError.kt @@ -1,7 +1,5 @@ package jp.hotdrop.considercline.model -import kotlin.coroutines.cancellation.CancellationException - sealed class AppError { data class NetworkError(val error: Throwable): AppError() data class UnknownError(val error: Throwable): AppError() @@ -15,17 +13,4 @@ sealed class AppError { } } -fun mapToDomain(appError: AppError): Throwable = - when (appError) { - is AppError.NetworkError -> appError.error - is AppError.UnknownError -> appError.error - is AppError.ProgramError -> ProgramException(appError.message) - } - -fun mapToThrowable(t: Throwable): AppError = - when (t) { - is CancellationException -> throw t - else -> AppError.UnknownError(t) - } - data class ProgramException(val errorMessage: String): Exception(errorMessage) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/HistoryUseCase.kt b/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/HistoryUseCase.kt index d789340..07d96e8 100644 --- a/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/HistoryUseCase.kt +++ b/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/HistoryUseCase.kt @@ -1,25 +1,22 @@ package jp.hotdrop.considercline.usecase +import jp.hotdrop.considercline.model.AppError import jp.hotdrop.considercline.model.AppResult import jp.hotdrop.considercline.model.PointHistory -import jp.hotdrop.considercline.model.mapToDomain import jp.hotdrop.considercline.repository.HistoryRepository +import kotlin.coroutines.cancellation.CancellationException class HistoryUseCase( private val repository: HistoryRepository ) { suspend fun findAll(): AppResult> { - return fetchHistories() - } - - suspend fun findAllForIos(): List { - return when(val result = fetchHistories()) { - is AppResult.Success -> result.data - is AppResult.Error -> throw mapToDomain(result.error) + return runCatching { + repository.findAll() + }.getOrElse { throwable -> + if (throwable is CancellationException) { + throw throwable + } + AppResult.Error(AppError.UnknownError(throwable)) } } - - private suspend fun fetchHistories(): AppResult> { - return repository.findAll() - } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/PointUseCase.kt b/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/PointUseCase.kt index 4efca41..c38febe 100644 --- a/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/PointUseCase.kt +++ b/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/PointUseCase.kt @@ -1,63 +1,52 @@ package jp.hotdrop.considercline.usecase import jp.hotdrop.considercline.model.AppComplete +import jp.hotdrop.considercline.model.AppError import jp.hotdrop.considercline.model.AppResult import jp.hotdrop.considercline.model.Point import jp.hotdrop.considercline.model.flatMap -import jp.hotdrop.considercline.model.mapToDomain import jp.hotdrop.considercline.repository.HistoryRepository import jp.hotdrop.considercline.repository.PointRepository +import kotlin.coroutines.cancellation.CancellationException class PointUseCase( private val pointRepository: PointRepository, private val historyRepository: HistoryRepository ) { suspend fun find(): AppResult { - return fetchPoint() - } - - suspend fun findForIos(): Point { - return when(val result = fetchPoint()) { - is AppResult.Success -> result.data - is AppResult.Error -> throw mapToDomain(result.error) + return runCatching { + pointRepository.find() + }.getOrElse { throwable -> + if (throwable is CancellationException) { + throw throwable + } + AppResult.Error(AppError.UnknownError(throwable)) } } - private suspend fun fetchPoint(): AppResult { - return pointRepository.find() - } - suspend fun acquire(inputPoint: Int): AppComplete { - return acquireToRepo(inputPoint) - } - - suspend fun acquireForIos(inputPoint: Int) { - return when(val result = acquireToRepo(inputPoint)) { - is AppComplete.Complete -> Unit - is AppComplete.Error -> throw mapToDomain(result.error) - } - } - - private suspend fun acquireToRepo(inputPoint: Int): AppComplete { - return pointRepository.acquire(inputPoint).flatMap { - historyRepository.saveAcquire(inputPoint) + return runCatching { + pointRepository.acquire(inputPoint).flatMap { + historyRepository.saveAcquire(inputPoint) + } + }.getOrElse { throwable -> + if (throwable is CancellationException) { + throw throwable + } + AppComplete.Error(AppError.UnknownError(throwable)) } } suspend fun use(inputPoint: Int): AppComplete { - return useToRepo(inputPoint) - } - - suspend fun useForIos(inputPoint: Int) { - return when(val result = useToRepo(inputPoint)) { - is AppComplete.Complete -> Unit - is AppComplete.Error -> throw mapToDomain(result.error) - } - } - - private suspend fun useToRepo(inputPoint: Int): AppComplete { - return pointRepository.use(inputPoint).flatMap { - historyRepository.saveUse(inputPoint) + return runCatching { + pointRepository.use(inputPoint).flatMap { + historyRepository.saveUse(inputPoint) + } + }.getOrElse { throwable -> + if (throwable is CancellationException) { + throw throwable + } + AppComplete.Error(AppError.UnknownError(throwable)) } } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/UserUseCase.kt b/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/UserUseCase.kt index f0b0772..78876f7 100644 --- a/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/UserUseCase.kt +++ b/shared/src/commonMain/kotlin/jp/hotdrop/considercline/usecase/UserUseCase.kt @@ -1,41 +1,34 @@ package jp.hotdrop.considercline.usecase import jp.hotdrop.considercline.model.AppComplete +import jp.hotdrop.considercline.model.AppError import jp.hotdrop.considercline.model.AppResult import jp.hotdrop.considercline.model.User -import jp.hotdrop.considercline.model.mapToDomain import jp.hotdrop.considercline.repository.UserRepository +import kotlin.coroutines.cancellation.CancellationException class UserUseCase( private val userRepository: UserRepository ) { suspend fun find(): AppResult { - return fetchUser() - } - - suspend fun findForIos(): User { - return when (val result = fetchUser()) { - is AppResult.Success -> result.data - is AppResult.Error -> throw mapToDomain(result.error) + return runCatching { + userRepository.find() + }.getOrElse { throwable -> + if (throwable is CancellationException) { + throw throwable + } + AppResult.Error(AppError.UnknownError(throwable)) } } - private suspend fun fetchUser(): AppResult { - return userRepository.find() - } - suspend fun registerUser(nickname: String?, email: String?): AppComplete { - return saveRegisterUser(nickname, email) - } - - suspend fun registerUserForIos(nickname: String?, email: String?) { - return when (val result = saveRegisterUser(nickname, email)) { - is AppComplete.Complete -> Unit - is AppComplete.Error -> throw mapToDomain(result.error) + return runCatching { + userRepository.registerUser(nickname, email) + }.getOrElse { throwable -> + if (throwable is CancellationException) { + throw throwable + } + AppComplete.Error(AppError.UnknownError(throwable)) } } - - private suspend fun saveRegisterUser(nickname: String?, email: String?): AppComplete { - return userRepository.registerUser(nickname, email) - } } \ No newline at end of file From d3514c97d1299ee25eca9190931552f018e49153 Mon Sep 17 00:00:00 2001 From: kenji Date: Sun, 7 Sep 2025 11:05:36 +0900 Subject: [PATCH 2/3] =?UTF-8?q?UseCase=E3=81=AE=E5=A4=89=E6=9B=B4=E3=81=AB?= =?UTF-8?q?=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6iOS=E3=81=AEViewModel?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/ui/home/HomeViewModel.swift | 58 +++++++++++-------- .../ui/pointget/PointGetViewModel.swift | 21 ++++--- .../Sources/ui/slpash/SplashViewModel.swift | 17 ++++-- .../Sources/ui/start/StartViewModel.swift | 10 +++- 4 files changed, 69 insertions(+), 37 deletions(-) diff --git a/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift b/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift index 4022b3a..8bf2d9c 100644 --- a/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift +++ b/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift @@ -25,34 +25,46 @@ class HomeViewModel: ObservableObject { viewState = .initialLoading do { - async let userDefered = userUseCase.findForIos() - async let pointDefered = pointUseCase.findForIos() - async let historyDefered = historyUseCase.findAllForIos() + async let userDefered = userUseCase.find() + async let pointDefered = pointUseCase.find() + async let historyDefered = historyUseCase.findAll() let (userResult, pointResult, historiesResult) = try await (userDefered, pointDefered, historyDefered) - viewState = .loaded( - user: userResult, - point: Int(pointResult.balance) - ) - historyState = .loaded(histories: historiesResult) + + var user: User? + + switch asEnumResult(result: userResult) { + case .success(let data): + if let userResult = data as? User { + user = userResult + } + case .error(let error): + viewState = .error(message: error.message) + } + + switch asEnumResult(result: pointResult) { + case .success(let data): + if let pointResult = data as? Point { + viewState = .loaded( + user: user!, + point: Int(pointResult.balance) + ) + } + case .error(let error): + viewState = .error(message: error.message) + } + + switch asEnumResult(result: historiesResult) { + case .success(let data): + if let historiesResult = data as? [PointHistory] { + historyState = .loaded(histories: historiesResult) + } + case .error(let error): + viewState = .error(message: error.message) + } } catch is CancellationError { // no operation(not change UI) return - } catch let e as AppError { - switch (e) { - case is AppError.NetworkError: - viewState = .error(message: e.message) - break - case is AppError.ProgramError: - viewState = .error(message: e.message) - break - case is AppError.UnknownError: - viewState = .error(message: e.message) - break - default: - viewState = .error(message: e.message) - break - } } catch { viewState = .error(message: "Unknown Error") } diff --git a/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift b/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift index e87c563..fac48fd 100644 --- a/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift +++ b/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift @@ -17,13 +17,20 @@ class PointGetViewModel: ObservableObject { @MainActor func load() async { do { - let point = try await pointUseCase.findForIos() - viewState = .success( - currentPoint: point, - inputPoint: 0, - errorMessage: nil, - isEnableConfirm: false - ) + let result = try await pointUseCase.find() + switch asEnumResult(result: result) { + case .success(let data): + if let point = data as? Point { + viewState = .success( + currentPoint: point, + inputPoint: 0, + errorMessage: nil, + isEnableConfirm: false + ) + } + case .error(error: let error): + viewState = .error(message: error.message) + } } catch { viewState = .error(message: "ポイント残高の取得に失敗しました。") } diff --git a/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift b/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift index 76b83ed..44d5675 100644 --- a/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift +++ b/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift @@ -15,11 +15,18 @@ class SplashViewModel: ObservableObject { func load() async { viewState = .loading do { - let user = try await userUseCase.findForIos() - if user.isInitialized(), let userId = user.userId { - self.viewState = .loaded(userId: userId) - } else { - self.viewState = .firstTime + let result = try await userUseCase.find() + switch asEnumResult(result: result) { + case .success(data: let data): + if let user = data as? User { + if user.isInitialized(), let userId = user.userId { + self.viewState = .loaded(userId: userId) + } else { + self.viewState = .firstTime + } + } + case .error(error: let error): + self.viewState = .error(message: error.message) } } catch is CancellationError { // no operation(not change UI) diff --git a/iosApp/iosApp/Sources/ui/start/StartViewModel.swift b/iosApp/iosApp/Sources/ui/start/StartViewModel.swift index 3673715..1c08bd3 100644 --- a/iosApp/iosApp/Sources/ui/start/StartViewModel.swift +++ b/iosApp/iosApp/Sources/ui/start/StartViewModel.swift @@ -19,8 +19,14 @@ class StartViewModel: ObservableObject { viewState = .loading do { - try await userUseCase.registerUserForIos(nickname: nickname, email: email) - viewState = .success + let result = try await userUseCase.registerUser(nickname: nickname, email: email) + switch asEnumComplete(complete: result) { + case .complete: + viewState = .success + case .error(let error): + viewState = .idle + errorAlertItem = StartErrorAlertItem(message: error.message) + } } catch is CancellationError { // no operation(not change UI) return From 0d65a96a260f0b6eb48eb5aec48fd8aa2b8ea102 Mon Sep 17 00:00:00 2001 From: kenji Date: Sun, 7 Sep 2025 11:27:14 +0900 Subject: [PATCH 3/3] =?UTF-8?q?iOS=E3=81=A7AppResult=E5=A4=89=E6=8F=9B?= =?UTF-8?q?=E6=99=82=E3=81=AB=E3=82=AD=E3=83=A3=E3=82=B9=E3=83=88=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=81=AE=E3=81=8C=E9=9D=A2=E5=80=92=E3=81=A0=E3=81=A3?= =?UTF-8?q?=E3=81=9F=E3=81=AE=E3=81=A7=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/ui/home/HomeViewModel.swift | 20 +++++++++---------- .../ui/pointget/PointGetViewModel.swift | 17 +++++++--------- .../Sources/ui/slpash/SplashViewModel.swift | 12 +++++------ iosApp/iosApp/Sources/util/Extension.swift | 12 +++++++---- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift b/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift index 8bf2d9c..54cdf2b 100644 --- a/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift +++ b/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift @@ -34,28 +34,26 @@ class HomeViewModel: ObservableObject { var user: User? switch asEnumResult(result: userResult) { - case .success(let data): - if let userResult = data as? User { - user = userResult - } + case .success(let userResult): + user = userResult case .error(let error): viewState = .error(message: error.message) } switch asEnumResult(result: pointResult) { - case .success(let data): - if let pointResult = data as? Point { - viewState = .loaded( - user: user!, - point: Int(pointResult.balance) - ) - } + case .success(let pointResult): + viewState = .loaded( + user: user!, + point: Int(pointResult.balance) + ) case .error(let error): viewState = .error(message: error.message) } switch asEnumResult(result: historiesResult) { case .success(let data): + // Kotlin/Native の List は Swift 側では [T] ではない場合があります。 + // そのため、ここは既存動作を保ちつつ安全にキャストを試みます。 if let historiesResult = data as? [PointHistory] { historyState = .loaded(histories: historiesResult) } diff --git a/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift b/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift index fac48fd..9a50d2e 100644 --- a/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift +++ b/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift @@ -19,15 +19,13 @@ class PointGetViewModel: ObservableObject { do { let result = try await pointUseCase.find() switch asEnumResult(result: result) { - case .success(let data): - if let point = data as? Point { - viewState = .success( - currentPoint: point, - inputPoint: 0, - errorMessage: nil, - isEnableConfirm: false - ) - } + case .success(let point): + viewState = .success( + currentPoint: point, + inputPoint: 0, + errorMessage: nil, + isEnableConfirm: false + ) case .error(error: let error): viewState = .error(message: error.message) } @@ -87,4 +85,3 @@ enum PointAcquireEventState: Equatable { case success case error(message: String) } - diff --git a/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift b/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift index 44d5675..35d4c36 100644 --- a/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift +++ b/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift @@ -17,13 +17,11 @@ class SplashViewModel: ObservableObject { do { let result = try await userUseCase.find() switch asEnumResult(result: result) { - case .success(data: let data): - if let user = data as? User { - if user.isInitialized(), let userId = user.userId { - self.viewState = .loaded(userId: userId) - } else { - self.viewState = .firstTime - } + case .success(data: let user): + if user.isInitialized(), let userId = user.userId { + self.viewState = .loaded(userId: userId) + } else { + self.viewState = .firstTime } case .error(error: let error): self.viewState = .error(message: error.message) diff --git a/iosApp/iosApp/Sources/util/Extension.swift b/iosApp/iosApp/Sources/util/Extension.swift index 95090dc..4487979 100644 --- a/iosApp/iosApp/Sources/util/Extension.swift +++ b/iosApp/iosApp/Sources/util/Extension.swift @@ -6,14 +6,18 @@ import Foundation import shared // Sealed Classが使いづらいのでAppResultをEnumにする -enum AppResultEx { - case success(data: Any) +enum AppResultEx { + case success(data: T) case error(error: AppError) } -func asEnumResult(result: AppResult) -> AppResultEx { +func asEnumResult(result: AppResult) -> AppResultEx { if let success = result as? AppResultSuccess { - return .success(data: success.data as Any) + // Kotlin/Native ではジェネリクス T が Swift 側で T? として表現されることがあるため明示的にアンラップ + if let data = success.data { + return .success(data: data) + } + fatalError(#function + ": Success の data が nil です(想定外)") } if let failure = result as? AppResultError { return .error(error: failure.error)