From 8da18fb37a3b6b8d4d8b353031067344d200ccbb Mon Sep 17 00:00:00 2001 From: DG Date: Sat, 13 Jul 2024 23:09:11 +0900 Subject: [PATCH] Feature/workout mode/main (#53) * feat: add shortcut * feat: add training mode * feat: GetRecordGoalStrengthUsecase * feat: SubscribeTrainingModeUsecase * feat: subscribe training mode * feat: add test case * feat: add business logic * feat: CheckGoalAchievedUsecase * refactor: using usecase * feat: assign strength goal * feat: change goal view * feat: adjust ui * feat: change test case * feat: add navigation * feat: PostTraingModeUsecase * feat: manage training mode view * feat: footer --- .../CheckGoalAchievedUsecaseTest.swift | 65 +++++++++++++++++ ...CheckStrengthGoalAchievedUsecaseTest.swift | 70 +++++++++++++++++++ .../GetRecordGoalStrengthUsecaseTest.swift | 59 ++++++++++++++++ .../sources/App/DI/HistoryAssembly.swift | 7 ++ .../sources/App/DI/MainAssembly.swift | 3 +- .../sources/App/Delegate/AppDelegate.swift | 10 +++ .../sources/App/Delegate/SceneDelegate.swift | 13 +++- .../DataLayer/User/Model/TrainingMode.swift | 32 +++++++++ .../DataLayer/User/Model/UserData.swift | 5 +- .../User/Repository/UserRepositoryImpl.swift | 7 +- .../Usecase/CheckGoalAchievedUsecase.swift | 18 +++++ .../CheckStrengthGoalAchievedUsecase.swift | 25 +++++++ .../GetRecordGoalStrengthUsecase.swift | 28 ++++++++ .../Usecase/GetRecordGoalUsecase.swift | 2 +- .../Domain/User/Model/TrainingMode.swift | 13 ++++ .../sources/Domain/User/Model/User.swift | 5 +- .../User/Repository/UserRepository.swift | 1 + .../User/Usecase/PostTraingModeUsecase.swift | 20 ++++++ .../SubscribeTrainingModeUsecase.swift | 34 +++++++++ .../sources/MockData/Data/User.swift | 15 ++-- .../Repository/UserRepositoryMock.swift | 4 ++ .../Common/Model/TrainingMode.swift | 41 +++++++++++ .../Presentation/Common/Model/User.swift | 6 +- .../Friend/List/Model/Friend.swift | 5 +- .../Friend/List/Model/TrainingMode.swift | 33 +++++++++ .../History/View/Form/Manage/GoalView.swift | 10 ++- .../View/Form/Manage/ManageRecordView.swift | 19 ++++- .../Form/Manage/ManageRecordViewModel.swift | 53 ++++++++++---- .../Form/Manage/ManageTrainingModeView.swift | 54 ++++++++++++++ .../Manage/ManageTrainingModeViewModel.swift | 44 ++++++++++++ .../Main/Coordinator/HistoryCoordinator.swift | 4 ++ .../Main/Navigation/HistoryNavigation.swift | 1 + .../Main/View/NavigationView.swift | 7 +- 33 files changed, 681 insertions(+), 32 deletions(-) create mode 100644 dg-muscle-ios/Tests/History/CheckGoalAchievedUsecaseTest.swift create mode 100644 dg-muscle-ios/Tests/History/CheckStrengthGoalAchievedUsecaseTest.swift create mode 100644 dg-muscle-ios/Tests/History/GetRecordGoalStrengthUsecaseTest.swift create mode 100644 dg-muscle-ios/sources/DataLayer/User/Model/TrainingMode.swift create mode 100644 dg-muscle-ios/sources/Domain/History/Usecase/CheckGoalAchievedUsecase.swift create mode 100644 dg-muscle-ios/sources/Domain/History/Usecase/CheckStrengthGoalAchievedUsecase.swift create mode 100644 dg-muscle-ios/sources/Domain/History/Usecase/GetRecordGoalStrengthUsecase.swift create mode 100644 dg-muscle-ios/sources/Domain/User/Model/TrainingMode.swift create mode 100644 dg-muscle-ios/sources/Domain/User/Usecase/PostTraingModeUsecase.swift create mode 100644 dg-muscle-ios/sources/Domain/User/Usecase/SubscribeTrainingModeUsecase.swift create mode 100644 dg-muscle-ios/sources/Presentation/Common/Model/TrainingMode.swift create mode 100644 dg-muscle-ios/sources/Presentation/Friend/List/Model/TrainingMode.swift create mode 100644 dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageTrainingModeView.swift create mode 100644 dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageTrainingModeViewModel.swift diff --git a/dg-muscle-ios/Tests/History/CheckGoalAchievedUsecaseTest.swift b/dg-muscle-ios/Tests/History/CheckGoalAchievedUsecaseTest.swift new file mode 100644 index 00000000..13f3d286 --- /dev/null +++ b/dg-muscle-ios/Tests/History/CheckGoalAchievedUsecaseTest.swift @@ -0,0 +1,65 @@ +// +// CheckGoalAchievedUsecaseTest.swift +// AppTests +// +// Created by 신동규 on 7/13/24. +// + +import XCTest +import Domain + +final class CheckGoalAchievedUsecaseTest: XCTestCase { + + func testAchieved() { + // given + let usecase = CheckGoalAchievedUsecase() + + let goal: ExerciseSet = .init( + id: UUID().uuidString, + unit: .kg, + reps: 13, + weight: 60 + ) + + let record: ExerciseRecord = .init( + id: UUID().uuidString, + exerciseId: "squat", + sets: [ + .init(id: UUID().uuidString, unit: .kg, reps: 13, weight: 60) + ] + ) + + // when + let result = usecase.implement(goal: goal, record: record) + + // then + XCTAssertTrue(result) + } + + func testNotAchieved() { + // given + let usecase = CheckGoalAchievedUsecase() + + let goal: ExerciseSet = .init( + id: UUID().uuidString, + unit: .kg, + reps: 13, + weight: 60 + ) + + let record: ExerciseRecord = .init( + id: UUID().uuidString, + exerciseId: "squat", + sets: [ + .init(id: UUID().uuidString, unit: .kg, reps: 12, weight: 60) + ] + ) + + // when + let result = usecase.implement(goal: goal, record: record) + + // then + XCTAssertFalse(result) + } + +} diff --git a/dg-muscle-ios/Tests/History/CheckStrengthGoalAchievedUsecaseTest.swift b/dg-muscle-ios/Tests/History/CheckStrengthGoalAchievedUsecaseTest.swift new file mode 100644 index 00000000..c06e2302 --- /dev/null +++ b/dg-muscle-ios/Tests/History/CheckStrengthGoalAchievedUsecaseTest.swift @@ -0,0 +1,70 @@ +// +// CheckStrengthGoalAchievedUsecaseTest.swift +// AppTests +// +// Created by 신동규 on 7/13/24. +// + +import XCTest +import Domain + +final class CheckStrengthGoalAchievedUsecaseTest: XCTestCase { + + func testFalseCase() { + // given + let usecase = CheckStrengthGoalAchievedUsecase() + let goal: ExerciseSet = .init( + id: UUID().uuidString, + unit: .kg, + reps: 5, + weight: 65 + ) + + let record: ExerciseRecord = .init( + id: UUID().uuidString, + exerciseId: "squat", + sets: [ + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + ] + ) + + // when + let result = usecase.implement(goal: goal, record: record) + + // then + XCTAssertFalse(result) + } + + func testTrueCase() { + // given + let usecase = CheckStrengthGoalAchievedUsecase() + let goal: ExerciseSet = .init( + id: UUID().uuidString, + unit: .kg, + reps: 5, + weight: 60 + ) + + let record: ExerciseRecord = .init( + id: UUID().uuidString, + exerciseId: "squat", + sets: [ + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + ] + ) + + // when + let result = usecase.implement(goal: goal, record: record) + + // then + XCTAssertTrue(result) + } +} diff --git a/dg-muscle-ios/Tests/History/GetRecordGoalStrengthUsecaseTest.swift b/dg-muscle-ios/Tests/History/GetRecordGoalStrengthUsecaseTest.swift new file mode 100644 index 00000000..e405ecb7 --- /dev/null +++ b/dg-muscle-ios/Tests/History/GetRecordGoalStrengthUsecaseTest.swift @@ -0,0 +1,59 @@ +// +// GetRecordGoalStrengthUsecaseTest.swift +// AppTests +// +// Created by 신동규 on 7/13/24. +// + +import XCTest +import Domain + +final class GetRecordGoalStrengthUsecaseTest: XCTestCase { + + func testMoreWeight() { + // given + let useCase = GetRecordGoalStrengthUsecase() + + let record: ExerciseRecord = .init( + id: UUID().uuidString, + exerciseId: "squat", + sets: [ + .init(id: UUID().uuidString, unit: .kg, reps: 15, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 15, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: 60) + ] + ) + + // when + let goal = useCase.implement(previousRecord: record) + + // then + XCTAssertEqual(goal?.weight, 65) + XCTAssertEqual(goal?.reps, 5) + } + + func testSameWeigth() { + // given + let useCase = GetRecordGoalStrengthUsecase() + + let record: ExerciseRecord = .init( + id: UUID().uuidString, + exerciseId: "squat", + sets: [ + .init(id: UUID().uuidString, unit: .kg, reps: 15, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 15, weight: 60), + .init(id: UUID().uuidString, unit: .kg, reps: 15, weight: 60) + ] + ) + + // when + let goal = useCase.implement(previousRecord: record) + + // then + XCTAssertEqual(goal?.weight, 60) + XCTAssertEqual(goal?.reps, 5) + } + +} diff --git a/dg-muscle-ios/sources/App/DI/HistoryAssembly.swift b/dg-muscle-ios/sources/App/DI/HistoryAssembly.swift index e47eb9f9..fffbfb64 100644 --- a/dg-muscle-ios/sources/App/DI/HistoryAssembly.swift +++ b/dg-muscle-ios/sources/App/DI/HistoryAssembly.swift @@ -108,5 +108,12 @@ public struct HistoryAssembly: Assembly { return DateToSelectHistoryView(historyRepository: historyRepository) } + + container.register(ManageTrainingModeView.self) { resolver in + + let userRepository = resolver.resolve(UserRepository.self)! + + return ManageTrainingModeView(userRepository: userRepository) + } } } diff --git a/dg-muscle-ios/sources/App/DI/MainAssembly.swift b/dg-muscle-ios/sources/App/DI/MainAssembly.swift index 5a98c0bf..33fbde1b 100644 --- a/dg-muscle-ios/sources/App/DI/MainAssembly.swift +++ b/dg-muscle-ios/sources/App/DI/MainAssembly.swift @@ -41,7 +41,8 @@ public struct MainAssembly: Assembly { logsFactory: { resolver.resolve(LogsView.self)! }, friendMainFactory: { anchor in resolver.resolve(FriendMainView.self, argument: anchor)! }, friendHistoryFactory: { friendId, today in resolver.resolve(FriendHistoryView.self, arguments: friendId, today)! }, - historyDetailFactory: { friendId, historyId in resolver.resolve(HistoryDetailView.self, arguments: friendId, historyId)! } + historyDetailFactory: { friendId, historyId in resolver.resolve(HistoryDetailView.self, arguments: friendId, historyId)! }, + manageTrainingModeFactory: { resolver.resolve(ManageTrainingModeView.self)! } ) } diff --git a/dg-muscle-ios/sources/App/Delegate/AppDelegate.swift b/dg-muscle-ios/sources/App/Delegate/AppDelegate.swift index f42f91d4..2eb2f170 100644 --- a/dg-muscle-ios/sources/App/Delegate/AppDelegate.swift +++ b/dg-muscle-ios/sources/App/Delegate/AppDelegate.swift @@ -21,6 +21,16 @@ class AppDelegate: NSObject, UIApplicationDelegate { UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in } application.registerForRemoteNotifications() Messaging.messaging().delegate = self + + application.shortcutItems = [ + UIApplicationShortcutItem( + type: "dgmuscle://history", + localizedTitle: "Quick Record", + localizedSubtitle: "move to today record page directly", + icon: .init(systemImageName: "figure.snowboarding") + ) + ] + return true } diff --git a/dg-muscle-ios/sources/App/Delegate/SceneDelegate.swift b/dg-muscle-ios/sources/App/Delegate/SceneDelegate.swift index 78b3feea..31f15730 100644 --- a/dg-muscle-ios/sources/App/Delegate/SceneDelegate.swift +++ b/dg-muscle-ios/sources/App/Delegate/SceneDelegate.swift @@ -15,7 +15,7 @@ class SceneDelegate: NSObject, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let shortcutItem = connectionOptions.shortcutItem { - print("dg: shortcutItem is \(shortcutItem)") + handleShortCutItem(item: shortcutItem) } if let urlContext = connectionOptions.urlContexts.first { @@ -26,7 +26,7 @@ class SceneDelegate: NSObject, UIWindowSceneDelegate { } func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - print("dg: shortcutItem is \(shortcutItem)") + handleShortCutItem(item: shortcutItem) completionHandler(true) } @@ -35,6 +35,12 @@ class SceneDelegate: NSObject, UIWindowSceneDelegate { handleURL(url: url) } + private func handleShortCutItem(item: UIApplicationShortcutItem) { + if let url = URL(string: item.type) { + handleURL(url: url) + } + } + private func handleURL(url: URL) { if UserRepositoryImpl.shared.isReady == false { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { @@ -91,7 +97,8 @@ class SceneDelegate: NSObject, UIWindowSceneDelegate { coordinator?.history.setDuration(duration: Int(duration) ?? 0) case "datetoselecthistory": coordinator?.history.dateToSelectHistory() - + case "managetraingmode": + coordinator?.history.manageTrainigMode() default: break } } diff --git a/dg-muscle-ios/sources/DataLayer/User/Model/TrainingMode.swift b/dg-muscle-ios/sources/DataLayer/User/Model/TrainingMode.swift new file mode 100644 index 00000000..c53c85f2 --- /dev/null +++ b/dg-muscle-ios/sources/DataLayer/User/Model/TrainingMode.swift @@ -0,0 +1,32 @@ +// +// TrainingMode.swift +// DataLayer +// +// Created by 신동규 on 7/13/24. +// + +import Foundation +import Domain + +public enum TrainingMode: Codable { + case mass + case strength + + init(domain: Domain.TrainingMode) { + switch domain { + case .mass: + self = .mass + case .strength: + self = .strength + } + } + + var domain: Domain.TrainingMode { + switch self { + case .mass: + return .mass + case .strength: + return .strength + } + } +} diff --git a/dg-muscle-ios/sources/DataLayer/User/Model/UserData.swift b/dg-muscle-ios/sources/DataLayer/User/Model/UserData.swift index a661f5db..3923c7f7 100644 --- a/dg-muscle-ios/sources/DataLayer/User/Model/UserData.swift +++ b/dg-muscle-ios/sources/DataLayer/User/Model/UserData.swift @@ -19,6 +19,7 @@ struct UserData: Codable { var link: String? var developer: Bool? var onlyShowsFavoriteExercises: Bool? + var trainingMode: TrainingMode? init(domain: Domain.User) { self.id = domain.uid @@ -31,6 +32,7 @@ struct UserData: Codable { self.deleted = false self.developer = domain.developer self.onlyShowsFavoriteExercises = domain.onlyShowsFavoriteExercises + self.trainingMode = .init(domain: domain.trainingMode) } var domain: Domain.User { @@ -43,7 +45,8 @@ struct UserData: Codable { fcmToken: fcmToken, link: .init(string: link ?? ""), developer: developer ?? false, - onlyShowsFavoriteExercises: onlyShowsFavoriteExercises ?? false + onlyShowsFavoriteExercises: onlyShowsFavoriteExercises ?? false, + trainingMode: trainingMode?.domain ?? .mass ) } } diff --git a/dg-muscle-ios/sources/DataLayer/User/Repository/UserRepositoryImpl.swift b/dg-muscle-ios/sources/DataLayer/User/Repository/UserRepositoryImpl.swift index 0888ac91..8d3c227a 100644 --- a/dg-muscle-ios/sources/DataLayer/User/Repository/UserRepositoryImpl.swift +++ b/dg-muscle-ios/sources/DataLayer/User/Repository/UserRepositoryImpl.swift @@ -92,6 +92,10 @@ public final class UserRepositoryImpl: UserRepository { _user?.onlyShowsFavoriteExercises = onlyShowsFavoriteExercises } + public func updateUser(trainingMode: Domain.TrainingMode) { + _user?.trainingMode = trainingMode + } + public func withDrawal() async -> (any Error)? { if _user?.uid == "taEJh30OpGVsR3FEFN2s67A8FvF3" { @@ -169,7 +173,8 @@ public final class UserRepositoryImpl: UserRepository { fcmToken: nil, link: nil, developer: false, - onlyShowsFavoriteExercises: false + onlyShowsFavoriteExercises: false, + trainingMode: .mass ) return user } diff --git a/dg-muscle-ios/sources/Domain/History/Usecase/CheckGoalAchievedUsecase.swift b/dg-muscle-ios/sources/Domain/History/Usecase/CheckGoalAchievedUsecase.swift new file mode 100644 index 00000000..36f8d309 --- /dev/null +++ b/dg-muscle-ios/sources/Domain/History/Usecase/CheckGoalAchievedUsecase.swift @@ -0,0 +1,18 @@ +// +// CheckGoalAchievedUsecase.swift +// Domain +// +// Created by 신동규 on 7/13/24. +// + +import Foundation + +public final class CheckGoalAchievedUsecase { + public init() { } + + public func implement(goal: ExerciseSet, record: ExerciseRecord) -> Bool { + return !record.sets.filter({ set in + goal.weight <= set.weight && goal.reps <= set.reps + }).isEmpty + } +} diff --git a/dg-muscle-ios/sources/Domain/History/Usecase/CheckStrengthGoalAchievedUsecase.swift b/dg-muscle-ios/sources/Domain/History/Usecase/CheckStrengthGoalAchievedUsecase.swift new file mode 100644 index 00000000..0025a141 --- /dev/null +++ b/dg-muscle-ios/sources/Domain/History/Usecase/CheckStrengthGoalAchievedUsecase.swift @@ -0,0 +1,25 @@ +// +// CheckStrengthGoalAchievedUsecase.swift +// Domain +// +// Created by 신동규 on 7/13/24. +// + +import Foundation + +public final class CheckStrengthGoalAchievedUsecase { + public init() { } + + public func implement(goal: ExerciseSet, record: ExerciseRecord) -> Bool { + var result: Bool = false + + var sets = record.sets + + sets = sets + .filter({ $0.weight >= goal.weight && $0.reps >= goal.reps }) + + result = sets.count >= 5 + + return result + } +} diff --git a/dg-muscle-ios/sources/Domain/History/Usecase/GetRecordGoalStrengthUsecase.swift b/dg-muscle-ios/sources/Domain/History/Usecase/GetRecordGoalStrengthUsecase.swift new file mode 100644 index 00000000..8960201e --- /dev/null +++ b/dg-muscle-ios/sources/Domain/History/Usecase/GetRecordGoalStrengthUsecase.swift @@ -0,0 +1,28 @@ +// +// GetRecordGoalStrengthUsecase.swift +// Domain +// +// Created by 신동규 on 7/13/24. +// + +import Foundation + +/// Recommends Goal of ExerciseSet for strength +public final class GetRecordGoalStrengthUsecase { + public init() { } + + public func implement(previousRecord: ExerciseRecord) -> ExerciseSet? { + var result: ExerciseSet? + var sets = previousRecord.sets + guard let maxWeight = sets.map({ $0.weight }).max() else { return nil } + var goalWeight = maxWeight + sets = sets.filter({ $0.weight >= maxWeight }) + + if sets.count >= 5 { + goalWeight += 5 + } + + result = .init(id: UUID().uuidString, unit: .kg, reps: 5, weight: goalWeight) + return result + } +} diff --git a/dg-muscle-ios/sources/Domain/History/Usecase/GetRecordGoalUsecase.swift b/dg-muscle-ios/sources/Domain/History/Usecase/GetRecordGoalUsecase.swift index b8f4e851..e41e34e7 100644 --- a/dg-muscle-ios/sources/Domain/History/Usecase/GetRecordGoalUsecase.swift +++ b/dg-muscle-ios/sources/Domain/History/Usecase/GetRecordGoalUsecase.swift @@ -7,7 +7,7 @@ import Foundation -/// Recommends Goal of ExerciseSet +/// Recommends Goal of ExerciseSet for muscle mass public final class GetRecordGoalUsecase { public init() { } diff --git a/dg-muscle-ios/sources/Domain/User/Model/TrainingMode.swift b/dg-muscle-ios/sources/Domain/User/Model/TrainingMode.swift new file mode 100644 index 00000000..8899f113 --- /dev/null +++ b/dg-muscle-ios/sources/Domain/User/Model/TrainingMode.swift @@ -0,0 +1,13 @@ +// +// TrainingMode.swift +// Domain +// +// Created by 신동규 on 7/13/24. +// + +import Foundation + +public enum TrainingMode { + case mass + case strength +} diff --git a/dg-muscle-ios/sources/Domain/User/Model/User.swift b/dg-muscle-ios/sources/Domain/User/Model/User.swift index 285503f6..5d15d8f5 100644 --- a/dg-muscle-ios/sources/Domain/User/Model/User.swift +++ b/dg-muscle-ios/sources/Domain/User/Model/User.swift @@ -17,6 +17,7 @@ public struct User { public var link: URL? public var developer: Bool public var onlyShowsFavoriteExercises: Bool + public var trainingMode: TrainingMode public init( uid: String, @@ -27,7 +28,8 @@ public struct User { fcmToken: String?, link: URL?, developer: Bool, - onlyShowsFavoriteExercises: Bool + onlyShowsFavoriteExercises: Bool, + trainingMode: TrainingMode ) { self.uid = uid self.displayName = displayName @@ -38,5 +40,6 @@ public struct User { self.link = link self.developer = developer self.onlyShowsFavoriteExercises = onlyShowsFavoriteExercises + self.trainingMode = trainingMode } } diff --git a/dg-muscle-ios/sources/Domain/User/Repository/UserRepository.swift b/dg-muscle-ios/sources/Domain/User/Repository/UserRepository.swift index bbec8711..8f6c248d 100644 --- a/dg-muscle-ios/sources/Domain/User/Repository/UserRepository.swift +++ b/dg-muscle-ios/sources/Domain/User/Repository/UserRepository.swift @@ -26,5 +26,6 @@ public protocol UserRepository { func updateUser(backgroundImage: UIImage?) async throws func updateUser(link: URL?) func updateUser(onlyShowsFavoriteExercises: Bool) + func updateUser(trainingMode: TrainingMode) func withDrawal() async -> Error? } diff --git a/dg-muscle-ios/sources/Domain/User/Usecase/PostTraingModeUsecase.swift b/dg-muscle-ios/sources/Domain/User/Usecase/PostTraingModeUsecase.swift new file mode 100644 index 00000000..6b83298e --- /dev/null +++ b/dg-muscle-ios/sources/Domain/User/Usecase/PostTraingModeUsecase.swift @@ -0,0 +1,20 @@ +// +// PostTraingModeUsecase.swift +// Domain +// +// Created by 신동규 on 7/13/24. +// + +import Foundation + +public final class PostTraingModeUsecase { + let userRepository: UserRepository + + public init(userRepository: UserRepository) { + self.userRepository = userRepository + } + + public func implement(mode: TrainingMode) { + userRepository.updateUser(trainingMode: mode) + } +} diff --git a/dg-muscle-ios/sources/Domain/User/Usecase/SubscribeTrainingModeUsecase.swift b/dg-muscle-ios/sources/Domain/User/Usecase/SubscribeTrainingModeUsecase.swift new file mode 100644 index 00000000..66a05a3e --- /dev/null +++ b/dg-muscle-ios/sources/Domain/User/Usecase/SubscribeTrainingModeUsecase.swift @@ -0,0 +1,34 @@ +// +// SubscribeTrainingModeUsecase.swift +// Domain +// +// Created by 신동규 on 7/13/24. +// + +import Foundation +import Combine + +public final class SubscribeTrainingModeUsecase { + + @Published private var traingMode: TrainingMode + + let userRepository: UserRepository + + public init(userRepository: UserRepository) { + self.userRepository = userRepository + traingMode = userRepository.get()?.trainingMode ?? .mass + + bind() + } + + public func implement() -> AnyPublisher { + $traingMode.eraseToAnyPublisher() + } + + private func bind() { + userRepository + .user + .compactMap({ $0?.trainingMode }) + .assign(to: &$traingMode) + } +} diff --git a/dg-muscle-ios/sources/MockData/Data/User.swift b/dg-muscle-ios/sources/MockData/Data/User.swift index 0fc34e4c..c613a186 100644 --- a/dg-muscle-ios/sources/MockData/Data/User.swift +++ b/dg-muscle-ios/sources/MockData/Data/User.swift @@ -19,7 +19,8 @@ public let USER_DG: User = .init( fcmToken: nil, link: .init(string: "https://github.com/donggyushin"), developer: true, - onlyShowsFavoriteExercises: true + onlyShowsFavoriteExercises: true, + trainingMode: .strength ) @@ -32,7 +33,8 @@ public let USER_1: User = .init( fcmToken: nil, link: nil, developer: false, - onlyShowsFavoriteExercises: false + onlyShowsFavoriteExercises: false, + trainingMode: .mass ) public let USER_2: User = .init( @@ -44,7 +46,8 @@ public let USER_2: User = .init( fcmToken: nil, link: nil, developer: false, - onlyShowsFavoriteExercises: false + onlyShowsFavoriteExercises: false, + trainingMode: .mass ) public let USER_3: User = .init( @@ -56,7 +59,8 @@ public let USER_3: User = .init( fcmToken: nil, link: nil, developer: false, - onlyShowsFavoriteExercises: false + onlyShowsFavoriteExercises: false, + trainingMode: .mass ) public let USER_4: User = .init( @@ -68,5 +72,6 @@ public let USER_4: User = .init( fcmToken: nil, link: .init(string: "https://github.com/donggyushin"), developer: false, - onlyShowsFavoriteExercises: false + onlyShowsFavoriteExercises: false, + trainingMode: .mass ) diff --git a/dg-muscle-ios/sources/MockData/Repository/UserRepositoryMock.swift b/dg-muscle-ios/sources/MockData/Repository/UserRepositoryMock.swift index 1434bcff..96387139 100644 --- a/dg-muscle-ios/sources/MockData/Repository/UserRepositoryMock.swift +++ b/dg-muscle-ios/sources/MockData/Repository/UserRepositoryMock.swift @@ -52,6 +52,10 @@ public final class UserRepositoryMock: UserRepository { _user?.onlyShowsFavoriteExercises = onlyShowsFavoriteExercises } + public func updateUser(trainingMode: TrainingMode) { + _user?.trainingMode = trainingMode + } + public func withDrawal() async -> (any Error)? { nil } diff --git a/dg-muscle-ios/sources/Presentation/Common/Model/TrainingMode.swift b/dg-muscle-ios/sources/Presentation/Common/Model/TrainingMode.swift new file mode 100644 index 00000000..107b50a1 --- /dev/null +++ b/dg-muscle-ios/sources/Presentation/Common/Model/TrainingMode.swift @@ -0,0 +1,41 @@ +// +// TrainingMode.swift +// Common +// +// Created by 신동규 on 7/13/24. +// + +import Foundation +import Domain + +public enum TrainingMode: CaseIterable { + case mass + case strength + + public init(domain: Domain.TrainingMode) { + switch domain { + case .mass: + self = .mass + case .strength: + self = .strength + } + } + + public var domain: Domain.TrainingMode { + switch self { + case .mass: + return .mass + case .strength: + return .strength + } + } + + public var text: String { + switch self { + case .mass: + "Muscle Mass" + case .strength: + "Strengh" + } + } +} diff --git a/dg-muscle-ios/sources/Presentation/Common/Model/User.swift b/dg-muscle-ios/sources/Presentation/Common/Model/User.swift index f794ce43..ed3a4d0a 100644 --- a/dg-muscle-ios/sources/Presentation/Common/Model/User.swift +++ b/dg-muscle-ios/sources/Presentation/Common/Model/User.swift @@ -20,6 +20,7 @@ public struct User: Hashable, Identifiable { public let link: URL? public let developer: Bool public let onlyShowsFavoriteExercises: Bool + public let trainingMode: TrainingMode public init() { uid = UUID().uuidString @@ -31,6 +32,7 @@ public struct User: Hashable, Identifiable { link = nil developer = false onlyShowsFavoriteExercises = false + trainingMode = .mass } public init(domain: Domain.User) { @@ -43,6 +45,7 @@ public struct User: Hashable, Identifiable { self.link = domain.link self.developer = domain.developer self.onlyShowsFavoriteExercises = domain.onlyShowsFavoriteExercises + self.trainingMode = .init(domain: domain.trainingMode) } public var domain: Domain.User { @@ -55,7 +58,8 @@ public struct User: Hashable, Identifiable { fcmToken: fcmToken, link: link, developer: developer, - onlyShowsFavoriteExercises: onlyShowsFavoriteExercises + onlyShowsFavoriteExercises: onlyShowsFavoriteExercises, + trainingMode: trainingMode.domain ) } } diff --git a/dg-muscle-ios/sources/Presentation/Friend/List/Model/Friend.swift b/dg-muscle-ios/sources/Presentation/Friend/List/Model/Friend.swift index e39df781..5a447c2a 100644 --- a/dg-muscle-ios/sources/Presentation/Friend/List/Model/Friend.swift +++ b/dg-muscle-ios/sources/Presentation/Friend/List/Model/Friend.swift @@ -19,6 +19,7 @@ struct Friend: Hashable, Identifiable { var link: URL? var developer: Bool var onlyShowsFavoriteExercises: Bool + var traningMode: TrainingMode init(domain: Domain.User) { uid = domain.uid @@ -29,6 +30,7 @@ struct Friend: Hashable, Identifiable { link = domain.link developer = domain.developer onlyShowsFavoriteExercises = domain.onlyShowsFavoriteExercises + traningMode = .init(domain: domain.trainingMode) } var domain: Domain.User { @@ -41,7 +43,8 @@ struct Friend: Hashable, Identifiable { fcmToken: nil, link: link, developer: developer, - onlyShowsFavoriteExercises: onlyShowsFavoriteExercises + onlyShowsFavoriteExercises: onlyShowsFavoriteExercises, + trainingMode: traningMode.domain ) } } diff --git a/dg-muscle-ios/sources/Presentation/Friend/List/Model/TrainingMode.swift b/dg-muscle-ios/sources/Presentation/Friend/List/Model/TrainingMode.swift new file mode 100644 index 00000000..714edec5 --- /dev/null +++ b/dg-muscle-ios/sources/Presentation/Friend/List/Model/TrainingMode.swift @@ -0,0 +1,33 @@ +// +// TrainingMode.swift +// Friend +// +// Created by 신동규 on 7/13/24. +// + +import Foundation +import Domain + +public enum TrainingMode { + case mass + case strength + + init(domain: Domain.TrainingMode) { + switch domain { + case .mass: + self = .mass + case .strength: + self = .strength + } + } + + var domain: Domain.TrainingMode { + switch self { + case .mass: + return .mass + case .strength: + return .strength + } + } +} + diff --git a/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/GoalView.swift b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/GoalView.swift index 132622df..348d3218 100644 --- a/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/GoalView.swift +++ b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/GoalView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import Common struct GoalView: View { let goal: Goal let color: Color + let trainingMode: TrainingMode var body: some View { Section { @@ -19,6 +21,10 @@ struct GoalView: View { Text(" kg x ") + Text("\(goal.reps)").foregroundStyle(color) + if trainingMode == .strength { + Text("x 5") + } + Spacer() Image(systemName: "flag.checkered") @@ -36,7 +42,9 @@ struct GoalView: View { #Preview { List { - GoalView(goal: .init(weight: 70, reps: 9, achive: true), color: .purple) + GoalView(goal: .init(weight: 70, reps: 9, achive: true), color: .purple, trainingMode: .mass) + + GoalView(goal: .init(weight: 70, reps: 5, achive: true), color: .purple, trainingMode: .strength) } .preferredColorScheme(.dark) } diff --git a/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageRecordView.swift b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageRecordView.swift index 7f309d1f..c0b2ff56 100644 --- a/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageRecordView.swift +++ b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageRecordView.swift @@ -32,8 +32,23 @@ public struct ManageRecordView: View { public var body: some View { List { - if let goal = viewModel.goal { - GoalView(goal: goal, color: viewModel.color) + if let mode = viewModel.traingMode { + switch mode { + case .mass: + if let goal = viewModel.goal { + GoalView(goal: goal, color: viewModel.color, trainingMode: mode) + .onTapGesture { + URLManager.shared.open(url: "dgmuscle://managetraingmode") + } + } + case .strength: + if let goal = viewModel.strengthGoal { + GoalView(goal: goal, color: viewModel.color, trainingMode: mode) + .onTapGesture { + URLManager.shared.open(url: "dgmuscle://managetraingmode") + } + } + } } Section { diff --git a/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageRecordViewModel.swift b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageRecordViewModel.swift index e4922e79..c7b309d3 100644 --- a/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageRecordViewModel.swift +++ b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageRecordViewModel.swift @@ -19,12 +19,18 @@ final class ManageRecordViewModel: ObservableObject { @Published var previousRecord: ExerciseRecord? @Published var diffWithPreviousRecord: Int? @Published var goal: Goal? + @Published var strengthGoal: Goal? + @Published var traingMode: Common.TrainingMode? private let recordId: String private let getHeatMapColorUsecase: GetHeatMapColorUsecase private let subscribeHeatMapColorUsecase: SubscribeHeatMapColorUsecase private let getPreviousRecordUsecase: GetPreviousRecordUsecase private let getRecordGoalUsecase: GetRecordGoalUsecase + private let getRecordGoalStrengthUsecase: GetRecordGoalStrengthUsecase + private let subscribeTrainingModeUsecase: SubscribeTrainingModeUsecase + private let checkGoalAchievedUsecase: CheckGoalAchievedUsecase + private let checkStrengthGoalAchievedUsecase: CheckStrengthGoalAchievedUsecase private var cancellables = Set() init( @@ -48,6 +54,10 @@ final class ManageRecordViewModel: ObservableObject { subscribeHeatMapColorUsecase = .init(userRepository: userRepository) getPreviousRecordUsecase = .init(historyRepository: historyRepository) getRecordGoalUsecase = .init() + getRecordGoalStrengthUsecase = .init() + subscribeTrainingModeUsecase = .init(userRepository: userRepository) + checkGoalAchievedUsecase = .init() + checkStrengthGoalAchievedUsecase = .init() let color: Common.HeatMapColor = .init(domain: getHeatMapColorUsecase.implement()) self.color = color.color @@ -104,22 +114,39 @@ final class ManageRecordViewModel: ObservableObject { $previousRecord .compactMap({ $0?.domain }) .map({ [weak self] in self?.getRecordGoalUsecase.implement(previousRecord: $0) }) - .map({ (set) -> Goal? in - guard let set else { return nil } - return Goal(weight: set.weight, reps: set.reps, achive: false) - }) .combineLatest($record) + .map({ [weak self] (goal, record) -> Goal? in + guard let self else { return nil } + if let goal = goal { + let achieved = checkGoalAchievedUsecase.implement(goal: goal, record: record.domain) + return Goal(weight: goal.weight, reps: goal.reps, achive: achieved) + } else { + return nil + } + }) .receive(on: DispatchQueue.main) - .sink { [weak self] goal, record in - if var goal { - var sets = record.sets - sets = sets.filter({ $0.weight >= goal.weight && $0.reps >= goal.reps }) - goal.achive = sets.isEmpty == false - self?.goal = goal + .assign(to: &$goal) + + $previousRecord + .compactMap({ $0?.domain }) + .map({ [weak self] in self?.getRecordGoalStrengthUsecase.implement(previousRecord: $0) }) + .combineLatest($record) + .map({ [weak self] (goal, record) -> Goal? in + guard let self else { return nil } + if let goal = goal { + let achieved = checkStrengthGoalAchievedUsecase.implement(goal: goal, record: record.domain) + return Goal(weight: goal.weight, reps: goal.reps, achive: achieved) } else { - self?.goal = nil + return nil } - } - .store(in: &cancellables) + }) + .receive(on: DispatchQueue.main) + .assign(to: &$strengthGoal) + + subscribeTrainingModeUsecase + .implement() + .receive(on: DispatchQueue.main) + .map({ Common.TrainingMode(domain: $0) }) + .assign(to: &$traingMode) } } diff --git a/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageTrainingModeView.swift b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageTrainingModeView.swift new file mode 100644 index 00000000..83143837 --- /dev/null +++ b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageTrainingModeView.swift @@ -0,0 +1,54 @@ +// +// ManageTrainingModeView.swift +// History +// +// Created by 신동규 on 7/13/24. +// + +import SwiftUI +import Domain +import MockData +import Common + +public struct ManageTrainingModeView: View { + + @StateObject var viewModel: ManageTrainingModeViewModel + + public init(userRepository: UserRepository) { + _viewModel = .init(wrappedValue: .init(userRepository: userRepository)) + } + + public var body: some View { + List { + Section { + ForEach(Common.TrainingMode.allCases, id: \.self) { mode in + modeView(mode: mode, selectedMode: viewModel.mode) + .onTapGesture { + viewModel.updateMode(mode: mode) + } + } + } footer: { + Text("Select your training mode") + } + + + } + .animation(.default, value: viewModel.mode) + } + + func modeView(mode: Common.TrainingMode, selectedMode: Common.TrainingMode?) -> some View { + HStack { + Text(mode.text) + + if selectedMode == mode { + Image(systemName: "checkmark.circle") + .foregroundStyle(viewModel.color.color) + } + } + } +} + +#Preview { + ManageTrainingModeView(userRepository: UserRepositoryMock()) + .preferredColorScheme(.dark) +} diff --git a/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageTrainingModeViewModel.swift b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageTrainingModeViewModel.swift new file mode 100644 index 00000000..a11050da --- /dev/null +++ b/dg-muscle-ios/sources/Presentation/History/View/Form/Manage/ManageTrainingModeViewModel.swift @@ -0,0 +1,44 @@ +// +// ManageTrainingModeViewModel.swift +// History +// +// Created by 신동규 on 7/13/24. +// + +import Foundation +import Combine +import Domain +import Common + +final class ManageTrainingModeViewModel: ObservableObject { + + @Published var mode: Common.TrainingMode? + + let color: Common.HeatMapColor + + let subscribeTrainingModeUsecase: SubscribeTrainingModeUsecase + let postTraingModeUsecase: PostTraingModeUsecase + let getHeatMapColorUsecase: GetHeatMapColorUsecase + + init(userRepository: UserRepository) { + subscribeTrainingModeUsecase = .init(userRepository: userRepository) + postTraingModeUsecase = .init(userRepository: userRepository) + getHeatMapColorUsecase = .init(userRepository: userRepository) + + color = .init(domain: getHeatMapColorUsecase.implement()) + + bind() + } + + func updateMode(mode: Common.TrainingMode) { + postTraingModeUsecase.implement(mode: mode.domain) + } + + private func bind() { + subscribeTrainingModeUsecase + .implement() + .receive(on: DispatchQueue.main) + .map({ Common.TrainingMode(domain: $0) }) + .assign(to: &$mode) + } +} diff --git a/dg-muscle-ios/sources/Presentation/Main/Coordinator/HistoryCoordinator.swift b/dg-muscle-ios/sources/Presentation/Main/Coordinator/HistoryCoordinator.swift index f716b1bd..04608e70 100644 --- a/dg-muscle-ios/sources/Presentation/Main/Coordinator/HistoryCoordinator.swift +++ b/dg-muscle-ios/sources/Presentation/Main/Coordinator/HistoryCoordinator.swift @@ -71,4 +71,8 @@ public final class HistoryCoordinator { public func dateToSelectHistory() { path.append(HistoryNavigation(name: .dateToSelectHistory)) } + + public func manageTrainigMode() { + path.append(HistoryNavigation(name: .manageTrainingMode)) + } } diff --git a/dg-muscle-ios/sources/Presentation/Main/Navigation/HistoryNavigation.swift b/dg-muscle-ios/sources/Presentation/Main/Navigation/HistoryNavigation.swift index 7cabd314..8d4df206 100644 --- a/dg-muscle-ios/sources/Presentation/Main/Navigation/HistoryNavigation.swift +++ b/dg-muscle-ios/sources/Presentation/Main/Navigation/HistoryNavigation.swift @@ -38,5 +38,6 @@ extension HistoryNavigation { case setDuration(Int) case manageMemo(Binding) case dateToSelectHistory + case manageTrainingMode } } diff --git a/dg-muscle-ios/sources/Presentation/Main/View/NavigationView.swift b/dg-muscle-ios/sources/Presentation/Main/View/NavigationView.swift index 8e879a5e..8beee18d 100644 --- a/dg-muscle-ios/sources/Presentation/Main/View/NavigationView.swift +++ b/dg-muscle-ios/sources/Presentation/Main/View/NavigationView.swift @@ -34,6 +34,7 @@ public struct NavigationView: View { let friendMainFactory: (PageAnchorView.Page) -> FriendMainView let friendHistoryFactory: (String, Date) -> FriendHistoryView let historyDetailFactory: (String, String) -> HistoryDetailView + let manageTrainingModeFactory: () -> ManageTrainingModeView public init( today: Date, @@ -54,7 +55,8 @@ public struct NavigationView: View { logsFactory: @escaping () -> LogsView, friendMainFactory: @escaping (PageAnchorView.Page) -> FriendMainView, friendHistoryFactory: @escaping (String, Date) -> FriendHistoryView, - historyDetailFactory: @escaping (String, String) -> HistoryDetailView + historyDetailFactory: @escaping (String, String) -> HistoryDetailView, + manageTrainingModeFactory: @escaping () -> ManageTrainingModeView ) { self.today = today self.historyRepository = historyRepository @@ -75,6 +77,7 @@ public struct NavigationView: View { self.friendMainFactory = friendMainFactory self.friendHistoryFactory = friendHistoryFactory self.historyDetailFactory = historyDetailFactory + self.manageTrainingModeFactory = manageTrainingModeFactory } public var body: some View { @@ -106,6 +109,8 @@ public struct NavigationView: View { manageMemoFactory(memo) case .dateToSelectHistory: dateToSelectHistoryFactory() + case .manageTrainingMode: + manageTrainingModeFactory() } } .navigationDestination(for: MyNavigation.self) { navigation in