diff --git a/.travis.yml b/.travis.yml index 93425997e..d6076fcab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: swift -osx_image: xcode11 +osx_image: xcode11.4 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.4,name=iPhone 11 Pro Max 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.xcodeproj/project.pbxproj b/CareKit/CareKit.xcodeproj/project.pbxproj index fa66f0de6..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 */; }; @@ -84,6 +82,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 */; }; @@ -141,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 = ""; }; @@ -232,6 +216,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 +469,7 @@ 51FF9B8C2373374200BAEDB2 /* Calendar */, 511372452374DFBD00831191 /* TestSynchronizedContext.swift */, 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */, + 51D8E27E24115D7D0026C716 /* TestListView.swift */, 5196C7FD226F8F8F00F1C2A2 /* Info.plist */, ); path = CareKitTests; @@ -747,7 +733,6 @@ 8605A5B71C4F04EC00DD65FF /* Headers */, 8605A5B61C4F04EC00DD65FF /* Frameworks */, 8605A5B81C4F04EC00DD65FF /* Resources */, - 1409474722B020A2005C1D16 /* Embed Frameworks */, ); buildRules = ( ); @@ -871,6 +856,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/Controller/OCKDailyPageViewController.swift b/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift index 7939b3cb5..c64b8b449 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: animated) + 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() { 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/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/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/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 } 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/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. /// 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..402875664 100644 --- a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift @@ -104,15 +104,15 @@ 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) } } // 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 } @@ -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) 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) + } +} 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/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/OCKCDVersionedObject.swift b/CareKitStore/CareKitStore/CoreData/OCKCDVersionedObject.swift index babed0ab6..2f4c0f992 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)), - NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.deletedDate)) + notDeletedPredicate, + NSPredicate(format: "%K == nil", #keyPath(OCKCDVersionedObject.next)) ]) } 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/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 diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+CarePlans.swift index 32d3ea0f3..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() @@ -68,7 +71,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 +153,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 +166,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]) } @@ -193,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 cb9d8ef6d..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)"))) } @@ -186,9 +189,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 +202,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]) } @@ -229,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 b8208fcdd..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() @@ -57,6 +60,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 +79,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 +135,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 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 event for the outcome is dated \(eventDate), 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 +175,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 } } @@ -194,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 2009e27af..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() @@ -147,10 +150,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) @@ -176,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..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 = { @@ -95,9 +109,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/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/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataTaskStore.swift index f6cb2a7ec..9a35364aa 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 { @@ -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) @@ -120,6 +121,42 @@ extension OCKCoreDataTaskStoreProtocol { } } } + + 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) { @@ -228,8 +265,51 @@ 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 } + + 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 + 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 + 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 47cfa3e21..5fafacb6d 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. @@ -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)) } }) @@ -154,25 +155,62 @@ 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 + + // 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)) - 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 = query.dateInterval.start 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/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/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/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. 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 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/CareKitStore/Structs/OCKSchedule.swift b/CareKitStore/CareKitStore/Structs/OCKSchedule.swift index dcc4c52c8..dd54775c6 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. @@ -120,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/CareKitStore/Structs/OCKScheduleElement.swift b/CareKitStore/CareKitStore/Structs/OCKScheduleElement.swift index 27544eeb6..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!") } @@ -223,6 +225,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/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+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 { diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Outcomes.swift index 8297b05f5..703d9f844 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 { @@ -173,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 { diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Tasks.swift index 1a975e3c2..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 { @@ -237,6 +256,71 @@ 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 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 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)) @@ -279,6 +363,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/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)) + } +} 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) + } } diff --git a/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift b/CareKitStore/CareKitStoreTests/Structs/TestSchedule.swift index bbd72adca..631a1c97f 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 { @@ -64,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)! @@ -170,6 +195,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/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] diff --git a/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift b/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift index 6b5bcd191..588ed8c96 100644 --- a/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift +++ b/CareKitStore/CareKitStoreTests/TestStoreProtocolExtensions.swift @@ -217,6 +217,41 @@ 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") + } + + 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 { 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 388b48bf7..ff7964966 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 + applyTintColor() 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 - } + applyTintColor() } override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -136,15 +142,21 @@ 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 applyTintColor() { + fillLayer.fillColor = tintColor.cgColor + borderLayer.fillColor = tintColor.cgColor } } 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/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/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/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/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/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/Buttons/OCKLabeledCheckmarkButton.swift b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift index db9f0e0a4..3279763ed 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() + applyTintColor() } private func addSubviews() { @@ -85,16 +86,19 @@ open class OCKLabeledCheckmarkButton: OCKAnimatedButton { NSLayoutConstraint.activate(contentStackView.constraints(equalTo: self)) } + private func applyTintColor() { + 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 + 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() } } 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 } 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/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.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 */ }; 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]) - } -} diff --git a/README.md b/README.md index dd7a73e92..33e80ca8c 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 @@ -208,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") } } } @@ -336,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 @@ -351,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: @@ -539,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 { @@ -616,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. -