Skip to content

Commit 0179a6a

Browse files
author
Rick Pasetto
authored
DIY: Add "Services" section to Settings for DIY only (#209)
* checkpoint * DIY: Add "Services" section to Settings for DIY only * Change "hack" to switch presence of Services to a FeatureFlag (will require a concomitant change to `Loop/LoopOverride.xcconfig`)
1 parent 08fa11b commit 0179a6a

File tree

6 files changed

+157
-12
lines changed

6 files changed

+157
-12
lines changed

Common/FeatureFlags.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct FeatureFlagConfiguration: Decodable {
2020
let walshInsulinModelEnabled: Bool
2121
let fiaspInsulinModelEnabled: Bool
2222
let observeHealthKitSamplesFromOtherApps: Bool
23+
let includeServicesInSettingsEnabled: Bool
2324

2425
fileprivate init() {
2526
// Swift compiler config is inverse, since the default state is enabled.
@@ -81,6 +82,13 @@ struct FeatureFlagConfiguration: Decodable {
8182
#else
8283
self.observeHealthKitSamplesFromOtherApps = true
8384
#endif
85+
86+
// Swift compiler config is inverse, since the default state is enabled.
87+
#if INCLUDE_SERVICES_IN_SETTINGS_DISABLED
88+
self.includeServicesInSettingsEnabled = false
89+
#else
90+
self.includeServicesInSettingsEnabled = true
91+
#endif
8492
}
8593
}
8694

@@ -97,6 +105,7 @@ extension FeatureFlagConfiguration : CustomDebugStringConvertible {
97105
"* walshInsulinModelEnabled: \(walshInsulinModelEnabled)",
98106
"* fiaspInsulinModelEnabled: \(fiaspInsulinModelEnabled)",
99107
"* observeHealthKitSamplesFromOtherApps: \(observeHealthKitSamplesFromOtherApps)",
108+
"* includeServicesInSettingsEnabled: \(includeServicesInSettingsEnabled)",
100109
].joined(separator: "\n")
101110
}
102111
}

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D05219C2469F1F5000EBBDE /* AlertStore.swift */; };
2727
1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1D080CBB2473214A00356610 /* AlertStore.xcdatamodeld */; };
2828
1D2609AD248EEB9900A6F258 /* LoopAlertsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2609AC248EEB9900A6F258 /* LoopAlertsManager.swift */; };
29+
1D49795824E7289700948F05 /* ServicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D49795724E7289700948F05 /* ServicesViewModel.swift */; };
2930
1D4990E824A25931005CC357 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E267FB2292456700A3F2AF /* FeatureFlags.swift */; };
3031
1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */; };
3132
1D4A3E2E2478628500FD601B /* StoredAlert+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */; };
@@ -644,6 +645,7 @@
644645
1D05219C2469F1F5000EBBDE /* AlertStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStore.swift; sourceTree = "<group>"; };
645646
1D080CBC2473214A00356610 /* AlertStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AlertStore.xcdatamodel; sourceTree = "<group>"; };
646647
1D2609AC248EEB9900A6F258 /* LoopAlertsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAlertsManager.swift; sourceTree = "<group>"; };
648+
1D49795724E7289700948F05 /* ServicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesViewModel.swift; sourceTree = "<group>"; };
647649
1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataClass.swift"; sourceTree = "<group>"; };
648650
1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataProperties.swift"; sourceTree = "<group>"; };
649651
1D80313C24746274002810DF /* AlertStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStoreTests.swift; sourceTree = "<group>"; };
@@ -1728,6 +1730,7 @@
17281730
89CAB36224C8FE95009EE3CE /* PredictedGlucoseChartView.swift */,
17291731
438D42FA1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift */,
17301732
439706E522D2E84900C81566 /* PredictionSettingTableViewCell.swift */,
1733+
1D49795724E7289700948F05 /* ServicesViewModel.swift */,
17311734
43C3B6EB20B650A80026CAFA /* SettingsImageTableViewCell.swift */,
17321735
1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */,
17331736
1DB1CA4E24A56D7600B3B94C /* SettingsViewModel.swift */,
@@ -2876,6 +2879,7 @@
28762879
4372E487213C86240068E043 /* SampleValue.swift in Sources */,
28772880
437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */,
28782881
C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */,
2882+
1D49795824E7289700948F05 /* ServicesViewModel.swift in Sources */,
28792883
1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */,
28802884
892D7C5123B54A15008A9656 /* CarbEntryViewController.swift in Sources */,
28812885
A999D40424663CE1004C89D4 /* DoseStore.swift in Sources */,

Loop/View Controllers/SettingsTableViewController.swift

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -629,10 +629,7 @@ final class SettingsTableViewController: UITableViewController, IdentifiableClas
629629
case .services:
630630
if indexPath.row < activeServices.count {
631631
if let serviceUI = activeServices[indexPath.row] as? ServiceUI {
632-
var settings = serviceUI.settingsViewController(chartColors: .primary, carbTintColor: .carbTintColor, glucoseTintColor: .glucoseTintColor, guidanceColors: .default, insulinTintColor: .insulinTintColor)
633-
settings.serviceSettingsDelegate = self
634-
settings.completionDelegate = self
635-
present(settings, animated: true)
632+
didTapService(serviceUI)
636633
}
637634
tableView.deselectRow(at: indexPath, animated: true)
638635
} else {
@@ -792,10 +789,15 @@ final class SettingsTableViewController: UITableViewController, IdentifiableClas
792789
bolusVolumes: $0.supportedBolusVolumes,
793790
maximumBasalScheduleEntryCount: $0.maximumBasalScheduleEntryCount)
794791
}
792+
let servicesViewModel = ServicesViewModel(showServices: FeatureFlags.includeServicesInSettingsEnabled,
793+
availableServices: availableServices,
794+
activeServices: activeServices,
795+
delegate: self)
795796
let viewModel = SettingsViewModel(appNameAndVersion: Bundle.main.localizedNameAndVersion,
796797
notificationsCriticalAlertPermissionsViewModel: notificationsCriticalAlertPermissionsViewModel,
797798
pumpManagerSettingsViewModel: pumpViewModel,
798799
cgmManagerSettingsViewModel: cgmViewModel,
800+
servicesViewModel: servicesViewModel,
799801
therapySettings: dataManager.loopManager.therapySettings,
800802
supportedInsulinModelSettings: SupportedInsulinModelSettings(fiaspModelEnabled: FeatureFlags.fiaspInsulinModelEnabled, walshModelEnabled: FeatureFlags.walshInsulinModelEnabled),
801803
pumpSupportedIncrements: pumpSupportedIncrements,
@@ -993,7 +995,14 @@ extension SettingsTableViewController {
993995
fileprivate var inactiveServices: [AvailableService] {
994996
return availableServices.filter { availableService in !dataManager.servicesManager.activeServices.contains { type(of: $0).serviceIdentifier == availableService.identifier } }
995997
}
996-
998+
999+
fileprivate func didTapService(_ serviceUI: ServiceUI) {
1000+
var settings = serviceUI.settingsViewController(chartColors: .primary, carbTintColor: .carbTintColor, glucoseTintColor: .glucoseTintColor, guidanceColors: .default, insulinTintColor: .insulinTintColor)
1001+
settings.serviceSettingsDelegate = self
1002+
settings.completionDelegate = self
1003+
present(settings, animated: true)
1004+
}
1005+
9971006
fileprivate func setupService(withIdentifier identifier: String) {
9981007
guard let serviceUIType = dataManager.servicesManager.serviceUITypeByIdentifier(identifier) else {
9991008
return
@@ -1022,6 +1031,18 @@ extension SettingsTableViewController: ServiceSettingsDelegate {
10221031
}
10231032
}
10241033

1034+
extension SettingsTableViewController: ServicesViewModelDelegate {
1035+
func addService(identifier: String) {
1036+
setupService(withIdentifier: identifier)
1037+
}
1038+
func gotoService(identifier: String) {
1039+
guard let serviceUI = activeServices.first(where: { $0.serviceIdentifier == identifier }) as? ServiceUI else {
1040+
return
1041+
}
1042+
didTapService(serviceUI)
1043+
}
1044+
}
1045+
10251046
private extension UIAlertController {
10261047
convenience init(pumpDataDeletionHandler handler: @escaping () -> Void) {
10271048
self.init(

Loop/Views/ServicesViewModel.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// ServicesViewModel.swift
3+
// Loop
4+
//
5+
// Created by Rick Pasetto on 8/14/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import LoopKit
10+
import SwiftUI
11+
12+
public protocol ServicesViewModelDelegate: class {
13+
func addService(identifier: String)
14+
func gotoService(identifier: String)
15+
}
16+
17+
public class ServicesViewModel: ObservableObject {
18+
19+
@Published var showServices: Bool
20+
@Published var availableServices: [AvailableService]
21+
@Published var activeServices: [Service]
22+
23+
var inactiveServices: [AvailableService] {
24+
return availableServices.filter { availableService in
25+
!activeServices.contains { $0.serviceIdentifier == availableService.identifier }
26+
}
27+
}
28+
29+
weak var delegate: ServicesViewModelDelegate?
30+
31+
init(showServices: Bool,
32+
availableServices: [AvailableService],
33+
activeServices: [Service],
34+
delegate: ServicesViewModelDelegate? = nil) {
35+
self.showServices = showServices
36+
self.activeServices = activeServices
37+
self.availableServices = availableServices
38+
self.delegate = delegate
39+
}
40+
41+
func didTapService(_ index: Int) {
42+
delegate?.gotoService(identifier: activeServices[index].serviceIdentifier)
43+
}
44+
45+
func didTapAddService(_ availableService: AvailableService) {
46+
delegate?.addService(identifier: availableService.identifier)
47+
}
48+
}

Loop/Views/SettingsView.swift

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import LoopKit
1010
import LoopKitUI
11+
import MockKit
1112
import SwiftUI
1213

1314
public struct SettingsView: View, HorizontalSizeClassOverride {
@@ -16,6 +17,8 @@ public struct SettingsView: View, HorizontalSizeClassOverride {
1617

1718
@ObservedObject var viewModel: SettingsViewModel
1819

20+
@State var showServiceChooser: Bool = false
21+
1922
public init(viewModel: SettingsViewModel) {
2023
self.viewModel = viewModel
2124
}
@@ -29,6 +32,9 @@ public struct SettingsView: View, HorizontalSizeClassOverride {
2932
}
3033
therapySettingsSection
3134
deviceSettingsSection
35+
if viewModel.servicesViewModel.showServices {
36+
servicesSection
37+
}
3238
if viewModel.pumpManagerSettingsViewModel.isTestingDevice {
3339
deletePumpDataSection
3440
}
@@ -81,13 +87,14 @@ extension SettingsView {
8187

8288
private var therapySettingsSection: some View {
8389
Section(header: SectionHeader(label: NSLocalizedString("Configuration", comment: "The title of the Configuration section in settings"))) {
84-
return NavigationLink(destination: TherapySettingsView(viewModel: TherapySettingsViewModel(mode: .settings,
85-
therapySettings: viewModel.therapySettings,
86-
supportedInsulinModelSettings: viewModel.supportedInsulinModelSettings,
87-
pumpSupportedIncrements: viewModel.pumpSupportedIncrements,
88-
syncPumpSchedule: viewModel.syncPumpSchedule,
89-
chartColors: .primary,
90-
didSave: viewModel.didSave))) {
90+
return NavigationLink(destination: TherapySettingsView(
91+
viewModel: TherapySettingsViewModel(mode: .settings,
92+
therapySettings: viewModel.therapySettings,
93+
supportedInsulinModelSettings: viewModel.supportedInsulinModelSettings,
94+
pumpSupportedIncrements: viewModel.pumpSupportedIncrements,
95+
syncPumpSchedule: viewModel.syncPumpSchedule,
96+
chartColors: .primary,
97+
didSave: viewModel.didSave))) {
9198
LargeButton(action: { },
9299
includeArrow: false,
93100
imageView: AnyView(Image("Therapy Icon")),
@@ -136,6 +143,36 @@ extension SettingsView {
136143
}
137144
}
138145

146+
private var servicesSection: some View {
147+
Section(header: SectionHeader(label: NSLocalizedString("Services", comment: "The title of the services section in settings"))) {
148+
ForEach(viewModel.servicesViewModel.activeServices.indices, id: \.self) { index in
149+
// TODO: this "dismiss then call didTapService()" here is temporary, until we've completely gotten rid of SettingsTableViewController
150+
Button(action: { self.dismiss(); self.viewModel.servicesViewModel.didTapService(index) }, label: {
151+
Text(self.viewModel.servicesViewModel.activeServices[index].localizedTitle)
152+
})
153+
.accentColor(.primary)
154+
}
155+
Button(action: { self.showServiceChooser = true }, label: {
156+
Text("Add Service", comment: "The title of the services section in settings")
157+
})
158+
.actionSheet(isPresented: $showServiceChooser) {
159+
ActionSheet(title: Text("Add Service", comment: "The title of the services section in settings"), buttons: serviceChoices)
160+
}
161+
}
162+
}
163+
164+
private var serviceChoices: [ActionSheet.Button] {
165+
var result = viewModel.servicesViewModel.inactiveServices.map { availableService in
166+
ActionSheet.Button.default(Text(availableService.localizedTitle)) {
167+
// TODO: this "dismiss then call didTapAddService()" here is temporary, until we've completely gotten rid of SettingsTableViewController
168+
self.dismiss()
169+
self.viewModel.servicesViewModel.didTapAddService(availableService)
170+
}
171+
}
172+
result.append(.cancel())
173+
return result
174+
}
175+
139176
private var deletePumpDataSection: some View {
140177
Section {
141178
Button(action: { self.viewModel.pumpManagerSettingsViewModel.deleteData?() }) {
@@ -224,12 +261,34 @@ fileprivate struct LargeButton: View {
224261
}
225262
}
226263

264+
fileprivate class FakeService1: Service {
265+
static var localizedTitle: String = "Service 1"
266+
static var serviceIdentifier: String = "FakeService1"
267+
var serviceDelegate: ServiceDelegate?
268+
var rawState: RawStateValue = [:]
269+
required init?(rawState: RawStateValue) {}
270+
convenience init() { self.init(rawState: [:])! }
271+
var available: AvailableService { AvailableService(identifier: serviceIdentifier, localizedTitle: localizedTitle) }
272+
}
273+
fileprivate class FakeService2: Service {
274+
static var localizedTitle: String = "Service 2"
275+
static var serviceIdentifier: String = "FakeService2"
276+
var serviceDelegate: ServiceDelegate?
277+
var rawState: RawStateValue = [:]
278+
required init?(rawState: RawStateValue) {}
279+
convenience init() { self.init(rawState: [:])! }
280+
var available: AvailableService { AvailableService(identifier: serviceIdentifier, localizedTitle: localizedTitle) }
281+
}
282+
fileprivate let servicesViewModel = ServicesViewModel(showServices: true,
283+
availableServices: [FakeService1().available, FakeService2().available],
284+
activeServices: [FakeService1()])
227285
public struct SettingsView_Previews: PreviewProvider {
228286
public static var previews: some View {
229287
let viewModel = SettingsViewModel(appNameAndVersion: "Tidepool Loop v1.2.3.456",
230288
notificationsCriticalAlertPermissionsViewModel: NotificationsCriticalAlertPermissionsViewModel(),
231289
pumpManagerSettingsViewModel: DeviceViewModel(),
232290
cgmManagerSettingsViewModel: DeviceViewModel(),
291+
servicesViewModel: servicesViewModel,
233292
therapySettings: TherapySettings(),
234293
supportedInsulinModelSettings: SupportedInsulinModelSettings(fiaspModelEnabled: true, walshModelEnabled: true),
235294
pumpSupportedIncrements: nil,

Loop/Views/SettingsViewModel.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class SettingsViewModel: ObservableObject {
5555

5656
var pumpManagerSettingsViewModel: DeviceViewModel
5757
var cgmManagerSettingsViewModel: DeviceViewModel
58+
var servicesViewModel: ServicesViewModel
5859
var therapySettings: TherapySettings
5960
let supportedInsulinModelSettings: SupportedInsulinModelSettings
6061
let pumpSupportedIncrements: PumpSupportedIncrements?
@@ -68,6 +69,7 @@ public class SettingsViewModel: ObservableObject {
6869
notificationsCriticalAlertPermissionsViewModel: NotificationsCriticalAlertPermissionsViewModel,
6970
pumpManagerSettingsViewModel: DeviceViewModel,
7071
cgmManagerSettingsViewModel: DeviceViewModel,
72+
servicesViewModel: ServicesViewModel,
7173
therapySettings: TherapySettings,
7274
supportedInsulinModelSettings: SupportedInsulinModelSettings,
7375
pumpSupportedIncrements: PumpSupportedIncrements?,
@@ -90,6 +92,8 @@ public class SettingsViewModel: ObservableObject {
9092
self.syncPumpSchedule = syncPumpSchedule
9193
self.sensitivityOverridesEnabled = sensitivityOverridesEnabled
9294
self.didSave = didSave
95+
96+
self.servicesViewModel = servicesViewModel
9397

9498
// This strangeness ensures the composed ViewModels' (ObservableObjects') changes get reported to this ViewModel (ObservableObject)
9599
notificationsCriticalAlertPermissionsViewModel.objectWillChange.sink { [weak self] in

0 commit comments

Comments
 (0)