From a634e2fa8bb465ba082f0f08c67a0e17c7e4b684 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 31 Aug 2025 10:33:18 -0500 Subject: [PATCH 1/3] Updates for iOS26. Fix bolus action button --- .../StatusTableViewController.swift | 5 ++ Loop/Views/BolusEntryView.swift | 80 +++++++++---------- LoopUI/Views/DeviceStatusHUDView.swift | 4 +- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 0e14f2167c..94df013244 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -380,6 +380,11 @@ final class StatusTableViewController: LoopChartsTableViewController { override func reloadData(animated: Bool = false) { dispatchPrecondition(condition: .onQueue(.main)) + + guard view.window != nil else { + return + } + // This should be kept up to date immediately hudView?.loopCompletionHUD.lastLoopCompleted = deviceManager.loopManager.lastLoopCompleted diff --git a/Loop/Views/BolusEntryView.swift b/Loop/Views/BolusEntryView.swift index 3a25576f01..23012f50cb 100644 --- a/Loop/Views/BolusEntryView.swift +++ b/Loop/Views/BolusEntryView.swift @@ -18,17 +18,22 @@ struct BolusEntryView: View { @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference @Environment(\.dismissAction) var dismiss @Environment(\.appName) var appName - + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + @ObservedObject var viewModel: BolusEntryViewModel @State private var enteredBolusString = "" - @State private var shouldBolusEntryBecomeFirstResponder = false @State private var isInteractingWithChart = false - @State private var isKeyboardVisible = false @State private var pickerShouldExpand = false @State private var editedBolusAmount = false + @FocusState private var bolusFieldFocused: Bool + + private var accessoryClearance: CGFloat { + dynamicTypeSize.isAccessibilitySize ? 72 : 52 + } + var body: some View { GeometryReader { geometry in VStack(spacing: 0) { @@ -36,27 +41,9 @@ struct BolusEntryView: View { self.chartSection self.summarySection } - // As of iOS 13, we can't programmatically scroll to the Bolus entry text field. This ugly hack scoots the - // list up instead, so the summarySection is visible and the keyboard shows when you tap "Enter Bolus". - // Unfortunately, after entry, the field scoots back down and remains hidden. So this is not a great solution. - // TODO: Fix this in Xcode 12 when we're building for iOS 14. - .padding(.top, self.shouldAutoScroll(basedOn: geometry) ? -200 : -28) .insetGroupedListStyle() - self.actionArea - .frame(height: self.isKeyboardVisible ? 0 : nil) - .opacity(self.isKeyboardVisible ? 0 : 1) } - .onKeyboardStateChange { state in - self.isKeyboardVisible = state.height > 0 - - if state.height == 0 { - // Ensure tapping 'Enter Bolus' can make the text field the first responder again - self.shouldBolusEntryBecomeFirstResponder = false - } - } - .keyboardAware() - .edgesIgnoringSafeArea(self.isKeyboardVisible ? [] : .bottom) .navigationBarTitle(self.title) .supportedInterfaceOrientations(.portrait) .alert(item: self.$viewModel.activeAlert, content: self.alert(for:)) @@ -73,6 +60,14 @@ struct BolusEntryView: View { enteredBolusStringBinding.wrappedValue = newEnteredBolusString } } + .safeAreaInset(edge: .bottom, spacing: 0) { + if bolusFieldFocused { + // Reserve space so the toolbar doesn’t overlap the field + Color.clear.frame(height: accessoryClearance) + } else { + actionArea + } + } } } @@ -83,12 +78,6 @@ struct BolusEntryView: View { return Text("Meal Bolus", comment: "Title for bolus entry screen when also entering carbs") } - private func shouldAutoScroll(basedOn geometry: GeometryProxy) -> Bool { - // Taking a guess of 640 to cover iPhone SE, iPod Touch, and other smaller devices. - // Devices such as the iPhone 11 Pro Max do not need to auto-scroll. - return shouldBolusEntryBecomeFirstResponder && geometry.size.height > 640 - } - private var chartSection: some View { Section { VStack(spacing: 8) { @@ -253,18 +242,29 @@ struct BolusEntryView: View { Text("Bolus", comment: "Label for bolus entry row on bolus screen") Spacer() HStack(alignment: .firstTextBaseline) { - DismissibleKeyboardTextField( - text: enteredBolusStringBinding, - placeholder: viewModel.formatBolusAmount(0.0), - font: .preferredFont(forTextStyle: .title1), - textColor: .loopAccent, - textAlignment: .right, - keyboardType: .decimalPad, - shouldBecomeFirstResponder: shouldBolusEntryBecomeFirstResponder, - maxLength: 5, - doneButtonColor: .loopAccent, - textFieldDidBeginEditing: didBeginEditing - ) + TextField(viewModel.formatBolusAmount(0.0), text: enteredBolusStringBinding) + .keyboardType(.decimalPad) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .font(.title) + .multilineTextAlignment(.trailing) + .foregroundColor(.loopAccent) + .focused($bolusFieldFocused) + .onTapGesture { didBeginEditing() } + // Optional: keep to 5 chars like before + .onChange(of: enteredBolusString) { newValue in + if newValue.count > 5 { + enteredBolusString = String(newValue.prefix(5)) + viewModel.updateEnteredBolus(enteredBolusString) + } + } + // Keyboard toolbar "Done" + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Done") { bolusFieldFocused = false } + } + } bolusUnitsLabel } } @@ -354,7 +354,7 @@ struct BolusEntryView: View { Button( action: { if self.viewModel.actionButtonAction == .enterBolus { - self.shouldBolusEntryBecomeFirstResponder = true + self.bolusFieldFocused = true } else { Task { if await self.viewModel.didPressActionButton() { diff --git a/LoopUI/Views/DeviceStatusHUDView.swift b/LoopUI/Views/DeviceStatusHUDView.swift index 3951aca3ec..904f9d44cb 100644 --- a/LoopUI/Views/DeviceStatusHUDView.swift +++ b/LoopUI/Views/DeviceStatusHUDView.swift @@ -32,8 +32,8 @@ import LoopKitUI // round the edges of the progress view progressView.layer.cornerRadius = 2 progressView.clipsToBounds = true - progressView.layer.sublayers![1].cornerRadius = 2 - progressView.subviews[1].clipsToBounds = true + progressView.layer.sublayers!.last!.cornerRadius = 2 + progressView.subviews.last!.clipsToBounds = true } } From 9716cdf0ff4bd0e980f06d6d783e29b00d7890dc Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 31 Aug 2025 10:37:45 -0500 Subject: [PATCH 2/3] Cleanup --- Loop/Views/BolusEntryView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Loop/Views/BolusEntryView.swift b/Loop/Views/BolusEntryView.swift index 23012f50cb..10e6fa4ccd 100644 --- a/Loop/Views/BolusEntryView.swift +++ b/Loop/Views/BolusEntryView.swift @@ -23,9 +23,7 @@ struct BolusEntryView: View { @ObservedObject var viewModel: BolusEntryViewModel @State private var enteredBolusString = "" - @State private var isInteractingWithChart = false - @State private var pickerShouldExpand = false @State private var editedBolusAmount = false @FocusState private var bolusFieldFocused: Bool @@ -251,14 +249,12 @@ struct BolusEntryView: View { .foregroundColor(.loopAccent) .focused($bolusFieldFocused) .onTapGesture { didBeginEditing() } - // Optional: keep to 5 chars like before .onChange(of: enteredBolusString) { newValue in if newValue.count > 5 { enteredBolusString = String(newValue.prefix(5)) viewModel.updateEnteredBolus(enteredBolusString) } } - // Keyboard toolbar "Done" .toolbar { ToolbarItemGroup(placement: .keyboard) { Spacer() From 01412d931cbea546e63f28f21b74ecc36c4e16f0 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 1 Sep 2025 14:03:12 -0500 Subject: [PATCH 3/3] Update manual dose entry UI for iOS 26 --- Loop/Views/ManualEntryDoseView.swift | 75 +++++++++++++--------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/Loop/Views/ManualEntryDoseView.swift b/Loop/Views/ManualEntryDoseView.swift index e81dccdabb..0c6700e6eb 100644 --- a/Loop/Views/ManualEntryDoseView.swift +++ b/Loop/Views/ManualEntryDoseView.swift @@ -15,16 +15,21 @@ import LoopUI struct ManualEntryDoseView: View { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + @ObservedObject var viewModel: ManualEntryDoseViewModel @State private var enteredBolusString = "" - @State private var shouldBolusEntryBecomeFirstResponder = false - @State private var isInteractingWithChart = false - @State private var isKeyboardVisible = false + + @FocusState private var bolusFieldFocused: Bool @Environment(\.dismissAction) var dismiss + private var accessoryClearance: CGFloat { + dynamicTypeSize.isAccessibilitySize ? 72 : 52 + } + var body: some View { GeometryReader { geometry in VStack(spacing: 0) { @@ -32,29 +37,18 @@ struct ManualEntryDoseView: View { self.chartSection self.summarySection } - // As of iOS 13, we can't programmatically scroll to the Bolus entry text field. This ugly hack scoots the - // list up instead, so the summarySection is visible and the keyboard shows when you tap "Enter Bolus". - // Unfortunately, after entry, the field scoots back down and remains hidden. So this is not a great solution. - // TODO: Fix this in Xcode 12 when we're building for iOS 14. - .padding(.top, self.shouldAutoScroll(basedOn: geometry) ? -200 : -28) .insetGroupedListStyle() - - self.actionArea - .frame(height: self.isKeyboardVisible ? 0 : nil) - .opacity(self.isKeyboardVisible ? 0 : 1) - } - .onKeyboardStateChange { state in - self.isKeyboardVisible = state.height > 0 - - if state.height == 0 { - // Ensure tapping 'Enter Bolus' can make the text field the first responder again - self.shouldBolusEntryBecomeFirstResponder = false - } } - .keyboardAware() - .edgesIgnoringSafeArea(self.isKeyboardVisible ? [] : .bottom) .navigationBarTitle(self.title) .supportedInterfaceOrientations(.portrait) + .safeAreaInset(edge: .bottom, spacing: 0) { + if bolusFieldFocused { + // Reserve space so the toolbar doesn’t overlap the field + Color.clear.frame(height: accessoryClearance) + } else { + actionArea + } + } } } @@ -62,12 +56,6 @@ struct ManualEntryDoseView: View { return Text("Log Dose", comment: "Title for dose logging screen") } - private func shouldAutoScroll(basedOn geometry: GeometryProxy) -> Bool { - // Taking a guess of 640 to cover iPhone SE, iPod Touch, and other smaller devices. - // Devices such as the iPhone 11 Pro Max do not need to auto-scroll. - shouldBolusEntryBecomeFirstResponder && geometry.size.height < 640 - } - private var chartSection: some View { Section { VStack(spacing: 8) { @@ -189,16 +177,25 @@ struct ManualEntryDoseView: View { Text("Bolus", comment: "Label for bolus entry row on bolus screen") Spacer() HStack(alignment: .firstTextBaseline) { - DismissibleKeyboardTextField( - text: typedBolusEntry, - placeholder: Self.doseAmountFormatter.string(from: 0.0)!, - font: .preferredFont(forTextStyle: .title1), - textColor: .loopAccent, - textAlignment: .right, - keyboardType: .decimalPad, - shouldBecomeFirstResponder: shouldBolusEntryBecomeFirstResponder, - maxLength: 5 - ) + TextField(Self.doseAmountFormatter.string(from: 0.0)!, text: typedBolusEntry) + .keyboardType(.decimalPad) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .font(.title) + .multilineTextAlignment(.trailing) + .foregroundColor(.loopAccent) + .focused($bolusFieldFocused) + .onChange(of: enteredBolusString) { newValue in + if newValue.count > 5 { + enteredBolusString = String(newValue.prefix(5)) + } + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Done") { bolusFieldFocused = false } + } + } bolusUnitsLabel } } @@ -250,7 +247,7 @@ struct ManualEntryDoseView: View { } } -extension InsulinType: Labeled { +extension InsulinType: @retroactive Labeled { public var label: String { return title }