Skip to content

Commit 1eddd6f

Browse files
committed
Refactor: Implement Dependency Injection via EnvironmentObject
1 parent bc75e26 commit 1eddd6f

14 files changed

Lines changed: 163 additions & 53 deletions

Floreboard/App/ContentView.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import SwiftUI
22

33
struct ContentView: View {
4-
@StateObject private var authService = AuthService.shared
5-
@StateObject private var localizationManager = LocalizationManager.shared
4+
@EnvironmentObject var authService: AuthService
5+
@EnvironmentObject var localizationManager: LocalizationManager
6+
@Environment(\.hapticManager) var hapticManager
67
@State private var selection = 0
78

89
var body: some View {
@@ -22,7 +23,7 @@ struct ContentView: View {
2223
.tag(1)
2324

2425
HistoryView(onStartDesign: {
25-
HapticManager.shared.impact(style: .light)
26+
hapticManager.impact(style: .light)
2627
selection = 3
2728
})
2829
.tabItem {

Floreboard/App/FloreboardApp.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ struct FloreboardApp: App {
3131
.modelContainer(container)
3232
.environmentObject(AuthService.shared)
3333
.environmentObject(InventoryService.shared)
34+
.environmentObject(HistoryService.shared)
35+
.environmentObject(LocalizationManager.shared)
3436
}
3537
}
3638
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import SwiftUI
2+
3+
// MARK: - AIService Environment Key
4+
5+
private struct AIServiceKey: EnvironmentKey {
6+
static let defaultValue = AIService.shared
7+
}
8+
9+
extension EnvironmentValues {
10+
var aiService: AIService {
11+
get { self[AIServiceKey.self] }
12+
set { self[AIServiceKey.self] = newValue }
13+
}
14+
}
15+
16+
// MARK: - ImagePersistence Environment Key
17+
18+
private struct ImagePersistenceKey: EnvironmentKey {
19+
static let defaultValue = ImagePersistence.shared
20+
}
21+
22+
extension EnvironmentValues {
23+
var imagePersistence: ImagePersistence {
24+
get { self[ImagePersistenceKey.self] }
25+
set { self[ImagePersistenceKey.self] = newValue }
26+
}
27+
}
28+
29+
// MARK: - HapticManager Environment Key
30+
31+
private struct HapticManagerKey: EnvironmentKey {
32+
static let defaultValue = HapticManager.shared
33+
}
34+
35+
extension EnvironmentValues {
36+
var hapticManager: HapticManager {
37+
get { self[HapticManagerKey.self] }
38+
set { self[HapticManagerKey.self] = newValue }
39+
}
40+
}

Floreboard/Features/Auth/LoginView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import SwiftUI
22

33
struct LoginView: View {
4-
@StateObject private var auth = AuthService.shared
4+
@EnvironmentObject var auth: AuthService
55
@State private var storeName = ""
66
@State private var password = ""
77

Floreboard/Features/Design/DesignMainView.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ struct AlertItem: Identifiable {
77
}
88

99
struct DesignMainView: View {
10+
@Environment(\.aiService) var aiService
11+
@Environment(\.imagePersistence) var imagePersistence
12+
@Environment(\.hapticManager) var hapticManager
13+
@EnvironmentObject var inventoryService: InventoryService
14+
@EnvironmentObject var historyService: HistoryService
15+
@EnvironmentObject var loc: LocalizationManager
1016
@StateObject private var viewModel = DesignViewModel()
11-
@ObservedObject private var loc = LocalizationManager.shared
1217
@State private var pickerItem: PhotosPickerItem? = nil
1318

1419
var body: some View {
@@ -32,7 +37,7 @@ struct DesignMainView: View {
3237
}
3338
.pickerStyle(SegmentedPickerStyle())
3439
.onChange(of: viewModel.isProfessionalMode) { _, _ in
35-
HapticManager.shared.impact(style: .medium)
40+
hapticManager.impact(style: .medium)
3641
}
3742
}
3843
.padding()
@@ -67,11 +72,20 @@ struct DesignMainView: View {
6772
isEnabled: !viewModel.isLoading
6873
) {
6974
hideKeyboard()
70-
HapticManager.shared.notification(type: .success)
75+
hapticManager.notification(type: .success)
7176
viewModel.generateDesign()
7277
}
7378
}
7479
.keyboardDismissToolbar()
80+
.onAppear {
81+
viewModel.setup(
82+
inventoryService: inventoryService,
83+
aiService: aiService,
84+
imagePersistence: imagePersistence,
85+
historyService: historyService,
86+
localizationManager: loc
87+
)
88+
}
7589
// Loading Overlay
7690
.overlay {
7791
if viewModel.isLoading {

Floreboard/Features/Design/DesignViewModel.swift

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ class DesignViewModel: ObservableObject {
2020
@Published var loadingStep: Int = 0
2121
@Published var loadingStatus: String = ""
2222

23+
private var inventoryService: InventoryService?
24+
private var aiService: AIService?
25+
private var imagePersistence: ImagePersistence?
26+
private var historyService: HistoryService?
27+
private var localizationManager: LocalizationManager?
28+
29+
init() {}
30+
31+
func setup(
32+
inventoryService: InventoryService,
33+
aiService: AIService,
34+
imagePersistence: ImagePersistence,
35+
historyService: HistoryService,
36+
localizationManager: LocalizationManager
37+
) {
38+
guard self.inventoryService == nil else { return }
39+
self.inventoryService = inventoryService
40+
self.aiService = aiService
41+
self.imagePersistence = imagePersistence
42+
self.historyService = historyService
43+
self.localizationManager = localizationManager
44+
}
45+
2346
enum CultureFilter: String, CaseIterable, Identifiable {
2447
case all = "All"
2548
case japanese = "Japanese"
@@ -57,11 +80,11 @@ class DesignViewModel: ObservableObject {
5780
}
5881

5982
func generateDesign() {
60-
guard !isLoading else { return }
83+
guard !isLoading, let aiService = aiService, let inventoryService = inventoryService, let localizationManager = localizationManager, let historyService = historyService, let imagePersistence = imagePersistence else { return }
6184

6285
isLoading = true
6386
loadingStep = 0
64-
loadingStatus = Tx.t("design.loading.analyze") // "Analyzing Request..."
87+
loadingStatus = localizationManager.t("design.loading.analyze") // "Analyzing Request..."
6588
errorMessage = nil
6689

6790
Task {
@@ -70,27 +93,27 @@ class DesignViewModel: ObservableObject {
7093
try await Task.sleep(nanoseconds: 800_000_000)
7194
await MainActor.run {
7295
self.loadingStep = 1
73-
self.loadingStatus = Tx.t("design.loading.technique")
96+
self.loadingStatus = localizationManager.t("design.loading.technique")
7497
} // "Selecting Technique..."
7598

7699
try await Task.sleep(nanoseconds: 800_000_000)
77100
await MainActor.run {
78101
self.loadingStep = 2
79-
self.loadingStatus = Tx.t("design.loading.match")
102+
self.loadingStatus = localizationManager.t("design.loading.match")
80103
} // "Matching Inventory..."
81104

82105
try await Task.sleep(nanoseconds: 800_000_000)
83106
await MainActor.run {
84107
self.loadingStep = 3
85-
self.loadingStatus = Tx.t("design.loading.generate")
108+
self.loadingStatus = localizationManager.t("design.loading.generate")
86109
} // "Generating Design..."
87110

88-
let inventory = InventoryService.shared.flowers
111+
let inventory = inventoryService.flowers
89112
var result: DesignResult
90113

91114
if let image = selectedImage {
92115
// Visual Muse Mode
93-
result = try await AIService.shared.generateDesignFromImage(
116+
result = try await aiService.generateDesignFromImage(
94117
image: image, request: request, inventory: inventory)
95118
} else {
96119
// Standard Mode
@@ -99,19 +122,19 @@ class DesignViewModel: ObservableObject {
99122
if isProfessionalMode {
100123
currentRequest.designMode = "professional"
101124
}
102-
result = try await AIService.shared.generateFlowerPlan(
125+
result = try await aiService.generateFlowerPlan(
103126
request: currentRequest, inventory: inventory)
104127
}
105128

106129
// Image Generation Step
107130
if let prompt = result.imagePrompt, !prompt.isEmpty {
108131
await MainActor.run {
109-
self.loadingStatus = Tx.t("design.loading.dreaming") // "Dreaming up visual..."
132+
self.loadingStatus = localizationManager.t("design.loading.dreaming") // "Dreaming up visual..."
110133
}
111134

112135
do {
113136
// 1. Generate Image URL (or Base64)
114-
let imageUrlString = try await AIService.shared.generateImage(
137+
let imageUrlString = try await aiService.generateImage(
115138
prompt: prompt,
116139
requestId: result.syncId ?? result.requestId
117140
)
@@ -121,23 +144,23 @@ class DesignViewModel: ObservableObject {
121144

122145
// 3. Save to Persistence
123146
if let validImage = image {
124-
if let filename = ImagePersistence.shared.saveImage(validImage, name: result.id) {
147+
if let filename = imagePersistence.saveImage(validImage, name: result.id) {
125148
result.imageUrl = filename
126149
} else {
127150
AppLogger.image.error("Failed to save image to disk")
128-
result.imageError = Tx.t("error.saveImage")
151+
result.imageError = localizationManager.t("error.saveImage")
129152
}
130153
} else {
131154
AppLogger.image.error("Failed to decode image from string: \(imageUrlString.prefix(100))...")
132-
result.imageError = Tx.t("error.invalidImageData")
155+
result.imageError = localizationManager.t("error.invalidImageData")
133156
}
134157
} catch {
135158
AppLogger.ai.error("Image generation failed: \(error)")
136159
result.imageError = error.localizedDescription
137160
}
138161
} else if let selectedImg = selectedImage {
139162
// Visual Muse: Save input image as the design image
140-
if let filename = ImagePersistence.shared.saveImage(selectedImg, name: result.id) {
163+
if let filename = imagePersistence.saveImage(selectedImg, name: result.id) {
141164
result.imageUrl = filename
142165
}
143166
}
@@ -148,7 +171,7 @@ class DesignViewModel: ObservableObject {
148171
self.showResult = true
149172
self.isLoading = false
150173
// Save to History
151-
HistoryService.shared.saveDesign(finalizedResult)
174+
historyService.saveDesign(finalizedResult)
152175
}
153176

154177
} catch {

Floreboard/Features/History/HistoryDetailView.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import SwiftUI
22

33
struct DesignDetailView: View {
44
let design: DesignResult
5+
@EnvironmentObject var historyService: HistoryService
6+
@EnvironmentObject var inventoryService: InventoryService
7+
@Environment(\.imagePersistence) var imagePersistence
58
@State private var designImage: UIImage? = nil
69
@State private var isShowingFullScreen = false
710
@State private var displayedStatus: DesignStatus?
@@ -213,7 +216,7 @@ struct DesignDetailView: View {
213216
}
214217

215218
private func executeDesign() {
216-
let currentShortages = InventoryService.shared.stockShortages(for: design.flowerList)
219+
let currentShortages = inventoryService.stockShortages(for: design.flowerList)
217220
guard currentShortages.isEmpty else {
218221
shortages = currentShortages
219222
showStockWarning = true
@@ -224,14 +227,14 @@ struct DesignDetailView: View {
224227
}
225228

226229
private func commitExecution() {
227-
HistoryService.shared.executeDesign(design)
230+
historyService.executeDesign(design)
228231
displayedStatus = .completed
229232
}
230233

231234
private func loadDetailImageAsync() async {
232235
if let path = design.imageUrl, !path.hasPrefix("http") {
233-
let loaded = await Task.detached {
234-
ImagePersistence.shared.loadImage(named: path)
236+
let loaded = await Task.detached { [imagePersistence] in
237+
imagePersistence.loadImage(named: path)
235238
}.value
236239
self.designImage = loaded
237240
}

Floreboard/Features/History/HistoryListView.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import SwiftUI
22

33
struct HistoryView: View {
4-
@StateObject private var historyService = HistoryService.shared
4+
@EnvironmentObject var historyService: HistoryService
5+
@Environment(\.hapticManager) var hapticManager
56
@State private var searchText = ""
67
var onStartDesign: (() -> Void)? = nil
78

@@ -59,7 +60,7 @@ struct HistoryView: View {
5960
.padding(.top, 4)
6061
} else if !searchText.isEmpty {
6162
Button {
62-
HapticManager.shared.impact(style: .light)
63+
hapticManager.impact(style: .light)
6364
searchText = ""
6465
} label: {
6566
Label(Tx.t("history.search.clear"), systemImage: "xmark.circle")

Floreboard/Features/History/HistoryRowView.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import SwiftUI
22

33
struct HistoryRow: View {
44
let design: DesignResult
5+
@EnvironmentObject var historyService: HistoryService
6+
@Environment(\.imagePersistence) var imagePersistence
57
@State private var thumbnail: UIImage?
68

79
var body: some View {
@@ -66,7 +68,7 @@ struct HistoryRow: View {
6668
.glassmorphic()
6769
.contextMenu {
6870
Button(role: .destructive) {
69-
HistoryService.shared.deleteDesign(id: design.id)
71+
historyService.deleteDesign(id: design.id)
7072
} label: {
7173
Label(Tx.t("general.delete"), systemImage: "trash")
7274
}
@@ -75,8 +77,8 @@ struct HistoryRow: View {
7577

7678
func loadImageAsync() async {
7779
if let path = design.imageUrl, !path.hasPrefix("http") {
78-
let loaded = await Task.detached {
79-
ImagePersistence.shared.loadImage(named: path)
80+
let loaded = await Task.detached { [imagePersistence] in
81+
imagePersistence.loadImage(named: path)
8082
}.value
8183
self.thumbnail = loaded
8284
}

0 commit comments

Comments
 (0)