Skip to content

Commit 4ce8f5d

Browse files
authored
Feature/exercise db add exercise/add (#90) (#91)
* CheckAddableExerciseUsecase * RegisterRapidExerciseUsecase * refactor * RapidExerciseDetailViewModel * feat: DI * feat: binding * chore * fix * fix: sorting
1 parent e8a4636 commit 4ce8f5d

File tree

7 files changed

+260
-60
lines changed

7 files changed

+260
-60
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// CheckAddableExerciseUsecaseTests.swift
3+
// AppTests
4+
//
5+
// Created by Happymoonday on 8/13/24.
6+
//
7+
8+
import XCTest
9+
import Domain
10+
import MockData
11+
12+
final class CheckAddableExerciseUsecaseTests: XCTestCase {
13+
func testExample() {
14+
let usecase = CheckAddableExerciseUsecase()
15+
let exercise = RAPID_EXERCISES[0]
16+
XCTAssertTrue(usecase.implement(exercise: exercise))
17+
}
18+
}

dg-muscle-ios/sources/App/DI/RapidAssembly.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ public struct RapidAssembly: Assembly {
3232
}
3333

3434
container.register(RapidExerciseDetailView.self) { (resolver, exercise: RapidExerciseDomain) in
35-
return RapidExerciseDetailView(exercise: exercise)
35+
36+
let exerciseRepository = resolver.resolve(ExerciseRepository.self)!
37+
38+
return RapidExerciseDetailView(exercise: exercise,
39+
exerciseRepository: exerciseRepository)
3640
}
3741
}
3842
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// CheckAddableExerciseUsecase.swift
3+
// Domain
4+
//
5+
// Created by Happymoonday on 8/13/24.
6+
//
7+
8+
import Foundation
9+
10+
public final class CheckAddableExerciseUsecase {
11+
12+
public init() { }
13+
14+
public func implement(exercise: RapidExerciseDomain) -> Bool {
15+
var result: Bool = false
16+
17+
switch exercise.bodyPart {
18+
19+
case .back,
20+
.chest,
21+
.lowerArms,
22+
.lowerLegs,
23+
.shoulders,
24+
.upperArms,
25+
.waist,
26+
.upperLegs:
27+
result = true
28+
case .cardio, .neck:
29+
break
30+
}
31+
32+
return result
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// RegisterRapidExerciseUsecase.swift
3+
// Domain
4+
//
5+
// Created by Happymoonday on 8/13/24.
6+
//
7+
8+
import Foundation
9+
10+
public final class RegisterRapidExerciseUsecase {
11+
private let exerciseRepository: ExerciseRepository
12+
13+
public init(exerciseRepository: ExerciseRepository) {
14+
self.exerciseRepository = exerciseRepository
15+
}
16+
17+
public func implement(exercise: RapidExerciseDomain) async throws {
18+
var exerciseDomain: Exercise?
19+
20+
var parts: [Exercise.Part] = []
21+
22+
switch exercise.bodyPart {
23+
case .back:
24+
parts.append(.back)
25+
case .chest:
26+
parts.append(.chest)
27+
case .lowerArms, .upperArms:
28+
parts.append(.arm)
29+
case .lowerLegs, .upperLegs:
30+
parts.append(.leg)
31+
case .shoulders:
32+
parts.append(.shoulder)
33+
case .waist:
34+
parts.append(.core)
35+
case .cardio, .neck:
36+
break
37+
}
38+
39+
guard parts.isEmpty == false else { return }
40+
41+
exerciseDomain = .init(
42+
id: exercise.name.filter({ $0.isLetter }),
43+
name: exercise.name,
44+
parts: parts,
45+
favorite: true
46+
)
47+
48+
guard let exerciseDomain else { return }
49+
try await exerciseRepository.post(exerciseDomain)
50+
}
51+
}

dg-muscle-ios/sources/Presentation/History/View/Form/Main/SelectExerciseViewModel.swift

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ final class SelectExerciseViewModel: ObservableObject {
6868
let sections = $0.map({ part, exercises in ExerciseSection(part: .init(domain: part), exercises: exercises.map({ .init(domain: $0) }))})
6969
return self?.configureExercisePopularity(sections: sections)
7070
})
71+
.map({ (sections: [ExerciseSection]) -> [ExerciseSection] in
72+
sections.sorted(by: { $0.part.rawValue < $1.part.rawValue })
73+
})
7174
.assign(to: &$exericeSections)
7275

7376
}

dg-muscle-ios/sources/Presentation/Rapid/View/SearchDetail/RapidExerciseDetailView.swift

+101-59
Original file line numberDiff line numberDiff line change
@@ -10,86 +10,125 @@ import Domain
1010
import MockData
1111
import Kingfisher
1212
import Flow
13+
import Common
1314

1415
public struct RapidExerciseDetailView: View {
1516

16-
let data: RapidExercisePresentation
17-
@State private var showsSecondaryMuscles: Bool = false
17+
@StateObject var viewModel: RapidExerciseDetailViewModel
1818

19-
public init(exercise: Domain.RapidExerciseDomain) {
20-
data = .init(domain: exercise)
19+
public init(
20+
exercise: Domain.RapidExerciseDomain,
21+
exerciseRepository: ExerciseRepository
22+
) {
23+
_viewModel = .init(wrappedValue: .init(
24+
exercise: exercise,
25+
exerciseRepository: exerciseRepository
26+
))
2127
}
2228

2329
public var body: some View {
2430
ScrollView {
25-
26-
KFAnimatedImage(.init(string: data.gifUrl))
27-
31+
KFAnimatedImage(.init(string: viewModel.data.gifUrl))
2832
VStack(alignment: .leading) {
29-
Text(data.equipment.capitalized)
33+
Text(viewModel.data.equipment.capitalized)
3034
.fontWeight(.black)
31-
3235
Divider()
33-
34-
HStack {
35-
Text("Body Part:")
36-
.foregroundStyle(Color(uiColor: .secondaryLabel))
37-
.italic()
38-
Text("\(data.bodyPart.rawValue.capitalized)(\(data.target.capitalized))")
39-
}
40-
36+
bodyPartsView
4137
Spacer(minLength: 8)
42-
43-
Section {
44-
if showsSecondaryMuscles {
45-
HFlow {
46-
ForEach(data.secondaryMuscles, id: \.self) { secondaryMuscle in
47-
Text(secondaryMuscle)
48-
.padding(.vertical, 4)
49-
.padding(.horizontal, 8)
50-
.background(
51-
RoundedRectangle(cornerRadius: 8)
52-
.fill(Color(uiColor: .secondarySystemGroupedBackground))
53-
)
54-
}
55-
}
56-
}
57-
} header: {
58-
Button {
59-
showsSecondaryMuscles.toggle()
60-
} label: {
61-
HStack {
62-
Text("secondary muscles".capitalized)
63-
}
64-
}
65-
}
66-
38+
secondayMusclesView
6739
Spacer(minLength: 12)
68-
69-
7040
Text("Instructions")
7141
.font(.title)
7242
.padding(.bottom, 8)
73-
74-
75-
ForEach(Array(zip(data.instructions.indices, data.instructions)), id: \.0) { (index, instruction) in
76-
HStack(alignment: .top) {
77-
Text("\(index + 1). ")
78-
Text(instruction)
79-
.fixedSize(horizontal: false, vertical: true)
80-
}
81-
.padding(.bottom, 8)
82-
}
83-
84-
43+
instructionsView
8544
}
8645
.padding(.horizontal)
8746

8847
Spacer(minLength: 60)
8948
}
90-
.animation(.default, value: showsSecondaryMuscles)
91-
.navigationTitle(data.name.capitalized)
49+
.animation(.default, value: viewModel.showsSecondaryMuscles)
50+
.navigationTitle(viewModel.data.name.capitalized)
9251
.scrollIndicators(.hidden)
52+
.overlay {
53+
ZStack {
54+
if viewModel.loading {
55+
ProgressView()
56+
}
57+
58+
if viewModel.showsAddButton {
59+
VStack {
60+
Spacer()
61+
62+
HStack {
63+
Spacer()
64+
Button {
65+
viewModel.add()
66+
} label: {
67+
Image(systemName: "pencil.tip.crop.circle.badge.plus")
68+
.padding()
69+
.background {
70+
Circle()
71+
.fill(.thickMaterial)
72+
.shadow(radius: 10)
73+
}
74+
}
75+
.buttonStyle(.plain)
76+
.padding()
77+
}
78+
}
79+
}
80+
81+
if viewModel.snackbarMessage != nil {
82+
Common.SnackbarView(message: $viewModel.snackbarMessage)
83+
}
84+
}
85+
}
86+
}
87+
88+
var bodyPartsView: some View {
89+
HStack {
90+
Text("Body Part:")
91+
.foregroundStyle(Color(uiColor: .secondaryLabel))
92+
.italic()
93+
Text("\(viewModel.data.bodyPart.rawValue.capitalized)(\(viewModel.data.target.capitalized))")
94+
}
95+
}
96+
97+
var secondayMusclesView: some View {
98+
Section {
99+
if viewModel.showsSecondaryMuscles {
100+
HFlow {
101+
ForEach(viewModel.data.secondaryMuscles, id: \.self) { secondaryMuscle in
102+
Text(secondaryMuscle)
103+
.padding(.vertical, 4)
104+
.padding(.horizontal, 8)
105+
.background(
106+
RoundedRectangle(cornerRadius: 8)
107+
.fill(Color(uiColor: .secondarySystemGroupedBackground))
108+
)
109+
}
110+
}
111+
}
112+
} header: {
113+
Button {
114+
viewModel.showsSecondaryMuscles.toggle()
115+
} label: {
116+
HStack {
117+
Text("secondary muscles".capitalized)
118+
}
119+
}
120+
}
121+
}
122+
123+
var instructionsView: some View {
124+
ForEach(Array(zip(viewModel.data.instructions.indices, viewModel.data.instructions)), id: \.0) { (index, instruction) in
125+
HStack(alignment: .top) {
126+
Text("\(index + 1). ")
127+
Text(instruction)
128+
.fixedSize(horizontal: false, vertical: true)
129+
}
130+
.padding(.bottom, 8)
131+
}
93132
}
94133
}
95134

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

100139
return NavigationStack {
101-
RapidExerciseDetailView(exercise: repository.get()[0])
140+
RapidExerciseDetailView(
141+
exercise: repository.get()[0],
142+
exerciseRepository: ExerciseRepositoryMock()
143+
)
102144
.preferredColorScheme(.dark)
103145
}
104146
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// RapidExerciseDetailViewModel.swift
3+
// Weight
4+
//
5+
// Created by Happymoonday on 8/13/24.
6+
//
7+
8+
import Foundation
9+
import Combine
10+
import Domain
11+
12+
final class RapidExerciseDetailViewModel: ObservableObject {
13+
let data: RapidExercisePresentation
14+
@Published var showsSecondaryMuscles: Bool = false
15+
@Published var showsAddButton: Bool = false
16+
@Published var snackbarMessage: String?
17+
@Published var loading: Bool = false
18+
19+
private let checkAddableExerciseUsecase: CheckAddableExerciseUsecase
20+
private let registerRapidExerciseUsecase: RegisterRapidExerciseUsecase
21+
22+
init(
23+
exercise: Domain.RapidExerciseDomain,
24+
exerciseRepository: ExerciseRepository
25+
) {
26+
data = .init(domain: exercise)
27+
28+
checkAddableExerciseUsecase = .init()
29+
registerRapidExerciseUsecase = .init(exerciseRepository: exerciseRepository)
30+
31+
showsAddButton = checkAddableExerciseUsecase.implement(exercise: data.domain)
32+
}
33+
34+
@MainActor
35+
func add() {
36+
Task {
37+
guard loading == false else { return }
38+
loading = true
39+
do {
40+
try await registerRapidExerciseUsecase.implement(exercise: data.domain)
41+
snackbarMessage = "Exercise Registered!"
42+
} catch {
43+
snackbarMessage = error.localizedDescription
44+
}
45+
loading = false
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)