Skip to content

Commit e0ca22c

Browse files
committed
Add loading screen on startup
1 parent 624e2b8 commit e0ca22c

File tree

4 files changed

+210
-9
lines changed

4 files changed

+210
-9
lines changed

LoopFollow/Controllers/Nightscout/BGData.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ extension MainViewController {
254254
Observable.shared.deltaText.value = "+" + Localizer.toDisplayUnits(String(deltaBG))
255255
}
256256

257+
// Mark BG data as loaded for initial loading state
258+
self.markDataLoaded("bg")
259+
257260
// Update contact
258261
if Storage.shared.contactEnabled.value {
259262
self.contactImageUpdater

LoopFollow/Controllers/Nightscout/DeviceStatus.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ extension MainViewController {
203203
}
204204

205205
evaluateNotLooping()
206+
207+
// Mark device status as loaded for initial loading state
208+
markDataLoaded("deviceStatus")
209+
206210
LogManager.shared.log(category: .deviceStatus, message: "Update Device Status done", isDebug: true)
207211
}
208212
}

LoopFollow/Controllers/Nightscout/Profile.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ extension MainViewController {
2424
profileManager.loadProfile(from: profileData)
2525
infoManager.updateInfoData(type: .profile, value: profileData.defaultProfile)
2626

27+
// Mark profile data as loaded for initial loading state
28+
markDataLoaded("profile")
29+
2730
basalProfile.removeAll()
2831
for basalEntry in store.basal {
2932
let entry = basalProfileStruct(value: basalEntry.value, time: basalEntry.time, timeAsSeconds: basalEntry.timeAsSeconds)

LoopFollow/ViewControllers/MainViewController.swift

Lines changed: 200 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
122122

123123
private var cancellables = Set<AnyCancellable>()
124124

125+
// Loading state management
126+
private var loadingOverlay: UIView?
127+
private var isInitialLoad = true
128+
private var loadingStates: [String: Bool] = [
129+
"bg": false,
130+
"profile": false,
131+
"deviceStatus": false,
132+
]
133+
private var loadingTimeoutTimer: Timer?
134+
125135
override func viewDidLoad() {
126136
super.viewDidLoad()
127137

@@ -388,10 +398,116 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
388398

389399
speechSynthesizer.delegate = self
390400

391-
// Check if this is first-time setup and show import button
401+
// Check configuration and show appropriate UI
402+
if isDataSourceConfigured() {
403+
// Data source configured - show loading overlay
404+
setupLoadingState()
405+
showLoadingOverlay()
406+
} else {
407+
// No data source - hide all data UI and show setup buttons
408+
hideAllDataUI()
409+
isInitialLoad = false
410+
}
411+
392412
checkAndShowImportButtonIfNeeded()
393413
}
394414

415+
// MARK: - Loading Overlay
416+
417+
private func isDataSourceConfigured() -> Bool {
418+
let isNightscoutConfigured = !Storage.shared.url.value.isEmpty
419+
let isDexcomConfigured = !Storage.shared.shareUserName.value.isEmpty && !Storage.shared.sharePassword.value.isEmpty
420+
return isNightscoutConfigured || isDexcomConfigured
421+
}
422+
423+
private func setupLoadingState() {
424+
// If Nightscout is not enabled, mark profile and deviceStatus as loaded
425+
// since we only need BG data from Dexcom Share
426+
if !IsNightscoutEnabled() {
427+
loadingStates["profile"] = true
428+
loadingStates["deviceStatus"] = true
429+
}
430+
}
431+
432+
private func showLoadingOverlay() {
433+
guard loadingOverlay == nil else { return }
434+
435+
// Hide all data UI while loading
436+
hideAllDataUI()
437+
438+
let overlay = UIView(frame: view.bounds)
439+
overlay.backgroundColor = UIColor.systemBackground
440+
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
441+
442+
let activityIndicator = UIActivityIndicatorView(style: .large)
443+
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
444+
activityIndicator.startAnimating()
445+
446+
let loadingLabel = UILabel()
447+
loadingLabel.translatesAutoresizingMaskIntoConstraints = false
448+
loadingLabel.text = "Loading..."
449+
loadingLabel.textAlignment = .center
450+
loadingLabel.font = UIFont.systemFont(ofSize: 17, weight: .medium)
451+
loadingLabel.textColor = UIColor.secondaryLabel
452+
453+
overlay.addSubview(activityIndicator)
454+
overlay.addSubview(loadingLabel)
455+
456+
NSLayoutConstraint.activate([
457+
activityIndicator.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
458+
activityIndicator.centerYAnchor.constraint(equalTo: overlay.centerYAnchor, constant: -20),
459+
460+
loadingLabel.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
461+
loadingLabel.topAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 16),
462+
])
463+
464+
view.addSubview(overlay)
465+
loadingOverlay = overlay
466+
467+
// Set a timeout to hide the loading overlay if data takes too long
468+
loadingTimeoutTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
469+
guard let self = self else { return }
470+
if self.isInitialLoad {
471+
LogManager.shared.log(category: .general, message: "Loading timeout reached, hiding overlay")
472+
self.isInitialLoad = false
473+
self.hideLoadingOverlay()
474+
}
475+
}
476+
}
477+
478+
private func hideLoadingOverlay() {
479+
guard let overlay = loadingOverlay else { return }
480+
481+
// Cancel the timeout timer
482+
loadingTimeoutTimer?.invalidate()
483+
loadingTimeoutTimer = nil
484+
485+
// Show all data UI now that loading is complete
486+
showAllDataUI()
487+
488+
UIView.animate(withDuration: 0.3, animations: {
489+
overlay.alpha = 0
490+
}, completion: { _ in
491+
overlay.removeFromSuperview()
492+
self.loadingOverlay = nil
493+
})
494+
}
495+
496+
func markDataLoaded(_ key: String) {
497+
guard isInitialLoad else { return }
498+
499+
loadingStates[key] = true
500+
501+
// Check if all critical data is loaded
502+
let allLoaded = loadingStates.values.allSatisfy { $0 }
503+
if allLoaded {
504+
isInitialLoad = false
505+
DispatchQueue.main.async {
506+
self.hideLoadingOverlay()
507+
}
508+
}
509+
}
510+
395511
private func setupTabBar() {
396512
guard let tabBarController = tabBarController else { return }
397513

@@ -723,6 +839,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
723839
}
724840

725841
func showHideNSDetails() {
842+
if isInitialLoad || !isDataSourceConfigured() {
843+
return
844+
}
845+
726846
var isHidden = false
727847
if !IsNightscoutEnabled() {
728848
isHidden = true
@@ -978,17 +1098,23 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
9781098
// MARK: - First Time Setup
9791099

9801100
private func checkAndShowImportButtonIfNeeded() {
981-
// Check if this is first-time setup (no Nightscout URL configured AND no Dexcom configured)
982-
let isNightscoutConfigured = !Storage.shared.url.value.isEmpty && !Storage.shared.token.value.isEmpty
983-
let isDexcomConfigured = !Storage.shared.shareUserName.value.isEmpty && !Storage.shared.sharePassword.value.isEmpty
984-
let isFirstTimeSetup = !isNightscoutConfigured && !isDexcomConfigured
1101+
// Check if this is first-time setup (no data source configured)
1102+
let isFirstTimeSetup = !isDataSourceConfigured()
9851103

9861104
if isFirstTimeSetup {
9871105
setupFirstTimeButtons()
988-
hideGraphs()
1106+
hideAllDataUI()
1107+
// Hide loading overlay if it's showing and mark as not loading
1108+
if loadingOverlay != nil {
1109+
isInitialLoad = false
1110+
hideLoadingOverlay()
1111+
}
9891112
} else {
9901113
hideFirstTimeButtons()
991-
showGraphs()
1114+
// Only show data UI if we're not in initial loading state
1115+
if !isInitialLoad || loadingOverlay == nil {
1116+
showAllDataUI()
1117+
}
9921118
}
9931119
}
9941120

@@ -1105,8 +1231,55 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
11051231
updateGraphVisibility()
11061232
}
11071233

1234+
private func hideAllDataUI() {
1235+
// Hide graphs
1236+
BGChart.isHidden = true
1237+
BGChartFull.isHidden = true
1238+
1239+
// Hide BG display elements
1240+
BGText.isHidden = true
1241+
DeltaText.isHidden = true
1242+
DirectionText.isHidden = true
1243+
MinAgoText.isHidden = true
1244+
serverText.isHidden = true
1245+
1246+
// Hide info table and stats
1247+
infoTable.isHidden = true
1248+
statsView.isHidden = true
1249+
1250+
// Hide loop status and prediction
1251+
LoopStatusLabel.isHidden = true
1252+
PredictionLabel.isHidden = true
1253+
}
1254+
1255+
private func showAllDataUI() {
1256+
// Show BG display elements
1257+
BGText.isHidden = false
1258+
DeltaText.isHidden = false
1259+
DirectionText.isHidden = false
1260+
MinAgoText.isHidden = false
1261+
serverText.isHidden = false
1262+
1263+
// Show graphs based on settings
1264+
updateGraphVisibility()
1265+
1266+
// Show/hide info table and stats based on user settings
1267+
let isNightscoutEnabled = IsNightscoutEnabled()
1268+
if isNightscoutEnabled {
1269+
infoTable.isHidden = Storage.shared.hideInfoTable.value
1270+
LoopStatusLabel.isHidden = false
1271+
PredictionLabel.isHidden = IsNotLooping
1272+
} else {
1273+
infoTable.isHidden = true
1274+
LoopStatusLabel.isHidden = true
1275+
PredictionLabel.isHidden = true
1276+
}
1277+
1278+
statsView.isHidden = !Storage.shared.showStats.value
1279+
}
1280+
11081281
private func updateGraphVisibility() {
1109-
let isFirstTimeSetup = Storage.shared.url.value.isEmpty && Storage.shared.token.value.isEmpty
1282+
let isFirstTimeSetup = !isDataSourceConfigured()
11101283

11111284
if isFirstTimeSetup {
11121285
BGChart.isHidden = true
@@ -1130,7 +1303,25 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
11301303
}
11311304

11321305
@objc private func dismissModal() {
1133-
dismiss(animated: true)
1306+
dismiss(animated: true) { [weak self] in
1307+
guard let self = self else { return }
1308+
1309+
// Check if user just configured a data source
1310+
if self.isDataSourceConfigured(), self.loadingOverlay == nil {
1311+
// Reset loading states for fresh load
1312+
self.loadingStates = [
1313+
"bg": false,
1314+
"profile": false,
1315+
"deviceStatus": false,
1316+
]
1317+
self.isInitialLoad = true
1318+
1319+
// Show loading overlay and trigger refresh
1320+
self.setupLoadingState()
1321+
self.showLoadingOverlay()
1322+
self.refresh()
1323+
}
1324+
}
11341325
}
11351326
}
11361327

0 commit comments

Comments
 (0)