Skip to content

Commit 37a9bd8

Browse files
authored
Utilities to support new bolus screen (#165)
* Support for new bolus screen * Pull down reusable utilities * Support pop navigation in DismissibleHostingController
1 parent d6c8184 commit 37a9bd8

7 files changed

Lines changed: 214 additions & 10 deletions

File tree

LoopKit.xcodeproj/project.pbxproj

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,9 @@
470470
89CA2B35226D1624004D9350 /* MutableCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CA2B33226D15E0004D9350 /* MutableCollection.swift */; };
471471
89CA2B36226D1627004D9350 /* MutableCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CA2B33226D15E0004D9350 /* MutableCollection.swift */; };
472472
89CA2B38226D4456004D9350 /* DateRelativeQuantity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CA2B37226D4456004D9350 /* DateRelativeQuantity.swift */; };
473+
89CAB36B24C9EC25009EE3CE /* DismissibleKeyboardTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CAB36A24C9EC25009EE3CE /* DismissibleKeyboardTextField.swift */; };
474+
89CAB36D24C9EC98009EE3CE /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CAB36C24C9EC98009EE3CE /* Keyboard.swift */; };
475+
89CAB36F24C9ECCA009EE3CE /* View+KeyboardAware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CAB36E24C9ECCA009EE3CE /* View+KeyboardAware.swift */; };
473476
89CC35D42403450E008FB633 /* ThumbView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CC35D32403450E008FB633 /* ThumbView.swift */; };
474477
89D2048221CC7BD8001238CC /* MockKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 89D2047421CC7BD7001238CC /* MockKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
475478
89D2048921CC7BF7001238CC /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4301582C1C7ECD7A00B64B63 /* HealthKit.framework */; };
@@ -1376,6 +1379,9 @@
13761379
89BE75CA2464BC2000B145D9 /* AlertContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertContent.swift; sourceTree = "<group>"; };
13771380
89CA2B33226D15E0004D9350 /* MutableCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableCollection.swift; sourceTree = "<group>"; };
13781381
89CA2B37226D4456004D9350 /* DateRelativeQuantity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateRelativeQuantity.swift; sourceTree = "<group>"; };
1382+
89CAB36A24C9EC25009EE3CE /* DismissibleKeyboardTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissibleKeyboardTextField.swift; sourceTree = "<group>"; };
1383+
89CAB36C24C9EC98009EE3CE /* Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = "<group>"; };
1384+
89CAB36E24C9ECCA009EE3CE /* View+KeyboardAware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+KeyboardAware.swift"; sourceTree = "<group>"; };
13791385
89CC35D32403450E008FB633 /* ThumbView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbView.swift; sourceTree = "<group>"; };
13801386
89CCD4F121A87D340068C3FB /* MockCGMDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCGMDataSource.swift; sourceTree = "<group>"; };
13811387
89CCD4F321A8A2B30068C3FB /* MockCGMManager+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockCGMManager+UI.swift"; sourceTree = "<group>"; };
@@ -1841,6 +1847,7 @@
18411847
E916F56724AD31F900BE3547 /* Settings Editors */,
18421848
E9077D2824ACD5AA0066A88D /* Information Screens */,
18431849
1D096BF924C242300078B6B5 /* CheckmarkListItem.swift */,
1850+
89CAB36A24C9EC25009EE3CE /* DismissibleKeyboardTextField.swift */,
18441851
);
18451852
path = Views;
18461853
sourceTree = "<group>";
@@ -2413,15 +2420,17 @@
24132420
B4C004B8241085FE00B40429 /* Color.swift */,
24142421
B4F3D24E24AF59B50095CE44 /* DeviceLifecycleProgressState+Color.swift */,
24152422
B46A70B924B3B4BF00B4DEEC /* DeviceStatusHighlight+Color.swift */,
2423+
E96175AD24B7BE38008E5080 /* Dictionary.swift */,
24162424
89BE75C62464B4A900B145D9 /* Environment+Dismiss.swift */,
24172425
B429D66B24BF7204003E1B4A /* GlucoseTrend.swift */,
24182426
B42C950824A3BCC200857C73 /* GlucoseValueType+Color.swift */,
2427+
89186C0624BF7FC70003D0F3 /* Guardrail+UI.swift */,
24192428
C1E4B309242E99A800E70CCB /* Image.swift */,
2429+
89CAB36C24C9EC98009EE3CE /* Keyboard.swift */,
24202430
E916F56A24AD346D00BE3547 /* UIColor+LoopKit.swift */,
2421-
E96175AD24B7BE38008E5080 /* Dictionary.swift */,
2422-
89186C0624BF7FC70003D0F3 /* Guardrail+UI.swift */,
24232431
B41A60B123D1DBC700636320 /* UIFont.swift */,
24242432
B429D66D24BF7255003E1B4A /* UIImage.swift */,
2433+
89CAB36E24C9ECCA009EE3CE /* View+KeyboardAware.swift */,
24252434
);
24262435
path = Extensions;
24272436
sourceTree = "<group>";
@@ -3112,6 +3121,7 @@
31123121
89AF78C22447E353002B4FCC /* Splat.swift in Sources */,
31133122
893C9F8C2447DBD900CD4185 /* CardBuilder.swift in Sources */,
31143123
89BE75C524649C8100B145D9 /* NewScheduleItemEditor.swift in Sources */,
3124+
89CAB36F24C9ECCA009EE3CE /* View+KeyboardAware.swift in Sources */,
31153125
89653C822473592600E1BAA5 /* CarbRatioScheduleEditor.swift in Sources */,
31163126
1D66303D24C661FD00F2EF6E /* SuspendThresholdReview.swift in Sources */,
31173127
1D1FCE2B24BE704A000300A8 /* TherapySetting+Settings.swift in Sources */,
@@ -3121,6 +3131,7 @@
31213131
895FE08022011F0C00FCF18A /* EmojiDataSource.swift in Sources */,
31223132
432CF86920D76B320066B889 /* SetupButton.swift in Sources */,
31233133
898E6E702241EDB70019E459 /* PercentageTextFieldTableViewController.swift in Sources */,
3134+
89CAB36D24C9EC98009EE3CE /* Keyboard.swift in Sources */,
31243135
1DEF977524C62F8400D630CB /* SupportedInsulinModelSettings.swift in Sources */,
31253136
89186C0724BF7FC70003D0F3 /* Guardrail+UI.swift in Sources */,
31263137
432CF87420D774520066B889 /* NumberFormatter.swift in Sources */,
@@ -3234,6 +3245,7 @@
32343245
892A5D9A2231E0E4008961AB /* SettingsNavigationViewController.swift in Sources */,
32353246
B46B62B123FF0CA6001E69BA /* DescriptiveText.swift in Sources */,
32363247
E949E38924AFC82F00024DA0 /* DeliveryLimitsInformationView.swift in Sources */,
3248+
89CAB36B24C9EC25009EE3CE /* DismissibleKeyboardTextField.swift in Sources */,
32373249
89AC9DCB24529927004A6B8A /* QuantityPicker.swift in Sources */,
32383250
1D096BFA24C242300078B6B5 /* CheckmarkListItem.swift in Sources */,
32393251
B4C004D22416961300B40429 /* GuideNavigationButton.swift in Sources */,

LoopKit/QuantityFormatter.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ open class QuantityFormatter {
1515
public init() {
1616
}
1717

18+
public convenience init(for unit: HKUnit) {
19+
self.init()
20+
setPreferredNumberFormatter(for: unit)
21+
}
22+
1823
/// The unit style determines how the unit strings are abbreviated, and spacing between the value and unit
1924
open var unitStyle: Formatter.UnitStyle = .medium {
2025
didSet {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// Keyboard.swift
3+
// LoopKitUI
4+
//
5+
// Created by Michael Pangburn on 7/18/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Combine
10+
import UIKit
11+
12+
13+
public final class Keyboard: ObservableObject {
14+
public struct State {
15+
public var height: CGFloat = 0
16+
public var animationDuration: TimeInterval = 0.25
17+
}
18+
19+
@Published var state = State()
20+
private var keyboardFrameChangeCancellable: AnyCancellable?
21+
22+
static let shared = Keyboard()
23+
24+
private init() {
25+
keyboardFrameChangeCancellable = NotificationCenter.default
26+
.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
27+
.receive(on: DispatchQueue.main)
28+
.sink { [weak self] notification in
29+
guard let self = self, let userInfo = notification.userInfo else {
30+
return
31+
}
32+
33+
let height: CGFloat
34+
if let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
35+
height = UIScreen.main.bounds.intersection(keyboardFrame).height
36+
} else {
37+
height = 0
38+
}
39+
40+
let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0.25
41+
42+
self.state = State(height: height, animationDuration: animationDuration)
43+
}
44+
}
45+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// View+KeyboardAware.swift
3+
// LoopKitUI
4+
//
5+
// Created by Michael Pangburn on 7/22/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
12+
// NOTE: In iOS 14, keyboard management is handled automatically in SwiftUI.
13+
extension View {
14+
public func onKeyboardStateChange(perform updateForKeyboardState: @escaping (_ keyboardHeight: Keyboard.State) -> Void) -> some View {
15+
onReceive(Keyboard.shared.$state, perform: updateForKeyboardState)
16+
}
17+
18+
public func keyboardAware() -> some View {
19+
modifier(KeyboardAware())
20+
}
21+
}
22+
23+
public struct KeyboardAware: ViewModifier {
24+
@State var keyboardHeight: CGFloat = 0
25+
26+
public func body(content: Content) -> some View {
27+
content
28+
.padding(.bottom, keyboardHeight)
29+
.edgesIgnoringSafeArea(keyboardHeight > 0 ? .bottom : [])
30+
.onKeyboardStateChange { state in
31+
if state.height == 0 {
32+
// Only animate the transition as the keyboard comes up; animating the opposite direction is jittery.
33+
self.keyboardHeight = 0
34+
} else {
35+
withAnimation(.easeInOut(duration: state.animationDuration)) {
36+
self.keyboardHeight = state.height
37+
}
38+
}
39+
}
40+
}
41+
}

LoopKitUI/View Controllers/DismissableHostingController.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,41 @@ import SwiftUI
1010

1111

1212
public class DismissibleHostingController: UIHostingController<AnyView> {
13+
public enum DismissalMode {
14+
case modalDismiss
15+
case pop(to: UIViewController.Type)
16+
}
17+
1318
private var onDisappear: () -> Void = {}
1419

15-
public convenience init<Content: View>(rootView: Content, onDisappear: @escaping () -> Void = {}) {
20+
public convenience init<Content: View>(
21+
rootView: Content,
22+
dismissalMode: DismissalMode = .modalDismiss,
23+
isModalInPresentation: Bool = true,
24+
onDisappear: @escaping () -> Void = {}
25+
) {
1626
// Delay initialization of dismissal closure pushed into SwiftUI Environment until after calling the designated initializer
1727
var dismiss = {}
1828
self.init(rootView: AnyView(rootView.environment(\.dismiss, { dismiss() })))
19-
dismiss = { [weak self] in self?.dismiss(animated: true) }
20-
self.onDisappear = onDisappear
2129

22-
isModalInPresentation = true
30+
switch dismissalMode {
31+
case .modalDismiss:
32+
dismiss = { [weak self] in self?.dismiss(animated: true) }
33+
case .pop(to: let PredecessorViewController):
34+
dismiss = { [weak self] in
35+
guard
36+
let navigationController = self?.navigationController,
37+
let predecessor = navigationController.viewControllers.last(where: { $0.isKind(of: PredecessorViewController) })
38+
else {
39+
return
40+
}
41+
42+
navigationController.popToViewController(predecessor, animated: true)
43+
}
44+
}
45+
46+
self.onDisappear = onDisappear
47+
self.isModalInPresentation = isModalInPresentation
2348
}
2449

2550
public override func viewWillDisappear(_ animated: Bool) {

LoopKitUI/Views/ActionButtonStyle.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import SwiftUI
1010

1111

12-
struct ActionButtonStyle: ButtonStyle {
13-
enum ButtonType {
12+
public struct ActionButtonStyle: ButtonStyle {
13+
public enum ButtonType {
1414
case primary
1515
case secondary
1616
case destructive
@@ -22,7 +22,7 @@ struct ActionButtonStyle: ButtonStyle {
2222
private let cornerRadius: CGFloat = 10
2323
private let squidge: CGFloat = 1
2424

25-
init(_ style: ButtonType = .primary) {
25+
public init(_ style: ButtonType = .primary) {
2626
switch style {
2727
case .primary:
2828
fontColor = .white
@@ -39,7 +39,7 @@ struct ActionButtonStyle: ButtonStyle {
3939
}
4040
}
4141

42-
func makeBody(configuration: Configuration) -> some View {
42+
public func makeBody(configuration: Configuration) -> some View {
4343
configuration.label
4444
.padding(configuration.isPressed ? -squidge : 0)
4545
.padding()
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// DismissibleKeyboardTextField.swift
3+
// LoopKitUI
4+
//
5+
// Created by Michael Pangburn on 7/22/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
12+
public struct DismissibleKeyboardTextField: UIViewRepresentable {
13+
@Binding var text: String
14+
var placeholder: String
15+
var font: UIFont
16+
var textColor: UIColor
17+
var textAlignment: NSTextAlignment
18+
var keyboardType: UIKeyboardType
19+
20+
public init(
21+
text: Binding<String>,
22+
placeholder: String,
23+
font: UIFont = .preferredFont(forTextStyle: .body),
24+
textColor: UIColor = .label,
25+
textAlignment: NSTextAlignment = .natural,
26+
keyboardType: UIKeyboardType = .default
27+
) {
28+
self._text = text
29+
self.placeholder = placeholder
30+
self.font = font
31+
self.textColor = textColor
32+
self.textAlignment = textAlignment
33+
self.keyboardType = keyboardType
34+
}
35+
36+
public func makeUIView(context: Context) -> UITextField {
37+
let textField = UITextField()
38+
textField.placeholder = placeholder
39+
textField.font = font
40+
textField.textColor = textColor
41+
textField.textAlignment = textAlignment
42+
textField.keyboardType = keyboardType
43+
textField.inputAccessoryView = makeDoneToolbar(for: textField)
44+
textField.addTarget(context.coordinator, action: #selector(Coordinator.textChanged), for: .editingChanged)
45+
return textField
46+
}
47+
48+
private func makeDoneToolbar(for textField: UITextField) -> UIToolbar {
49+
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
50+
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
51+
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: textField, action: #selector(UITextField.resignFirstResponder))
52+
toolbar.items = [flexibleSpace, doneButton]
53+
toolbar.sizeToFit()
54+
return toolbar
55+
}
56+
57+
public func updateUIView(_ textField: UITextField, context: Context) {
58+
textField.text = text
59+
}
60+
61+
public func makeCoordinator() -> Coordinator {
62+
Coordinator(self)
63+
}
64+
65+
public final class Coordinator {
66+
var parent: DismissibleKeyboardTextField
67+
68+
init(_ parent: DismissibleKeyboardTextField) {
69+
self.parent = parent
70+
}
71+
72+
@objc fileprivate func textChanged(_ textField: UITextField) {
73+
parent.text = textField.text ?? ""
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)