diff --git a/LoopKit.xcodeproj/project.pbxproj b/LoopKit.xcodeproj/project.pbxproj index db6b069da..094c27162 100644 --- a/LoopKit.xcodeproj/project.pbxproj +++ b/LoopKit.xcodeproj/project.pbxproj @@ -883,6 +883,7 @@ E9F54F5E25802D5E0034795E /* InsulinType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F54F5D25802D5E0034795E /* InsulinType.swift */; }; E9F54FBA258052130034795E /* InsulinType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F54F5D25802D5E0034795E /* InsulinType.swift */; }; E9F54FC62581C50D0034795E /* ExpandableDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F54FC52581C50C0034795E /* ExpandableDatePicker.swift */; }; + FB3933475C6B263D1D70BC83 /* FloatingActionArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15ABBB61554CA19FAA58E419 /* FloatingActionArea.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1055,6 +1056,7 @@ 14D906ED2A84579B006EB79A /* NewFavoriteFood.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewFavoriteFood.swift; sourceTree = ""; }; 14D906EF2A8457AB006EB79A /* StoredFavoriteFood.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredFavoriteFood.swift; sourceTree = ""; }; 14D906F12A845868006EB79A /* FavoriteFoodListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteFoodListRow.swift; sourceTree = ""; }; + 15ABBB61554CA19FAA58E419 /* FloatingActionArea.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FloatingActionArea.swift; sourceTree = ""; }; 1D096BF924C242300078B6B5 /* CheckmarkListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkListItem.swift; sourceTree = ""; }; 1D096C0424C624F70078B6B5 /* InsulinModelSettings+LoopKitUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "InsulinModelSettings+LoopKitUI.swift"; sourceTree = ""; }; 1D0E708426BDE3BB00AECF0D /* HKDeviceCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKDeviceCodableTests.swift; sourceTree = ""; }; @@ -2323,6 +2325,7 @@ 84EE97802D71293E00D5E941 /* GlucoseHistoryLayer.swift */, 84EE97B02D7A42DB00D5E941 /* ChartPointsScatterBorderedCirclesLayer.swift */, 84EE97B22D7A59AE00D5E941 /* ManualBolusDoseChartLayer.swift */, + 15ABBB61554CA19FAA58E419 /* FloatingActionArea.swift */, ); path = Views; sourceTree = ""; @@ -4140,6 +4143,7 @@ C18733AA29B9492300519CDF /* NumberFormatter.swift in Sources */, 89F6E311244A1AAB00CB9E15 /* SettingDescription.swift in Sources */, 1DE35E7A24ABEC720086F9AE /* DeviceManagerUI.swift in Sources */, + FB3933475C6B263D1D70BC83 /* FloatingActionArea.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/LoopKitUI/Extensions/Keyboard.swift b/LoopKitUI/Extensions/Keyboard.swift index 182217e4c..c1230987b 100644 --- a/LoopKitUI/Extensions/Keyboard.swift +++ b/LoopKitUI/Extensions/Keyboard.swift @@ -22,8 +22,19 @@ public final class Keyboard: ObservableObject { static let shared = Keyboard() private init() { - keyboardFrameChangeCancellable = NotificationCenter.default - .publisher(for: UIResponder.keyboardWillChangeFrameNotification) + let notificationNames: [Notification.Name] = [ + UIResponder.keyboardWillChangeFrameNotification, + UIResponder.keyboardDidChangeFrameNotification, + UIResponder.keyboardWillHideNotification, + UIResponder.keyboardDidHideNotification, + ] + let hideNames: Set = [ + UIResponder.keyboardWillHideNotification, + UIResponder.keyboardDidHideNotification, + ] + keyboardFrameChangeCancellable = Publishers.MergeMany( + notificationNames.map { NotificationCenter.default.publisher(for: $0) } + ) .receive(on: DispatchQueue.main) .sink { [weak self] notification in guard let self = self, let userInfo = notification.userInfo else { @@ -31,7 +42,9 @@ public final class Keyboard: ObservableObject { } let height: CGFloat - if let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + if hideNames.contains(notification.name) { + height = 0 + } else if let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { height = UIScreen.main.bounds.intersection(keyboardFrame).height } else { height = 0 @@ -39,7 +52,9 @@ public final class Keyboard: ObservableObject { let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0.25 - self.state = State(height: height, animationDuration: animationDuration) + if self.state.height != height { + self.state = State(height: height, animationDuration: animationDuration) + } } } } diff --git a/LoopKitUI/Extensions/View+KeyboardAware.swift b/LoopKitUI/Extensions/View+KeyboardAware.swift index d0b77d447..98cfbac94 100644 --- a/LoopKitUI/Extensions/View+KeyboardAware.swift +++ b/LoopKitUI/Extensions/View+KeyboardAware.swift @@ -15,27 +15,59 @@ extension View { onReceive(Keyboard.shared.$state, perform: updateForKeyboardState) } - public func keyboardAware() -> some View { - modifier(KeyboardAware()) + public func keyboardEntryPage() -> some View { + modifier(KeyboardEntryPage()) + } + + public func autoFocusOnFirstAppearance(_ shouldFocus: Binding, enabled: @autoclosure @escaping () -> Bool = true) -> some View { + modifier(AutoFocusOnFirstAppearance(shouldFocus: shouldFocus, enabled: enabled)) } } -fileprivate struct KeyboardAware: ViewModifier { - @State var keyboardHeight: CGFloat = 0 +private struct AutoFocusOnFirstAppearance: ViewModifier { + @Binding var shouldFocus: Bool + let enabled: () -> Bool + + @State private var hasAutoFocused = false + @State private var isVisible = false + + private static var transitionSettleDelay: TimeInterval { 0.5 } func body(content: Content) -> some View { content - .padding(.bottom, keyboardHeight) - .edgesIgnoringSafeArea(keyboardHeight > 0 ? .bottom : []) - .onKeyboardStateChange { state in - if state.height == 0 { - // Only animate the transition as the keyboard comes up; animating the opposite direction is jittery. - self.keyboardHeight = 0 - } else { - withAnimation(.easeInOut(duration: state.animationDuration)) { - self.keyboardHeight = state.height - } + .onAppear { + isVisible = true + guard !hasAutoFocused, enabled() else { return } + hasAutoFocused = true + DispatchQueue.main.asyncAfter(deadline: .now() + Self.transitionSettleDelay) { + guard isVisible else { return } + shouldFocus = true } } + .onDisappear { + isVisible = false + shouldFocus = false + } + } +} + +private struct KeyboardEntryPage: ViewModifier { + @State private var isKeyboardVisible = false + + func body(content: Content) -> some View { + content + .scrollBounceBehavior(.always) + .scrollDismissesKeyboard(.interactively) + .interactiveDismissDisabled(isKeyboardVisible) + .onKeyboardStateChange { state in + isKeyboardVisible = state.height > 0 + } + } +} + +@available(iOSApplicationExtension, unavailable) +public enum KeyboardDismissal { + public static func resignFirstResponder() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } diff --git a/LoopKitUI/View Controllers/DismissibleHostingController.swift b/LoopKitUI/View Controllers/DismissibleHostingController.swift index cdcdf3bfc..5e083a9c5 100644 --- a/LoopKitUI/View Controllers/DismissibleHostingController.swift +++ b/LoopKitUI/View Controllers/DismissibleHostingController.swift @@ -6,6 +6,7 @@ // Copyright © 2020 LoopKit Authors. All rights reserved. // +import Combine import SwiftUI public struct _DismissibleHostingView: View { @@ -49,6 +50,7 @@ public class DismissibleHostingController: UIHostingController<_D } private var onDisappear: () -> Void = {} + private var keyboardObservation: AnyCancellable? public convenience init ( content: Content, @@ -106,6 +108,13 @@ public class DismissibleHostingController: UIHostingController<_D self.onDisappear = onDisappear self.isModalInPresentation = isModalInPresentation + + if !isModalInPresentation { + keyboardObservation = Keyboard.shared.$state + .sink { [weak self] state in + self?.isModalInPresentation = state.height > 0 + } + } } public override func viewWillDisappear(_ animated: Bool) { diff --git a/LoopKitUI/Views/CardList/Card.swift b/LoopKitUI/Views/CardList/Card.swift index 47fa19280..850155592 100644 --- a/LoopKitUI/Views/CardList/Card.swift +++ b/LoopKitUI/Views/CardList/Card.swift @@ -40,6 +40,17 @@ import SwiftUI /// Text("Below dynamic data") /// } /// ``` + +/// Matches the corner radius of the system's inset-grouped list cards: +/// iOS 26 adopted large concentric corners; earlier versions use the classic 10pt. +public var cardCornerRadius: CGFloat { + if #available(iOS 26.0, *) { + return 26 + } else { + return 10 + } +} + public struct Card: View { var hero: AnyView? var parts: [AnyView?] @@ -89,7 +100,7 @@ public struct Card: View { } .frame(maxWidth: .infinity, alignment: .leading) .background(CardBackground(color: backgroundColor)) - .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .clipShape(RoundedRectangle(cornerRadius: cardCornerRadius, style: .continuous)) .padding(.horizontal) } } @@ -220,7 +231,7 @@ public struct CardBackground: View { } public var body: some View { - RoundedRectangle(cornerRadius: 10, style: .continuous) + RoundedRectangle(cornerRadius: cardCornerRadius, style: .continuous) .foregroundColor(color) } } diff --git a/LoopKitUI/Views/CardList/CardList.swift b/LoopKitUI/Views/CardList/CardList.swift index 3ca095cd2..6fd2b8f99 100644 --- a/LoopKitUI/Views/CardList/CardList.swift +++ b/LoopKitUI/Views/CardList/CardList.swift @@ -71,7 +71,7 @@ public struct CardList: View { } } } - .background(Color(.systemGroupedBackground)) + .background(Color(.systemGroupedBackground).ignoresSafeArea(.container, edges: .bottom)) } @ViewBuilder diff --git a/LoopKitUI/Views/ConfigurationPage.swift b/LoopKitUI/Views/ConfigurationPage.swift index 7ccec0d14..7b86c8d44 100644 --- a/LoopKitUI/Views/ConfigurationPage.swift +++ b/LoopKitUI/Views/ConfigurationPage.swift @@ -32,9 +32,8 @@ public struct ConfigurationPage: View { VStack(spacing: 0) { CardList(title: title, style: cardListStyle) - VStack(spacing: 0) { + FloatingActionArea { actionAreaContent - .padding([.top, .horizontal]) .transition(AnyTransition.opacity.combined(with: .move(edge: .bottom))) Button( @@ -54,7 +53,6 @@ public struct ConfigurationPage: View { ) .buttonStyle(ActionButtonStyle(.primary)) .disabled(actionButtonState != .enabled) - .padding() .accessibilityIdentifier("button_confirmSave") if let secondaryActionButtonTitle, let secondaryAction { @@ -75,15 +73,10 @@ public struct ConfigurationPage: View { ) .buttonStyle(ActionButtonStyle(.secondary)) .disabled(secondaryActionButtonState ?? .enabled != .enabled) - .padding([.horizontal, .bottom]) - .padding(.top, -6) .accessibilityIdentifier("button_secondaryAction") } } - .padding(.bottom) // FIXME: unnecessary on iPhone 8 size devices - .background(Color(.secondarySystemGroupedBackground).shadow(radius: 5)) } - .edgesIgnoringSafeArea(.bottom) } } @@ -103,7 +96,7 @@ extension ConfigurationPage { self.actionButtonTitle = actionButtonTitle self.secondaryActionButtonTitle = secondaryActionButtonTitle self.actionButtonState = actionButtonState - self.actionButtonState = actionButtonState + self.secondaryActionButtonState = secondaryActionButtonState self.cardListStyle = .simple(cards()) self.actionAreaContent = actionAreaContent() self.action = action diff --git a/LoopKitUI/Views/ConfirmationToggle.swift b/LoopKitUI/Views/ConfirmationToggle.swift index 3233fb6a6..6b735b002 100644 --- a/LoopKitUI/Views/ConfirmationToggle.swift +++ b/LoopKitUI/Views/ConfirmationToggle.swift @@ -66,7 +66,6 @@ public struct ConfirmationToggle: View { self.alertBody = alertBody self.confirmAction = confirmAction self._isOn = isOn - self.showConfirmAlert = showConfirmAlert } public var body: some View { diff --git a/LoopKitUI/Views/DismissibleKeyboardTextField.swift b/LoopKitUI/Views/DismissibleKeyboardTextField.swift index 02fc76bbe..221c3e709 100644 --- a/LoopKitUI/Views/DismissibleKeyboardTextField.swift +++ b/LoopKitUI/Views/DismissibleKeyboardTextField.swift @@ -91,6 +91,9 @@ public struct DismissibleKeyboardTextField: UIViewRepresentable { context.coordinator.didBecomeFirstResponder = true } } else if !shouldBecomeFirstResponder && context.coordinator.didBecomeFirstResponder { + if textField.isFirstResponder { + textField.resignFirstResponder() + } context.coordinator.didBecomeFirstResponder = false } } diff --git a/LoopKitUI/Views/FavoriteFoodListRow.swift b/LoopKitUI/Views/FavoriteFoodListRow.swift index 5629e4d7d..9a0d76aa8 100644 --- a/LoopKitUI/Views/FavoriteFoodListRow.swift +++ b/LoopKitUI/Views/FavoriteFoodListRow.swift @@ -65,8 +65,8 @@ public struct FavoriteFoodListRow: View { disclosure } } - .padding(.horizontal) - .padding(.vertical, 8) + .padding(.horizontal, 20) + .padding(.vertical, 16) .contentShape(Rectangle()) .onTapGesture { onTap(food) diff --git a/LoopKitUI/Views/FloatingActionArea.swift b/LoopKitUI/Views/FloatingActionArea.swift new file mode 100644 index 000000000..37b874ff8 --- /dev/null +++ b/LoopKitUI/Views/FloatingActionArea.swift @@ -0,0 +1,215 @@ +// +// FloatingActionArea.swift +// LoopKitUI +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import SwiftUI + +/// The floating bar pinned below a page's scrolling content that hosts its +/// primary calls to action. +/// +/// Layout contract: the bar's content lays out entirely inside the safe area; +/// only the background extends through the bottom safe area (home indicator). +/// Pages using `FloatingActionArea` must NOT apply `.edgesIgnoringSafeArea(.bottom)` +/// for the bar's benefit. +public struct FloatingActionArea: View { + private let content: Content + + @State private var bottomSafeAreaInset: CGFloat = 0 + + public init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + public var body: some View { + if Content.self != EmptyView.self { + VStack(spacing: 12) { + content + } + .padding([.horizontal, .top]) + .padding(.bottom, bottomSafeAreaInset > 0 ? 12 : 16) + .frame(maxWidth: .infinity) + .background( + GeometryReader { geometry in + Color.clear + .onAppear { bottomSafeAreaInset = geometry.safeAreaInsets.bottom } + .onChange(of: geometry.safeAreaInsets.bottom) { _, inset in + bottomSafeAreaInset = inset + } + } + ) + .actionAreaShadow() + } + } +} + +public struct SecondaryActionButton: View { + private let title: Text + private let action: () -> Void + + public init(_ title: Text, action: @escaping () -> Void) { + self.title = title + self.action = action + } + + public var body: some View { + HStack { + Spacer() + Button(action: action) { title.bold() } + .padding(8) + Spacer() + } + } +} + +public extension View { + func actionAreaShadow() -> some View { + background( + Color(.secondarySystemGroupedBackground) + .shadow(radius: 5) + .ignoresSafeArea([.container, .keyboard], edges: .bottom) + ) + } + + @ViewBuilder + func actionAreaInset(@ViewBuilder _ barContent: () -> BarContent) -> some View { + if #available(iOS 26.0, *) { + safeAreaBar(edge: .bottom, spacing: 0) { FloatingActionArea(content: barContent) } + } else { + safeAreaInset(edge: .bottom, spacing: 0) { FloatingActionArea(content: barContent) } + } + } +} + +/// The iOS 26 workaround mount: the bar ignores the keyboard safe-area region +/// (whose tracking is unreliable there) and is lifted by the keyboard overlap +/// measured through `UIKeyboardLayoutGuide`. The guide is constraint-driven, +/// so it reports exact geometry in every state — including frame-by-frame +/// while an interactive swipe drags the keyboard, where notifications are +/// silent until the gesture commits. +@available(iOS 26.0, *) +private struct KeyboardLiftedActionAreaInset: ViewModifier { + let barContent: BarContent + + @State private var keyboardLift: CGFloat = 0 + + func body(content: Content) -> some View { + content + .safeAreaInset(edge: .bottom, spacing: 0) { + FloatingActionArea { barContent } + .padding(.bottom, keyboardLift) + .ignoresSafeArea(.keyboard, edges: .bottom) + } + .background( + KeyboardOverlapReader { overlap in + guard overlap != keyboardLift else { return } + keyboardLift = overlap + } + ) + } +} + +/// Reports the height of the region between the keyboard's top edge and the +/// window's bottom safe area — 0 when no keyboard is up, and continuously +/// updated while the keyboard animates or tracks an interactive dismissal. +@available(iOS 26.0, *) +private struct KeyboardOverlapReader: UIViewRepresentable { + let onChange: (CGFloat) -> Void + + func makeUIView(context: Context) -> AnchorView { + let view = AnchorView() + view.isUserInteractionEnabled = false + view.onChange = onChange + return view + } + + func updateUIView(_ uiView: AnchorView, context: Context) { + uiView.onChange = onChange + } + + final class AnchorView: UIView { + var onChange: ((CGFloat) -> Void)? + private var follower: FollowerView? + private var notificationTokens: [NSObjectProtocol] = [] + + deinit { + notificationTokens.forEach(NotificationCenter.default.removeObserver) + } + + override func didMoveToWindow() { + super.didMoveToWindow() + follower?.removeFromSuperview() + follower = nil + notificationTokens.forEach(NotificationCenter.default.removeObserver) + notificationTokens = [] + guard let window else { return } + + let follower = FollowerView() + follower.translatesAutoresizingMaskIntoConstraints = false + follower.isUserInteractionEnabled = false + follower.isHidden = true + window.addSubview(follower) + NSLayoutConstraint.activate([ + follower.leadingAnchor.constraint(equalTo: window.leadingAnchor), + follower.widthAnchor.constraint(equalToConstant: 1), + follower.topAnchor.constraint(equalTo: window.keyboardLayoutGuide.topAnchor), + follower.bottomAnchor.constraint(equalTo: window.bottomAnchor), + ]) + follower.onLayout = { [weak self] in + let duration = UIView.inheritedAnimationDuration + DispatchQueue.main.async { self?.report(animatedOver: duration) } + } + self.follower = follower + + let names: [Notification.Name] = [ + UIResponder.keyboardWillShowNotification, + UIResponder.keyboardDidShowNotification, + UIResponder.keyboardWillHideNotification, + UIResponder.keyboardDidHideNotification, + UIResponder.keyboardWillChangeFrameNotification, + UIResponder.keyboardDidChangeFrameNotification, + ] + for name in names { + notificationTokens.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] notification in + let hiding = notification.name == UIResponder.keyboardWillHideNotification + || notification.name == UIResponder.keyboardDidHideNotification + let frameEnd = hiding ? nil : notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect + let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0.25 + DispatchQueue.main.async { + self?.report(notificationFrameEnd: frameEnd, animatedOver: duration) + } + }) + } + } + + private func report(notificationFrameEnd: CGRect? = nil, animatedOver duration: TimeInterval) { + guard let window, let follower else { return } + let bottomInset = window.safeAreaInsets.bottom + let guideOverlap = max(0, follower.bounds.height - bottomInset) + let frameOverlap: CGFloat + if let frameEnd = notificationFrameEnd { + let frameInWindow = window.convert(frameEnd, from: window.screen.coordinateSpace) + frameOverlap = max(0, window.bounds.maxY - max(frameInWindow.minY, 0) - bottomInset) + } else { + frameOverlap = 0 + } + let overlap = max(guideOverlap, frameOverlap) + if duration > 0 { + withAnimation(.easeOut(duration: duration)) { onChange?(overlap) } + } else { + onChange?(overlap) + } + } + + final class FollowerView: UIView { + var onLayout: (() -> Void)? + + override func layoutSubviews() { + super.layoutSubviews() + onLayout?() + } + } + } +} diff --git a/LoopKitUI/Views/GuidePage.swift b/LoopKitUI/Views/GuidePage.swift index e05bd46a5..21e09e6c3 100644 --- a/LoopKitUI/Views/GuidePage.swift +++ b/LoopKitUI/Views/GuidePage.swift @@ -22,24 +22,19 @@ public struct GuidePage: View where Content: View, A } public var body: some View { - VStack(spacing: 0) { - List { - if self.horizontalSizeClass == .compact { - Section(header: EmptyView(), footer: EmptyView()) { - self.content - } - } else { + List { + if self.horizontalSizeClass == .compact { + Section(header: EmptyView(), footer: EmptyView()) { self.content } + } else { + self.content } - .insetGroupedListStyle() - VStack { - self.actionAreaContent - } - .padding(self.horizontalSizeClass == .regular ? .bottom : []) - .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5)) } - .edgesIgnoringSafeArea(.bottom) + .insetGroupedListStyle() + .actionAreaInset { + self.actionAreaContent + } } } @@ -54,8 +49,8 @@ struct GuidePage_Previews: PreviewProvider { print("Button tapped") }) { Text("Action Button") - .actionButtonStyle() } + .buttonStyle(ActionButtonStyle()) } } } diff --git a/LoopKitUI/Views/Information Screens/InformationView.swift b/LoopKitUI/Views/Information Screens/InformationView.swift index b08cbe418..3c2474bf7 100644 --- a/LoopKitUI/Views/Information Screens/InformationView.swift +++ b/LoopKitUI/Views/Information Screens/InformationView.swift @@ -14,6 +14,8 @@ struct InformationView : View { var buttonText: Text var onExit: (() -> Void) let mode: SettingsPresentationMode + + @State private var scrollViewHeight: CGFloat = 0 init( title: Text, @@ -46,22 +48,31 @@ struct InformationView : View { } var body: some View { - GeometryReader { geometry in + switch mode { + case .acceptanceFlow: ScrollView { - bodyForMode + bodyForAcceptanceFlow .padding() - .frame(minHeight: geometry.size.height) } - } - } - - @ViewBuilder - private var bodyForMode: some View { - switch mode { - case .acceptanceFlow: - bodyForAcceptanceFlow + .actionAreaInset { + nextPageButton + } case .settings: - bodyForSettings + ScrollView { + bodyForSettings + .padding() + .frame(minHeight: scrollViewHeight) + } + .background( + GeometryReader { geometry in + let visibleHeight = geometry.size.height - (geometry.frame(in: .global).maxY > UIScreen.main.bounds.height - geometry.safeAreaInsets.bottom + 0.5 ? geometry.safeAreaInsets.bottom : 0) + Color.clear + .onAppear { scrollViewHeight = visibleHeight } + .onChange(of: visibleHeight) { _, height in + scrollViewHeight = height + } + } + ) } } @@ -70,8 +81,6 @@ struct InformationView : View { titleView Divider() informationalContent - Spacer() - nextPageButton } } @@ -104,7 +113,8 @@ struct InformationView : View { private var nextPageButton: some View { Button(action: onExit) { buttonText - .actionButtonStyle(.primary) - }.accessibilityIdentifier("button_continue") + } + .buttonStyle(ActionButtonStyle(.primary)) + .accessibilityIdentifier("button_continue") } } diff --git a/LoopKitUI/Views/LabeledNumberInput.swift b/LoopKitUI/Views/LabeledNumberInput.swift index 7f23d37f7..d340d9b8e 100644 --- a/LoopKitUI/Views/LabeledNumberInput.swift +++ b/LoopKitUI/Views/LabeledNumberInput.swift @@ -57,7 +57,7 @@ public struct LabeledNumberInput: View { font: font, textAlignment: .right, keyboardType: allowFractions ? .decimalPad : .numberPad, - shouldBecomeFirstResponder: true, + shouldBecomeFirstResponder: shouldBecomeFirstResponder, isDismissible: false ) .accessibility(label: Text(String(format: LocalizedString("Enter %1$@ value", comment: "Format string for accessibility label for value entry. (1: value label)"), label))) diff --git a/LoopKitUI/Views/MuteAllAppSoundsDurationSheetView.swift b/LoopKitUI/Views/MuteAllAppSoundsDurationSheetView.swift index f53eba87d..2f6e6c52f 100644 --- a/LoopKitUI/Views/MuteAllAppSoundsDurationSheetView.swift +++ b/LoopKitUI/Views/MuteAllAppSoundsDurationSheetView.swift @@ -87,7 +87,7 @@ public struct DurationSheet: View { .readContentHeight(to: $sheetContentHeight) } - VStack(spacing: 12) { + FloatingActionArea { Button { durationWasSelected = true } label: { @@ -105,9 +105,6 @@ public struct DurationSheet: View { .font(.body.bold()) .frame(maxWidth: .infinity) } - .padding([.horizontal, .top]) - .padding(.bottom, 2) - .background(Color(.secondarySystemGroupedBackground).shadow(radius: 5).ignoresSafeArea()) .readContentHeight(to: $sheetActionContentHeight) } .sheetDetent(height: sheetContentHeight + sheetActionContentHeight) diff --git a/LoopKitUI/Views/Presets/Components/CardSectionScrollView.swift b/LoopKitUI/Views/Presets/Components/CardSectionScrollView.swift index 3cd9cb0e7..106344a22 100644 --- a/LoopKitUI/Views/Presets/Components/CardSectionScrollView.swift +++ b/LoopKitUI/Views/Presets/Components/CardSectionScrollView.swift @@ -13,18 +13,18 @@ import SwiftUI -public struct CardSectionScrollView: View { +public struct CardSectionScrollView: View { let content: Content - let actionArea: ActionArea? + let actionArea: ActionAreaContent? // Initializer for custom view header - public init(@ViewBuilder content: () -> Content, @ViewBuilder actionArea: () -> ActionArea) { + public init(@ViewBuilder content: () -> Content, @ViewBuilder actionArea: () -> ActionAreaContent) { self.content = content() self.actionArea = actionArea() } // Initializer for no action area - public init(@ViewBuilder content: () -> Content) where ActionArea == Text { + public init(@ViewBuilder content: () -> Content) where ActionAreaContent == Text { self.content = content() self.actionArea = nil } @@ -38,15 +38,11 @@ public struct CardSectionScrollView: View { .padding() } if let actionArea { - VStack(spacing: 12) { + FloatingActionArea { actionArea } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(Color(.secondarySystemGroupedBackground).shadow(radius: 5)) } } .background(Color(.systemGroupedBackground)) - .edgesIgnoringSafeArea(actionArea != nil ? .bottom : []) } } diff --git a/LoopKitUI/Views/Presets/CreatePresetView.swift b/LoopKitUI/Views/Presets/CreatePresetView.swift index 7344f6b86..049bfc6c1 100644 --- a/LoopKitUI/Views/Presets/CreatePresetView.swift +++ b/LoopKitUI/Views/Presets/CreatePresetView.swift @@ -111,7 +111,6 @@ public struct CreatePresetView: View { actionArea } - .edgesIgnoringSafeArea(.bottom) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) .navigationDestination(for: CreatePresetPage.self) { page in @@ -187,11 +186,10 @@ public struct CreatePresetView: View { } private var actionArea: some View { - VStack(spacing: 0) { + FloatingActionArea { guardrailWarningIfNecessary actionButton } - .background(Color(.secondarySystemGroupedBackground).shadow(radius: 5)) } private var actionButton: some View { @@ -199,7 +197,6 @@ public struct CreatePresetView: View { path.append(CreatePresetPage.correctionRange) } .buttonStyle(ActionButtonStyle(.primary)) - .padding() } } diff --git a/LoopKitUI/Views/VideoPlayView.swift b/LoopKitUI/Views/VideoPlayView.swift index 3e7e945fe..eb98e48c2 100644 --- a/LoopKitUI/Views/VideoPlayView.swift +++ b/LoopKitUI/Views/VideoPlayView.swift @@ -61,6 +61,7 @@ public struct VideoPlayView: View { thumbnail() .aspectRatio(contentMode: .fill) .frame(maxWidth: .infinity) + .clipped() Image(frameworkImage: "play-button", decorative: true) } diff --git a/MockKitUI/Views/DeliveryUncertaintyRecoveryView.swift b/MockKitUI/Views/DeliveryUncertaintyRecoveryView.swift index 5f3f31bd9..1a9a97f12 100644 --- a/MockKitUI/Views/DeliveryUncertaintyRecoveryView.swift +++ b/MockKitUI/Views/DeliveryUncertaintyRecoveryView.swift @@ -26,9 +26,8 @@ struct DeliveryUncertaintyRecoveryView: View, HorizontalSizeClassOverride { self.dismiss() }) { Text(LocalizedString("Recover Simulator", comment: "Button title recovering comms")) - .actionButtonStyle() - .padding() } + .buttonStyle(ActionButtonStyle()) } .environment(\.horizontalSizeClass, horizontalOverride) .navigationBarTitle(Text("Comms Recovery"), displayMode: .large)