Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LoopKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -1055,6 +1056,7 @@
14D906ED2A84579B006EB79A /* NewFavoriteFood.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewFavoriteFood.swift; sourceTree = "<group>"; };
14D906EF2A8457AB006EB79A /* StoredFavoriteFood.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredFavoriteFood.swift; sourceTree = "<group>"; };
14D906F12A845868006EB79A /* FavoriteFoodListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteFoodListRow.swift; sourceTree = "<group>"; };
15ABBB61554CA19FAA58E419 /* FloatingActionArea.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FloatingActionArea.swift; sourceTree = "<group>"; };
1D096BF924C242300078B6B5 /* CheckmarkListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkListItem.swift; sourceTree = "<group>"; };
1D096C0424C624F70078B6B5 /* InsulinModelSettings+LoopKitUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "InsulinModelSettings+LoopKitUI.swift"; sourceTree = "<group>"; };
1D0E708426BDE3BB00AECF0D /* HKDeviceCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKDeviceCodableTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2323,6 +2325,7 @@
84EE97802D71293E00D5E941 /* GlucoseHistoryLayer.swift */,
84EE97B02D7A42DB00D5E941 /* ChartPointsScatterBorderedCirclesLayer.swift */,
84EE97B22D7A59AE00D5E941 /* ManualBolusDoseChartLayer.swift */,
15ABBB61554CA19FAA58E419 /* FloatingActionArea.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down
23 changes: 19 additions & 4 deletions LoopKitUI/Extensions/Keyboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,39 @@ 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<Notification.Name> = [
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 {
return
}

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
}

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)
}
}
}
}
60 changes: 46 additions & 14 deletions LoopKitUI/Extensions/View+KeyboardAware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool>, 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)
}
}
9 changes: 9 additions & 0 deletions LoopKitUI/View Controllers/DismissibleHostingController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2020 LoopKit Authors. All rights reserved.
//

import Combine
import SwiftUI

public struct _DismissibleHostingView<Content: View>: View {
Expand Down Expand Up @@ -49,6 +50,7 @@ public class DismissibleHostingController<Content: View>: UIHostingController<_D
}

private var onDisappear: () -> Void = {}
private var keyboardObservation: AnyCancellable?

public convenience init (
content: Content,
Expand Down Expand Up @@ -106,6 +108,13 @@ public class DismissibleHostingController<Content: View>: 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) {
Expand Down
15 changes: 13 additions & 2 deletions LoopKitUI/Views/CardList/Card.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?]
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down
2 changes: 1 addition & 1 deletion LoopKitUI/Views/CardList/CardList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public struct CardList<Trailer: View>: View {
}
}
}
.background(Color(.systemGroupedBackground))
.background(Color(.systemGroupedBackground).ignoresSafeArea(.container, edges: .bottom))
}

@ViewBuilder
Expand Down
11 changes: 2 additions & 9 deletions LoopKitUI/Views/ConfigurationPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ public struct ConfigurationPage<ActionAreaContent: View>: 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(
Expand All @@ -54,7 +53,6 @@ public struct ConfigurationPage<ActionAreaContent: View>: View {
)
.buttonStyle(ActionButtonStyle(.primary))
.disabled(actionButtonState != .enabled)
.padding()
.accessibilityIdentifier("button_confirmSave")

if let secondaryActionButtonTitle, let secondaryAction {
Expand All @@ -75,15 +73,10 @@ public struct ConfigurationPage<ActionAreaContent: View>: 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)
}
}

Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion LoopKitUI/Views/ConfirmationToggle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ public struct ConfirmationToggle<Label: View, ActionLabel: View>: View {
self.alertBody = alertBody
self.confirmAction = confirmAction
self._isOn = isOn
self.showConfirmAlert = showConfirmAlert
}

public var body: some View {
Expand Down
3 changes: 3 additions & 0 deletions LoopKitUI/Views/DismissibleKeyboardTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
4 changes: 2 additions & 2 deletions LoopKitUI/Views/FavoriteFoodListRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading