diff --git a/G7SensorKit/G7CGMManager/G7CGMManager.swift b/G7SensorKit/G7CGMManager/G7CGMManager.swift index 533a23b..a1055cd 100644 --- a/G7SensorKit/G7CGMManager/G7CGMManager.swift +++ b/G7SensorKit/G7CGMManager/G7CGMManager.swift @@ -599,25 +599,24 @@ extension G7CGMManager { /// cache window, and at launch it replays any past-due delayed alert as /// immediate until the user acknowledges it. A deleted CGM's scheduled /// expiry reminders would keep firing for weeks (LibreLoop #13). The - /// retractions are queued on the delegate queue ahead of the deletion - /// notification, so they land before Loop releases us. LoopKit's default - /// `delete` only notifies, so the notification is re-issued here. - public func delete(completion: @escaping () -> Void) { + /// retractions are awaited before the delegate is asked to delete us. + public func delete() async { cancelSuspectedSessionEndScan() // A manager passed its configuration lets go of the sensor and nothing more: the sensor's // lifecycle and records belong to the controller that passed it. - guard !state.configuredByAnotherController else { + if state.configuredByAnotherController { sensor.stopScanning() - retractAllLifecycleAlerts() - notifyDelegateOfDeletion(completion: completion) - return + } else { + signOutOfShare() + sensor.stopScanning() + recordSensorEndIfNeeded() + archiveCurrentSensor(reason: .deleted) } - signOutOfShare() - sensor.stopScanning() - retractAllLifecycleAlerts() - recordSensorEndIfNeeded() - archiveCurrentSensor(reason: .deleted) - notifyDelegateOfDeletion(completion: completion) + let delegate = cgmManagerDelegate + for alert in G7LifecycleAlert.allCases { + await delegate?.retractAlert(identifier: alert.identifier(managerIdentifier: pluginIdentifier)) + } + await delegate?.cgmManagerWantsDeletion(self) } // MARK: - Session events diff --git a/G7SensorKitTests/G7DeviceHandoffTests.swift b/G7SensorKitTests/G7DeviceHandoffTests.swift index 6d1baa0..da20605 100644 --- a/G7SensorKitTests/G7DeviceHandoffTests.swift +++ b/G7SensorKitTests/G7DeviceHandoffTests.swift @@ -138,11 +138,9 @@ final class G7DeviceHandoffTests: XCTestCase { XCTAssertNil(manager.state.suspectedSessionEndAt) } - func testDeletingAPassedConfigurationRecordsNoSensorEnd() { + func testDeletingAPassedConfigurationRecordsNoSensorEnd() async { let manager = makeManager(state: G7CGMManagerState.adopted(from: makeManager(state: phoneState).exportConfiguration().state)) - let deleted = expectation(description: "deleted") - manager.delete { deleted.fulfill() } - wait(for: [deleted], timeout: 2) + await manager.delete() XCTAssertNil(manager.state.sensorEndRecordedFor, "the sensor's lifecycle is the other controller's") XCTAssertNil(manager.state.previousSensor) XCTAssertEqual(manager.state.sensorID, sensorID) diff --git a/G7SensorKitTests/G7LifecycleAlertTests.swift b/G7SensorKitTests/G7LifecycleAlertTests.swift index 8196f51..7e38acf 100644 --- a/G7SensorKitTests/G7LifecycleAlertTests.swift +++ b/G7SensorKitTests/G7LifecycleAlertTests.swift @@ -256,18 +256,16 @@ class G7LifecycleAlertManagerTests: XCTestCase { /// replayed by Loop at every launch until acknowledged. Deleting must /// retract every alert this manager can issue, and must do so before the /// deletion notification that makes Loop release the manager. - func testDeletingTheCGMRetractsEveryAlertBeforeNotifying() { + func testDeletingTheCGMRetractsEveryAlertBeforeNotifying() async { var state = G7CGMManagerState() state.sensorID = "DXCM99" state.activatedAt = Date() state.lifecycleAlertsScheduledFor = "scheduled" let manager = makeManager(state: state) - let done = expectation(description: "delete completed") - manager.delete { done.fulfill() } - wait(for: [done], timeout: 2) - settle() + await manager.delete() + XCTAssertEqual(recorder.events.last, "delete", "delete() returns once Loop has been told to delete") XCTAssertEqual( Set(recorder.retracted.map(\.alertIdentifier)), Set(G7LifecycleAlert.allCases.map(\.rawValue)), @@ -281,7 +279,7 @@ class G7LifecycleAlertManagerTests: XCTestCase { } /// A manager passed its configuration schedules the same alerts, so deleting it retracts them. - func testDeletingAPassedConfigurationRetractsEveryAlert() { + func testDeletingAPassedConfigurationRetractsEveryAlert() async { var shared = G7CGMManagerState() shared.sensorID = "DXCM99" shared.activatedAt = Date() @@ -289,10 +287,7 @@ class G7LifecycleAlertManagerTests: XCTestCase { state.lifecycleAlertsScheduledFor = "scheduled" let manager = makeManager(state: state) - let done = expectation(description: "delete completed") - manager.delete { done.fulfill() } - wait(for: [done], timeout: 2) - settle() + await manager.delete() XCTAssertEqual(Set(recorder.retracted.map(\.alertIdentifier)), Set(G7LifecycleAlert.allCases.map(\.rawValue))) } diff --git a/G7SensorKitTests/G7SessionModeMigrationTests.swift b/G7SensorKitTests/G7SessionModeMigrationTests.swift index 7c32e5d..21d19f5 100644 --- a/G7SensorKitTests/G7SessionModeMigrationTests.swift +++ b/G7SensorKitTests/G7SessionModeMigrationTests.swift @@ -484,13 +484,11 @@ final class G7SessionModeMigrationTests: XCTestCase { XCTAssertGreaterThan(manager.state.pairedAt!, state.pairedAt!) } - func testDeletingKeepsThePreviousSensorAsRemoved() { + func testDeletingKeepsThePreviousSensorAsRemoved() async { var state = G7CGMManagerState(rawValue: legacyRawState) state.sensorID = "DXCM99" let manager = makeManager(state: state) - let done = expectation(description: "deleted") - manager.delete { done.fulfill() } - wait(for: [done], timeout: 2) + await manager.delete() XCTAssertEqual(manager.state.previousSensor?.endReason, .deleted) } diff --git a/G7SensorKitUI/G7CGMManager/G7UICoordinator.swift b/G7SensorKitUI/G7CGMManager/G7UICoordinator.swift index b6fc2e1..768e86c 100644 --- a/G7SensorKitUI/G7CGMManager/G7UICoordinator.swift +++ b/G7SensorKitUI/G7CGMManager/G7UICoordinator.swift @@ -237,15 +237,11 @@ class G7UICoordinator: UINavigationController, CGMManagerOnboarding, CompletionN } }, deleteCGM: { [ weak self] in - // `delete`, not `notifyDelegateOfDeletion`: the manager's - // own teardown retracts its standing alerts first, and Loop - // replays anything left behind at every launch. - self?.cgmManager?.delete { - DispatchQueue.main.async { - if let self = self { - self.completionDelegate?.completionNotifyingDidComplete(self) - self.dismiss(animated: true) - } + Task { + await self?.cgmManager?.delete() + if let self = self { + self.completionDelegate?.completionNotifyingDidComplete(self) + self.dismiss(animated: true) } } },