diff --git a/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift b/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift index 4022b3a..54cdf2b 100644 --- a/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift +++ b/iosApp/iosApp/Sources/ui/home/HomeViewModel.swift @@ -25,34 +25,44 @@ 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 userResult): + user = userResult + case .error(let error): + viewState = .error(message: error.message) + } + + switch asEnumResult(result: pointResult) { + 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) + } + 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..9a50d2e 100644 --- a/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift +++ b/iosApp/iosApp/Sources/ui/pointget/PointGetViewModel.swift @@ -17,13 +17,18 @@ 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 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: "ポイント残高の取得に失敗しました。") } @@ -80,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 76b83ed..35d4c36 100644 --- a/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift +++ b/iosApp/iosApp/Sources/ui/slpash/SplashViewModel.swift @@ -15,11 +15,16 @@ 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 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 diff --git a/iosApp/iosApp/Sources/util/Extension.swift b/iosApp/iosApp/Sources/util/Extension.swift new file mode 100644 index 0000000..4487979 --- /dev/null +++ b/iosApp/iosApp/Sources/util/Extension.swift @@ -0,0 +1,56 @@ +// +// Extension.swift +// iosApp +// +import Foundation +import shared + +// Sealed Classが使いづらいのでAppResultをEnumにする +enum AppResultEx { + case success(data: T) + case error(error: AppError) +} + +func asEnumResult(result: AppResult) -> AppResultEx { + if let success = result as? AppResultSuccess { + // 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) + } + 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