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/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/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 94df013244..37751e093b 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 {
@@ -1378,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)
@@ -1437,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()
}
@@ -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) {
@@ -1544,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()
}
@@ -1781,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
}
@@ -1802,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 {
@@ -1822,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 {
@@ -1839,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)
}
}
@@ -2042,6 +2084,15 @@ final class StatusTableViewController: LoopChartsTableViewController {
}
}
+private extension UIButton {
+ 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)
diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift
index aa0da33134..4d65f5b2ca 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,72 @@ 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()
+ }
+
+ func makeUIViewController(context: Context) -> UIViewController {
+ UIViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
+ let coordinator = context.coordinator
+
+ 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
+ self.isPresented = false
+ action.handler()
+ })
+ }
+
+ alert.addAction(UIAlertAction(
+ title: NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet"),
+ style: .destructive
+ ) { _ in
+ self.isPresented = false
+ })
+
+ if let popover = alert.popoverPresentationController {
+ popover.sourceView = uiViewController.view
+ popover.sourceRect = uiViewController.view.bounds
+ popover.delegate = coordinator
+ }
+
+ uiViewController.present(alert, animated: true)
+ }
+
+ final class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
+ var didPresent = false
+ var onDismiss: (() -> Void)?
+
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ onDismiss?()
+ }
+ }
+}
+
public struct SettingsView_Previews: PreviewProvider {
public static var previews: some View {