-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathWidgetsViewModel.swift
More file actions
323 lines (277 loc) · 10.6 KB
/
WidgetsViewModel.swift
File metadata and controls
323 lines (277 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
import Foundation
import SwiftUI
// MARK: - Widget Options Protocol
protocol WidgetOptionsProtocol: Codable, Equatable {
static var defaultOptions: Self { get }
}
// MARK: - Widget Options Types
/// Default options for each widget type
func getDefaultOptions(for type: WidgetType) -> Any {
switch type {
case .suggestions, .calculator:
return EmptyWidgetOptions()
case .blocks:
return BlocksWidgetOptions()
case .facts:
return FactsWidgetOptions()
case .news:
return NewsWidgetOptions()
case .weather:
return WeatherWidgetOptions()
case .price:
return PriceWidgetOptions()
}
}
/// Empty options for widgets that don't have customization yet
struct EmptyWidgetOptions: Codable, Equatable {}
// MARK: - Widget Metadata
struct WidgetMetadata {
let name: String
let description: String
let icon: String
init(type: WidgetType, fiatSymbol: String = "$") {
name = t("widgets__\(type.rawValue)__name")
description = t("widgets__\(type.rawValue)__description", variables: ["fiatSymbol": fiatSymbol])
icon = "\(type.rawValue)-widget"
}
}
// MARK: - Widget Models
struct Widget: Identifiable {
let type: WidgetType
/// Use type as identifier since only one widget per type is allowed
var id: WidgetType {
type
}
/// Widget metadata computed on demand
func metadata(fiatSymbol: String = "$") -> WidgetMetadata {
return WidgetMetadata(type: type, fiatSymbol: fiatSymbol)
}
@MainActor
@ViewBuilder
func view(widgetsViewModel: WidgetsViewModel, isEditing: Bool, onEditingEnd: (() -> Void)? = nil, isPreview: Bool = false) -> some View {
switch type {
case .suggestions:
SuggestionsWidget(isEditing: isEditing, onEditingEnd: onEditingEnd, isPreview: isPreview)
case .blocks:
BlocksWidget(
options: widgetsViewModel.getOptions(for: type, as: BlocksWidgetOptions.self),
isEditing: isEditing,
onEditingEnd: onEditingEnd
)
case .calculator:
CalculatorWidget(isEditing: isEditing, onEditingEnd: onEditingEnd)
case .facts:
FactsWidget(
options: widgetsViewModel.getOptions(for: type, as: FactsWidgetOptions.self),
isEditing: isEditing,
onEditingEnd: onEditingEnd
)
case .news:
NewsWidget(
options: widgetsViewModel.getOptions(for: type, as: NewsWidgetOptions.self),
isEditing: isEditing,
onEditingEnd: onEditingEnd
)
case .price:
PriceWidget(
options: widgetsViewModel.getOptions(for: type, as: PriceWidgetOptions.self),
isEditing: isEditing,
onEditingEnd: onEditingEnd
)
case .weather:
WeatherWidget(
options: widgetsViewModel.getOptions(for: type, as: WeatherWidgetOptions.self),
isEditing: isEditing,
onEditingEnd: onEditingEnd
)
}
}
}
/// Saved widget with options
struct SavedWidget: Codable, Identifiable {
let type: WidgetType
let optionsData: Data?
/// Use type as identifier since only one widget per type is allowed
var id: WidgetType {
type
}
init(type: WidgetType, optionsData: Data? = nil) {
self.type = type
self.optionsData = optionsData
}
/// Convert to Widget for UI
func toWidget() -> Widget {
return Widget(type: type)
}
}
/// Placeholder widget for unimplemented widgets
struct PlaceholderWidget: View {
let type: WidgetType
let message: String
var body: some View {
VStack {
Text("Widget Preview")
.foregroundColor(.textSecondary)
Text(message)
.foregroundColor(.textSecondary)
.font(.caption)
}
.frame(maxWidth: .infinity)
.frame(height: 120)
.background(Color.white10)
.cornerRadius(16)
}
}
// MARK: - Widget Types
enum WidgetType: String, CaseIterable, Codable {
case price
case news
case blocks
case facts
case weather
case calculator
case suggestions
}
// MARK: - WidgetsViewModel
@MainActor
class WidgetsViewModel: ObservableObject {
@Published var savedWidgets: [Widget] = []
private static let savedWidgetsKey = "savedWidgets"
/// In-memory storage for saved widgets with options
private var savedWidgetsWithOptions: [SavedWidget] = []
/// Default widgets for new installs and resets
private static let defaultSavedWidgets: [SavedWidget] = [
SavedWidget(type: .suggestions),
SavedWidget(type: .price),
SavedWidget(type: .blocks),
]
init() {
loadSavedWidgets()
}
// MARK: - Public Methods
/// Check if a widget type is already saved
func isWidgetSaved(_ type: WidgetType) -> Bool {
return savedWidgets.contains { $0.type == type }
}
/// Save a new widget
func saveWidget(_ type: WidgetType) {
// Don't add duplicates
guard !isWidgetSaved(type) else { return }
let newSavedWidget = SavedWidget(type: type)
savedWidgetsWithOptions.append(newSavedWidget)
savedWidgets.append(newSavedWidget.toWidget())
persistSavedWidgets()
}
/// Delete a widget
func deleteWidget(_ type: WidgetType) {
savedWidgetsWithOptions.removeAll { $0.type == type }
savedWidgets.removeAll { $0.type == type }
persistSavedWidgets()
}
/// Reorder the widgets list by moving one widget to a new index.
func reorderWidgets(from sourceIndex: Int, to destinationIndex: Int) {
guard sourceIndex != destinationIndex,
sourceIndex >= 0, sourceIndex < savedWidgetsWithOptions.count,
destinationIndex >= 0, destinationIndex < savedWidgetsWithOptions.count
else { return }
let moved = savedWidgetsWithOptions.remove(at: sourceIndex)
savedWidgetsWithOptions.insert(moved, at: destinationIndex)
savedWidgets = savedWidgetsWithOptions.map { $0.toWidget() }
persistSavedWidgets()
}
/// Clear all persisted widgets and restore defaults
func clearWidgets() {
savedWidgetsWithOptions = WidgetsViewModel.defaultSavedWidgets
savedWidgets = savedWidgetsWithOptions.map { $0.toWidget() }
persistSavedWidgets()
}
// MARK: - Widget Options Methods
/// Get options for a specific widget type
func getOptions<T: Codable>(for type: WidgetType, as optionsType: T.Type) -> T {
// Find the saved widget with this type
if let savedWidget = savedWidgetsWithOptions.first(where: { $0.type == type }),
let optionsData = savedWidget.optionsData,
let options = try? JSONDecoder().decode(optionsType, from: optionsData)
{
return options
}
// Return default options if none saved
return getDefaultOptions(for: type) as! T
}
/// Save options for a specific widget type
func saveOptions(_ options: some Codable, for type: WidgetType) {
do {
let optionsData = try JSONEncoder().encode(options)
// Find existing saved widget or create new one
if let index = savedWidgetsWithOptions.firstIndex(where: { $0.type == type }) {
// Update existing widget with new options
savedWidgetsWithOptions[index] = SavedWidget(
type: type,
optionsData: optionsData
)
} else {
// Create new saved widget with options
savedWidgetsWithOptions.append(SavedWidget(type: type, optionsData: optionsData))
}
persistSavedWidgets()
} catch {
print("Failed to save widget options: \(error)")
}
}
/// Check if widget has custom options (different from default)
func hasCustomOptions(for type: WidgetType) -> Bool {
switch type {
case .suggestions, .calculator:
return false
case .blocks:
let current: BlocksWidgetOptions = getOptions(for: type, as: BlocksWidgetOptions.self)
let defaultOptions = BlocksWidgetOptions()
return current != defaultOptions
case .facts:
let current: FactsWidgetOptions = getOptions(for: type, as: FactsWidgetOptions.self)
let defaultOptions = FactsWidgetOptions()
return current != defaultOptions
case .news:
let current: NewsWidgetOptions = getOptions(for: type, as: NewsWidgetOptions.self)
let defaultOptions = NewsWidgetOptions()
return current != defaultOptions
case .weather:
let current: WeatherWidgetOptions = getOptions(for: type, as: WeatherWidgetOptions.self)
let defaultOptions = WeatherWidgetOptions()
return current != defaultOptions
case .price:
let current: PriceWidgetOptions = getOptions(for: type, as: PriceWidgetOptions.self)
let defaultOptions = PriceWidgetOptions()
return current != defaultOptions
}
}
// MARK: - Private Methods
func loadSavedWidgets() {
let widgetsData = UserDefaults.standard.data(forKey: Self.savedWidgetsKey) ?? .init()
do {
savedWidgetsWithOptions = try JSONDecoder().decode([SavedWidget].self, from: widgetsData)
savedWidgets = savedWidgetsWithOptions.map { $0.toWidget() }
} catch {
// If no saved data or decode fails, start with default widgets
savedWidgetsWithOptions = WidgetsViewModel.defaultSavedWidgets
savedWidgets = savedWidgetsWithOptions.map { $0.toWidget() }
persistSavedWidgets()
}
syncPriceOptionsToHomeScreenWidget()
}
private func persistSavedWidgets() {
do {
let encodedData = try JSONEncoder().encode(savedWidgetsWithOptions)
UserDefaults.standard.set(encodedData, forKey: Self.savedWidgetsKey)
} catch {
print("Failed to persist widgets: \(error)")
}
syncPriceOptionsToHomeScreenWidget()
}
/// Keeps the home-screen WidgetKit price widget in sync with in-app price widget options (App Group).
private func syncPriceOptionsToHomeScreenWidget() {
let options: PriceWidgetOptions = getOptions(for: .price, as: PriceWidgetOptions.self)
PriceHomeScreenWidgetOptionsStore.save(options)
PriceHomeScreenWidgetOptionsStore.reloadHomeScreenWidgetIfNeeded()
}
}