From a17efb1beab2a4ae3c31aefa2a5abf7b87b58c0e Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 16 Jul 2026 14:34:59 -0700 Subject: [PATCH 1/6] toolbar fix --- Loop/Info.plist | 2 - .../StatusTableViewController.swift | 130 ++++++++++++------ 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/Loop/Info.plist b/Loop/Info.plist index db76e6e846..d6ee292459 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -89,8 +89,6 @@ remote-notification audio - UIDesignRequiresCompatibility - UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 94df013244..0b1063fc63 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -273,41 +273,76 @@ final class StatusTableViewController: LoopChartsTableViewController { deviceManager.pumpManagerHUDProvider?.visible = active && onscreen } + private lazy var carbEntryButton = makeToolbarButton(imageNamed: "carbs", tintColor: .carbTintColor, action: #selector(userTappedAddCarbs)) + private lazy var bolusButton = makeToolbarButton(imageNamed: "bolus", tintColor: .insulinTintColor, action: #selector(presentBolusScreen)) + private lazy var settingsButton = makeToolbarButton(imageNamed: "settings", tintColor: .secondaryLabel, action: #selector(onSettingsTapped)) + + private lazy var workoutButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .glucoseTintColor + button.addTarget(self, action: #selector(toggleWorkoutMode(_:)), for: .touchUpInside) + button.constrainToToolbarIconSize() + return button + }() + + private func makeToolbarButton(imageNamed name: String, tintColor: UIColor, action: Selector) -> UIButton { + let button = UIButton(type: .system) + button.setImage(UIImage(named: name), for: .normal) + button.tintColor = tintColor + button.addTarget(self, action: action, for: .touchUpInside) + button.constrainToToolbarIconSize() + return button + } + private func setupToolbarItems() { - let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) - let carbs = UIBarButtonItem(image: UIImage(named: "carbs"), style: .plain, target: self, action: #selector(userTappedAddCarbs)) - let bolus = UIBarButtonItem(image: UIImage(named: "bolus"), style: .plain, target: self, action: #selector(presentBolusScreen)) - let settings = UIBarButtonItem(image: UIImage(named: "settings"), style: .plain, target: self, action: #selector(onSettingsTapped)) - + let carbs = UIBarButtonItem(customView: carbEntryButton) + let bolus = UIBarButtonItem(customView: bolusButton) + let settings = UIBarButtonItem(customView: settingsButton) + + carbs.title = "Add Meal" + let preMeal = createPreMealButtonItem(selected: false, isEnabled: true) - let workout = createWorkoutButtonItem(selected: false, isEnabled: true) - toolbarItems = [ - carbs, - space, - preMeal, - space, - bolus, - space, - workout, - space, - settings - ] - } + updateWorkoutButton(selected: false, isEnabled: true) + let workout = UIBarButtonItem(customView: workoutButton) + + func flexibleSpace() -> UIBarButtonItem { + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + } + if #available(iOS 26, *) { + toolbarItems = [carbs, preMeal, bolus, workout, settings] + } else { + toolbarItems = [ + carbs, + flexibleSpace(), + preMeal, + flexibleSpace(), + bolus, + flexibleSpace(), + workout, + flexibleSpace(), + settings + ] + } + } + private func updateToolbarItems() { let isPumpOnboarded = onboardingManager.isComplete || deviceManager.pumpManager?.isOnboarded == true - toolbarItems![0].accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") - toolbarItems![0].isEnabled = isPumpOnboarded - toolbarItems![0].tintColor = UIColor.carbTintColor - toolbarItems![4].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") - toolbarItems![4].isEnabled = isPumpOnboarded - toolbarItems![4].tintColor = UIColor.insulinTintColor - toolbarItems![8].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") - toolbarItems![8].tintColor = UIColor.secondaryLabel - - toolbarItems![2] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) - toolbarItems![6] = createWorkoutButtonItem(selected: workoutMode == true && workoutModeAllowed, isEnabled: workoutModeAllowed) + carbEntryButton.accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") + carbEntryButton.isEnabled = isPumpOnboarded + bolusButton.accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") + bolusButton.isEnabled = isPumpOnboarded + settingsButton.accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") + + let preMealIndex: Int + if #available(iOS 26, *) { + preMealIndex = 1 + } else { + preMealIndex = 2 + } + toolbarItems![preMealIndex] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) + updateWorkoutButton(selected: workoutMode == true && workoutModeAllowed, isEnabled: workoutModeAllowed) } public var basalDeliveryState: PumpManagerStatus.BasalDeliveryState? = nil { @@ -1459,21 +1494,20 @@ final class StatusTableViewController: LoopChartsTableViewController { return item } - private func createWorkoutButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage.workoutImage(selected: selected), style: .plain, target: self, action: #selector(toggleWorkoutMode(_:))) - item.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") + private func updateWorkoutButton(selected: Bool, isEnabled: Bool) { + workoutButton.setImage(UIImage.workoutImage(selected: selected), for: .normal) + workoutButton.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") if selected { - item.accessibilityTraits.insert(.selected) - item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") + workoutButton.accessibilityTraits.insert(.selected) + workoutButton.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") } else { - item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") + workoutButton.accessibilityTraits.remove(.selected) + workoutButton.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") } - item.tintColor = UIColor.glucoseTintColor - item.isEnabled = isEnabled - - return item + workoutButton.isEnabled = isEnabled + workoutButton.sizeToFit() } @IBAction func premealButtonTapped(_ sender: UIBarButtonItem) { @@ -1781,15 +1815,15 @@ final class StatusTableViewController: LoopChartsTableViewController { @objc private func pumpStatusTapped(_ sender: UIGestureRecognizer) { if let pumpStatusView = sender.view as? PumpStatusHUDView { - executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD)) + executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD), zoomingFrom: pumpStatusView) } } @objc private func cgmStatusTapped( _ sender: UIGestureRecognizer) { - executeHUDTapAction(deviceManager.didTapOnCGMStatus()) + executeHUDTapAction(deviceManager.didTapOnCGMStatus(), zoomingFrom: sender.view) } - private func executeHUDTapAction(_ action: HUDTapAction?) { + private func executeHUDTapAction(_ action: HUDTapAction?, zoomingFrom sourceView: UIView? = nil) { guard let action = action else { return } @@ -2042,6 +2076,18 @@ final class StatusTableViewController: LoopChartsTableViewController { } } +private extension UIButton { + /// Keeps a custom-view toolbar button tight to its icon by refusing to stretch. Without + /// this the toolbar expands the button to fill its distributed slot, and the zoom + /// transition then originates from that full-width slot (the bar) rather than the icon. + func constrainToToolbarIconSize() { + setContentHuggingPriority(.required, for: .horizontal) + setContentHuggingPriority(.required, for: .vertical) + setContentCompressionResistancePriority(.required, for: .horizontal) + setContentCompressionResistancePriority(.required, for: .vertical) + } +} + extension UIAlertController { func addActivityIndicator() { let frame = CGRect(x: 0, y: 0, width: 40, height: 40) From fd07cefabe2a900944286e538ffb321c09e2ac42 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 16 Jul 2026 14:39:31 -0700 Subject: [PATCH 2/6] toolbar fix --- Loop/View Controllers/StatusTableViewController.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 0b1063fc63..d3a7e61acc 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1815,15 +1815,15 @@ final class StatusTableViewController: LoopChartsTableViewController { @objc private func pumpStatusTapped(_ sender: UIGestureRecognizer) { if let pumpStatusView = sender.view as? PumpStatusHUDView { - executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD), zoomingFrom: pumpStatusView) + executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD)) } } @objc private func cgmStatusTapped( _ sender: UIGestureRecognizer) { - executeHUDTapAction(deviceManager.didTapOnCGMStatus(), zoomingFrom: sender.view) + executeHUDTapAction(deviceManager.didTapOnCGMStatus()) } - private func executeHUDTapAction(_ action: HUDTapAction?, zoomingFrom sourceView: UIView? = nil) { + private func executeHUDTapAction(_ action: HUDTapAction?) { guard let action = action else { return } @@ -2077,9 +2077,6 @@ final class StatusTableViewController: LoopChartsTableViewController { } private extension UIButton { - /// Keeps a custom-view toolbar button tight to its icon by refusing to stretch. Without - /// this the toolbar expands the button to fill its distributed slot, and the zoom - /// transition then originates from that full-width slot (the bar) rather than the icon. func constrainToToolbarIconSize() { setContentHuggingPriority(.required, for: .horizontal) setContentHuggingPriority(.required, for: .vertical) From 007cd897057db3cd38b769ed5a9056d3f0ae6e02 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Fri, 17 Jul 2026 15:03:58 -0700 Subject: [PATCH 3/6] toolbar fix --- Loop/View Controllers/StatusTableViewController.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index d3a7e61acc..84d278bac0 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1578,7 +1578,13 @@ final class StatusTableViewController: LoopChartsTableViewController { } } else { if FeatureFlags.sensitivityOverridesEnabled { - performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![6]) + let overridesIndex: Int + if #available(iOS 26, *) { + overridesIndex = 3 + } else { + overridesIndex = 6 + } + performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![overridesIndex]) } else { presentWorkoutModeAlertController() } From 5e388f511332ca2fee8627aa6126f1b200731686 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Wed, 29 Jul 2026 12:59:40 -0700 Subject: [PATCH 4/6] Liquid Glass Bug Fixes (Pt. 1) --- Loop/Extensions/UIAlertController.swift | 14 +- Loop/Managers/AlertPermissionsChecker.swift | 2 +- .../StatusTableViewController.swift | 24 +-- Loop/Views/SettingsView.swift | 146 +++++++++++++----- 4 files changed, 135 insertions(+), 51 deletions(-) diff --git a/Loop/Extensions/UIAlertController.swift b/Loop/Extensions/UIAlertController.swift index 3b83aa11d8..dbec0093fc 100644 --- a/Loop/Extensions/UIAlertController.swift +++ b/Loop/Extensions/UIAlertController.swift @@ -88,6 +88,8 @@ extension UIAlertController { } )) } + + addCancelAction() } /// Initializes an action sheet-styled controller for selecting a CGMManager @@ -112,6 +114,8 @@ extension UIAlertController { } )) } + + addCancelAction() } internal convenience init(deleteCGMManagerHandler handler: @escaping (_ isDeleted: Bool) -> Void) { @@ -141,10 +145,12 @@ extension UIAlertController { /// - selectionHandler: A closure to execute when a service is selected. /// - identifier: The identifier of the selected service. internal convenience init(availableServices: [ServiceDescriptor], selectionHandler: @escaping (_ identifier: String) -> Void) { + let preferredStyle: UIAlertController.Style = .alert + self.init( title: NSLocalizedString("Add Service", comment: "Action sheet title selecting service"), message: nil, - preferredStyle: .actionSheet + preferredStyle: preferredStyle ) for availableService in availableServices { @@ -156,11 +162,15 @@ extension UIAlertController { } )) } + + if #available(iOS 26.0, *) { + addCancelAction() + } } internal func addCancelAction(handler: ((UIAlertAction) -> Void)? = nil) { let cancel = NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet") - addAction(UIAlertAction(title: cancel, style: .cancel, handler: handler)) + addAction(UIAlertAction(title: cancel, style: .destructive, handler: handler)) } } diff --git a/Loop/Managers/AlertPermissionsChecker.swift b/Loop/Managers/AlertPermissionsChecker.swift index bae4512e6a..fcd5edcbff 100644 --- a/Loop/Managers/AlertPermissionsChecker.swift +++ b/Loop/Managers/AlertPermissionsChecker.swift @@ -128,7 +128,7 @@ extension AlertPermissionsChecker { message: Self.unsafeNotificationPermissionsAlertContent.body, preferredStyle: .alert) let titleImageAttachment = NSTextAttachment() - titleImageAttachment.image = UIImage(systemName: "exclamationmark.triangle.fill")?.withTintColor(.critical) + titleImageAttachment.image = UIImage(systemName: "exclamationmark.triangle.fill")?.withTintColor(.critical, renderingMode: .alwaysOriginal) titleImageAttachment.bounds = CGRect(x: titleImageAttachment.bounds.origin.x, y: -10, width: 40, height: 35) let titleWithImage = NSMutableAttributedString(attachment: titleImageAttachment) titleWithImage.append(NSMutableAttributedString(string: "\n\n", attributes: [.font: UIFont.systemFont(ofSize: 8)])) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 84d278bac0..37751e093b 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1413,7 +1413,7 @@ final class StatusTableViewController: LoopChartsTableViewController { let bolusEntryView = SimpleBolusView(viewModel: viewModel).environmentObject(deviceManager.displayGlucosePreference) let hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false) navigationWrapper = UINavigationController(rootViewController: hostingController) - hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) + hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Cancel", comment: ""), style: .plain, target: navigationWrapper, action: #selector(dismissWithAnimation)) present(navigationWrapper, animated: true) } else { let viewModel = CarbEntryViewModel(delegate: deviceManager) @@ -1472,7 +1472,7 @@ final class StatusTableViewController: LoopChartsTableViewController { ) let navigationWrapper = UINavigationController(rootViewController: hostingController) - hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) + hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Cancel", comment: ""), style: .plain, target: navigationWrapper, action: #selector(dismissWithAnimation)) present(navigationWrapper, animated: true) deviceManager.analyticsServicesManager.didDisplayBolusScreen() } @@ -1821,15 +1821,15 @@ final class StatusTableViewController: LoopChartsTableViewController { @objc private func pumpStatusTapped(_ sender: UIGestureRecognizer) { if let pumpStatusView = sender.view as? PumpStatusHUDView { - executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD)) + executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD), from: sender.view) } } @objc private func cgmStatusTapped( _ sender: UIGestureRecognizer) { - executeHUDTapAction(deviceManager.didTapOnCGMStatus()) + executeHUDTapAction(deviceManager.didTapOnCGMStatus(), from: sender.view) } - private func executeHUDTapAction(_ action: HUDTapAction?) { + private func executeHUDTapAction(_ action: HUDTapAction?, from sourceView: UIView?) { guard let action = action else { return } @@ -1842,15 +1842,15 @@ final class StatusTableViewController: LoopChartsTableViewController { case .openAppURL(let url): UIApplication.shared.open(url) case .setupNewCGM: - addNewCGMManager() + addNewCGMManager(from: sourceView) case .setupNewPump: - addNewPumpManager() + addNewPumpManager(from: sourceView) default: return } } - private func addNewPumpManager() { + private func addNewPumpManager(from sourceView: UIView?) { let availablePumpManagers = deviceManager.availablePumpManagers switch availablePumpManagers.count { @@ -1862,12 +1862,13 @@ final class StatusTableViewController: LoopChartsTableViewController { let alert = UIAlertController(availablePumpManagers: availablePumpManagers) { [weak self] (identifier) in self?.addPumpManager(withIdentifier: identifier) } - alert.addCancelAction { _ in } + alert.popoverPresentationController?.sourceView = sourceView ?? view + alert.popoverPresentationController?.sourceRect = sourceView?.bounds ?? view.bounds present(alert, animated: true, completion: nil) } } - private func addNewCGMManager() { + private func addNewCGMManager(from sourceView: UIView?) { let availableCGMManagers = deviceManager.availableCGMManagers switch availableCGMManagers.count { @@ -1879,7 +1880,8 @@ final class StatusTableViewController: LoopChartsTableViewController { let alert = UIAlertController(availableCGMManagers: availableCGMManagers) { [weak self] identifier in self?.addCGMManager(withIdentifier: identifier) } - alert.addCancelAction { _ in } + alert.popoverPresentationController?.sourceView = sourceView ?? view + alert.popoverPresentationController?.sourceRect = sourceView?.bounds ?? view.bounds present(alert, animated: true, completion: nil) } } diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index aa0da33134..b3f0fe292e 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -107,25 +107,6 @@ public struct SettingsView: View { .insetGroupedListStyle() .navigationBarTitle(Text(NSLocalizedString("Settings", comment: "Settings screen title"))) .navigationBarItems(trailing: dismissButton) - .actionSheet(item: $actionSheet) { actionSheet in - switch actionSheet { - case .cgmPicker: - return ActionSheet( - title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), - buttons: cgmChoices - ) - case .pumpPicker: - return ActionSheet( - title: Text("Add Pump", comment: "The title of the pump chooser in settings"), - buttons: pumpChoices - ) - case .servicePicker: - return ActionSheet( - title: Text("Add Service", comment: "The title of the add service action sheet in settings"), - buttons: serviceChoices - ) - } - } .alert(item: $alert) { alert in switch alert { case .deleteCGMData: @@ -337,17 +318,22 @@ extension SettingsView { imageView: plusImage, label: NSLocalizedString("Add Pump", comment: "Title text for button to add pump device"), descriptiveText: NSLocalizedString("Tap here to set up a pump", comment: "Descriptive text for button to add pump device")) + .background( + PluginPopover( + isPresented: actionSheetBinding(for: .pumpPicker), + title: NSLocalizedString("Add Pump", comment: "The title of the pump chooser in settings"), + actions: pumpChoices + ) + ) } } - - private var pumpChoices: [ActionSheet.Button] { - var result = viewModel.pumpManagerSettingsViewModel.availableDevices.map { availableDevice in - ActionSheet.Button.default(Text(availableDevice.localizedTitle)) { + + private var pumpChoices: [PluginPopover.Action] { + viewModel.pumpManagerSettingsViewModel.availableDevices.map { availableDevice in + .init(title: availableDevice.localizedTitle) { self.viewModel.pumpManagerSettingsViewModel.didTapAdd(availableDevice) } } - result.append(.cancel()) - return result } @ViewBuilder @@ -364,6 +350,13 @@ extension SettingsView { imageView: plusImage, label: NSLocalizedString("Add CGM", comment: "Title text for button to add CGM device"), descriptiveText: NSLocalizedString("Tap here to set up a CGM", comment: "Descriptive text for button to add CGM device")) + .background( + PluginPopover( + isPresented: actionSheetBinding(for: .cgmPicker), + title: NSLocalizedString("Add CGM", comment: "The title of the CGM chooser in settings"), + actions: cgmChoices + ) + ) } } @@ -377,16 +370,14 @@ extension SettingsView { } } - private var cgmChoices: [ActionSheet.Button] { - var result = viewModel.cgmManagerSettingsViewModel.availableDevices + private var cgmChoices: [PluginPopover.Action] { + viewModel.cgmManagerSettingsViewModel.availableDevices .sorted(by: {$0.localizedTitle < $1.localizedTitle}) .map { availableDevice in - ActionSheet.Button.default(Text(availableDevice.localizedTitle)) { + .init(title: availableDevice.localizedTitle) { self.viewModel.cgmManagerSettingsViewModel.didTapAdd(availableDevice) + } } - } - result.append(.cancel()) - return result } private var servicesSection: some View { @@ -404,20 +395,36 @@ extension SettingsView { imageView: plusImage, label: NSLocalizedString("Add Service", comment: "The title of the add service button in settings"), descriptiveText: NSLocalizedString("Tap here to set up a Service", comment: "The descriptive text of the add service button in settings")) + .background( + PluginPopover( + isPresented: actionSheetBinding(for: .servicePicker), + title: NSLocalizedString("Add Service", comment: "The title of the add service action sheet in settings"), + actions: serviceChoices + ) + ) } } } - - private var serviceChoices: [ActionSheet.Button] { - var result = viewModel.servicesViewModel.inactiveServices().map { availableService in - ActionSheet.Button.default(Text(availableService.localizedTitle)) { + + private var serviceChoices: [PluginPopover.Action] { + viewModel.servicesViewModel.inactiveServices().map { availableService in + .init(title: availableService.localizedTitle) { self.viewModel.servicesViewModel.didTapAddService(availableService) } } - result.append(.cancel()) - return result } + private func actionSheetBinding(for destination: Destination.ActionSheet) -> Binding { + Binding( + get: { actionSheet == destination }, + set: { isPresented in + if !isPresented && actionSheet == destination { + actionSheet = nil + } + } + ) + } + private var deleteDataSection: some View { Section { if viewModel.pumpManagerSettingsViewModel.isTestingDevice { @@ -615,6 +622,71 @@ fileprivate struct LargeButton: View { } } +struct PluginPopover: UIViewControllerRepresentable { + struct Action { + let title: String + let handler: () -> Void + } + + @Binding var isPresented: Bool + let title: String + let actions: [Action] + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: Context) -> UIViewController { + UIViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + context.coordinator.parent = self + + guard isPresented else { return } + guard uiViewController.presentedViewController == nil else { return } + + let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) + for action in actions { + alert.addAction(UIAlertAction(title: action.title, style: .default) { _ in + context.coordinator.setPresented(false) + action.handler() + }) + } + + alert.addAction(UIAlertAction( + title: NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet"), + style: .destructive + ) { _ in + context.coordinator.setPresented(false) + }) + + if let popover = alert.popoverPresentationController { + popover.sourceView = uiViewController.view + popover.sourceRect = uiViewController.view.bounds + popover.delegate = context.coordinator + } + + uiViewController.present(alert, animated: true) + } + + final class Coordinator: NSObject, UIPopoverPresentationControllerDelegate { + var parent: PluginPopover + + init(_ parent: PluginPopover) { + self.parent = parent + } + + func setPresented(_ value: Bool) { + DispatchQueue.main.async { self.parent.isPresented = value } + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + setPresented(false) + } + } +} + public struct SettingsView_Previews: PreviewProvider { public static var previews: some View { From 442f00b98e34a2a82ff2c7b81ca4861f5f45665f Mon Sep 17 00:00:00 2001 From: camji55 Date: Wed, 29 Jul 2026 13:28:23 -0700 Subject: [PATCH 5/6] Fix settings popup after dismissal --- Loop/Views/SettingsView.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index b3f0fe292e..264ca4a5bc 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -649,8 +649,10 @@ struct PluginPopover: UIViewControllerRepresentable { let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) for action in actions { alert.addAction(UIAlertAction(title: action.title, style: .default) { _ in - context.coordinator.setPresented(false) - action.handler() + context.coordinator.parent.isPresented = false + DispatchQueue.main.async { + action.handler() + } }) } @@ -658,7 +660,7 @@ struct PluginPopover: UIViewControllerRepresentable { title: NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet"), style: .destructive ) { _ in - context.coordinator.setPresented(false) + context.coordinator.parent.isPresented = false }) if let popover = alert.popoverPresentationController { @@ -677,12 +679,8 @@ struct PluginPopover: UIViewControllerRepresentable { self.parent = parent } - func setPresented(_ value: Bool) { - DispatchQueue.main.async { self.parent.isPresented = value } - } - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - setPresented(false) + parent.isPresented = false } } } From 5a93636eabd2679a573fe88bc2a1ca7302920e6a Mon Sep 17 00:00:00 2001 From: camji55 Date: Wed, 29 Jul 2026 13:31:59 -0700 Subject: [PATCH 6/6] Fix settings popup after dismissal --- Loop/Views/SettingsView.swift | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 264ca4a5bc..4d65f5b2ca 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -633,7 +633,7 @@ struct PluginPopover: UIViewControllerRepresentable { let actions: [Action] func makeCoordinator() -> Coordinator { - Coordinator(self) + Coordinator() } func makeUIViewController(context: Context) -> UIViewController { @@ -641,18 +641,24 @@ struct PluginPopover: UIViewControllerRepresentable { } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { - context.coordinator.parent = self + let coordinator = context.coordinator - guard isPresented else { return } + if !isPresented { + coordinator.didPresent = false + return + } + + guard !coordinator.didPresent else { return } guard uiViewController.presentedViewController == nil else { return } + coordinator.didPresent = true + coordinator.onDismiss = { self.isPresented = false } + let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) for action in actions { alert.addAction(UIAlertAction(title: action.title, style: .default) { _ in - context.coordinator.parent.isPresented = false - DispatchQueue.main.async { - action.handler() - } + self.isPresented = false + action.handler() }) } @@ -660,27 +666,24 @@ struct PluginPopover: UIViewControllerRepresentable { title: NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet"), style: .destructive ) { _ in - context.coordinator.parent.isPresented = false + self.isPresented = false }) if let popover = alert.popoverPresentationController { popover.sourceView = uiViewController.view popover.sourceRect = uiViewController.view.bounds - popover.delegate = context.coordinator + popover.delegate = coordinator } uiViewController.present(alert, animated: true) } final class Coordinator: NSObject, UIPopoverPresentationControllerDelegate { - var parent: PluginPopover - - init(_ parent: PluginPopover) { - self.parent = parent - } + var didPresent = false + var onDismiss: (() -> Void)? func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - parent.isPresented = false + onDismiss?() } } }