Skip to content

Commit 4f651e8

Browse files
committed
Add Dexcom settings export and import functionality with persistent iCloud support
1 parent e0ca22c commit 4f651e8

File tree

5 files changed

+328
-24
lines changed

5 files changed

+328
-24
lines changed

LoopFollow/Loop Follow.entitlements

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
<string>development</string>
77
<key>com.apple.developer.aps-environment</key>
88
<string>development</string>
9+
<key>com.apple.developer.icloud-container-identifiers</key>
10+
<array>
11+
<string>iCloud.$(CFBundleIdentifier)</string>
12+
</array>
13+
<key>com.apple.developer.icloud-services</key>
14+
<array>
15+
<string>CloudDocuments</string>
16+
<string>CloudKit</string>
17+
</array>
18+
<key>com.apple.developer.ubiquity-container-identifiers</key>
19+
<array>
20+
<string>iCloud.$(CFBundleIdentifier)</string>
21+
</array>
922
<key>com.apple.security.app-sandbox</key>
1023
<true/>
1124
<key>com.apple.security.device.bluetooth</key>

LoopFollow/Settings/ImportExport/ExportableSettings.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,45 @@ struct NightscoutSettingsExport: Codable {
4343
}
4444
}
4545

46+
// MARK: - Dexcom Settings Export
47+
48+
struct DexcomSettingsExport: Codable {
49+
let version: String
50+
let userName: String
51+
let password: String
52+
let server: String
53+
54+
static func fromCurrentStorage() -> DexcomSettingsExport {
55+
let storage = Storage.shared
56+
return DexcomSettingsExport(
57+
version: AppVersionManager().version(),
58+
userName: storage.shareUserName.value,
59+
password: storage.sharePassword.value,
60+
server: storage.shareServer.value
61+
)
62+
}
63+
64+
func applyToStorage() {
65+
let storage = Storage.shared
66+
storage.shareUserName.value = userName
67+
storage.sharePassword.value = password
68+
storage.shareServer.value = server
69+
}
70+
71+
func encodeToJSON() -> String? {
72+
do {
73+
let data = try JSONEncoder().encode(self)
74+
return String(data: data, encoding: .utf8)
75+
} catch {
76+
return nil
77+
}
78+
}
79+
80+
func hasValidSettings() -> Bool {
81+
return !userName.isEmpty && !password.isEmpty
82+
}
83+
}
84+
4685
// MARK: - Alarm Settings Export
4786

4887
struct AlarmSettingsExport: Codable {
@@ -279,19 +318,22 @@ struct CombinedSettingsExport: Codable {
279318
let version: String
280319
let appVersion: String
281320
let nightscout: NightscoutSettingsExport?
321+
let dexcom: DexcomSettingsExport?
282322
let remote: RemoteSettingsExport?
283323
let alarms: AlarmSettingsExport?
284324
let exportType: String
285325
let timestamp: Date
286326

287327
init(nightscout: NightscoutSettingsExport? = nil,
328+
dexcom: DexcomSettingsExport? = nil,
288329
remote: RemoteSettingsExport? = nil,
289330
alarms: AlarmSettingsExport? = nil,
290331
exportType: String)
291332
{
292333
version = "1.0"
293334
appVersion = AppVersionManager().version()
294335
self.nightscout = nightscout
336+
self.dexcom = dexcom
295337
self.remote = remote
296338
self.alarms = alarms
297339
self.exportType = exportType

LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,168 @@ struct ImportExportSettingsView: View {
148148
.sheet(isPresented: $viewModel.showImportConfirmation) {
149149
ImportConfirmationView(viewModel: viewModel)
150150
}
151+
.sheet(isPresented: $viewModel.showExportSuccessAlert) {
152+
ExportSuccessView(viewModel: viewModel)
153+
}
154+
.sheet(isPresented: $viewModel.showImportNotFoundAlert) {
155+
ImportNotFoundView(viewModel: viewModel)
156+
}
157+
}
158+
}
159+
160+
struct ImportNotFoundView: View {
161+
@ObservedObject var viewModel: ImportExportSettingsViewModel
162+
163+
var body: some View {
164+
NavigationView {
165+
VStack(spacing: 20) {
166+
// Header
167+
VStack(spacing: 12) {
168+
Image(systemName: "icloud.slash")
169+
.font(.system(size: 60))
170+
.foregroundColor(.orange)
171+
172+
Text("No Settings Found")
173+
.font(.title2)
174+
.fontWeight(.semibold)
175+
176+
Text(viewModel.importNotFoundMessage)
177+
.font(.subheadline)
178+
.foregroundColor(.secondary)
179+
.multilineTextAlignment(.center)
180+
.padding(.horizontal)
181+
}
182+
.padding(.top, 30)
183+
184+
Spacer()
185+
186+
// Done Button
187+
Button(action: {
188+
viewModel.showImportNotFoundAlert = false
189+
}) {
190+
HStack {
191+
Image(systemName: "xmark")
192+
Text("OK")
193+
}
194+
.font(.headline)
195+
.foregroundColor(.white)
196+
.frame(maxWidth: .infinity)
197+
.padding()
198+
.background(Color.blue)
199+
.cornerRadius(12)
200+
}
201+
.padding(.horizontal)
202+
.padding(.bottom, 20)
203+
}
204+
.navigationBarHidden(true)
205+
}
206+
}
207+
}
208+
209+
struct ExportSuccessView: View {
210+
@ObservedObject var viewModel: ImportExportSettingsViewModel
211+
212+
var body: some View {
213+
NavigationView {
214+
VStack(spacing: 20) {
215+
// Header
216+
VStack(spacing: 12) {
217+
Image(systemName: "checkmark.icloud.fill")
218+
.font(.system(size: 60))
219+
.foregroundColor(.green)
220+
221+
Text("Export Successful")
222+
.font(.title2)
223+
.fontWeight(.semibold)
224+
225+
Text(viewModel.exportSuccessMessage)
226+
.font(.subheadline)
227+
.foregroundColor(.secondary)
228+
.multilineTextAlignment(.center)
229+
}
230+
.padding(.top, 30)
231+
232+
// Exported Settings Details
233+
if !viewModel.exportSuccessDetails.isEmpty {
234+
VStack(alignment: .leading, spacing: 16) {
235+
Text("Exported Settings")
236+
.font(.headline)
237+
.padding(.horizontal)
238+
239+
VStack(spacing: 12) {
240+
ForEach(viewModel.exportSuccessDetails, id: \.self) { detail in
241+
HStack(spacing: 12) {
242+
Image(systemName: iconForDetail(detail))
243+
.font(.title2)
244+
.foregroundColor(colorForDetail(detail))
245+
.frame(width: 30)
246+
247+
Text(detail)
248+
.font(.subheadline)
249+
.lineLimit(2)
250+
251+
Spacer()
252+
253+
Image(systemName: "checkmark.circle.fill")
254+
.foregroundColor(.green)
255+
}
256+
.padding()
257+
.background(Color(.systemGray6))
258+
.cornerRadius(10)
259+
}
260+
}
261+
.padding(.horizontal)
262+
}
263+
}
264+
265+
Spacer()
266+
267+
// Done Button
268+
Button(action: {
269+
viewModel.showExportSuccessAlert = false
270+
}) {
271+
HStack {
272+
Image(systemName: "checkmark")
273+
Text("Done")
274+
}
275+
.font(.headline)
276+
.foregroundColor(.white)
277+
.frame(maxWidth: .infinity)
278+
.padding()
279+
.background(Color.blue)
280+
.cornerRadius(12)
281+
}
282+
.padding(.horizontal)
283+
.padding(.bottom, 20)
284+
}
285+
.navigationBarHidden(true)
286+
}
287+
}
288+
289+
private func iconForDetail(_ detail: String) -> String {
290+
if detail.lowercased().contains("nightscout") {
291+
return "network"
292+
} else if detail.lowercased().contains("dexcom") {
293+
return "person.circle"
294+
} else if detail.lowercased().contains("remote") {
295+
return "antenna.radiowaves.left.and.right"
296+
} else if detail.lowercased().contains("alarm") {
297+
return "bell"
298+
}
299+
return "gear"
300+
}
301+
302+
private func colorForDetail(_ detail: String) -> Color {
303+
if detail.lowercased().contains("nightscout") {
304+
return .blue
305+
} else if detail.lowercased().contains("dexcom") {
306+
return .green
307+
} else if detail.lowercased().contains("remote") {
308+
return .orange
309+
} else if detail.lowercased().contains("alarm") {
310+
return .red
311+
}
312+
return .gray
151313
}
152314
}
153315

0 commit comments

Comments
 (0)