From a6e618932aa3d2f34f595962e6769a920159625d Mon Sep 17 00:00:00 2001 From: Gavi Rawson Date: Mon, 16 Dec 2019 17:56:09 -0800 Subject: [PATCH 01/33] Add link to the new website --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd7a73e92..2180a8e11 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ The primary CareKit framework codebase supports iOS and requires Xcode 11.0 or n # Getting Started +* [Website](https://www.researchandcare.org) * [Documentation](https://developer.apple.com/documentation/carekit) -* WWDC Video: [ResearchKit and CareKit Reimagined](https://developer.apple.com/videos/play/wwdc2019/217/) +* [WWDC: ResearchKit and CareKit Reimagined](https://developer.apple.com/videos/play/wwdc2019/217/) ### Installation (Option One): SPM From 95d568b190bace1aa8daf9b2baa4e7b021f23427 Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Wed, 18 Dec 2019 10:28:04 -0800 Subject: [PATCH 02/33] Move store manager (#343) We do not want to create or keep the store in the scene delegate. If the app supports multiple windows (new in iOS 13) a new scene delegate will be instantiated for every window, which means a new store would be created too. We want to share the store across windows, so it should go in the app delegate. This updates the sample app to reflect best practices. --- OCKCatalog/OCKCatalog/AppDelegate.swift | 6 ++ .../OCKCatalog/RootViewController.swift | 4 +- OCKCatalog/OCKCatalog/SceneDelegate.swift | 5 +- OCKSample/OCKSample/AppDelegate.swift | 88 +++++++++++++++++++ OCKSample/OCKSample/SceneDelegate.swift | 88 +------------------ 5 files changed, 100 insertions(+), 91 deletions(-) diff --git a/OCKCatalog/OCKCatalog/AppDelegate.swift b/OCKCatalog/OCKCatalog/AppDelegate.swift index 80b3ff1e0..f962b0b52 100644 --- a/OCKCatalog/OCKCatalog/AppDelegate.swift +++ b/OCKCatalog/OCKCatalog/AppDelegate.swift @@ -34,6 +34,12 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { + let storeManager: OCKSynchronizedStoreManager = { + let store = OCKStore(name: "carekit-catalog") + store.fillWithDummyData() + return OCKSynchronizedStoreManager(wrapping: store) + }() + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } diff --git a/OCKCatalog/OCKCatalog/RootViewController.swift b/OCKCatalog/OCKCatalog/RootViewController.swift index 7e6d9adce..37d871dc9 100644 --- a/OCKCatalog/OCKCatalog/RootViewController.swift +++ b/OCKCatalog/OCKCatalog/RootViewController.swift @@ -58,8 +58,8 @@ class RootViewController: UITableViewController { private let storeManager: OCKSynchronizedStoreManager - init(store: OCKStore) { - self.storeManager = .init(wrapping: store) + init(storeManager: OCKSynchronizedStoreManager) { + self.storeManager = storeManager super.init(style: .grouped) } diff --git a/OCKCatalog/OCKCatalog/SceneDelegate.swift b/OCKCatalog/OCKCatalog/SceneDelegate.swift index d9c419ee8..44a6595f4 100644 --- a/OCKCatalog/OCKCatalog/SceneDelegate.swift +++ b/OCKCatalog/OCKCatalog/SceneDelegate.swift @@ -37,9 +37,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - let store = OCKStore(name: "carekit-catalog") - store.fillWithDummyData() - let rootViewController = RootViewController(store: store) + let appDelegate = UIApplication.shared.delegate as! AppDelegate + let rootViewController = RootViewController(storeManager: appDelegate.storeManager) let navigationController = UINavigationController(rootViewController: rootViewController) if let windowScene = scene as? UIWindowScene { diff --git a/OCKSample/OCKSample/AppDelegate.swift b/OCKSample/OCKSample/AppDelegate.swift index f8ca138c2..f15f572c0 100644 --- a/OCKSample/OCKSample/AppDelegate.swift +++ b/OCKSample/OCKSample/AppDelegate.swift @@ -27,11 +27,22 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +import CareKit +import Contacts import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { + // Manages synchronization of a CoreData store + lazy var synchronizedStoreManager: OCKSynchronizedStoreManager = { + let store = OCKStore(name: "SampleAppStore") + store.populateSampleData() + let manager = OCKSynchronizedStoreManager(wrapping: store) + return manager + }() + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } @@ -40,3 +51,80 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } } + +private extension OCKStore { + + // Adds tasks and contacts into the store + func populateSampleData() { + + let thisMorning = Calendar.current.startOfDay(for: Date()) + let aFewDaysAgo = Calendar.current.date(byAdding: .day, value: -4, to: thisMorning)! + let beforeBreakfast = Calendar.current.date(byAdding: .hour, value: 8, to: aFewDaysAgo)! + let afterLunch = Calendar.current.date(byAdding: .hour, value: 14, to: aFewDaysAgo)! + + let schedule = OCKSchedule(composing: [ + OCKScheduleElement(start: beforeBreakfast, end: nil, + interval: DateComponents(day: 1)), + + OCKScheduleElement(start: afterLunch, end: nil, + interval: DateComponents(day: 2)) + ]) + + var doxylamine = OCKTask(id: "doxylamine", title: "Take Doxylamine", + carePlanID: nil, schedule: schedule) + doxylamine.instructions = "Take 25mg of doxylamine when you experience nausea." + + let nauseaSchedule = OCKSchedule(composing: [ + OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 1), + text: "Anytime throughout the day", targetValues: [], duration: .allDay) + ]) + + var nausea = OCKTask(id: "nausea", title: "Track your nausea", + carePlanID: nil, schedule: nauseaSchedule) + nausea.impactsAdherence = false + nausea.instructions = "Tap the button below anytime you experience nausea." + + let kegelSchedule = OCKSchedule(composing: [OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 2))]) + var kegels = OCKTask(id: "kegels", title: "Kegel Exercises", carePlanID: nil, schedule: kegelSchedule) + kegels.impactsAdherence = true + kegels.instructions = "Perform kegel exercies" + + addTasks([nausea, doxylamine, kegels], callbackQueue: .main, completion: nil) + + var contact1 = OCKContact(id: "jane", givenName: "Jane", + familyName: "Daniels", carePlanID: nil) + contact1.asset = "JaneDaniels" + contact1.title = "Family Practice Doctor" + contact1.role = "Dr. Daniels is a family practice doctor with 8 years of experience." + contact1.emailAddresses = [OCKLabeledValue(label: CNLabelEmailiCloud, value: "janedaniels@icloud.com")] + contact1.phoneNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] + contact1.messagingNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] + + contact1.address = { + let address = OCKPostalAddress() + address.street = "2598 Reposa Way" + address.city = "San Francisco" + address.state = "CA" + address.postalCode = "94127" + return address + }() + + var contact2 = OCKContact(id: "matthew", givenName: "Matthew", + familyName: "Reiff", carePlanID: nil) + contact2.asset = "MatthewReiff" + contact2.title = "OBGYN" + contact2.role = "Dr. Reiff is an OBGYN with 13 years of experience." + contact2.phoneNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] + contact2.messagingNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] + contact2.address = { + let address = OCKPostalAddress() + address.street = "396 El Verano Way" + address.city = "San Francisco" + address.state = "CA" + address.postalCode = "94127" + return address + }() + + addContacts([contact1, contact2]) + } +} diff --git a/OCKSample/OCKSample/SceneDelegate.swift b/OCKSample/OCKSample/SceneDelegate.swift index b32430f24..fb69ba8bb 100644 --- a/OCKSample/OCKSample/SceneDelegate.swift +++ b/OCKSample/OCKSample/SceneDelegate.swift @@ -29,23 +29,16 @@ */ import CareKit -import Contacts import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - // Manages synchronization of a CoreData store - lazy var manager: OCKSynchronizedStoreManager = { - let store = OCKStore(name: "SampleAppStore") - store.populateSampleData() - let manager = OCKSynchronizedStoreManager(wrapping: store) - return manager - }() - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + let appDelegate = UIApplication.shared.delegate as! AppDelegate + let manager = appDelegate.synchronizedStoreManager let careViewController = UINavigationController(rootViewController: CareViewController(storeManager: manager)) if let windowScene = scene as? UIWindowScene { @@ -56,80 +49,3 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } } - -private extension OCKStore { - - // Adds tasks and contacts into the store - func populateSampleData() { - - let thisMorning = Calendar.current.startOfDay(for: Date()) - let aFewDaysAgo = Calendar.current.date(byAdding: .day, value: -4, to: thisMorning)! - let beforeBreakfast = Calendar.current.date(byAdding: .hour, value: 8, to: aFewDaysAgo)! - let afterLunch = Calendar.current.date(byAdding: .hour, value: 14, to: aFewDaysAgo)! - - let schedule = OCKSchedule(composing: [ - OCKScheduleElement(start: beforeBreakfast, end: nil, - interval: DateComponents(day: 1)), - - OCKScheduleElement(start: afterLunch, end: nil, - interval: DateComponents(day: 2)) - ]) - - var doxylamine = OCKTask(id: "doxylamine", title: "Take Doxylamine", - carePlanID: nil, schedule: schedule) - doxylamine.instructions = "Take 25mg of doxylamine when you experience nausea." - - let nauseaSchedule = OCKSchedule(composing: [ - OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 1), - text: "Anytime throughout the day", targetValues: [], duration: .allDay) - ]) - - var nausea = OCKTask(id: "nausea", title: "Track your nausea", - carePlanID: nil, schedule: nauseaSchedule) - nausea.impactsAdherence = false - nausea.instructions = "Tap the button below anytime you experience nausea." - - let kegelSchedule = OCKSchedule(composing: [OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 2))]) - var kegels = OCKTask(id: "kegels", title: "Kegel Exercises", carePlanID: nil, schedule: kegelSchedule) - kegels.impactsAdherence = true - kegels.instructions = "Perform kegel exercies" - - addTasks([nausea, doxylamine, kegels], callbackQueue: .main, completion: nil) - - var contact1 = OCKContact(id: "jane", givenName: "Jane", - familyName: "Daniels", carePlanID: nil) - contact1.asset = "JaneDaniels" - contact1.title = "Family Practice Doctor" - contact1.role = "Dr. Daniels is a family practice doctor with 8 years of experience." - contact1.emailAddresses = [OCKLabeledValue(label: CNLabelEmailiCloud, value: "janedaniels@icloud.com")] - contact1.phoneNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] - contact1.messagingNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] - - contact1.address = { - let address = OCKPostalAddress() - address.street = "2598 Reposa Way" - address.city = "San Francisco" - address.state = "CA" - address.postalCode = "94127" - return address - }() - - var contact2 = OCKContact(id: "matthew", givenName: "Matthew", - familyName: "Reiff", carePlanID: nil) - contact2.asset = "MatthewReiff" - contact2.title = "OBGYN" - contact2.role = "Dr. Reiff is an OBGYN with 13 years of experience." - contact2.phoneNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] - contact2.messagingNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] - contact2.address = { - let address = OCKPostalAddress() - address.street = "396 El Verano Way" - address.city = "San Francisco" - address.state = "CA" - address.postalCode = "94127" - return address - }() - - addContacts([contact1, contact2]) - } -} From 74a078f7c0f214a81e71cab72b21784b82647d1c Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Wed, 18 Dec 2019 10:33:28 -0800 Subject: [PATCH 03/33] Date selection bug fix (#344) This change addresses a date selection bug that affected certain locales, including Germany. It also adds unit test passes in several new regions to ensure that this bug does not regress. --- CareKit.xctestplan | 87 ++++++++++++++++++- .../OCKCartesianChartViewSynchronizer.swift | 8 +- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/CareKit.xctestplan b/CareKit.xctestplan index 6fae851ec..8a337776a 100644 --- a/CareKit.xctestplan +++ b/CareKit.xctestplan @@ -2,10 +2,93 @@ "configurations" : [ { "id" : "28C2AC89-B957-4FB0-9242-53AEB9E6D4B0", - "name" : "Configuration 1", + "name" : "Default", "options" : { } + }, + { + "id" : "42D7C666-B3E1-4F36-88FB-E0B87436BB05", + "name" : "Germany", + "options" : { + "language" : "de", + "locationScenario" : { + "identifier" : "London, England", + "referenceType" : "built-in" + }, + "region" : "DE" + } + }, + { + "id" : "76269B8E-AEA1-4864-AA9A-F583B2F54AF1", + "name" : "Africa", + "options" : { + "locationScenario" : { + "identifier" : "Johannesburg, South Africa", + "referenceType" : "built-in" + }, + "region" : "EG" + } + }, + { + "id" : "34F9B126-5A5C-4F0A-A861-84C9B2376FA2", + "name" : "Australia", + "options" : { + "language" : "en-AU", + "locationScenario" : { + "identifier" : "Sydney, Australia", + "referenceType" : "built-in" + }, + "region" : "AU" + } + }, + { + "id" : "05B1E279-40AF-49C6-8BAC-86779932468D", + "name" : "England", + "options" : { + "language" : "en-GB", + "locationScenario" : { + "identifier" : "London, England", + "referenceType" : "built-in" + }, + "region" : "GB" + } + }, + { + "id" : "F4D2A746-C1E8-4D5A-BCD7-EEEC3D73CEC5", + "name" : "Japan", + "options" : { + "language" : "ja", + "locationScenario" : { + "identifier" : "Tokyo, Japan", + "referenceType" : "built-in" + }, + "region" : "JP" + } + }, + { + "id" : "4DB57CAB-36AB-4D1A-8259-C918197191CE", + "name" : "Hong Kong", + "options" : { + "language" : "zh-HK", + "locationScenario" : { + "identifier" : "Hong Kong, China", + "referenceType" : "built-in" + }, + "region" : "HK" + } + }, + { + "id" : "2DEE87AE-1E91-4B64-BA06-8B37298B2A16", + "name" : "Mexico", + "options" : { + "language" : "es-419", + "locationScenario" : { + "identifier" : "Mexico City, Mexico", + "referenceType" : "built-in" + }, + "region" : "MX" + } } ], "defaultOptions" : { @@ -19,7 +102,7 @@ { "containerPath" : "container:CareKitCarePlanStore\/CareKitCarePlanStore.xcodeproj", "identifier" : "E784B8F72232EED600736CA5", - "name" : "CareKitCarePlanStore" + "name" : "CareKitStore" } ] }, diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKCartesianChartViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKCartesianChartViewSynchronizer.swift index bf1924e69..05c3f0534 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKCartesianChartViewSynchronizer.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKCartesianChartViewSynchronizer.swift @@ -53,7 +53,13 @@ open class OCKCartesianChartViewSynchronizer: OCKChartViewSynchronizerProtocol { open func makeView() -> OCKCartesianChartView { let chartView = OCKCartesianChartView(type: plotType) - chartView.graphView.selectedIndex = Calendar.current.component(.weekday, from: selectedDate) - Calendar.current.firstWeekday + let currentWeekday = Calendar.current.component(.weekday, from: selectedDate) + let firstWeekday = Calendar.current.firstWeekday + var offset = (currentWeekday - 1) - (firstWeekday - 1) + if offset < 0 { + offset += 7 + } + chartView.graphView.selectedIndex = offset chartView.graphView.horizontalAxisMarkers = Calendar.current.orderedWeekdaySymbolsVeryShort() return chartView } From 00e50e5288c196110a21ebe87d9ba560485ed147 Mon Sep 17 00:00:00 2001 From: Erik Hornberger Date: Mon, 6 Jan 2020 17:52:52 -0800 Subject: [PATCH 04/33] Mark OCKHealthKitLinkage internal --- CareKitStore/CareKitStore/Structs/OCKHealthKitLinkage.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CareKitStore/CareKitStore/Structs/OCKHealthKitLinkage.swift b/CareKitStore/CareKitStore/Structs/OCKHealthKitLinkage.swift index 0bb566217..f6dcfdbfd 100644 --- a/CareKitStore/CareKitStore/Structs/OCKHealthKitLinkage.swift +++ b/CareKitStore/CareKitStore/Structs/OCKHealthKitLinkage.swift @@ -34,9 +34,9 @@ import HealthKit extension HKQuantityTypeIdentifier: Codable {} /// Describes how a task outcome values should be retrieved from HealthKit. -public struct OCKHealthKitLinkage: Equatable, Codable { +internal struct OCKHealthKitLinkage: Equatable, Codable { - public enum QuantityType: String, Codable { + internal enum QuantityType: String, Codable { /// Quantities that are defined over a period of time, such as step count or calories burned. case cumulative @@ -58,7 +58,7 @@ public struct OCKHealthKitLinkage: Equatable, Codable { /// - Parameter quantityIdentifier: A HealthKitQuantityIdentifier that describes the outcome's data type. /// - Parameter quantityType: Determines what kind of query will be used to fetch data from HealthKit. /// - Parameter unit: A HealthKit unit that will be associated with outcomes saved to and fetched from HealthKit. - public init(quantityIdentifier: HKQuantityTypeIdentifier, quantityType: QuantityType, unit: HKUnit) { + internal init(quantityIdentifier: HKQuantityTypeIdentifier, quantityType: QuantityType, unit: HKUnit) { self.quantityIdentifier = quantityIdentifier self.quantityType = quantityType self.unitString = unit.unitString From 50e57dac1827f5a1c05ee119d9569bd2c5baa8e7 Mon Sep 17 00:00:00 2001 From: Erik Hornberger Date: Mon, 13 Jan 2020 09:32:43 -0800 Subject: [PATCH 05/33] Fix crash caused by updating a task without changing its effective date These changes modify the behavior of versioned tasks to 1. Avoid a hard crash if a later version of a task has an earlier `effectiveDate` than a previous version. 2. Assure that outcomes can't be added to "shadowed" areas of past versions of tasks. Consider the following case. ``` |--------- Query Interval ------------| Task V1 ----------------------------> V2 ------------> V3-------------------> ``` When querying for events in the given interval, we start from version three and gather all outcomes. We then move on to version 2 and gather all outcomes in the region before V3 begins. A crash would occur in this case because there the time between the start of V2 and V3 is negative. **This was fixed skipping over versions that are fully shadowed**. Unit tests have been added to prevent regression. ``` |--------- Query Interval ------------| Task V1 ----------------------------> V2 -x------x--> V3-------------------> ``` Another possibly unexpected result could occur if there were outcomes saved to V2 when V3 was created. Before the creation of V3, queries would return outcomes from V2, but after the creation of V3, those outcomes would no longer be returned because V2 is shadowed by V3. **This was fixed by adding checks that make the transaction fail if updating a task or outcome would result in "shadowed" outcomes.** Unit tests have also been added to ensure the expected behavior in these cases as well. --- .../CareKitStore/CoreData/OCKCDOutcome.swift | 2 +- .../CoreData/OCKStore+Outcomes.swift | 72 +++++++++++++------ .../CoreData/OCKCoreDataTaskStore.swift | 41 +++++++++++ .../Protocols/Events/OCKEventStore.swift | 50 ++++++++++--- .../TestCoreDataSchema+Outcomes.swift | 1 + .../TestCoreDataSchemaIntegration.swift | 1 + .../OCKStore/TestStore+Outcomes.swift | 32 ++++++++- .../OCKStore/TestStore+Tasks.swift | 39 ++++++++++ 8 files changed, 206 insertions(+), 32 deletions(-) diff --git a/CareKitStore/CareKitStore/CoreData/OCKCDOutcome.swift b/CareKitStore/CareKitStore/CoreData/OCKCDOutcome.swift index 05a07f186..8c1f8ec8d 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKCDOutcome.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKCDOutcome.swift @@ -36,7 +36,7 @@ class OCKCDOutcome: OCKCDObject, OCKCDManageable { @NSManaged var taskOccurrenceIndex: Int @NSManaged var task: OCKCDTask? @NSManaged var values: Set - @NSManaged var date: Date? + @NSManaged var date: Date static var defaultSortDescriptors: [NSSortDescriptor] { return [NSSortDescriptor(keyPath: \OCKCDOutcome.createdDate, ascending: false)] diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift index b8208fcdd..b8eb47f4e 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift @@ -57,6 +57,7 @@ extension OCKStore { completion: ((Result<[OCKOutcome], OCKStoreError>) -> Void)? = nil) { context.perform { do { + try self.confirmOutcomesAreInValidRegionOfTaskVersionChain(outcomes) let persistableOutcomes = outcomes.map(self.createOutcome) try self.context.save() let updatedOutcomes = try persistableOutcomes.map(self.makeOutcome) @@ -75,26 +76,29 @@ extension OCKStore { open func updateOutcomes(_ outcomes: [OCKOutcome], callbackQueue: DispatchQueue = .main, completion: ((Result<[OCKOutcome], OCKStoreError>) -> Void)? = nil) { - do { - let objectIDs = try retrieveObjectIDs(for: outcomes) - let predicate = NSPredicate(format: "self IN %@", objectIDs) - let currentOutcomes = OCKCDOutcome.fetchFromStore(in: context, where: predicate) - for (outcomeIndex, objectID) in objectIDs.enumerated() { - guard let index = currentOutcomes.firstIndex(where: { $0.objectID == objectID }) else { - throw OCKStoreError.updateFailed(reason: "No OCKOutcome with matching ID could be found: \(objectID)") + context.perform { + do { + try self.confirmOutcomesAreInValidRegionOfTaskVersionChain(outcomes) + let objectIDs = try self.retrieveObjectIDs(for: outcomes) + let predicate = NSPredicate(format: "self IN %@", objectIDs) + let currentOutcomes = OCKCDOutcome.fetchFromStore(in: self.context, where: predicate) + for (outcomeIndex, objectID) in objectIDs.enumerated() { + guard let index = currentOutcomes.firstIndex(where: { $0.objectID == objectID }) else { + throw OCKStoreError.updateFailed(reason: "No OCKOutcome with matching ID could be found: \(objectID)") + } + self.copyOutcome(outcomes[outcomeIndex], to: currentOutcomes[index]) + } + try self.context.save() + let updatedOutcomes = try currentOutcomes.map(self.makeOutcome) + callbackQueue.async { + self.outcomeDelegate?.outcomeStore(self, didUpdateOutcomes: updatedOutcomes) + completion?(.success(updatedOutcomes)) + } + } catch { + self.context.rollback() + callbackQueue.async { + completion?(.failure(.updateFailed(reason: "Failed to update OCKOutcomes: [\(outcomes)]. \(error.localizedDescription)"))) } - copyOutcome(outcomes[outcomeIndex], to: currentOutcomes[index]) - } - try context.save() - let updatedOutcomes = try currentOutcomes.map(makeOutcome) - callbackQueue.async { - self.outcomeDelegate?.outcomeStore(self, didUpdateOutcomes: updatedOutcomes) - completion?(.success(updatedOutcomes)) - } - } catch { - context.rollback() - callbackQueue.async { - completion?(.failure(.updateFailed(reason: "Failed to update OCKOutcomes: [\(outcomes)]. \(error.localizedDescription)"))) } } } @@ -128,6 +132,34 @@ extension OCKStore { // MARK: Private + // Confirms that outcomes cannot be added to past versions of a task in regions covered by a newer version. + // + // |<------------- Time Line --------------->| + // TaskV1 a-------b-------------------> + // V2 ----------> + // V3------------------> + // + // Throws an error if the outcome is added to V1 outside the region between `a` and `b`. + // Throws an error if the outcome is added to V2 anywhere because V2 is fully eclipsed. + // Does not throw an error for outcomes to added to V3 because V3 is the newest version. + private func confirmOutcomesAreInValidRegionOfTaskVersionChain(_ outcomes: [Outcome]) throws { + for outcome in outcomes { + let taskID = try objectID(for: outcome.taskID) + guard var task = context.object(with: taskID) as? OCKCDTask else { fatalError("taskID pointed to a non-task class") } + let schedule = makeSchedule(elements: Array(task.scheduleElements)) + while let nextVersion = task.next as? OCKCDTask { + let outcomeDate = schedule.event(forOccurrenceIndex: outcome.taskOccurrenceIndex)!.start + if nextVersion.effectiveDate <= outcomeDate { + throw OCKStoreError.invalidValue(reason: """ + Tried to place an outcome in a date range overshadowed by a future version of task. + The outcome is dated \(outcomeDate), but a newer version of the task starts on \(nextVersion.effectiveDate). + """) + } + task = nextVersion + } + } + } + private func createOutcome(from outcome: OCKOutcome) -> OCKCDOutcome { let persistableOutcome = OCKCDOutcome(context: context) copyOutcome(outcome, to: persistableOutcome) @@ -140,7 +172,7 @@ extension OCKStore { persistableOutcome.taskOccurrenceIndex = outcome.taskOccurrenceIndex if let task: OCKCDTask = try? fetchObject(havingLocalID: outcome.taskID) { let schedule = makeSchedule(elements: Array(task.scheduleElements)) - persistableOutcome.date = schedule.event(forOccurrenceIndex: outcome.taskOccurrenceIndex)?.start + persistableOutcome.date = schedule.event(forOccurrenceIndex: outcome.taskOccurrenceIndex)!.start persistableOutcome.task = task } } diff --git a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift index f6cb2a7ec..699ebd821 100644 --- a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift +++ b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift @@ -105,6 +105,7 @@ extension OCKCoreDataTaskStoreProtocol { do { let ids = tasks.map { $0.id } try OCKCDTask.validateUpdateIdentifiers(ids, in: self.context) + try self.confirmUpdateWillNotCauseDataLoss(tasks: tasks) let updatedTasks = try self.performVersionedUpdate(values: tasks, addNewVersion: self.createTask) try self.context.save() let tasks = updatedTasks.map(self.makeTask) @@ -228,6 +229,46 @@ extension OCKCoreDataTaskStoreProtocol { return OCKHealthKitLinkage(quantityIdentifier: identifier, quantityType: quantity, unit: unit) } + // Ensure that new versions of tasks do not overwrite regions of previous + // versions that already have outcomes saved to them. + // + // |<------------- Time Line --------------->| + // TaskV1 ------x-------------------> + // V2 ----------> + // V3------------------> + // + // Throws an error when updating to V3 from V2 if V1 has outcomes after `x`. + // Throws an error when updating to V3 from V2 if V2 has any outcomes. + // Does not trow when updating to V3 from V2 if V1 has outcomes before `x`. + private func confirmUpdateWillNotCauseDataLoss(tasks: [Task]) throws { + let heads: [OCKCDTask] = OCKCDTask.fetchHeads(ids: tasks.map { $0.id }, in: context) + for task in heads { + + // For each task, gather all outcomes + var allOutcomes: Set = [] + var currentVersion: OCKCDTask? = task + while let version = currentVersion { + allOutcomes = allOutcomes.union(version.outcomes) + currentVersion = version.previous as? OCKCDTask + } + + // Get the date highest date on which an outcome exists. + // If there are no outcomes, then any update is safe. + guard let latestDate = allOutcomes.map({ $0.date }).max() + else { continue } + + if task.effectiveDate <= latestDate { + throw OCKStoreError.updateFailed(reason: """ + Updating task \(task.id) failed. The new version of the task takes effect on \(task.effectiveDate), but an outcome for a + previous version of the task exists on \(latestDate). To prevent implicit data loss, you must explicitly delete all outcomes + that exist after the new version's `effectiveDate` before applying the update, or move the new version's `effectiveDate` to + some date past the latest outcome's date. + """ + ) + } + } + } + func buildPredicate(for query: OCKTaskQuery) throws -> NSPredicate { var predicate = NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) // Not deleted diff --git a/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift b/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift index 47cfa3e21..da5cd212b 100644 --- a/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift +++ b/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift @@ -36,7 +36,7 @@ public protocol OCKReadOnlyEventStore: OCKAnyReadOnlyEventStore, OCKReadableTask // MARK: Implementation Provided when Task == OCKTask and Outcome == OCKOutcome - /// `fetchEvents` retrieves all the occurrences of the speficied task in the interval specified by the provided query. + /// `fetchEvents` retrieves all the occurrences of the specified task in the interval specified by the provided query. /// /// - Parameters: /// - taskID: A user-defined unique identifier for the task. @@ -154,25 +154,55 @@ public extension OCKReadOnlyEventStore where Task: OCKAnyVersionableTask, Outcom case .failure(let error): completion(.failure(error)) case .success(let outcomes): let events = self.join(task: task, with: outcomes, and: scheduleEvents) + previousEvents - guard let version = task.previousVersionID else { completion(.success(events)); return } - self.fetchTask(withVersionID: version, callbackQueue: callbackQueue, completion: { (result: Result) in + self.fetchNextValidPreviousVersion(for: task, callbackQueue: callbackQueue) { result in switch result { case .failure(let error): completion(.failure(error)) - case .success(let nextTask): + case .success(let previousVersion): + + // If there is no previous version, then we're done fetching all events. + guard let previousVersion = previousVersion else { + completion(.success(events)) + return + } + + // If there is a previous version, fetch the events for it that don't overlap with + // any of the versions we've already fetched events for. let nextEndDate = task.effectiveDate - let nextStartDate = max(query.dateInterval.start, nextTask.effectiveDate) + let nextStartDate = max(query.dateInterval.start, previousVersion.effectiveDate) let nextInterval = DateInterval(start: nextStartDate, end: nextEndDate) let nextQuery = OCKEventQuery(dateInterval: nextInterval) - self.fetchEvents(task: nextTask, query: nextQuery, previousEvents: events, - callbackQueue: callbackQueue, completion: { result in - completion(result) - }) + self.fetchEvents(task: previousVersion, query: nextQuery, previousEvents: events, + callbackQueue: callbackQueue, completion: completion) } - }) + + } } }) } + private func fetchNextValidPreviousVersion(for task: Task, callbackQueue: DispatchQueue, completion: @escaping OCKResultClosure) { + + guard let versionID = task.previousVersionID else { + completion(.success(nil)) + return + } + + fetchTask(withVersionID: versionID, callbackQueue: callbackQueue) { result in + switch result { + case .failure(let error): completion(.failure(error)) + case .success(let previousVersion): + + // If the newer version goes back further in time than the pervious version, skip fetching events for the older version. + if task.effectiveDate <= previousVersion.effectiveDate { + self.fetchNextValidPreviousVersion(for: previousVersion, callbackQueue: callbackQueue, completion: completion) + return + } + + completion(.success(previousVersion)) + } + } + } + private func fetchTask(withVersionID versionID: OCKLocalVersionID, callbackQueue: DispatchQueue, completion: @escaping OCKResultClosure) { var query = OCKTaskQuery() query.versionIDs = [versionID] diff --git a/CareKitStore/CareKitStoreTests/CoreDataSchema/TestCoreDataSchema+Outcomes.swift b/CareKitStore/CareKitStoreTests/CoreDataSchema/TestCoreDataSchema+Outcomes.swift index aadb4acb9..7a4bc388d 100644 --- a/CareKitStore/CareKitStoreTests/CoreDataSchema/TestCoreDataSchema+Outcomes.swift +++ b/CareKitStore/CareKitStoreTests/CoreDataSchema/TestCoreDataSchema+Outcomes.swift @@ -70,6 +70,7 @@ class TestCoreDataSchemaWithOutcomes: XCTestCase { outcome.taskOccurrenceIndex = 0 outcome.values = Set([value1, value2, value3]) outcome.task = task1 + outcome.date = Date() XCTAssertNoThrow(try store.context.save()) XCTAssert(outcome.values.count == 3) diff --git a/CareKitStore/CareKitStoreTests/CoreDataSchema/TestCoreDataSchemaIntegration.swift b/CareKitStore/CareKitStoreTests/CoreDataSchema/TestCoreDataSchemaIntegration.swift index 764e352ef..bfb594f8f 100644 --- a/CareKitStore/CareKitStoreTests/CoreDataSchema/TestCoreDataSchemaIntegration.swift +++ b/CareKitStore/CareKitStoreTests/CoreDataSchema/TestCoreDataSchemaIntegration.swift @@ -69,6 +69,7 @@ class TestCoreDataSchemaIntegration: XCTestCase { let outcome = OCKCDOutcome(context: store.context) outcome.taskOccurrenceIndex = 0 outcome.task = task + outcome.date = Date() let value = OCKCDOutcomeValue(context: store.context) value.kind = "pulse" diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift index 8297b05f5..d00b07d79 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift @@ -83,7 +83,7 @@ class TestStoreOutcomes: XCTestCase { XCTAssert(outcome.taskID == taskID) } - func testTwoOutcomesWithoutSameTaskIDAndOccurenceIndexCannotBeAdded() throws { + func testTwoOutcomesWithoutSameTaskIDAndOccurrenceIndexCannotBeAdded() throws { var task = OCKTask(id: "task", title: "My Task", carePlanID: nil, schedule: .mealTimesEachDay(start: Date(), end: nil)) task = try store.addTaskAndWait(task) let taskID = try task.getLocalID() @@ -92,6 +92,36 @@ class TestStoreOutcomes: XCTestCase { XCTAssertThrowsError(try store.addOutcomesAndWait([outcome, outcome])) } + func testCannotAddOutcomeToCoveredRegionOfPreviousTaskVersion() throws { + let thisMorning = Calendar.current.startOfDay(for: Date()) + let schedule = OCKSchedule.mealTimesEachDay(start: thisMorning, end: nil) + let task = OCKTask(id: "meds", title: "Medications", carePlanID: nil, schedule: schedule) + let taskV1 = try store.addTaskAndWait(task) + let taskV2 = try store.updateTaskAndWait(task) + let value = OCKOutcomeValue(123) + let outcome = OCKOutcome(taskID: try taskV1.getLocalID(), taskOccurrenceIndex: 1, values: [value]) + XCTAssert(taskV2.previousVersionID == taskV1.localDatabaseID) + XCTAssertThrowsError(try store.addOutcomeAndWait(outcome)) + } + + func testCannotUpdateOutcomeToCoveredRegionOfPreviousTaskVersion() throws { + let thisMorning = Calendar.current.startOfDay(for: Date()) + let tomorrowMorning = Calendar.current.date(byAdding: .day, value: 1, to: thisMorning)! + let schedule = OCKSchedule.mealTimesEachDay(start: thisMorning, end: nil) + + var task = OCKTask(id: "meds", title: "Medications", carePlanID: nil, schedule: schedule) + let taskV1 = try store.addTaskAndWait(task) + + task.effectiveDate = tomorrowMorning + try store.updateTaskAndWait(task) + + let value = OCKOutcomeValue(123) + var outcome = OCKOutcome(taskID: try taskV1.getLocalID(), taskOccurrenceIndex: 0, values: [value]) + outcome = try store.addOutcomeAndWait(outcome) + outcome.taskOccurrenceIndex = 8 + XCTAssertThrowsError(try store.updateOutcomeAndWait(outcome)) + } + // MARK: Querying func testOutcomeQueryGroupIdentifier() throws { diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift index 1a975e3c2..53ff48218 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift @@ -237,6 +237,45 @@ class TestStoreTasks: XCTestCase { XCTAssert(updatedTask.previousVersionID == task.localDatabaseID) } + func testCanFetchEventsWhenCurrentTaskVersionStartsAtSameTimeOrEarlierThanThePreviousVersion() throws { + let thisMorning = Calendar.current.startOfDay(for: Date()) + let aFewDaysAgo = Calendar.current.date(byAdding: .day, value: -4, to: thisMorning)! + let manyDaysAgo = Calendar.current.date(byAdding: .day, value: -10, to: thisMorning)! + let scheduleV1 = OCKSchedule.dailyAtTime(hour: 8, minutes: 0, start: manyDaysAgo, end: nil, text: nil) + let scheduleV2 = OCKSchedule.dailyAtTime(hour: 8, minutes: 0, start: aFewDaysAgo, end: nil, text: nil) + let scheduleV3 = OCKSchedule.dailyAtTime(hour: 8, minutes: 0, start: aFewDaysAgo, end: nil, text: nil) + + var nausea = OCKTask(id: "nausea", title: "V1", carePlanID: nil, schedule: scheduleV1) + let v1 = try store.addTaskAndWait(nausea) + XCTAssert(v1.effectiveDate == scheduleV1.startDate()) + + nausea.title = "V2" + nausea.schedule = scheduleV2 + nausea.effectiveDate = scheduleV2.startDate() + let v2 = try store.updateTaskAndWait(nausea) + XCTAssert(v2.effectiveDate == scheduleV2.startDate()) + + nausea.title = "V3" + nausea.schedule = scheduleV3 + nausea.effectiveDate = scheduleV3.startDate() + let v3 = try store.updateTaskAndWait(nausea) + XCTAssert(v3.effectiveDate == scheduleV3.startDate()) + + let query = OCKEventQuery(dateInterval: DateInterval(start: manyDaysAgo, end: thisMorning)) + let events = try store.fetchEventsAndWait(taskID: "nausea", query: query) + XCTAssert(events.count == 10, "Expected 10, but got \(events.count)") + XCTAssert(events.first?.task.title == "V1") + XCTAssert(events.last?.task.title == "V3") + } + + func testCannotUpdateTaskIfItResultsInImplicitDataLoss() throws { + let schedule = OCKSchedule.mealTimesEachDay(start: Date(), end: nil) + let task = try store.addTaskAndWait(OCKTask(id: "meds", title: "Medication", carePlanID: nil, schedule: schedule)) + let outcome = OCKOutcome(taskID: try task.getLocalID(), taskOccurrenceIndex: 5, values: [OCKOutcomeValue(1)]) + try store.addOutcomesAndWait([outcome]) + XCTAssertThrowsError(try store.updateTaskAndWait(task)) + } + func testUpdateFailsForUnsavedTasks() { let task = OCKTask(id: "meds", title: "Medication", carePlanID: nil, schedule: .mealTimesEachDay(start: Date(), end: nil)) XCTAssertThrowsError(try store.updateTaskAndWait(task)) From a368d25718b8508d4136f9ef87387b1c66b4d3b2 Mon Sep 17 00:00:00 2001 From: Erik Hornberger Date: Mon, 13 Jan 2020 09:40:55 -0800 Subject: [PATCH 06/33] Update managed object model with non-optional outcome date --- .../CareKitStore/CoreData/OCKManagedObjectModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CareKitStore/CareKitStore/CoreData/OCKManagedObjectModel.swift b/CareKitStore/CareKitStore/CoreData/OCKManagedObjectModel.swift index 7f1f32d2b..18c9505fc 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKManagedObjectModel.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKManagedObjectModel.swift @@ -52,7 +52,7 @@ import CoreData // private let secureUnarchiver = "NSSecureUnarchiveFromData" -private let schemaVersion = OCKSemanticVersion(majorVersion: 2, minorVersion: 0, patchNumber: 0) +private let schemaVersion = OCKSemanticVersion(majorVersion: 2, minorVersion: 0, patchNumber: 1) private func makeManagedObjectModel() -> NSManagedObjectModel { let managedObjectModel = NSManagedObjectModel() @@ -801,7 +801,7 @@ private func makeOutcomeEntity() -> NSEntityDescription { let date = NSAttributeDescription() date.name = "date" date.attributeType = .dateAttributeType - date.isOptional = true + date.isOptional = false outcomeEntity.properties = makeObjectAttributes() + [index, date] return outcomeEntity From f53a80485b01a698c93adb107912b43b4a879a67 Mon Sep 17 00:00:00 2001 From: Erik Hornberger Date: Thu, 16 Jan 2020 17:31:07 -0800 Subject: [PATCH 07/33] Fix a bug that involved scheduling of all day tasks --- CareKitStore/CareKitStore/Structs/OCKSchedule.swift | 7 ++++++- .../CareKitStore/Structs/OCKScheduleElement.swift | 7 +++++++ .../CareKitStoreTests/Structs/TestSchedule.swift | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CareKitStore/CareKitStore/Structs/OCKSchedule.swift b/CareKitStore/CareKitStore/Structs/OCKSchedule.swift index dcc4c52c8..37e3e221d 100644 --- a/CareKitStore/CareKitStore/Structs/OCKSchedule.swift +++ b/CareKitStore/CareKitStore/Structs/OCKSchedule.swift @@ -94,7 +94,12 @@ public struct OCKSchedule: Codable, Equatable { for index in 0..= start } + return allEvents.filter { event in + if event.element.duration == .allDay { + return event.start >= Calendar.current.startOfDay(for: start) + } + return event.start + event.element.duration.seconds >= start + } } /// Create a new schedule by shifting this schedule. diff --git a/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift b/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift index 27544eeb6..a793e1204 100644 --- a/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift +++ b/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift @@ -223,6 +223,13 @@ public struct OCKScheduleElement: Codable, Equatable, OCKObjectCompatible { /// Determines the last date at which an event could possibly occur private func determineStopDate(onOrBefore date: Date) -> Date { + if duration == .allDay { + let stopDay = end ?? date + let morningOfStopDay = Calendar.current.startOfDay(for: stopDay) + let endOfStopDay = Calendar.current.date(byAdding: .init(day: 1, second: -1), to: morningOfStopDay)! + return endOfStopDay + } + guard let endDate = end else { return date } return min(endDate, date) } diff --git a/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift b/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift index bbd72adca..d2e574dfb 100644 --- a/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift +++ b/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift @@ -57,6 +57,17 @@ class TestSchedule: XCTestCase { XCTAssert(events[2].occurrence == 2) } + func testAllDayEventsCapturedByEventsBetweenDates() { + let morning = Calendar.current.startOfDay(for: Date()) + let breakfast = Calendar.current.date(byAdding: .hour, value: 7, to: morning)! + let lunch = Calendar.current.date(byAdding: .hour, value: 12, to: morning)! + let dinner = Calendar.current.date(byAdding: .hour, value: 18, to: morning)! + let allDay = OCKScheduleElement(start: breakfast, end: nil, interval: DateComponents(day: 1), text: "Daily", duration: .allDay) + let schedule = OCKSchedule(composing: [allDay]) + let events = schedule.events(from: lunch, to: dinner) + XCTAssert(events.count == 1, "Expected 1 all day event, but got \(events.count)") + } + func testWeeklySchedule() { let schedule = OCKSchedule.weeklyAtTime(weekday: 1, hours: 0, minutes: 0, start: Date(), end: nil, targetValues: [], text: nil) for index in 0..<5 { From 821ab26acbd928176850e6c2d1af2742574da446 Mon Sep 17 00:00:00 2001 From: Erik Hornberger Date: Thu, 30 Jan 2020 13:54:00 -0800 Subject: [PATCH 08/33] Fix for a bug that prevented tasks with outcomes from being updated. --- .../Protocols/CoreData/OCKCoreDataTaskStore.swift | 5 ++++- .../CareKitStoreTests/OCKStore/TestStore+Tasks.swift | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift index 699ebd821..f86cef7cc 100644 --- a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift +++ b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift @@ -257,7 +257,10 @@ extension OCKCoreDataTaskStoreProtocol { guard let latestDate = allOutcomes.map({ $0.date }).max() else { continue } - if task.effectiveDate <= latestDate { + guard let proposedUpdate = tasks.first(where: { $0.id == task.id }) + else { fatalError("Fetched an OCKCDTask for which an update was not proposed.") } + + if proposedUpdate.effectiveDate <= latestDate { throw OCKStoreError.updateFailed(reason: """ Updating task \(task.id) failed. The new version of the task takes effect on \(task.effectiveDate), but an outcome for a previous version of the task exists on \(latestDate). To prevent implicit data loss, you must explicitly delete all outcomes diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift index 53ff48218..920f52c64 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift @@ -276,6 +276,15 @@ class TestStoreTasks: XCTestCase { XCTAssertThrowsError(try store.updateTaskAndWait(task)) } + func testCanUpdateTaskWithOutcomesIfDoesNotCauseDataLoss() throws { + let schedule = OCKSchedule.mealTimesEachDay(start: Date(), end: nil) + var task = try store.addTaskAndWait(OCKTask(id: "meds", title: "Medication", carePlanID: nil, schedule: schedule)) + let outcome = OCKOutcome(taskID: try task.getLocalID(), taskOccurrenceIndex: 0, values: [OCKOutcomeValue(1)]) + try store.addOutcomesAndWait([outcome]) + task.effectiveDate = task.schedule[5].start + XCTAssertNoThrow(try store.updateTaskAndWait(task)) + } + func testUpdateFailsForUnsavedTasks() { let task = OCKTask(id: "meds", title: "Medication", carePlanID: nil, schedule: .mealTimesEachDay(start: Date(), end: nil)) XCTAssertThrowsError(try store.updateTaskAndWait(task)) From 72625cb92f3c58f86c48bbb7317296faf2c1f568 Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Tue, 11 Feb 2020 12:25:45 -0800 Subject: [PATCH 09/33] Resolve issue #353 (#359) * Resolve issue #353 * Typo fix --- .../Chart/Controller/OCKChartController.swift | 78 ++----- .../CoreData/OCKCDVersionedObject.swift | 25 +-- .../CoreData/OCKStore+CarePlans.swift | 8 +- .../CoreData/OCKStore+Contacts.swift | 6 +- .../CoreData/OCKStore+Patients.swift | 5 +- .../CoreData/OCKCoreDataTaskStore.swift | 2 +- .../Protocols/Events/OCKEventStore.swift | 7 + .../OCKStore/TestStore+Tasks.swift | 29 +++ .../Structs/TestSchedule.swift | 26 ++- .../TestStoreProtocolExtensions.swift | 14 ++ .../OCKCatalog.xcodeproj/project.pbxproj | 210 +----------------- OCKSample/OCKSample.xcodeproj/project.pbxproj | 164 +------------- 12 files changed, 113 insertions(+), 461 deletions(-) diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartController.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartController.swift index ce40ab54c..7cfffd1b5 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartController.swift @@ -46,8 +46,8 @@ open class OCKChartController: OCKChartControllerProtocol, ObservableObject { // MARK: Properties - private let weekOfDate: Date - private var subscription: AnyCancellable? + private let eventQuery: OCKEventQuery + private var cancellables: Set = Set() // MARK: - Life Cycle @@ -55,7 +55,7 @@ open class OCKChartController: OCKChartControllerProtocol, ObservableObject { /// - Parameter weekOfDate: A date in the week of the insights range. /// - Parameter storeManager: Wraps the store that contains the insight data. public required init(weekOfDate: Date, storeManager: OCKSynchronizedStoreManager) { - self.weekOfDate = weekOfDate + self.eventQuery = OCKEventQuery(dateInterval: Calendar.current.dateIntervalOfWeek(for: weekOfDate)) self.storeManager = storeManager self.objectWillChange = .init([]) } @@ -67,44 +67,31 @@ open class OCKChartController: OCKChartControllerProtocol, ObservableObject { /// - configurations: An array of configurations to be plotted. open func fetchAndObserveInsights(forConfigurations configurations: [OCKDataSeriesConfiguration], errorHandler: ((Error) -> Void)? = nil) { - - // Fetch tasks, then fetch events for the tasks and set the view model - let eventQuery = OCKEventQuery(dateInterval: Calendar.current.dateIntervalOfWeek(for: weekOfDate)) - fetchTasks(eventQuery: eventQuery, configurations: configurations, errorHandler: errorHandler) - } - - private func fetchTasks(eventQuery: OCKEventQuery, configurations: [OCKDataSeriesConfiguration], - errorHandler: ((Error) -> Void)? = nil) { - - // Build up the task query - var taskQuery = OCKTaskQuery(for: Date()) - taskQuery.ids = configurations.map { $0.taskID } - - storeManager.store.fetchAnyTasks(query: taskQuery, callbackQueue: .main) { [weak self] result in - guard let self = self else { return } - switch result { - case .success(let tasks): - - // Fetch events and set the view model. Also set the view model when the events change - self.refetchEvents(eventQuery: eventQuery, configurations: configurations) { result in - if case let .failure(error) = result { - errorHandler?(error) - } - } - - self.subscribeTo(tasks: tasks, eventQuery: eventQuery, configurations: configurations) { result in - if case let .failure(error) = result { - errorHandler?(error) + cancellables = Set() + configurations.forEach { config in + store.fetchAnyEvents(taskID: config.taskID, query: eventQuery, callbackQueue: .main) { result in + switch result { + case let .failure(error): errorHandler?(error) + case let .success(events): + self.refetchEvents(configurations: configurations, completion: nil) + events.forEach { event in + self.storeManager + .publisher(forEvent: event, categories: [.add, .update, .delete]) + .sink(receiveValue: { _ in + self.refetchEvents(configurations: configurations) { result in + if case let .failure(error) = result { + errorHandler?(error) + } + } + }) + .store(in: &self.cancellables) } } - - case .failure(let error): - errorHandler?(error) } } } - private func refetchEvents(eventQuery: OCKEventQuery, configurations: [OCKDataSeriesConfiguration], + private func refetchEvents(configurations: [OCKDataSeriesConfiguration], completion: OCKResultClosure<[OCKDataSeries]>?) { var allDataSeries = [OCKDataSeries]() let group = DispatchGroup() @@ -138,25 +125,4 @@ open class OCKChartController: OCKChartControllerProtocol, ObservableObject { completion?(.success(allDataSeries)) } } - - private func subscribeTo(tasks: [OCKAnyTask], - eventQuery: OCKEventQuery, configurations: [OCKDataSeriesConfiguration], - completion: OCKResultClosure<[OCKDataSeries]>?) { - // Set the view model when the tasks change - let taskSubscriptions = tasks.map { task in - return storeManager.publisher(forTask: task, categories: [.update, .delete], fetchImmediately: false) - .sink { _ in self.refetchEvents(eventQuery: eventQuery, configurations: configurations, completion: completion) } - } - - // Set the view model when the events for the tasks change - let eventSubscriptions = tasks.map { task in - return self.storeManager.publisher(forEventsBelongingToTask: task, categories: [.update, .add, .delete]) - .sink { _ in self.refetchEvents(eventQuery: eventQuery, configurations: configurations, completion: completion) } - } - - subscription = AnyCancellable { - taskSubscriptions.forEach { $0.cancel() } - eventSubscriptions.forEach { $0.cancel() } - } - } } diff --git a/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift b/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift index babed0ab6..678e3f710 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift @@ -61,6 +61,10 @@ class OCKCDVersionedObject: OCKCDObject, OCKCDManageable { }.compactMap { $0 as? T } } + static var notDeletedPredicate: NSPredicate { + NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) + } + static func headerPredicate(for ids: [String]) -> NSPredicate { return NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "%K IN %@", #keyPath(OCKCDVersionedObject.id), ids), @@ -70,30 +74,25 @@ class OCKCDVersionedObject: OCKCDObject, OCKCDManageable { static func headerPredicate() -> NSPredicate { return NSCompoundPredicate(andPredicateWithSubpredicates: [ - NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.next)), + notDeletedPredicate, NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) ]) } static func newestVersionPredicate(in interval: DateInterval) -> NSPredicate { - let notDeletedYet = NSPredicate(format: "%K < %@ AND %K == nil", - #keyPath(OCKCDVersionedObject.effectiveDate), - interval.end as NSDate, - #keyPath(OCKCDVersionedObject.deletedDate)) - let deletedAfterQueryStart = NSPredicate(format: "%K < %@ AND %K > %@", + let startsBeforeEndOfQuery = NSPredicate(format: "%K < %@", #keyPath(OCKCDVersionedObject.effectiveDate), - interval.end as NSDate, - #keyPath(OCKCDVersionedObject.deletedDate), - interval.start as NSDate) - let noNextVersion = NSPredicate(format: "%K == nil OR %K.effectiveDate > %@", + interval.end as NSDate) + + let noNextVersion = NSPredicate(format: "%K == nil OR %K.effectiveDate >= %@", #keyPath(OCKCDVersionedObject.next), #keyPath(OCKCDVersionedObject.next), interval.end as NSDate) - let existsDuringQuery = NSCompoundPredicate(orPredicateWithSubpredicates: [ - notDeletedYet, deletedAfterQueryStart]) return NSCompoundPredicate(andPredicateWithSubpredicates: [ - existsDuringQuery, noNextVersion]) + startsBeforeEndOfQuery, + noNextVersion + ]) } static func validateNewIDs(_ ids: [String], in context: NSManagedObjectContext) throws { diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift index 32d3ea0f3..f5e9216e1 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift @@ -68,7 +68,7 @@ extension OCKStore { context.perform { do { try OCKCDCarePlan.validateNewIDs(plans.map { $0.id }, in: self.context) - let persistablePlans = plans.map (self.createCarePlan) + let persistablePlans = plans.map(self.createCarePlan) try self.context.save() let addedPlans = persistablePlans.map(self.makePlan) callbackQueue.async { @@ -150,9 +150,7 @@ extension OCKStore { } private func buildPredicate(for query: OCKCarePlanQuery) throws -> NSPredicate { - var predicate = NSPredicate(value: true) - let notDeletedPredicate = NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) - predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, notDeletedPredicate]) + var predicate = OCKCDVersionedObject.notDeletedPredicate if let interval = query.dateInterval { let intervalPredicate = OCKCDVersionedObject.newestVersionPredicate(in: interval) @@ -165,7 +163,7 @@ extension OCKStore { } if !query.versionIDs.isEmpty { - let versionPredicate = NSPredicate(format: "self IN %@", try query.versionIDs.map(objectID(for:))) + let versionPredicate = NSPredicate(format: "self IN %@", try query.versionIDs.map(objectID)) predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, versionPredicate]) } diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+Contacts.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+Contacts.swift index cb9d8ef6d..bd1067136 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+Contacts.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+Contacts.swift @@ -186,9 +186,7 @@ extension OCKStore { } private func buildPredicate(for query: OCKContactQuery) throws -> NSPredicate { - var predicate = NSPredicate(value: true) - let notDeletedPredicate = NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) - predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, notDeletedPredicate]) + var predicate = OCKCDVersionedObject.notDeletedPredicate if let interval = query.dateInterval { let intervalPredicate = OCKCDVersionedObject.newestVersionPredicate(in: interval) @@ -201,7 +199,7 @@ extension OCKStore { } if !query.versionIDs.isEmpty { - let versionPredicate = NSPredicate(format: "self IN %@", try query.versionIDs.map(objectID(for:))) + let versionPredicate = NSPredicate(format: "self IN %@", try query.versionIDs.map(objectID)) predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, versionPredicate]) } diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+Patients.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+Patients.swift index 2009e27af..32f02c1fa 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+Patients.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+Patients.swift @@ -147,10 +147,7 @@ extension OCKStore { } private func buildPredicate(for query: OCKPatientQuery) throws -> NSPredicate { - var predicate = NSPredicate(value: true) - - let notDeletedPredicate = NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) - predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, notDeletedPredicate]) + var predicate = OCKCDVersionedObject.notDeletedPredicate if let interval = query.dateInterval { let intervalPredicate = OCKCDVersionedObject.newestVersionPredicate(in: interval) diff --git a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift index f86cef7cc..dcaeb30c0 100644 --- a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift +++ b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift @@ -273,7 +273,7 @@ extension OCKCoreDataTaskStoreProtocol { } func buildPredicate(for query: OCKTaskQuery) throws -> NSPredicate { - var predicate = NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) // Not deleted + var predicate = OCKCDVersionedObject.notDeletedPredicate if let interval = query.dateInterval { let headPredicate = OCKCDVersionedObject.newestVersionPredicate(in: interval) diff --git a/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift b/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift index da5cd212b..7195460ab 100644 --- a/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift +++ b/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift @@ -154,6 +154,13 @@ public extension OCKReadOnlyEventStore where Task: OCKAnyVersionableTask, Outcom case .failure(let error): completion(.failure(error)) case .success(let outcomes): let events = self.join(task: task, with: outcomes, and: scheduleEvents) + previousEvents + + // If the query doesn't go back in time beyond the start of this version of the task, we're done. + guard query.dateInterval.start < task.effectiveDate else { + completion(.success(events)) + return + } + self.fetchNextValidPreviousVersion(for: task, callbackQueue: callbackQueue) { result in switch result { case .failure(let error): completion(.failure(error)) diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift index 920f52c64..75f3f1f3f 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift @@ -285,6 +285,23 @@ class TestStoreTasks: XCTestCase { XCTAssertNoThrow(try store.updateTaskAndWait(task)) } + func testQueryUpdatedTasksEvents() throws { + let schedule = OCKSchedule.mealTimesEachDay(start: Date(), end: nil) // 7:30AM, 12:00PM, 5:30PM + let original = try store.addTaskAndWait(OCKTask(id: "meds", title: "Original", carePlanID: nil, schedule: schedule)) + + var updated = original + updated.effectiveDate = schedule[5].start // 5:30PM tomorrow + updated.title = "Updated" + updated = try store.updateTaskAndWait(updated) + let query = OCKEventQuery(for: schedule[5].start) // 0:00AM - 23:59.99PM tomorrow + let events = try store.fetchEventsAndWait(taskID: "meds", query: query) + + XCTAssert(events.count == 3) + XCTAssert(events[0].task.localDatabaseID == original.localDatabaseID) + XCTAssert(events[1].task.localDatabaseID == original.localDatabaseID) + XCTAssert(events[2].task.localDatabaseID == updated.localDatabaseID) + } + func testUpdateFailsForUnsavedTasks() { let task = OCKTask(id: "meds", title: "Medication", carePlanID: nil, schedule: .mealTimesEachDay(start: Date(), end: nil)) XCTAssertThrowsError(try store.updateTaskAndWait(task)) @@ -327,6 +344,18 @@ class TestStoreTasks: XCTestCase { XCTAssert(fetched.first?.title == taskA.title) } + func testTaskQueryStartingExactlyOnEffectiveDateOfNewVersion() throws { + let schedule = OCKSchedule.dailyAtTime(hour: 0, minutes: 0, start: Date(), end: nil, text: nil) + let query = OCKTaskQuery(dateInterval: DateInterval(start: schedule[5].start, end: schedule[5].end)) + + var task = try store.addTaskAndWait(OCKTask(id: "meds", title: "Medication", carePlanID: nil, schedule: schedule)) + task.effectiveDate = task.schedule[5].start + task = try store.updateTaskAndWait(task) + + let fetched = try store.fetchTasksAndWait(query: query) + XCTAssert(fetched.first == task) + } + func testTaskQuerySpanningVersionsReturnsNewestVersionOnly() throws { let schedule = OCKSchedule.mealTimesEachDay(start: Date(), end: nil) diff --git a/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift b/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift index d2e574dfb..eab384da6 100644 --- a/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift +++ b/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift @@ -58,15 +58,15 @@ class TestSchedule: XCTestCase { } func testAllDayEventsCapturedByEventsBetweenDates() { - let morning = Calendar.current.startOfDay(for: Date()) - let breakfast = Calendar.current.date(byAdding: .hour, value: 7, to: morning)! - let lunch = Calendar.current.date(byAdding: .hour, value: 12, to: morning)! - let dinner = Calendar.current.date(byAdding: .hour, value: 18, to: morning)! - let allDay = OCKScheduleElement(start: breakfast, end: nil, interval: DateComponents(day: 1), text: "Daily", duration: .allDay) - let schedule = OCKSchedule(composing: [allDay]) - let events = schedule.events(from: lunch, to: dinner) - XCTAssert(events.count == 1, "Expected 1 all day event, but got \(events.count)") - } + let morning = Calendar.current.startOfDay(for: Date()) + let breakfast = Calendar.current.date(byAdding: .hour, value: 7, to: morning)! + let lunch = Calendar.current.date(byAdding: .hour, value: 12, to: morning)! + let dinner = Calendar.current.date(byAdding: .hour, value: 18, to: morning)! + let allDay = OCKScheduleElement(start: breakfast, end: nil, interval: DateComponents(day: 1), text: "Daily", duration: .allDay) + let schedule = OCKSchedule(composing: [allDay]) + let events = schedule.events(from: lunch, to: dinner) + XCTAssert(events.count == 1, "Expected 1 all day event, but got \(events.count)") + } func testWeeklySchedule() { let schedule = OCKSchedule.weeklyAtTime(weekday: 1, hours: 0, minutes: 0, start: Date(), end: nil, targetValues: [], text: nil) @@ -181,6 +181,14 @@ class TestSchedule: XCTestCase { XCTAssert(events[2].occurrence == 5) } + func testScheduleIntervalsHaveInclusiveLowerBoundAndExclusiveUpperBound() { + let element = OCKScheduleElement(start: Date(), end: nil, interval: DateComponents(day: 1), text: nil, targetValues: [], duration: .allDay) + let schedule = OCKSchedule(composing: [element]) + let events = schedule.events(from: schedule[1].start, to: schedule[1].end) + XCTAssert(events.count == 1) + XCTAssert(events.first?.occurrence == 1) + } + // Measure how long it takes to generate 10 years worth of events for a highly complex schedule with hourly events. // Results in the computatin of about 100,000 events. func testEventGenerationPerformanceHeavySchedule() { diff --git a/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift b/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift index 6b5bcd191..0e01785b6 100644 --- a/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift +++ b/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift @@ -217,6 +217,20 @@ class TestStoreProtocolExtensions: XCTestCase { XCTAssert(events.first?.task.title == versionA.title) } + func testFetchEventsReturnsOnlyTheNewerOfTwoEventsWhenTwoVersionsOfATaskHaveEventsAtQueryStart() throws { + let element = OCKScheduleElement(start: Date(), end: nil, interval: DateComponents(day: 1), + text: nil, targetValues: [], duration: .allDay) + let schedule = OCKSchedule(composing: [element]) + let versionA = OCKTask(id: "123", title: "A", carePlanID: nil, schedule: schedule) + try store.addTaskAndWait(versionA) + var versionB = OCKTask(id: "123", title: "B", carePlanID: nil, schedule: schedule) + versionB.effectiveDate = schedule[4].start + try store.updateTaskAndWait(versionB) + let events = try store.fetchEventsAndWait(taskID: "123", query: .init(for: schedule[4].start)) + XCTAssert(events.count == 1, "Expected 1, but got \(events.count)") + XCTAssert(events.first?.task.title == "B") + } + // MARK: Adherence and Insights func testFetchAdherenceAggregatesEventsAcrossTasks() throws { diff --git a/OCKCatalog/OCKCatalog.xcodeproj/project.pbxproj b/OCKCatalog/OCKCatalog.xcodeproj/project.pbxproj index f0de138ea..cd5860dd0 100644 --- a/OCKCatalog/OCKCatalog.xcodeproj/project.pbxproj +++ b/OCKCatalog/OCKCatalog.xcodeproj/project.pbxproj @@ -372,105 +372,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 052F8B31235779A900E45940 /* Public Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - FRAMEWORK_SEARCH_PATHS = "$(BUILD_DIR)/Release-$(PLATFORM_NAME)"; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; - SWIFT_VERSION = 5.0; - VALIDATE_PRODUCT = YES; - }; - name = "Public Release"; - }; - 052F8B32235779A900E45940 /* Public Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "$(SRCROOT)/OCKCatalog/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.example.carekit-samplecode.OCKCatalog-public"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - TARGETED_DEVICE_FAMILY = 1; - }; - name = "Public Release"; - }; - 052F8B33235779A900E45940 /* Public Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7JYUG8QGJ3; - INFOPLIST_FILE = OCKCatalogTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = Apple.OCKCatalogTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OCKCatalog.app/OCKCatalog"; - }; - name = "Public Release"; - }; 5143E1FD22C2832600E32526 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -492,26 +393,6 @@ }; name = Debug; }; - 5143E1FE22C2832600E32526 /* Internal Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7JYUG8QGJ3; - INFOPLIST_FILE = OCKCatalogTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = Apple.OCKCatalogTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OCKCatalog.app/OCKCatalog"; - }; - name = "Internal Release"; - }; E7D01108222498F400C008DE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -577,65 +458,6 @@ }; name = Debug; }; - E7D01109222498F400C008DE /* Internal Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - FRAMEWORK_SEARCH_PATHS = "$(BUILD_DIR)/Release-$(PLATFORM_NAME)"; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; - SWIFT_VERSION = 5.0; - VALIDATE_PRODUCT = YES; - }; - name = "Internal Release"; - }; E7D0110B222498F400C008DE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -656,26 +478,6 @@ }; name = Debug; }; - E7D0110C222498F400C008DE /* Internal Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "$(SRCROOT)/OCKCatalog/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.example.carekit-samplecode.OCKCatalog-qa"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - TARGETED_DEVICE_FAMILY = 1; - }; - name = "Internal Release"; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -683,31 +485,25 @@ isa = XCConfigurationList; buildConfigurations = ( 5143E1FD22C2832600E32526 /* Debug */, - 5143E1FE22C2832600E32526 /* Internal Release */, - 052F8B33235779A900E45940 /* Public Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Internal Release"; + defaultConfigurationName = Debug; }; E7D010F3222498F400C008DE /* Build configuration list for PBXProject "OCKCatalog" */ = { isa = XCConfigurationList; buildConfigurations = ( E7D01108222498F400C008DE /* Debug */, - E7D01109222498F400C008DE /* Internal Release */, - 052F8B31235779A900E45940 /* Public Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Internal Release"; + defaultConfigurationName = Debug; }; E7D0110A222498F400C008DE /* Build configuration list for PBXNativeTarget "OCKCatalog" */ = { isa = XCConfigurationList; buildConfigurations = ( E7D0110B222498F400C008DE /* Debug */, - E7D0110C222498F400C008DE /* Internal Release */, - 052F8B32235779A900E45940 /* Public Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Internal Release"; + defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ }; diff --git a/OCKSample/OCKSample.xcodeproj/project.pbxproj b/OCKSample/OCKSample.xcodeproj/project.pbxproj index 993d89def..d51bfab33 100644 --- a/OCKSample/OCKSample.xcodeproj/project.pbxproj +++ b/OCKSample/OCKSample.xcodeproj/project.pbxproj @@ -291,84 +291,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 052F8B2F235778A200E45940 /* Public Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; - SWIFT_VERSION = 5.0; - VALIDATE_PRODUCT = YES; - }; - name = "Public Release"; - }; - 052F8B30235778A200E45940 /* Public Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "$(SRCROOT)/OCKSample/Supporting Files/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.example.carekit-samplecode.OCKSample-public"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - TARGETED_DEVICE_FAMILY = 1; - }; - name = "Public Release"; - }; E72B2C16226939E4009A9438 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -433,64 +355,6 @@ }; name = Debug; }; - E72B2C17226939E4009A9438 /* Internal Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; - SWIFT_VERSION = 5.0; - VALIDATE_PRODUCT = YES; - }; - name = "Internal Release"; - }; E72B2C19226939E4009A9438 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -511,26 +375,6 @@ }; name = Debug; }; - E72B2C1A226939E4009A9438 /* Internal Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "$(SRCROOT)/OCKSample/Supporting Files/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.example.carekit-samplecode.OCKSample-qa"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - TARGETED_DEVICE_FAMILY = 1; - }; - name = "Internal Release"; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -538,21 +382,17 @@ isa = XCConfigurationList; buildConfigurations = ( E72B2C16226939E4009A9438 /* Debug */, - E72B2C17226939E4009A9438 /* Internal Release */, - 052F8B2F235778A200E45940 /* Public Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Internal Release"; + defaultConfigurationName = Debug; }; E72B2C18226939E4009A9438 /* Build configuration list for PBXNativeTarget "OCKSample" */ = { isa = XCConfigurationList; buildConfigurations = ( E72B2C19226939E4009A9438 /* Debug */, - E72B2C1A226939E4009A9438 /* Internal Release */, - 052F8B30235778A200E45940 /* Public Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Internal Release"; + defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ }; From ca35bd8420b7334a49f4287d6df1f0ebc811fe9c Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Tue, 18 Feb 2020 14:28:15 -0800 Subject: [PATCH 10/33] Update readme (#368) Update README.md --- README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2180a8e11..6f9ba8294 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,7 @@ store.addTask(task) { result in ``` The most important feature of `OCKStore` is that it is a versioned store with a notion of time. When querying the store using a date range, the result returned will be for the -state of the store during the interval specified. +state of the store during the interval specified. If no date interval is provided, all versions of the entity will be returned. ```swift // On January 1st @@ -352,21 +352,34 @@ store.updateTask(task) let earlyQuery = OCKTaskQuery(dateInterval: /* Jan 1st - 5th */) store.fetchTasks(query: earlyQuery, callbackQueue: .main) { result in - let title = try! result.get().first?.title // Take 1 Tablet of Doxylamine + let title = try! result.get().first?.title + // "Take 1 Tablet of Doxylamine" } let laterQuery = OCKTaskQuery(dateInterval: /* Jan 12th - 17th */) store.fetchTasks(query: laterQuery, callbackQueue: .main) { result in - let title = try! result.get().first?.title // Take 2 Tablets of Doxylamine + let title = try! result.get().first?.title + // "Take 2 Tablets of Doxylamine" } // Queries return the newest version of the task during the query interval! let midQuery = OCKTaskQuery(dateInterval: /* Jan 5th - 15th */) store.fetchTasks(query: laterQuery, callbackQueue: .main) { result in - let title = try! result.get().first?.title // Take 2 Tablets of Doxylamine + let title = try! result.get().first?.title + // "Take 2 Tablets of Doxylamine" +} + +// Queries with no date interval return all versions of the task +let allQuery = OCKTaskQuery() +store.fetchTasks(query: allQuery, callbackQueue: .main) { result in + let titles = try! result.get().map { $0.title } + // ["Take 2 Tablets of Doxylamine", "Take 1 Tablet of Doxylamine"] } ``` +This graphic visualizes how results are retrieved when querying versioned objects in CareKit. Note how a query over a date range returns the version of the object valid in that date range. +![3d608700-5193-11ea-8ec0-452688468c72](https://user-images.githubusercontent.com/51723116/74690609-8c5aec00-5194-11ea-919a-53196eeefb9f.png) + ### Schema CareKitStore defines six high level entities as illustrated in this diagram: @@ -540,8 +553,8 @@ class SurveyViewController: OCKInstructionsTaskViewController, ORKTaskViewContro // 3a. Present the survey to the user present(surveyViewController, animated: true, completion: nil) } - - // 3b. This method will be called when the user completes the survey. + + // 3b. This method will be called when the user completes the survey. func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) { taskViewController.dismiss(animated: true, completion: nil) guard reason == .completed else { @@ -617,4 +630,3 @@ GitHub is our primary forum for CareKit. Feel free to open up issues about quest # License This project is made available under the terms of a BSD license. See the [LICENSE](LICENSE) file. - From 967c3cc5524384bb71c36cdb5a4d1b5cc64376af Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Tue, 18 Feb 2020 16:36:13 -0800 Subject: [PATCH 11/33] Open notification publisher (#369) Open notification publisher --- .../OCKStoreNotifications.swift | 44 +++++++++---------- .../OCKSynchronizedStoreManager.swift | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKStoreNotifications.swift b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKStoreNotifications.swift index 15dbe7a8e..abf80c06d 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKStoreNotifications.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKStoreNotifications.swift @@ -31,40 +31,40 @@ import CareKitStore import Foundation -protocol OCKStoreNotification {} +public protocol OCKStoreNotification {} -enum OCKStoreNotificationCategory { +public enum OCKStoreNotificationCategory { case add case update case delete } -struct OCKPatientNotification: OCKStoreNotification { - let patient: OCKAnyPatient - let category: OCKStoreNotificationCategory - let storeManager: OCKSynchronizedStoreManager +public struct OCKPatientNotification: OCKStoreNotification { + public let patient: OCKAnyPatient + public let category: OCKStoreNotificationCategory + public let storeManager: OCKSynchronizedStoreManager } -struct OCKCarePlanNotification: OCKStoreNotification { - let carePlan: OCKAnyCarePlan - let category: OCKStoreNotificationCategory - let storeManager: OCKSynchronizedStoreManager +public struct OCKCarePlanNotification: OCKStoreNotification { + public let carePlan: OCKAnyCarePlan + public let category: OCKStoreNotificationCategory + public let storeManager: OCKSynchronizedStoreManager } -struct OCKContactNotification: OCKStoreNotification { - let contact: OCKAnyContact - let category: OCKStoreNotificationCategory - let storeManager: OCKSynchronizedStoreManager +public struct OCKContactNotification: OCKStoreNotification { + public let contact: OCKAnyContact + public let category: OCKStoreNotificationCategory + public let storeManager: OCKSynchronizedStoreManager } -struct OCKTaskNotification: OCKStoreNotification { - let task: OCKAnyTask - let category: OCKStoreNotificationCategory - let storeManager: OCKSynchronizedStoreManager +public struct OCKTaskNotification: OCKStoreNotification { + public let task: OCKAnyTask + public let category: OCKStoreNotificationCategory + public let storeManager: OCKSynchronizedStoreManager } -struct OCKOutcomeNotification: OCKStoreNotification { - let outcome: OCKAnyOutcome - let category: OCKStoreNotificationCategory - let storeManager: OCKSynchronizedStoreManager +public struct OCKOutcomeNotification: OCKStoreNotification { + public let outcome: OCKAnyOutcome + public let category: OCKStoreNotificationCategory + public let storeManager: OCKSynchronizedStoreManager } diff --git a/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager.swift b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager.swift index 6b8554548..14185121c 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager.swift @@ -41,7 +41,7 @@ OCKPatientStoreDelegate, OCKCarePlanStoreDelegate, OCKContactStoreDelegate, OCKT public let store: OCKAnyStoreProtocol internal lazy var subject = PassthroughSubject() - internal private (set) lazy var notificationPublisher = subject.share() + public private (set) lazy var notificationPublisher = subject.share().eraseToAnyPublisher() /// Initialize by wrapping a store. /// From e00c8247408fb4b5ff8f49aa306dc1c22d47dff5 Mon Sep 17 00:00:00 2001 From: tommysarni Date: Thu, 20 Feb 2020 13:33:30 -0500 Subject: [PATCH 12/33] =?UTF-8?q?added=20capability=20for=20a=20OCKDailyPa?= =?UTF-8?q?geViewController=20to=20select=20a=20certain=E2=80=A6=20(#371)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added ability for OCKDailyPageViewController to jump to a given date --- .../Controller/OCKDailyPageViewController.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift b/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift index 7939b3cb5..f228dd4fc 100644 --- a/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift +++ b/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift @@ -110,6 +110,13 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { // MARK: - Properties + open func selectDate(_ date: Date, animated: Bool) { + let previousDate = selectedDate + guard !Calendar.current.isDate(previousDate, inSameDayAs: date) else { return } + calendarWeekPageViewController.selectDate(date, animated: true) + weekCalendarPageViewController(calendarWeekPageViewController, didSelectDate: date, previousDate: previousDate) + } + override open func viewSafeAreaInsetsDidChange() { updateScrollViewInsets() } @@ -145,11 +152,7 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { @objc private func pressedToday(sender: UIBarButtonItem) { - let previousDate = selectedDate - let currentDate = Date() - guard !Calendar.current.isDate(previousDate, inSameDayAs: currentDate) else { return } - calendarWeekPageViewController.selectDate(currentDate, animated: true) - weekCalendarPageViewController(calendarWeekPageViewController, didSelectDate: currentDate, previousDate: previousDate) + selectDate(Date(), animated: true) } private func updateScrollViewInsets() { From 6a7f1535eb6bc8a385f17c162073f8732bec42cd Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Fri, 21 Feb 2020 15:00:24 -0800 Subject: [PATCH 13/33] Fix query by tag bug (#372) This commit fixes a problem in which queries against tags would work on .inMemory stores but not on .onDisk stores. The underlying problem was that CoreData serializes arrays of tags to binary when they are saved to disk, and predicates do no evaluate properly against binary blobs. In memory stores do not serialize anything, so predicates do work as one would expect. The solution was to perform filtering against tags at the CareKitStore level instead of the CoreData level. This solution is sub-optimal from a performance perspective, but will likely never manifest as a problem given the number of tasks in a typical CareKit app. We considered addressing the root problem by changing the tags from an array of strings into a many-to-many relationship with its own table. However, this would require a complicated database migration and doesn't seem worth the effort or the risk. In the future, we may consider making this change if it can be batched with other changes into a single migration. --- .../CoreData/OCKStore+CarePlans.swift | 9 ++++----- .../CareKitStore/CoreData/OCKStore+Contacts.swift | 9 ++++----- .../CareKitStore/CoreData/OCKStore+Outcomes.swift | 15 +++++++-------- .../CareKitStore/CoreData/OCKStore+Patients.swift | 9 ++++----- CareKitStore/CareKitStore/CoreData/OCKStore.swift | 5 ----- .../Protocols/OCKObjectCompatible.swift | 5 +++++ .../OCKStore/TestStore+Outcomes.swift | 15 +++++++++++++++ 7 files changed, 39 insertions(+), 28 deletions(-) diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift index f5e9216e1..aa6d64e0c 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift @@ -54,7 +54,10 @@ extension OCKStore { fetchRequest.sortDescriptors = self.buildSortDescriptors(from: query) } - let plans = persistedPlans.map(self.makePlan) + let plans = persistedPlans + .map(self.makePlan) + .filter({ $0.matches(tags: query.tags) }) + callbackQueue.async { completion(.success(plans)) } } catch { self.context.rollback() @@ -191,10 +194,6 @@ extension OCKStore { predicate = predicate.including(groupIdentifiers: query.groupIdentifiers) } - if !query.tags.isEmpty { - predicate = predicate.including(tags: query.tags) - } - return predicate } diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+Contacts.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+Contacts.swift index bd1067136..b5eedfd05 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+Contacts.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+Contacts.swift @@ -43,7 +43,10 @@ extension OCKStore { fetchRequest.sortDescriptors = self.buildSortDescriptors(for: query) } - let contacts = persistedContacts.map(self.makeContact) + let contacts = persistedContacts + .map(self.makeContact) + .filter({ $0.matches(tags: query.tags) }) + callbackQueue.async { completion(.success(contacts)) } } catch { callbackQueue.async { completion(.failure(.fetchFailed(reason: "Failed to fetch contacts. Error: \(error.localizedDescription)"))) } @@ -227,10 +230,6 @@ extension OCKStore { predicate = predicate.including(groupIdentifiers: query.groupIdentifiers) } - if !query.tags.isEmpty { - predicate = predicate.including(tags: query.tags) - } - return predicate } diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift index b8eb47f4e..70ab03a6b 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift @@ -43,7 +43,10 @@ extension OCKStore { fetchRequest.sortDescriptors = self.buildSortDescriptors(for: query) } - let outcomes = try objects.map(self.makeOutcome) + let outcomes = try objects + .map(self.makeOutcome) + .filter({ $0.matches(tags: query.tags) }) + callbackQueue.async { completion(.success(outcomes)) } } catch { self.context.rollback() @@ -148,11 +151,11 @@ extension OCKStore { guard var task = context.object(with: taskID) as? OCKCDTask else { fatalError("taskID pointed to a non-task class") } let schedule = makeSchedule(elements: Array(task.scheduleElements)) while let nextVersion = task.next as? OCKCDTask { - let outcomeDate = schedule.event(forOccurrenceIndex: outcome.taskOccurrenceIndex)!.start - if nextVersion.effectiveDate <= outcomeDate { + let eventDate = schedule.event(forOccurrenceIndex: outcome.taskOccurrenceIndex)!.start + if nextVersion.effectiveDate <= eventDate { throw OCKStoreError.invalidValue(reason: """ Tried to place an outcome in a date range overshadowed by a future version of task. - The outcome is dated \(outcomeDate), but a newer version of the task starts on \(nextVersion.effectiveDate). + The event for the outcome is dated \(eventDate), but a newer version of the task starts on \(nextVersion.effectiveDate). """) } task = nextVersion @@ -226,10 +229,6 @@ extension OCKStore { predicate = predicate.including(groupIdentifiers: query.groupIdentifiers) } - if !query.tags.isEmpty { - predicate = predicate.including(tags: query.tags) - } - return predicate } diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+Patients.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+Patients.swift index 32f02c1fa..1df91954d 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+Patients.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+Patients.swift @@ -43,7 +43,10 @@ extension OCKStore { fetchRequest.sortDescriptors = self.buildSortDescriptors(from: query) } - let patients = patientsObjects.map(self.makePatient) + let patients = patientsObjects + .map(self.makePatient) + .filter({ $0.matches(tags: query.tags) }) + callbackQueue.async { completion(.success(patients)) } } catch { self.context.rollback() @@ -173,10 +176,6 @@ extension OCKStore { predicate = predicate.including(groupIdentifiers: query.groupIdentifiers) } - if !query.tags.isEmpty { - predicate = predicate.including(tags: query.tags) - } - return predicate } diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore.swift b/CareKitStore/CareKitStore/CoreData/OCKStore.swift index a0ed1fd50..61e7fee42 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore.swift @@ -95,9 +95,4 @@ internal extension NSPredicate { let groupPredicate = NSPredicate(format: "%K IN %@", #keyPath(OCKCDObject.groupIdentifier), groupIdentifiers) return NSCompoundPredicate(andPredicateWithSubpredicates: [self, groupPredicate]) } - - func including(tags: [String]) -> NSPredicate { - let tagsPredicate = NSPredicate(format: "SOME %K IN %@", #keyPath(OCKCDObject.tags), tags) - return NSCompoundPredicate(andPredicateWithSubpredicates: [self, tagsPredicate]) - } } diff --git a/CareKitStore/CareKitStore/Protocols/OCKObjectCompatible.swift b/CareKitStore/CareKitStore/Protocols/OCKObjectCompatible.swift index 974f687e8..e12f5644c 100644 --- a/CareKitStore/CareKitStore/Protocols/OCKObjectCompatible.swift +++ b/CareKitStore/CareKitStore/Protocols/OCKObjectCompatible.swift @@ -116,6 +116,11 @@ extension OCKObjectCompatible { return note } } + + func matches(tags: [String]) -> Bool { + if tags.isEmpty { return true } + return !Set(self.tags ?? []).isDisjoint(with: tags) + } } extension OCKVersionedObjectCompatible { diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift index d00b07d79..703d9f844 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift @@ -203,6 +203,21 @@ class TestStoreOutcomes: XCTestCase { XCTAssert(fetched == outcome) } + func testQueryOutcomeByTag() throws { + var task = OCKTask(id: "A", title: nil, carePlanID: nil, schedule: .mealTimesEachDay(start: Date(), end: nil)) + task = try store.addTaskAndWait(task) + + var outcome = OCKOutcome(taskID: try task.getLocalID(), taskOccurrenceIndex: 0, values: []) + outcome.tags = ["123"] + outcome = try store.addOutcomeAndWait(outcome) + + var query = OCKOutcomeQuery(for: Date()) + query.tags = ["123"] + + let fetched = try store.fetchOutcomesAndWait(query: query).first + XCTAssert(fetched == outcome) + } + // MARK: Updating func testUpdateOutcomes() throws { From 8e4a14c25b4b22b7722876cf3c133f2fa8724364 Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Sat, 22 Feb 2020 08:31:56 -0800 Subject: [PATCH 14/33] Fixed query regression (#373) * Fixed query regression The problem was that a previous commit (hash 72625cb92f3c58f86c48bbb7317296faf2c1f568) incorrectly modified the header predicate such that it was no longer checking if a "next" version exists. The solution was to rectify the predicate. * Update .travis.yml --- .travis.yml | 4 +-- .../CoreData/OCKCDVersionedObject.swift | 2 +- .../OCKStore/TestStore+Contacts.swift | 33 +++++++++++++++---- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 93425997e..36f366b92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: swift -osx_image: xcode11 +osx_image: xcode11.3 xcode_workspace: CKWorkspace.xcworkspace xcode_scheme: CareKit -xcode_destination: platform=iOS Simulator,OS=13.0,name=iPhone 11 Pro Max +xcode_destination: platform=iOS Simulator,OS=13.3,name=iPhone 11 Pro Max diff --git a/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift b/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift index 678e3f710..2f4c0f992 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift @@ -75,7 +75,7 @@ class OCKCDVersionedObject: OCKCDObject, OCKCDManageable { static func headerPredicate() -> NSPredicate { return NSCompoundPredicate(andPredicateWithSubpredicates: [ notDeletedPredicate, - NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) + NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.next)) ]) } diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Contacts.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Contacts.swift index 1f1a85209..324de420d 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Contacts.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Contacts.swift @@ -234,12 +234,33 @@ class TestStoreContacts: XCTestCase { XCTAssertThrowsError(try store.updateContactAndWait(patient)) } - func testContactQueryOnlyReturnsLatestVersionOfAContact() throws { - let versionA = try store.addContactAndWait(OCKContact(id: "contact", givenName: "Amy", familyName: "Frost", carePlanID: nil)) - let versionB = try store.updateContactAndWait(OCKContact(id: "contact", givenName: "Mariana", familyName: "Lin", carePlanID: nil)) - let fetched = try store.fetchContactAndWait(id: versionA.id) - XCTAssert(fetched?.id == versionB.id) - XCTAssert(fetched?.name == versionB.name) + func testContactQueryByIDOnlyReturnsLatestVersionOfAContact() throws { + try store.addContactAndWait(OCKContact(id: "contact", givenName: "A", familyName: "", carePlanID: nil)) + try store.updateContactAndWait(OCKContact(id: "contact", givenName: "B", familyName: "", carePlanID: nil)) + try store.updateContactAndWait(OCKContact(id: "contact", givenName: "C", familyName: "", carePlanID: nil)) + let versionD = try store.updateContactAndWait(OCKContact(id: "contact", givenName: "D", familyName: "", carePlanID: nil)) + let fetched = try store.fetchContactAndWait(id: "contact") + XCTAssert(fetched?.id == versionD.id) + XCTAssert(fetched?.name == versionD.name) + } + + func testContactQueryWithDateOnlyReturnsLatestVersionOfAContact() throws { + try store.addContactAndWait(OCKContact(id: "contact", givenName: "A", familyName: "", carePlanID: nil)) + try store.updateContactAndWait(OCKContact(id: "contact", givenName: "B", familyName: "", carePlanID: nil)) + try store.updateContactAndWait(OCKContact(id: "contact", givenName: "C", familyName: "", carePlanID: nil)) + try store.updateContactAndWait(OCKContact(id: "contact", givenName: "D", familyName: "", carePlanID: nil)) + let fetched = try store.fetchContactsAndWait(query: OCKContactQuery(for: Date())) + XCTAssert(fetched.count == 1) + XCTAssert(fetched.first?.name.givenName == "D") + } + + func testContactQueryWithNoDateReturnsAllVersionsOfAContact() throws { + try store.addContactAndWait(OCKContact(id: "contact", givenName: "A", familyName: "", carePlanID: nil)) + try store.updateContactAndWait(OCKContact(id: "contact", givenName: "B", familyName: "", carePlanID: nil)) + try store.updateContactAndWait(OCKContact(id: "contact", givenName: "C", familyName: "", carePlanID: nil)) + try store.updateContactAndWait(OCKContact(id: "contact", givenName: "D", familyName: "", carePlanID: nil)) + let fetched = try store.fetchContactsAndWait(query: OCKContactQuery()) + XCTAssert(fetched.count == 4) } func testContactQueryOnPastDateReturnsPastVersionOfAContact() throws { From f5cbdc9ef0139b53b18ca0921133486ac242e22e Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Tue, 25 Feb 2020 16:44:24 -0800 Subject: [PATCH 15/33] Resolve task version chain bug (#374) * Ensure task view controllers update when their watched task changes * Address version look up logic error This commit addresses a logic error in the store's recursive event fetch method. When fetching events from old versions of a task, the query start date was being set to a date that was later than the query applied at the beginning of the recursion. This resulted in invalid date ranges when recursing back more than one task version. * Fix single event query bug This commit addresses a problem in which fetchEvent(task:occurrence:) would fail if the outcome for the event was nil. The solution was to use the fetchOutcomes (plural) method instead of fetchOutcome (singular) because the former succeeds with an empty array when no outcomes exist. --- .../Task/Controllers/OCKTaskController.swift | 17 +++++++++------ .../OCKTaskViewController.swift | 2 +- .../Protocols/Events/OCKEventStore.swift | 9 ++++---- .../TestStoreProtocolExtensions.swift | 21 +++++++++++++++++++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskController.swift index 251ef23bd..4bc2d38ed 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskController.swift @@ -45,7 +45,7 @@ open class OCKTaskController: OCKTaskControllerProtocol, ObservableObject { /// The store manager against which the task will be synchronized. public let storeManager: OCKSynchronizedStoreManager - private var subscription: AnyCancellable? + private var cancellables: Set = Set() // MARK: - Life Cycle @@ -63,7 +63,7 @@ open class OCKTaskController: OCKTaskControllerProtocol, ObservableObject { /// - task: The task to watch for changes. /// - eventQuery: A query describing the date range over which to watch for changes. open func fetchAndObserveEvents(forTask task: OCKAnyTask, eventQuery: OCKEventQuery, errorHandler: ((Error) -> Void)? = nil) { - fetchAndSubscribeToEvents(forTask: task, query: eventQuery, errorHandler: errorHandler) + fetchAndObserveEvents(forTaskIDs: [task.id], eventQuery: eventQuery, errorHandler: errorHandler) } /// Begin watching events from multiple tasks for changes. @@ -72,6 +72,7 @@ open class OCKTaskController: OCKTaskControllerProtocol, ObservableObject { /// - taskIDs: The user-chosen unique identifiers for the tasks to be watched. /// - eventQuery: A query describing the date range over which to watch for changes. open func fetchAndObserveEvents(forTaskIDs taskIDs: [String], eventQuery: OCKEventQuery, errorHandler: ((Error) -> Void)? = nil) { + cancellables = Set() // Build the task query from the event query var taskQuery = OCKTaskQuery(dateInterval: eventQuery.dateInterval) @@ -83,7 +84,11 @@ open class OCKTaskController: OCKTaskControllerProtocol, ObservableObject { case .failure(let error): errorHandler?(error) case .success(let tasks): tasks.forEach { - self?.fetchAndSubscribeToEvents(forTask: $0, query: eventQuery, errorHandler: errorHandler) + guard let self = self else { return } + self.fetchAndSubscribeToEvents(forTask: $0, query: eventQuery, errorHandler: errorHandler) + self.storeManager.publisher(forTask: $0, categories: [.add, .update, .delete]).sink { [weak self] _ in + self?.fetchAndObserveEvents(forTaskIDs: taskIDs, eventQuery: eventQuery, errorHandler: errorHandler) + }.store(in: &self.cancellables) } } } @@ -103,7 +108,7 @@ open class OCKTaskController: OCKTaskControllerProtocol, ObservableObject { assert(taskIds.dropFirst().allSatisfy { $0 == taskIds.first }, "Events should belong to the same task.") // Add each event to the view model and set the view model value - var viewModel = self.objectWillChange.value ?? OCKTaskEvents() + var viewModel = OCKTaskEvents() events.map { self.modified(event: $0) } .sorted(by: { $0.scheduleEvent.start < $1.scheduleEvent.start }) .forEach { viewModel.addEvent($0) } @@ -116,14 +121,14 @@ open class OCKTaskController: OCKTaskControllerProtocol, ObservableObject { // Update the view model when events for a particular task change func subscribeTo(eventsBelongingToTask task: OCKAnyTask, eventQuery: OCKEventQuery) { - subscription = storeManager.publisher(forEventsBelongingToTask: task, query: eventQuery, categories: [.update, .add, .delete]) + storeManager.publisher(forEventsBelongingToTask: task, query: eventQuery, categories: [.update, .add, .delete]) .sink { [weak self] newValue in guard let self = self else { return } let modifiedEvent = self.modified(event: newValue) self.objectWillChange.value?.containsEvent(modifiedEvent) ?? false ? self.objectWillChange.value?.updateEvent(modifiedEvent) : self.objectWillChange.value?.addEvent(modifiedEvent) - } + }.store(in: &cancellables) } private func fetchAndSubscribeToEvents(forTask task: OCKAnyTask, query: OCKEventQuery, errorHandler: ((Error) -> Void)? = nil) { diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift index 227bb9e6b..4cc2be00a 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift @@ -112,7 +112,7 @@ UIViewController, OCKTaskViewDelegate { // Reset view state on a failure // Note: This is needed because the UI assumes user interactions (lke button taps) will be successful, and displays the corresponding - // state immedately. When the interaction is actually unsuccessful, we need to reset the UI. + // state immediately. When the interaction is actually unsuccessful, we need to reset the UI. func resetViewState() { controller.objectWillChange.value = controller.objectWillChange.value // triggers an update to the view } diff --git a/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift b/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift index 7195460ab..5fafacb6d 100644 --- a/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift +++ b/CareKitStore/CareKitStore/Protocols/Events/OCKEventStore.swift @@ -127,11 +127,12 @@ public extension OCKReadOnlyEventStore where Task: OCKAnyVersionableTask, Outcom let late = scheduleEvent.end.addingTimeInterval(1) var query = OCKOutcomeQuery(dateInterval: DateInterval(start: early, end: late)) query.taskVersionIDs = [taskVersionID] - self.fetchOutcome(query: query, callbackQueue: callbackQueue, completion: { result in + self.fetchOutcomes(query: query, callbackQueue: callbackQueue, completion: { result in switch result { case .failure(let error): completion(.failure(.fetchFailed(reason: "Couldn't find outcome. \(error.localizedDescription)"))) - case .success(let outcome): - let event = OCKEvent(task: task, outcome: outcome, scheduleEvent: scheduleEvent) + case .success(let outcomes): + let matchingOutcome = outcomes.first(where: { $0.taskOccurrenceIndex == occurrenceIndex }) + let event = OCKEvent(task: task, outcome: matchingOutcome, scheduleEvent: scheduleEvent) completion(.success(event)) } }) @@ -175,7 +176,7 @@ public extension OCKReadOnlyEventStore where Task: OCKAnyVersionableTask, Outcom // If there is a previous version, fetch the events for it that don't overlap with // any of the versions we've already fetched events for. let nextEndDate = task.effectiveDate - let nextStartDate = max(query.dateInterval.start, previousVersion.effectiveDate) + let nextStartDate = query.dateInterval.start let nextInterval = DateInterval(start: nextStartDate, end: nextEndDate) let nextQuery = OCKEventQuery(dateInterval: nextInterval) self.fetchEvents(task: previousVersion, query: nextQuery, previousEvents: events, diff --git a/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift b/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift index 0e01785b6..588ed8c96 100644 --- a/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift +++ b/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift @@ -231,6 +231,27 @@ class TestStoreProtocolExtensions: XCTestCase { XCTAssert(events.first?.task.title == "B") } + func testFetchEventsReturnsAnEventForEachVersionOfATaskWhenEventsAreAllDayDuration() throws { + let midnight = Calendar.current.startOfDay(for: Date()) + let schedule = OCKSchedule.dailyAtTime(hour: 0, minutes: 0, start: midnight, end: nil, text: nil, duration: .allDay, targetValues: []) + let task = OCKTask(id: "A", title: "Original", carePlanID: nil, schedule: schedule) + try store.addTaskAndWait(task) + for i in 1...10 { + var update = task + update.effectiveDate = midnight.advanced(by: 10 * TimeInterval(i)) + update.title = "Update \(i)" + try store.updateTaskAndWait(update) + } + let events = try store.fetchEventsAndWait(taskID: "A", query: .init(for: midnight)) + XCTAssert(events.count == 11) + } + + func testFetchSingleEventSucceedsEvenIfThereIsNoOutcome() throws { + let schedule = OCKSchedule.mealTimesEachDay(start: Date(), end: nil) + let task = try store.addTaskAndWait(OCKTask(id: "A", title: "ABC", carePlanID: nil, schedule: schedule)) + XCTAssertNoThrow(try store.fetchEventAndWait(forTask: task, occurrence: 0)) + } + // MARK: Adherence and Insights func testFetchAdherenceAggregatesEventsAcrossTasks() throws { From 141e4116f39f2cc48d96b6992f93f11d318b84b7 Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Tue, 3 Mar 2020 13:22:39 -0800 Subject: [PATCH 16/33] Add method for deleting an OCKStore (#381) Added a method for deleting OCKStore from disk. --- .../CareKitStore.xcodeproj/project.pbxproj | 5 +- .../CareKitStore/CoreData/OCKStore.swift | 14 ++++++ .../CoreData/OCKCoreDataStoreProtocol.swift | 18 ++++++- .../OCKStore/TestStore.swift | 50 +++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 CareKitStore/CareKitStoreTests/OCKStore/TestStore.swift diff --git a/CareKitStore/CareKitStore.xcodeproj/project.pbxproj b/CareKitStore/CareKitStore.xcodeproj/project.pbxproj index f008c172c..e352c2c67 100644 --- a/CareKitStore/CareKitStore.xcodeproj/project.pbxproj +++ b/CareKitStore/CareKitStore.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 03678DE82342C59200E27926 /* OCKLabeledValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03678DE72342C59200E27926 /* OCKLabeledValue.swift */; }; 03678DF02343B21F00E27926 /* OCKAnyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03678DEF2343B21F00E27926 /* OCKAnyEvent.swift */; }; 03678DF22343B23400E27926 /* OCKAnyEventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03678DF12343B23400E27926 /* OCKAnyEventStore.swift */; }; + 03D40832240D87CC0033C09E /* TestStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D40831240D87CC0033C09E /* TestStore.swift */; }; 03ABAAB823146772001FCACE /* OCKUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ABAAB723146772001FCACE /* OCKUtilities.swift */; }; 03CB6EBF2316F14C0081AA7C /* OCKHealthKitLinkage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CB6EBE2316F14C0081AA7C /* OCKHealthKitLinkage.swift */; }; 03CF3601230DCDF100A66A38 /* OCKSemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CF3600230DCDF100A66A38 /* OCKSemanticVersion.swift */; }; @@ -172,6 +173,7 @@ 03678DE72342C59200E27926 /* OCKLabeledValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLabeledValue.swift; sourceTree = ""; }; 03678DEF2343B21F00E27926 /* OCKAnyEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAnyEvent.swift; sourceTree = ""; }; 03678DF12343B23400E27926 /* OCKAnyEventStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAnyEventStore.swift; sourceTree = ""; }; + 03D40831240D87CC0033C09E /* TestStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStore.swift; sourceTree = ""; }; 03ABAAB723146772001FCACE /* OCKUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKUtilities.swift; sourceTree = ""; }; 03CB6EBE2316F14C0081AA7C /* OCKHealthKitLinkage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKHealthKitLinkage.swift; sourceTree = ""; }; 03CF3600230DCDF100A66A38 /* OCKSemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSemanticVersion.swift; sourceTree = ""; }; @@ -568,6 +570,7 @@ E7F445E022CFAC6F0090CC36 /* OCKStore */ = { isa = PBXGroup; children = ( + 03D40831240D87CC0033C09E /* TestStore.swift */, E726C04B22CFADCD001236E2 /* TestStore+Patients.swift */, E726C04D22CFADDE001236E2 /* TestStore+CarePlans.swift */, E726C04F22CFADEC001236E2 /* TestStore+Contacts.swift */, @@ -683,7 +686,6 @@ }; /* End PBXResourcesBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ E784B8F42232EED600736CA5 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -780,6 +782,7 @@ buildActionMask = 2147483647; files = ( E747EC462238140800775C0D /* TestCoreDataSchema+ScheduleElements.swift in Sources */, + 03D40832240D87CC0033C09E /* TestStore.swift in Sources */, E726C05422CFAE04001236E2 /* TestStore+Outcomes.swift in Sources */, 035FE18123451E7600851723 /* TestContact.swift in Sources */, E7857A8C229B60B100FBEFAF /* TestCoreDataSchema+PostalAddress.swift in Sources */, diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore.swift b/CareKitStore/CareKitStore/CoreData/OCKStore.swift index 61e7fee42..cc5f35e90 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore.swift @@ -79,6 +79,20 @@ open class OCKStore: OCKStoreProtocol, OCKCoreDataStoreProtocol, Equatable { self.name = name } + /// Completely deletes the store and all its files from disk. + /// + /// You should not attempt to call any other methods an instance of `OCKStore` + /// after it has been deleted. + public func delete() throws { + try persistentContainer + .persistentStoreCoordinator + .destroyPersistentStore(at: storeURL, ofType: storeType.stringValue, options: nil) + + try FileManager.default.removeItem(at: storeURL) + try FileManager.default.removeItem(at: shmFileURL) + try FileManager.default.removeItem(at: walFileURL) + } + // MARK: Internal internal lazy var persistentContainer: NSPersistentContainer = { diff --git a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataStoreProtocol.swift b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataStoreProtocol.swift index 18f1fe8ac..0109d55f7 100644 --- a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataStoreProtocol.swift +++ b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataStoreProtocol.swift @@ -57,10 +57,26 @@ internal protocol OCKCoreDataStoreProtocol { extension OCKCoreDataStoreProtocol { + var storeDirectory: URL { + NSPersistentContainer.defaultDirectoryURL() + } + + var storeURL: URL { + storeDirectory.appendingPathComponent(name + ".sqlite") + } + + var walFileURL: URL { + storeDirectory.appendingPathComponent(name + ".sqlite-wal") + } + + var shmFileURL: URL { + storeDirectory.appendingPathComponent(name + ".sqlite-shm") + } + func makePersistentContainer() -> NSPersistentContainer { let container = NSPersistentContainer(name: self.name, managedObjectModel: sharedManagedObjectModel) let descriptor = NSPersistentStoreDescription() - descriptor.url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent(name + ".sqlite") + descriptor.url = storeURL descriptor.type = storeType.stringValue descriptor.shouldAddStoreAsynchronously = false descriptor.setOption(FileProtectionType.complete as NSObject, forKey: NSPersistentStoreFileProtectionKey) diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore.swift new file mode 100644 index 000000000..e4c523f56 --- /dev/null +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore.swift @@ -0,0 +1,50 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@testable import CareKitStore +import XCTest + +class TestStore: XCTestCase { + + func testDeleteStore() { + let store = OCKStore(name: "test", type: .onDisk) + _ = store.context // Storage is created lazily. Access context to force file creation. + + XCTAssertTrue(FileManager.default.fileExists(atPath: store.storeURL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: store.walFileURL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: store.shmFileURL.path)) + + XCTAssertNoThrow(try store.delete()) + + XCTAssertFalse(FileManager.default.fileExists(atPath: store.storeURL.path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: store.walFileURL.path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: store.shmFileURL.path)) + } +} From 9ab05beef03164ec8fabcb17fc781d666d16941b Mon Sep 17 00:00:00 2001 From: gavirawson-apple <51756298+gavirawson-apple@users.noreply.github.com> Date: Fri, 6 Mar 2020 15:09:56 -0800 Subject: [PATCH 17/33] Propagate background color in `OCKListView` (#384) --- CareKit/CareKit.xcodeproj/project.pbxproj | 4 ++ CareKit/CareKit/Lists/View/OCKListView.swift | 11 +++-- CareKit/CareKitTests/TestListView.swift | 44 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 CareKit/CareKitTests/TestListView.swift diff --git a/CareKit/CareKit.xcodeproj/project.pbxproj b/CareKit/CareKit.xcodeproj/project.pbxproj index fa66f0de6..5f37103e0 100644 --- a/CareKit/CareKit.xcodeproj/project.pbxproj +++ b/CareKit/CareKit.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ 51B714862367849100590A5A /* OCKButtonLogTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B714852367849100590A5A /* OCKButtonLogTaskView+Updatable.swift */; }; 51BEAD5A237E00C600B32D55 /* TestDailyTasksPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */; }; 51CF0A00235528EC00A343F9 /* OCKTaskControllerProtocol+Methods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CF09FF235528EC00A343F9 /* OCKTaskControllerProtocol+Methods.swift */; }; + 51D8E27F24115D7D0026C716 /* TestListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D8E27E24115D7D0026C716 /* TestListView.swift */; }; 51E88828234CE61300763B97 /* OCKContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E88827234CE61300763B97 /* OCKContactViewController.swift */; }; 51EF7E46234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF7E45234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift */; }; 51EF7E48234FAB7A00B28C0A /* OCKInstructionsTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF7E47234FAB7A00B28C0A /* OCKInstructionsTaskViewSynchronizer.swift */; }; @@ -232,6 +233,7 @@ 51B714852367849100590A5A /* OCKButtonLogTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKButtonLogTaskView+Updatable.swift"; sourceTree = ""; }; 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDailyTasksPageViewController.swift; sourceTree = ""; }; 51CF09FF235528EC00A343F9 /* OCKTaskControllerProtocol+Methods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKTaskControllerProtocol+Methods.swift"; sourceTree = ""; }; + 51D8E27E24115D7D0026C716 /* TestListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestListView.swift; sourceTree = ""; }; 51E88827234CE61300763B97 /* OCKContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKContactViewController.swift; sourceTree = ""; }; 51EF7E45234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleTaskViewSynchronizer.swift; sourceTree = ""; }; 51EF7E47234FAB7A00B28C0A /* OCKInstructionsTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKInstructionsTaskViewSynchronizer.swift; sourceTree = ""; }; @@ -484,6 +486,7 @@ 51FF9B8C2373374200BAEDB2 /* Calendar */, 511372452374DFBD00831191 /* TestSynchronizedContext.swift */, 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */, + 51D8E27E24115D7D0026C716 /* TestListView.swift */, 5196C7FD226F8F8F00F1C2A2 /* Info.plist */, ); path = CareKitTests; @@ -871,6 +874,7 @@ 511372442374CA2B00831191 /* TestCustomChartViewController.swift in Sources */, 511372422374CA1900831191 /* TestCartesianChartViewSynchronizer.swift in Sources */, 51FF9B8E2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift in Sources */, + 51D8E27F24115D7D0026C716 /* TestListView.swift in Sources */, 511372462374DFBD00831191 /* TestSynchronizedContext.swift in Sources */, 51BEAD5A237E00C600B32D55 /* TestDailyTasksPageViewController.swift in Sources */, ); diff --git a/CareKit/CareKit/Lists/View/OCKListView.swift b/CareKit/CareKit/Lists/View/OCKListView.swift index 6374e7960..c7c614109 100644 --- a/CareKit/CareKit/Lists/View/OCKListView.swift +++ b/CareKit/CareKit/Lists/View/OCKListView.swift @@ -33,6 +33,13 @@ import UIKit /// A view enclosing a scrollable stack view. internal class OCKListView: OCKView { + override var backgroundColor: UIColor? { + didSet { + contentView.backgroundColor = backgroundColor + scrollView.backgroundColor = backgroundColor + } + } + // MARK: Properties /// The stack view embedded inside the scroll view. @@ -45,7 +52,7 @@ internal class OCKListView: OCKView { /// The scroll view that contains the stack view. let scrollView = UIScrollView() - private let contentView = UIView() + let contentView = UIView() // MARK: - Life Cycle @@ -68,7 +75,6 @@ internal class OCKListView: OCKView { } private func styleSubviews() { - scrollView.backgroundColor = contentView.backgroundColor scrollView.alwaysBounceVertical = true } @@ -97,7 +103,6 @@ internal class OCKListView: OCKView { let cachedStyle = style() contentView.directionalLayoutMargins = cachedStyle.dimension.directionalInsets1 backgroundColor = cachedStyle.color.customGroupedBackground - contentView.backgroundColor = cachedStyle.color.customGroupedBackground stackView.spacing = cachedStyle.dimension.directionalInsets1.top } } diff --git a/CareKit/CareKitTests/TestListView.swift b/CareKit/CareKitTests/TestListView.swift new file mode 100644 index 000000000..349e66307 --- /dev/null +++ b/CareKit/CareKitTests/TestListView.swift @@ -0,0 +1,44 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@testable import CareKit +import Foundation +import XCTest + +class TestListView: XCTestCase { + + func testBackgroundColorPropagates() { + let view = OCKListView() + view.backgroundColor = .red + XCTAssertEqual(view.backgroundColor, .red) + XCTAssertEqual(view.backgroundColor, view.scrollView.backgroundColor) + XCTAssertEqual(view.contentView.backgroundColor, view.scrollView.backgroundColor) + } +} From 713d9320ce304d24f55cd09aeaa875398110d639 Mon Sep 17 00:00:00 2001 From: gavirawson-apple <51756298+gavirawson-apple@users.noreply.github.com> Date: Wed, 18 Mar 2020 12:46:37 -0700 Subject: [PATCH 18/33] Fix umbrella framework (#389) --- CareKit/CareKit.xcodeproj/project.pbxproj | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/CareKit/CareKit.xcodeproj/project.pbxproj b/CareKit/CareKit.xcodeproj/project.pbxproj index 5f37103e0..b82478faf 100644 --- a/CareKit/CareKit.xcodeproj/project.pbxproj +++ b/CareKit/CareKit.xcodeproj/project.pbxproj @@ -8,9 +8,7 @@ /* Begin PBXBuildFile section */ 032C86F02326B68D00D0A0EA /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */; }; - 1409474C22B020C4005C1D16 /* CareKitStore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1409474A22B020C4005C1D16 /* CareKitStore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1409474E22B020CA005C1D16 /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1409474D22B020CA005C1D16 /* CareKitUI.framework */; }; - 1409474F22B020CA005C1D16 /* CareKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1409474D22B020CA005C1D16 /* CareKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1409475122B02153005C1D16 /* CareKitStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1409475022B02153005C1D16 /* CareKitStore.framework */; }; 5101E6CB23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */; }; 51094D94234F8E3E00B4BFFB /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */; }; @@ -142,21 +140,6 @@ }; /* End PBXContainerItemProxy section */ -/* Begin PBXCopyFilesBuildPhase section */ - 1409474722B020A2005C1D16 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 1409474F22B020CA005C1D16 /* CareKitUI.framework in Embed Frameworks */, - 1409474C22B020C4005C1D16 /* CareKitStore.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; 03A2F774237F51C200A13638 /* CareKitStore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CareKitStore.xcodeproj; path = ../CareKitStore/CareKitStore.xcodeproj; sourceTree = ""; }; @@ -750,7 +733,6 @@ 8605A5B71C4F04EC00DD65FF /* Headers */, 8605A5B61C4F04EC00DD65FF /* Frameworks */, 8605A5B81C4F04EC00DD65FF /* Resources */, - 1409474722B020A2005C1D16 /* Embed Frameworks */, ); buildRules = ( ); From 1e26d6702b18a3478c53857b93d98bd533344dbf Mon Sep 17 00:00:00 2001 From: gavirawson-apple <51756298+gavirawson-apple@users.noreply.github.com> Date: Wed, 18 Mar 2020 14:27:32 -0700 Subject: [PATCH 19/33] Memory leak fix (#390) Fix memory leak --- .../View Controllers/OCKCalendarViewController.swift | 6 +++--- .../Chart/View Controllers/OCKChartViewController.swift | 6 +++--- .../Contact/View Controllers/OCKContactViewController.swift | 6 +++--- .../Task/View Controllers/OCKTaskViewController.swift | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKCalendarViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKCalendarViewController.swift index ee48d99fc..1c7d76d32 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKCalendarViewController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKCalendarViewController.swift @@ -105,9 +105,9 @@ UIViewController, OCKCalendarViewDelegate { viewModelSubscription?.cancel() viewModelSubscription = controller.objectWillChange .context() - .sink { [view] context in - guard let typedView = view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } - self.viewSynchronizer.updateView(typedView, context: context) + .sink { [weak self] context in + guard let typedView = self?.view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } + self?.viewSynchronizer.updateView(typedView, context: context) } } diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKChartViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKChartViewController.swift index dc1da8a01..fb15cc32f 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKChartViewController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKChartViewController.swift @@ -104,9 +104,9 @@ UIViewController, OCKChartViewDelegate { viewModelSubscription?.cancel() viewModelSubscription = controller.objectWillChange .context() - .sink { [view] context in - guard let typedView = view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } - self.viewSynchronizer.updateView(typedView, context: context) + .sink { [weak self] context in + guard let typedView = self?.view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } + self?.viewSynchronizer.updateView(typedView, context: context) } } diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKContactViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKContactViewController.swift index 4f6284f87..c01157de2 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKContactViewController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKContactViewController.swift @@ -105,9 +105,9 @@ UIViewController, OCKContactViewDelegate, MFMessageComposeViewControllerDelegate viewModelSubscription?.cancel() viewModelSubscription = controller.objectWillChange .context() - .sink { [view] context in - guard let typedView = view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } - self.viewSynchronizer.updateView(typedView, context: context) + .sink { [weak self] context in + guard let typedView = self?.view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } + self?.viewSynchronizer.updateView(typedView, context: context) } } diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift index 4cc2be00a..84ca79312 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift @@ -104,9 +104,9 @@ UIViewController, OCKTaskViewDelegate { viewModelSubscription?.cancel() viewModelSubscription = controller.objectWillChange .context() - .sink { [view] context in - guard let typedView = view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } - self.viewSynchronizer.updateView(typedView, context: context) + .sink { [weak self] context in + guard let typedView = self?.view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } + self?.viewSynchronizer.updateView(typedView, context: context) } } From edc5b8f74fb7e0423ee7deee7d34f6bfc0772f41 Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Thu, 19 Mar 2020 17:07:51 -0700 Subject: [PATCH 20/33] Fix bug in deserialization of OCKScheduleElement (#391) --- CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift | 2 ++ .../CareKitStoreTests/Structs/TestScheduleElement.swift | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift b/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift index a793e1204..a4f22e332 100644 --- a/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift +++ b/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift @@ -78,9 +78,11 @@ public struct OCKScheduleElement: Codable, Equatable, OCKObjectCompatible { let container = try decoder.container(keyedBy: CodingKeys.self) if try container.decodeIfPresent(Bool.self, forKey: .isAllDay) == true { self = .allDay + return } if let seconds = try container.decodeIfPresent(Double.self, forKey: .seconds) { self = .seconds(seconds) + return } throw DecodingError.dataCorruptedError(forKey: CodingKeys.seconds, in: container, debugDescription: "No seconds or allDay key was found!") } diff --git a/CareKitStore/CareKitStoreTests/Structs/TestScheduleElement.swift b/CareKitStore/CareKitStoreTests/Structs/TestScheduleElement.swift index 5b5e6ac1d..7c2dfa047 100644 --- a/CareKitStore/CareKitStoreTests/Structs/TestScheduleElement.swift +++ b/CareKitStore/CareKitStoreTests/Structs/TestScheduleElement.swift @@ -51,6 +51,13 @@ class TestScheduleElement: XCTestCase { var element: OCKScheduleElement { return OCKScheduleElement(start: date, end: nil, interval: interval, text: "Wedding Anniversary", targetValues: []) } + + func testSerialization() throws { + XCTAssertNoThrow(try JSONEncoder().encode(element)) + let data = try JSONEncoder().encode(element) + let decoded = try JSONDecoder().decode(OCKScheduleElement.self, from: data) + XCTAssert(element == decoded) + } func testSubscript() { let event = element[0] From 62aba159abfef0da6254e1bcc48ed89beaaaa0c7 Mon Sep 17 00:00:00 2001 From: gavirawson-apple <51756298+gavirawson-apple@users.noreply.github.com> Date: Fri, 20 Mar 2020 17:33:39 -0700 Subject: [PATCH 21/33] Fix disappearing button border (#392) Fix disappearing grid circles --- .../Common/Controls/OCKCheckmarkButton.swift | 79 ++++++++++++------- .../Style/Stylers/OCKAppearanceStyler.swift | 2 +- .../Buttons/OCKLabeledCheckmarkButton.swift | 8 +- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift b/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift index 388b48bf7..fff329f0d 100644 --- a/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift +++ b/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift @@ -48,21 +48,17 @@ open class OCKCheckmarkButton: OCKAnimatedButton { self?.invalidateIntrinsicContentSize() } - lazy var lineWidth = OCKAccessibleValue(container: style(), keyPath: \.appearance.borderWidth1) { [circleMaskBorderLayer] scaledValue in - circleMaskBorderLayer.lineWidth = scaledValue + lazy var lineWidth = OCKAccessibleValue(container: style(), keyPath: \.appearance.borderWidth1) { [weak self] scaledValue in + guard let self = self else { return } + self.updateLayers(for: self.bounds, borderWidth: scaledValue) } lazy var imageViewPointSize = OCKAccessibleValue(container: style(), keyPath: \.dimension.symbolPointSize3) { [imageView] scaledValue in imageView.preferredSymbolConfiguration = .init(pointSize: scaledValue, weight: .bold) } - private let circleMaskBorderLayer: CAShapeLayer = { - let layer = CAShapeLayer() - layer.fillColor = UIColor.clear.cgColor - return layer - }() - - private let circleMask = CAShapeLayer() + private let borderLayer = CAShapeLayer() + private let fillLayer = CAShapeLayer() // MARK: Life cycle @@ -78,7 +74,7 @@ open class OCKCheckmarkButton: OCKAnimatedButton { override open func layoutSubviews() { super.layoutSubviews() - updateMaskFor(rect: bounds) + updateLayers(for: bounds, borderWidth: lineWidth.scaledValue) } // MARK: Methods @@ -87,12 +83,13 @@ open class OCKCheckmarkButton: OCKAnimatedButton { constrainSubviews() styleSubviews() - layer.mask = circleMask - layer.addSublayer(circleMaskBorderLayer) + layer.insertSublayer(borderLayer, below: imageView.layer) + layer.insertSublayer(fillLayer, below: imageView.layer) } private func styleSubviews() { clipsToBounds = true + updateLayerTintColors() setStyleForSelectedState(false) } @@ -104,9 +101,22 @@ open class OCKCheckmarkButton: OCKAnimatedButton { ]) } - private func updateMaskFor(rect: CGRect) { - circleMask.path = UIBezierPath(ovalIn: rect).cgPath - circleMaskBorderLayer.path = UIBezierPath(ovalIn: rect).cgPath + private func updateLayers(for bounds: CGRect, borderWidth: CGFloat) { + // Outer mask to make the view a circle + let circleMask = UIBezierPath(ovalIn: bounds) + + // Set the path for the fill layer + fillLayer.path = circleMask.cgPath + + // A smaller rect that takes the border width into account + let innerRect = CGRect(x: bounds.minX + borderWidth, y: bounds.minY + borderWidth, + width: bounds.width - borderWidth * 2, height: bounds.height - borderWidth * 2) + let path = UIBezierPath(ovalIn: innerRect) + path.append(circleMask) + + // Set the path for the border layer + borderLayer.fillRule = .evenOdd + borderLayer.path = path.cgPath } override open func styleDidChange() { @@ -122,11 +132,7 @@ open class OCKCheckmarkButton: OCKAnimatedButton { override open func tintColorDidChange() { super.tintColorDidChange() - circleMaskBorderLayer.strokeColor = tintColor.cgColor - - if isSelected { - backgroundColor = tintColor - } + updateLayerTintColors() } override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -136,15 +142,30 @@ open class OCKCheckmarkButton: OCKAnimatedButton { } } - /// Set the style for the selected state. This function may be called in an animation block if the state is being animated. - /// - Parameter isSelected: True if the button is in the selected state. - override open func setStyleForSelectedState(_ isSelected: Bool) { - if isSelected { - imageView.tintColor = style().color.customBackground - backgroundColor = tintColor - } else { - imageView.tintColor = .clear - backgroundColor = .clear + override open func setStyleForSelectedState(_ isSelected: Bool) {} + + override open func setSelected(_ isSelected: Bool, animated: Bool) { + super.setSelected(isSelected, animated: animated) + + // Note: CALayers properties are implicitly animated, but this function may get called multiple times during the course of an animation. + // Without turning off animations, the button will flash when tapped multiple times. + CATransaction.performWithoutAnimations { [weak self] in + self?.fillLayer.isHidden = !isSelected } + imageView.tintColor = isSelected ? style().color.customBackground : .clear + } + + private func updateLayerTintColors() { + fillLayer.fillColor = tintColor.cgColor + borderLayer.fillColor = tintColor.cgColor + } +} + +private extension CATransaction { + static func performWithoutAnimations(_ block: () -> Void) { + CATransaction.begin() + CATransaction.setDisableActions(true) + block() + CATransaction.commit() } } diff --git a/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAppearanceStyler.swift b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAppearanceStyler.swift index 78c56970d..5cfccd965 100644 --- a/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAppearanceStyler.swift +++ b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAppearanceStyler.swift @@ -56,7 +56,7 @@ public extension OCKAppearanceStyler { var cornerRadius1: CGFloat { 15 } var cornerRadius2: CGFloat { 12 } - var borderWidth1: CGFloat { 3 } + var borderWidth1: CGFloat { 2 } var borderWidth2: CGFloat { 1 } } diff --git a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift index db9f0e0a4..708dc8e6c 100644 --- a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift +++ b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift @@ -73,6 +73,7 @@ open class OCKLabeledCheckmarkButton: OCKAnimatedButton { private func setup() { addSubviews() constrainSubviews() + updateTintedViews() } private func addSubviews() { @@ -85,16 +86,19 @@ open class OCKLabeledCheckmarkButton: OCKAnimatedButton { NSLayoutConstraint.activate(contentStackView.constraints(equalTo: self)) } + private func updateTintedViews() { + label.textColor = tintColor + } + override open func styleDidChange() { super.styleDidChange() let style = self.style() - label.textColor = style.color.secondaryLabel contentStackView.spacing = style.dimension.directionalInsets2.bottom } override open func tintColorDidChange() { super.tintColorDidChange() - label.textColor = tintColor + updateTintedViews() } override open func setSelected(_ isSelected: Bool, animated: Bool) { From 7576b3d45f2339618bb6f6ed1597e00639a3dc14 Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Fri, 20 Mar 2020 21:16:07 -0700 Subject: [PATCH 22/33] Added an atomic addOrUpdateTasks method to OCKStore. (#393) --- .../CoreData/OCKCoreDataTaskStore.swift | 43 ++++++++++++++++++- .../OCKStoreProtocol+Synchronous.swift | 14 ++++++ .../OCKStore/TestStore+Tasks.swift | 19 ++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift index dcaeb30c0..42886a223 100644 --- a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift +++ b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift @@ -83,7 +83,7 @@ extension OCKCoreDataTaskStoreProtocol { context.perform { do { try OCKCDTask.validateNewIDs(tasks.map { $0.id }, in: self.context) - let persistableTasks = tasks.map { self.createTask(from: $0) } + let persistableTasks = tasks.map(self.createTask) try self.context.save() let addedTasks = persistableTasks.map(self.makeTask) callbackQueue.async { @@ -121,6 +121,47 @@ extension OCKCoreDataTaskStoreProtocol { } } } + + /// Adds, updates, and deletes tasks in a single atomic transaction + /// - Parameter tasks: Tasks that should be either added or updated, depending on whether or not they already exist. + /// - Parameter deleteTasks: Tasks that should be deleted from the store. + /// - Parameter callbackQueue: The queue that the callback will be performed on + /// - Parameter completion: A result closure that takes arrays of added, updated, and deleted tasks. + public func addUpdateOrDeleteTasks(addOrUpdate tasks: [Task], delete deleteTasks: [Task], + callbackQueue: DispatchQueue = .main, + completion: ((Result<([Task], [Task], [Task]), OCKStoreError>) -> Void)? = nil) { + context.perform { + do { + let existingTaskIDs = OCKCDTask.fetchHeads(ids: tasks.map { $0.id }, in: self.context).map { $0.id } + let addTasks = tasks.filter { !existingTaskIDs.contains($0.id) } + let updateTasks = tasks.filter { existingTaskIDs.contains($0.id) } + try self.confirmUpdateWillNotCauseDataLoss(tasks: updateTasks) + + let inserted = addTasks.map(self.createTask) + let updated = try self.performVersionedUpdate(values: updateTasks, addNewVersion: self.createTask) + let deleted: [OCKCDTask] = try self.performDeletion(values: deleteTasks) + + try self.context.save() + + let addedTasks = inserted.map(self.makeTask) + let updatedTasks = updated.map(self.makeTask) + let deletedTasks = deleted.map(self.makeTask) + + callbackQueue.async { + self.taskDelegate?.taskStore(self, didAddTasks: addedTasks) + self.taskDelegate?.taskStore(self, didUpdateTasks: updatedTasks) + self.taskDelegate?.taskStore(self, didDeleteTasks: deleteTasks) + completion?(.success((addedTasks, updateTasks, deletedTasks))) + } + + } catch { + self.context.rollback() + callbackQueue.async { + completion?(.failure(.updateFailed(reason: "\(error.localizedDescription)"))) + } + } + } + } public func deleteTasks(_ tasks: [Task], callbackQueue: DispatchQueue = .main, completion: ((Result<[Task], OCKStoreError>) -> Void)? = nil) { diff --git a/CareKitStore/CareKitStore/Protocols/OCKStoreProtocol+Synchronous.swift b/CareKitStore/CareKitStore/Protocols/OCKStoreProtocol+Synchronous.swift index a8860c138..1468f0182 100644 --- a/CareKitStore/CareKitStore/Protocols/OCKStoreProtocol+Synchronous.swift +++ b/CareKitStore/CareKitStore/Protocols/OCKStoreProtocol+Synchronous.swift @@ -294,7 +294,21 @@ extension OCKReadOnlyEventStore { } extension OCKAnyTaskStore { + @discardableResult func addAnyTaskAndWait(_ task: OCKAnyTask) throws -> OCKAnyTask { try performSynchronously { addAnyTask(task, callbackQueue: backgroundQueue, completion: $0) } } } + +extension OCKCoreDataTaskStoreProtocol { + @discardableResult + func addUpdateOrDeleteTasksAndWait(addOrUpdate tasksToAddOrUpdate: [Task], + delete tasksToDelete: [Task]) throws -> ([Task], [Task], [Task]) { + try performSynchronously { + addUpdateOrDeleteTasks( + addOrUpdate: tasksToAddOrUpdate, + delete: tasksToDelete, + callbackQueue: backgroundQueue, completion: $0) + } + } +} diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift index 75f3f1f3f..a4ec6baeb 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift @@ -92,6 +92,24 @@ class TestStoreTasks: XCTestCase { guard let fetchedElement = task.schedule.elements.first else { XCTFail("Bad schedule"); return } XCTAssertTrue(fetchedElement.duration == .allDay) } + + func testAddUpdateOrDelete() throws { + let schedule = OCKSchedule.mealTimesEachDay(start: Date(), end: nil) + let taskC = OCKTask(id: "C", title: "OriginalC", carePlanID: nil, schedule: schedule) + try store.addTaskAndWait(OCKTask(id: "A", title: "OriginalA", carePlanID: nil, schedule: schedule)) + try store.addTaskAndWait(taskC) + + let taskA = OCKTask(id: "A", title: "UpdatedA", carePlanID: nil, schedule: schedule) + let taskB = OCKTask(id: "B", title: "OriginalB", carePlanID: nil, schedule: schedule) + try store.addUpdateOrDeleteTasksAndWait(addOrUpdate: [taskA, taskB], delete: [taskC]) + + let tasks = try store.fetchTasksAndWait(query: OCKTaskQuery()) + let titles = tasks.map { $0.title } + XCTAssert(tasks.count == 3) + XCTAssert(titles.contains("OriginalA")) + XCTAssert(titles.contains("UpdatedA")) + XCTAssert(titles.contains("OriginalB")) + } // MARK: Querying @@ -227,6 +245,7 @@ class TestStoreTasks: XCTestCase { let fetched = try store.fetchTasksAndWait(query: query).first XCTAssert(fetched == task) } + // MARK: Versioning func testUpdateTaskCreateNewVersion() throws { From b70e4efabefe36c168fd4e95ec20fafd26655c1b Mon Sep 17 00:00:00 2001 From: gavirawson-apple <51756298+gavirawson-apple@users.noreply.github.com> Date: Sun, 22 Mar 2020 16:17:30 -0700 Subject: [PATCH 23/33] Fix tint color propagation (#395) --- CareKitUI/CareKitUI.xcodeproj/project.pbxproj | 4 ++ .../Common/Controls/OCKCheckmarkButton.swift | 15 ++----- .../Extensions/CATransaction+Extension.swift | 43 +++++++++++++++++++ .../Buttons/OCKCompletionRingButton.swift | 8 +++- .../Charts/OCKCartesianGraphView.swift | 8 +++- .../Components/Charts/OCKGraphAxisView.swift | 10 ++++- .../Contact/Buttons/OCKAddressButton.swift | 7 ++- .../Contact/Buttons/OCKContactButton.swift | 10 ++++- .../Buttons/OCKLabeledCheckmarkButton.swift | 6 +-- .../Task/Buttons/OCKLogItemButton.swift | 7 ++- 10 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 CareKitUI/CareKitUI/Common/Extensions/CATransaction+Extension.swift diff --git a/CareKitUI/CareKitUI.xcodeproj/project.pbxproj b/CareKitUI/CareKitUI.xcodeproj/project.pbxproj index f18c6300e..c936905c4 100644 --- a/CareKitUI/CareKitUI.xcodeproj/project.pbxproj +++ b/CareKitUI/CareKitUI.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ 518F9DBB22961BF6009CAA48 /* OCKLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D8222961BF5009CAA48 /* OCKLabel.swift */; }; 518F9DBC22961BF6009CAA48 /* OCKCardable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D8322961BF5009CAA48 /* OCKCardable.swift */; }; 518F9DC022961BF6009CAA48 /* OCKStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D8722961BF5009CAA48 /* OCKStackView.swift */; }; + 519288732427E2DC00D0AF43 /* CATransaction+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519288722427E2DC00D0AF43 /* CATransaction+Extension.swift */; }; 5196B54322D5872C00800706 /* OCKTaskDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5196B54222D5872C00800706 /* OCKTaskDisplayable.swift */; }; 51AB06C022FE539400B73FC2 /* OCKStylable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AB06BF22FE539400B73FC2 /* OCKStylable.swift */; }; 51AB06C222FE53E300B73FC2 /* TestStylableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AB06C122FE53E300B73FC2 /* TestStylableView.swift */; }; @@ -153,6 +154,7 @@ 518F9D8222961BF5009CAA48 /* OCKLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLabel.swift; sourceTree = ""; }; 518F9D8322961BF5009CAA48 /* OCKCardable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCardable.swift; sourceTree = ""; }; 518F9D8722961BF5009CAA48 /* OCKStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKStackView.swift; sourceTree = ""; }; + 519288722427E2DC00D0AF43 /* CATransaction+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATransaction+Extension.swift"; sourceTree = ""; }; 5196B54222D5872C00800706 /* OCKTaskDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKTaskDisplayable.swift; sourceTree = ""; }; 51AB06BF22FE539400B73FC2 /* OCKStylable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKStylable.swift; sourceTree = ""; }; 51AB06C122FE53E300B73FC2 /* TestStylableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStylableView.swift; sourceTree = ""; }; @@ -318,6 +320,7 @@ 518F9D6F22961BF5009CAA48 /* UIFont+Extensions.swift */, 5103C55122F37B44007A7403 /* Number+Extensions.swift */, 03089D7E231ED7AF0054EA23 /* Calendar+Extensions.swift */, + 519288722427E2DC00D0AF43 /* CATransaction+Extension.swift */, ); path = Extensions; sourceTree = ""; @@ -676,6 +679,7 @@ 518F9DA022961BF6009CAA48 /* OCKDataSeries.swift in Sources */, 512EE90022975F850052F37C /* OCKDetailView.swift in Sources */, 51AB06C022FE539400B73FC2 /* OCKStylable.swift in Sources */, + 519288732427E2DC00D0AF43 /* CATransaction+Extension.swift in Sources */, 51656029234AB47500F2A21F /* OCKCheckmarkButton.swift in Sources */, 518F9DA422961BF6009CAA48 /* OCKLinePlotView.swift in Sources */, 518F9DA322961BF6009CAA48 /* OCKGridView.swift in Sources */, diff --git a/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift b/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift index fff329f0d..ff7964966 100644 --- a/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift +++ b/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift @@ -89,7 +89,7 @@ open class OCKCheckmarkButton: OCKAnimatedButton { private func styleSubviews() { clipsToBounds = true - updateLayerTintColors() + applyTintColor() setStyleForSelectedState(false) } @@ -132,7 +132,7 @@ open class OCKCheckmarkButton: OCKAnimatedButton { override open func tintColorDidChange() { super.tintColorDidChange() - updateLayerTintColors() + applyTintColor() } override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -155,17 +155,8 @@ open class OCKCheckmarkButton: OCKAnimatedButton { imageView.tintColor = isSelected ? style().color.customBackground : .clear } - private func updateLayerTintColors() { + private func applyTintColor() { fillLayer.fillColor = tintColor.cgColor borderLayer.fillColor = tintColor.cgColor } } - -private extension CATransaction { - static func performWithoutAnimations(_ block: () -> Void) { - CATransaction.begin() - CATransaction.setDisableActions(true) - block() - CATransaction.commit() - } -} diff --git a/CareKitUI/CareKitUI/Common/Extensions/CATransaction+Extension.swift b/CareKitUI/CareKitUI/Common/Extensions/CATransaction+Extension.swift new file mode 100644 index 000000000..1b9c4248c --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Extensions/CATransaction+Extension.swift @@ -0,0 +1,43 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation +import UIKit + +extension CATransaction { + + /// Modify a property on a CALayer without the implicit animation. + static func performWithoutAnimations(_ block: () -> Void) { + CATransaction.begin() + CATransaction.setDisableActions(true) + block() + CATransaction.commit() + } +} diff --git a/CareKitUI/CareKitUI/Components/Calendar/Ring/Buttons/OCKCompletionRingButton.swift b/CareKitUI/CareKitUI/Components/Calendar/Ring/Buttons/OCKCompletionRingButton.swift index b54462e4d..05701f9ef 100644 --- a/CareKitUI/CareKitUI/Components/Calendar/Ring/Buttons/OCKCompletionRingButton.swift +++ b/CareKitUI/CareKitUI/Components/Calendar/Ring/Buttons/OCKCompletionRingButton.swift @@ -88,8 +88,9 @@ open class OCKCompletionRingButton: OCKAnimatedButton { /// Called when the tint color of the view changes. override open func tintColorDidChange() { + super.tintColorDidChange() updateRingColors() - ring.strokeColor = tintColor + applyTintColor() } /// Changes the display state of the button @@ -110,6 +111,7 @@ open class OCKCompletionRingButton: OCKAnimatedButton { private func setup() { addSubviews() + applyTintColor() } private func updateRingColors() { @@ -126,4 +128,8 @@ open class OCKCompletionRingButton: OCKAnimatedButton { addSubview(contentStackView) [label, ring].forEach { contentStackView.addArrangedSubview($0) } } + + private func applyTintColor() { + ring.strokeColor = tintColor + } } diff --git a/CareKitUI/CareKitUI/Components/Charts/OCKCartesianGraphView.swift b/CareKitUI/CareKitUI/Components/Charts/OCKCartesianGraphView.swift index 4520782f5..f220613fc 100644 --- a/CareKitUI/CareKitUI/Components/Charts/OCKCartesianGraphView.swift +++ b/CareKitUI/CareKitUI/Components/Charts/OCKCartesianGraphView.swift @@ -152,7 +152,7 @@ open class OCKCartesianGraphView: OCKView, OCKMultiPlotable { override open func tintColorDidChange() { super.tintColorDidChange() - axisView.tintColor = tintColor + applyTintColor() } private func updateScaling(for dataSeries: [OCKDataSeries]) { @@ -191,5 +191,11 @@ open class OCKCartesianGraphView: OCKView, OCKMultiPlotable { legend.centerXAnchor.constraint(equalTo: centerXAnchor), legend.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) ]) + + applyTintColor() + } + + private func applyTintColor() { + axisView.tintColor = tintColor } } diff --git a/CareKitUI/CareKitUI/Components/Charts/OCKGraphAxisView.swift b/CareKitUI/CareKitUI/Components/Charts/OCKGraphAxisView.swift index a6cfd571d..97025556a 100644 --- a/CareKitUI/CareKitUI/Components/Charts/OCKGraphAxisView.swift +++ b/CareKitUI/CareKitUI/Components/Charts/OCKGraphAxisView.swift @@ -88,7 +88,7 @@ private class OCKCircleLabelView: OCKView { override func tintColorDidChange() { super.tintColorDidChange() - circleLayer.fillColor = isSelected ? tintColor.cgColor : nil + applyTintColor() } var isSelected: Bool = false { @@ -113,6 +113,7 @@ private class OCKCircleLabelView: OCKView { super.setup() addSubview(label) updateLabelColor() + applyTintColor() label.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -142,4 +143,11 @@ private class OCKCircleLabelView: OCKView { super.styleDidChange() updateLabelColor() } + + private func applyTintColor() { + // Note: If animation is not disabled, the axis will fly in from the top of the view. + CATransaction.performWithoutAnimations { + circleLayer.fillColor = isSelected ? tintColor.cgColor : nil + } + } } diff --git a/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKAddressButton.swift b/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKAddressButton.swift index 3687a229a..f1ef41f30 100644 --- a/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKAddressButton.swift +++ b/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKAddressButton.swift @@ -99,6 +99,11 @@ open class OCKAddressButton: OCKAnimatedButton { private func styleSubviews() { accessibilityLabel = titleLabel.text accessibilityHint = loc("DOUBLE_TAP_MAP") + applyTintColor() + } + + private func applyTintColor() { + titleLabel.textColor = tintColor } private func addSubviews() { @@ -126,7 +131,7 @@ open class OCKAddressButton: OCKAnimatedButton { override open func tintColorDidChange() { super.tintColorDidChange() - titleLabel.textColor = tintColor + applyTintColor() } override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { diff --git a/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKContactButton.swift b/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKContactButton.swift index 46b2989c5..205cbc508 100644 --- a/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKContactButton.swift +++ b/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKContactButton.swift @@ -117,6 +117,8 @@ open class OCKContactButton: OCKAnimatedButton { case .email: accessibilityLabel = loc("EMAIL") case .message: accessibilityLabel = loc("MESSAGE") } + + applyTintColor() } private func addSubviews() { @@ -128,12 +130,16 @@ open class OCKContactButton: OCKAnimatedButton { NSLayoutConstraint.activate(contentStackView.constraints(equalTo: layoutMarginsGuide)) } - override open func tintColorDidChange() { - super.tintColorDidChange() + private func applyTintColor() { imageView.tintColor = tintColor label.textColor = tintColor } + override open func tintColorDidChange() { + super.tintColorDidChange() + applyTintColor() + } + override open func styleDidChange() { super.styleDidChange() let style = self.style() diff --git a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift index 708dc8e6c..3279763ed 100644 --- a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift +++ b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift @@ -73,7 +73,7 @@ open class OCKLabeledCheckmarkButton: OCKAnimatedButton { private func setup() { addSubviews() constrainSubviews() - updateTintedViews() + applyTintColor() } private func addSubviews() { @@ -86,7 +86,7 @@ open class OCKLabeledCheckmarkButton: OCKAnimatedButton { NSLayoutConstraint.activate(contentStackView.constraints(equalTo: self)) } - private func updateTintedViews() { + private func applyTintColor() { label.textColor = tintColor } @@ -98,7 +98,7 @@ open class OCKLabeledCheckmarkButton: OCKAnimatedButton { override open func tintColorDidChange() { super.tintColorDidChange() - updateTintedViews() + applyTintColor() } override open func setSelected(_ isSelected: Bool, animated: Bool) { diff --git a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLogItemButton.swift b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLogItemButton.swift index d2e16b354..a22a1fc4c 100644 --- a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLogItemButton.swift +++ b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLogItemButton.swift @@ -88,6 +88,7 @@ open class OCKLogItemButton: OCKAnimatedButton { private func styleSubviews() { contentStackView.setCustomSpacing(Constants.spacing, after: imageView) + applyTintColor() } private func addSubviews() { @@ -105,6 +106,10 @@ open class OCKLogItemButton: OCKAnimatedButton { ) } + private func applyTintColor() { + detailLabel.textColor = tintColor + } + override open func styleDidChange() { super.styleDidChange() let style = self.style() @@ -115,6 +120,6 @@ open class OCKLogItemButton: OCKAnimatedButton { override open func tintColorDidChange() { super.tintColorDidChange() - detailLabel.textColor = tintColor + applyTintColor() } } From 87d2e4c10d44240c52e9a7732753781d9d680ef5 Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Mon, 23 Mar 2020 12:09:23 -0700 Subject: [PATCH 24/33] Include addUpdateOrDelete in TaskStoreProtocol (#396) --- .../Protocols/CoreData/OCKCoreDataTaskStore.swift | 5 ----- .../CareKitStore/Protocols/Tasks/OCKTaskStore.swift | 13 ++++++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift index 42886a223..9a35364aa 100644 --- a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift +++ b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift @@ -122,11 +122,6 @@ extension OCKCoreDataTaskStoreProtocol { } } - /// Adds, updates, and deletes tasks in a single atomic transaction - /// - Parameter tasks: Tasks that should be either added or updated, depending on whether or not they already exist. - /// - Parameter deleteTasks: Tasks that should be deleted from the store. - /// - Parameter callbackQueue: The queue that the callback will be performed on - /// - Parameter completion: A result closure that takes arrays of added, updated, and deleted tasks. public func addUpdateOrDeleteTasks(addOrUpdate tasks: [Task], delete deleteTasks: [Task], callbackQueue: DispatchQueue = .main, completion: ((Result<([Task], [Task], [Task]), OCKStoreError>) -> Void)? = nil) { diff --git a/CareKitStore/CareKitStore/Protocols/Tasks/OCKTaskStore.swift b/CareKitStore/CareKitStore/Protocols/Tasks/OCKTaskStore.swift index 202c73819..428d182c2 100644 --- a/CareKitStore/CareKitStore/Protocols/Tasks/OCKTaskStore.swift +++ b/CareKitStore/CareKitStore/Protocols/Tasks/OCKTaskStore.swift @@ -80,7 +80,18 @@ public protocol OCKTaskStore: OCKReadableTaskStore, OCKAnyTaskStore { /// - callbackQueue: The queue that the completion closure should be called on. In most cases this should be the main queue. /// - completion: A callback that will fire on the provided callback queue. func deleteTasks(_ tasks: [Task], callbackQueue: DispatchQueue, completion: OCKResultClosure<[Task]>?) - + + /// Adds, updates, and deletes tasks in a single atomic transaction + /// - Parameter tasks: Tasks that should be either added or updated, depending on whether or not they already exist. + /// - Parameter deleteTasks: Tasks that should be deleted from the store. + /// - Parameter callbackQueue: The queue that the callback will be performed on + /// - Parameter completion: A result closure that takes arrays of added, updated, and deleted tasks. + func addUpdateOrDeleteTasks( + addOrUpdate tasks: [Task], + delete deleteTasks: [Task], + callbackQueue: DispatchQueue, + completion: ((Result<([Task], [Task], [Task]), OCKStoreError>) -> Void)?) + // MARK: Implementation Provided /// `addTask` asynchronously adds a task to the store. From b806cdfa4c1be86b6ade40119d013bc0098bca4e Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Mon, 30 Mar 2020 11:38:47 -0400 Subject: [PATCH 25/33] fix: SwiftUI logic typo (#399) It appears that this is actually inverted for what it should be. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f9ba8294..33e80ca8c 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ struct ContentView: View { let isComplete = self.event?.outcome != nil self.controller.setEvent(atIndexPath: IndexPath(row: 0, section: 0), isComplete: !isComplete, completion: nil) }) { - self.event?.outcome != nil ? Text("Mark as Completed") : Text("Completed") + self.event?.outcome == nil ? Text("Mark as Completed") : Text("Completed") } } } From f6a048d97f95c920876e3f54259531e5e2031bbe Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Fri, 3 Apr 2020 09:53:22 -0700 Subject: [PATCH 26/33] Fix issue in which the address field on OCKContact was not serialized (#402) --- .../Structs/OCKPostalAddress.swift | 47 ++++++++++++++++++- .../Structs/TestContact.swift | 31 ++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/CareKitStore/CareKitStore/Structs/OCKPostalAddress.swift b/CareKitStore/CareKitStore/Structs/OCKPostalAddress.swift index 3a44a83fc..ded32bccc 100644 --- a/CareKitStore/CareKitStore/Structs/OCKPostalAddress.swift +++ b/CareKitStore/CareKitStore/Structs/OCKPostalAddress.swift @@ -31,4 +31,49 @@ import Contacts /// A `Codable` subclass of `CNMutablePostalAddress`. @objc // We subclass for sole purpose of adding conformance to Codable. -public class OCKPostalAddress: CNMutablePostalAddress, Codable {} +public class OCKPostalAddress: CNMutablePostalAddress, Codable { + + public required init(from decoder: Decoder) throws { + super.init() + let container = try decoder.container(keyedBy: Keys.self) + self.street = try container.decode(String.self, forKey: .street) + self.subLocality = try container.decode(String.self, forKey: .subLocality) + self.city = try container.decode(String.self, forKey: .city) + self.subAdministrativeArea = try container.decode(String.self, forKey: .subAdministrativeArea) + self.state = try container.decode(String.self, forKey: .state) + self.postalCode = try container.decode(String.self, forKey: .postalCode) + self.country = try container.decode(String.self, forKey: .country) + self.isoCountryCode = try container.decode(String.self, forKey: .isoCountryCode) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: Keys.self) + try container.encode(street, forKey: .street) + try container.encode(subLocality, forKey: .subLocality) + try container.encode(city, forKey: .city) + try container.encode(subAdministrativeArea, forKey: .subAdministrativeArea) + try container.encode(state, forKey: .state) + try container.encode(postalCode, forKey: .postalCode) + try container.encode(country, forKey: .country) + try container.encode(isoCountryCode, forKey: .isoCountryCode) + } + + public override init() { + super.init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private enum Keys: CodingKey, CaseIterable { + case street + case subLocality + case city + case subAdministrativeArea + case state + case postalCode + case country + case isoCountryCode + } +} diff --git a/CareKitStore/CareKitStoreTests/Structs/TestContact.swift b/CareKitStore/CareKitStoreTests/Structs/TestContact.swift index 61761ed8b..5ca56095e 100644 --- a/CareKitStore/CareKitStoreTests/Structs/TestContact.swift +++ b/CareKitStore/CareKitStoreTests/Structs/TestContact.swift @@ -30,6 +30,7 @@ @testable import CareKitStore import XCTest +import Contacts class TestContact: XCTestCase { @@ -45,4 +46,34 @@ class TestContact: XCTestCase { let contact = OCKContact(id: "B", givenName: "Mary", familyName: "Frost", carePlanID: plan.localDatabaseID) XCTAssertTrue(contact.belongs(to: plan)) } + + func testContactSerialzation() throws { + var contact = OCKContact(id: "jane", givenName: "Jane", familyName: "Daniels", carePlanID: nil) + contact.asset = "JaneDaniels" + contact.title = "Family Practice Doctor" + contact.role = "Dr. Daniels is a family practice doctor with 8 years of experience." + contact.emailAddresses = [OCKLabeledValue(label: CNLabelEmailiCloud, value: "janedaniels@icloud.com")] + contact.phoneNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] + contact.messagingNumbers = [OCKLabeledValue(label: CNLabelWork, value: "(324) 555-7415")] + + contact.address = { + let address = OCKPostalAddress() + address.street = "2598 Reposa Way" + address.city = "San Francisco" + address.state = "CA" + address.postalCode = "94127" + return address + }() + + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + XCTAssertNoThrow(try encoder.encode(contact)) + + let data = try encoder.encode(contact) + let json = String(data: data, encoding: .utf8)! + XCTAssertNoThrow(try JSONDecoder().decode(OCKContact.self, from: json.data(using: .utf8)!)) + + let deserialized = try JSONDecoder().decode(OCKContact.self, from: json.data(using: .utf8)!) + XCTAssertEqual(deserialized, contact) + } } From 9d0695b7eb374aab1bccfb6dae42b9f6f9844b3a Mon Sep 17 00:00:00 2001 From: gavirawson-apple <51756298+gavirawson-apple@users.noreply.github.com> Date: Mon, 6 Apr 2020 14:00:15 -0700 Subject: [PATCH 27/33] Fix memory leak (#403) Fixes an issue that prevented the deallocation of some views. --- .../CareKitUI/Components/Charts/OCKCartesianChartView.swift | 3 +-- .../CareKitUI/Components/Contact/OCKDetailedContactView.swift | 3 +-- .../CareKitUI/Components/Contact/OCKSimpleContactView.swift | 3 +-- CareKitUI/CareKitUI/Components/Task/OCKChecklistTaskView.swift | 3 +-- CareKitUI/CareKitUI/Components/Task/OCKGridTaskView.swift | 2 +- .../CareKitUI/Components/Task/OCKInstructionsTaskView.swift | 3 +-- CareKitUI/CareKitUI/Components/Task/OCKLogTaskView.swift | 3 +-- CareKitUI/CareKitUI/Components/Task/OCKSimpleTaskView.swift | 3 +-- 8 files changed, 8 insertions(+), 15 deletions(-) diff --git a/CareKitUI/CareKitUI/Components/Charts/OCKCartesianChartView.swift b/CareKitUI/CareKitUI/Components/Charts/OCKCartesianChartView.swift index c547489d0..59afcddd7 100644 --- a/CareKitUI/CareKitUI/Components/Charts/OCKCartesianChartView.swift +++ b/CareKitUI/CareKitUI/Components/Charts/OCKCartesianChartView.swift @@ -37,8 +37,6 @@ open class OCKCartesianChartView: OCKView, OCKChartDisplayable { private let contentView = OCKView() - private lazy var cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) - private let headerContainerView = UIView() /// Handles events related to an `OCKChartDisplayable` object. @@ -112,6 +110,7 @@ open class OCKCartesianChartView: OCKView, OCKChartDisplayable { override open func styleDidChange() { super.styleDidChange() let cachedStyle = style() + let cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) cardBuilder.enableCardStyling(true, style: cachedStyle) contentStackView.spacing = cachedStyle.dimension.directionalInsets1.top directionalLayoutMargins = cachedStyle.dimension.directionalInsets1 diff --git a/CareKitUI/CareKitUI/Components/Contact/OCKDetailedContactView.swift b/CareKitUI/CareKitUI/Components/Contact/OCKDetailedContactView.swift index 07f457f8b..20d67859e 100644 --- a/CareKitUI/CareKitUI/Components/Contact/OCKDetailedContactView.swift +++ b/CareKitUI/CareKitUI/Components/Contact/OCKDetailedContactView.swift @@ -113,8 +113,6 @@ open class OCKDetailedContactView: OCKView, OCKContactDisplayable { return buttons.compactMap { $0 as? OCKContactButton } } - private lazy var cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) - /// Stack view that holds phone, message, and email contact action buttons. private lazy var contactStackView: OCKStackView = { let stackView = OCKStackView() @@ -183,6 +181,7 @@ open class OCKDetailedContactView: OCKView, OCKContactDisplayable { override open func styleDidChange() { super.styleDidChange() let cachedStyle = style() + let cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) cardBuilder.enableCardStyling(true, style: cachedStyle) instructionsLabel.textColor = cachedStyle.color.label directionalLayoutMargins = cachedStyle.dimension.directionalInsets1 diff --git a/CareKitUI/CareKitUI/Components/Contact/OCKSimpleContactView.swift b/CareKitUI/CareKitUI/Components/Contact/OCKSimpleContactView.swift index a0cedfbd1..70ca3be97 100644 --- a/CareKitUI/CareKitUI/Components/Contact/OCKSimpleContactView.swift +++ b/CareKitUI/CareKitUI/Components/Contact/OCKSimpleContactView.swift @@ -65,8 +65,6 @@ open class OCKSimpleContactView: OCKView, OCKContactDisplayable { return view }() - private lazy var cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) - // Button that displays the highlighted state for the view. private lazy var backgroundButton = OCKAnimatedButton(contentView: contentStackView, highlightOptions: [.defaultOverlay, .defaultDelayOnSelect], handlesSelection: false) @@ -112,6 +110,7 @@ open class OCKSimpleContactView: OCKView, OCKContactDisplayable { override open func styleDidChange() { super.styleDidChange() let style = self.style() + let cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) cardBuilder.enableCardStyling(true, style: style) directionalLayoutMargins = style.dimension.directionalInsets1 contentStackView.spacing = style.dimension.directionalInsets1.top diff --git a/CareKitUI/CareKitUI/Components/Task/OCKChecklistTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKChecklistTaskView.swift index 5f58aea7c..64caf8c18 100644 --- a/CareKitUI/CareKitUI/Components/Task/OCKChecklistTaskView.swift +++ b/CareKitUI/CareKitUI/Components/Task/OCKChecklistTaskView.swift @@ -78,8 +78,6 @@ open class OCKChecklistTaskView: OCKView, OCKTaskDisplayable { return stackView }() - private lazy var cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) - private let headerStackView = OCKStackView.vertical() private lazy var headerButton = OCKAnimatedButton(contentView: headerView, highlightOptions: [.defaultDelayOnSelect, .defaultOverlay], @@ -232,6 +230,7 @@ open class OCKChecklistTaskView: OCKView, OCKTaskDisplayable { override open func styleDidChange() { super.styleDidChange() let style = self.style() + let cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) cardBuilder.enableCardStyling(true, style: style) instructionsLabel.textColor = style.color.secondaryLabel directionalLayoutMargins = style.dimension.directionalInsets1 diff --git a/CareKitUI/CareKitUI/Components/Task/OCKGridTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKGridTaskView.swift index c3425af68..c1a920dbe 100644 --- a/CareKitUI/CareKitUI/Components/Task/OCKGridTaskView.swift +++ b/CareKitUI/CareKitUI/Components/Task/OCKGridTaskView.swift @@ -107,7 +107,6 @@ open class OCKGridTaskView: OCKView, OCKTaskDisplayable, UICollectionViewDelegat return view }() - private lazy var cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) private let headerStackView = OCKStackView.vertical() private lazy var headerButton = OCKAnimatedButton(contentView: headerView, highlightOptions: [.defaultDelayOnSelect, .defaultOverlay], @@ -228,6 +227,7 @@ open class OCKGridTaskView: OCKView, OCKTaskDisplayable, UICollectionViewDelegat override open func styleDidChange() { super.styleDidChange() let style = self.style() + let cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) cardBuilder.enableCardStyling(true, style: style) instructionsLabel.textColor = style.color.secondaryLabel contentStackView.spacing = style.dimension.directionalInsets1.top diff --git a/CareKitUI/CareKitUI/Components/Task/OCKInstructionsTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKInstructionsTaskView.swift index 372906b77..ee6a1fe09 100644 --- a/CareKitUI/CareKitUI/Components/Task/OCKInstructionsTaskView.swift +++ b/CareKitUI/CareKitUI/Components/Task/OCKInstructionsTaskView.swift @@ -62,8 +62,6 @@ open class OCKInstructionsTaskView: OCKView, OCKTaskDisplayable { return view }() - private lazy var cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) - private lazy var headerButton = OCKAnimatedButton(contentView: headerView, highlightOptions: [.defaultDelayOnSelect, .defaultOverlay], handlesSelection: false) @@ -144,6 +142,7 @@ open class OCKInstructionsTaskView: OCKView, OCKTaskDisplayable { override open func styleDidChange() { super.styleDidChange() let style = self.style() + let cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) cardBuilder.enableCardStyling(true, style: style) instructionsLabel.textColor = style.color.label contentStackView.spacing = style.dimension.directionalInsets1.top diff --git a/CareKitUI/CareKitUI/Components/Task/OCKLogTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKLogTaskView.swift index 9cdd181c4..c6934d12d 100644 --- a/CareKitUI/CareKitUI/Components/Task/OCKLogTaskView.swift +++ b/CareKitUI/CareKitUI/Components/Task/OCKLogTaskView.swift @@ -41,8 +41,6 @@ open class OCKLogTaskView: OCKView, OCKTaskDisplayable { return view }() - private lazy var cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) - private lazy var headerButton = OCKAnimatedButton(contentView: headerView, highlightOptions: [.defaultDelayOnSelect, .defaultOverlay], handlesSelection: false) @@ -131,6 +129,7 @@ open class OCKLogTaskView: OCKView, OCKTaskDisplayable { override open func styleDidChange() { super.styleDidChange() let style = self.style() + let cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) cardBuilder.enableCardStyling(true, style: style) contentStackView.spacing = style.dimension.directionalInsets1.top directionalLayoutMargins = style.dimension.directionalInsets1 diff --git a/CareKitUI/CareKitUI/Components/Task/OCKSimpleTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKSimpleTaskView.swift index e987b464f..95d97add1 100644 --- a/CareKitUI/CareKitUI/Components/Task/OCKSimpleTaskView.swift +++ b/CareKitUI/CareKitUI/Components/Task/OCKSimpleTaskView.swift @@ -56,8 +56,6 @@ open class OCKSimpleTaskView: OCKView, OCKTaskDisplayable { // Button that displays the highlighted state for the view. private lazy var backgroundButton = OCKAnimatedButton(contentView: horizontalContentStackView, handlesSelection: false) - private lazy var cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) - private let horizontalContentStackView: OCKStackView = { let stack = OCKStackView.horizontal() stack.alignment = .center @@ -117,6 +115,7 @@ open class OCKSimpleTaskView: OCKView, OCKTaskDisplayable { override open func styleDidChange() { super.styleDidChange() let style = self.style() + let cardBuilder = OCKCardBuilder(cardView: self, contentView: contentView) cardBuilder.enableCardStyling(true, style: style) backgroundButton.directionalLayoutMargins = style.dimension.directionalInsets1 } From e4c1471ea5e2c8fc69ab9418c082778e00b3a622 Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Fri, 10 Apr 2020 15:21:42 -0700 Subject: [PATCH 28/33] Fixes a bug in which the start date for weekly schedules was incorrect. (#406) --- .../CareKitStore/Structs/OCKSchedule.swift | 4 ++-- .../CareKitStoreTests/Structs/TestSchedule.swift | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CareKitStore/CareKitStore/Structs/OCKSchedule.swift b/CareKitStore/CareKitStore/Structs/OCKSchedule.swift index 37e3e221d..dd54775c6 100644 --- a/CareKitStore/CareKitStore/Structs/OCKSchedule.swift +++ b/CareKitStore/CareKitStore/Structs/OCKSchedule.swift @@ -125,8 +125,8 @@ public struct OCKSchedule: Codable, Equatable { public static func weeklyAtTime(weekday: Int, hours: Int, minutes: Int, start: Date, end: Date?, targetValues: [OCKOutcomeValue], text: String?, duration: OCKScheduleElement.Duration = .hours(1)) -> OCKSchedule { let interval = DateComponents(weekOfYear: 1) - var startTime = Calendar.current.date(bySettingHour: hours, minute: minutes, second: 0, of: start)! - startTime = Calendar.current.date(bySetting: .weekday, value: weekday, of: startTime)! + var startTime = Calendar.current.date(bySetting: .weekday, value: weekday, of: start)! + startTime = Calendar.current.date(bySettingHour: hours, minute: minutes, second: 0, of: startTime)! let element = OCKScheduleElement(start: startTime, end: end, interval: interval, text: text, targetValues: targetValues, duration: duration) return OCKSchedule(composing: [element]) diff --git a/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift b/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift index eab384da6..631a1c97f 100644 --- a/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift +++ b/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift @@ -75,6 +75,20 @@ class TestSchedule: XCTestCase { } } + func testWeeklyScheduleStartDate() { + let firstDay = Date() + + let weekly = OCKSchedule.weeklyAtTime( + weekday: 1, hours: 5, minutes: 30, + start: firstDay, end: nil, targetValues: [], text: nil) + + let hours = Calendar.current.component(.hour, from: weekly.startDate()) + let minutes = Calendar.current.component(.minute, from: weekly.startDate()) + + XCTAssert(hours == 5, "Expected 5, but got \(hours)") + XCTAssert(minutes == 30, "Expected 30, but got \(minutes)") + } + func testScheduleComposition() { let components = DateComponents(year: 2_019, month: 1, day: 19, hour: 15, minute: 30) let startDate = Calendar.current.date(from: components)! From 3eba835d6d2429d81fe2465a47c55dcf7164f7c4 Mon Sep 17 00:00:00 2001 From: gavirawson-apple <51756298+gavirawson-apple@users.noreply.github.com> Date: Mon, 13 Apr 2020 16:40:30 -0700 Subject: [PATCH 29/33] SwiftUI API for the InstructionsTaskView (#409) --- CareKit/CareKit.xcodeproj/project.pbxproj | 20 +++ .../SwiftUI/InstructionsTaskView.swift | 100 +++++++++++ .../InstructionsTaskViewConfiguration.swift | 58 ++++++ .../OCKTaskControllerProtocol+Extension.swift | 63 +++++++ CareKitUI/CareKitUI.xcodeproj/project.pbxproj | 28 +++ .../CareKitUI/Common/Style/OCKStylable.swift | 3 +- .../CareKitUI/Common/Style/OCKStyler.swift | 25 ++- CareKitUI/CareKitUI/SwiftUI/CardView.swift | 115 ++++++++++++ CareKitUI/CareKitUI/SwiftUI/HeaderView.swift | 77 ++++++++ .../SwiftUI/InstructionsTaskView.swift | 167 ++++++++++++++++++ .../CareKitUI/SwiftUI/NoHighlightStyle.swift | 38 ++++ .../SwiftUI/RectangularCompletionView.swift | 76 ++++++++ README.md | 96 ++++++++-- 13 files changed, 854 insertions(+), 12 deletions(-) create mode 100644 CareKit/CareKit/SwiftUI/InstructionsTaskView.swift create mode 100644 CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift create mode 100644 CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift create mode 100644 CareKitUI/CareKitUI/SwiftUI/CardView.swift create mode 100644 CareKitUI/CareKitUI/SwiftUI/HeaderView.swift create mode 100644 CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift create mode 100644 CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift create mode 100644 CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift diff --git a/CareKit/CareKit.xcodeproj/project.pbxproj b/CareKit/CareKit.xcodeproj/project.pbxproj index b82478faf..c5de5a2ec 100644 --- a/CareKit/CareKit.xcodeproj/project.pbxproj +++ b/CareKit/CareKit.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 1409475122B02153005C1D16 /* CareKitStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1409475022B02153005C1D16 /* CareKitStore.framework */; }; 5101E6CB23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */; }; 51094D94234F8E3E00B4BFFB /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */; }; + 510A4D982444DF52009D7FC2 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D952444DF52009D7FC2 /* InstructionsTaskView.swift */; }; + 510A4D992444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D962444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift */; }; + 510A4D9A2444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D972444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift */; }; 510A6656236147FF00074275 /* OCKAnyEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */; }; 510EA10B234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */; }; 510EA10D234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */; }; @@ -150,6 +153,9 @@ 1409475022B02153005C1D16 /* CareKitStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomCalendarViewSynchronizer.swift; sourceTree = ""; }; 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskController.swift; sourceTree = ""; }; + 510A4D952444DF52009D7FC2 /* InstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskView.swift; sourceTree = ""; }; + 510A4D962444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskViewConfiguration.swift; sourceTree = ""; }; + 510A4D972444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCKTaskControllerProtocol+Extension.swift"; sourceTree = ""; }; 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKAnyEvent+Extension.swift"; sourceTree = ""; }; 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKGridTaskViewSynchronizer.swift; sourceTree = ""; }; 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskViewSynchronizer.swift; sourceTree = ""; }; @@ -292,6 +298,16 @@ name = Frameworks; sourceTree = ""; }; + 510A4D902444DF33009D7FC2 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 510A4D972444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift */, + 510A4D952444DF52009D7FC2 /* InstructionsTaskView.swift */, + 510A4D962444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; 510EA109234EA207005D6793 /* Synchronizers */ = { isa = PBXGroup; children = ( @@ -530,6 +546,7 @@ children = ( 64EABDE52321B1AF00CFBB9F /* Info.plist */, 64EABDA32321B1AF00CFBB9F /* CareKit.swift */, + 510A4D902444DF33009D7FC2 /* SwiftUI */, 5167B92123340BF9002BC69C /* View Updaters */, 64EABD9A2321B1AF00CFBB9F /* Lists */, 64EABDA42321B1AF00CFBB9F /* Details */, @@ -933,6 +950,7 @@ 64EABDEA2321B1AF00CFBB9F /* OCKListViewController.swift in Sources */, 64EABE012321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift in Sources */, 514FDE392356362B0044E3B8 /* OCKCalendarController.swift in Sources */, + 510A4D992444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift in Sources */, 51676C0A23556F29002C97E7 /* OCKCalendarControllerProtocol.swift in Sources */, 51B714862367849100590A5A /* OCKButtonLogTaskView+Updatable.swift in Sources */, 51127310235E3B4E007B18DF /* OCKSimpleContactController.swift in Sources */, @@ -942,8 +960,10 @@ 51692D9D23564C2C00D03A44 /* OCKDailyPageViewController.swift in Sources */, 51127326235E5928007B18DF /* OCKChecklistTaskViewController.swift in Sources */, 5112730C235E3AE3007B18DF /* OCKSimpleContactViewController.swift in Sources */, + 510A4D982444DF52009D7FC2 /* InstructionsTaskView.swift in Sources */, 64EABDF02321B1AF00CFBB9F /* OCKDetailViewController.swift in Sources */, 51127328235E597C007B18DF /* OCKInstructionsTaskViewController.swift in Sources */, + 510A4D9A2444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift in Sources */, 51EF7E46234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift b/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift new file mode 100644 index 000000000..fbe4a1c79 --- /dev/null +++ b/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift @@ -0,0 +1,100 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitUI +import Foundation +import SwiftUI + +/// A card that updates when a controller changes. The view displays a header view, multi-line label, and a completion button. +/// +/// In CareKit, this view is intended to display a particular event for a task. The state of the button indicates the completion state of the event. +/// +/// # View Updates +/// The view updates with the observed controller. By default, data from the controller is mapped to the view. The mapping can be customized by +/// providing a closure that returns a view. The closure is called whenever the controller changes. +/// +/// # Style +/// The card supports styling using `careKitStyle(_:)`. +/// +/// ``` +/// +-------------------------------------------------------+ +/// | | +/// | | +/// | <Detail> | +/// | | +/// | -------------------------------------------------- | +/// | | +/// | <Instructions> | +/// | | +/// | +-------------------------------------------------+ | +/// | | <Completion Button> | | +/// | +-------------------------------------------------+ | +/// | | +/// +-------------------------------------------------------+ +/// ``` +public struct InstructionsTaskView<Header: View, Footer: View>: View { + + private let content: (_ configuration: InstructionsTaskViewConfiguration) -> CareKitUI.InstructionsTaskView<Header, Footer> + + /// Owns the view model that drives the view. + @ObservedObject public var controller: OCKInstructionsTaskController + + public var body: some View { + content(.init(controller: controller)) + } + + /// Create an instance that updates the content view when the observed controller changes. + /// - Parameter controller: Owns the view model that drives the view. + /// - Parameter content: Return a view to display whenever the controller changes. + public init(controller: OCKInstructionsTaskController, + content: @escaping (_ configuration: InstructionsTaskViewConfiguration) -> + CareKitUI.InstructionsTaskView<Header, Footer>) { + self.controller = controller + self.content = content + } +} + +public extension InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { + + /// Create an instance that updates the content view when the observed controller changes. The default view will be displayed whenever the + /// controller changes. + /// - Parameter controller: Owns the view model that drives the view. + init(controller: OCKInstructionsTaskController) { + self.init(controller: controller, content: { .init(configuration: $0) }) + } +} + +private extension CareKitUI.InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { + init(configuration: InstructionsTaskViewConfiguration) { + self.init(title: Text(configuration.title), detail: configuration.detail.map { Text($0) }, + instructions: configuration.instructions.map { Text($0) }, + isComplete: configuration.isComplete, action: configuration.action) + } +} diff --git a/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift b/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift new file mode 100644 index 000000000..8ce163d56 --- /dev/null +++ b/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift @@ -0,0 +1,58 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +/// Default data used to map data from an `OCKInstructionsTaskController` to a `CareKitUI.InstructionsTaskView`. +public struct InstructionsTaskViewConfiguration { + + /// The title text to display in the header. + public let title: String + + /// The detail text to display in the header. + public let detail: String? + + /// The instructions text to display under the header. + public let instructions: String? + + /// The action to perform when the button is tapped. + public let action: (() -> Void)? + + /// True if the labeled button is complete. + public let isComplete: Bool + + init(controller: OCKTaskControllerProtocol) { + self.title = controller.title + self.detail = controller.event.map { OCKScheduleUtility.scheduleLabel(for: $0) } ?? "" + self.instructions = controller.instructions + self.isComplete = controller.isFirstEventComplete + self.action = controller.toggleActionForFirstEvent + } +} diff --git a/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift b/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift new file mode 100644 index 000000000..cefcb3114 --- /dev/null +++ b/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift @@ -0,0 +1,63 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Foundation +import SwiftUI + +extension OCKTaskControllerProtocol { + + var event: OCKAnyEvent? { objectWillChange.value?.firstEvent } + + var title: String { event?.task.title ?? "" } + + var instructions: String { event?.task.instructions ?? "" } + + var isFirstEventComplete: Bool { event?.outcome != nil } + + var toggleActionForFirstEvent: () -> Void { { self.toggleFirstEvent() } } + + func isEventComplete(atIndexPath indexPath: IndexPath) -> Bool { + return eventFor(indexPath: indexPath)?.outcome != nil + } + + func toggleActionForEvent(atIndexPath indexPath: IndexPath) -> () -> Void { + return { self.toggleEvent(atIndexPath: indexPath) } + } + + private func toggleEvent(atIndexPath indexPath: IndexPath) { + let isComplete = isEventComplete(atIndexPath: indexPath) + setEvent(atIndexPath: indexPath, isComplete: !isComplete, completion: nil) + } + + private func toggleFirstEvent() { + setEvent(atIndexPath: .init(row: 0, section: 0), isComplete: !isFirstEventComplete, completion: nil) + } +} diff --git a/CareKitUI/CareKitUI.xcodeproj/project.pbxproj b/CareKitUI/CareKitUI.xcodeproj/project.pbxproj index c936905c4..3d712912f 100644 --- a/CareKitUI/CareKitUI.xcodeproj/project.pbxproj +++ b/CareKitUI/CareKitUI.xcodeproj/project.pbxproj @@ -12,6 +12,11 @@ 03AD384623625A3500F6E7DC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 03AD384423625A3500F6E7DC /* Localizable.stringsdict */; }; 5103C55222F37B44007A7403 /* Number+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103C55122F37B44007A7403 /* Number+Extensions.swift */; }; 5105911F237651C8004EDC84 /* OCKAccessibleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */; }; + 510A4D8B2444DE97009D7FC2 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D862444DE96009D7FC2 /* InstructionsTaskView.swift */; }; + 510A4D8C2444DE97009D7FC2 /* NoHighlightStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D872444DE97009D7FC2 /* NoHighlightStyle.swift */; }; + 510A4D8D2444DE97009D7FC2 /* RectangularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D882444DE97009D7FC2 /* RectangularCompletionView.swift */; }; + 510A4D8E2444DE97009D7FC2 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D892444DE97009D7FC2 /* HeaderView.swift */; }; + 510A4D8F2444DE97009D7FC2 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D8A2444DE97009D7FC2 /* CardView.swift */; }; 510A5762234A7B920006C376 /* OCKAnimatedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */; }; 510D862D23020D410073776E /* OCKStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D862C23020D410073776E /* OCKStyler.swift */; }; 512B013322C2F82900ABCB1D /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5174225B224185290054E97C /* CareKitUI.framework */; }; @@ -97,6 +102,11 @@ 05A6B74C237F49F7009D7D1F /* CareKitUI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CareKitUI.xctestplan; sourceTree = "<group>"; }; 5103C55122F37B44007A7403 /* Number+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Extensions.swift"; sourceTree = "<group>"; }; 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAccessibleValue.swift; sourceTree = "<group>"; }; + 510A4D862444DE96009D7FC2 /* InstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskView.swift; sourceTree = "<group>"; }; + 510A4D872444DE97009D7FC2 /* NoHighlightStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoHighlightStyle.swift; sourceTree = "<group>"; }; + 510A4D882444DE97009D7FC2 /* RectangularCompletionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RectangularCompletionView.swift; sourceTree = "<group>"; }; + 510A4D892444DE97009D7FC2 /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; }; + 510A4D8A2444DE97009D7FC2 /* CardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; }; 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAnimatedButton.swift; sourceTree = "<group>"; }; 510D862C23020D410073776E /* OCKStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKStyler.swift; sourceTree = "<group>"; }; 512B012E22C2F82900ABCB1D /* CareKitUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CareKitUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -187,6 +197,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 510A4D852444DE7A009D7FC2 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 510A4D8A2444DE97009D7FC2 /* CardView.swift */, + 510A4D892444DE97009D7FC2 /* HeaderView.swift */, + 510A4D862444DE96009D7FC2 /* InstructionsTaskView.swift */, + 510A4D872444DE97009D7FC2 /* NoHighlightStyle.swift */, + 510A4D882444DE97009D7FC2 /* RectangularCompletionView.swift */, + ); + path = SwiftUI; + sourceTree = "<group>"; + }; 512B012F22C2F82900ABCB1D /* CareKitUITests */ = { isa = PBXGroup; children = ( @@ -227,6 +249,7 @@ 5174225D224185290054E97C /* CareKitUI */ = { isa = PBXGroup; children = ( + 510A4D852444DE7A009D7FC2 /* SwiftUI */, 51E7643A22C2ED7300160C22 /* Authentication */, 51F12D3C229CA88800CA265B /* Common */, 64EEC2342318252B00B1012F /* Supporting Files */, @@ -672,7 +695,10 @@ 518F9D8C22961BF5009CAA48 /* OCKChecklistTaskView.swift in Sources */, 518F9D9422961BF5009CAA48 /* OCKGridTaskCell.swift in Sources */, 5105911F237651C8004EDC84 /* OCKAccessibleValue.swift in Sources */, + 510A4D8D2444DE97009D7FC2 /* RectangularCompletionView.swift in Sources */, 518F9D9A22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift in Sources */, + 510A4D8C2444DE97009D7FC2 /* NoHighlightStyle.swift in Sources */, + 510A4D8F2444DE97009D7FC2 /* CardView.swift in Sources */, 518F9DBC22961BF6009CAA48 /* OCKCardable.swift in Sources */, 518F9DBB22961BF6009CAA48 /* OCKLabel.swift in Sources */, 518F9DA122961BF6009CAA48 /* OCKCartesianChartView.swift in Sources */, @@ -717,6 +743,7 @@ 518F9D8E22961BF5009CAA48 /* OCKSimpleTaskView.swift in Sources */, 51637EA322F3A68400BAF65C /* OCKLogTaskView.swift in Sources */, 5178FBDA22E253FC00794353 /* OCKChartDisplayable.swift in Sources */, + 510A4D8B2444DE97009D7FC2 /* InstructionsTaskView.swift in Sources */, 518F9D8F22961BF5009CAA48 /* OCKChecklistItemButton.swift in Sources */, 518F9DB722961BF6009CAA48 /* OCKSeparatorView.swift in Sources */, 516A02BC22F363D900D55AD7 /* OCKView.swift in Sources */, @@ -725,6 +752,7 @@ 510A5762234A7B920006C376 /* OCKAnimatedButton.swift in Sources */, 518F9DAB22961BF6009CAA48 /* UIFont+Extensions.swift in Sources */, 5103C55222F37B44007A7403 /* Number+Extensions.swift in Sources */, + 510A4D8E2444DE97009D7FC2 /* HeaderView.swift in Sources */, 64E1BD4E2309FCF200DFFE52 /* OCKResponsiveLayout.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift b/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift index be0737c1b..f7899dc75 100644 --- a/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift +++ b/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift @@ -31,7 +31,8 @@ import UIKit // An object that can be styled. -public protocol OCKStylable: AnyObject { +public protocol OCKStylable { + /// Used to override the style. var customStyle: OCKStyler? { get set } diff --git a/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift b/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift index a9d4dbe5f..d4a47f8e5 100644 --- a/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift +++ b/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift @@ -29,6 +29,7 @@ */ import Foundation +import SwiftUI /// Defines styling constants. public protocol OCKStyler { @@ -46,7 +47,29 @@ public extension OCKStyler { var dimension: OCKDimensionStyler { OCKDimensionStyle() } } -// Concrete object that contains style constants +// Concrete object that contains style constants. public struct OCKStyle: OCKStyler { public init() {} } + +private struct StyleEnvironmentKey: EnvironmentKey { + static var defaultValue: OCKStyler = OCKStyle() +} + +public extension EnvironmentValues { + + /// Style constants that can be used by a view. + var careKitStyle: OCKStyler { + get { self[StyleEnvironmentKey.self] } + set { self[StyleEnvironmentKey.self] = newValue } + } +} + +public extension View { + + /// Provide style constants that can be used by a view. + /// - Parameter style: Style constants that can be used by a view. + func careKitStyle(_ style: OCKStyler) -> some View { + return self.environment(\.careKitStyle, style) + } +} diff --git a/CareKitUI/CareKitUI/SwiftUI/CardView.swift b/CareKitUI/CareKitUI/SwiftUI/CardView.swift new file mode 100644 index 000000000..47e76f05c --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/CardView.swift @@ -0,0 +1,115 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI + +/// A card whose content can be injected. +/// +/// # Style +/// The card supports styling using `careKitStyle(_:)`. +/// +/// # Composing Cards +/// To combine SwiftUI views with CareKit views that are already inside of a `CardView`, wrap the views in a new `CardView`. A CareKit view inside of +/// a `CardView` is rendered without its border and background, since it inherits those visual affordances from the surrounding `CardView`. +/// +/// ``` +/// CardView { +/// CardView { +/// Text("Only the outer card's visual features are rendered.") +/// } +/// } +/// ``` +public struct CardView<Content: View>: View { + + // MARK: - Properties + + @Environment(\.careKitStyle) private var style + @Environment(\.cardEnabled) private var cardEnabled + + private var stackedContent: some View { + VStack(alignment: .leading, spacing: style.dimension.directionalInsets1.top) { + content + } + } + + private let content: Content + + public var body: some View { + cardEnabled ? + ViewBuilder.buildEither(first: stackedContent.modifier(CardModifier(style: self.style))) : + ViewBuilder.buildEither(second: stackedContent) + } + + // MARK: - Init + + /// Create a card with injected content. + /// - Parameter content: Content view injected into the card. + public init(@ViewBuilder content: () -> Content) { + self.content = content() + } +} + +private struct CardModifier: ViewModifier { + + let style: OCKStyler + + func body(content: Content) -> some View { + content + .padding() + .background(GeometryReader { geometry in + RoundedRectangle(cornerRadius: self.style.appearance.cornerRadius2, style: .continuous) + .frame(width: geometry.size.width, height: geometry.size.height) + .foregroundColor(Color(self.style.color.secondaryCustomGroupedBackground)) + .shadow(color: Color(hue: 0, saturation: 0, brightness: 0, opacity: Double(self.style.appearance.shadowOpacity1)), + radius: self.style.appearance.shadowRadius1, + x: self.style.appearance.shadowOffset1.width, + y: self.style.appearance.shadowOffset1.height) + }).cardEnabled(false) + } +} + +// MARK: - Environment + +private struct CardEnabledEnvironmentKey: EnvironmentKey { + static var defaultValue = true +} + +private extension EnvironmentValues { + var cardEnabled: Bool { + get { self[CardEnabledEnvironmentKey.self] } + set { self[CardEnabledEnvironmentKey.self] = newValue } + } +} + +private extension View { + func cardEnabled(_ enabled: Bool) -> some View { + return self.environment(\.cardEnabled, enabled) + } +} diff --git a/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift b/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift new file mode 100644 index 000000000..8190152cd --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift @@ -0,0 +1,77 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI + +/// Header used for most CareKit cards. +/// +/// # Style +/// The card supports styling using `careKitStyle(_:)`. +/// +/// ``` +/// +----------------------------------------+ +/// | | +/// | <Title> | +/// | <Detail> | +/// | | +/// +----------------------------------------+ +/// ``` +public struct HeaderView: View { + + // MARK: - Properties + + @Environment(\.careKitStyle) private var style + + private let title: Text + private let detail: Text? + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: style.dimension.directionalInsets1.top / 4.0) { + title + .font(.headline) + .fontWeight(.bold) + detail? + .font(.caption) + .fontWeight(.medium) + }.foregroundColor(Color.primary) + } + } + + // MARK: - Init + + /// Create an instance. + /// - Parameter title: The title text to display above the detail. + /// - Parameter detail: The detail text to display below the title. + public init(title: Text, detail: Text?) { + self.title = title + self.detail = detail + } +} diff --git a/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift b/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift new file mode 100644 index 000000000..faca08453 --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift @@ -0,0 +1,167 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +import Foundation +import SwiftUI + +/// A card that displays a header view, multi-line label, and a completion button. +/// +/// In CareKit, this view is intended to display a particular event for a task. The state of the button indicates the completion state of the event. +/// +/// # Style +/// The card supports styling using `careKitStyle(_:)`. +/// +/// ``` +/// +-------------------------------------------------------+ +/// | | +/// | <Title> | +/// | <Detail> | +/// | | +/// | -------------------------------------------------- | +/// | | +/// | <Instructions> | +/// | | +/// | +-------------------------------------------------+ | +/// | | <Completion Button> | | +/// | +-------------------------------------------------+ | +/// | | +/// +-------------------------------------------------------+ +/// ``` +public struct InstructionsTaskView<Header: View, Footer: View>: View { + + // MARK: - Properties + + @Environment(\.careKitStyle) private var style + + private let header: Header + private let footer: Footer + private let instructions: Text? + + public var body: some View { + CardView { + VStack { + header + } + Divider() + instructions? + .font(.subheadline) + .fontWeight(.medium) + .lineLimit(nil) + VStack { + footer + } + } + } + + // MARK: - Init + + /// Create an instance. + /// - Parameter instructions: Instructions text to display under the header. + /// - Parameter header: Header to inject at the top of the card. Specified content will be stacked vertically. + /// - Parameter footer: View to inject under the instructions. Specified content will be stacked vertically. + public init(instructions: Text?, @ViewBuilder header: () -> Header, @ViewBuilder footer: () -> Footer) { + self.instructions = instructions + self.header = header() + self.footer = footer() + } +} + +public extension InstructionsTaskView where Header == HeaderView { + + /// Create an instance. + /// - Parameter title: Title text to display in the header. + /// - Parameter detail: Detail text to display in the header. + /// - Parameter instructions: Instructions text to display under the header. + /// - Parameter footer: View to inject under the instructions. Specified content will be stacked vertically. + init(title: Text, detail: Text?, instructions: Text?, @ViewBuilder footer: () -> Footer) { + self.init(instructions: instructions, header: { + Header(title: title, detail: detail) + }, footer: footer) + } +} + +public extension InstructionsTaskView where Footer == _InstructionsTaskViewFooter { + + /// Create an instance. + /// - Parameter isComplete: True if the button under the instructions is in the completed. + /// - Parameter instructions: Instructions text to display under the header. + /// - Parameter action: Action to perform when the button is tapped. + /// - Parameter header: Header to inject at the top of the card. Specified content will be stacked vertically. + init(isComplete: Bool, instructions: Text?, action: (() -> Void)?, @ViewBuilder header: () -> Header) { + self.init(instructions: instructions, header: header, footer: { + _InstructionsTaskViewFooter(isComplete: isComplete, action: action) + }) + } +} + +public extension InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { + + /// Create an instance. + /// - Parameter title: Title text to display in the header. + /// - Parameter detail: Detail text to display in the header. + /// - Parameter instructions: Instructions text to display under the header. + /// - Parameter isComplete: True if the button under the instructions is in the completed state. + /// - Parameter action: Action to perform when the button is tapped. + init(title: Text, detail: Text?, instructions: Text?, isComplete: Bool, action: (() -> Void)?) { + self.init(instructions: instructions, header: { + Header(title: title, detail: detail) + }, footer: { + _InstructionsTaskViewFooter(isComplete: isComplete, action: action) + }) + } +} + +// swiftlint:disable type_name + +/// The default footer used by an `InstructionsTaskView`. +public struct _InstructionsTaskViewFooter: View { + + @Environment(\.careKitStyle) private var style + + private var text: String { + isComplete ? loc("COMPLETED") : loc("MARK_COMPLETE") + } + + fileprivate let isComplete: Bool + fileprivate let action: (() -> Void)? + + public var body: some View { + Button(action: action ?? {}) { + RectangularCompletionView(isComplete: isComplete) { + HStack { + Spacer() + Text(text) + Spacer() + } + } + }.buttonStyle(NoHighlightStyle()) + } +} + +// swiftlint:enable type_name diff --git a/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift b/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift new file mode 100644 index 000000000..f462fad5e --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift @@ -0,0 +1,38 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI + +/// Turns off the highlighted (AKA pressed) state. +struct NoHighlightStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + } +} diff --git a/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift b/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift new file mode 100644 index 000000000..caf539b42 --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift @@ -0,0 +1,76 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation +import SwiftUI + +/// A view that denotes a completion state. The style of the view differs based on the completion state. +/// +/// # Style +/// The view supports styling using `careKitStyle(_:)`. +/// +/// ``` +/// +----------------------+ +/// | <Content> | +/// +----------------------+ +/// ``` +public struct RectangularCompletionView<Content: View>: View { + + @Environment(\.careKitStyle) private var style + + private var foregroundColor: Color { + isComplete ? Color.accentColor : Color(style.color.white) + } + + private var backgroundColor: Color { + isComplete ? .init(style.color.tertiaryCustomFill) : .accentColor + } + + private let content: Content + private let isComplete: Bool + + public var body: some View { + VStack { content } + .padding() + .font(Font.subheadline.weight(.medium)) + .foregroundColor(foregroundColor) + .background(backgroundColor) + .cornerRadius(style.appearance.cornerRadius2) + } + + /// Create an instance. + /// - Parameters: + /// - isComplete: The completion state that affects the style of the view. + /// - content: The content of the view. The content will be vertically stacked. + public init(isComplete: Bool, @ViewBuilder content: () -> Content) { + self.isComplete = isComplete + self.content = content() + } +} diff --git a/README.md b/README.md index 33e80ca8c..0ce9b7dfa 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ CareKit™ is an open source software framework for creating apps that help peop * [Charts](#charts) * [Contacts](#contacts) * [Styling](#styling) + * [SwiftUI](#carekitui-swiftui) * [CareKitStore](#carekitstore) * [Store](#store) * [Schema](#schema) @@ -187,9 +188,41 @@ let viewController = TaskButtonViewController(controller: TaskButtonController(s viewController.controller.fetchAndObserveEvents(forTaskID: "Doxylamine", eventQuery: OCKEventQuery(for: Date())) ``` -### SwiftUI <a name="swiftui"></a> +### SwiftUI <a name="carekit-swiftui"></a> -CareKit controllers are compatible with SwiftUI, and can help take care of synchronization with the store. Start by defining a SwiftUI view: +A SwiftUI API is currently available for the `InstructionsTaskView`. The API is a starting point to demonstrate the API architecture. We would love to integrate community contributions that follow the API structure! + +Similar to our UIKit API, there are two types of views - views in CareKitUI that can be initialized with static properties, and views in CareKit that are dynamic and update when data in the store changes. + +To create a view in CareKit that is synchronized with the store: + +```swift +// Create a controller that acts as an `ObservableObject` for the view. +let controller = OCKInstructionsTaskController(storeManager: storeManager) + +// Fetch events for the Doxylamine task, and listen for changes to the task in the store. +controller.fetchAndObserveEvents(forTaskID: "doxylamine", eventQuery: OCKEventQuery(for: Date())) + +// Create the view with the controller. +let view = InstructionsTaskView(controller: controller) +``` + +Data from the controller will be automatically mapped to the view. When the data in the controller changes, the view will update itself. You may want to customize the way in which data is mapped from the controller to the view. To do so, use this initializer on the CareKit view: + +```swift +CareKit.InstructionsTaskView(controller: controller) { configuration in + + // Create an instance of a CareKitUI.InstructionsTaskView to render. Use the `configuration` for the default values to map to the view. + CareKitUI.InstructionsTaskView(title: Text("Custom Title"), // Inject custom text into the title here. + detail: configuration.detail.map { Text($0) }, + instructions: configuration.instructions.map { Text($0) }, + isComplete: configuration.isComplete, + action: configuration.action) +``` + +This initializer requires the initialization of a static SwiftUI view from CareKitUI. That opens up the customization points that are available in CareKitUI. See [SwiftUI in CareKitUI](#carekitui-swiftui) + +You can also create your own SwiftUI views that are synchronized with the store. CareKit controllers are compatible with SwiftUI and can help update the view when data in the store changes: ```swift struct ContentView: View { @@ -216,14 +249,6 @@ struct ContentView: View { } ``` -Next, create a controller and instantiate the view: - -```swift -let controller = OCKSimpleTaskController(storeManager: manager) -controller.fetchAndObserveEvents(forTaskID: "doxylamine", eventQuery: OCKEventQuery(for: Date())) -let contentView = ContentView(controller: controller) -``` - # CareKitUI <a name="carekitui"></a> CareKitUI provides cards to represent tasks, charts, and contacts. There are multiple provided styles for each category of card. @@ -311,6 +336,57 @@ Note that each view in CareKitUI is by default styled with `OCKStyle`. Setting a ![Styling](https://user-images.githubusercontent.com/51756298/69107433-32784800-0a26-11ea-9622-74bb30ce4abd.png) +For information on styling SwiftUI views with `OCKStylable`, see [SwiftUI in CareKitUI](#carekitui-swiftui). + +### SwiftUI <a name="carekitui-swiftui"></a> + +A SwiftUI API is currently available for the `InstructionsTaskView`. The API is a starting point to demonstrate the API architecture. We would love to integrate community contributions that follow the API structure! + +SwiftUI views in CareKitUI are static. To create a static view: + +```swift +InstructionsTaskView(title: Text("Title"), + detail: Text("Detail"), + instructions: Text("Instructions"), + isComplete: false, + action: action) +``` + +Given that the initializer takes `Text`, you can apply custom modifiers to style the view: + +```swift +InstructionsTaskView(title: Text("Title").font(Font.title.weight(.bold)), // Apply custom modifiers here. + detail: Text("Detail"), + instructions: Text("Instructions"), + isComplete: false, + action: action) +``` + +The SwiftUI views are structured with generic containers in which custom content can be placed. The default versions of the views place content in the containers for you, but you can stray from the default behavior and inject custom content. For example, rather than using the default header, you can inject your own custom header: + +```swift +InstructionsTaskView(isComplete: false, + instructions: Text("Instructions"), + action: action, + header: { CustomHeaderView() }) // Inject a custom header here. +``` + +You can also append or prepend custom views. To do so, wrap the default view and your custom views in a `CardView`. Both views will render inside of the same card: + +```swift +CardView { + InstructionsTaskView(/*...*/) + CustomAppendedView() +} +``` + +Finally, SwiftUI views support styling with `OCKStylable` using a view modifier: + +```swift +InstructionsTaskView(/*...*/) + .careKitStyle(CustomStyle()) +``` + # CareKitStore <a name="carekitstore"></a> The CareKitStore package defines the `OCKStoreProtocol` that CareKit uses to talk to data stores, and a concrete implementation that leverages CoreData, called `OCKStore`. From df01680f23714d96b7bd29be39c7f2f48bd281ec Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Tue, 14 Apr 2020 10:54:23 -0700 Subject: [PATCH 30/33] Fix bug where the animated flag in selectDate(_:animated:) wasn't passed (#412) --- .../CareKit/Lists/Controller/OCKDailyPageViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift b/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift index f228dd4fc..c64b8b449 100644 --- a/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift +++ b/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift @@ -113,7 +113,7 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { open func selectDate(_ date: Date, animated: Bool) { let previousDate = selectedDate guard !Calendar.current.isDate(previousDate, inSameDayAs: date) else { return } - calendarWeekPageViewController.selectDate(date, animated: true) + calendarWeekPageViewController.selectDate(date, animated: animated) weekCalendarPageViewController(calendarWeekPageViewController, didSelectDate: date, previousDate: previousDate) } From 610624a9f75f8233989b65956749209d0d21d335 Mon Sep 17 00:00:00 2001 From: Corey <coreyearleon@icloud.com> Date: Tue, 14 Apr 2020 17:54:09 -0400 Subject: [PATCH 31/33] Anchor action sheet in TaskViewController (#410) Fixes a crash on iPad and MacOS devices because the action sheet isn't anchored. --- .../Task/View Controllers/OCKTaskViewController.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift index 84ca79312..402875664 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift @@ -134,6 +134,11 @@ UIViewController, OCKTaskViewDelegate { do { let alert = try controller.initiateDeletionForOutcomeValue(atIndex: index, eventIndexPath: eventIndexPath, deletionCompletion: notifyDelegateAndResetViewOnError) + if let anchor = sender as? UIView { + alert.popoverPresentationController?.sourceRect = anchor.bounds + alert.popoverPresentationController?.sourceView = anchor + alert.popoverPresentationController?.permittedArrowDirections = .any + } present(alert, animated: true, completion: nil) } catch { delegate?.taskViewController(self, didEncounterError: error) From 843b79d86b95fec4994e6caf4f367bcad4175ee1 Mon Sep 17 00:00:00 2001 From: Erik Hornberger <erik_h@apple.com> Date: Mon, 4 May 2020 13:45:28 -0700 Subject: [PATCH 32/33] Remote SwiftUI work from Stable release --- CareKit/CareKit.xcodeproj/project.pbxproj | 20 --- .../SwiftUI/InstructionsTaskView.swift | 100 ----------- .../InstructionsTaskViewConfiguration.swift | 58 ------ .../OCKTaskControllerProtocol+Extension.swift | 63 ------- CareKitUI/CareKitUI.xcodeproj/project.pbxproj | 28 --- .../CareKitUI/Common/Style/OCKStylable.swift | 3 +- .../CareKitUI/Common/Style/OCKStyler.swift | 25 +-- CareKitUI/CareKitUI/SwiftUI/CardView.swift | 115 ------------ CareKitUI/CareKitUI/SwiftUI/HeaderView.swift | 77 -------- .../SwiftUI/InstructionsTaskView.swift | 167 ------------------ .../CareKitUI/SwiftUI/NoHighlightStyle.swift | 38 ---- .../SwiftUI/RectangularCompletionView.swift | 76 -------- README.md | 96 ++-------- 13 files changed, 12 insertions(+), 854 deletions(-) delete mode 100644 CareKit/CareKit/SwiftUI/InstructionsTaskView.swift delete mode 100644 CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift delete mode 100644 CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift delete mode 100644 CareKitUI/CareKitUI/SwiftUI/CardView.swift delete mode 100644 CareKitUI/CareKitUI/SwiftUI/HeaderView.swift delete mode 100644 CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift delete mode 100644 CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift delete mode 100644 CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift diff --git a/CareKit/CareKit.xcodeproj/project.pbxproj b/CareKit/CareKit.xcodeproj/project.pbxproj index c5de5a2ec..b82478faf 100644 --- a/CareKit/CareKit.xcodeproj/project.pbxproj +++ b/CareKit/CareKit.xcodeproj/project.pbxproj @@ -12,9 +12,6 @@ 1409475122B02153005C1D16 /* CareKitStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1409475022B02153005C1D16 /* CareKitStore.framework */; }; 5101E6CB23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */; }; 51094D94234F8E3E00B4BFFB /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */; }; - 510A4D982444DF52009D7FC2 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D952444DF52009D7FC2 /* InstructionsTaskView.swift */; }; - 510A4D992444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D962444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift */; }; - 510A4D9A2444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D972444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift */; }; 510A6656236147FF00074275 /* OCKAnyEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */; }; 510EA10B234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */; }; 510EA10D234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */; }; @@ -153,9 +150,6 @@ 1409475022B02153005C1D16 /* CareKitStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomCalendarViewSynchronizer.swift; sourceTree = "<group>"; }; 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskController.swift; sourceTree = "<group>"; }; - 510A4D952444DF52009D7FC2 /* InstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskView.swift; sourceTree = "<group>"; }; - 510A4D962444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskViewConfiguration.swift; sourceTree = "<group>"; }; - 510A4D972444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCKTaskControllerProtocol+Extension.swift"; sourceTree = "<group>"; }; 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKAnyEvent+Extension.swift"; sourceTree = "<group>"; }; 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKGridTaskViewSynchronizer.swift; sourceTree = "<group>"; }; 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskViewSynchronizer.swift; sourceTree = "<group>"; }; @@ -298,16 +292,6 @@ name = Frameworks; sourceTree = "<group>"; }; - 510A4D902444DF33009D7FC2 /* SwiftUI */ = { - isa = PBXGroup; - children = ( - 510A4D972444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift */, - 510A4D952444DF52009D7FC2 /* InstructionsTaskView.swift */, - 510A4D962444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift */, - ); - path = SwiftUI; - sourceTree = "<group>"; - }; 510EA109234EA207005D6793 /* Synchronizers */ = { isa = PBXGroup; children = ( @@ -546,7 +530,6 @@ children = ( 64EABDE52321B1AF00CFBB9F /* Info.plist */, 64EABDA32321B1AF00CFBB9F /* CareKit.swift */, - 510A4D902444DF33009D7FC2 /* SwiftUI */, 5167B92123340BF9002BC69C /* View Updaters */, 64EABD9A2321B1AF00CFBB9F /* Lists */, 64EABDA42321B1AF00CFBB9F /* Details */, @@ -950,7 +933,6 @@ 64EABDEA2321B1AF00CFBB9F /* OCKListViewController.swift in Sources */, 64EABE012321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift in Sources */, 514FDE392356362B0044E3B8 /* OCKCalendarController.swift in Sources */, - 510A4D992444DF52009D7FC2 /* InstructionsTaskViewConfiguration.swift in Sources */, 51676C0A23556F29002C97E7 /* OCKCalendarControllerProtocol.swift in Sources */, 51B714862367849100590A5A /* OCKButtonLogTaskView+Updatable.swift in Sources */, 51127310235E3B4E007B18DF /* OCKSimpleContactController.swift in Sources */, @@ -960,10 +942,8 @@ 51692D9D23564C2C00D03A44 /* OCKDailyPageViewController.swift in Sources */, 51127326235E5928007B18DF /* OCKChecklistTaskViewController.swift in Sources */, 5112730C235E3AE3007B18DF /* OCKSimpleContactViewController.swift in Sources */, - 510A4D982444DF52009D7FC2 /* InstructionsTaskView.swift in Sources */, 64EABDF02321B1AF00CFBB9F /* OCKDetailViewController.swift in Sources */, 51127328235E597C007B18DF /* OCKInstructionsTaskViewController.swift in Sources */, - 510A4D9A2444DF52009D7FC2 /* OCKTaskControllerProtocol+Extension.swift in Sources */, 51EF7E46234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift b/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift deleted file mode 100644 index fbe4a1c79..000000000 --- a/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift +++ /dev/null @@ -1,100 +0,0 @@ -/* - Copyright (c) 2019, Apple Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder(s) nor the names of any contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. No license is granted to the trademarks of - the copyright holders even if such marks are included in this software. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import CareKitUI -import Foundation -import SwiftUI - -/// A card that updates when a controller changes. The view displays a header view, multi-line label, and a completion button. -/// -/// In CareKit, this view is intended to display a particular event for a task. The state of the button indicates the completion state of the event. -/// -/// # View Updates -/// The view updates with the observed controller. By default, data from the controller is mapped to the view. The mapping can be customized by -/// providing a closure that returns a view. The closure is called whenever the controller changes. -/// -/// # Style -/// The card supports styling using `careKitStyle(_:)`. -/// -/// ``` -/// +-------------------------------------------------------+ -/// | | -/// | <Title> | -/// | <Detail> | -/// | | -/// | -------------------------------------------------- | -/// | | -/// | <Instructions> | -/// | | -/// | +-------------------------------------------------+ | -/// | | <Completion Button> | | -/// | +-------------------------------------------------+ | -/// | | -/// +-------------------------------------------------------+ -/// ``` -public struct InstructionsTaskView<Header: View, Footer: View>: View { - - private let content: (_ configuration: InstructionsTaskViewConfiguration) -> CareKitUI.InstructionsTaskView<Header, Footer> - - /// Owns the view model that drives the view. - @ObservedObject public var controller: OCKInstructionsTaskController - - public var body: some View { - content(.init(controller: controller)) - } - - /// Create an instance that updates the content view when the observed controller changes. - /// - Parameter controller: Owns the view model that drives the view. - /// - Parameter content: Return a view to display whenever the controller changes. - public init(controller: OCKInstructionsTaskController, - content: @escaping (_ configuration: InstructionsTaskViewConfiguration) -> - CareKitUI.InstructionsTaskView<Header, Footer>) { - self.controller = controller - self.content = content - } -} - -public extension InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { - - /// Create an instance that updates the content view when the observed controller changes. The default view will be displayed whenever the - /// controller changes. - /// - Parameter controller: Owns the view model that drives the view. - init(controller: OCKInstructionsTaskController) { - self.init(controller: controller, content: { .init(configuration: $0) }) - } -} - -private extension CareKitUI.InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { - init(configuration: InstructionsTaskViewConfiguration) { - self.init(title: Text(configuration.title), detail: configuration.detail.map { Text($0) }, - instructions: configuration.instructions.map { Text($0) }, - isComplete: configuration.isComplete, action: configuration.action) - } -} diff --git a/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift b/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift deleted file mode 100644 index 8ce163d56..000000000 --- a/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift +++ /dev/null @@ -1,58 +0,0 @@ -/* - Copyright (c) 2020, Apple Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder(s) nor the names of any contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. No license is granted to the trademarks of - the copyright holders even if such marks are included in this software. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import Foundation - -/// Default data used to map data from an `OCKInstructionsTaskController` to a `CareKitUI.InstructionsTaskView`. -public struct InstructionsTaskViewConfiguration { - - /// The title text to display in the header. - public let title: String - - /// The detail text to display in the header. - public let detail: String? - - /// The instructions text to display under the header. - public let instructions: String? - - /// The action to perform when the button is tapped. - public let action: (() -> Void)? - - /// True if the labeled button is complete. - public let isComplete: Bool - - init(controller: OCKTaskControllerProtocol) { - self.title = controller.title - self.detail = controller.event.map { OCKScheduleUtility.scheduleLabel(for: $0) } ?? "" - self.instructions = controller.instructions - self.isComplete = controller.isFirstEventComplete - self.action = controller.toggleActionForFirstEvent - } -} diff --git a/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift b/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift deleted file mode 100644 index cefcb3114..000000000 --- a/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift +++ /dev/null @@ -1,63 +0,0 @@ -/* - Copyright (c) 2020, Apple Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder(s) nor the names of any contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. No license is granted to the trademarks of - the copyright holders even if such marks are included in this software. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import CareKitStore -import Foundation -import SwiftUI - -extension OCKTaskControllerProtocol { - - var event: OCKAnyEvent? { objectWillChange.value?.firstEvent } - - var title: String { event?.task.title ?? "" } - - var instructions: String { event?.task.instructions ?? "" } - - var isFirstEventComplete: Bool { event?.outcome != nil } - - var toggleActionForFirstEvent: () -> Void { { self.toggleFirstEvent() } } - - func isEventComplete(atIndexPath indexPath: IndexPath) -> Bool { - return eventFor(indexPath: indexPath)?.outcome != nil - } - - func toggleActionForEvent(atIndexPath indexPath: IndexPath) -> () -> Void { - return { self.toggleEvent(atIndexPath: indexPath) } - } - - private func toggleEvent(atIndexPath indexPath: IndexPath) { - let isComplete = isEventComplete(atIndexPath: indexPath) - setEvent(atIndexPath: indexPath, isComplete: !isComplete, completion: nil) - } - - private func toggleFirstEvent() { - setEvent(atIndexPath: .init(row: 0, section: 0), isComplete: !isFirstEventComplete, completion: nil) - } -} diff --git a/CareKitUI/CareKitUI.xcodeproj/project.pbxproj b/CareKitUI/CareKitUI.xcodeproj/project.pbxproj index 3d712912f..c936905c4 100644 --- a/CareKitUI/CareKitUI.xcodeproj/project.pbxproj +++ b/CareKitUI/CareKitUI.xcodeproj/project.pbxproj @@ -12,11 +12,6 @@ 03AD384623625A3500F6E7DC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 03AD384423625A3500F6E7DC /* Localizable.stringsdict */; }; 5103C55222F37B44007A7403 /* Number+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103C55122F37B44007A7403 /* Number+Extensions.swift */; }; 5105911F237651C8004EDC84 /* OCKAccessibleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */; }; - 510A4D8B2444DE97009D7FC2 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D862444DE96009D7FC2 /* InstructionsTaskView.swift */; }; - 510A4D8C2444DE97009D7FC2 /* NoHighlightStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D872444DE97009D7FC2 /* NoHighlightStyle.swift */; }; - 510A4D8D2444DE97009D7FC2 /* RectangularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D882444DE97009D7FC2 /* RectangularCompletionView.swift */; }; - 510A4D8E2444DE97009D7FC2 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D892444DE97009D7FC2 /* HeaderView.swift */; }; - 510A4D8F2444DE97009D7FC2 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A4D8A2444DE97009D7FC2 /* CardView.swift */; }; 510A5762234A7B920006C376 /* OCKAnimatedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */; }; 510D862D23020D410073776E /* OCKStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D862C23020D410073776E /* OCKStyler.swift */; }; 512B013322C2F82900ABCB1D /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5174225B224185290054E97C /* CareKitUI.framework */; }; @@ -102,11 +97,6 @@ 05A6B74C237F49F7009D7D1F /* CareKitUI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CareKitUI.xctestplan; sourceTree = "<group>"; }; 5103C55122F37B44007A7403 /* Number+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Extensions.swift"; sourceTree = "<group>"; }; 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAccessibleValue.swift; sourceTree = "<group>"; }; - 510A4D862444DE96009D7FC2 /* InstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskView.swift; sourceTree = "<group>"; }; - 510A4D872444DE97009D7FC2 /* NoHighlightStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoHighlightStyle.swift; sourceTree = "<group>"; }; - 510A4D882444DE97009D7FC2 /* RectangularCompletionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RectangularCompletionView.swift; sourceTree = "<group>"; }; - 510A4D892444DE97009D7FC2 /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; }; - 510A4D8A2444DE97009D7FC2 /* CardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; }; 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAnimatedButton.swift; sourceTree = "<group>"; }; 510D862C23020D410073776E /* OCKStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKStyler.swift; sourceTree = "<group>"; }; 512B012E22C2F82900ABCB1D /* CareKitUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CareKitUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -197,18 +187,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 510A4D852444DE7A009D7FC2 /* SwiftUI */ = { - isa = PBXGroup; - children = ( - 510A4D8A2444DE97009D7FC2 /* CardView.swift */, - 510A4D892444DE97009D7FC2 /* HeaderView.swift */, - 510A4D862444DE96009D7FC2 /* InstructionsTaskView.swift */, - 510A4D872444DE97009D7FC2 /* NoHighlightStyle.swift */, - 510A4D882444DE97009D7FC2 /* RectangularCompletionView.swift */, - ); - path = SwiftUI; - sourceTree = "<group>"; - }; 512B012F22C2F82900ABCB1D /* CareKitUITests */ = { isa = PBXGroup; children = ( @@ -249,7 +227,6 @@ 5174225D224185290054E97C /* CareKitUI */ = { isa = PBXGroup; children = ( - 510A4D852444DE7A009D7FC2 /* SwiftUI */, 51E7643A22C2ED7300160C22 /* Authentication */, 51F12D3C229CA88800CA265B /* Common */, 64EEC2342318252B00B1012F /* Supporting Files */, @@ -695,10 +672,7 @@ 518F9D8C22961BF5009CAA48 /* OCKChecklistTaskView.swift in Sources */, 518F9D9422961BF5009CAA48 /* OCKGridTaskCell.swift in Sources */, 5105911F237651C8004EDC84 /* OCKAccessibleValue.swift in Sources */, - 510A4D8D2444DE97009D7FC2 /* RectangularCompletionView.swift in Sources */, 518F9D9A22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift in Sources */, - 510A4D8C2444DE97009D7FC2 /* NoHighlightStyle.swift in Sources */, - 510A4D8F2444DE97009D7FC2 /* CardView.swift in Sources */, 518F9DBC22961BF6009CAA48 /* OCKCardable.swift in Sources */, 518F9DBB22961BF6009CAA48 /* OCKLabel.swift in Sources */, 518F9DA122961BF6009CAA48 /* OCKCartesianChartView.swift in Sources */, @@ -743,7 +717,6 @@ 518F9D8E22961BF5009CAA48 /* OCKSimpleTaskView.swift in Sources */, 51637EA322F3A68400BAF65C /* OCKLogTaskView.swift in Sources */, 5178FBDA22E253FC00794353 /* OCKChartDisplayable.swift in Sources */, - 510A4D8B2444DE97009D7FC2 /* InstructionsTaskView.swift in Sources */, 518F9D8F22961BF5009CAA48 /* OCKChecklistItemButton.swift in Sources */, 518F9DB722961BF6009CAA48 /* OCKSeparatorView.swift in Sources */, 516A02BC22F363D900D55AD7 /* OCKView.swift in Sources */, @@ -752,7 +725,6 @@ 510A5762234A7B920006C376 /* OCKAnimatedButton.swift in Sources */, 518F9DAB22961BF6009CAA48 /* UIFont+Extensions.swift in Sources */, 5103C55222F37B44007A7403 /* Number+Extensions.swift in Sources */, - 510A4D8E2444DE97009D7FC2 /* HeaderView.swift in Sources */, 64E1BD4E2309FCF200DFFE52 /* OCKResponsiveLayout.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift b/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift index f7899dc75..be0737c1b 100644 --- a/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift +++ b/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift @@ -31,8 +31,7 @@ import UIKit // An object that can be styled. -public protocol OCKStylable { - +public protocol OCKStylable: AnyObject { /// Used to override the style. var customStyle: OCKStyler? { get set } diff --git a/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift b/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift index d4a47f8e5..a9d4dbe5f 100644 --- a/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift +++ b/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift @@ -29,7 +29,6 @@ */ import Foundation -import SwiftUI /// Defines styling constants. public protocol OCKStyler { @@ -47,29 +46,7 @@ public extension OCKStyler { var dimension: OCKDimensionStyler { OCKDimensionStyle() } } -// Concrete object that contains style constants. +// Concrete object that contains style constants public struct OCKStyle: OCKStyler { public init() {} } - -private struct StyleEnvironmentKey: EnvironmentKey { - static var defaultValue: OCKStyler = OCKStyle() -} - -public extension EnvironmentValues { - - /// Style constants that can be used by a view. - var careKitStyle: OCKStyler { - get { self[StyleEnvironmentKey.self] } - set { self[StyleEnvironmentKey.self] = newValue } - } -} - -public extension View { - - /// Provide style constants that can be used by a view. - /// - Parameter style: Style constants that can be used by a view. - func careKitStyle(_ style: OCKStyler) -> some View { - return self.environment(\.careKitStyle, style) - } -} diff --git a/CareKitUI/CareKitUI/SwiftUI/CardView.swift b/CareKitUI/CareKitUI/SwiftUI/CardView.swift deleted file mode 100644 index 47e76f05c..000000000 --- a/CareKitUI/CareKitUI/SwiftUI/CardView.swift +++ /dev/null @@ -1,115 +0,0 @@ -/* - Copyright (c) 2019, Apple Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder(s) nor the names of any contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. No license is granted to the trademarks of - the copyright holders even if such marks are included in this software. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import SwiftUI - -/// A card whose content can be injected. -/// -/// # Style -/// The card supports styling using `careKitStyle(_:)`. -/// -/// # Composing Cards -/// To combine SwiftUI views with CareKit views that are already inside of a `CardView`, wrap the views in a new `CardView`. A CareKit view inside of -/// a `CardView` is rendered without its border and background, since it inherits those visual affordances from the surrounding `CardView`. -/// -/// ``` -/// CardView { -/// CardView { -/// Text("Only the outer card's visual features are rendered.") -/// } -/// } -/// ``` -public struct CardView<Content: View>: View { - - // MARK: - Properties - - @Environment(\.careKitStyle) private var style - @Environment(\.cardEnabled) private var cardEnabled - - private var stackedContent: some View { - VStack(alignment: .leading, spacing: style.dimension.directionalInsets1.top) { - content - } - } - - private let content: Content - - public var body: some View { - cardEnabled ? - ViewBuilder.buildEither(first: stackedContent.modifier(CardModifier(style: self.style))) : - ViewBuilder.buildEither(second: stackedContent) - } - - // MARK: - Init - - /// Create a card with injected content. - /// - Parameter content: Content view injected into the card. - public init(@ViewBuilder content: () -> Content) { - self.content = content() - } -} - -private struct CardModifier: ViewModifier { - - let style: OCKStyler - - func body(content: Content) -> some View { - content - .padding() - .background(GeometryReader { geometry in - RoundedRectangle(cornerRadius: self.style.appearance.cornerRadius2, style: .continuous) - .frame(width: geometry.size.width, height: geometry.size.height) - .foregroundColor(Color(self.style.color.secondaryCustomGroupedBackground)) - .shadow(color: Color(hue: 0, saturation: 0, brightness: 0, opacity: Double(self.style.appearance.shadowOpacity1)), - radius: self.style.appearance.shadowRadius1, - x: self.style.appearance.shadowOffset1.width, - y: self.style.appearance.shadowOffset1.height) - }).cardEnabled(false) - } -} - -// MARK: - Environment - -private struct CardEnabledEnvironmentKey: EnvironmentKey { - static var defaultValue = true -} - -private extension EnvironmentValues { - var cardEnabled: Bool { - get { self[CardEnabledEnvironmentKey.self] } - set { self[CardEnabledEnvironmentKey.self] = newValue } - } -} - -private extension View { - func cardEnabled(_ enabled: Bool) -> some View { - return self.environment(\.cardEnabled, enabled) - } -} diff --git a/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift b/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift deleted file mode 100644 index 8190152cd..000000000 --- a/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright (c) 2019, Apple Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder(s) nor the names of any contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. No license is granted to the trademarks of - the copyright holders even if such marks are included in this software. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import SwiftUI - -/// Header used for most CareKit cards. -/// -/// # Style -/// The card supports styling using `careKitStyle(_:)`. -/// -/// ``` -/// +----------------------------------------+ -/// | | -/// | <Title> | -/// | <Detail> | -/// | | -/// +----------------------------------------+ -/// ``` -public struct HeaderView: View { - - // MARK: - Properties - - @Environment(\.careKitStyle) private var style - - private let title: Text - private let detail: Text? - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: style.dimension.directionalInsets1.top / 4.0) { - title - .font(.headline) - .fontWeight(.bold) - detail? - .font(.caption) - .fontWeight(.medium) - }.foregroundColor(Color.primary) - } - } - - // MARK: - Init - - /// Create an instance. - /// - Parameter title: The title text to display above the detail. - /// - Parameter detail: The detail text to display below the title. - public init(title: Text, detail: Text?) { - self.title = title - self.detail = detail - } -} diff --git a/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift b/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift deleted file mode 100644 index faca08453..000000000 --- a/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift +++ /dev/null @@ -1,167 +0,0 @@ -/* - Copyright (c) 2019, Apple Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder(s) nor the names of any contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. No license is granted to the trademarks of - the copyright holders even if such marks are included in this software. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -import Foundation -import SwiftUI - -/// A card that displays a header view, multi-line label, and a completion button. -/// -/// In CareKit, this view is intended to display a particular event for a task. The state of the button indicates the completion state of the event. -/// -/// # Style -/// The card supports styling using `careKitStyle(_:)`. -/// -/// ``` -/// +-------------------------------------------------------+ -/// | | -/// | <Title> | -/// | <Detail> | -/// | | -/// | -------------------------------------------------- | -/// | | -/// | <Instructions> | -/// | | -/// | +-------------------------------------------------+ | -/// | | <Completion Button> | | -/// | +-------------------------------------------------+ | -/// | | -/// +-------------------------------------------------------+ -/// ``` -public struct InstructionsTaskView<Header: View, Footer: View>: View { - - // MARK: - Properties - - @Environment(\.careKitStyle) private var style - - private let header: Header - private let footer: Footer - private let instructions: Text? - - public var body: some View { - CardView { - VStack { - header - } - Divider() - instructions? - .font(.subheadline) - .fontWeight(.medium) - .lineLimit(nil) - VStack { - footer - } - } - } - - // MARK: - Init - - /// Create an instance. - /// - Parameter instructions: Instructions text to display under the header. - /// - Parameter header: Header to inject at the top of the card. Specified content will be stacked vertically. - /// - Parameter footer: View to inject under the instructions. Specified content will be stacked vertically. - public init(instructions: Text?, @ViewBuilder header: () -> Header, @ViewBuilder footer: () -> Footer) { - self.instructions = instructions - self.header = header() - self.footer = footer() - } -} - -public extension InstructionsTaskView where Header == HeaderView { - - /// Create an instance. - /// - Parameter title: Title text to display in the header. - /// - Parameter detail: Detail text to display in the header. - /// - Parameter instructions: Instructions text to display under the header. - /// - Parameter footer: View to inject under the instructions. Specified content will be stacked vertically. - init(title: Text, detail: Text?, instructions: Text?, @ViewBuilder footer: () -> Footer) { - self.init(instructions: instructions, header: { - Header(title: title, detail: detail) - }, footer: footer) - } -} - -public extension InstructionsTaskView where Footer == _InstructionsTaskViewFooter { - - /// Create an instance. - /// - Parameter isComplete: True if the button under the instructions is in the completed. - /// - Parameter instructions: Instructions text to display under the header. - /// - Parameter action: Action to perform when the button is tapped. - /// - Parameter header: Header to inject at the top of the card. Specified content will be stacked vertically. - init(isComplete: Bool, instructions: Text?, action: (() -> Void)?, @ViewBuilder header: () -> Header) { - self.init(instructions: instructions, header: header, footer: { - _InstructionsTaskViewFooter(isComplete: isComplete, action: action) - }) - } -} - -public extension InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { - - /// Create an instance. - /// - Parameter title: Title text to display in the header. - /// - Parameter detail: Detail text to display in the header. - /// - Parameter instructions: Instructions text to display under the header. - /// - Parameter isComplete: True if the button under the instructions is in the completed state. - /// - Parameter action: Action to perform when the button is tapped. - init(title: Text, detail: Text?, instructions: Text?, isComplete: Bool, action: (() -> Void)?) { - self.init(instructions: instructions, header: { - Header(title: title, detail: detail) - }, footer: { - _InstructionsTaskViewFooter(isComplete: isComplete, action: action) - }) - } -} - -// swiftlint:disable type_name - -/// The default footer used by an `InstructionsTaskView`. -public struct _InstructionsTaskViewFooter: View { - - @Environment(\.careKitStyle) private var style - - private var text: String { - isComplete ? loc("COMPLETED") : loc("MARK_COMPLETE") - } - - fileprivate let isComplete: Bool - fileprivate let action: (() -> Void)? - - public var body: some View { - Button(action: action ?? {}) { - RectangularCompletionView(isComplete: isComplete) { - HStack { - Spacer() - Text(text) - Spacer() - } - } - }.buttonStyle(NoHighlightStyle()) - } -} - -// swiftlint:enable type_name diff --git a/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift b/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift deleted file mode 100644 index f462fad5e..000000000 --- a/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift +++ /dev/null @@ -1,38 +0,0 @@ -/* - Copyright (c) 2019, Apple Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder(s) nor the names of any contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. No license is granted to the trademarks of - the copyright holders even if such marks are included in this software. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import SwiftUI - -/// Turns off the highlighted (AKA pressed) state. -struct NoHighlightStyle: ButtonStyle { - func makeBody(configuration: Self.Configuration) -> some View { - configuration.label - } -} diff --git a/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift b/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift deleted file mode 100644 index caf539b42..000000000 --- a/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright (c) 2020, Apple Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder(s) nor the names of any contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. No license is granted to the trademarks of - the copyright holders even if such marks are included in this software. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import Foundation -import SwiftUI - -/// A view that denotes a completion state. The style of the view differs based on the completion state. -/// -/// # Style -/// The view supports styling using `careKitStyle(_:)`. -/// -/// ``` -/// +----------------------+ -/// | <Content> | -/// +----------------------+ -/// ``` -public struct RectangularCompletionView<Content: View>: View { - - @Environment(\.careKitStyle) private var style - - private var foregroundColor: Color { - isComplete ? Color.accentColor : Color(style.color.white) - } - - private var backgroundColor: Color { - isComplete ? .init(style.color.tertiaryCustomFill) : .accentColor - } - - private let content: Content - private let isComplete: Bool - - public var body: some View { - VStack { content } - .padding() - .font(Font.subheadline.weight(.medium)) - .foregroundColor(foregroundColor) - .background(backgroundColor) - .cornerRadius(style.appearance.cornerRadius2) - } - - /// Create an instance. - /// - Parameters: - /// - isComplete: The completion state that affects the style of the view. - /// - content: The content of the view. The content will be vertically stacked. - public init(isComplete: Bool, @ViewBuilder content: () -> Content) { - self.isComplete = isComplete - self.content = content() - } -} diff --git a/README.md b/README.md index 0ce9b7dfa..33e80ca8c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ CareKit™ is an open source software framework for creating apps that help peop * [Charts](#charts) * [Contacts](#contacts) * [Styling](#styling) - * [SwiftUI](#carekitui-swiftui) * [CareKitStore](#carekitstore) * [Store](#store) * [Schema](#schema) @@ -188,41 +187,9 @@ let viewController = TaskButtonViewController(controller: TaskButtonController(s viewController.controller.fetchAndObserveEvents(forTaskID: "Doxylamine", eventQuery: OCKEventQuery(for: Date())) ``` -### SwiftUI <a name="carekit-swiftui"></a> +### SwiftUI <a name="swiftui"></a> -A SwiftUI API is currently available for the `InstructionsTaskView`. The API is a starting point to demonstrate the API architecture. We would love to integrate community contributions that follow the API structure! - -Similar to our UIKit API, there are two types of views - views in CareKitUI that can be initialized with static properties, and views in CareKit that are dynamic and update when data in the store changes. - -To create a view in CareKit that is synchronized with the store: - -```swift -// Create a controller that acts as an `ObservableObject` for the view. -let controller = OCKInstructionsTaskController(storeManager: storeManager) - -// Fetch events for the Doxylamine task, and listen for changes to the task in the store. -controller.fetchAndObserveEvents(forTaskID: "doxylamine", eventQuery: OCKEventQuery(for: Date())) - -// Create the view with the controller. -let view = InstructionsTaskView(controller: controller) -``` - -Data from the controller will be automatically mapped to the view. When the data in the controller changes, the view will update itself. You may want to customize the way in which data is mapped from the controller to the view. To do so, use this initializer on the CareKit view: - -```swift -CareKit.InstructionsTaskView(controller: controller) { configuration in - - // Create an instance of a CareKitUI.InstructionsTaskView to render. Use the `configuration` for the default values to map to the view. - CareKitUI.InstructionsTaskView(title: Text("Custom Title"), // Inject custom text into the title here. - detail: configuration.detail.map { Text($0) }, - instructions: configuration.instructions.map { Text($0) }, - isComplete: configuration.isComplete, - action: configuration.action) -``` - -This initializer requires the initialization of a static SwiftUI view from CareKitUI. That opens up the customization points that are available in CareKitUI. See [SwiftUI in CareKitUI](#carekitui-swiftui) - -You can also create your own SwiftUI views that are synchronized with the store. CareKit controllers are compatible with SwiftUI and can help update the view when data in the store changes: +CareKit controllers are compatible with SwiftUI, and can help take care of synchronization with the store. Start by defining a SwiftUI view: ```swift struct ContentView: View { @@ -249,6 +216,14 @@ struct ContentView: View { } ``` +Next, create a controller and instantiate the view: + +```swift +let controller = OCKSimpleTaskController(storeManager: manager) +controller.fetchAndObserveEvents(forTaskID: "doxylamine", eventQuery: OCKEventQuery(for: Date())) +let contentView = ContentView(controller: controller) +``` + # CareKitUI <a name="carekitui"></a> CareKitUI provides cards to represent tasks, charts, and contacts. There are multiple provided styles for each category of card. @@ -336,57 +311,6 @@ Note that each view in CareKitUI is by default styled with `OCKStyle`. Setting a ![Styling](https://user-images.githubusercontent.com/51756298/69107433-32784800-0a26-11ea-9622-74bb30ce4abd.png) -For information on styling SwiftUI views with `OCKStylable`, see [SwiftUI in CareKitUI](#carekitui-swiftui). - -### SwiftUI <a name="carekitui-swiftui"></a> - -A SwiftUI API is currently available for the `InstructionsTaskView`. The API is a starting point to demonstrate the API architecture. We would love to integrate community contributions that follow the API structure! - -SwiftUI views in CareKitUI are static. To create a static view: - -```swift -InstructionsTaskView(title: Text("Title"), - detail: Text("Detail"), - instructions: Text("Instructions"), - isComplete: false, - action: action) -``` - -Given that the initializer takes `Text`, you can apply custom modifiers to style the view: - -```swift -InstructionsTaskView(title: Text("Title").font(Font.title.weight(.bold)), // Apply custom modifiers here. - detail: Text("Detail"), - instructions: Text("Instructions"), - isComplete: false, - action: action) -``` - -The SwiftUI views are structured with generic containers in which custom content can be placed. The default versions of the views place content in the containers for you, but you can stray from the default behavior and inject custom content. For example, rather than using the default header, you can inject your own custom header: - -```swift -InstructionsTaskView(isComplete: false, - instructions: Text("Instructions"), - action: action, - header: { CustomHeaderView() }) // Inject a custom header here. -``` - -You can also append or prepend custom views. To do so, wrap the default view and your custom views in a `CardView`. Both views will render inside of the same card: - -```swift -CardView { - InstructionsTaskView(/*...*/) - CustomAppendedView() -} -``` - -Finally, SwiftUI views support styling with `OCKStylable` using a view modifier: - -```swift -InstructionsTaskView(/*...*/) - .careKitStyle(CustomStyle()) -``` - # CareKitStore <a name="carekitstore"></a> The CareKitStore package defines the `OCKStoreProtocol` that CareKit uses to talk to data stores, and a concrete implementation that leverages CoreData, called `OCKStore`. From 754a107e5f2d49c54adf9815e2d4a44363a6a25d Mon Sep 17 00:00:00 2001 From: erik-apple <51723116+erik-apple@users.noreply.github.com> Date: Mon, 4 May 2020 14:03:20 -0700 Subject: [PATCH 33/33] Update .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 36f366b92..d6076fcab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: swift -osx_image: xcode11.3 +osx_image: xcode11.4 xcode_workspace: CKWorkspace.xcworkspace xcode_scheme: CareKit -xcode_destination: platform=iOS Simulator,OS=13.3,name=iPhone 11 Pro Max +xcode_destination: platform=iOS Simulator,OS=13.4,name=iPhone 11 Pro Max