Skip to content

Commit

Permalink
Feature/exercise db add exercise/add (#90)
Browse files Browse the repository at this point in the history
* CheckAddableExerciseUsecase

* RegisterRapidExerciseUsecase

* refactor

* RapidExerciseDetailViewModel

* feat: DI

* feat: binding

* chore

* fix

* fix: sorting
  • Loading branch information
donggyushin authored Aug 13, 2024
1 parent e8a4636 commit 898f295
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 60 deletions.
18 changes: 18 additions & 0 deletions dg-muscle-ios/Tests/Rapid/CheckAddableExerciseUsecaseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// CheckAddableExerciseUsecaseTests.swift
// AppTests
//
// Created by Happymoonday on 8/13/24.
//

import XCTest
import Domain
import MockData

final class CheckAddableExerciseUsecaseTests: XCTestCase {
func testExample() {
let usecase = CheckAddableExerciseUsecase()
let exercise = RAPID_EXERCISES[0]
XCTAssertTrue(usecase.implement(exercise: exercise))
}
}
6 changes: 5 additions & 1 deletion dg-muscle-ios/sources/App/DI/RapidAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ public struct RapidAssembly: Assembly {
}

container.register(RapidExerciseDetailView.self) { (resolver, exercise: RapidExerciseDomain) in
return RapidExerciseDetailView(exercise: exercise)

let exerciseRepository = resolver.resolve(ExerciseRepository.self)!

return RapidExerciseDetailView(exercise: exercise,
exerciseRepository: exerciseRepository)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// CheckAddableExerciseUsecase.swift
// Domain
//
// Created by Happymoonday on 8/13/24.
//

import Foundation

public final class CheckAddableExerciseUsecase {

public init() { }

public func implement(exercise: RapidExerciseDomain) -> Bool {
var result: Bool = false

switch exercise.bodyPart {

case .back,
.chest,
.lowerArms,
.lowerLegs,
.shoulders,
.upperArms,
.waist,
.upperLegs:
result = true
case .cardio, .neck:
break
}

return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// RegisterRapidExerciseUsecase.swift
// Domain
//
// Created by Happymoonday on 8/13/24.
//

import Foundation

public final class RegisterRapidExerciseUsecase {
private let exerciseRepository: ExerciseRepository

public init(exerciseRepository: ExerciseRepository) {
self.exerciseRepository = exerciseRepository
}

public func implement(exercise: RapidExerciseDomain) async throws {
var exerciseDomain: Exercise?

var parts: [Exercise.Part] = []

switch exercise.bodyPart {
case .back:
parts.append(.back)
case .chest:
parts.append(.chest)
case .lowerArms, .upperArms:
parts.append(.arm)
case .lowerLegs, .upperLegs:
parts.append(.leg)
case .shoulders:
parts.append(.shoulder)
case .waist:
parts.append(.core)
case .cardio, .neck:
break
}

guard parts.isEmpty == false else { return }

exerciseDomain = .init(
id: exercise.name.filter({ $0.isLetter }),
name: exercise.name,
parts: parts,
favorite: true
)

guard let exerciseDomain else { return }
try await exerciseRepository.post(exerciseDomain)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ final class SelectExerciseViewModel: ObservableObject {
let sections = $0.map({ part, exercises in ExerciseSection(part: .init(domain: part), exercises: exercises.map({ .init(domain: $0) }))})
return self?.configureExercisePopularity(sections: sections)
})
.map({ (sections: [ExerciseSection]) -> [ExerciseSection] in
sections.sorted(by: { $0.part.rawValue < $1.part.rawValue })
})
.assign(to: &$exericeSections)

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,86 +10,125 @@ import Domain
import MockData
import Kingfisher
import Flow
import Common

public struct RapidExerciseDetailView: View {

let data: RapidExercisePresentation
@State private var showsSecondaryMuscles: Bool = false
@StateObject var viewModel: RapidExerciseDetailViewModel

public init(exercise: Domain.RapidExerciseDomain) {
data = .init(domain: exercise)
public init(
exercise: Domain.RapidExerciseDomain,
exerciseRepository: ExerciseRepository
) {
_viewModel = .init(wrappedValue: .init(
exercise: exercise,
exerciseRepository: exerciseRepository
))
}

public var body: some View {
ScrollView {

KFAnimatedImage(.init(string: data.gifUrl))

KFAnimatedImage(.init(string: viewModel.data.gifUrl))
VStack(alignment: .leading) {
Text(data.equipment.capitalized)
Text(viewModel.data.equipment.capitalized)
.fontWeight(.black)

Divider()

HStack {
Text("Body Part:")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.italic()
Text("\(data.bodyPart.rawValue.capitalized)(\(data.target.capitalized))")
}

bodyPartsView
Spacer(minLength: 8)

Section {
if showsSecondaryMuscles {
HFlow {
ForEach(data.secondaryMuscles, id: \.self) { secondaryMuscle in
Text(secondaryMuscle)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color(uiColor: .secondarySystemGroupedBackground))
)
}
}
}
} header: {
Button {
showsSecondaryMuscles.toggle()
} label: {
HStack {
Text("secondary muscles".capitalized)
}
}
}

secondayMusclesView
Spacer(minLength: 12)


Text("Instructions")
.font(.title)
.padding(.bottom, 8)


ForEach(Array(zip(data.instructions.indices, data.instructions)), id: \.0) { (index, instruction) in
HStack(alignment: .top) {
Text("\(index + 1). ")
Text(instruction)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.bottom, 8)
}


instructionsView
}
.padding(.horizontal)

Spacer(minLength: 60)
}
.animation(.default, value: showsSecondaryMuscles)
.navigationTitle(data.name.capitalized)
.animation(.default, value: viewModel.showsSecondaryMuscles)
.navigationTitle(viewModel.data.name.capitalized)
.scrollIndicators(.hidden)
.overlay {
ZStack {
if viewModel.loading {
ProgressView()
}

if viewModel.showsAddButton {
VStack {
Spacer()

HStack {
Spacer()
Button {
viewModel.add()
} label: {
Image(systemName: "pencil.tip.crop.circle.badge.plus")
.padding()
.background {
Circle()
.fill(.thickMaterial)
.shadow(radius: 10)
}
}
.buttonStyle(.plain)
.padding()
}
}
}

if viewModel.snackbarMessage != nil {
Common.SnackbarView(message: $viewModel.snackbarMessage)
}
}
}
}

var bodyPartsView: some View {
HStack {
Text("Body Part:")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.italic()
Text("\(viewModel.data.bodyPart.rawValue.capitalized)(\(viewModel.data.target.capitalized))")
}
}

var secondayMusclesView: some View {
Section {
if viewModel.showsSecondaryMuscles {
HFlow {
ForEach(viewModel.data.secondaryMuscles, id: \.self) { secondaryMuscle in
Text(secondaryMuscle)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color(uiColor: .secondarySystemGroupedBackground))
)
}
}
}
} header: {
Button {
viewModel.showsSecondaryMuscles.toggle()
} label: {
HStack {
Text("secondary muscles".capitalized)
}
}
}
}

var instructionsView: some View {
ForEach(Array(zip(viewModel.data.instructions.indices, viewModel.data.instructions)), id: \.0) { (index, instruction) in
HStack(alignment: .top) {
Text("\(index + 1). ")
Text(instruction)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.bottom, 8)
}
}
}

Expand All @@ -98,7 +137,10 @@ public struct RapidExerciseDetailView: View {
let repository = RapidRepositoryMock()

return NavigationStack {
RapidExerciseDetailView(exercise: repository.get()[0])
RapidExerciseDetailView(
exercise: repository.get()[0],
exerciseRepository: ExerciseRepositoryMock()
)
.preferredColorScheme(.dark)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// RapidExerciseDetailViewModel.swift
// Weight
//
// Created by Happymoonday on 8/13/24.
//

import Foundation
import Combine
import Domain

final class RapidExerciseDetailViewModel: ObservableObject {
let data: RapidExercisePresentation
@Published var showsSecondaryMuscles: Bool = false
@Published var showsAddButton: Bool = false
@Published var snackbarMessage: String?
@Published var loading: Bool = false

private let checkAddableExerciseUsecase: CheckAddableExerciseUsecase
private let registerRapidExerciseUsecase: RegisterRapidExerciseUsecase

init(
exercise: Domain.RapidExerciseDomain,
exerciseRepository: ExerciseRepository
) {
data = .init(domain: exercise)

checkAddableExerciseUsecase = .init()
registerRapidExerciseUsecase = .init(exerciseRepository: exerciseRepository)

showsAddButton = checkAddableExerciseUsecase.implement(exercise: data.domain)
}

@MainActor
func add() {
Task {
guard loading == false else { return }
loading = true
do {
try await registerRapidExerciseUsecase.implement(exercise: data.domain)
snackbarMessage = "Exercise Registered!"
} catch {
snackbarMessage = error.localizedDescription
}
loading = false
}
}
}

0 comments on commit 898f295

Please sign in to comment.