diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 000000000..0ebc41c06 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,5 @@ +# OTFCareKit + +OTFCareKit is a fork of Apple's CareKit. It is an open source software framework for creating apps that help people better understand and manage their health. + +The full documentation is available here: https://github.com/HippocratesTech/OTFCareKit/blob/master/README.md diff --git a/CareKit.podspec b/CareKit.podspec new file mode 100644 index 000000000..a50b70e1e --- /dev/null +++ b/CareKit.podspec @@ -0,0 +1,26 @@ +Pod::Spec.new do |s| + s.name = 'CareKit' + s.version = '2.0' + s.summary = 'CareKit is an open source software framework for creating apps that help people better understand and manage their health.' + s.homepage = 'https://github.com/carekit-apple/CareKit/' + s.documentation_url = 'https://developer.apple.com/documentation/carekit' + s.screenshots = [ 'https://user-images.githubusercontent.com/51756298/69096972-66de0b00-0a0a-11ea-96f0-4605d04ab396.gif', + 'https://user-images.githubusercontent.com/51756298/69107801-7586eb00-0a27-11ea-8aa2-eca687602c76.gif'] + s.license = { :type => 'BSD', :file => 'LICENSE' } + s.author = { 'researchandcare.org' => 'https://www.researchandcare.org' } + s.platform = :ios + s.ios.deployment_target = '13.0' + s.watchos.deployment_target = '6.0' + s.swift_versions = '5.0' + s.source = { :git => 'https://github.com/carekit-apple/carekit.git', :tag => s.version.to_s} + + s.source_files = 'CareKit/CareKit/**/*' + s.exclude_files = [ 'CareKit/CareKit/**/*.plist', 'OCKCatalog', 'OCKSample', 'DerivedData' ] + s.xcconfig = { 'LIBRARY_SEARCH_PATHS' => "$(SRCROOT)/Pods/**" } + #sp.module_map = 'CareKit/CareKit.modulemap' + s.requires_arc = true + s.frameworks = 'CareKitUI', 'CareKitStore' + s.dependency 'CareKitUI', '2.0' + s.dependency 'CareKitStore', '2.0' + +end diff --git a/CareKit/CareKit.xcodeproj/project.pbxproj b/CareKit/CareKit.xcodeproj/project.pbxproj index b606c95d2..8e66284fa 100644 --- a/CareKit/CareKit.xcodeproj/project.pbxproj +++ b/CareKit/CareKit.xcodeproj/project.pbxproj @@ -1,1529 +1,1533 @@ // !$*UTF8*$! { - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { /* Begin PBXBuildFile section */ - 032C86F02326B68D00D0A0EA /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */; }; - 0346743E2397026A0074891C /* OCKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0346743D2397026A0074891C /* OCKLog.swift */; }; - 03530202248AC32E0073579D /* TestScheduleUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03530201248AC32E0073579D /* TestScheduleUtility.swift */; }; - 5101E6CB23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */; }; - 510591232378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510591222378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift */; }; - 5105912523790D06004EDC84 /* TestMockTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105912423790D06004EDC84 /* TestMockTaskEvents.swift */; }; - 5106616D2473590C00C93CAB /* TestTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5106616B2473590000C93CAB /* TestTaskViewController.swift */; }; - 51094D94234F8E3E00B4BFFB /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */; }; - 510A6656236147FF00074275 /* OCKAnyEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */; }; - 510EA10B234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */; }; - 510EA10D234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */; }; - 51108C6523596BBD0029F7A2 /* OCKSynchronizationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51108C6423596BBD0029F7A2 /* OCKSynchronizationContext.swift */; }; - 5112730C235E3AE3007B18DF /* OCKSimpleContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112730B235E3AE3007B18DF /* OCKSimpleContactViewController.swift */; }; - 5112730E235E3B12007B18DF /* OCKDetailedContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112730D235E3B12007B18DF /* OCKDetailedContactViewController.swift */; }; - 51127310235E3B4E007B18DF /* OCKSimpleContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112730F235E3B4E007B18DF /* OCKSimpleContactController.swift */; }; - 51127312235E3B62007B18DF /* OCKDetailedContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127311235E3B62007B18DF /* OCKDetailedContactController.swift */; }; - 51127314235E3BF4007B18DF /* OCKWeekCalendarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127313235E3BF4007B18DF /* OCKWeekCalendarViewController.swift */; }; - 51127316235E5728007B18DF /* OCKWeekCalendarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127315235E5728007B18DF /* OCKWeekCalendarController.swift */; }; - 51127318235E5849007B18DF /* OCKCartesianChartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127317235E5849007B18DF /* OCKCartesianChartViewController.swift */; }; - 5112731A235E588B007B18DF /* OCKCartesianChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127319235E588B007B18DF /* OCKCartesianChartController.swift */; }; - 5112731C235E58B2007B18DF /* OCKGridTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731B235E58B2007B18DF /* OCKGridTaskController.swift */; }; - 5112731E235E58C4007B18DF /* OCKChecklistTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731D235E58C4007B18DF /* OCKChecklistTaskController.swift */; }; - 51127320235E58D5007B18DF /* OCKSimpleTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731F235E58D5007B18DF /* OCKSimpleTaskController.swift */; }; - 51127322235E58E8007B18DF /* OCKInstructionsTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127321235E58E8007B18DF /* OCKInstructionsTaskController.swift */; }; - 51127326235E5928007B18DF /* OCKChecklistTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127325235E5928007B18DF /* OCKChecklistTaskViewController.swift */; }; - 51127328235E597C007B18DF /* OCKInstructionsTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127327235E597C007B18DF /* OCKInstructionsTaskViewController.swift */; }; - 5112732A235E59AA007B18DF /* OCKButtonLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127329235E59AA007B18DF /* OCKButtonLogViewController.swift */; }; - 511372422374CA1900831191 /* TestCartesianChartViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511372412374CA1900831191 /* TestCartesianChartViewSynchronizer.swift */; }; - 511372442374CA2B00831191 /* TestCustomChartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511372432374CA2B00831191 /* TestCustomChartViewController.swift */; }; - 511372462374DFBD00831191 /* TestSynchronizedContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511372452374DFBD00831191 /* TestSynchronizedContext.swift */; }; - 51168D97246E1EFB0002CC69 /* TestSimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51168D95246E1DBF0002CC69 /* TestSimpleTaskView.swift */; }; - 51168D99246E20B40002CC69 /* TestNumericProgressTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51168D98246E20B40002CC69 /* TestNumericProgressTaskView.swift */; }; - 51168D9B246E211B0002CC69 /* TestInstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51168D9A246E211B0002CC69 /* TestInstructionsTaskView.swift */; }; - 51178E0023AA95270068BAB1 /* OCKTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E17D12351262100637153 /* OCKTaskEvents.swift */; }; - 51178E0123AA95270068BAB1 /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */; }; - 51178E0323AA95270068BAB1 /* OCKChecklistTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731D235E58C4007B18DF /* OCKChecklistTaskController.swift */; }; - 51178E0423AA95270068BAB1 /* OCKSimpleTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731F235E58D5007B18DF /* OCKSimpleTaskController.swift */; }; - 51178E0523AA95270068BAB1 /* OCKInstructionsTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127321235E58E8007B18DF /* OCKInstructionsTaskController.swift */; }; - 51178E0D23AAAA2A0068BAB1 /* OCKSynchronizationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51108C6423596BBD0029F7A2 /* OCKSynchronizationContext.swift */; }; - 51178E0E23AAAA2A0068BAB1 /* OCKStoreNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBD2321B1AF00CFBB9F /* OCKStoreNotifications.swift */; }; - 51178E0F23AAAA2A0068BAB1 /* OCKSynchronizedStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBE2321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift */; }; - 51178E1023AAAA2A0068BAB1 /* OCKSynchronizedStoreManager+Publishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBF2321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift */; }; - 51178E1123AAAACB0068BAB1 /* OCKAnyEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */; }; - 51178E1723AAAD480068BAB1 /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */; }; - 5123901B235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5123901A235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift */; }; - 5123901F2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5123901E2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift */; }; - 5125A215248F2E00009C9643 /* OCKTaskEvents+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5125A214248F2E00009C9643 /* OCKTaskEvents+Extension.swift */; }; - 512BF20F2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512BF20E2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift */; }; - 513271EF234FA33D0025810A /* OCKTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513271EE234FA33D0025810A /* OCKTaskViewController.swift */; }; - 51355EDF24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51355EDE24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift */; }; - 51355EE024997212009DE0A4 /* OCKSynchronizedTaskQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51355EDE24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift */; }; - 51355EE824997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51355EE724997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift */; }; - 51355EEA2499852D009DE0A4 /* OCKTaskEvents+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5125A214248F2E00009C9643 /* OCKTaskEvents+Extension.swift */; }; - 513E721723553E7700AC9620 /* OCKChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513E721623553E7700AC9620 /* OCKChartController.swift */; }; - 514FDE392356362B0044E3B8 /* OCKCalendarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514FDE382356362B0044E3B8 /* OCKCalendarController.swift */; }; - 515862CA23A9C4D600630AB5 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F18223A9C2F20087C900 /* InstructionsTaskView.swift */; }; - 515862CB23A9C4D600630AB5 /* SimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F18323A9C2F20087C900 /* SimpleTaskView.swift */; }; - 515C0A1623BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515C0A1523BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift */; }; - 515C0A1723BF9D46009A9774 /* OCKTaskControllerProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515C0A1523BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift */; }; - 515E17D22351262100637153 /* OCKTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E17D12351262100637153 /* OCKTaskEvents.swift */; }; - 5161BEC22334530E001F03FC /* OCKGridTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5161BEC12334530E001F03FC /* OCKGridTaskView+Updatable.swift */; }; - 516210112355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516210102355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift */; }; - 516433C023539C3B00999B64 /* OCKSimpleTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516433BF23539C3B00999B64 /* OCKSimpleTaskViewController.swift */; }; - 516433C223539EC400999B64 /* OCKButtonLogTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516433C123539EC400999B64 /* OCKButtonLogTaskController.swift */; }; - 51676C0223556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51676C0123556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift */; }; - 51676C0423556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51676C0323556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift */; }; - 51676C0723556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51676C0623556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift */; }; - 5167B9232334187A002BC69C /* OCKSimpleTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B9222334187A002BC69C /* OCKSimpleTaskView+Updatable.swift */; }; - 5167B925233418AA002BC69C /* Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B924233418AA002BC69C /* Updatable.swift */; }; - 5167B92723341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B92623341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift */; }; - 5167B92923341C00002BC69C /* OCKLogTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B92823341C00002BC69C /* OCKLogTaskView+Updatable.swift */; }; - 5167B92F23343A64002BC69C /* OCKHeaderView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B92E23343A64002BC69C /* OCKHeaderView+Updatable.swift */; }; - 5167B93223343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B93123343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift */; }; - 51692D9D23564C2C00D03A44 /* OCKDailyPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABD9C2321B1AF00CFBB9F /* OCKDailyPageViewController.swift */; }; - 516CD56A2354D370005E2779 /* OCKGridTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516CD5692354D370005E2779 /* OCKGridTaskViewController.swift */; }; - 51757C7B2450CF6E0081F133 /* TestTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51757C792450CF690081F133 /* TestTaskEvents.swift */; }; - 51757C812451136A0081F133 /* OCKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0346743D2397026A0074891C /* OCKLog.swift */; }; - 517C784A239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517C7849239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift */; }; - 517F3E4523345872004FE251 /* OCKCartesianChartView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517F3E4423345872004FE251 /* OCKCartesianChartView+Updatable.swift */; }; - 517F3E4823345A39004FE251 /* OCKSimpleContactView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517F3E4723345A39004FE251 /* OCKSimpleContactView+Updatable.swift */; }; - 517F3E4A23345BE9004FE251 /* OCKDetailedContactView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517F3E4923345BE9004FE251 /* OCKDetailedContactView+Updatable.swift */; }; - 5183EA8C2353BC7600C46113 /* OCKScheduleUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDCA2321B1AF00CFBB9F /* OCKScheduleUtility.swift */; }; - 518E153B237265930018541B /* TestSimpleContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518E153A237265930018541B /* TestSimpleContactViewSynchronizer.swift */; }; - 518E154023726A690018541B /* TestDetailedContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518E153F23726A690018541B /* TestDetailedContactViewSynchronizer.swift */; }; - 518E154223726F590018541B /* TestCustomContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518E154123726F590018541B /* TestCustomContactViewSynchronizer.swift */; }; - 518F3D3023552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F3D2F23552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift */; }; - 518F3D362355317B00E00902 /* OCKDataSeriesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDC22321B1AF00CFBB9F /* OCKDataSeriesConfiguration.swift */; }; - 518F3D3923553CB500E00902 /* OCKChartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F3D3823553CB500E00902 /* OCKChartViewController.swift */; }; - 5196C7FE226F8F8F00F1C2A2 /* CareKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8605A5BA1C4F04EC00DD65FF /* CareKit.framework */; }; - 51983EBC23563B0000329252 /* OCKCalendarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51983EBB23563B0000329252 /* OCKCalendarViewController.swift */; }; - 5199906B2447E24F005CD581 /* NumericProgressTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5199906A2447E24F005CD581 /* NumericProgressTaskView.swift */; }; - 519990712448B864005CD581 /* TestNumericProgressTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519990702448B864005CD581 /* TestNumericProgressTaskViewModel.swift */; }; - 51A52A05235669230056150D /* OCKWeekCalendarPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51983EC12356439B00329252 /* OCKWeekCalendarPageViewController.swift */; }; - 51A52A0823566D240056150D /* OCKDailyTasksPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABD9E2321B1AF00CFBB9F /* OCKDailyTasksPageViewController.swift */; }; - 51A52A0923566E5D0056150D /* OCKContactsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABD9F2321B1AF00CFBB9F /* OCKContactsListViewController.swift */; }; - 51AA2304234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AA2303234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift */; }; - 51AF514E24983B86005D385F /* LabeledValueTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF514D24983B86005D385F /* LabeledValueTaskView.swift */; }; - 51AF515024983BE8005D385F /* OCKLabeledValueTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF514F24983BE8005D385F /* OCKLabeledValueTaskController.swift */; }; - 51AF515224984163005D385F /* TestLabeledValueTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF515124984163005D385F /* TestLabeledValueTaskView.swift */; }; - 51AF51542498420D005D385F /* TestLabeledValueTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF51532498420D005D385F /* TestLabeledValueTaskViewModel.swift */; }; - 51AF515624984ACA005D385F /* OCKOutcomeValue+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF515524984ACA005D385F /* OCKOutcomeValue+Extension.swift */; }; - 51AF515824984B6F005D385F /* Number+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF515724984B6F005D385F /* Number+Extension.swift */; }; - 51B1E41B2348149D00AA77D0 /* OCKContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B1E41A2348149D00AA77D0 /* OCKContactController.swift */; }; - 51B1E41D234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B1E41C234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift */; }; - 51B25DC423D28A870050205A /* CareKitStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51B25DC223D28A870050205A /* CareKitStore.framework */; }; - 51B25DC623D28A870050205A /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51B25DC323D28A870050205A /* CareKitUI.framework */; }; - 51B25DCB23D28A900050205A /* CareKitStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51B25DC923D28A900050205A /* CareKitStore.framework */; }; - 51B25DCD23D28A900050205A /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51B25DCA23D28A900050205A /* CareKitUI.framework */; }; - 51BEAD5A237E00C600B32D55 /* TestDailyTasksPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */; }; - 51C599A2246B64DF00880CE2 /* SynchronizedTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C599A1246B64DF00880CE2 /* SynchronizedTaskView.swift */; }; - 51C599A3246B64F200880CE2 /* SynchronizedTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C599A1246B64DF00880CE2 /* SynchronizedTaskView.swift */; }; - 51CE62C024523AD90027B7C1 /* TestTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE62BF24523AD90027B7C1 /* TestTaskController.swift */; }; - 51CFBD8023DA20C5007C0BA8 /* TestSimpleTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CFBD7D23DA1EE1007C0BA8 /* TestSimpleTaskViewModel.swift */; }; - 51CFBD8423DA2499007C0BA8 /* TestInstructionsTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CFBD8123DA2494007C0BA8 /* TestInstructionsTaskViewModel.swift */; }; - 51D544912397557700898683 /* TestChecklistViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D544902397557700898683 /* TestChecklistViewSynchronizer.swift */; }; - 51D544932397584D00898683 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D544922397584D00898683 /* Array+Extension.swift */; }; - 51D5449523975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5449423975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift */; }; - 51D544972397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D544962397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift */; }; - 51D54499239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D54498239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift */; }; - 51D8E27F24115D7D0026C716 /* TestListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D8E27E24115D7D0026C716 /* TestListView.swift */; }; - 51DAA14524A2C8DD008F6655 /* OCKNumericProgressTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DAA14424A2C8DD008F6655 /* OCKNumericProgressTaskController.swift */; }; - 51E76F2224004FA1008B09E7 /* OCKScheduleUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDCA2321B1AF00CFBB9F /* OCKScheduleUtility.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 */; }; - 51EF7E4A234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF7E49234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift */; }; - 51F9F18723A9C2F20087C900 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F18223A9C2F20087C900 /* InstructionsTaskView.swift */; }; - 51F9F18823A9C2F20087C900 /* SimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F18323A9C2F20087C900 /* SimpleTaskView.swift */; }; - 51FF9B8E2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FF9B8D2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift */; }; - 64EABDEA2321B1AF00CFBB9F /* OCKListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABD9D2321B1AF00CFBB9F /* OCKListViewController.swift */; }; - 64EABDED2321B1AF00CFBB9F /* OCKListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA12321B1AF00CFBB9F /* OCKListView.swift */; }; - 64EABDEE2321B1AF00CFBB9F /* OCKHeaderBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA22321B1AF00CFBB9F /* OCKHeaderBodyView.swift */; }; - 64EABDF02321B1AF00CFBB9F /* OCKDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA52321B1AF00CFBB9F /* OCKDetailViewController.swift */; }; - 64EABDF12321B1AF00CFBB9F /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA72321B1AF00CFBB9F /* UIViewController+Extensions.swift */; }; - 64EABDF22321B1AF00CFBB9F /* OCKContact+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA82321B1AF00CFBB9F /* OCKContact+Extensions.swift */; }; - 64EABE002321B1AF00CFBB9F /* OCKStoreNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBD2321B1AF00CFBB9F /* OCKStoreNotifications.swift */; }; - 64EABE012321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBE2321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift */; }; - 64EABE022321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBF2321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift */; }; - 64EABE0B2321B1AF00CFBB9F /* OCKContactUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDCB2321B1AF00CFBB9F /* OCKContactUtility.swift */; }; - 64EABE202321B1AF00CFBB9F /* locversion.plist in Resources */ = {isa = PBXBuildFile; fileRef = 64EABDE72321B1AF00CFBB9F /* locversion.plist */; }; + 032C86F02326B68D00D0A0EA /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */; }; + 0346743E2397026A0074891C /* OCKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0346743D2397026A0074891C /* OCKLog.swift */; }; + 03530202248AC32E0073579D /* TestScheduleUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03530201248AC32E0073579D /* TestScheduleUtility.swift */; }; + 5101E6CB23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */; }; + 510591232378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510591222378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift */; }; + 5105912523790D06004EDC84 /* TestMockTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105912423790D06004EDC84 /* TestMockTaskEvents.swift */; }; + 5106616D2473590C00C93CAB /* TestTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5106616B2473590000C93CAB /* TestTaskViewController.swift */; }; + 51094D94234F8E3E00B4BFFB /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */; }; + 510A6656236147FF00074275 /* OCKAnyEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */; }; + 510EA10B234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */; }; + 510EA10D234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */; }; + 51108C6523596BBD0029F7A2 /* OCKSynchronizationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51108C6423596BBD0029F7A2 /* OCKSynchronizationContext.swift */; }; + 5112730C235E3AE3007B18DF /* OCKSimpleContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112730B235E3AE3007B18DF /* OCKSimpleContactViewController.swift */; }; + 5112730E235E3B12007B18DF /* OCKDetailedContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112730D235E3B12007B18DF /* OCKDetailedContactViewController.swift */; }; + 51127310235E3B4E007B18DF /* OCKSimpleContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112730F235E3B4E007B18DF /* OCKSimpleContactController.swift */; }; + 51127312235E3B62007B18DF /* OCKDetailedContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127311235E3B62007B18DF /* OCKDetailedContactController.swift */; }; + 51127314235E3BF4007B18DF /* OCKWeekCalendarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127313235E3BF4007B18DF /* OCKWeekCalendarViewController.swift */; }; + 51127316235E5728007B18DF /* OCKWeekCalendarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127315235E5728007B18DF /* OCKWeekCalendarController.swift */; }; + 51127318235E5849007B18DF /* OCKCartesianChartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127317235E5849007B18DF /* OCKCartesianChartViewController.swift */; }; + 5112731A235E588B007B18DF /* OCKCartesianChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127319235E588B007B18DF /* OCKCartesianChartController.swift */; }; + 5112731C235E58B2007B18DF /* OCKGridTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731B235E58B2007B18DF /* OCKGridTaskController.swift */; }; + 5112731E235E58C4007B18DF /* OCKChecklistTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731D235E58C4007B18DF /* OCKChecklistTaskController.swift */; }; + 51127320235E58D5007B18DF /* OCKSimpleTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731F235E58D5007B18DF /* OCKSimpleTaskController.swift */; }; + 51127322235E58E8007B18DF /* OCKInstructionsTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127321235E58E8007B18DF /* OCKInstructionsTaskController.swift */; }; + 51127326235E5928007B18DF /* OCKChecklistTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127325235E5928007B18DF /* OCKChecklistTaskViewController.swift */; }; + 51127328235E597C007B18DF /* OCKInstructionsTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127327235E597C007B18DF /* OCKInstructionsTaskViewController.swift */; }; + 5112732A235E59AA007B18DF /* OCKButtonLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127329235E59AA007B18DF /* OCKButtonLogViewController.swift */; }; + 511372422374CA1900831191 /* TestCartesianChartViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511372412374CA1900831191 /* TestCartesianChartViewSynchronizer.swift */; }; + 511372442374CA2B00831191 /* TestCustomChartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511372432374CA2B00831191 /* TestCustomChartViewController.swift */; }; + 511372462374DFBD00831191 /* TestSynchronizedContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511372452374DFBD00831191 /* TestSynchronizedContext.swift */; }; + 51168D97246E1EFB0002CC69 /* TestSimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51168D95246E1DBF0002CC69 /* TestSimpleTaskView.swift */; }; + 51168D99246E20B40002CC69 /* TestNumericProgressTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51168D98246E20B40002CC69 /* TestNumericProgressTaskView.swift */; }; + 51168D9B246E211B0002CC69 /* TestInstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51168D9A246E211B0002CC69 /* TestInstructionsTaskView.swift */; }; + 51178E0023AA95270068BAB1 /* OCKTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E17D12351262100637153 /* OCKTaskEvents.swift */; }; + 51178E0123AA95270068BAB1 /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */; }; + 51178E0323AA95270068BAB1 /* OCKChecklistTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731D235E58C4007B18DF /* OCKChecklistTaskController.swift */; }; + 51178E0423AA95270068BAB1 /* OCKSimpleTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5112731F235E58D5007B18DF /* OCKSimpleTaskController.swift */; }; + 51178E0523AA95270068BAB1 /* OCKInstructionsTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51127321235E58E8007B18DF /* OCKInstructionsTaskController.swift */; }; + 51178E0D23AAAA2A0068BAB1 /* OCKSynchronizationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51108C6423596BBD0029F7A2 /* OCKSynchronizationContext.swift */; }; + 51178E0E23AAAA2A0068BAB1 /* OCKStoreNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBD2321B1AF00CFBB9F /* OCKStoreNotifications.swift */; }; + 51178E0F23AAAA2A0068BAB1 /* OCKSynchronizedStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBE2321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift */; }; + 51178E1023AAAA2A0068BAB1 /* OCKSynchronizedStoreManager+Publishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBF2321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift */; }; + 51178E1123AAAACB0068BAB1 /* OCKAnyEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */; }; + 51178E1723AAAD480068BAB1 /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */; }; + 5123901B235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5123901A235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift */; }; + 5123901F2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5123901E2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift */; }; + 5125A215248F2E00009C9643 /* OCKTaskEvents+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5125A214248F2E00009C9643 /* OCKTaskEvents+Extension.swift */; }; + 512BF20F2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512BF20E2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift */; }; + 513271EF234FA33D0025810A /* OCKTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513271EE234FA33D0025810A /* OCKTaskViewController.swift */; }; + 51355EDF24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51355EDE24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift */; }; + 51355EE024997212009DE0A4 /* OCKSynchronizedTaskQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51355EDE24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift */; }; + 51355EE824997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51355EE724997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift */; }; + 51355EEA2499852D009DE0A4 /* OCKTaskEvents+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5125A214248F2E00009C9643 /* OCKTaskEvents+Extension.swift */; }; + 513E721723553E7700AC9620 /* OCKChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513E721623553E7700AC9620 /* OCKChartController.swift */; }; + 514FDE392356362B0044E3B8 /* OCKCalendarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514FDE382356362B0044E3B8 /* OCKCalendarController.swift */; }; + 515862CA23A9C4D600630AB5 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F18223A9C2F20087C900 /* InstructionsTaskView.swift */; }; + 515862CB23A9C4D600630AB5 /* SimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F18323A9C2F20087C900 /* SimpleTaskView.swift */; }; + 515C0A1623BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515C0A1523BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift */; }; + 515C0A1723BF9D46009A9774 /* OCKTaskControllerProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515C0A1523BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift */; }; + 515E17D22351262100637153 /* OCKTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E17D12351262100637153 /* OCKTaskEvents.swift */; }; + 5161BEC22334530E001F03FC /* OCKGridTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5161BEC12334530E001F03FC /* OCKGridTaskView+Updatable.swift */; }; + 516210112355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516210102355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift */; }; + 516433C023539C3B00999B64 /* OCKSimpleTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516433BF23539C3B00999B64 /* OCKSimpleTaskViewController.swift */; }; + 516433C223539EC400999B64 /* OCKButtonLogTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516433C123539EC400999B64 /* OCKButtonLogTaskController.swift */; }; + 51676C0223556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51676C0123556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift */; }; + 51676C0423556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51676C0323556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift */; }; + 51676C0723556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51676C0623556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift */; }; + 5167B9232334187A002BC69C /* OCKSimpleTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B9222334187A002BC69C /* OCKSimpleTaskView+Updatable.swift */; }; + 5167B925233418AA002BC69C /* Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B924233418AA002BC69C /* Updatable.swift */; }; + 5167B92723341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B92623341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift */; }; + 5167B92923341C00002BC69C /* OCKLogTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B92823341C00002BC69C /* OCKLogTaskView+Updatable.swift */; }; + 5167B92F23343A64002BC69C /* OCKHeaderView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B92E23343A64002BC69C /* OCKHeaderView+Updatable.swift */; }; + 5167B93223343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5167B93123343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift */; }; + 51692D9D23564C2C00D03A44 /* OCKDailyPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABD9C2321B1AF00CFBB9F /* OCKDailyPageViewController.swift */; }; + 516CD56A2354D370005E2779 /* OCKGridTaskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516CD5692354D370005E2779 /* OCKGridTaskViewController.swift */; }; + 516D85DE24B822FA00489200 /* TestWeekCalendarPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516D85DD24B822FA00489200 /* TestWeekCalendarPageViewController.swift */; }; + 51757C7B2450CF6E0081F133 /* TestTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51757C792450CF690081F133 /* TestTaskEvents.swift */; }; + 51757C812451136A0081F133 /* OCKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0346743D2397026A0074891C /* OCKLog.swift */; }; + 517C784A239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517C7849239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift */; }; + 517F3E4523345872004FE251 /* OCKCartesianChartView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517F3E4423345872004FE251 /* OCKCartesianChartView+Updatable.swift */; }; + 517F3E4823345A39004FE251 /* OCKSimpleContactView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517F3E4723345A39004FE251 /* OCKSimpleContactView+Updatable.swift */; }; + 517F3E4A23345BE9004FE251 /* OCKDetailedContactView+Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517F3E4923345BE9004FE251 /* OCKDetailedContactView+Updatable.swift */; }; + 5183EA8C2353BC7600C46113 /* OCKScheduleUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDCA2321B1AF00CFBB9F /* OCKScheduleUtility.swift */; }; + 518E153B237265930018541B /* TestSimpleContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518E153A237265930018541B /* TestSimpleContactViewSynchronizer.swift */; }; + 518E154023726A690018541B /* TestDetailedContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518E153F23726A690018541B /* TestDetailedContactViewSynchronizer.swift */; }; + 518E154223726F590018541B /* TestCustomContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518E154123726F590018541B /* TestCustomContactViewSynchronizer.swift */; }; + 518F3D3023552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F3D2F23552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift */; }; + 518F3D362355317B00E00902 /* OCKDataSeriesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDC22321B1AF00CFBB9F /* OCKDataSeriesConfiguration.swift */; }; + 518F3D3923553CB500E00902 /* OCKChartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F3D3823553CB500E00902 /* OCKChartViewController.swift */; }; + 5196C7FE226F8F8F00F1C2A2 /* CareKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8605A5BA1C4F04EC00DD65FF /* CareKit.framework */; }; + 51983EBC23563B0000329252 /* OCKCalendarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51983EBB23563B0000329252 /* OCKCalendarViewController.swift */; }; + 5199906B2447E24F005CD581 /* NumericProgressTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5199906A2447E24F005CD581 /* NumericProgressTaskView.swift */; }; + 519990712448B864005CD581 /* TestNumericProgressTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519990702448B864005CD581 /* TestNumericProgressTaskViewModel.swift */; }; + 51A52A05235669230056150D /* OCKWeekCalendarPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51983EC12356439B00329252 /* OCKWeekCalendarPageViewController.swift */; }; + 51A52A0823566D240056150D /* OCKDailyTasksPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABD9E2321B1AF00CFBB9F /* OCKDailyTasksPageViewController.swift */; }; + 51A52A0923566E5D0056150D /* OCKContactsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABD9F2321B1AF00CFBB9F /* OCKContactsListViewController.swift */; }; + 51AA2304234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AA2303234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift */; }; + 51AF514E24983B86005D385F /* LabeledValueTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF514D24983B86005D385F /* LabeledValueTaskView.swift */; }; + 51AF515024983BE8005D385F /* OCKLabeledValueTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF514F24983BE8005D385F /* OCKLabeledValueTaskController.swift */; }; + 51AF515224984163005D385F /* TestLabeledValueTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF515124984163005D385F /* TestLabeledValueTaskView.swift */; }; + 51AF51542498420D005D385F /* TestLabeledValueTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF51532498420D005D385F /* TestLabeledValueTaskViewModel.swift */; }; + 51AF515624984ACA005D385F /* OCKOutcomeValue+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF515524984ACA005D385F /* OCKOutcomeValue+Extension.swift */; }; + 51AF515824984B6F005D385F /* Number+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF515724984B6F005D385F /* Number+Extension.swift */; }; + 51B1E41B2348149D00AA77D0 /* OCKContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B1E41A2348149D00AA77D0 /* OCKContactController.swift */; }; + 51B1E41D234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B1E41C234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift */; }; + 51B25DC423D28A870050205A /* CareKitStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51B25DC223D28A870050205A /* CareKitStore.framework */; }; + 51B25DC623D28A870050205A /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51B25DC323D28A870050205A /* CareKitUI.framework */; }; + 51B25DCB23D28A900050205A /* CareKitStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51B25DC923D28A900050205A /* CareKitStore.framework */; }; + 51B25DCD23D28A900050205A /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51B25DCA23D28A900050205A /* CareKitUI.framework */; }; + 51BEAD5A237E00C600B32D55 /* TestDailyTasksPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */; }; + 51C599A2246B64DF00880CE2 /* SynchronizedTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C599A1246B64DF00880CE2 /* SynchronizedTaskView.swift */; }; + 51C599A3246B64F200880CE2 /* SynchronizedTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C599A1246B64DF00880CE2 /* SynchronizedTaskView.swift */; }; + 51CE62C024523AD90027B7C1 /* TestTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE62BF24523AD90027B7C1 /* TestTaskController.swift */; }; + 51CFBD8023DA20C5007C0BA8 /* TestSimpleTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CFBD7D23DA1EE1007C0BA8 /* TestSimpleTaskViewModel.swift */; }; + 51CFBD8423DA2499007C0BA8 /* TestInstructionsTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CFBD8123DA2494007C0BA8 /* TestInstructionsTaskViewModel.swift */; }; + 51D544912397557700898683 /* TestChecklistViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D544902397557700898683 /* TestChecklistViewSynchronizer.swift */; }; + 51D544932397584D00898683 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D544922397584D00898683 /* Array+Extension.swift */; }; + 51D5449523975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5449423975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift */; }; + 51D544972397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D544962397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift */; }; + 51D54499239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D54498239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift */; }; + 51D8E27F24115D7D0026C716 /* TestListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D8E27E24115D7D0026C716 /* TestListView.swift */; }; + 51DAA14524A2C8DD008F6655 /* OCKNumericProgressTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DAA14424A2C8DD008F6655 /* OCKNumericProgressTaskController.swift */; }; + 51E76F2224004FA1008B09E7 /* OCKScheduleUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDCA2321B1AF00CFBB9F /* OCKScheduleUtility.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 */; }; + 51EF7E4A234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF7E49234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift */; }; + 51F9F18723A9C2F20087C900 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F18223A9C2F20087C900 /* InstructionsTaskView.swift */; }; + 51F9F18823A9C2F20087C900 /* SimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F18323A9C2F20087C900 /* SimpleTaskView.swift */; }; + 51FF9B8E2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FF9B8D2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift */; }; + 64EABDEA2321B1AF00CFBB9F /* OCKListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABD9D2321B1AF00CFBB9F /* OCKListViewController.swift */; }; + 64EABDED2321B1AF00CFBB9F /* OCKListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA12321B1AF00CFBB9F /* OCKListView.swift */; }; + 64EABDEE2321B1AF00CFBB9F /* OCKHeaderBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA22321B1AF00CFBB9F /* OCKHeaderBodyView.swift */; }; + 64EABDF02321B1AF00CFBB9F /* OCKDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA52321B1AF00CFBB9F /* OCKDetailViewController.swift */; }; + 64EABDF12321B1AF00CFBB9F /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA72321B1AF00CFBB9F /* UIViewController+Extensions.swift */; }; + 64EABDF22321B1AF00CFBB9F /* OCKContact+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDA82321B1AF00CFBB9F /* OCKContact+Extensions.swift */; }; + 64EABE002321B1AF00CFBB9F /* OCKStoreNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBD2321B1AF00CFBB9F /* OCKStoreNotifications.swift */; }; + 64EABE012321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBE2321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift */; }; + 64EABE022321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDBF2321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift */; }; + 64EABE0B2321B1AF00CFBB9F /* OCKContactUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EABDCB2321B1AF00CFBB9F /* OCKContactUtility.swift */; }; + 64EABE202321B1AF00CFBB9F /* locversion.plist in Resources */ = {isa = PBXBuildFile; fileRef = 64EABDE72321B1AF00CFBB9F /* locversion.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 5196C7FF226F8F8F00F1C2A2 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 8605A5B11C4F04EC00DD65FF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8605A5B91C4F04EC00DD65FF; - remoteInfo = CareKit; - }; + 5196C7FF226F8F8F00F1C2A2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8605A5B11C4F04EC00DD65FF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8605A5B91C4F04EC00DD65FF; + remoteInfo = CareKit; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; - 0346743D2397026A0074891C /* OCKLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLog.swift; sourceTree = ""; }; - 03530201248AC32E0073579D /* TestScheduleUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestScheduleUtility.swift; sourceTree = ""; }; - 05A6B74A237F43D2009D7D1F /* CareKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = CareKit.xctestplan; path = ../../CareKit.xctestplan; sourceTree = ""; }; - 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomCalendarViewSynchronizer.swift; sourceTree = ""; }; - 510591222378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGridTaskViewSynchronizer.swift; sourceTree = ""; }; - 5105912423790D06004EDC84 /* TestMockTaskEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMockTaskEvents.swift; sourceTree = ""; }; - 5106616B2473590000C93CAB /* TestTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTaskViewController.swift; sourceTree = ""; }; - 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskController.swift; sourceTree = ""; }; - 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKAnyEvent+Extension.swift"; sourceTree = ""; }; - 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKGridTaskViewSynchronizer.swift; sourceTree = ""; }; - 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskViewSynchronizer.swift; sourceTree = ""; }; - 51108C6423596BBD0029F7A2 /* OCKSynchronizationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSynchronizationContext.swift; sourceTree = ""; }; - 5112730B235E3AE3007B18DF /* OCKSimpleContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleContactViewController.swift; sourceTree = ""; }; - 5112730D235E3B12007B18DF /* OCKDetailedContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailedContactViewController.swift; sourceTree = ""; }; - 5112730F235E3B4E007B18DF /* OCKSimpleContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleContactController.swift; sourceTree = ""; }; - 51127311235E3B62007B18DF /* OCKDetailedContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailedContactController.swift; sourceTree = ""; }; - 51127313235E3BF4007B18DF /* OCKWeekCalendarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarViewController.swift; sourceTree = ""; }; - 51127315235E5728007B18DF /* OCKWeekCalendarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarController.swift; sourceTree = ""; }; - 51127317235E5849007B18DF /* OCKCartesianChartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCartesianChartViewController.swift; sourceTree = ""; }; - 51127319235E588B007B18DF /* OCKCartesianChartController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCartesianChartController.swift; sourceTree = ""; }; - 5112731B235E58B2007B18DF /* OCKGridTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKGridTaskController.swift; sourceTree = ""; }; - 5112731D235E58C4007B18DF /* OCKChecklistTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskController.swift; sourceTree = ""; }; - 5112731F235E58D5007B18DF /* OCKSimpleTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleTaskController.swift; sourceTree = ""; }; - 51127321235E58E8007B18DF /* OCKInstructionsTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKInstructionsTaskController.swift; sourceTree = ""; }; - 51127325235E5928007B18DF /* OCKChecklistTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskViewController.swift; sourceTree = ""; }; - 51127327235E597C007B18DF /* OCKInstructionsTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKInstructionsTaskViewController.swift; sourceTree = ""; }; - 51127329235E59AA007B18DF /* OCKButtonLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKButtonLogViewController.swift; sourceTree = ""; }; - 511372412374CA1900831191 /* TestCartesianChartViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCartesianChartViewSynchronizer.swift; sourceTree = ""; }; - 511372432374CA2B00831191 /* TestCustomChartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomChartViewController.swift; sourceTree = ""; }; - 511372452374DFBD00831191 /* TestSynchronizedContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSynchronizedContext.swift; sourceTree = ""; }; - 51168D95246E1DBF0002CC69 /* TestSimpleTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSimpleTaskView.swift; sourceTree = ""; }; - 51168D98246E20B40002CC69 /* TestNumericProgressTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNumericProgressTaskView.swift; sourceTree = ""; }; - 51168D9A246E211B0002CC69 /* TestInstructionsTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInstructionsTaskView.swift; sourceTree = ""; }; - 5123901A235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChartViewSynchronizerProtocol.swift; sourceTree = ""; }; - 5123901E2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskViewSynchronizerProtocol.swift; sourceTree = ""; }; - 5125A214248F2E00009C9643 /* OCKTaskEvents+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKTaskEvents+Extension.swift"; sourceTree = ""; }; - 512BF20E2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extensions.swift"; sourceTree = ""; }; - 513271EE234FA33D0025810A /* OCKTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskViewController.swift; sourceTree = ""; }; - 51355EDE24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSynchronizedTaskQuery.swift; sourceTree = ""; }; - 51355EE724997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSynchronizedContactQuery.swift; sourceTree = ""; }; - 513E721623553E7700AC9620 /* OCKChartController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChartController.swift; sourceTree = ""; }; - 514FDE382356362B0044E3B8 /* OCKCalendarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCalendarController.swift; sourceTree = ""; }; - 515C0A1523BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKTaskControllerProtocol+Extension.swift"; sourceTree = ""; }; - 515E17D12351262100637153 /* OCKTaskEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskEvents.swift; sourceTree = ""; }; - 5161BEC12334530E001F03FC /* OCKGridTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKGridTaskView+Updatable.swift"; sourceTree = ""; }; - 516210102355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKContactViewSynchronizerProtocol.swift; sourceTree = ""; }; - 516433BF23539C3B00999B64 /* OCKSimpleTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleTaskViewController.swift; sourceTree = ""; }; - 516433C123539EC400999B64 /* OCKButtonLogTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKButtonLogTaskController.swift; sourceTree = ""; }; - 51676C0123556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCalendarViewSynchronizerProtocol.swift; sourceTree = ""; }; - 51676C0323556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarViewSynchronizer.swift; sourceTree = ""; }; - 51676C0623556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKWeekCalendarView+Updatable.swift"; sourceTree = ""; }; - 5167B9222334187A002BC69C /* OCKSimpleTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKSimpleTaskView+Updatable.swift"; sourceTree = ""; }; - 5167B924233418AA002BC69C /* Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updatable.swift; sourceTree = ""; }; - 5167B92623341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKInstructionsTaskView+Updatable.swift"; sourceTree = ""; }; - 5167B92823341C00002BC69C /* OCKLogTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKLogTaskView+Updatable.swift"; sourceTree = ""; }; - 5167B92E23343A64002BC69C /* OCKHeaderView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKHeaderView+Updatable.swift"; sourceTree = ""; }; - 5167B93123343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKChecklistTaskView+Updatable.swift"; sourceTree = ""; }; - 516CD5692354D370005E2779 /* OCKGridTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKGridTaskViewController.swift; sourceTree = ""; }; - 51757C792450CF690081F133 /* TestTaskEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTaskEvents.swift; sourceTree = ""; }; - 517C7849239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomTaskViewSynchronizer.swift; sourceTree = ""; }; - 517F3E4423345872004FE251 /* OCKCartesianChartView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKCartesianChartView+Updatable.swift"; sourceTree = ""; }; - 517F3E4723345A39004FE251 /* OCKSimpleContactView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKSimpleContactView+Updatable.swift"; sourceTree = ""; }; - 517F3E4923345BE9004FE251 /* OCKDetailedContactView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKDetailedContactView+Updatable.swift"; sourceTree = ""; }; - 518E153A237265930018541B /* TestSimpleContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSimpleContactViewSynchronizer.swift; sourceTree = ""; }; - 518E153F23726A690018541B /* TestDetailedContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDetailedContactViewSynchronizer.swift; sourceTree = ""; }; - 518E154123726F590018541B /* TestCustomContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomContactViewSynchronizer.swift; sourceTree = ""; }; - 518F3D2F23552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCartesianChartViewSynchronizer.swift; sourceTree = ""; }; - 518F3D3823553CB500E00902 /* OCKChartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChartViewController.swift; sourceTree = ""; }; - 5196C7F9226F8F8F00F1C2A2 /* CareKitTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CareKitTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 5196C7FD226F8F8F00F1C2A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 51983EBB23563B0000329252 /* OCKCalendarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCalendarViewController.swift; sourceTree = ""; }; - 51983EC12356439B00329252 /* OCKWeekCalendarPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarPageViewController.swift; sourceTree = ""; }; - 5199906A2447E24F005CD581 /* NumericProgressTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericProgressTaskView.swift; sourceTree = ""; }; - 519990702448B864005CD581 /* TestNumericProgressTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNumericProgressTaskViewModel.swift; sourceTree = ""; }; - 51AA2303234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleContactViewSynchronizer.swift; sourceTree = ""; }; - 51AF514D24983B86005D385F /* LabeledValueTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledValueTaskView.swift; sourceTree = ""; }; - 51AF514F24983BE8005D385F /* OCKLabeledValueTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLabeledValueTaskController.swift; sourceTree = ""; }; - 51AF515124984163005D385F /* TestLabeledValueTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLabeledValueTaskView.swift; sourceTree = ""; }; - 51AF51532498420D005D385F /* TestLabeledValueTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLabeledValueTaskViewModel.swift; sourceTree = ""; }; - 51AF515524984ACA005D385F /* OCKOutcomeValue+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKOutcomeValue+Extension.swift"; sourceTree = ""; }; - 51AF515724984B6F005D385F /* Number+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Extension.swift"; sourceTree = ""; }; - 51B1E41A2348149D00AA77D0 /* OCKContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKContactController.swift; sourceTree = ""; }; - 51B1E41C234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailedContactViewSynchronizer.swift; sourceTree = ""; }; - 51B25DC223D28A870050205A /* CareKitStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 51B25DC323D28A870050205A /* CareKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 51B25DC923D28A900050205A /* CareKitStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 51B25DCA23D28A900050205A /* CareKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDailyTasksPageViewController.swift; sourceTree = ""; }; - 51C599A1246B64DF00880CE2 /* SynchronizedTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedTaskView.swift; sourceTree = ""; }; - 51CE62BF24523AD90027B7C1 /* TestTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTaskController.swift; sourceTree = ""; }; - 51CFBD7D23DA1EE1007C0BA8 /* TestSimpleTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSimpleTaskViewModel.swift; sourceTree = ""; }; - 51CFBD8123DA2494007C0BA8 /* TestInstructionsTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInstructionsTaskViewModel.swift; sourceTree = ""; }; - 51D544902397557700898683 /* TestChecklistViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestChecklistViewSynchronizer.swift; sourceTree = ""; }; - 51D544922397584D00898683 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; - 51D5449423975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSimpleTaskViewSynchronizer.swift; sourceTree = ""; }; - 51D544962397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInstructionsTaskViewSynchronizer.swift; sourceTree = ""; }; - 51D54498239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestButtonLogTaskViewSynchronizer.swift; sourceTree = ""; }; - 51D8E27E24115D7D0026C716 /* TestListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestListView.swift; sourceTree = ""; }; - 51DAA14424A2C8DD008F6655 /* OCKNumericProgressTaskController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKNumericProgressTaskController.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 = ""; }; - 51EF7E49234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKButtonLogTaskViewSynchronizer.swift; sourceTree = ""; }; - 51F9F17323A9C1A50087C900 /* CareKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CareKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 51F9F18223A9C2F20087C900 /* InstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskView.swift; sourceTree = ""; }; - 51F9F18323A9C2F20087C900 /* SimpleTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleTaskView.swift; sourceTree = ""; }; - 51FF9B8D2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWeekCalendarViewSynchronizer.swift; sourceTree = ""; }; - 64EABD9C2321B1AF00CFBB9F /* OCKDailyPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDailyPageViewController.swift; sourceTree = ""; }; - 64EABD9D2321B1AF00CFBB9F /* OCKListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKListViewController.swift; sourceTree = ""; }; - 64EABD9E2321B1AF00CFBB9F /* OCKDailyTasksPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDailyTasksPageViewController.swift; sourceTree = ""; }; - 64EABD9F2321B1AF00CFBB9F /* OCKContactsListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKContactsListViewController.swift; sourceTree = ""; }; - 64EABDA12321B1AF00CFBB9F /* OCKListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKListView.swift; sourceTree = ""; }; - 64EABDA22321B1AF00CFBB9F /* OCKHeaderBodyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKHeaderBodyView.swift; sourceTree = ""; }; - 64EABDA52321B1AF00CFBB9F /* OCKDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDetailViewController.swift; sourceTree = ""; }; - 64EABDA72321B1AF00CFBB9F /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; - 64EABDA82321B1AF00CFBB9F /* OCKContact+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCKContact+Extensions.swift"; sourceTree = ""; }; - 64EABDBD2321B1AF00CFBB9F /* OCKStoreNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKStoreNotifications.swift; sourceTree = ""; }; - 64EABDBE2321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSynchronizedStoreManager.swift; sourceTree = ""; }; - 64EABDBF2321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCKSynchronizedStoreManager+Publishers.swift"; sourceTree = ""; }; - 64EABDC22321B1AF00CFBB9F /* OCKDataSeriesConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDataSeriesConfiguration.swift; sourceTree = ""; }; - 64EABDCA2321B1AF00CFBB9F /* OCKScheduleUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKScheduleUtility.swift; sourceTree = ""; }; - 64EABDCB2321B1AF00CFBB9F /* OCKContactUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKContactUtility.swift; sourceTree = ""; }; - 64EABDE52321B1AF00CFBB9F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 64EABDE82321B1AF00CFBB9F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/locversion.plist; sourceTree = ""; }; - 8605A5BA1C4F04EC00DD65FF /* CareKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CareKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; + 0346743D2397026A0074891C /* OCKLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLog.swift; sourceTree = ""; }; + 03530201248AC32E0073579D /* TestScheduleUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestScheduleUtility.swift; sourceTree = ""; }; + 05A6B74A237F43D2009D7D1F /* CareKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = CareKit.xctestplan; path = ../../CareKit.xctestplan; sourceTree = ""; }; + 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomCalendarViewSynchronizer.swift; sourceTree = ""; }; + 510591222378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGridTaskViewSynchronizer.swift; sourceTree = ""; }; + 5105912423790D06004EDC84 /* TestMockTaskEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMockTaskEvents.swift; sourceTree = ""; }; + 5106616B2473590000C93CAB /* TestTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTaskViewController.swift; sourceTree = ""; }; + 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskController.swift; sourceTree = ""; }; + 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKAnyEvent+Extension.swift"; sourceTree = ""; }; + 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKGridTaskViewSynchronizer.swift; sourceTree = ""; }; + 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskViewSynchronizer.swift; sourceTree = ""; }; + 51108C6423596BBD0029F7A2 /* OCKSynchronizationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSynchronizationContext.swift; sourceTree = ""; }; + 5112730B235E3AE3007B18DF /* OCKSimpleContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleContactViewController.swift; sourceTree = ""; }; + 5112730D235E3B12007B18DF /* OCKDetailedContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailedContactViewController.swift; sourceTree = ""; }; + 5112730F235E3B4E007B18DF /* OCKSimpleContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleContactController.swift; sourceTree = ""; }; + 51127311235E3B62007B18DF /* OCKDetailedContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailedContactController.swift; sourceTree = ""; }; + 51127313235E3BF4007B18DF /* OCKWeekCalendarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarViewController.swift; sourceTree = ""; }; + 51127315235E5728007B18DF /* OCKWeekCalendarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarController.swift; sourceTree = ""; }; + 51127317235E5849007B18DF /* OCKCartesianChartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCartesianChartViewController.swift; sourceTree = ""; }; + 51127319235E588B007B18DF /* OCKCartesianChartController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCartesianChartController.swift; sourceTree = ""; }; + 5112731B235E58B2007B18DF /* OCKGridTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKGridTaskController.swift; sourceTree = ""; }; + 5112731D235E58C4007B18DF /* OCKChecklistTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskController.swift; sourceTree = ""; }; + 5112731F235E58D5007B18DF /* OCKSimpleTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleTaskController.swift; sourceTree = ""; }; + 51127321235E58E8007B18DF /* OCKInstructionsTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKInstructionsTaskController.swift; sourceTree = ""; }; + 51127325235E5928007B18DF /* OCKChecklistTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskViewController.swift; sourceTree = ""; }; + 51127327235E597C007B18DF /* OCKInstructionsTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKInstructionsTaskViewController.swift; sourceTree = ""; }; + 51127329235E59AA007B18DF /* OCKButtonLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKButtonLogViewController.swift; sourceTree = ""; }; + 511372412374CA1900831191 /* TestCartesianChartViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCartesianChartViewSynchronizer.swift; sourceTree = ""; }; + 511372432374CA2B00831191 /* TestCustomChartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomChartViewController.swift; sourceTree = ""; }; + 511372452374DFBD00831191 /* TestSynchronizedContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSynchronizedContext.swift; sourceTree = ""; }; + 51168D95246E1DBF0002CC69 /* TestSimpleTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSimpleTaskView.swift; sourceTree = ""; }; + 51168D98246E20B40002CC69 /* TestNumericProgressTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNumericProgressTaskView.swift; sourceTree = ""; }; + 51168D9A246E211B0002CC69 /* TestInstructionsTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInstructionsTaskView.swift; sourceTree = ""; }; + 5123901A235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChartViewSynchronizerProtocol.swift; sourceTree = ""; }; + 5123901E2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskViewSynchronizerProtocol.swift; sourceTree = ""; }; + 5125A214248F2E00009C9643 /* OCKTaskEvents+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKTaskEvents+Extension.swift"; sourceTree = ""; }; + 512BF20E2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extensions.swift"; sourceTree = ""; }; + 513271EE234FA33D0025810A /* OCKTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskViewController.swift; sourceTree = ""; }; + 51355EDE24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSynchronizedTaskQuery.swift; sourceTree = ""; }; + 51355EE724997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSynchronizedContactQuery.swift; sourceTree = ""; }; + 513E721623553E7700AC9620 /* OCKChartController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChartController.swift; sourceTree = ""; }; + 514FDE382356362B0044E3B8 /* OCKCalendarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCalendarController.swift; sourceTree = ""; }; + 515C0A1523BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKTaskControllerProtocol+Extension.swift"; sourceTree = ""; }; + 515E17D12351262100637153 /* OCKTaskEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKTaskEvents.swift; sourceTree = ""; }; + 5161BEC12334530E001F03FC /* OCKGridTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKGridTaskView+Updatable.swift"; sourceTree = ""; }; + 516210102355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKContactViewSynchronizerProtocol.swift; sourceTree = ""; }; + 516433BF23539C3B00999B64 /* OCKSimpleTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleTaskViewController.swift; sourceTree = ""; }; + 516433C123539EC400999B64 /* OCKButtonLogTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKButtonLogTaskController.swift; sourceTree = ""; }; + 51676C0123556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCalendarViewSynchronizerProtocol.swift; sourceTree = ""; }; + 51676C0323556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarViewSynchronizer.swift; sourceTree = ""; }; + 51676C0623556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKWeekCalendarView+Updatable.swift"; sourceTree = ""; }; + 5167B9222334187A002BC69C /* OCKSimpleTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKSimpleTaskView+Updatable.swift"; sourceTree = ""; }; + 5167B924233418AA002BC69C /* Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updatable.swift; sourceTree = ""; }; + 5167B92623341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKInstructionsTaskView+Updatable.swift"; sourceTree = ""; }; + 5167B92823341C00002BC69C /* OCKLogTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKLogTaskView+Updatable.swift"; sourceTree = ""; }; + 5167B92E23343A64002BC69C /* OCKHeaderView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKHeaderView+Updatable.swift"; sourceTree = ""; }; + 5167B93123343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKChecklistTaskView+Updatable.swift"; sourceTree = ""; }; + 516CD5692354D370005E2779 /* OCKGridTaskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKGridTaskViewController.swift; sourceTree = ""; }; + 516D85DD24B822FA00489200 /* TestWeekCalendarPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWeekCalendarPageViewController.swift; sourceTree = ""; }; + 51757C792450CF690081F133 /* TestTaskEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTaskEvents.swift; sourceTree = ""; }; + 517C7849239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomTaskViewSynchronizer.swift; sourceTree = ""; }; + 517F3E4423345872004FE251 /* OCKCartesianChartView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKCartesianChartView+Updatable.swift"; sourceTree = ""; }; + 517F3E4723345A39004FE251 /* OCKSimpleContactView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKSimpleContactView+Updatable.swift"; sourceTree = ""; }; + 517F3E4923345BE9004FE251 /* OCKDetailedContactView+Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKDetailedContactView+Updatable.swift"; sourceTree = ""; }; + 518E153A237265930018541B /* TestSimpleContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSimpleContactViewSynchronizer.swift; sourceTree = ""; }; + 518E153F23726A690018541B /* TestDetailedContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDetailedContactViewSynchronizer.swift; sourceTree = ""; }; + 518E154123726F590018541B /* TestCustomContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomContactViewSynchronizer.swift; sourceTree = ""; }; + 518F3D2F23552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCartesianChartViewSynchronizer.swift; sourceTree = ""; }; + 518F3D3823553CB500E00902 /* OCKChartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChartViewController.swift; sourceTree = ""; }; + 5196C7F9226F8F8F00F1C2A2 /* CareKitTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CareKitTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5196C7FD226F8F8F00F1C2A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 51983EBB23563B0000329252 /* OCKCalendarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCalendarViewController.swift; sourceTree = ""; }; + 51983EC12356439B00329252 /* OCKWeekCalendarPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarPageViewController.swift; sourceTree = ""; }; + 5199906A2447E24F005CD581 /* NumericProgressTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericProgressTaskView.swift; sourceTree = ""; }; + 519990702448B864005CD581 /* TestNumericProgressTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNumericProgressTaskViewModel.swift; sourceTree = ""; }; + 51AA2303234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleContactViewSynchronizer.swift; sourceTree = ""; }; + 51AF514D24983B86005D385F /* LabeledValueTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledValueTaskView.swift; sourceTree = ""; }; + 51AF514F24983BE8005D385F /* OCKLabeledValueTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLabeledValueTaskController.swift; sourceTree = ""; }; + 51AF515124984163005D385F /* TestLabeledValueTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLabeledValueTaskView.swift; sourceTree = ""; }; + 51AF51532498420D005D385F /* TestLabeledValueTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLabeledValueTaskViewModel.swift; sourceTree = ""; }; + 51AF515524984ACA005D385F /* OCKOutcomeValue+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKOutcomeValue+Extension.swift"; sourceTree = ""; }; + 51AF515724984B6F005D385F /* Number+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Extension.swift"; sourceTree = ""; }; + 51B1E41A2348149D00AA77D0 /* OCKContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKContactController.swift; sourceTree = ""; }; + 51B1E41C234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailedContactViewSynchronizer.swift; sourceTree = ""; }; + 51B25DC223D28A870050205A /* CareKitStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 51B25DC323D28A870050205A /* CareKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 51B25DC923D28A900050205A /* CareKitStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 51B25DCA23D28A900050205A /* CareKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CareKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDailyTasksPageViewController.swift; sourceTree = ""; }; + 51C599A1246B64DF00880CE2 /* SynchronizedTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedTaskView.swift; sourceTree = ""; }; + 51CE62BF24523AD90027B7C1 /* TestTaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTaskController.swift; sourceTree = ""; }; + 51CFBD7D23DA1EE1007C0BA8 /* TestSimpleTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSimpleTaskViewModel.swift; sourceTree = ""; }; + 51CFBD8123DA2494007C0BA8 /* TestInstructionsTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInstructionsTaskViewModel.swift; sourceTree = ""; }; + 51D544902397557700898683 /* TestChecklistViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestChecklistViewSynchronizer.swift; sourceTree = ""; }; + 51D544922397584D00898683 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; + 51D5449423975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSimpleTaskViewSynchronizer.swift; sourceTree = ""; }; + 51D544962397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInstructionsTaskViewSynchronizer.swift; sourceTree = ""; }; + 51D54498239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestButtonLogTaskViewSynchronizer.swift; sourceTree = ""; }; + 51D8E27E24115D7D0026C716 /* TestListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestListView.swift; sourceTree = ""; }; + 51DAA14424A2C8DD008F6655 /* OCKNumericProgressTaskController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKNumericProgressTaskController.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 = ""; }; + 51EF7E49234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKButtonLogTaskViewSynchronizer.swift; sourceTree = ""; }; + 51F9F17323A9C1A50087C900 /* CareKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CareKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 51F9F18223A9C2F20087C900 /* InstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskView.swift; sourceTree = ""; }; + 51F9F18323A9C2F20087C900 /* SimpleTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleTaskView.swift; sourceTree = ""; }; + 51FF9B8D2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWeekCalendarViewSynchronizer.swift; sourceTree = ""; }; + 64EABD9C2321B1AF00CFBB9F /* OCKDailyPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDailyPageViewController.swift; sourceTree = ""; }; + 64EABD9D2321B1AF00CFBB9F /* OCKListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKListViewController.swift; sourceTree = ""; }; + 64EABD9E2321B1AF00CFBB9F /* OCKDailyTasksPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDailyTasksPageViewController.swift; sourceTree = ""; }; + 64EABD9F2321B1AF00CFBB9F /* OCKContactsListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKContactsListViewController.swift; sourceTree = ""; }; + 64EABDA12321B1AF00CFBB9F /* OCKListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKListView.swift; sourceTree = ""; }; + 64EABDA22321B1AF00CFBB9F /* OCKHeaderBodyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKHeaderBodyView.swift; sourceTree = ""; }; + 64EABDA52321B1AF00CFBB9F /* OCKDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDetailViewController.swift; sourceTree = ""; }; + 64EABDA72321B1AF00CFBB9F /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; + 64EABDA82321B1AF00CFBB9F /* OCKContact+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCKContact+Extensions.swift"; sourceTree = ""; }; + 64EABDBD2321B1AF00CFBB9F /* OCKStoreNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKStoreNotifications.swift; sourceTree = ""; }; + 64EABDBE2321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSynchronizedStoreManager.swift; sourceTree = ""; }; + 64EABDBF2321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCKSynchronizedStoreManager+Publishers.swift"; sourceTree = ""; }; + 64EABDC22321B1AF00CFBB9F /* OCKDataSeriesConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDataSeriesConfiguration.swift; sourceTree = ""; }; + 64EABDCA2321B1AF00CFBB9F /* OCKScheduleUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKScheduleUtility.swift; sourceTree = ""; }; + 64EABDCB2321B1AF00CFBB9F /* OCKContactUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKContactUtility.swift; sourceTree = ""; }; + 64EABDE52321B1AF00CFBB9F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 64EABDE82321B1AF00CFBB9F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/locversion.plist; sourceTree = ""; }; + 8605A5BA1C4F04EC00DD65FF /* CareKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CareKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 5196C7F6226F8F8F00F1C2A2 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5196C7FE226F8F8F00F1C2A2 /* CareKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51F9F17023A9C1A50087C900 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 51B25DCD23D28A900050205A /* CareKitUI.framework in Frameworks */, - 51B25DCB23D28A900050205A /* CareKitStore.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8605A5B61C4F04EC00DD65FF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 51B25DC623D28A870050205A /* CareKitUI.framework in Frameworks */, - 51B25DC423D28A870050205A /* CareKitStore.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 5196C7F6226F8F8F00F1C2A2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5196C7FE226F8F8F00F1C2A2 /* CareKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51F9F17023A9C1A50087C900 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 51B25DCD23D28A900050205A /* CareKitUI.framework in Frameworks */, + 51B25DCB23D28A900050205A /* CareKitStore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8605A5B61C4F04EC00DD65FF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 51B25DC623D28A870050205A /* CareKitUI.framework in Frameworks */, + 51B25DC423D28A870050205A /* CareKitStore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 510466FA24A286C400D0FD53 /* iOS */ = { - isa = PBXGroup; - children = ( - 64EABDE02321B1AF00CFBB9F /* Authentication */, - 64EABD9A2321B1AF00CFBB9F /* Higher Order */, - 64EABDA42321B1AF00CFBB9F /* Details */, - 510466FF24A2874600D0FD53 /* Task */, - 64EABDAB2321B1AF00CFBB9F /* Contact */, - 64EABDB22321B1AF00CFBB9F /* Calendar */, - 64EABDC02321B1AF00CFBB9F /* Chart */, - 5167B92123340BF9002BC69C /* View Updaters */, - 64EABDC92321B1AF00CFBB9F /* Utilities */, - 64EABDA62321B1AF00CFBB9F /* Extensions */, - 64EABDE62321B1AF00CFBB9F /* Localization */, - ); - path = iOS; - sourceTree = ""; - }; - 510466FC24A286CF00D0FD53 /* Shared */ = { - isa = PBXGroup; - children = ( - 0346743D2397026A0074891C /* OCKLog.swift */, - 64EABDBC2321B1AF00CFBB9F /* Synchronization */, - 510466FE24A2873A00D0FD53 /* Task */, - 510466FD24A2871800D0FD53 /* Extensions */, - 5104670024A28C7600D0FD53 /* Utilities */, - ); - path = Shared; - sourceTree = ""; - }; - 510466FD24A2871800D0FD53 /* Extensions */ = { - isa = PBXGroup; - children = ( - 515C0A1523BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift */, - 5125A214248F2E00009C9643 /* OCKTaskEvents+Extension.swift */, - 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */, - 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 510466FE24A2873A00D0FD53 /* Task */ = { - isa = PBXGroup; - children = ( - 51355EDE24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift */, - 51C599A1246B64DF00880CE2 /* SynchronizedTaskView.swift */, - 51F9F18323A9C2F20087C900 /* SimpleTaskView.swift */, - 51F9F18223A9C2F20087C900 /* InstructionsTaskView.swift */, - 51D5C97224A2953200EC45B5 /* Controller */, - ); - path = Task; - sourceTree = ""; - }; - 510466FF24A2874600D0FD53 /* Task */ = { - isa = PBXGroup; - children = ( - 5199906A2447E24F005CD581 /* NumericProgressTaskView.swift */, - 51AF514D24983B86005D385F /* LabeledValueTaskView.swift */, - 513271ED234FA3170025810A /* View Controllers */, - 510EA10E234EA3CD005D6793 /* Controllers */, - 510EA109234EA207005D6793 /* Synchronizers */, - ); - path = Task; - sourceTree = ""; - }; - 5104670024A28C7600D0FD53 /* Utilities */ = { - isa = PBXGroup; - children = ( - 64EABDCA2321B1AF00CFBB9F /* OCKScheduleUtility.swift */, - ); - path = Utilities; - sourceTree = ""; - }; - 5104670124A28D9400D0FD53 /* Higher Order */ = { - isa = PBXGroup; - children = ( - 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */, - 51D8E27E24115D7D0026C716 /* TestListView.swift */, - ); - path = "Higher Order"; - sourceTree = ""; - }; - 5104670224A28DB600D0FD53 /* Synchronization */ = { - isa = PBXGroup; - children = ( - 511372452374DFBD00831191 /* TestSynchronizedContext.swift */, - ); - path = Synchronization; - sourceTree = ""; - }; - 510591212378FEF3004EDC84 /* Task */ = { - isa = PBXGroup; - children = ( - 51757C792450CF690081F133 /* TestTaskEvents.swift */, - 5105912423790D06004EDC84 /* TestMockTaskEvents.swift */, - 510591222378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift */, - 51D544902397557700898683 /* TestChecklistViewSynchronizer.swift */, - 51D5449423975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift */, - 51D544962397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift */, - 51D54498239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift */, - 517C7849239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift */, - 5106616B2473590000C93CAB /* TestTaskViewController.swift */, - 51168D95246E1DBF0002CC69 /* TestSimpleTaskView.swift */, - 51168D98246E20B40002CC69 /* TestNumericProgressTaskView.swift */, - 51168D9A246E211B0002CC69 /* TestInstructionsTaskView.swift */, - 51AF515124984163005D385F /* TestLabeledValueTaskView.swift */, - 51CFBD7D23DA1EE1007C0BA8 /* TestSimpleTaskViewModel.swift */, - 51CFBD8123DA2494007C0BA8 /* TestInstructionsTaskViewModel.swift */, - 519990702448B864005CD581 /* TestNumericProgressTaskViewModel.swift */, - 51AF51532498420D005D385F /* TestLabeledValueTaskViewModel.swift */, - 51CE62BF24523AD90027B7C1 /* TestTaskController.swift */, - ); - path = Task; - sourceTree = ""; - }; - 510EA109234EA207005D6793 /* Synchronizers */ = { - isa = PBXGroup; - children = ( - 5123901E2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift */, - 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */, - 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */, - 51EF7E45234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift */, - 51EF7E47234FAB7A00B28C0A /* OCKInstructionsTaskViewSynchronizer.swift */, - 51EF7E49234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift */, - ); - path = Synchronizers; - sourceTree = ""; - }; - 510EA10E234EA3CD005D6793 /* Controllers */ = { - isa = PBXGroup; - children = ( - 5112731B235E58B2007B18DF /* OCKGridTaskController.swift */, - 5112731D235E58C4007B18DF /* OCKChecklistTaskController.swift */, - 516433C123539EC400999B64 /* OCKButtonLogTaskController.swift */, - 51AF514F24983BE8005D385F /* OCKLabeledValueTaskController.swift */, - 51DAA14424A2C8DD008F6655 /* OCKNumericProgressTaskController.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - 511372402374C9DB00831191 /* Chart */ = { - isa = PBXGroup; - children = ( - 511372412374CA1900831191 /* TestCartesianChartViewSynchronizer.swift */, - 511372432374CA2B00831191 /* TestCustomChartViewController.swift */, - ); - path = Chart; - sourceTree = ""; - }; - 513271ED234FA3170025810A /* View Controllers */ = { - isa = PBXGroup; - children = ( - 513271EE234FA33D0025810A /* OCKTaskViewController.swift */, - 516CD5692354D370005E2779 /* OCKGridTaskViewController.swift */, - 51127325235E5928007B18DF /* OCKChecklistTaskViewController.swift */, - 516433BF23539C3B00999B64 /* OCKSimpleTaskViewController.swift */, - 51127327235E597C007B18DF /* OCKInstructionsTaskViewController.swift */, - 51127329235E59AA007B18DF /* OCKButtonLogViewController.swift */, - ); - path = "View Controllers"; - sourceTree = ""; - }; - 51676C0023556B1B002C97E7 /* Synchronizers */ = { - isa = PBXGroup; - children = ( - 51676C0123556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift */, - 51676C0323556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift */, - ); - path = Synchronizers; - sourceTree = ""; - }; - 51676C0523556CDC002C97E7 /* Calendar */ = { - isa = PBXGroup; - children = ( - 51676C0623556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift */, - ); - path = Calendar; - sourceTree = ""; - }; - 51676C0823556F17002C97E7 /* Controllers */ = { - isa = PBXGroup; - children = ( - 514FDE382356362B0044E3B8 /* OCKCalendarController.swift */, - 51127315235E5728007B18DF /* OCKWeekCalendarController.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - 5167B92123340BF9002BC69C /* View Updaters */ = { - isa = PBXGroup; - children = ( - 5167B924233418AA002BC69C /* Updatable.swift */, - 5167B92E23343A64002BC69C /* OCKHeaderView+Updatable.swift */, - 5167B93023343CB6002BC69C /* Task */, - 517F3E4323345831004FE251 /* Chart */, - 517F3E4623345A28004FE251 /* Contact */, - 51676C0523556CDC002C97E7 /* Calendar */, - ); - path = "View Updaters"; - sourceTree = ""; - }; - 5167B93023343CB6002BC69C /* Task */ = { - isa = PBXGroup; - children = ( - 5167B9222334187A002BC69C /* OCKSimpleTaskView+Updatable.swift */, - 5167B92623341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift */, - 5167B92823341C00002BC69C /* OCKLogTaskView+Updatable.swift */, - 5167B93123343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift */, - 5161BEC12334530E001F03FC /* OCKGridTaskView+Updatable.swift */, - ); - path = Task; - sourceTree = ""; - }; - 517F3E4323345831004FE251 /* Chart */ = { - isa = PBXGroup; - children = ( - 517F3E4423345872004FE251 /* OCKCartesianChartView+Updatable.swift */, - ); - path = Chart; - sourceTree = ""; - }; - 517F3E4623345A28004FE251 /* Contact */ = { - isa = PBXGroup; - children = ( - 517F3E4723345A39004FE251 /* OCKSimpleContactView+Updatable.swift */, - 517F3E4923345BE9004FE251 /* OCKDetailedContactView+Updatable.swift */, - ); - path = Contact; - sourceTree = ""; - }; - 518653362356A41D00782F1D /* Paging */ = { - isa = PBXGroup; - children = ( - 51983EC12356439B00329252 /* OCKWeekCalendarPageViewController.swift */, - ); - path = Paging; - sourceTree = ""; - }; - 518E153C23726A270018541B /* Contact */ = { - isa = PBXGroup; - children = ( - 518E153A237265930018541B /* TestSimpleContactViewSynchronizer.swift */, - 518E153F23726A690018541B /* TestDetailedContactViewSynchronizer.swift */, - 518E154123726F590018541B /* TestCustomContactViewSynchronizer.swift */, - ); - path = Contact; - sourceTree = ""; - }; - 518F3D2E23552ADC00E00902 /* Synchronizers */ = { - isa = PBXGroup; - children = ( - 5123901A235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift */, - 518F3D2F23552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift */, - ); - path = Synchronizers; - sourceTree = ""; - }; - 518F3D3123552CEB00E00902 /* Controller */ = { - isa = PBXGroup; - children = ( - 64EABDC22321B1AF00CFBB9F /* OCKDataSeriesConfiguration.swift */, - 513E721623553E7700AC9620 /* OCKChartController.swift */, - 51127319235E588B007B18DF /* OCKCartesianChartController.swift */, - ); - path = Controller; - sourceTree = ""; - }; - 518F3D3723553C9F00E00902 /* View Controllers */ = { - isa = PBXGroup; - children = ( - 518F3D3823553CB500E00902 /* OCKChartViewController.swift */, - 51127317235E5849007B18DF /* OCKCartesianChartViewController.swift */, - ); - path = "View Controllers"; - sourceTree = ""; - }; - 5196C7FA226F8F8F00F1C2A2 /* CareKitTests */ = { - isa = PBXGroup; - children = ( - 05A6B74A237F43D2009D7D1F /* CareKit.xctestplan */, - 51D544922397584D00898683 /* Array+Extension.swift */, - 03530201248AC32E0073579D /* TestScheduleUtility.swift */, - 5104670124A28D9400D0FD53 /* Higher Order */, - 511372402374C9DB00831191 /* Chart */, - 510591212378FEF3004EDC84 /* Task */, - 518E153C23726A270018541B /* Contact */, - 51FF9B8C2373374200BAEDB2 /* Calendar */, - 5104670224A28DB600D0FD53 /* Synchronization */, - 5196C7FD226F8F8F00F1C2A2 /* Info.plist */, - ); - path = CareKitTests; - sourceTree = ""; - }; - 51983EBA23563AF000329252 /* View Controllers */ = { - isa = PBXGroup; - children = ( - 51983EBB23563B0000329252 /* OCKCalendarViewController.swift */, - 51127313235E3BF4007B18DF /* OCKWeekCalendarViewController.swift */, - ); - path = "View Controllers"; - sourceTree = ""; - }; - 51B1E41F2348275C00AA77D0 /* Controllers */ = { - isa = PBXGroup; - children = ( - 51B1E41A2348149D00AA77D0 /* OCKContactController.swift */, - 5112730F235E3B4E007B18DF /* OCKSimpleContactController.swift */, - 51127311235E3B62007B18DF /* OCKDetailedContactController.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - 51B1E4202348276A00AA77D0 /* View Controllers */ = { - isa = PBXGroup; - children = ( - 51355EE724997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift */, - 51E88827234CE61300763B97 /* OCKContactViewController.swift */, - 5112730B235E3AE3007B18DF /* OCKSimpleContactViewController.swift */, - 5112730D235E3B12007B18DF /* OCKDetailedContactViewController.swift */, - ); - path = "View Controllers"; - sourceTree = ""; - }; - 51B1E4212348277700AA77D0 /* Synchronizers */ = { - isa = PBXGroup; - children = ( - 516210102355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift */, - 51AA2303234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift */, - 51B1E41C234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift */, - ); - path = Synchronizers; - sourceTree = ""; - }; - 51B25DC123D28A870050205A /* Frameworks */ = { - isa = PBXGroup; - children = ( - 51B25DC923D28A900050205A /* CareKitStore.framework */, - 51B25DCA23D28A900050205A /* CareKitUI.framework */, - 51B25DC223D28A870050205A /* CareKitStore.framework */, - 51B25DC323D28A870050205A /* CareKitUI.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 51D5C97224A2953200EC45B5 /* Controller */ = { - isa = PBXGroup; - children = ( - 515E17D12351262100637153 /* OCKTaskEvents.swift */, - 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */, - 5112731F235E58D5007B18DF /* OCKSimpleTaskController.swift */, - 51127321235E58E8007B18DF /* OCKInstructionsTaskController.swift */, - ); - path = Controller; - sourceTree = ""; - }; - 51FF9B8C2373374200BAEDB2 /* Calendar */ = { - isa = PBXGroup; - children = ( - 51FF9B8D2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift */, - 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */, - ); - path = Calendar; - sourceTree = ""; - }; - 64EABD992321B1AF00CFBB9F /* CareKit */ = { - isa = PBXGroup; - children = ( - 510466FC24A286CF00D0FD53 /* Shared */, - 510466FA24A286C400D0FD53 /* iOS */, - 64EABDE52321B1AF00CFBB9F /* Info.plist */, - ); - path = CareKit; - sourceTree = ""; - }; - 64EABD9A2321B1AF00CFBB9F /* Higher Order */ = { - isa = PBXGroup; - children = ( - 64EABD9B2321B1AF00CFBB9F /* ViewController */, - 64EABDA02321B1AF00CFBB9F /* View */, - ); - path = "Higher Order"; - sourceTree = ""; - }; - 64EABD9B2321B1AF00CFBB9F /* ViewController */ = { - isa = PBXGroup; - children = ( - 64EABD9C2321B1AF00CFBB9F /* OCKDailyPageViewController.swift */, - 64EABD9D2321B1AF00CFBB9F /* OCKListViewController.swift */, - 64EABD9E2321B1AF00CFBB9F /* OCKDailyTasksPageViewController.swift */, - 64EABD9F2321B1AF00CFBB9F /* OCKContactsListViewController.swift */, - ); - path = ViewController; - sourceTree = ""; - }; - 64EABDA02321B1AF00CFBB9F /* View */ = { - isa = PBXGroup; - children = ( - 64EABDA12321B1AF00CFBB9F /* OCKListView.swift */, - 64EABDA22321B1AF00CFBB9F /* OCKHeaderBodyView.swift */, - ); - path = View; - sourceTree = ""; - }; - 64EABDA42321B1AF00CFBB9F /* Details */ = { - isa = PBXGroup; - children = ( - 64EABDA52321B1AF00CFBB9F /* OCKDetailViewController.swift */, - ); - path = Details; - sourceTree = ""; - }; - 64EABDA62321B1AF00CFBB9F /* Extensions */ = { - isa = PBXGroup; - children = ( - 512BF20E2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift */, - 64EABDA72321B1AF00CFBB9F /* UIViewController+Extensions.swift */, - 64EABDA82321B1AF00CFBB9F /* OCKContact+Extensions.swift */, - 51AF515524984ACA005D385F /* OCKOutcomeValue+Extension.swift */, - 51AF515724984B6F005D385F /* Number+Extension.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 64EABDAB2321B1AF00CFBB9F /* Contact */ = { - isa = PBXGroup; - children = ( - 51B1E4202348276A00AA77D0 /* View Controllers */, - 51B1E4212348277700AA77D0 /* Synchronizers */, - 51B1E41F2348275C00AA77D0 /* Controllers */, - ); - path = Contact; - sourceTree = ""; - }; - 64EABDB22321B1AF00CFBB9F /* Calendar */ = { - isa = PBXGroup; - children = ( - 51983EBA23563AF000329252 /* View Controllers */, - 51676C0823556F17002C97E7 /* Controllers */, - 51676C0023556B1B002C97E7 /* Synchronizers */, - 518653362356A41D00782F1D /* Paging */, - ); - path = Calendar; - sourceTree = ""; - }; - 64EABDBC2321B1AF00CFBB9F /* Synchronization */ = { - isa = PBXGroup; - children = ( - 51108C6423596BBD0029F7A2 /* OCKSynchronizationContext.swift */, - 64EABDBD2321B1AF00CFBB9F /* OCKStoreNotifications.swift */, - 64EABDBE2321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift */, - 64EABDBF2321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift */, - ); - path = Synchronization; - sourceTree = ""; - }; - 64EABDC02321B1AF00CFBB9F /* Chart */ = { - isa = PBXGroup; - children = ( - 518F3D3723553C9F00E00902 /* View Controllers */, - 518F3D3123552CEB00E00902 /* Controller */, - 518F3D2E23552ADC00E00902 /* Synchronizers */, - ); - path = Chart; - sourceTree = ""; - }; - 64EABDC92321B1AF00CFBB9F /* Utilities */ = { - isa = PBXGroup; - children = ( - 64EABDCB2321B1AF00CFBB9F /* OCKContactUtility.swift */, - ); - path = Utilities; - sourceTree = ""; - }; - 64EABDE02321B1AF00CFBB9F /* Authentication */ = { - isa = PBXGroup; - children = ( - ); - path = Authentication; - sourceTree = ""; - }; - 64EABDE62321B1AF00CFBB9F /* Localization */ = { - isa = PBXGroup; - children = ( - 64EABDE72321B1AF00CFBB9F /* locversion.plist */, - ); - path = Localization; - sourceTree = ""; - }; - 8605A5B01C4F04EC00DD65FF = { - isa = PBXGroup; - children = ( - 64EABD992321B1AF00CFBB9F /* CareKit */, - 5196C7FA226F8F8F00F1C2A2 /* CareKitTests */, - 8605A5BB1C4F04EC00DD65FF /* Products */, - 51B25DC123D28A870050205A /* Frameworks */, - ); - indentWidth = 4; - sourceTree = ""; - tabWidth = 4; - }; - 8605A5BB1C4F04EC00DD65FF /* Products */ = { - isa = PBXGroup; - children = ( - 8605A5BA1C4F04EC00DD65FF /* CareKit.framework */, - 5196C7F9226F8F8F00F1C2A2 /* CareKitTests iOS.xctest */, - 51F9F17323A9C1A50087C900 /* CareKit.framework */, - ); - name = Products; - sourceTree = ""; - }; + 510466FA24A286C400D0FD53 /* iOS */ = { + isa = PBXGroup; + children = ( + 64EABDE02321B1AF00CFBB9F /* Authentication */, + 64EABD9A2321B1AF00CFBB9F /* Higher Order */, + 64EABDA42321B1AF00CFBB9F /* Details */, + 510466FF24A2874600D0FD53 /* Task */, + 64EABDAB2321B1AF00CFBB9F /* Contact */, + 64EABDB22321B1AF00CFBB9F /* Calendar */, + 64EABDC02321B1AF00CFBB9F /* Chart */, + 5167B92123340BF9002BC69C /* View Updaters */, + 64EABDC92321B1AF00CFBB9F /* Utilities */, + 64EABDA62321B1AF00CFBB9F /* Extensions */, + 64EABDE62321B1AF00CFBB9F /* Localization */, + ); + path = iOS; + sourceTree = ""; + }; + 510466FC24A286CF00D0FD53 /* Shared */ = { + isa = PBXGroup; + children = ( + 0346743D2397026A0074891C /* OCKLog.swift */, + 64EABDBC2321B1AF00CFBB9F /* Synchronization */, + 510466FE24A2873A00D0FD53 /* Task */, + 510466FD24A2871800D0FD53 /* Extensions */, + 5104670024A28C7600D0FD53 /* Utilities */, + ); + path = Shared; + sourceTree = ""; + }; + 510466FD24A2871800D0FD53 /* Extensions */ = { + isa = PBXGroup; + children = ( + 515C0A1523BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift */, + 5125A214248F2E00009C9643 /* OCKTaskEvents+Extension.swift */, + 032C86EF2326B68D00D0A0EA /* Calendar+Extensions.swift */, + 510A6655236147FF00074275 /* OCKAnyEvent+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 510466FE24A2873A00D0FD53 /* Task */ = { + isa = PBXGroup; + children = ( + 51355EDE24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift */, + 51C599A1246B64DF00880CE2 /* SynchronizedTaskView.swift */, + 51F9F18323A9C2F20087C900 /* SimpleTaskView.swift */, + 51F9F18223A9C2F20087C900 /* InstructionsTaskView.swift */, + 51D5C97224A2953200EC45B5 /* Controller */, + ); + path = Task; + sourceTree = ""; + }; + 510466FF24A2874600D0FD53 /* Task */ = { + isa = PBXGroup; + children = ( + 5199906A2447E24F005CD581 /* NumericProgressTaskView.swift */, + 51AF514D24983B86005D385F /* LabeledValueTaskView.swift */, + 513271ED234FA3170025810A /* View Controllers */, + 510EA10E234EA3CD005D6793 /* Controllers */, + 510EA109234EA207005D6793 /* Synchronizers */, + ); + path = Task; + sourceTree = ""; + }; + 5104670024A28C7600D0FD53 /* Utilities */ = { + isa = PBXGroup; + children = ( + 64EABDCA2321B1AF00CFBB9F /* OCKScheduleUtility.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 5104670124A28D9400D0FD53 /* Higher Order */ = { + isa = PBXGroup; + children = ( + 51BEAD59237E00C600B32D55 /* TestDailyTasksPageViewController.swift */, + 51D8E27E24115D7D0026C716 /* TestListView.swift */, + 516D85DD24B822FA00489200 /* TestWeekCalendarPageViewController.swift */, + ); + path = "Higher Order"; + sourceTree = ""; + }; + 5104670224A28DB600D0FD53 /* Synchronization */ = { + isa = PBXGroup; + children = ( + 511372452374DFBD00831191 /* TestSynchronizedContext.swift */, + ); + path = Synchronization; + sourceTree = ""; + }; + 510591212378FEF3004EDC84 /* Task */ = { + isa = PBXGroup; + children = ( + 51757C792450CF690081F133 /* TestTaskEvents.swift */, + 5105912423790D06004EDC84 /* TestMockTaskEvents.swift */, + 510591222378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift */, + 51D544902397557700898683 /* TestChecklistViewSynchronizer.swift */, + 51D5449423975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift */, + 51D544962397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift */, + 51D54498239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift */, + 517C7849239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift */, + 5106616B2473590000C93CAB /* TestTaskViewController.swift */, + 51168D95246E1DBF0002CC69 /* TestSimpleTaskView.swift */, + 51168D98246E20B40002CC69 /* TestNumericProgressTaskView.swift */, + 51168D9A246E211B0002CC69 /* TestInstructionsTaskView.swift */, + 51AF515124984163005D385F /* TestLabeledValueTaskView.swift */, + 51CFBD7D23DA1EE1007C0BA8 /* TestSimpleTaskViewModel.swift */, + 51CFBD8123DA2494007C0BA8 /* TestInstructionsTaskViewModel.swift */, + 519990702448B864005CD581 /* TestNumericProgressTaskViewModel.swift */, + 51AF51532498420D005D385F /* TestLabeledValueTaskViewModel.swift */, + 51CE62BF24523AD90027B7C1 /* TestTaskController.swift */, + ); + path = Task; + sourceTree = ""; + }; + 510EA109234EA207005D6793 /* Synchronizers */ = { + isa = PBXGroup; + children = ( + 5123901E2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift */, + 510EA10A234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift */, + 510EA10C234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift */, + 51EF7E45234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift */, + 51EF7E47234FAB7A00B28C0A /* OCKInstructionsTaskViewSynchronizer.swift */, + 51EF7E49234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift */, + ); + path = Synchronizers; + sourceTree = ""; + }; + 510EA10E234EA3CD005D6793 /* Controllers */ = { + isa = PBXGroup; + children = ( + 5112731B235E58B2007B18DF /* OCKGridTaskController.swift */, + 5112731D235E58C4007B18DF /* OCKChecklistTaskController.swift */, + 516433C123539EC400999B64 /* OCKButtonLogTaskController.swift */, + 51AF514F24983BE8005D385F /* OCKLabeledValueTaskController.swift */, + 51DAA14424A2C8DD008F6655 /* OCKNumericProgressTaskController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 511372402374C9DB00831191 /* Chart */ = { + isa = PBXGroup; + children = ( + 511372412374CA1900831191 /* TestCartesianChartViewSynchronizer.swift */, + 511372432374CA2B00831191 /* TestCustomChartViewController.swift */, + ); + path = Chart; + sourceTree = ""; + }; + 513271ED234FA3170025810A /* View Controllers */ = { + isa = PBXGroup; + children = ( + 513271EE234FA33D0025810A /* OCKTaskViewController.swift */, + 516CD5692354D370005E2779 /* OCKGridTaskViewController.swift */, + 51127325235E5928007B18DF /* OCKChecklistTaskViewController.swift */, + 516433BF23539C3B00999B64 /* OCKSimpleTaskViewController.swift */, + 51127327235E597C007B18DF /* OCKInstructionsTaskViewController.swift */, + 51127329235E59AA007B18DF /* OCKButtonLogViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 51676C0023556B1B002C97E7 /* Synchronizers */ = { + isa = PBXGroup; + children = ( + 51676C0123556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift */, + 51676C0323556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift */, + ); + path = Synchronizers; + sourceTree = ""; + }; + 51676C0523556CDC002C97E7 /* Calendar */ = { + isa = PBXGroup; + children = ( + 51676C0623556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift */, + ); + path = Calendar; + sourceTree = ""; + }; + 51676C0823556F17002C97E7 /* Controllers */ = { + isa = PBXGroup; + children = ( + 514FDE382356362B0044E3B8 /* OCKCalendarController.swift */, + 51127315235E5728007B18DF /* OCKWeekCalendarController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 5167B92123340BF9002BC69C /* View Updaters */ = { + isa = PBXGroup; + children = ( + 5167B924233418AA002BC69C /* Updatable.swift */, + 5167B92E23343A64002BC69C /* OCKHeaderView+Updatable.swift */, + 5167B93023343CB6002BC69C /* Task */, + 517F3E4323345831004FE251 /* Chart */, + 517F3E4623345A28004FE251 /* Contact */, + 51676C0523556CDC002C97E7 /* Calendar */, + ); + path = "View Updaters"; + sourceTree = ""; + }; + 5167B93023343CB6002BC69C /* Task */ = { + isa = PBXGroup; + children = ( + 5167B9222334187A002BC69C /* OCKSimpleTaskView+Updatable.swift */, + 5167B92623341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift */, + 5167B92823341C00002BC69C /* OCKLogTaskView+Updatable.swift */, + 5167B93123343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift */, + 5161BEC12334530E001F03FC /* OCKGridTaskView+Updatable.swift */, + ); + path = Task; + sourceTree = ""; + }; + 517F3E4323345831004FE251 /* Chart */ = { + isa = PBXGroup; + children = ( + 517F3E4423345872004FE251 /* OCKCartesianChartView+Updatable.swift */, + ); + path = Chart; + sourceTree = ""; + }; + 517F3E4623345A28004FE251 /* Contact */ = { + isa = PBXGroup; + children = ( + 517F3E4723345A39004FE251 /* OCKSimpleContactView+Updatable.swift */, + 517F3E4923345BE9004FE251 /* OCKDetailedContactView+Updatable.swift */, + ); + path = Contact; + sourceTree = ""; + }; + 518653362356A41D00782F1D /* Paging */ = { + isa = PBXGroup; + children = ( + 51983EC12356439B00329252 /* OCKWeekCalendarPageViewController.swift */, + ); + path = Paging; + sourceTree = ""; + }; + 518E153C23726A270018541B /* Contact */ = { + isa = PBXGroup; + children = ( + 518E153A237265930018541B /* TestSimpleContactViewSynchronizer.swift */, + 518E153F23726A690018541B /* TestDetailedContactViewSynchronizer.swift */, + 518E154123726F590018541B /* TestCustomContactViewSynchronizer.swift */, + ); + path = Contact; + sourceTree = ""; + }; + 518F3D2E23552ADC00E00902 /* Synchronizers */ = { + isa = PBXGroup; + children = ( + 5123901A235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift */, + 518F3D2F23552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift */, + ); + path = Synchronizers; + sourceTree = ""; + }; + 518F3D3123552CEB00E00902 /* Controller */ = { + isa = PBXGroup; + children = ( + 64EABDC22321B1AF00CFBB9F /* OCKDataSeriesConfiguration.swift */, + 513E721623553E7700AC9620 /* OCKChartController.swift */, + 51127319235E588B007B18DF /* OCKCartesianChartController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 518F3D3723553C9F00E00902 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 518F3D3823553CB500E00902 /* OCKChartViewController.swift */, + 51127317235E5849007B18DF /* OCKCartesianChartViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 5196C7FA226F8F8F00F1C2A2 /* CareKitTests */ = { + isa = PBXGroup; + children = ( + 05A6B74A237F43D2009D7D1F /* CareKit.xctestplan */, + 51D544922397584D00898683 /* Array+Extension.swift */, + 03530201248AC32E0073579D /* TestScheduleUtility.swift */, + 5104670124A28D9400D0FD53 /* Higher Order */, + 511372402374C9DB00831191 /* Chart */, + 510591212378FEF3004EDC84 /* Task */, + 518E153C23726A270018541B /* Contact */, + 51FF9B8C2373374200BAEDB2 /* Calendar */, + 5104670224A28DB600D0FD53 /* Synchronization */, + 5196C7FD226F8F8F00F1C2A2 /* Info.plist */, + ); + path = CareKitTests; + sourceTree = ""; + }; + 51983EBA23563AF000329252 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 51983EBB23563B0000329252 /* OCKCalendarViewController.swift */, + 51127313235E3BF4007B18DF /* OCKWeekCalendarViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 51B1E41F2348275C00AA77D0 /* Controllers */ = { + isa = PBXGroup; + children = ( + 51B1E41A2348149D00AA77D0 /* OCKContactController.swift */, + 5112730F235E3B4E007B18DF /* OCKSimpleContactController.swift */, + 51127311235E3B62007B18DF /* OCKDetailedContactController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 51B1E4202348276A00AA77D0 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 51355EE724997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift */, + 51E88827234CE61300763B97 /* OCKContactViewController.swift */, + 5112730B235E3AE3007B18DF /* OCKSimpleContactViewController.swift */, + 5112730D235E3B12007B18DF /* OCKDetailedContactViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 51B1E4212348277700AA77D0 /* Synchronizers */ = { + isa = PBXGroup; + children = ( + 516210102355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift */, + 51AA2303234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift */, + 51B1E41C234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift */, + ); + path = Synchronizers; + sourceTree = ""; + }; + 51B25DC123D28A870050205A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 51B25DC923D28A900050205A /* CareKitStore.framework */, + 51B25DCA23D28A900050205A /* CareKitUI.framework */, + 51B25DC223D28A870050205A /* CareKitStore.framework */, + 51B25DC323D28A870050205A /* CareKitUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 51D5C97224A2953200EC45B5 /* Controller */ = { + isa = PBXGroup; + children = ( + 515E17D12351262100637153 /* OCKTaskEvents.swift */, + 51094D93234F8E3E00B4BFFB /* OCKTaskController.swift */, + 5112731F235E58D5007B18DF /* OCKSimpleTaskController.swift */, + 51127321235E58E8007B18DF /* OCKInstructionsTaskController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 51FF9B8C2373374200BAEDB2 /* Calendar */ = { + isa = PBXGroup; + children = ( + 51FF9B8D2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift */, + 5101E6CA23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift */, + ); + path = Calendar; + sourceTree = ""; + }; + 64EABD992321B1AF00CFBB9F /* CareKit */ = { + isa = PBXGroup; + children = ( + 510466FC24A286CF00D0FD53 /* Shared */, + 510466FA24A286C400D0FD53 /* iOS */, + 64EABDE52321B1AF00CFBB9F /* Info.plist */, + ); + path = CareKit; + sourceTree = ""; + }; + 64EABD9A2321B1AF00CFBB9F /* Higher Order */ = { + isa = PBXGroup; + children = ( + 64EABD9B2321B1AF00CFBB9F /* ViewController */, + 64EABDA02321B1AF00CFBB9F /* View */, + ); + path = "Higher Order"; + sourceTree = ""; + }; + 64EABD9B2321B1AF00CFBB9F /* ViewController */ = { + isa = PBXGroup; + children = ( + 64EABD9C2321B1AF00CFBB9F /* OCKDailyPageViewController.swift */, + 64EABD9D2321B1AF00CFBB9F /* OCKListViewController.swift */, + 64EABD9E2321B1AF00CFBB9F /* OCKDailyTasksPageViewController.swift */, + 64EABD9F2321B1AF00CFBB9F /* OCKContactsListViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + 64EABDA02321B1AF00CFBB9F /* View */ = { + isa = PBXGroup; + children = ( + 64EABDA12321B1AF00CFBB9F /* OCKListView.swift */, + 64EABDA22321B1AF00CFBB9F /* OCKHeaderBodyView.swift */, + ); + path = View; + sourceTree = ""; + }; + 64EABDA42321B1AF00CFBB9F /* Details */ = { + isa = PBXGroup; + children = ( + 64EABDA52321B1AF00CFBB9F /* OCKDetailViewController.swift */, + ); + path = Details; + sourceTree = ""; + }; + 64EABDA62321B1AF00CFBB9F /* Extensions */ = { + isa = PBXGroup; + children = ( + 512BF20E2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift */, + 64EABDA72321B1AF00CFBB9F /* UIViewController+Extensions.swift */, + 64EABDA82321B1AF00CFBB9F /* OCKContact+Extensions.swift */, + 51AF515524984ACA005D385F /* OCKOutcomeValue+Extension.swift */, + 51AF515724984B6F005D385F /* Number+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 64EABDAB2321B1AF00CFBB9F /* Contact */ = { + isa = PBXGroup; + children = ( + 51B1E4202348276A00AA77D0 /* View Controllers */, + 51B1E4212348277700AA77D0 /* Synchronizers */, + 51B1E41F2348275C00AA77D0 /* Controllers */, + ); + path = Contact; + sourceTree = ""; + }; + 64EABDB22321B1AF00CFBB9F /* Calendar */ = { + isa = PBXGroup; + children = ( + 51983EBA23563AF000329252 /* View Controllers */, + 51676C0823556F17002C97E7 /* Controllers */, + 51676C0023556B1B002C97E7 /* Synchronizers */, + 518653362356A41D00782F1D /* Paging */, + ); + path = Calendar; + sourceTree = ""; + }; + 64EABDBC2321B1AF00CFBB9F /* Synchronization */ = { + isa = PBXGroup; + children = ( + 51108C6423596BBD0029F7A2 /* OCKSynchronizationContext.swift */, + 64EABDBD2321B1AF00CFBB9F /* OCKStoreNotifications.swift */, + 64EABDBE2321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift */, + 64EABDBF2321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift */, + ); + path = Synchronization; + sourceTree = ""; + }; + 64EABDC02321B1AF00CFBB9F /* Chart */ = { + isa = PBXGroup; + children = ( + 518F3D3723553C9F00E00902 /* View Controllers */, + 518F3D3123552CEB00E00902 /* Controller */, + 518F3D2E23552ADC00E00902 /* Synchronizers */, + ); + path = Chart; + sourceTree = ""; + }; + 64EABDC92321B1AF00CFBB9F /* Utilities */ = { + isa = PBXGroup; + children = ( + 64EABDCB2321B1AF00CFBB9F /* OCKContactUtility.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 64EABDE02321B1AF00CFBB9F /* Authentication */ = { + isa = PBXGroup; + children = ( + ); + path = Authentication; + sourceTree = ""; + }; + 64EABDE62321B1AF00CFBB9F /* Localization */ = { + isa = PBXGroup; + children = ( + 64EABDE72321B1AF00CFBB9F /* locversion.plist */, + ); + path = Localization; + sourceTree = ""; + }; + 8605A5B01C4F04EC00DD65FF = { + isa = PBXGroup; + children = ( + 64EABD992321B1AF00CFBB9F /* CareKit */, + 5196C7FA226F8F8F00F1C2A2 /* CareKitTests */, + 8605A5BB1C4F04EC00DD65FF /* Products */, + 51B25DC123D28A870050205A /* Frameworks */, + ); + indentWidth = 4; + sourceTree = ""; + tabWidth = 4; + }; + 8605A5BB1C4F04EC00DD65FF /* Products */ = { + isa = PBXGroup; + children = ( + 8605A5BA1C4F04EC00DD65FF /* CareKit.framework */, + 5196C7F9226F8F8F00F1C2A2 /* CareKitTests iOS.xctest */, + 51F9F17323A9C1A50087C900 /* CareKit.framework */, + ); + name = Products; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 51F9F16E23A9C1A50087C900 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8605A5B71C4F04EC00DD65FF /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 51F9F16E23A9C1A50087C900 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8605A5B71C4F04EC00DD65FF /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 5196C7F8226F8F8F00F1C2A2 /* CareKitTests iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5196C804226F8F8F00F1C2A2 /* Build configuration list for PBXNativeTarget "CareKitTests iOS" */; - buildPhases = ( - 5196C7F5226F8F8F00F1C2A2 /* Sources */, - 5196C7F6226F8F8F00F1C2A2 /* Frameworks */, - 5196C7F7226F8F8F00F1C2A2 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 5196C800226F8F8F00F1C2A2 /* PBXTargetDependency */, - ); - name = "CareKitTests iOS"; - productName = CareKitTests; - productReference = 5196C7F9226F8F8F00F1C2A2 /* CareKitTests iOS.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 51F9F17223A9C1A50087C900 /* CareKit Watch */ = { - isa = PBXNativeTarget; - buildConfigurationList = 51F9F17823A9C1A50087C900 /* Build configuration list for PBXNativeTarget "CareKit Watch" */; - buildPhases = ( - 51F9F16E23A9C1A50087C900 /* Headers */, - 51F9F16F23A9C1A50087C900 /* Sources */, - 51F9F17023A9C1A50087C900 /* Frameworks */, - 51F9F17123A9C1A50087C900 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CareKit Watch"; - productName = "CareKit Watch"; - productReference = 51F9F17323A9C1A50087C900 /* CareKit.framework */; - productType = "com.apple.product-type.framework"; - }; - 8605A5B91C4F04EC00DD65FF /* CareKit iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 8605A5CE1C4F04EC00DD65FF /* Build configuration list for PBXNativeTarget "CareKit iOS" */; - buildPhases = ( - 8605A5B51C4F04EC00DD65FF /* Sources */, - 8605A5B71C4F04EC00DD65FF /* Headers */, - 8605A5B61C4F04EC00DD65FF /* Frameworks */, - 8605A5B81C4F04EC00DD65FF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CareKit iOS"; - productName = CareKit; - productReference = 8605A5BA1C4F04EC00DD65FF /* CareKit.framework */; - productType = "com.apple.product-type.framework"; - }; + 5196C7F8226F8F8F00F1C2A2 /* CareKitTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5196C804226F8F8F00F1C2A2 /* Build configuration list for PBXNativeTarget "CareKitTests iOS" */; + buildPhases = ( + 5196C7F5226F8F8F00F1C2A2 /* Sources */, + 5196C7F6226F8F8F00F1C2A2 /* Frameworks */, + 5196C7F7226F8F8F00F1C2A2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5196C800226F8F8F00F1C2A2 /* PBXTargetDependency */, + ); + name = "CareKitTests iOS"; + productName = CareKitTests; + productReference = 5196C7F9226F8F8F00F1C2A2 /* CareKitTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 51F9F17223A9C1A50087C900 /* CareKit Watch */ = { + isa = PBXNativeTarget; + buildConfigurationList = 51F9F17823A9C1A50087C900 /* Build configuration list for PBXNativeTarget "CareKit Watch" */; + buildPhases = ( + 51F9F16E23A9C1A50087C900 /* Headers */, + 51F9F16F23A9C1A50087C900 /* Sources */, + 51F9F17023A9C1A50087C900 /* Frameworks */, + 51F9F17123A9C1A50087C900 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "CareKit Watch"; + productName = "CareKit Watch"; + productReference = 51F9F17323A9C1A50087C900 /* CareKit.framework */; + productType = "com.apple.product-type.framework"; + }; + 8605A5B91C4F04EC00DD65FF /* CareKit iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8605A5CE1C4F04EC00DD65FF /* Build configuration list for PBXNativeTarget "CareKit iOS" */; + buildPhases = ( + 8605A5B51C4F04EC00DD65FF /* Sources */, + 8605A5B71C4F04EC00DD65FF /* Headers */, + 8605A5B61C4F04EC00DD65FF /* Frameworks */, + 8605A5B81C4F04EC00DD65FF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "CareKit iOS"; + productName = CareKit; + productReference = 8605A5BA1C4F04EC00DD65FF /* CareKit.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 8605A5B11C4F04EC00DD65FF /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1020; - LastUpgradeCheck = 1200; - ORGANIZATIONNAME = carekit.org; - TargetAttributes = { - 5196C7F8226F8F8F00F1C2A2 = { - CreatedOnToolsVersion = 10.2.1; - LastSwiftMigration = 1110; - ProvisioningStyle = Automatic; - }; - 51F9F17223A9C1A50087C900 = { - CreatedOnToolsVersion = 11.3; - ProvisioningStyle = Automatic; - }; - 8605A5B91C4F04EC00DD65FF = { - CreatedOnToolsVersion = 7.2; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = 8605A5B41C4F04EC00DD65FF /* Build configuration list for PBXProject "CareKit" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 8605A5B01C4F04EC00DD65FF; - productRefGroup = 8605A5BB1C4F04EC00DD65FF /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 8605A5B91C4F04EC00DD65FF /* CareKit iOS */, - 51F9F17223A9C1A50087C900 /* CareKit Watch */, - 5196C7F8226F8F8F00F1C2A2 /* CareKitTests iOS */, - ); - }; + 8605A5B11C4F04EC00DD65FF /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1020; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = carekit.org; + TargetAttributes = { + 5196C7F8226F8F8F00F1C2A2 = { + CreatedOnToolsVersion = 10.2.1; + LastSwiftMigration = 1110; + ProvisioningStyle = Automatic; + }; + 51F9F17223A9C1A50087C900 = { + CreatedOnToolsVersion = 11.3; + ProvisioningStyle = Automatic; + }; + 8605A5B91C4F04EC00DD65FF = { + CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 1020; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 8605A5B41C4F04EC00DD65FF /* Build configuration list for PBXProject "CareKit" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8605A5B01C4F04EC00DD65FF; + productRefGroup = 8605A5BB1C4F04EC00DD65FF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8605A5B91C4F04EC00DD65FF /* CareKit iOS */, + 51F9F17223A9C1A50087C900 /* CareKit Watch */, + 5196C7F8226F8F8F00F1C2A2 /* CareKitTests iOS */, + ); + }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 5196C7F7226F8F8F00F1C2A2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51F9F17123A9C1A50087C900 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8605A5B81C4F04EC00DD65FF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 64EABE202321B1AF00CFBB9F /* locversion.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 5196C7F7226F8F8F00F1C2A2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51F9F17123A9C1A50087C900 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8605A5B81C4F04EC00DD65FF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 64EABE202321B1AF00CFBB9F /* locversion.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 5196C7F5226F8F8F00F1C2A2 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 518E153B237265930018541B /* TestSimpleContactViewSynchronizer.swift in Sources */, - 51D544972397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift in Sources */, - 51AF515224984163005D385F /* TestLabeledValueTaskView.swift in Sources */, - 51CE62C024523AD90027B7C1 /* TestTaskController.swift in Sources */, - 51CFBD8423DA2499007C0BA8 /* TestInstructionsTaskViewModel.swift in Sources */, - 51D544912397557700898683 /* TestChecklistViewSynchronizer.swift in Sources */, - 518E154223726F590018541B /* TestCustomContactViewSynchronizer.swift in Sources */, - 518E154023726A690018541B /* TestDetailedContactViewSynchronizer.swift in Sources */, - 5101E6CB23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift in Sources */, - 51D5449523975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift in Sources */, - 03530202248AC32E0073579D /* TestScheduleUtility.swift in Sources */, - 511372442374CA2B00831191 /* TestCustomChartViewController.swift in Sources */, - 51168D9B246E211B0002CC69 /* TestInstructionsTaskView.swift in Sources */, - 511372422374CA1900831191 /* TestCartesianChartViewSynchronizer.swift in Sources */, - 51FF9B8E2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift in Sources */, - 51757C7B2450CF6E0081F133 /* TestTaskEvents.swift in Sources */, - 510591232378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift in Sources */, - 519990712448B864005CD581 /* TestNumericProgressTaskViewModel.swift in Sources */, - 517C784A239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift in Sources */, - 5105912523790D06004EDC84 /* TestMockTaskEvents.swift in Sources */, - 51CFBD8023DA20C5007C0BA8 /* TestSimpleTaskViewModel.swift in Sources */, - 51D54499239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift in Sources */, - 5106616D2473590C00C93CAB /* TestTaskViewController.swift in Sources */, - 51168D97246E1EFB0002CC69 /* TestSimpleTaskView.swift in Sources */, - 51D8E27F24115D7D0026C716 /* TestListView.swift in Sources */, - 51AF51542498420D005D385F /* TestLabeledValueTaskViewModel.swift in Sources */, - 511372462374DFBD00831191 /* TestSynchronizedContext.swift in Sources */, - 51168D99246E20B40002CC69 /* TestNumericProgressTaskView.swift in Sources */, - 51D544932397584D00898683 /* Array+Extension.swift in Sources */, - 51BEAD5A237E00C600B32D55 /* TestDailyTasksPageViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51F9F16F23A9C1A50087C900 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 51178E1723AAAD480068BAB1 /* Calendar+Extensions.swift in Sources */, - 51178E0E23AAAA2A0068BAB1 /* OCKStoreNotifications.swift in Sources */, - 51355EE024997212009DE0A4 /* OCKSynchronizedTaskQuery.swift in Sources */, - 51178E0523AA95270068BAB1 /* OCKInstructionsTaskController.swift in Sources */, - 51178E1123AAAACB0068BAB1 /* OCKAnyEvent+Extension.swift in Sources */, - 515862CB23A9C4D600630AB5 /* SimpleTaskView.swift in Sources */, - 51178E0F23AAAA2A0068BAB1 /* OCKSynchronizedStoreManager.swift in Sources */, - 51178E0D23AAAA2A0068BAB1 /* OCKSynchronizationContext.swift in Sources */, - 515862CA23A9C4D600630AB5 /* InstructionsTaskView.swift in Sources */, - 51178E0323AA95270068BAB1 /* OCKChecklistTaskController.swift in Sources */, - 51355EEA2499852D009DE0A4 /* OCKTaskEvents+Extension.swift in Sources */, - 51178E0123AA95270068BAB1 /* OCKTaskController.swift in Sources */, - 51178E1023AAAA2A0068BAB1 /* OCKSynchronizedStoreManager+Publishers.swift in Sources */, - 515C0A1723BF9D46009A9774 /* OCKTaskControllerProtocol+Extension.swift in Sources */, - 51C599A3246B64F200880CE2 /* SynchronizedTaskView.swift in Sources */, - 51757C812451136A0081F133 /* OCKLog.swift in Sources */, - 51178E0423AA95270068BAB1 /* OCKSimpleTaskController.swift in Sources */, - 51E76F2224004FA1008B09E7 /* OCKScheduleUtility.swift in Sources */, - 51178E0023AA95270068BAB1 /* OCKTaskEvents.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8605A5B51C4F04EC00DD65FF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5167B925233418AA002BC69C /* Updatable.swift in Sources */, - 51E88828234CE61300763B97 /* OCKContactViewController.swift in Sources */, - 510EA10B234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift in Sources */, - 5167B9232334187A002BC69C /* OCKSimpleTaskView+Updatable.swift in Sources */, - 5199906B2447E24F005CD581 /* NumericProgressTaskView.swift in Sources */, - 51676C0723556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift in Sources */, - 518F3D3923553CB500E00902 /* OCKChartViewController.swift in Sources */, - 51127312235E3B62007B18DF /* OCKDetailedContactController.swift in Sources */, - 5167B92923341C00002BC69C /* OCKLogTaskView+Updatable.swift in Sources */, - 517F3E4823345A39004FE251 /* OCKSimpleContactView+Updatable.swift in Sources */, - 516433C023539C3B00999B64 /* OCKSimpleTaskViewController.swift in Sources */, - 510EA10D234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift in Sources */, - 032C86F02326B68D00D0A0EA /* Calendar+Extensions.swift in Sources */, - 51127318235E5849007B18DF /* OCKCartesianChartViewController.swift in Sources */, - 517F3E4523345872004FE251 /* OCKCartesianChartView+Updatable.swift in Sources */, - 5112730E235E3B12007B18DF /* OCKDetailedContactViewController.swift in Sources */, - 51AF514E24983B86005D385F /* LabeledValueTaskView.swift in Sources */, - 515E17D22351262100637153 /* OCKTaskEvents.swift in Sources */, - 5167B92F23343A64002BC69C /* OCKHeaderView+Updatable.swift in Sources */, - 5112731A235E588B007B18DF /* OCKCartesianChartController.swift in Sources */, - 0346743E2397026A0074891C /* OCKLog.swift in Sources */, - 51EF7E48234FAB7A00B28C0A /* OCKInstructionsTaskViewSynchronizer.swift in Sources */, - 51B1E41B2348149D00AA77D0 /* OCKContactController.swift in Sources */, - 513E721723553E7700AC9620 /* OCKChartController.swift in Sources */, - 51676C0423556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift in Sources */, - 5167B92723341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift in Sources */, - 51A52A0923566E5D0056150D /* OCKContactsListViewController.swift in Sources */, - 518F3D362355317B00E00902 /* OCKDataSeriesConfiguration.swift in Sources */, - 51127322235E58E8007B18DF /* OCKInstructionsTaskController.swift in Sources */, - 64EABDF12321B1AF00CFBB9F /* UIViewController+Extensions.swift in Sources */, - 51108C6523596BBD0029F7A2 /* OCKSynchronizationContext.swift in Sources */, - 51127320235E58D5007B18DF /* OCKSimpleTaskController.swift in Sources */, - 51AA2304234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift in Sources */, - 64EABE002321B1AF00CFBB9F /* OCKStoreNotifications.swift in Sources */, - 51AF515024983BE8005D385F /* OCKLabeledValueTaskController.swift in Sources */, - 51AF515824984B6F005D385F /* Number+Extension.swift in Sources */, - 5183EA8C2353BC7600C46113 /* OCKScheduleUtility.swift in Sources */, - 510A6656236147FF00074275 /* OCKAnyEvent+Extension.swift in Sources */, - 517F3E4A23345BE9004FE251 /* OCKDetailedContactView+Updatable.swift in Sources */, - 5112731C235E58B2007B18DF /* OCKGridTaskController.swift in Sources */, - 512BF20F2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift in Sources */, - 516433C223539EC400999B64 /* OCKButtonLogTaskController.swift in Sources */, - 5123901F2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift in Sources */, - 64EABE0B2321B1AF00CFBB9F /* OCKContactUtility.swift in Sources */, - 51F9F18823A9C2F20087C900 /* SimpleTaskView.swift in Sources */, - 515C0A1623BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift in Sources */, - 51983EBC23563B0000329252 /* OCKCalendarViewController.swift in Sources */, - 5123901B235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift in Sources */, - 5112731E235E58C4007B18DF /* OCKChecklistTaskController.swift in Sources */, - 5125A215248F2E00009C9643 /* OCKTaskEvents+Extension.swift in Sources */, - 518F3D3023552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift in Sources */, - 51127314235E3BF4007B18DF /* OCKWeekCalendarViewController.swift in Sources */, - 64EABDF22321B1AF00CFBB9F /* OCKContact+Extensions.swift in Sources */, - 5167B93223343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift in Sources */, - 51B1E41D234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift in Sources */, - 51127316235E5728007B18DF /* OCKWeekCalendarController.swift in Sources */, - 516CD56A2354D370005E2779 /* OCKGridTaskViewController.swift in Sources */, - 51C599A2246B64DF00880CE2 /* SynchronizedTaskView.swift in Sources */, - 51355EE824997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift in Sources */, - 64EABE022321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift in Sources */, - 51AF515624984ACA005D385F /* OCKOutcomeValue+Extension.swift in Sources */, - 513271EF234FA33D0025810A /* OCKTaskViewController.swift in Sources */, - 51676C0223556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift in Sources */, - 5112732A235E59AA007B18DF /* OCKButtonLogViewController.swift in Sources */, - 64EABDED2321B1AF00CFBB9F /* OCKListView.swift in Sources */, - 51EF7E4A234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift in Sources */, - 64EABDEE2321B1AF00CFBB9F /* OCKHeaderBodyView.swift in Sources */, - 51F9F18723A9C2F20087C900 /* InstructionsTaskView.swift in Sources */, - 51A52A0823566D240056150D /* OCKDailyTasksPageViewController.swift in Sources */, - 51094D94234F8E3E00B4BFFB /* OCKTaskController.swift in Sources */, - 64EABDEA2321B1AF00CFBB9F /* OCKListViewController.swift in Sources */, - 64EABE012321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift in Sources */, - 51DAA14524A2C8DD008F6655 /* OCKNumericProgressTaskController.swift in Sources */, - 514FDE392356362B0044E3B8 /* OCKCalendarController.swift in Sources */, - 51127310235E3B4E007B18DF /* OCKSimpleContactController.swift in Sources */, - 51355EDF24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift in Sources */, - 516210112355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift in Sources */, - 51A52A05235669230056150D /* OCKWeekCalendarPageViewController.swift in Sources */, - 5161BEC22334530E001F03FC /* OCKGridTaskView+Updatable.swift in Sources */, - 51692D9D23564C2C00D03A44 /* OCKDailyPageViewController.swift in Sources */, - 51127326235E5928007B18DF /* OCKChecklistTaskViewController.swift in Sources */, - 5112730C235E3AE3007B18DF /* OCKSimpleContactViewController.swift in Sources */, - 64EABDF02321B1AF00CFBB9F /* OCKDetailViewController.swift in Sources */, - 51127328235E597C007B18DF /* OCKInstructionsTaskViewController.swift in Sources */, - 51EF7E46234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 5196C7F5226F8F8F00F1C2A2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 518E153B237265930018541B /* TestSimpleContactViewSynchronizer.swift in Sources */, + 51D544972397642B00898683 /* TestInstructionsTaskViewSynchronizer.swift in Sources */, + 51AF515224984163005D385F /* TestLabeledValueTaskView.swift in Sources */, + 51CE62C024523AD90027B7C1 /* TestTaskController.swift in Sources */, + 51CFBD8423DA2499007C0BA8 /* TestInstructionsTaskViewModel.swift in Sources */, + 51D544912397557700898683 /* TestChecklistViewSynchronizer.swift in Sources */, + 518E154223726F590018541B /* TestCustomContactViewSynchronizer.swift in Sources */, + 518E154023726A690018541B /* TestDetailedContactViewSynchronizer.swift in Sources */, + 5101E6CB23733F3B0023B8A6 /* TestCustomCalendarViewSynchronizer.swift in Sources */, + 51D5449523975DCD00898683 /* TestSimpleTaskViewSynchronizer.swift in Sources */, + 03530202248AC32E0073579D /* TestScheduleUtility.swift in Sources */, + 511372442374CA2B00831191 /* TestCustomChartViewController.swift in Sources */, + 51168D9B246E211B0002CC69 /* TestInstructionsTaskView.swift in Sources */, + 511372422374CA1900831191 /* TestCartesianChartViewSynchronizer.swift in Sources */, + 51FF9B8E2373377500BAEDB2 /* TestWeekCalendarViewSynchronizer.swift in Sources */, + 51757C7B2450CF6E0081F133 /* TestTaskEvents.swift in Sources */, + 510591232378FF24004EDC84 /* TestGridTaskViewSynchronizer.swift in Sources */, + 519990712448B864005CD581 /* TestNumericProgressTaskViewModel.swift in Sources */, + 517C784A239860C1005B2549 /* TestCustomTaskViewSynchronizer.swift in Sources */, + 5105912523790D06004EDC84 /* TestMockTaskEvents.swift in Sources */, + 51CFBD8023DA20C5007C0BA8 /* TestSimpleTaskViewModel.swift in Sources */, + 51D54499239767AE00898683 /* TestButtonLogTaskViewSynchronizer.swift in Sources */, + 5106616D2473590C00C93CAB /* TestTaskViewController.swift in Sources */, + 51168D97246E1EFB0002CC69 /* TestSimpleTaskView.swift in Sources */, + 51D8E27F24115D7D0026C716 /* TestListView.swift in Sources */, + 51AF51542498420D005D385F /* TestLabeledValueTaskViewModel.swift in Sources */, + 511372462374DFBD00831191 /* TestSynchronizedContext.swift in Sources */, + 51168D99246E20B40002CC69 /* TestNumericProgressTaskView.swift in Sources */, + 516D85DE24B822FA00489200 /* TestWeekCalendarPageViewController.swift in Sources */, + 51D544932397584D00898683 /* Array+Extension.swift in Sources */, + 51BEAD5A237E00C600B32D55 /* TestDailyTasksPageViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51F9F16F23A9C1A50087C900 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 51178E1723AAAD480068BAB1 /* Calendar+Extensions.swift in Sources */, + 51178E0E23AAAA2A0068BAB1 /* OCKStoreNotifications.swift in Sources */, + 51355EE024997212009DE0A4 /* OCKSynchronizedTaskQuery.swift in Sources */, + 51178E0523AA95270068BAB1 /* OCKInstructionsTaskController.swift in Sources */, + 51178E1123AAAACB0068BAB1 /* OCKAnyEvent+Extension.swift in Sources */, + 515862CB23A9C4D600630AB5 /* SimpleTaskView.swift in Sources */, + 51178E0F23AAAA2A0068BAB1 /* OCKSynchronizedStoreManager.swift in Sources */, + 51178E0D23AAAA2A0068BAB1 /* OCKSynchronizationContext.swift in Sources */, + 515862CA23A9C4D600630AB5 /* InstructionsTaskView.swift in Sources */, + 51178E0323AA95270068BAB1 /* OCKChecklistTaskController.swift in Sources */, + 51355EEA2499852D009DE0A4 /* OCKTaskEvents+Extension.swift in Sources */, + 51178E0123AA95270068BAB1 /* OCKTaskController.swift in Sources */, + 51178E1023AAAA2A0068BAB1 /* OCKSynchronizedStoreManager+Publishers.swift in Sources */, + 515C0A1723BF9D46009A9774 /* OCKTaskControllerProtocol+Extension.swift in Sources */, + 51C599A3246B64F200880CE2 /* SynchronizedTaskView.swift in Sources */, + 51757C812451136A0081F133 /* OCKLog.swift in Sources */, + 51178E0423AA95270068BAB1 /* OCKSimpleTaskController.swift in Sources */, + 51E76F2224004FA1008B09E7 /* OCKScheduleUtility.swift in Sources */, + 51178E0023AA95270068BAB1 /* OCKTaskEvents.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8605A5B51C4F04EC00DD65FF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5167B925233418AA002BC69C /* Updatable.swift in Sources */, + 51E88828234CE61300763B97 /* OCKContactViewController.swift in Sources */, + 510EA10B234EA218005D6793 /* OCKGridTaskViewSynchronizer.swift in Sources */, + 5167B9232334187A002BC69C /* OCKSimpleTaskView+Updatable.swift in Sources */, + 5199906B2447E24F005CD581 /* NumericProgressTaskView.swift in Sources */, + 51676C0723556D38002C97E7 /* OCKWeekCalendarView+Updatable.swift in Sources */, + 518F3D3923553CB500E00902 /* OCKChartViewController.swift in Sources */, + 51127312235E3B62007B18DF /* OCKDetailedContactController.swift in Sources */, + 5167B92923341C00002BC69C /* OCKLogTaskView+Updatable.swift in Sources */, + 517F3E4823345A39004FE251 /* OCKSimpleContactView+Updatable.swift in Sources */, + 516433C023539C3B00999B64 /* OCKSimpleTaskViewController.swift in Sources */, + 510EA10D234EA381005D6793 /* OCKChecklistTaskViewSynchronizer.swift in Sources */, + 032C86F02326B68D00D0A0EA /* Calendar+Extensions.swift in Sources */, + 51127318235E5849007B18DF /* OCKCartesianChartViewController.swift in Sources */, + 517F3E4523345872004FE251 /* OCKCartesianChartView+Updatable.swift in Sources */, + 5112730E235E3B12007B18DF /* OCKDetailedContactViewController.swift in Sources */, + 51AF514E24983B86005D385F /* LabeledValueTaskView.swift in Sources */, + 515E17D22351262100637153 /* OCKTaskEvents.swift in Sources */, + 5167B92F23343A64002BC69C /* OCKHeaderView+Updatable.swift in Sources */, + 5112731A235E588B007B18DF /* OCKCartesianChartController.swift in Sources */, + 0346743E2397026A0074891C /* OCKLog.swift in Sources */, + 51EF7E48234FAB7A00B28C0A /* OCKInstructionsTaskViewSynchronizer.swift in Sources */, + 51B1E41B2348149D00AA77D0 /* OCKContactController.swift in Sources */, + 513E721723553E7700AC9620 /* OCKChartController.swift in Sources */, + 51676C0423556C27002C97E7 /* OCKWeekCalendarViewSynchronizer.swift in Sources */, + 5167B92723341AB3002BC69C /* OCKInstructionsTaskView+Updatable.swift in Sources */, + 51A52A0923566E5D0056150D /* OCKContactsListViewController.swift in Sources */, + 518F3D362355317B00E00902 /* OCKDataSeriesConfiguration.swift in Sources */, + 51127322235E58E8007B18DF /* OCKInstructionsTaskController.swift in Sources */, + 64EABDF12321B1AF00CFBB9F /* UIViewController+Extensions.swift in Sources */, + 51108C6523596BBD0029F7A2 /* OCKSynchronizationContext.swift in Sources */, + 51127320235E58D5007B18DF /* OCKSimpleTaskController.swift in Sources */, + 51AA2304234E687B00A90DA2 /* OCKSimpleContactViewSynchronizer.swift in Sources */, + 64EABE002321B1AF00CFBB9F /* OCKStoreNotifications.swift in Sources */, + 51AF515024983BE8005D385F /* OCKLabeledValueTaskController.swift in Sources */, + 51AF515824984B6F005D385F /* Number+Extension.swift in Sources */, + 5183EA8C2353BC7600C46113 /* OCKScheduleUtility.swift in Sources */, + 510A6656236147FF00074275 /* OCKAnyEvent+Extension.swift in Sources */, + 517F3E4A23345BE9004FE251 /* OCKDetailedContactView+Updatable.swift in Sources */, + 5112731C235E58B2007B18DF /* OCKGridTaskController.swift in Sources */, + 512BF20F2326FCDE00BF672D /* NSLayoutConstraint+Extensions.swift in Sources */, + 516433C223539EC400999B64 /* OCKButtonLogTaskController.swift in Sources */, + 5123901F2355573300BF2674 /* OCKTaskViewSynchronizerProtocol.swift in Sources */, + 64EABE0B2321B1AF00CFBB9F /* OCKContactUtility.swift in Sources */, + 51F9F18823A9C2F20087C900 /* SimpleTaskView.swift in Sources */, + 515C0A1623BF9D3C009A9774 /* OCKTaskControllerProtocol+Extension.swift in Sources */, + 51983EBC23563B0000329252 /* OCKCalendarViewController.swift in Sources */, + 5123901B235551FA00BF2674 /* OCKChartViewSynchronizerProtocol.swift in Sources */, + 5112731E235E58C4007B18DF /* OCKChecklistTaskController.swift in Sources */, + 5125A215248F2E00009C9643 /* OCKTaskEvents+Extension.swift in Sources */, + 518F3D3023552B0400E00902 /* OCKCartesianChartViewSynchronizer.swift in Sources */, + 51127314235E3BF4007B18DF /* OCKWeekCalendarViewController.swift in Sources */, + 64EABDF22321B1AF00CFBB9F /* OCKContact+Extensions.swift in Sources */, + 5167B93223343CE0002BC69C /* OCKChecklistTaskView+Updatable.swift in Sources */, + 51B1E41D234825A100AA77D0 /* OCKDetailedContactViewSynchronizer.swift in Sources */, + 51127316235E5728007B18DF /* OCKWeekCalendarController.swift in Sources */, + 516CD56A2354D370005E2779 /* OCKGridTaskViewController.swift in Sources */, + 51C599A2246B64DF00880CE2 /* SynchronizedTaskView.swift in Sources */, + 51355EE824997FB8009DE0A4 /* OCKSynchronizedContactQuery.swift in Sources */, + 64EABE022321B1AF00CFBB9F /* OCKSynchronizedStoreManager+Publishers.swift in Sources */, + 51AF515624984ACA005D385F /* OCKOutcomeValue+Extension.swift in Sources */, + 513271EF234FA33D0025810A /* OCKTaskViewController.swift in Sources */, + 51676C0223556B34002C97E7 /* OCKCalendarViewSynchronizerProtocol.swift in Sources */, + 5112732A235E59AA007B18DF /* OCKButtonLogViewController.swift in Sources */, + 64EABDED2321B1AF00CFBB9F /* OCKListView.swift in Sources */, + 51EF7E4A234FABA800B28C0A /* OCKButtonLogTaskViewSynchronizer.swift in Sources */, + 64EABDEE2321B1AF00CFBB9F /* OCKHeaderBodyView.swift in Sources */, + 51F9F18723A9C2F20087C900 /* InstructionsTaskView.swift in Sources */, + 51A52A0823566D240056150D /* OCKDailyTasksPageViewController.swift in Sources */, + 51094D94234F8E3E00B4BFFB /* OCKTaskController.swift in Sources */, + 64EABDEA2321B1AF00CFBB9F /* OCKListViewController.swift in Sources */, + 64EABE012321B1AF00CFBB9F /* OCKSynchronizedStoreManager.swift in Sources */, + 51DAA14524A2C8DD008F6655 /* OCKNumericProgressTaskController.swift in Sources */, + 514FDE392356362B0044E3B8 /* OCKCalendarController.swift in Sources */, + 51127310235E3B4E007B18DF /* OCKSimpleContactController.swift in Sources */, + 51355EDF24997212009DE0A4 /* OCKSynchronizedTaskQuery.swift in Sources */, + 516210112355505300B7D012 /* OCKContactViewSynchronizerProtocol.swift in Sources */, + 51A52A05235669230056150D /* OCKWeekCalendarPageViewController.swift in Sources */, + 5161BEC22334530E001F03FC /* OCKGridTaskView+Updatable.swift in Sources */, + 51692D9D23564C2C00D03A44 /* OCKDailyPageViewController.swift in Sources */, + 51127326235E5928007B18DF /* OCKChecklistTaskViewController.swift in Sources */, + 5112730C235E3AE3007B18DF /* OCKSimpleContactViewController.swift in Sources */, + 64EABDF02321B1AF00CFBB9F /* OCKDetailViewController.swift in Sources */, + 51127328235E597C007B18DF /* OCKInstructionsTaskViewController.swift in Sources */, + 51EF7E46234FAAB700B28C0A /* OCKSimpleTaskViewSynchronizer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 5196C800226F8F8F00F1C2A2 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 8605A5B91C4F04EC00DD65FF /* CareKit iOS */; - targetProxy = 5196C7FF226F8F8F00F1C2A2 /* PBXContainerItemProxy */; - }; + 5196C800226F8F8F00F1C2A2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8605A5B91C4F04EC00DD65FF /* CareKit iOS */; + targetProxy = 5196C7FF226F8F8F00F1C2A2 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 64EABDE72321B1AF00CFBB9F /* locversion.plist */ = { - isa = PBXVariantGroup; - children = ( - 64EABDE82321B1AF00CFBB9F /* en */, - ); - name = locversion.plist; - sourceTree = ""; - }; + 64EABDE72321B1AF00CFBB9F /* locversion.plist */ = { + isa = PBXVariantGroup; + children = ( + 64EABDE82321B1AF00CFBB9F /* en */, + ); + name = locversion.plist; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 5196C801226F8F8F00F1C2A2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = CareKitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = Apple.CareKitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 5196C802226F8F8F00F1C2A2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = CareKitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = Apple.CareKitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 51F9F17923A9C1A50087C900 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEBUG_INFORMATION_FORMAT = dwarf; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = CareKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.carekit.CareKit-Watch"; - PRODUCT_NAME = "$(PROJECT_NAME)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.1; - }; - name = Debug; - }; - 51F9F17A23A9C1A50087C900 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = CareKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.carekit.CareKit-Watch"; - PRODUCT_NAME = "$(PROJECT_NAME)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.1; - }; - name = Release; - }; - 8605A5CC1C4F04EC00DD65FF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; - CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; - CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; - CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; - CLANG_WARN_ASSIGN_ENUM = 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_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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_MISSING_NEWLINE = YES; - GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_SHADOW = YES; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNKNOWN_PRAGMAS = YES; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_LABEL = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - RUN_CLANG_STATIC_ANALYZER = YES; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_COMPILATION_MODE = singlefile; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 2.1; - }; - name = Debug; - }; - 8605A5CD1C4F04EC00DD65FF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; - CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; - CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; - CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; - CLANG_WARN_ASSIGN_ENUM = 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_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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_MISSING_NEWLINE = YES; - GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_SHADOW = YES; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNKNOWN_PRAGMAS = YES; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_LABEL = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - ONLY_ACTIVE_ARCH = NO; - RUN_CLANG_STATIC_ANALYZER = YES; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 8605A5CF1C4F04EC00DD65FF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_BITCODE = YES; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - INFOPLIST_FILE = CareKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.carekit.CareKit; - PRODUCT_NAME = "$(PROJECT_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; - "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = DEBUG; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Debug; - }; - 8605A5D01C4F04EC00DD65FF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_BITCODE = YES; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - INFOPLIST_FILE = CareKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.carekit.CareKit; - PRODUCT_NAME = "$(PROJECT_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Release; - }; + 5196C801226F8F8F00F1C2A2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = CareKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = Apple.CareKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5196C802226F8F8F00F1C2A2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = CareKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = Apple.CareKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 51F9F17923A9C1A50087C900 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = CareKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "org.carekit.CareKit-Watch"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 6.1; + }; + name = Debug; + }; + 51F9F17A23A9C1A50087C900 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = CareKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "org.carekit.CareKit-Watch"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 6.1; + }; + name = Release; + }; + 8605A5CC1C4F04EC00DD65FF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; + CLANG_WARN_ASSIGN_ENUM = 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_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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + RUN_CLANG_STATIC_ANALYZER = YES; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = singlefile; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 2.1; + }; + name = Debug; + }; + 8605A5CD1C4F04EC00DD65FF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; + CLANG_WARN_ASSIGN_ENUM = 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_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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + RUN_CLANG_STATIC_ANALYZER = YES; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 8605A5CF1C4F04EC00DD65FF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + INFOPLIST_FILE = CareKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.carekit.CareKit; + PRODUCT_NAME = "$(PROJECT_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = DEBUG; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 8605A5D01C4F04EC00DD65FF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + INFOPLIST_FILE = CareKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.carekit.CareKit; + PRODUCT_NAME = "$(PROJECT_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 5196C804226F8F8F00F1C2A2 /* Build configuration list for PBXNativeTarget "CareKitTests iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5196C801226F8F8F00F1C2A2 /* Debug */, - 5196C802226F8F8F00F1C2A2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 51F9F17823A9C1A50087C900 /* Build configuration list for PBXNativeTarget "CareKit Watch" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 51F9F17923A9C1A50087C900 /* Debug */, - 51F9F17A23A9C1A50087C900 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 8605A5B41C4F04EC00DD65FF /* Build configuration list for PBXProject "CareKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8605A5CC1C4F04EC00DD65FF /* Debug */, - 8605A5CD1C4F04EC00DD65FF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 8605A5CE1C4F04EC00DD65FF /* Build configuration list for PBXNativeTarget "CareKit iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8605A5CF1C4F04EC00DD65FF /* Debug */, - 8605A5D01C4F04EC00DD65FF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; + 5196C804226F8F8F00F1C2A2 /* Build configuration list for PBXNativeTarget "CareKitTests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5196C801226F8F8F00F1C2A2 /* Debug */, + 5196C802226F8F8F00F1C2A2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 51F9F17823A9C1A50087C900 /* Build configuration list for PBXNativeTarget "CareKit Watch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 51F9F17923A9C1A50087C900 /* Debug */, + 51F9F17A23A9C1A50087C900 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8605A5B41C4F04EC00DD65FF /* Build configuration list for PBXProject "CareKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8605A5CC1C4F04EC00DD65FF /* Debug */, + 8605A5CD1C4F04EC00DD65FF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8605A5CE1C4F04EC00DD65FF /* Build configuration list for PBXNativeTarget "CareKit iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8605A5CF1C4F04EC00DD65FF /* Debug */, + 8605A5D01C4F04EC00DD65FF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ - }; - rootObject = 8605A5B11C4F04EC00DD65FF /* Project object */; + }; + rootObject = 8605A5B11C4F04EC00DD65FF /* Project object */; } diff --git a/CareKit/CareKit.xcodeproj/xcshareddata/xcschemes/CareKit.xcscheme b/CareKit/CareKit.xcodeproj/xcshareddata/xcschemes/CareKit.xcscheme new file mode 100644 index 000000000..e82a506ef --- /dev/null +++ b/CareKit/CareKit.xcodeproj/xcshareddata/xcschemes/CareKit.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CareKit/CareKit.xcodeproj/xcshareddata/xcschemes/CareKitTests.xcscheme b/CareKit/CareKit.xcodeproj/xcshareddata/xcschemes/CareKitTests.xcscheme new file mode 100644 index 000000000..25f70f5bb --- /dev/null +++ b/CareKit/CareKit.xcodeproj/xcshareddata/xcschemes/CareKitTests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/CareKit/CareKit/CareKit.swift b/CareKit/CareKit/CareKit.swift new file mode 100644 index 000000000..dbd343930 --- /dev/null +++ b/CareKit/CareKit/CareKit.swift @@ -0,0 +1,32 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@_exported import CareKitStore +@_exported import CareKitUI diff --git a/CareKit/CareKit/Details/OCKDetailViewController.swift b/CareKit/CareKit/Details/OCKDetailViewController.swift new file mode 100644 index 000000000..a1294f42d --- /dev/null +++ b/CareKit/CareKit/Details/OCKDetailViewController.swift @@ -0,0 +1,62 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitUI +import UIKit + +/// A view controller that can be customized to display the details of another view. +/// The detail view's content stack view can be populated with arbitrary content. +open class OCKDetailViewController: UIViewController { + private lazy var doneButton: UIBarButtonItem = { + let item = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissViewController)) + return item + }() + + /// A detailed content view. + public var detailView: OCKDetailView { + guard let view = view as? OCKDetailView else { fatalError("Unsupported view.") } + return view + } + + @available(*, unavailable) + override open func loadView() { + view = OCKDetailView() + } + + override open func viewDidLoad() { + super.viewDidLoad() + navigationItem.rightBarButtonItem = doneButton + } + + @objc + private func dismissViewController() { + dismiss(animated: true, completion: nil) + } +} diff --git a/CareKit/CareKit/Extensions/Calendar+Extensions.swift b/CareKit/CareKit/Extensions/Calendar+Extensions.swift new file mode 100644 index 000000000..b1bf554ea --- /dev/null +++ b/CareKit/CareKit/Extensions/Calendar+Extensions.swift @@ -0,0 +1,71 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +extension Calendar { + + /// Returns a date interval that spans the entire week of the given date. The difference between this method and the + /// Foundation `Calendar.dateInterval(of:for:)` method is that this method produces non-overlapping + /// intervals. + func dateIntervalOfWeek(for date: Date) -> DateInterval { + var interval = Calendar.current.dateInterval(of: .weekOfYear, for: date)! + interval.duration -= 1 // The default interval contains 1 second of the next day after the interval. Subtract that off + return interval + } + + /// Returns string representations of the weekdays, in the order the weekdays occur on the local calendar. + /// This differs with the Foundation `Calendar.veryShortWeekdaySymbols` in that the ordering is changed such + /// that the first element of the array corresponds to the first weekday in the current locale, instead of Sunday. + /// + /// This method is required for handling certain regions in which the first day of the week is Monday. + func orderedWeekdaySymbolsVeryShort() -> [String] { + var symbols = veryShortWeekdaySymbols + Array(1.. [String] { + var symbols = weekdaySymbols + Array(1.. NSLayoutConstraint { + priority = new + return self + } +} + +extension UIView { + func setContentHuggingPriorities(_ new: UILayoutPriority) { + setContentHuggingPriority(new, for: .horizontal) + setContentHuggingPriority(new, for: .vertical) + } + + func setContentCompressionResistancePriorities(_ new: UILayoutPriority) { + setContentCompressionResistancePriority(new, for: .horizontal) + setContentCompressionResistancePriority(new, for: .vertical) + } + + func constraints(equalTo other: UIView, directions: LayoutDirection = .all, + priority: UILayoutPriority = .required) -> [NSLayoutConstraint] { + var constraints: [NSLayoutConstraint] = [] + if directions.contains(.top) { + constraints.append(topAnchor.constraint(equalTo: other.topAnchor).withPriority(priority)) + } + if directions.contains(.leading) { + constraints.append(leadingAnchor.constraint(equalTo: other.leadingAnchor).withPriority(priority)) + } + if directions.contains(.bottom) { + constraints.append(bottomAnchor.constraint(equalTo: other.bottomAnchor).withPriority(priority)) + } + if directions.contains(.trailing) { + constraints.append(trailingAnchor.constraint(equalTo: other.trailingAnchor).withPriority(priority)) + } + return constraints + } + + func constraints(equalTo layoutGuide: UILayoutGuide, directions: LayoutDirection = .all, + priority: UILayoutPriority = .required) -> [NSLayoutConstraint] { + var constraints: [NSLayoutConstraint] = [] + if directions.contains(.top) { + constraints.append(topAnchor.constraint(equalTo: layoutGuide.topAnchor).withPriority(priority)) + } + if directions.contains(.leading) { + constraints.append(leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).withPriority(priority)) + } + if directions.contains(.bottom) { + constraints.append(bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).withPriority(priority)) + } + if directions.contains(.trailing) { + constraints.append(trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).withPriority(priority)) + } + return constraints + } + + var isRightToLeft: Bool { + return UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) == .rightToLeft + } +} diff --git a/CareKit/CareKit/Extensions/OCKAnyEvent+Extension.swift b/CareKit/CareKit/Extensions/OCKAnyEvent+Extension.swift new file mode 100644 index 000000000..db17a0e88 --- /dev/null +++ b/CareKit/CareKit/Extensions/OCKAnyEvent+Extension.swift @@ -0,0 +1,51 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +extension OCKAnyEvent { + + /// Sort outcome values by descending updated/created date + func sortedOutcomeValuesByRecency() -> OCKAnyEvent { + guard + var newOutcome = outcome, + !newOutcome.values.isEmpty else { return self } + + let sortedValues = newOutcome.values.sorted { + guard + let date1 = $0.updatedDate ?? $0.createdDate, + let date2 = $1.updatedDate ?? $0.createdDate else { return false } + return date1 > date2 + } + + newOutcome.values = sortedValues + return OCKAnyEvent(task: task, outcome: newOutcome, scheduleEvent: scheduleEvent) + } +} diff --git a/CareKit/CareKit/Extensions/OCKContact+Extensions.swift b/CareKit/CareKit/Extensions/OCKContact+Extensions.swift new file mode 100644 index 000000000..59955d12e --- /dev/null +++ b/CareKit/CareKit/Extensions/OCKContact+Extensions.swift @@ -0,0 +1,68 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Contacts +import Foundation + +private extension OCKLabeledValue { + func toLabeledString() -> CNLabeledValue { CNLabeledValue(label: label, value: NSString(string: value)) } + func toLabeledPhoneNumber() -> CNLabeledValue { CNLabeledValue(label: label, value: CNPhoneNumber(stringValue: label)) } +} + +private extension OCKPostalAddress { + func toCNLabeledValue() -> CNLabeledValue { CNLabeledValue(label: "", value: self) } +} + + +extension CNMutableContact { + convenience init(from contact: OCKAnyContact) { + self.init() + if let value = contact.organization { organizationName = value } + if let value = contact.phoneNumbers { phoneNumbers = value.map { $0.toLabeledPhoneNumber() } } + if let value = contact.address { postalAddresses = [value.toCNLabeledValue()] } + if let value = contact.emailAddresses { emailAddresses = value.map { $0.toLabeledString() } } + if let value = contact.title { jobTitle = value } + if let value = contact.role { note = value } + if let value = contact.organization { departmentName = value } + if let value = contact.emailAddresses { emailAddresses = value.map { $0.toLabeledString() } } + if let value = contact.name.familyName { familyName = value } + if let value = contact.name.givenName { givenName = value } + if let value = contact.name.middleName { middleName = value } + if let value = contact.name.namePrefix { namePrefix = value } + if let value = contact.name.nameSuffix { nameSuffix = value } + if let value = contact.name.nickname { nickname = value } + + if let value = contact.name.phoneticRepresentation?.familyName { phoneticFamilyName = value } + if let value = contact.name.phoneticRepresentation?.givenName { phoneticGivenName = value } + if let value = contact.name.phoneticRepresentation?.middleName { phoneticMiddleName = value } + } +} + diff --git a/CareKit/CareKit/Extensions/UIViewController+Extensions.swift b/CareKit/CareKit/Extensions/UIViewController+Extensions.swift new file mode 100644 index 000000000..0d8809fa1 --- /dev/null +++ b/CareKit/CareKit/Extensions/UIViewController+Extensions.swift @@ -0,0 +1,52 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitUI +import UIKit + +internal extension UIViewController { + func clearContainment() { + willMove(toParent: nil) + removeFromParent() + view.removeFromSuperview() + } + + func setupContainment(in containerViewController: UIViewController, stackView: OCKStackView, animated: Bool = false) { + containerViewController.addChild(self) + stackView.addArrangedSubview(view, animated: animated) + didMove(toParent: containerViewController) + } + + func setupContainment(in containerViewController: UIViewController, stackView: OCKStackView, at index: Int, animated: Bool = false) { + containerViewController.addChild(self) + stackView.insertArrangedSubview(view, at: index, animated: animated) + didMove(toParent: containerViewController) + } +} diff --git a/CareKit/CareKit/Lists/Controller/OCKContactsListViewController.swift b/CareKit/CareKit/Lists/Controller/OCKContactsListViewController.swift new file mode 100644 index 000000000..bd7ef97d5 --- /dev/null +++ b/CareKit/CareKit/Lists/Controller/OCKContactsListViewController.swift @@ -0,0 +1,110 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import Foundation + +/// Classes that conform to this protocol can recieve updates about the state of +/// the `OCKContactsListViewControllerDelegate`. +public protocol OCKContactsListViewControllerDelegate: AnyObject { + func contactsListViewController(_ viewController: OCKContactsListViewController, didEncounterError: Error) +} + +/// An `OCKListViewController` that automatically queries and displays contacts in the `Store` using +/// `OCKDetailedContactViewController`s. +open class OCKContactsListViewController: OCKListViewController { + + // MARK: Properties + + /// The manager of the `Store` from which the `Contact` data is fetched. + public let storeManager: OCKSynchronizedStoreManager + + /// If set, the delegate will receive callbacks when important events happen at the list view controller level. + public weak var delegate: OCKContactsListViewControllerDelegate? + + /// If set, the delegate will receive callbacks when important events happen inside the contact view controllers. + public weak var contactDelegate: OCKContactViewControllerDelegate? + + private var subscription: Cancellable? + + // MARK: - Life Cycle + + /// Initialize using a store manager. All of the contacts in the store manager will be queried and dispalyed. + /// + /// - Parameters: + /// - storeManager: The store manager owning the store whose contacts should be displayed. + public init(storeManager: OCKSynchronizedStoreManager) { + self.storeManager = storeManager + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func viewDidLoad() { + super.viewDidLoad() + navigationController?.navigationBar.prefersLargeTitles = true + title = loc("CONTACTS") + subscribe() + fetchContacts() + } + + // MARK: - Methods + + private func subscribe() { + subscription?.cancel() + subscription = storeManager.contactsPublisher(categories: [.add, .update]).sink { _ in + self.fetchContacts() + } + } + + /// `fetchContacts` asynchronously retrieves an array of contacts stored in a `Result` + /// and makes corresponding `OCKDetailedContactViewController`s. + private func fetchContacts() { + storeManager.store.fetchAnyContacts(query: OCKContactQuery(), callbackQueue: .main) { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): + self.delegate?.contactsListViewController(self, didEncounterError: error) + case .success(let contacts): + self.clear() + for contact in contacts { + let contactViewController = OCKDetailedContactViewController(contact: contact, storeManager: self.storeManager) + contactViewController.delegate = self.contactDelegate + self.appendViewController(contactViewController, animated: false) + } + } + } + } +} diff --git a/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift b/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift new file mode 100644 index 000000000..c64b8b449 --- /dev/null +++ b/CareKit/CareKit/Lists/Controller/OCKDailyPageViewController.swift @@ -0,0 +1,265 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +/// Conform to this protocol to receive callbacks when important events occur in an `OCKDailyPageViewController`. +public protocol OCKDailyPageViewControllerDelegate: AnyObject { + /// This method will be called anytime an unhandled error is encountered. + /// + /// - Parameters: + /// - dailyPageViewController: The daily page view controller in which the error occurred. + /// - error: The error that occurred + func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController, didFailWithError error: Error) +} + +public extension OCKDailyPageViewControllerDelegate { + /// This method will be called anytime an unhandled error is encountered. + /// + /// - Parameters: + /// - dailyPageViewController: The daily page view controller in which the error occurred. + /// - error: The error that occurred + func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController, didFailWithError error: Error) {} +} + +/// Any class that can provide content for an `OCKDailyPageViewController` should conform to this protocol. +public protocol OCKDailyPageViewControllerDataSource: AnyObject { + /// - Parameters: + /// - dailyPageViewController: The daily page view controller for which content should be provided. + /// - listViewController: The list view controller that should be populated with content. + /// - date: A date that should be used to determine what content to insert into the list view controller. + func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController, + prepare listViewController: OCKListViewController, for date: Date) +} + +/// Displays a calendar page view controller in the header, and a view controllers in the body. The view controllers must +/// be manually queried and set from outside of the class. +open class OCKDailyPageViewController: UIViewController, +OCKDailyPageViewControllerDataSource, OCKDailyPageViewControllerDelegate, OCKWeekCalendarPageViewControllerDelegate, +UIPageViewControllerDataSource, UIPageViewControllerDelegate { + + // MARK: Properties + + public weak var dataSource: OCKDailyPageViewControllerDataSource? + public weak var delegate: OCKDailyPageViewControllerDelegate? + + public var selectedDate: Date { + return calendarWeekPageViewController.selectedDate + } + + /// The store manager the view controller uses for synchronization + public let storeManager: OCKSynchronizedStoreManager + + /// Page view managing ListViewControllers. + private let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + + /// The calendar view controller in the header. + private let calendarWeekPageViewController: OCKWeekCalendarPageViewController + + // MARK: - Life cycle + + /// Create an instance of the view controller. Will hook up the calendar to the tasks collection, + /// and query and display the tasks. + /// + /// - Parameter storeManager: The store from which to query the tasks. + /// - Parameter adherenceAggregator: An aggregator that will be used to compute the adherence values shown at the top of the view. + public init(storeManager: OCKSynchronizedStoreManager, adherenceAggregator: OCKAdherenceAggregator = .compareTargetValues) { + self.storeManager = storeManager + self.calendarWeekPageViewController = .init(storeManager: storeManager, aggregator: adherenceAggregator) + super.init(nibName: nil, bundle: nil) + self.calendarWeekPageViewController.dataSource = self + self.pageViewController.dataSource = self + self.pageViewController.delegate = self + self.dataSource = self + self.delegate = self + } + + @available(*, unavailable) + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // 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() + } + + override open func loadView() { + [calendarWeekPageViewController, pageViewController].forEach { addChild($0) } + view = OCKHeaderBodyView(headerView: calendarWeekPageViewController.view, bodyView: pageViewController.view) + [calendarWeekPageViewController, pageViewController].forEach { $0.didMove(toParent: self) } + } + + override open func viewDidLoad() { + super.viewDidLoad() + let now = Date() + calendarWeekPageViewController.calendarDelegate = self + calendarWeekPageViewController.selectDate(now, animated: false) + pageViewController.setViewControllers([makePage(date: now)], direction: .forward, animated: false, completion: nil) + pageViewController.accessibilityHint = loc("THREE_FINGER_SWIPE_DAY") + navigationItem.leftBarButtonItem = UIBarButtonItem(title: loc("TODAY"), style: .plain, target: self, action: #selector(pressedToday(sender:))) + } + + private func makePage(date: Date) -> OCKDatedListViewController { + let listViewController = OCKDatedListViewController(date: date) + let dateLabel = OCKDateLabel(textStyle: .title2, weight: .bold) + dateLabel.setDate(date) + dateLabel.accessibilityTraits = .header + + listViewController.insertView(dateLabel, at: 0, animated: false) + + setInsets(for: listViewController) + dataSource?.dailyPageViewController(self, prepare: listViewController, for: date) + return listViewController + } + + @objc + private func pressedToday(sender: UIBarButtonItem) { + selectDate(Date(), animated: true) + } + + private func updateScrollViewInsets() { + pageViewController.viewControllers?.forEach({ child in + guard let listVC = child as? OCKListViewController else { fatalError("Unexpected type") } + setInsets(for: listVC) + }) + } + + private func setInsets(for listViewController: OCKListViewController) { + guard let listView = listViewController.view as? OCKListView else { fatalError("Unexpected type") } + guard let headerView = view as? OCKHeaderBodyView else { fatalError("Unexpected type") } + let insets = UIEdgeInsets(top: headerView.headerInset, left: 0, bottom: 0, right: 0) + listView.scrollView.contentInset = insets + listView.scrollView.scrollIndicatorInsets = insets + } + + // MARK: - OCKCalendarPageViewControllerDelegate + + public func weekCalendarPageViewController(_ viewController: OCKWeekCalendarPageViewController, didSelectDate date: Date, previousDate: Date) { + let newComponents = Calendar.current.dateComponents([.weekday, .weekOfYear, .year], from: date) + let oldComponents = Calendar.current.dateComponents([.weekday, .weekOfYear, .year], from: previousDate) + guard newComponents != oldComponents else { return } // do nothing if we have selected a date for the same day of the year + let moveLeft = date < previousDate + let listViewController = makePage(date: date) + pageViewController.setViewControllers([listViewController], direction: moveLeft ? .reverse : .forward, animated: true, completion: nil) + } + + public func weekCalendarPageViewController(_ viewController: OCKWeekCalendarPageViewController, didChangeDateInterval interval: DateInterval) {} + + public func weekCalendarPageViewController(_ viewController: OCKWeekCalendarPageViewController, didEncounterError error: Error) { + delegate?.dailyPageViewController(self, didFailWithError: error) + } + + // MARK: OCKDailyPageViewControllerDataSource & Delegate + + open func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController, + prepare listViewController: OCKListViewController, for date: Date) {} + + open func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController, didFailWithError error: Error) {} + + // MARK: - UIPageViewControllerDelegate + + open func pageViewController(_ pageViewController: UIPageViewController, + viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let currentViewController = viewController as? OCKDatedListViewController else { fatalError("Unexpected type") } + let targetDate = Calendar.current.date(byAdding: .day, value: -1, to: currentViewController.date)! + return makePage(date: targetDate) + } + + open func pageViewController(_ pageViewController: UIPageViewController, + viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let currentViewController = viewController as? OCKDatedListViewController else { fatalError("Unexpected type") } + let targetDate = Calendar.current.date(byAdding: .day, value: 1, to: currentViewController.date)! + return makePage(date: targetDate) + } + + // MARK: - UIPageViewControllerDataSource + + open func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, + previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + guard completed else { return } + guard let listViewController = pageViewController.viewControllers?.first as? OCKDatedListViewController else { fatalError("Unexpected type") } + calendarWeekPageViewController.selectDate(listViewController.date, animated: true) + } +} + +// This is private subclass of the list view controller that imbues it with a date that can be uesd by the page view controller to determine +// which direction was just swiped. +private class OCKDatedListViewController: OCKListViewController { + let date: Date + + init(date: Date) { + self.date = date + super.init(nibName: nil, bundle: nil) + listView.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private class OCKDateLabel: OCKLabel { + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter + }() + + func setDate(_ date: Date) { + text = OCKDateLabel.dateFormatter.string(from: date) + } + + override init(textStyle: UIFont.TextStyle, weight: UIFont.Weight) { + super.init(textStyle: textStyle, weight: weight) + styleDidChange() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func styleDidChange() { + super.styleDidChange() + textColor = style().color.label + } +} diff --git a/CareKit/CareKit/Lists/Controller/OCKDailyTasksPageViewController.swift b/CareKit/CareKit/Lists/Controller/OCKDailyTasksPageViewController.swift new file mode 100644 index 000000000..4d30cfa80 --- /dev/null +++ b/CareKit/CareKit/Lists/Controller/OCKDailyTasksPageViewController.swift @@ -0,0 +1,180 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import UIKit + +/// Handles events related to an `OCKDailyTasksPageViewController`. +public protocol OCKDailyTasksPageViewControllerDelegate: OCKTaskViewControllerDelegate { + + /// Return a view controller to display for the given task and events. + /// - Parameters: + /// - viewController: The view controller displaying the returned view controller. + /// - task: The task to be displayed by the returned view controller. + /// - events: The events to be displayed by the returned view controller. + /// - eventQuery: The query used to retrieve the events for the task. + func dailyTasksPageViewController(_ viewController: OCKDailyTasksPageViewController, viewControllerForTask task: OCKAnyTask, + events: [OCKAnyEvent], eventQuery: OCKEventQuery) -> UIViewController? +} + +/// Displays a calendar page view controller in the header, and a collection of tasks +/// in the body. The tasks are automatically queried based on the selection in the calendar. +open class OCKDailyTasksPageViewController: OCKDailyPageViewController, OCKDailyTasksPageViewControllerDelegate { + private let emptyLabelMargin: CGFloat = 4 + + // MARK: Properties + + /// If set, the delegate will receive callbacks when important events happen at the task view controller level. + public weak var tasksDelegate: OCKDailyTasksPageViewControllerDelegate? + + // MARK: - Life Cycle + + override open func viewDidLoad() { + super.viewDidLoad() + tasksDelegate = self + } + + // MARK: - Methods + + private func fetchTasks(for date: Date, andPopulateIn listViewController: OCKListViewController) { + let taskQuery = OCKTaskQuery(for: date) + storeManager.store.fetchAnyTasks(query: taskQuery, callbackQueue: .main) { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): self.delegate?.dailyPageViewController(self, didFailWithError: error) + case .success(let tasks): + + // Show an empty label if there are no tasks + guard !tasks.isEmpty else { + listViewController.listView.stackView.spacing = self.emptyLabelMargin + let emptyLabel = OCKEmptyLabel(textStyle: .subheadline, weight: .medium) + listViewController.appendView(emptyLabel, animated: false) + return + } + + // Aggregate the view controllers returned after fetching the events + let group = DispatchGroup() + var viewControllers: [UIViewController] = [] + tasks.forEach { + group.enter() + self.viewController(forTask: $0, fromQuery: taskQuery) { viewController in + viewController.map { viewControllers.append($0) } + group.leave() + } + } + + // Add the view controllers to the view + group.notify(queue: .main) { + viewControllers.forEach { listViewController.appendViewController($0, animated: false) } + } + } + } + } + + // Fetch events and return a view controller to display the data + private func viewController(forTask task: OCKAnyTask, fromQuery query: OCKTaskQuery, + result: @escaping (UIViewController?) -> Void) { + guard let dateInterval = query.dateInterval else { fatalError("Task query should have a set date") } + let eventQuery = OCKEventQuery(dateInterval: dateInterval) + self.storeManager.store.fetchAnyEvents(taskID: task.id, query: eventQuery, callbackQueue: .main) { [weak self] fetchResult in + guard let self = self else { return } + switch fetchResult { + case .failure(let error): self.delegate?.dailyPageViewController(self, didFailWithError: error) + case .success(let events): + let viewController = + self.tasksDelegate?.dailyTasksPageViewController(self, viewControllerForTask: task, events: events, eventQuery: eventQuery) ?? + self.dailyTasksPageViewController(self, viewControllerForTask: task, events: events, eventQuery: eventQuery) + result(viewController) + } + } + } + + override open func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController, + prepare listViewController: OCKListViewController, for date: Date) { + fetchTasks(for: date, andPopulateIn: listViewController) + } + + // MARK: - OCKDailyTasksPageViewControllerDelegate + + open func dailyTasksPageViewController(_ viewController: OCKDailyTasksPageViewController, viewControllerForTask task: OCKAnyTask, + events: [OCKAnyEvent], eventQuery: OCKEventQuery) -> UIViewController? { + // Show the button log if the task does not impact adherence + if !task.impactsAdherence { + let taskViewController = OCKButtonLogTaskViewController(controller: .init(storeManager: self.storeManager), + viewSynchronizer: .init()) + taskViewController.setup(withEvents: events, task: task, eventQuery: eventQuery, delegate: self.tasksDelegate) + return taskViewController + + // Show the simple if there is only one event. Visually this is the best style for a single event. + } else if events.count == 1 { + let taskViewController = OCKSimpleTaskViewController(controller: .init(storeManager: self.storeManager), + viewSynchronizer: .init()) + taskViewController.setup(withEvents: events, task: task, eventQuery: eventQuery, delegate: self.tasksDelegate) + return taskViewController + + // Else default to the grid + } else { + let taskViewController = OCKGridTaskViewController(controller: .init(storeManager: self.storeManager), + viewSynchronizer: .init()) + taskViewController.setup(withEvents: events, task: task, eventQuery: eventQuery, delegate: self.tasksDelegate) + return taskViewController + } + } + + open func taskViewController(_ viewController: OCKTaskViewController, + didEncounterError: Error) where C: OCKTaskControllerProtocol, VS: OCKTaskViewSynchronizerProtocol {} +} + +private extension OCKTaskViewController where Controller: OCKTaskController { + func setup(withEvents events: [OCKAnyEvent], task: OCKAnyTask, eventQuery: OCKEventQuery, delegate: OCKTaskViewControllerDelegate?) { + controller.updateViewModel(withEvents: events) + controller.subscribeTo(eventsBelongingToTask: task, eventQuery: eventQuery) + self.delegate = delegate + } +} + +private class OCKEmptyLabel: OCKLabel { + override init(textStyle: UIFont.TextStyle, weight: UIFont.Weight) { + super.init(textStyle: textStyle, weight: weight) + text = loc("NO_TASKS") + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func styleDidChange() { + super.styleDidChange() + textColor = style().color.label + } +} diff --git a/CareKit/CareKit/Lists/Controller/OCKListViewController.swift b/CareKit/CareKit/Lists/Controller/OCKListViewController.swift new file mode 100644 index 000000000..5652df8d4 --- /dev/null +++ b/CareKit/CareKit/Lists/Controller/OCKListViewController.swift @@ -0,0 +1,119 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +/// A view controller displaying views in an `OCKListView`. +/// `OCKDailyPageViewController` uses `OCKListViewController`s to display scrollable stacks of +/// embedded view controllers. +open class OCKListViewController: UIViewController { + + // MARK: Properties + + /// The list view that displays the view controller's views. + var listView: OCKListView { + guard let view = self.view as? OCKListView else { fatalError("Unsupported view type.") } + return view + } + + // MARK: - Life cycle + + override open func loadView() { + view = OCKListView() + } + + // MARK: - Methods + + /// Sets up the containment of `viewController` in OCKListViewController and appends its view + /// to the vertical stack of listed views. + /// + /// - Parameters: + /// - viewController: The view controller with the view to append. If the new child view controller + /// is already the child of a container view controller, it is removed from that container before being appended.. + /// - animated: Pass `true` to animate the addition of the `viewController`'s view. + open func appendViewController(_ viewController: UIViewController, animated: Bool) { + viewController.setupContainment(in: self, stackView: listView.stackView, animated: animated) + } + + /// Appends `view` to the vertical stack of listed views. + /// + /// - Parameters: + /// - view: The view to append to the current end of the list. + /// - animated: Pass `true` to animate the addition of the view. + open func appendView(_ view: UIView, animated: Bool) { + listView.stackView.addArrangedSubview(view, animated: animated) + } + + /// Sets up the containment of `viewController` in OCKListViewController and inserts its view + /// in the vertical stack of listed views at the specified index. + /// + /// - Parameters: + /// - viewController: The view controller with the view to insert. If the view controller is already the child of + /// a container view controller, it is removed from that container before being inserted. + /// - index: The index at which to insert the `viewController`'s view. This value must not be greater + /// than the number of views in the `OCKListViewController`. If the index is out of bounds, this method + /// throws an internalInconsistencyException exception. + /// - animated: Pass `true` to animate the insertion of `viewController`'s view. + open func insertViewController(_ viewController: UIViewController, at index: Int, animated: Bool) { + viewController.setupContainment(in: self, stackView: listView.stackView, at: index, animated: animated) + } + + /// Inserts a view in the vertical stack of listed views at the specified index. + /// + /// - Parameters: + /// - view: The view to insert in the list. + /// - index: The index at which to insert `view`. This value must not be greater than the number of views + /// in the `OCKListViewController`. If the index is out of bounds, this method throws an + /// internalInconsistencyException exception. + /// - animated: Pass `true` to animate the insertion of `view`. + open func insertView(_ view: UIView, at index: Int, animated: Bool) { + listView.stackView.insertArrangedSubview(view, at: index, animated: animated) + } + + /// Removes the view located at `index`. + /// + /// - Parameter index: The index of the view to be removed. This must be a valid index in the number of views listed. + open func remove(at index: Int) { + let view = listView.stackView.arrangedSubviews[index] + if let viewController = children.first(where: { $0.view == view }) { + viewController.clearContainment() + } else { + view.removeFromSuperview() + } + } + + /// Removes all displayed views without animation. + open func clear() { + listView.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + children.forEach { $0.clearContainment() } + } +} diff --git a/CareKit/CareKit/Lists/View/OCKHeaderBodyView.swift b/CareKit/CareKit/Lists/View/OCKHeaderBodyView.swift new file mode 100644 index 000000000..169f9b78d --- /dev/null +++ b/CareKit/CareKit/Lists/View/OCKHeaderBodyView.swift @@ -0,0 +1,113 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitUI +import UIKit + +internal class OCKHeaderBodyView: OCKView { + + enum Constants { + static let headerContentHeight: CGFloat = 60 + static let topMargin: CGFloat = 20 + static let margin: CGFloat = 16 + } + + // MARK: Properties + + var headerHeight: CGFloat { + return Constants.headerContentHeight + 2 * Constants.margin + } + + var headerInset: CGFloat { + return headerHeight + Constants.topMargin + } + + private let headerView: UIView + private let bodyView: UIView + + private let headerBackgroundView: UIView = { + let view = UIView() + return view + }() + + private let separatorView = OCKSeparatorView() + + // MARK: Life cycle + + init(headerView: UIView, bodyView: UIView) { + self.headerView = headerView + self.bodyView = bodyView + super.init() + setup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Methods + + private func setup() { + addSubviews() + constrainSubviews() + } + + private func addSubviews() { + [bodyView, headerBackgroundView, separatorView, headerView].forEach { addSubview($0) } + } + + private func constrainSubviews() { + [headerBackgroundView, separatorView, bodyView, headerView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate([ + headerView.centerYAnchor.constraint(equalTo: headerBackgroundView.centerYAnchor), + headerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + headerView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + headerView.heightAnchor.constraint(equalToConstant: Constants.headerContentHeight), + + headerBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), + headerBackgroundView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + headerBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), + headerBackgroundView.heightAnchor.constraint(equalToConstant: headerHeight), + + separatorView.bottomAnchor.constraint(equalTo: headerBackgroundView.bottomAnchor), + separatorView.leadingAnchor.constraint(equalTo: headerBackgroundView.leadingAnchor), + separatorView.trailingAnchor.constraint(equalTo: headerBackgroundView.trailingAnchor) + ] + bodyView.constraints(equalTo: self)) + } + + override func styleDidChange() { + super.styleDidChange() + let style = self.style() + backgroundColor = style.color.customGroupedBackground + headerBackgroundView.backgroundColor = style.color.customGroupedBackground + directionalLayoutMargins = style.dimension.directionalInsets1 + } +} diff --git a/CareKit/CareKit/Lists/View/OCKListView.swift b/CareKit/CareKit/Lists/View/OCKListView.swift new file mode 100644 index 000000000..c7c614109 --- /dev/null +++ b/CareKit/CareKit/Lists/View/OCKListView.swift @@ -0,0 +1,108 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import 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. + let stackView: OCKStackView = { + let stackView = OCKStackView() + stackView.axis = .vertical + return stackView + }() + + /// The scroll view that contains the stack view. + let scrollView = UIScrollView() + + let contentView = UIView() + + // MARK: - Life Cycle + + override init() { + super.init() + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + // MARK: Methods + + private func setup() { + addSubviews() + styleSubviews() + constrainSubviews() + } + + private func styleSubviews() { + scrollView.alwaysBounceVertical = true + } + + private func addSubviews() { + addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(stackView) + } + + private func constrainSubviews() { + [scrollView, contentView, stackView].forEach { $0?.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor), + + stackView.topAnchor.constraint(equalTo: contentView.topAnchor), + stackView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), + stackView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor) + ] + scrollView.constraints(equalTo: self) + + contentView.constraints(equalTo: scrollView)) + } + + override func styleDidChange() { + super.styleDidChange() + let cachedStyle = style() + contentView.directionalLayoutMargins = cachedStyle.dimension.directionalInsets1 + backgroundColor = cachedStyle.color.customGroupedBackground + stackView.spacing = cachedStyle.dimension.directionalInsets1.top + } +} diff --git a/CareKit/CareKit/Localization/en.lproj/locversion.plist b/CareKit/CareKit/Localization/en.lproj/locversion.plist new file mode 100644 index 000000000..b8e7e7877 --- /dev/null +++ b/CareKit/CareKit/Localization/en.lproj/locversion.plist @@ -0,0 +1,14 @@ + + + + + LprojCompatibleVersion + 29 + LprojLocale + en + LprojRevisionLevel + 1 + LprojVersion + 31 + + diff --git a/CareKit/CareKit/Shared/Task/Controller/OCKTaskEvents.swift b/CareKit/CareKit/Shared/Task/Controller/OCKTaskEvents.swift index dceac8320..69954c6dc 100644 --- a/CareKit/CareKit/Shared/Task/Controller/OCKTaskEvents.swift +++ b/CareKit/CareKit/Shared/Task/Controller/OCKTaskEvents.swift @@ -81,7 +81,7 @@ public struct OCKTaskEvents: Collection, Identifiable { @discardableResult public mutating func append(event: OCKAnyEvent) -> (OCKAnyEvent?, Bool) { // The task needs to have a stable identity. Sections are created based on the task's stable identity. - guard event.task.stableID != nil else { return (nil, false) } + guard event.task.uuid != nil else { return (nil, false) } // First make sure there is no matching event already stored in the data structure. let indexPath = self.indexPath(of: event) @@ -187,8 +187,6 @@ private extension OCKAnyTask { /// The matching criteria used to check for uniqueness of two tasks. func matches(_ other: OCKAnyTask) -> Bool { - stableID == other.stableID + uuid == other.uuid } - - var stableID: String? { uuid?.uuidString } } diff --git a/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift b/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift new file mode 100644 index 000000000..fbe4a1c79 --- /dev/null +++ b/CareKit/CareKit/SwiftUI/InstructionsTaskView.swift @@ -0,0 +1,100 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitUI +import Foundation +import SwiftUI + +/// A card that updates when a controller changes. The view displays a header view, multi-line label, and a completion button. +/// +/// In CareKit, this view is intended to display a particular event for a task. The state of the button indicates the completion state of the event. +/// +/// # View Updates +/// The view updates with the observed controller. By default, data from the controller is mapped to the view. The mapping can be customized by +/// providing a closure that returns a view. The closure is called whenever the controller changes. +/// +/// # Style +/// The card supports styling using `careKitStyle(_:)`. +/// +/// ``` +/// +-------------------------------------------------------+ +/// | | +/// | | +/// | <Detail> | +/// | | +/// | -------------------------------------------------- | +/// | | +/// | <Instructions> | +/// | | +/// | +-------------------------------------------------+ | +/// | | <Completion Button> | | +/// | +-------------------------------------------------+ | +/// | | +/// +-------------------------------------------------------+ +/// ``` +public struct InstructionsTaskView<Header: View, Footer: View>: View { + + private let content: (_ configuration: InstructionsTaskViewConfiguration) -> CareKitUI.InstructionsTaskView<Header, Footer> + + /// Owns the view model that drives the view. + @ObservedObject public var controller: OCKInstructionsTaskController + + public var body: some View { + content(.init(controller: controller)) + } + + /// Create an instance that updates the content view when the observed controller changes. + /// - Parameter controller: Owns the view model that drives the view. + /// - Parameter content: Return a view to display whenever the controller changes. + public init(controller: OCKInstructionsTaskController, + content: @escaping (_ configuration: InstructionsTaskViewConfiguration) -> + CareKitUI.InstructionsTaskView<Header, Footer>) { + self.controller = controller + self.content = content + } +} + +public extension InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { + + /// Create an instance that updates the content view when the observed controller changes. The default view will be displayed whenever the + /// controller changes. + /// - Parameter controller: Owns the view model that drives the view. + init(controller: OCKInstructionsTaskController) { + self.init(controller: controller, content: { .init(configuration: $0) }) + } +} + +private extension CareKitUI.InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { + init(configuration: InstructionsTaskViewConfiguration) { + self.init(title: Text(configuration.title), detail: configuration.detail.map { Text($0) }, + instructions: configuration.instructions.map { Text($0) }, + isComplete: configuration.isComplete, action: configuration.action) + } +} diff --git a/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift b/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift new file mode 100644 index 000000000..8ce163d56 --- /dev/null +++ b/CareKit/CareKit/SwiftUI/InstructionsTaskViewConfiguration.swift @@ -0,0 +1,58 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +/// Default data used to map data from an `OCKInstructionsTaskController` to a `CareKitUI.InstructionsTaskView`. +public struct InstructionsTaskViewConfiguration { + + /// The title text to display in the header. + public let title: String + + /// The detail text to display in the header. + public let detail: String? + + /// The instructions text to display under the header. + public let instructions: String? + + /// The action to perform when the button is tapped. + public let action: (() -> Void)? + + /// True if the labeled button is complete. + public let isComplete: Bool + + init(controller: OCKTaskControllerProtocol) { + self.title = controller.title + self.detail = controller.event.map { OCKScheduleUtility.scheduleLabel(for: $0) } ?? "" + self.instructions = controller.instructions + self.isComplete = controller.isFirstEventComplete + self.action = controller.toggleActionForFirstEvent + } +} diff --git a/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift b/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift new file mode 100644 index 000000000..cefcb3114 --- /dev/null +++ b/CareKit/CareKit/SwiftUI/OCKTaskControllerProtocol+Extension.swift @@ -0,0 +1,63 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Foundation +import SwiftUI + +extension OCKTaskControllerProtocol { + + var event: OCKAnyEvent? { objectWillChange.value?.firstEvent } + + var title: String { event?.task.title ?? "" } + + var instructions: String { event?.task.instructions ?? "" } + + var isFirstEventComplete: Bool { event?.outcome != nil } + + var toggleActionForFirstEvent: () -> Void { { self.toggleFirstEvent() } } + + func isEventComplete(atIndexPath indexPath: IndexPath) -> Bool { + return eventFor(indexPath: indexPath)?.outcome != nil + } + + func toggleActionForEvent(atIndexPath indexPath: IndexPath) -> () -> Void { + return { self.toggleEvent(atIndexPath: indexPath) } + } + + private func toggleEvent(atIndexPath indexPath: IndexPath) { + let isComplete = isEventComplete(atIndexPath: indexPath) + setEvent(atIndexPath: indexPath, isComplete: !isComplete, completion: nil) + } + + private func toggleFirstEvent() { + setEvent(atIndexPath: .init(row: 0, section: 0), isComplete: !isFirstEventComplete, completion: nil) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKCalendarController.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKCalendarController.swift new file mode 100644 index 000000000..e8c04dcf3 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKCalendarController.swift @@ -0,0 +1,138 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import Foundation +import UIKit + +/// A basic controller capable of updating a calendar. +open class OCKCalendarController: OCKCalendarControllerProtocol, ObservableObject { + + // MARK: OCKCalendarControllerProtocol + + public var store: OCKAnyEventStore { storeManager.store } + public let objectWillChange: CurrentValueSubject<[OCKCompletionRingButton.CompletionState], Never> + + // MARK: - Properties + + /// The store manager against which the calendar will be synchronized. + public let storeManager: OCKSynchronizedStoreManager + + /// The date interval displayed by the calendar. + public let dateInterval: DateInterval + + private var subscription: AnyCancellable? + + // MARK: - Life Cycle + + /// Initialize the controller. + /// - Parameter dateInterval: The date interval for the adherence range. + /// - Parameter storeManager: Wraps the store that contains the adherence data. + public required init(dateInterval: DateInterval, storeManager: OCKSynchronizedStoreManager) { + self.dateInterval = dateInterval + self.storeManager = storeManager + self.objectWillChange = .init([]) + } + + // MARK: - Methods + + /// Begin observing adherence in the calendar's date interval. + /// + /// - Parameters: + /// - aggregator: An aggregator that will be used to compute adherence. + open func fetchAndObserveAdherence(usingAggregator aggregator: OCKAdherenceAggregator, errorHandler: ((OCKStoreError) -> Void)? = nil) { + + // Set the view model when outcomes change + subscription = storeManager.notificationPublisher + .compactMap { $0 as? OCKOutcomeNotification } + .sink(receiveValue: { [weak self] _ in self?.fetchAdherence(usingAggregator: aggregator, completion: { result in + if case let .failure(error) = result { + errorHandler?(error) + } + }) }) + + // Fetch adherence and set the view model + fetchAdherence(usingAggregator: aggregator) { result in + if case let .failure(error) = result { + errorHandler?(error) + } + } + } + + private func makeAdherenceQuery(withAggregator aggregator: OCKAdherenceAggregator) -> OCKAdherenceQuery { + var adherenceQuery = OCKAdherenceQuery(taskIDs: [], dateInterval: dateInterval) + adherenceQuery.aggregator = aggregator + return adherenceQuery + } + + private func convertAdherenceToCompletionRingState(adherence: [OCKAdherence], + query: OCKAdherenceQuery) -> [OCKCompletionRingButton.CompletionState] { + return zip(query.dateInterval.dates(), adherence).map { date, adherence in + let isInFuture = date > Date() && !Calendar.current.isDateInToday(date) + switch adherence { + case .noTasks: return .dimmed + case .noEvents: return .empty + case .progress(let value): + if value > 0 { return .progress(CGFloat(value)) } + return isInFuture ? .empty : .zero + } + } + } + + /// Fetch the adherence state for the days in the calendar and set the view model. + private func fetchAdherence(usingAggregator aggregator: OCKAdherenceAggregator, + completion: OCKResultClosure<[OCKCompletionRingButton.CompletionState]>?) { + let query = makeAdherenceQuery(withAggregator: aggregator) + storeManager.store.fetchAdherence(query: query) { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): completion?(.failure(error)) + case .success(let adherence): + let states = self.convertAdherenceToCompletionRingState(adherence: adherence, query: query) + self.objectWillChange.value = states + completion?(.success(states)) + } + } + } +} + +private extension DateInterval { + func dates() -> [Date] { + var dates = [Date]() + var currentDate = start + while currentDate < end { + dates.append(currentDate) + currentDate = Calendar.current.date(byAdding: .day, value: 1, to: currentDate)! + } + return dates + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKCalendarControllerProtocol.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKCalendarControllerProtocol.swift new file mode 100644 index 000000000..8f8d35677 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKCalendarControllerProtocol.swift @@ -0,0 +1,44 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import Foundation + +/// Describes an object capable of tracking the state of a calendar. +public protocol OCKCalendarControllerProtocol: AnyObject { + + /// A reference to a store that can be used to fetch and update events. + var store: OCKAnyEventStore { get } + + /// A publisher that publishes completion ring states when events change in the store. + var objectWillChange: CurrentValueSubject<[OCKCompletionRingButton.CompletionState], Never> { get } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKWeekCalendarController.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKWeekCalendarController.swift new file mode 100644 index 000000000..25aa5ed26 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/Controllers/OCKWeekCalendarController.swift @@ -0,0 +1,57 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Combine +import Foundation + +/// A calendar controller that handles a calendar showing a single week. +open class OCKWeekCalendarController: OCKCalendarController { + + public init(weekOfDate date: Date, storeManager: OCKSynchronizedStoreManager) { + var weekInterval = Calendar.current.dateInterval(of: .weekOfYear, for: date)! + weekInterval.duration -= 1 // Note: Default interval includes the first second of the next week + super.init(dateInterval: weekInterval, storeManager: storeManager) + } + + public required init(dateInterval: DateInterval, storeManager: OCKSynchronizedStoreManager) { + var weekInterval = Calendar.current.dateInterval(of: .weekOfYear, for: Date())! + weekInterval.duration -= 1 // Note: Default interval includes the first second of the next week + guard Self.dateIntervalIsValid(dateInterval) else { fatalError("Date interval should be one week long") } + super.init(dateInterval: dateInterval, storeManager: storeManager) + } + + // Date interval is valid if it spans one week + private static func dateIntervalIsValid(_ dateInterval: DateInterval) -> Bool { + var weekInterval = Calendar.current.dateInterval(of: .weekOfYear, for: dateInterval.start)! + weekInterval.duration -= 1 // Note: Default interval includes the first second of the next week + + return weekInterval.start == dateInterval.start && weekInterval.end == dateInterval.end + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/Paging/OCKWeekCalendarPageViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/Paging/OCKWeekCalendarPageViewController.swift new file mode 100644 index 000000000..2168f967c --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/Paging/OCKWeekCalendarPageViewController.swift @@ -0,0 +1,202 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +/// Handles events related to an `OCKWeekCalendarPageViewController`. +public protocol OCKWeekCalendarPageViewControllerDelegate: AnyObject { + /// Called when a date in the calendar has been selected. + /// - Parameter viewController: The view controller that displays the calendar. + /// - Parameter date: The newly selected date. + /// - Parameter previousDate: The previously selected date. + func weekCalendarPageViewController(_ viewController: OCKWeekCalendarPageViewController, didSelectDate date: Date, previousDate: Date) + + /// Called when the date interval in the calendar has been changed. + /// - Parameter viewController: The view controller that displays the calendar. + /// - Parameter interval: The new date interval. + func weekCalendarPageViewController(_ viewController: OCKWeekCalendarPageViewController, didChangeDateInterval interval: DateInterval) + + func weekCalendarPageViewController(_ viewController: OCKWeekCalendarPageViewController, didEncounterError error: Error) +} + +/// A view controller that allows paging through adjacent weeks of task adherence content. +open class OCKWeekCalendarPageViewController: +UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OCKCalendarViewDelegate { + // MARK: Properties + + /// Handles events related to an `OCKCalendarPageViewController`. + public weak var calendarDelegate: OCKWeekCalendarPageViewControllerDelegate? + + /// The currently selected date in the calendar. + public var selectedDate: Date { + return currentViewController?.calendarView.selectedDate ?? Date() + } + + /// The date interval currently being displayed. + public var dateInterval: DateInterval? { + return currentViewController?.calendarView.dateInterval + } + + private let aggregator: OCKAdherenceAggregator + private var previouslySelectedDate = Date() + private let storeManager: OCKSynchronizedStoreManager + + var currentViewController: OCKWeekCalendarViewController? { + guard + let viewControllers = viewControllers, + !viewControllers.isEmpty else { return nil } + + guard let viewController = viewControllers.first! as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } + return viewController + } + + // MARK: - Life Cycle + + public init(storeManager: OCKSynchronizedStoreManager, aggregator: OCKAdherenceAggregator) { + self.storeManager = storeManager + self.aggregator = aggregator + super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func viewDidLoad() { + super.viewDidLoad() + dataSource = self + delegate = self + + // Create the first view controller + let viewController = makeViewController(forDate: previouslySelectedDate) + viewController.calendarView.delegate = self + viewController.calendarView.selectDate(previouslySelectedDate) + setViewControllers([viewController], direction: .forward, animated: false, completion: nil) + } + + // MARK: - Methods + + private func makeViewController(forDate date: Date) -> OCKWeekCalendarViewController { + let viewController = OCKWeekCalendarViewController(weekOfDate: date, aggregator: aggregator, storeManager: storeManager) + + let interval = Calendar.current.dateInterval(of: .weekOfYear, for: date)! + viewController.calendarView.showDate(interval.start) + return viewController + } + + // Send errors through to the calendarDelegate + private func handleResult(_ result: Result<[OCKCompletionRingButton.CompletionState], OCKStoreError>) { + switch result { + case .failure(let error): calendarDelegate?.weekCalendarPageViewController(self, didEncounterError: error) + case .success: break + } + } + + /// Select a date in the calendar. If the date is not in the current date interval being displayed, the view controller will automatically + /// page to the date interval that contains the new date. + /// - Parameter date: The new date to select. + /// - Parameter animated: True to animate selection of the new date. + open func selectDate(_ date: Date, animated: Bool) { + guard let currentVC = currentViewController else { return } + if currentVC.calendarView.dateInterval.contains(date) { + currentVC.calendarView.selectDate(date) + return + } + + // Create the next view controller + let nextVC = makeViewController(forDate: date) + nextVC.calendarView.delegate = self + let isLeft = currentVC.calendarView.dateInterval.start > date + nextVC.calendarView.selectDate(date) + setViewControllers([nextVC], direction: isLeft ? .reverse : .forward, animated: animated, completion: nil ) + } + + // MARK: UIPageViewController DataSource & Delegate + + open func pageViewController(_ pageViewController: UIPageViewController, + viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let typedViewController = viewController as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } + let dateInterval = typedViewController.calendarView.dateInterval + let previousDate = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: dateInterval.start)! + let previousPage = makeViewController(forDate: previousDate) + previousPage.calendarView.delegate = self + return previousPage + } + + open func pageViewController(_ pageViewController: UIPageViewController, + viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let typedViewController = viewController as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } + let dateInterval = typedViewController.calendarView.dateInterval + let nextDate = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: dateInterval.start)! + let nextPage = makeViewController(forDate: nextDate) + nextPage.calendarView.delegate = self + return nextPage + } + + open func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, + previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + guard + completed, + let previousViewController = previousViewControllers.first, + let currentViewController = currentViewController, + let currentWeek = dateInterval + else { return } + guard let typedPreviousViewController = previousViewController as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } + + let didMoveForwards = typedPreviousViewController.calendarView.dateInterval < currentViewController.calendarView.dateInterval + let offset = didMoveForwards ? 1 : -1 + let previousSelectedDate = typedPreviousViewController.calendarView.selectedDate + let newSelectedDate = Calendar.current.date(byAdding: .weekOfYear, value: offset, + to: typedPreviousViewController.calendarView.selectedDate)! + currentViewController.calendarView.selectDate(newSelectedDate) + calendarDelegate?.weekCalendarPageViewController(self, didChangeDateInterval: currentWeek) + calendarDelegate?.weekCalendarPageViewController(self, didSelectDate: newSelectedDate, previousDate: previousSelectedDate) + } + + // MARK: OCKCalendarViewDelegate + + /// Called when a particular date in the calendar was selected. + /// - Parameter calendarView: The view displaying the calendar. + /// - Parameter date: The date that was selected. + /// - Parameter index: The index of the date that was selected with respect to the collection of days in the current `dateInterval`. + /// - Parameter sender: The sender that initiated the selection. + open func calendarView(_ calendarView: UIView & OCKCalendarDisplayable, didSelectDate date: Date, at index: Int, sender: Any?) { + guard + let startOfWeek = dateInterval?.start, + let dateInterval = dateInterval else { return } + let comparison = Calendar.current.compare(dateInterval.start, to: startOfWeek, toGranularity: .weekOfYear) + guard comparison == .orderedSame else { return } + calendarDelegate?.weekCalendarPageViewController(self, didSelectDate: date, previousDate: previouslySelectedDate) + previouslySelectedDate = date + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/Synchronizers/OCKCalendarViewSynchronizerProtocol.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/Synchronizers/OCKCalendarViewSynchronizerProtocol.swift new file mode 100644 index 000000000..119372325 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/Synchronizers/OCKCalendarViewSynchronizerProtocol.swift @@ -0,0 +1,49 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +/// Describes a view synchronizer for calendars. +public protocol OCKCalendarViewSynchronizerProtocol { + + /// The type of the view that will be synchronized + associatedtype View: UIView & OCKCalendarDisplayable + + /// Initialize a view to be synchronized. + func makeView() -> View + + /// Update a view using the given context. + /// - Parameters: + /// - view: The view to be updated. + /// - context: Information about the update that is occurring. + func updateView(_ view: View, context: OCKSynchronizationContext<[OCKCompletionRingButton.CompletionState]>) +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/Synchronizers/OCKWeekCalendarViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/Synchronizers/OCKWeekCalendarViewSynchronizer.swift new file mode 100644 index 000000000..b702c6e4e --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/Synchronizers/OCKWeekCalendarViewSynchronizer.swift @@ -0,0 +1,55 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A view synchronizer that creates and updates an `OCKWeekCalendarView`. +open class OCKWeekCalendarViewSynchronizer: OCKCalendarViewSynchronizerProtocol { + + private let date: Date + + /// Initialize by specifying a date. The calendar will show the entire week that + /// the provided date falls within. + public init(weekOfDate date: Date) { + self.date = date + } + + open func makeView() -> OCKWeekCalendarView { + let view = OCKWeekCalendarView(weekOfDate: date) + view.accessibilityHint = loc("THREE_FINGER_SWIPE_WEEK") + return view + } + + open func updateView(_ view: OCKWeekCalendarView, context: OCKSynchronizationContext<[OCKCompletionRingButton.CompletionState]>) { + view.updateWith(states: context.viewModel, animated: context.animated) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKCalendarViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKCalendarViewController.swift new file mode 100644 index 000000000..1c7d76d32 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKCalendarViewController.swift @@ -0,0 +1,137 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import MessageUI +import UIKit + +/// Types wishing to receive updates from calendar view controllers can conform to this protocol. +public protocol OCKCalendarViewControllerDelegate: AnyObject { + + /// Called when an unhandled error is encountered in a calendar view controller. + /// - Parameters: + /// - viewController: The view controller in which the error was encountered. + /// - didEncounterError: The error that was unhandled. + func calendarViewController<C: OCKCalendarControllerProtocol, VS: OCKCalendarViewSynchronizerProtocol>( + _ viewController: OCKCalendarViewController<C, VS>, didEncounterError: Error) +} + +/// A view controller that displays a calendar view and keep it synchronized with a store. +open class OCKCalendarViewController<Controller: OCKCalendarControllerProtocol, ViewSynchronizer: OCKCalendarViewSynchronizerProtocol>: +UIViewController, OCKCalendarViewDelegate { + + // MARK: Properties + + /// If set, the delegate will receive updates when import events happen + public weak var delegate: OCKCalendarViewControllerDelegate? + + /// Handles the responsibility of updating the view when data in the store changes. + public let viewSynchronizer: ViewSynchronizer + + /// Handles the responsibility of interacting with data from the store. + public let controller: Controller + + /// The view that is being synchronized against the store. + public var calendarView: ViewSynchronizer.View { + guard let view = self.view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } + return view + } + + private var viewDidLoadCompletion: (() -> Void)? + private var viewModelSubscription: AnyCancellable? + + // MARK: - Life Cycle + + /// Initialize with a controller and a synchronizer + public init(controller: Controller, viewSynchronizer: ViewSynchronizer) { + self.controller = controller + self.viewSynchronizer = viewSynchronizer + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @available(*, unavailable) + override open func loadView() { + view = viewSynchronizer.makeView() + } + + override open func viewDidLoad() { + super.viewDidLoad() + calendarView.delegate = self + + // Begin listening for changes in the view model. Note, when we subscribe to the view model, it sends its current value through the stream + startObservingViewModel() + + viewDidLoadCompletion?() + } + + // MARK: - Methods + + // Create a subscription that updates the view when the view model is updated. + private func startObservingViewModel() { + viewModelSubscription?.cancel() + viewModelSubscription = controller.objectWillChange + .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) + } + } + + // MARK: - OCKCalendarViewDelegate + + open func calendarView(_ calendarView: UIView & OCKCalendarDisplayable, didSelectDate date: Date, at index: Int, sender: Any?) {} +} + +public extension OCKCalendarViewController where Controller: OCKCalendarController { + + /// Initialize a view controller that displays adherence. Fetches and stays synchronized with the adherence data. + /// - Parameter viewSynchronizer: Manages the calendar view. + /// - Parameter dateInterval: The date interval for the adherence range. + /// - Parameter aggregator: Used to aggregate adherence over the date interval. + /// - Parameter storeManager: Wraps the store that contains the adherence data. + convenience init(viewSynchronizer: ViewSynchronizer, dateInterval: DateInterval, + aggregator: OCKAdherenceAggregator, storeManager: OCKSynchronizedStoreManager) { + let controller = Controller(dateInterval: dateInterval, storeManager: storeManager) + self.init(controller: controller, viewSynchronizer: viewSynchronizer) + viewDidLoadCompletion = { [weak self] in + self?.controller.fetchAndObserveAdherence(usingAggregator: aggregator, errorHandler: { [weak self] error in + guard let self = self else { return } + self.delegate?.calendarViewController(self, didEncounterError: error) + }) + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKWeekCalendarViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKWeekCalendarViewController.swift new file mode 100644 index 000000000..de58ede8e --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Calendar/View Controllers/OCKWeekCalendarViewController.swift @@ -0,0 +1,48 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Foundation + +public typealias OCKWeekCalendarViewController = OCKCalendarViewController<OCKWeekCalendarController, OCKWeekCalendarViewSynchronizer> + +public extension OCKCalendarViewController where Controller: OCKWeekCalendarController, ViewSynchronizer == OCKWeekCalendarViewSynchronizer { + + /// Initialize a view controller that displays adherence. Fetches and stays synchronized with the adherence data. + /// - Parameter weekOfDate: A date in the week for which adherence will be fetched. + /// - Parameter aggregator: Used to aggregate adherence over the date interval. + /// - Parameter storeManager: Wraps the store that contains the adherence data. + convenience init(weekOfDate date: Date, aggregator: OCKAdherenceAggregator, storeManager: OCKSynchronizedStoreManager) { + var weekInterval = Calendar.current.dateInterval(of: .weekOfYear, for: date)! + weekInterval.duration -= 1 // Standard interval returns 1 second of the next week + let viewSynchronizer = ViewSynchronizer(weekOfDate: date) + self.init(viewSynchronizer: viewSynchronizer, dateInterval: weekInterval, aggregator: aggregator, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKCartesianChartController.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKCartesianChartController.swift new file mode 100644 index 000000000..18d1ec0b5 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKCartesianChartController.swift @@ -0,0 +1,33 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKCartesianChartController = OCKChartController diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartController.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartController.swift new file mode 100644 index 000000000..7cfffd1b5 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartController.swift @@ -0,0 +1,128 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import Foundation +import UIKit + +/// A basic controller capable of updating charts. +open class OCKChartController: OCKChartControllerProtocol, ObservableObject { + + // MARK: OCKChartControllerProtocol + public var store: OCKAnyEventStore { storeManager.store } + public let objectWillChange: CurrentValueSubject<[OCKDataSeries], Never> + + /// The store manager against which the chart will be synchronized. + public let storeManager: OCKSynchronizedStoreManager + + // MARK: Properties + + private let eventQuery: OCKEventQuery + private var cancellables: Set<AnyCancellable> = Set() + + // MARK: - Life Cycle + + /// Initialize the controller. + /// - 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.eventQuery = OCKEventQuery(dateInterval: Calendar.current.dateIntervalOfWeek(for: weekOfDate)) + self.storeManager = storeManager + self.objectWillChange = .init([]) + } + + // MARK: - Methods + + /// Begin observing an array of data series configurations. + /// - Parameters: + /// - configurations: An array of configurations to be plotted. + open func fetchAndObserveInsights(forConfigurations configurations: [OCKDataSeriesConfiguration], + errorHandler: ((Error) -> Void)? = nil) { + 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) + } + } + } + } + } + + private func refetchEvents(configurations: [OCKDataSeriesConfiguration], + completion: OCKResultClosure<[OCKDataSeries]>?) { + var allDataSeries = [OCKDataSeries]() + let group = DispatchGroup() + + // Aggregate events, then set the view model + for config in configurations { + let insightsQuery = OCKInsightQuery(taskID: config.taskID, dateInterval: eventQuery.dateInterval, aggregator: config.aggregator) + group.enter() + storeManager.store.fetchInsights(query: insightsQuery, callbackQueue: .main) { result in + switch result { + case .failure(let error): + completion?(.failure(error)) + return + case .success(let values): + var series = OCKDataSeries(values: values.map { CGFloat($0) }, title: config.legendTitle, + gradientStartColor: config.gradientStartColor, gradientEndColor: config.gradientEndColor, + size: config.markerSize) + + let accessibilityLabels = zip(Calendar.current.orderedWeekdaySymbols(), values).map { "\(config.legendTitle), \($0), \($1)" } + series.accessibilityLabels = accessibilityLabels + allDataSeries.append(series) + } + group.leave() + } + } + + // Wait for the events to be aggregated, then set the view model + group.notify(queue: .main) { [weak self] in + guard let self = self else { return } + self.objectWillChange.value = allDataSeries + completion?(.success(allDataSeries)) + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartControllerProtocol.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartControllerProtocol.swift new file mode 100644 index 000000000..2821d6d95 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKChartControllerProtocol.swift @@ -0,0 +1,43 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine + +/// Describes an object capable of tracking and updating the state of a chart. +public protocol OCKChartControllerProtocol: AnyObject { + + /// A reference to a writable store. + var store: OCKAnyTaskStore & OCKAnyEventStore { get } + + /// A publisher that publishers new values when the watched data series change in the store. + var objectWillChange: CurrentValueSubject<[OCKDataSeries], Never> { get } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKDataSeriesConfiguration.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKDataSeriesConfiguration.swift new file mode 100644 index 000000000..cc48c5765 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/Controller/OCKDataSeriesConfiguration.swift @@ -0,0 +1,75 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import UIKit + +/// A configuration object that specifies which data should be queried and how it should be displayed by the graph. +public struct OCKDataSeriesConfiguration { + /// A user-provided unique id for a task. + public var taskID: String + + /// The title that will be used to represent this data series in the legend. + public var legendTitle: String + + /// The first of two colors that will be used in the gradient when plotting the data. + public var gradientStartColor: UIColor + + /// The second of two colors that will be used in the gradient when plotting the data. + public var gradientEndColor: UIColor + + /// The marker size determines the size of the line, bar, or scatter plot elements. The precise behavior is different for each type of plot. + /// - For line plots, it will be the width of the line. + /// - For scatter plots, it will be the radius of the markers. + /// - For bar plots, it will be the width of the bar. + public var markerSize: CGFloat + + /// A closure that accepts as an argument a day's worth of events and returns a y-axis value for that day. + public var aggregator: OCKEventAggregator + + /// Initialize a new `OCKDataSeriesConfiguration`. + /// + /// - Parameters: + /// - taskID: A user-provided unique id for a task. + /// - legendTitle: The title that will be used to represent this data series in the legend. + /// - gradientStartColor: The first of two colors that will be used in the gradient when plotting the data. + /// - gradientEndColor: The second of two colors that will be used in the gradient when plotting the data. + /// - markerSize: The marker size determines the size of the line, bar, or scatter plot elements. The precise behavior varies by plot type. + /// - eventAggregator: A an aggregator that accepts as an argument a day's worth of events and returns a y-axis value for that day. + public init(taskID: String, legendTitle: String, gradientStartColor: UIColor, gradientEndColor: UIColor, + markerSize: CGFloat, eventAggregator: OCKEventAggregator) { + self.taskID = taskID + self.legendTitle = legendTitle + self.gradientStartColor = gradientStartColor + self.gradientEndColor = gradientEndColor + self.markerSize = markerSize + self.aggregator = eventAggregator + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKCartesianChartViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKCartesianChartViewSynchronizer.swift new file mode 100644 index 000000000..05c3f0534 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKCartesianChartViewSynchronizer.swift @@ -0,0 +1,66 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A chart view controller capable of displaying charts drawn on a Cartesian coordinate system. +open class OCKCartesianChartViewSynchronizer: OCKChartViewSynchronizerProtocol { + + /// The type of the plot that this view controller displays. + public let plotType: OCKCartesianGraphView.PlotType + + /// The currently selected date. + public let selectedDate: Date + + /// Initialize by providing a chart type and date. + public required init(plotType: OCKCartesianGraphView.PlotType, selectedDate: Date) { + self.plotType = plotType + self.selectedDate = selectedDate + } + + open func updateView(_ view: OCKCartesianChartView, context: OCKSynchronizationContext<[OCKDataSeries]>) { + view.updateWith(dataSeries: context.viewModel, animated: context.animated) + } + + open func makeView() -> OCKCartesianChartView { + let chartView = OCKCartesianChartView(type: plotType) + 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/Synchronizers/OCKChartViewSynchronizerProtocol.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKChartViewSynchronizerProtocol.swift new file mode 100644 index 000000000..77c227932 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/Synchronizers/OCKChartViewSynchronizerProtocol.swift @@ -0,0 +1,49 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +/// Describes a view synchronizer for charts. +public protocol OCKChartViewSynchronizerProtocol { + + /// The type of the view that will be synchronized + associatedtype View: UIView & OCKChartDisplayable + + /// Initialize a view to be synchronized. + func makeView() -> View + + /// Update a view using the given context. + /// - Parameters: + /// - view: The view to be updated. + /// - context: Information about the update that is occurring. + func updateView(_ view: View, context: OCKSynchronizationContext<[OCKDataSeries]>) +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKCartesianChartViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKCartesianChartViewController.swift new file mode 100644 index 000000000..35fae5f81 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKCartesianChartViewController.swift @@ -0,0 +1,46 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKCartesianChartViewController = OCKChartViewController<OCKCartesianChartController, OCKCartesianChartViewSynchronizer> + +public extension OCKChartViewController where Controller: OCKCartesianChartController, ViewSynchronizer == OCKCartesianChartViewSynchronizer { + + /// Initialize a view controller that displays a chart. Fetches and stays synchronized with insights. + /// - Parameter weekOfDate: A date in the week of the insights range to fetch. + /// - Parameter configurations: Configurations used to fetch the insights ad display the data. + /// - Parameter storeManager: Wraps the store that contains the insight data to fetch. + convenience init(plotType: OCKCartesianGraphView.PlotType, selectedDate: Date, + configurations: [OCKDataSeriesConfiguration], storeManager: OCKSynchronizedStoreManager) { + let viewSynchronizer = ViewSynchronizer(plotType: plotType, selectedDate: selectedDate) + self.init(viewSynchronizer: viewSynchronizer, weekOfDate: selectedDate, configurations: configurations, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKChartViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKChartViewController.swift new file mode 100644 index 000000000..fb15cc32f --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Chart/View Controllers/OCKChartViewController.swift @@ -0,0 +1,136 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import MessageUI +import UIKit + +/// Types wishing to receive updates from chart view controllers can conform to this protocol. +public protocol OCKChartViewControllerDelegate: AnyObject { + + /// Called when an unhandled error is encountered in a calendar view controller. + /// - Parameters: + /// - viewController: The view controller in which the error was encountered. + /// - didEncounterError: The error that was unhandled. + func chartViewController<C: OCKChartControllerProtocol, VS: OCKChartViewSynchronizerProtocol>( + _ viewController: OCKChartViewController<C, VS>, didEncounterError: Error) +} + +/// A view controller that displays a chart view and keep it synchronized with a store. +open class OCKChartViewController<Controller: OCKChartControllerProtocol, ViewSynchronizer: OCKChartViewSynchronizerProtocol>: +UIViewController, OCKChartViewDelegate { + + // MARK: Properties + + /// If set, the delegate will receive updates when import events happen + public weak var delegate: OCKChartViewControllerDelegate? + + /// Handles the responsibility of updating the view when data in the store changes. + public let viewSynchronizer: ViewSynchronizer + + /// Handles the responsibility of interacting with data from the store. + public let controller: Controller + + /// The view that is being synchronized against the store. + public var chartView: ViewSynchronizer.View { + guard let view = self.view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } + return view + } + + private var viewDidLoadCompletion: (() -> Void)? + private var viewModelSubscription: AnyCancellable? + + // MARK: - Life Cycle + + public init(controller: Controller, viewSynchronizer: ViewSynchronizer) { + self.controller = controller + self.viewSynchronizer = viewSynchronizer + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @available(*, unavailable) + override open func loadView() { + view = viewSynchronizer.makeView() + } + + override open func viewDidLoad() { + super.viewDidLoad() + chartView.delegate = self + + // Begin listening for changes in the view model. Note, when we subscribe to the view model, it sends its current value through the stream + startObservingViewModel() + + viewDidLoadCompletion?() + } + + // MARK: - Methods + + // Create a subscription that updates the view when the view model is updated. + private func startObservingViewModel() { + viewModelSubscription?.cancel() + viewModelSubscription = controller.objectWillChange + .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) + } + } + + // MARK: - OCKChartViewDelegate + + open func didSelectChartView(_ chartView: UIView & OCKChartDisplayable) {} +} + +public extension OCKChartViewController where Controller: OCKChartController { + + /// Initialize a view controller that displays a chart. Fetches and stays synchronized with insights. + /// - Parameter viewSynchronizer: Manages the chart view. + /// - Parameter weekOfDate: A date in the week of the insights range to fetch. + /// - Parameter configurations: Configurations used to fetch the insights ad display the data. + /// - Parameter storeManager: Wraps the store that contains the insight data to fetch. + convenience init(viewSynchronizer: ViewSynchronizer, weekOfDate: Date, + configurations: [OCKDataSeriesConfiguration], storeManager: OCKSynchronizedStoreManager) { + let controller = Controller(weekOfDate: weekOfDate, storeManager: storeManager) + self.init(controller: controller, viewSynchronizer: viewSynchronizer) + viewDidLoadCompletion = { [weak self] in + self?.controller.fetchAndObserveInsights(forConfigurations: configurations, errorHandler: { [weak self] error in + guard let self = self else { return } + self.delegate?.chartViewController(self, didEncounterError: error) + }) + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactController.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactController.swift new file mode 100644 index 000000000..3ac783f1b --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactController.swift @@ -0,0 +1,121 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Combine +import Foundation +import MapKit +import MessageUI + +/// A basic controller capable of watching and updating contacts. +open class OCKContactController: OCKContactControllerProtocol, ObservableObject { + + // MARK: OCKContactControllerProtocol + + public var store: OCKAnyContactStore { storeManager.store } + public let objectWillChange: CurrentValueSubject<OCKAnyContact?, Never> + + // MARK: - Properties + + /// The store manager against which the task will be synchronized. + public let storeManager: OCKSynchronizedStoreManager + + private var subscription: AnyCancellable? + + // MARK: - Life Cycle + + /// Initialize with a store manager. + public required init(storeManager: OCKSynchronizedStoreManager) { + self.objectWillChange = .init(nil) + self.storeManager = storeManager + } + + // MARK: - Methods + + /// Begin observing a contact. + /// + /// - Parameter contact: The contact to watch for changes. + open func observeContact(_ contact: OCKAnyContact) { + objectWillChange.value = contact + + // Set the view model when the contact changes + subscription = storeManager.publisher(forContact: contact, categories: [.update, .add], fetchImmediately: false) + .sink { [weak self] newValue in + self?.objectWillChange.value = newValue + } + } + + /// Fetch and begin observing the first contact described by a query. + /// + /// - Parameters: + /// - query: Any contact query describing the contact to be fetched. + /// + /// - Note: If the query matches multiple contacts, the first one returned will be used. + open func fetchAndObserveContact(forQuery query: OCKAnyContactQuery, errorHandler: ((OCKStoreError) -> Void)? = nil) { + + // Fetch the contact to set as the view model value + storeManager.store.fetchAnyContacts(query: query, callbackQueue: .main) { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): errorHandler?(error) + case .success(let contacts): + self.objectWillChange.value = contacts.first + + // Set the view model when the contact changes + guard let id = self.objectWillChange.value?.id else { return } + self.subscription = self.storeManager.publisher(forContactID: id, categories: [.update, .add]).sink { [weak self] newValue in + self?.objectWillChange.value = newValue + } + } + } + } + + /// Fetch and begin observing the contact with the given identifier. + /// + /// - Parameters: + /// - id: The user-defined unique identifier for the contact. + open func fetchAndObserveContact(withID id: String, errorHandler: ((OCKStoreError) -> Void)? = nil) { + + // Fetch the contact to set as the view model value + storeManager.store.fetchAnyContact(withID: id, callbackQueue: .main) { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): errorHandler?(error) + case .success(let contact): + self.objectWillChange.value = contact + + // Set the view model when the contact changes + self.subscription = self.storeManager.publisher(forContactID: contact.id, categories: [.update, .add]).sink { [weak self] newValue in + self?.objectWillChange.value = newValue + } + } + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactControllerProtocol+Methods.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactControllerProtocol+Methods.swift new file mode 100644 index 000000000..4a7ca09db --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactControllerProtocol+Methods.swift @@ -0,0 +1,169 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Contacts +import ContactsUI +import Foundation +import MapKit +import MessageUI + +public extension OCKContactControllerProtocol { + + func initiateCall() throws -> URL { + let contact = try validateContact() + + // Ensure the contact has a phone number to call + guard let phoneNumber = contact.phoneNumbers?.first?.value else { + throw OCKContactControllerError.invalidPhoneNumber(nil) + } + + // Generate the URL to call the phone number + let filteredNumber = filteredDigits(for: phoneNumber) + guard let url = URL(string: "tel://" + filteredNumber) else { + throw OCKContactControllerError.invalidPhoneNumber(phoneNumber) + } + return url + } + + func initiateMessage() throws -> MFMessageComposeViewController { + let contact = try validateContact() + + // Ensure the contact has a phone number to message + guard let messageNumber = contact.messagingNumbers?.first?.value else { + throw OCKContactControllerError.invalidPhoneNumber(nil) + } + + // Ensure we can send messages + guard MFMessageComposeViewController.canSendText() else { + throw OCKContactControllerError.cannotSendMessages + } + + // Generate the message view controller + let filteredNumber = filteredDigits(for: messageNumber) + let composeViewController = MFMessageComposeViewController() + composeViewController.recipients = [filteredNumber] + return composeViewController + } + + func initiateEmail() throws -> MFMailComposeViewController { + let contact = try validateContact() + + // Ensure the contact has an email + guard let email = contact.emailAddresses?.first?.value else { + throw OCKContactControllerError.invalidEmail(nil) + } + + // Ensure we can send emails + guard MFMailComposeViewController.canSendMail() else { + throw OCKContactControllerError.cannotSendMail + } + + // Generate the mail view controller + let viewController = MFMailComposeViewController() + viewController.setToRecipients([email]) + return viewController + } + + func initiateAddressLookup(completion: @escaping (Result<MKMapItem, Error>) -> Void) { + let contact: OCKAnyContact + do { + contact = try validateContact() + } catch { + completion(.failure(error)) + return + } + + // Ensure the contact has an address + guard let address = contact.address else { + completion(.failure(OCKContactControllerError.invalidAddress(nil))) + return + } + + // Generate the map item that pinpoints the contact's address + let geoloc = CLGeocoder() + geoloc.geocodePostalAddress(address) { placemarks, _ in + guard let placemark = placemarks?.first else { + completion(.failure(OCKContactControllerError.invalidAddress(address))) + return + } + let mkPlacemark = MKPlacemark(placemark: placemark) + completion(.success(MKMapItem(placemark: mkPlacemark))) + } + } + + func initiateSystemContactLookup() throws -> CNContactViewController { + let contact = try validateContact() + + // Create a CNMutableContact from an OCKAnyContact + let mutableContact = CNMutableContact(from: contact) + + // Create a view controller that displays the contact + let contactViewController = CNContactViewController(forUnknownContact: mutableContact) + contactViewController.contactStore = CNContactStore() + contactViewController.allowsEditing = false + contactViewController.view.backgroundColor = OCKStyle().color.customGroupedBackground + return contactViewController + } + + // Remove non-numeric characters + private func filteredDigits(for value: String) -> String { + return value.filter("0123456789".contains) + } + + private func validateContact() throws -> OCKAnyContact { + guard let contact = objectWillChange.value else { + throw OCKContactControllerError.nilContact + } + return contact + } +} + +private enum OCKContactControllerError: Error, LocalizedError { + case nilContact + case invalidAddress(_ address: OCKPostalAddress?) + case invalidPhoneNumber(_ phoneNumber: String?) + case invalidMessageNumber(_ messageNumber: String?) + case invalidEmail(_ email: String?) + case cannotSendMail + case cannotSendMessages + + var errorDescription: String? { + switch self { + case .nilContact: return "Contact view model is nil" + case .invalidAddress(let address): return "Invalid address: \(String(describing: address))" + case .invalidPhoneNumber(let phoneNumber): return "Invalid phone number: \(String(describing: phoneNumber))" + case .invalidMessageNumber(let messageNumber): return "Invalid message number: \(String(describing: messageNumber))" + case .invalidEmail(let email): return "Invalid email: \(String(describing: email))" + case .cannotSendMail: return "Cannot send mail" + case .cannotSendMessages: return "Cannot send messages" + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactControllerProtocol.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactControllerProtocol.swift new file mode 100644 index 000000000..6eba2f98a --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKContactControllerProtocol.swift @@ -0,0 +1,65 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Combine +import Contacts +import ContactsUI +import Foundation +import MapKit +import MessageUI + +/// Describes an object capable of tracking and updating the state of a contact. +public protocol OCKContactControllerProtocol { + + /// A reference to a writable store. + var store: OCKAnyContactStore { get } + + /// A publisher that publishers new values when the watched contact changes in the store. + var objectWillChange: CurrentValueSubject<OCKAnyContact?, Never> { get } + + // MARK: Implementation Provided + + /// Initiate a phone call and return the URL to be dialed. + func initiateCall() throws -> URL + + /// Initiate a message and return the `MFMessageComposeViewController` that needs to be presented. + func initiateMessage() throws -> MFMessageComposeViewController + + /// Initiate an email and return the `MFMailComposeViewController` that should be presented. + func initiateEmail() throws -> MFMailComposeViewController + + /// Lookup the address of the current contact. + /// - Parameter completion: A closure to be called with the result + func initiateAddressLookup(completion: @escaping (Result<MKMapItem, Error>) -> Void) + + /// Attempt to find contact in the system contacts and return a `CNContactViewController` to display it. + func initiateSystemContactLookup() throws -> CNContactViewController +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKDetailedContactController.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKDetailedContactController.swift new file mode 100644 index 000000000..0a5025000 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKDetailedContactController.swift @@ -0,0 +1,33 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKDetailedContactController = OCKContactController diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKSimpleContactController.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKSimpleContactController.swift new file mode 100644 index 000000000..9bd1ea495 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/Controllers/OCKSimpleContactController.swift @@ -0,0 +1,33 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKSimpleContactController = OCKContactController diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKContactViewSynchronizerProtocol.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKContactViewSynchronizerProtocol.swift new file mode 100644 index 000000000..61526211a --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKContactViewSynchronizerProtocol.swift @@ -0,0 +1,73 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +/// Describes a type erased view synchronizer for contacts. +public protocol OCKAnyContactViewSynchronizerProtocol { + + /// Initialize a view to be synchronized. + func makeAnyView() -> UIView & OCKContactDisplayable + + /// Update a view using the given context + /// - Parameters: + /// - view: The view to be updated + /// - context: Information about the update that is occurring. + func updateAnyView(_ view: UIView & OCKContactDisplayable, context: OCKSynchronizationContext<OCKAnyContact?>) +} + +/// Describes a view synchronizer for contacts. +public protocol OCKContactViewSynchronizerProtocol: OCKAnyContactViewSynchronizerProtocol { + + /// The type of the view that will be synchronized + associatedtype View: UIView & OCKContactDisplayable + + /// Initialize a view to be synchronized. + func makeView() -> View + + /// Update a view using the given context. + /// - Parameters: + /// - view: The view to be updated. + /// - context: Information about the update that is occurring. + func updateView(_ view: View, context: OCKSynchronizationContext<OCKAnyContact?>) +} + +public extension OCKContactViewSynchronizerProtocol { + func makeAnyView() -> UIView & OCKContactDisplayable { + return makeView() + } + + func updateAnyView(_ view: UIView & OCKContactDisplayable, context: OCKSynchronizationContext<OCKAnyContact?>) { + guard let typedView = view as? View else { fatalError("Type mismatch") } + updateView(typedView, context: context) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKDetailedContactViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKDetailedContactViewSynchronizer.swift new file mode 100644 index 000000000..2de150416 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKDetailedContactViewSynchronizer.swift @@ -0,0 +1,47 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A view synchroinzer that creates and updates an `OCKDetailedContactView`. +open class OCKDetailedContactViewSynchronizer: OCKContactViewSynchronizerProtocol { + + public init() {} + + open func updateView(_ view: OCKDetailedContactView, context: OCKSynchronizationContext<OCKAnyContact?>) { + view.updateWith(contact: context.viewModel, animated: context.animated) + } + + open func makeView() -> OCKDetailedContactView { + return .init() + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKSimpleContactViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKSimpleContactViewSynchronizer.swift new file mode 100644 index 000000000..1df147b75 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/Synchronizers/OCKSimpleContactViewSynchronizer.swift @@ -0,0 +1,47 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A view synchronizer that creates and updates an `OCKSimpleContactView`. +open class OCKSimpleContactViewSynchronizer: OCKContactViewSynchronizerProtocol { + + public init() {} + + open func updateView(_ view: OCKSimpleContactView, context: OCKSynchronizationContext<OCKAnyContact?>) { + view.updateWith(contact: context.viewModel, animated: context.animated) + } + + open func makeView() -> OCKSimpleContactView { + return .init() + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKContactViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKContactViewController.swift new file mode 100644 index 000000000..c01157de2 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKContactViewController.swift @@ -0,0 +1,241 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import MessageUI +import UIKit + +/// Types wishing to receive updates from contact view controllers can conform to this protocol. +public protocol OCKContactViewControllerDelegate: AnyObject { + + /// Called when an unhandled error is encountered in a contact view controller. + /// - Parameters: + /// - viewController: The view controller in which the error was encountered. + /// - didEncounterError: The error that was unhandled. + func contactViewController<C: OCKContactControllerProtocol, VS: OCKContactViewSynchronizerProtocol>( + _ viewController: OCKContactViewController<C, VS>, didEncounterError: Error) +} + +/// A view controller that displays a contact view and keep it synchronized with a store. +open class OCKContactViewController<Controller: OCKContactControllerProtocol, ViewSynchronizer: OCKContactViewSynchronizerProtocol>: +UIViewController, OCKContactViewDelegate, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate { + + // MARK: Properties + + /// If set, the delegate will receive updates when import events happen + public weak var delegate: OCKContactViewControllerDelegate? + + /// Handles the responsibility of updating the view when data in the store changes. + public let viewSynchronizer: ViewSynchronizer + + /// Handles the responsibility of interacting with data from the store. + public let controller: Controller + + /// The view that is being synchronized against the store. + public var contactView: ViewSynchronizer.View { + guard let view = self.view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } + return view + } + + private var viewDidLoadCompletion: (() -> Void)? + private var viewModelSubscription: AnyCancellable? + + // MARK: - Life Cycle + + /// Initialize with a controller and synchronizer. + public init(controller: Controller, viewSynchronizer: ViewSynchronizer) { + self.controller = controller + self.viewSynchronizer = viewSynchronizer + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + override open func loadView() { + view = viewSynchronizer.makeView() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func viewDidLoad() { + super.viewDidLoad() + contactView.delegate = self + + // Begin listening for changes in the view model. Note, when we subscribe to the view model, it sends its current value through the stream + startObservingViewModel() + + viewDidLoadCompletion?() + } + + // MARK: - Methods + + // Create a subscription that updates the view when the view model is updated. + private func startObservingViewModel() { + viewModelSubscription?.cancel() + viewModelSubscription = controller.objectWillChange + .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) + } + } + + @objc + private func dismissViewController() { + dismiss(animated: true, completion: nil) + } + + func handleResult<Success>(_ result: Result<Success, Error>, successCompletion: (_ value: Success) -> Void) { + switch result { + case .failure(let error): delegate?.contactViewController(self, didEncounterError: error) + case .success(let value): successCompletion(value) + } + } + + func handleThrowable<T>(method: () throws -> T, success: (T) -> Void) { + do { + let result = try method() + success(result) + } catch { + delegate?.contactViewController(self, didEncounterError: error) + } + } + + // MARK: - OCKContactViewDelegate + + /// Present an alert to call the contact. By default, calls the first phone number in the contact's list of phone numbers. + /// - Parameter contactView: The view that displays the contact. + /// - Parameter sender: The sender that is initiating the call process. + open func contactView(_ contactView: UIView & OCKContactDisplayable, senderDidInitiateCall sender: Any?) { + handleThrowable(method: controller.initiateCall) { url in + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + /// Present the UI to message the contact. By default, the first messaging number will be used. + /// - Parameter contactView: The view that displays the contact. + /// - Parameter sender: The sender that is initiating the messaging process. + open func contactView(_ contactView: UIView & OCKContactDisplayable, senderDidInitiateMessage sender: Any?) { + handleThrowable(method: controller.initiateMessage) { [weak self] viewController in + guard let self = self else { return } + viewController.messageComposeDelegate = self + self.present(viewController, animated: true, completion: nil) + } + } + + /// Present the UI to email the contact. By default, the first email address will be used. + /// - Parameter contactView: The view that displays the contact. + /// - Parameter sender: The sender that is initiating the email process. + open func contactView(_ contactView: UIView & OCKContactDisplayable, senderDidInitiateEmail sender: Any?) { + handleThrowable(method: controller.initiateEmail) { [weak self] viewController in + guard let self = self else { return } + viewController.mailComposeDelegate = self + self.present(viewController, animated: true, completion: nil) + } + } + + /// Present a map with a marker on the contact's address. + /// - Parameter contactView: The view that displays the contact. + /// - Parameter sender: The sender that is initiating the address lookup process. + open func contactView(_ contactView: UIView & OCKContactDisplayable, senderDidInitiateAddressLookup sender: Any?) { + controller.initiateAddressLookup { [weak self] result in + self?.handleResult(result) { mapItem in + mapItem.openInMaps(launchOptions: nil) + } + } + } + + open func didSelectContactView(_ contactView: UIView & OCKContactDisplayable) { + handleThrowable(method: controller.initiateSystemContactLookup) { [weak self] contactViewController in + contactViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, + action: #selector(dismissViewController)) + let navigationController = UINavigationController(rootViewController: contactViewController) + present(navigationController, animated: true, completion: nil) + } + } + + // MARK: - MFMessageComposeViewControllerDelegate + + open func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) { + controller.dismiss(animated: true, completion: nil) + } + + // MARK: - MFMailComposeViewControllerDelegate + + open func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true, completion: nil) + } +} + +public extension OCKContactViewController where Controller: OCKContactController { + + /// Initialize a view controller that displays a contact. Fetches and stays synchronized with the contact. + /// - Parameter viewSynchronizer: Manages the contact view. + /// - Parameter query: Used to fetch the contact to display. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(viewSynchronizer: ViewSynchronizer, query: OCKAnyContactQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(controller: .init(storeManager: storeManager), viewSynchronizer: viewSynchronizer) + viewDidLoadCompletion = { [weak self] in + self?.controller.fetchAndObserveContact(forQuery: query, errorHandler: { [weak self] error in + guard let self = self else { return } + self.delegate?.contactViewController(self, didEncounterError: error) + }) + } + } + + /// Initialize a view controller that displays a contact in the store. Stays synchronized with the provided contact. + /// - Parameter viewSynchronizer: Manages the contact view. + /// - Parameter contact: The contact to display. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(viewSynchronizer: ViewSynchronizer, contact: OCKAnyContact, storeManager: OCKSynchronizedStoreManager) { + self.init(controller: .init(storeManager: storeManager), viewSynchronizer: viewSynchronizer) + viewDidLoadCompletion = { [weak self] in + self?.controller.observeContact(contact) + } + } + + /// Initialize a view controller that displays a contact. Fetches and stays synchronized with the contact. + /// - Parameter viewSynchronizer: Manages the contact view. + /// - Parameter contactID: The user-defined unique identifier for the contact to fetch. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(viewSynchronizer: ViewSynchronizer, contactID: String, storeManager: OCKSynchronizedStoreManager) { + self.init(controller: .init(storeManager: storeManager), viewSynchronizer: viewSynchronizer) + viewDidLoadCompletion = { [weak self] in + self?.controller.fetchAndObserveContact(withID: contactID, errorHandler: { [weak self] error in + guard let self = self else { return } + self.delegate?.contactViewController(self, didEncounterError: error) + }) + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKDetailedContactViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKDetailedContactViewController.swift new file mode 100644 index 000000000..030a88df2 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKDetailedContactViewController.swift @@ -0,0 +1,57 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKDetailedContactViewController = OCKContactViewController<OCKDetailedContactController, OCKDetailedContactViewSynchronizer> + +public extension OCKContactViewController where Controller: OCKSimpleContactController, ViewSynchronizer == OCKDetailedContactViewSynchronizer { + + /// Initialize a view controller that displays a contact in the store. Stays synchronized with the provided contact. + /// - Parameter contact: The contact to display. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(contact: OCKAnyContact, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), contact: contact, storeManager: storeManager) + } + + /// Initialize a view controller that displays a contact. Fetches and stays synchronized with the contact. + /// - Parameter query: Used to fetch the contact to display. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(query: OCKAnyContactQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), query: query, storeManager: storeManager) + } + + /// Initialize a view controller that displays a contact. Fetches and stays synchronized with the contact. + /// - Parameter contactID: The user-defined unique identifier for the contact to fetch. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(contactID: String, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), contactID: contactID, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKSimpleContactViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKSimpleContactViewController.swift new file mode 100644 index 000000000..cc9b93705 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Contact/View Controllers/OCKSimpleContactViewController.swift @@ -0,0 +1,58 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Foundation + +public typealias OCKSimpleContactViewController = OCKContactViewController<OCKSimpleContactController, OCKSimpleContactViewSynchronizer> + +public extension OCKContactViewController where Controller: OCKSimpleContactController, ViewSynchronizer == OCKSimpleContactViewSynchronizer { + + /// Initialize a view controller that displays a contact in the store. Stays synchronized with the provided contact. + /// - Parameter contact: The contact to display. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(contact: OCKAnyContact, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), contact: contact, storeManager: storeManager) + } + + /// Initialize a view controller that displays a contact. Fetches and stays synchronized with the contact. + /// - Parameter query: Used to fetch the contact to display. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(query: OCKAnyContactQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), query: query, storeManager: storeManager) + } + + /// Initialize a view controller that displays a contact. Fetches and stays synchronized with the contact. + /// - Parameter contactID: The user-defined unique identifier for the contact to fetch. + /// - Parameter storeManager: Wraps the store that contains the contact to fetch. + convenience init(contactID: String, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), contactID: contactID, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKStoreNotifications.swift b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKStoreNotifications.swift new file mode 100644 index 000000000..abf80c06d --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKStoreNotifications.swift @@ -0,0 +1,70 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Foundation + +public protocol OCKStoreNotification {} + +public enum OCKStoreNotificationCategory { + case add + case update + case delete +} + +public struct OCKPatientNotification: OCKStoreNotification { + public let patient: OCKAnyPatient + public let category: OCKStoreNotificationCategory + public let storeManager: OCKSynchronizedStoreManager +} + +public struct OCKCarePlanNotification: OCKStoreNotification { + public let carePlan: OCKAnyCarePlan + public let category: OCKStoreNotificationCategory + public let storeManager: OCKSynchronizedStoreManager +} + +public struct OCKContactNotification: OCKStoreNotification { + public let contact: OCKAnyContact + public let category: OCKStoreNotificationCategory + public let storeManager: OCKSynchronizedStoreManager +} + +public struct OCKTaskNotification: OCKStoreNotification { + public let task: OCKAnyTask + public let category: OCKStoreNotificationCategory + public 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/OCKSynchronizationContext.swift b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizationContext.swift new file mode 100644 index 000000000..72a141a39 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizationContext.swift @@ -0,0 +1,76 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Combine +import Foundation + +/// Provides context for a view model value. +public struct OCKSynchronizationContext<ViewModel> { + /// The current view model value. + public let viewModel: ViewModel + + /// The previous view model value. + public let oldViewModel: ViewModel + + /// Animated flag. + public let animated: Bool + + public init(viewModel: ViewModel, oldViewModel: ViewModel, animated: Bool) { + self.viewModel = viewModel + self.oldViewModel = oldViewModel + self.animated = animated + } +} + +protocol OptionalProtocol { + func isSome() -> Bool +} + +extension Optional: OptionalProtocol { + func isSome() -> Bool { + switch self { + case .some: return true + default: return false + } + } +} + +extension CurrentValueSubject { + func context() -> Publishers.Scan<CurrentValueSubject<Output, Failure>, OCKSynchronizationContext<Output>> { + // If the `Output` is an optional, only animate if the preious value was nil. This helps stops animations from occuring on the initial load. + let animated = value as? OptionalProtocol != nil ? (value as? OptionalProtocol)!.isSome() : true + let context = OCKSynchronizationContext(viewModel: value, oldViewModel: value, animated: animated) + return scan(context) { previousContext, newValue -> OCKSynchronizationContext<Output> in + let animated = previousContext.viewModel as? OptionalProtocol != nil ? + (previousContext.viewModel as? OptionalProtocol)!.isSome() : true + return OCKSynchronizationContext(viewModel: newValue, oldViewModel: previousContext.viewModel, animated: animated) + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager+Publishers.swift b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager+Publishers.swift new file mode 100644 index 000000000..46ba16234 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager+Publishers.swift @@ -0,0 +1,169 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Combine +import Foundation + +extension OCKSynchronizedStoreManager { + + // MARK: Patients + + func publisher(forPatient patient: OCKAnyPatient, + categories: [OCKStoreNotificationCategory]) -> AnyPublisher<OCKAnyPatient, Never> { + let presentValuePublisher = Future<OCKAnyPatient, Never>({ completion in + self.store.fetchAnyPatient(withID: patient.id) { result in + completion(.success((try? result.get()) ?? patient)) + } + }) + + return AnyPublisher(notificationPublisher + .compactMap { $0 as? OCKPatientNotification } + .filter { $0.patient.id == patient.id && categories.contains($0.category) } + .map { $0.patient } + .prepend(presentValuePublisher)) + } + + // MARK: CarePlans + + func publisher(forCarePlan plan: OCKAnyCarePlan, + categories: [OCKStoreNotificationCategory]) -> AnyPublisher<OCKAnyCarePlan, Never> { + let presentValuePublisher = Future<OCKAnyCarePlan, Never> { completion in + self.store.fetchAnyCarePlan(withID: plan.id) { result in + completion(.success((try? result.get()) ?? plan)) + } + } + + return AnyPublisher(notificationPublisher + .compactMap { $0 as? OCKCarePlanNotification } + .filter { $0.carePlan.id == plan.id && categories.contains($0.category) } + .map { $0.carePlan } + .prepend(presentValuePublisher)) + } + + // MARK: Contacts + + func contactsPublisher(categories: [OCKStoreNotificationCategory]) -> AnyPublisher<OCKAnyContact, Never> { + return AnyPublisher(notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { categories.contains($0.category) } + .map { $0.contact }) + } + + func publisher(forContactID id: String, + categories: [OCKStoreNotificationCategory]) -> AnyPublisher<OCKAnyContact, Never> { + return notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { $0.contact.id == id && categories.contains($0.category) } + .map { $0.contact } + .eraseToAnyPublisher() + } + + func publisher(forContact contact: OCKAnyContact, + categories: [OCKStoreNotificationCategory], + fetchImmediately: Bool = true) -> AnyPublisher<OCKAnyContact, Never> { + let presentValuePublisher = Future<OCKAnyContact, Never>({ completion in + self.store.fetchAnyContact(withID: contact.id) { result in + completion(.success((try? result.get()) ?? contact)) + } + }) + + let changePublisher = notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { $0.contact.id == contact.id && categories.contains($0.category) } + .map { $0.contact } + + return fetchImmediately ? AnyPublisher(changePublisher.prepend(presentValuePublisher)) : AnyPublisher(changePublisher) + } + + // MARK: Tasks + + func publisher(forTask task: OCKAnyTask, categories: [OCKStoreNotificationCategory], + fetchImmediately: Bool = true) -> AnyPublisher<OCKAnyTask, Never> { + let presentValuePublisher = Future<OCKAnyTask, Never>({ completion in + self.store.fetchAnyTask(withID: task.id) { result in + completion(.success((try? result.get()) ?? task)) + } + }) + + let publisher = notificationPublisher + .compactMap { $0 as? OCKTaskNotification } + .filter { $0.task.id == task.id && categories.contains($0.category) } + .map { $0.task } + + return fetchImmediately ? AnyPublisher(publisher.append(presentValuePublisher)) : AnyPublisher(publisher) + } + + func publisher(forEventsBelongingToTask task: OCKAnyTask, + categories: [OCKStoreNotificationCategory]) -> AnyPublisher<OCKAnyEvent, Never> { + return AnyPublisher(notificationPublisher + .compactMap { $0 as? OCKOutcomeNotification } + .filter { $0.outcome.belongs(to: task) && categories.contains($0.category) } + .map { self.makeEvent(task: task, outcome: $0.outcome, keepOutcome: $0.category != .delete) }) + } + + func publisher(forEventsBelongingToTask task: OCKAnyTask, query: OCKEventQuery, + categories: [OCKStoreNotificationCategory]) -> AnyPublisher<OCKAnyEvent, Never> { + + let validIndices = task.schedule.events(from: query.dateInterval.start, to: query.dateInterval.end) + .map { $0.occurrence } + + return publisher(forEventsBelongingToTask: task, categories: categories) + .filter { validIndices.contains($0.scheduleEvent.occurrence) } + .eraseToAnyPublisher() + } + + private func makeEvent(task: OCKAnyTask, outcome: OCKAnyOutcome, keepOutcome: Bool) -> OCKAnyEvent { + guard let scheduleEvent = task.schedule.event(forOccurrenceIndex: outcome.taskOccurrenceIndex) else { + fatalError("The outcome had an index of \(outcome.taskOccurrenceIndex), but the task's schedule doesn't have that many events.") + } + return OCKAnyEvent(task: task, outcome: keepOutcome ? outcome : nil, scheduleEvent: scheduleEvent) + } + + // MARK: Events + + func publisher(forEvent event: OCKAnyEvent, categories: [OCKStoreNotificationCategory]) -> AnyPublisher<OCKAnyEvent, Never> { + let presentValuePublisher = Future<OCKAnyEvent, Never>({ completion in + self.store.fetchAnyEvent(forTask: event.task, occurrence: event.scheduleEvent.occurrence, callbackQueue: .main) { result in + completion(.success((try? result.get()) ?? event)) + } + }) + + return AnyPublisher(notificationPublisher + .compactMap { $0 as? OCKOutcomeNotification } + .filter { self.outcomeMatchesEvent(outcome: $0.outcome, event: event) } + .map { self.makeEvent(task: event.task, outcome: $0.outcome, keepOutcome: $0.category != .delete) } + .prepend(presentValuePublisher)) + } + + private func outcomeMatchesEvent(outcome: OCKAnyOutcome, event: OCKAnyEvent) -> Bool { + outcome.belongs(to: event.task) && event.scheduleEvent.occurrence == outcome.taskOccurrenceIndex + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager.swift b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager.swift new file mode 100644 index 000000000..14185121c --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Synchronization/OCKSynchronizedStoreManager.swift @@ -0,0 +1,180 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Combine +import Foundation + +/// An `OCKSynchronizedStoreManager` wraps any store that conforms to `OCKStore` and provides synchronization to CareKit view +/// controllers by listening in on the store's activity by setting itself as the store delegate. +open class OCKSynchronizedStoreManager: +OCKPatientStoreDelegate, OCKCarePlanStoreDelegate, OCKContactStoreDelegate, OCKTaskStoreDelegate, OCKOutcomeStoreDelegate { + + /// The underlying database. + public let store: OCKAnyStoreProtocol + + internal lazy var subject = PassthroughSubject<OCKStoreNotification, Never>() + public private (set) lazy var notificationPublisher = subject.share().eraseToAnyPublisher() + + /// Initialize by wrapping a store. + /// + /// - Parameters: + /// - store: Any object that conforms to `OCKStoreProtocol`. + /// + /// - SeeAlso: `OCKStore` + public init(wrapping store: OCKAnyStoreProtocol) { + self.store = store + self.store.patientDelegate = self + self.store.carePlanDelegate = self + self.store.contactDelegate = self + self.store.taskDelegate = self + self.store.outcomeDelegate = self + } + + // MARK: OCKStoreDelegate Patients + + open func patientStore(_ store: OCKAnyReadOnlyPatientStore, didAddPatients patients: [OCKAnyPatient]) { + dispatchPatientNotifications(category: .add, patients: patients) + } + + open func patientStore(_ store: OCKAnyReadOnlyPatientStore, didUpdatePatients patients: [OCKAnyPatient]) { + dispatchPatientNotifications(category: .update, patients: patients) + } + + open func patientStore(_ store: OCKAnyReadOnlyPatientStore, didDeletePatients patients: [OCKAnyPatient]) { + dispatchPatientNotifications(category: .delete, patients: patients) + } + + private func dispatchPatientNotifications(category: OCKStoreNotificationCategory, patients: [OCKAnyPatient]) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + patients.forEach { + let notifications = OCKPatientNotification(patient: $0, category: category, storeManager: self) + self.subject.send(notifications) + } + } + } + + // MARK: OCKStoreDelegate CarePlans + + open func carePlanStore(_ store: OCKAnyReadOnlyCarePlanStore, didAddCarePlans carePlans: [OCKAnyCarePlan]) { + dispatchCarePlanNotifications(category: .add, plans: carePlans) + } + + open func carePlanStore(_ store: OCKAnyReadOnlyCarePlanStore, didUpdateCarePlans carePlans: [OCKAnyCarePlan]) { + dispatchCarePlanNotifications(category: .update, plans: carePlans) + } + + open func carePlanStore(_ store: OCKAnyReadOnlyCarePlanStore, didDeleteCarePlans carePlans: [OCKAnyCarePlan]) { + dispatchCarePlanNotifications(category: .delete, plans: carePlans) + } + + private func dispatchCarePlanNotifications(category: OCKStoreNotificationCategory, plans: [OCKAnyCarePlan]) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + plans.forEach { + let notification = OCKCarePlanNotification(carePlan: $0, category: category, storeManager: self) + self.subject.send(notification) + } + } + } + + // MARK: OCKStoreDelegate Contacts + + open func contactStore(_ store: OCKAnyReadOnlyContactStore, didAddContacts contacts: [OCKAnyContact]) { + dispatchContactNotifications(category: .add, contacts: contacts) + } + + open func contactStore(_ store: OCKAnyReadOnlyContactStore, didUpdateContacts contacts: [OCKAnyContact]) { + dispatchContactNotifications(category: .update, contacts: contacts) + } + + open func contactStore(_ store: OCKAnyReadOnlyContactStore, didDeleteContacts contacts: [OCKAnyContact]) { + dispatchContactNotifications(category: .delete, contacts: contacts) + } + + private func dispatchContactNotifications(category: OCKStoreNotificationCategory, contacts: [OCKAnyContact]) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + contacts.forEach { + let notification = OCKContactNotification(contact: $0, category: category, storeManager: self) + self.subject.send(notification) + } + } + } + + // MARK: OCKStoreDelegate Tasks + + open func taskStore(_ store: OCKAnyReadOnlyTaskStore, didAddTasks tasks: [OCKAnyTask]) { + dispatchTaskNotifications(category: .add, tasks: tasks) + } + + open func taskStore(_ store: OCKAnyReadOnlyTaskStore, didUpdateTasks tasks: [OCKAnyTask]) { + dispatchTaskNotifications(category: .update, tasks: tasks) + } + + open func taskStore(_ store: OCKAnyReadOnlyTaskStore, didDeleteTasks tasks: [OCKAnyTask]) { + dispatchTaskNotifications(category: .delete, tasks: tasks) + } + + private func dispatchTaskNotifications(category: OCKStoreNotificationCategory, tasks: [OCKAnyTask]) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + tasks.forEach { + let notification = OCKTaskNotification(task: $0, category: category, storeManager: self) + self.subject.send(notification) + } + } + } + + // MARK: OCKStoreDelegate Outcomes + + open func outcomeStore(_ store: OCKAnyReadOnlyOutcomeStore, didAddOutcomes outcomes: [OCKAnyOutcome]) { + dispatchOutcomeNotifications(category: .add, outcomes: outcomes) + } + + open func outcomeStore(_ store: OCKAnyReadOnlyOutcomeStore, didUpdateOutcomes outcomes: [OCKAnyOutcome]) { + dispatchOutcomeNotifications(category: .update, outcomes: outcomes) + } + + open func outcomeStore(_ store: OCKAnyReadOnlyOutcomeStore, didDeleteOutcomes outcomes: [OCKAnyOutcome]) { + dispatchOutcomeNotifications(category: .delete, outcomes: outcomes) + } + + private func dispatchOutcomeNotifications(category: OCKStoreNotificationCategory, outcomes: [OCKAnyOutcome]) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + outcomes.forEach { + let notification = OCKOutcomeNotification(outcome: $0, category: category, storeManager: self) + self.subject.send(notification) + } + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKButtonLogTaskController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKButtonLogTaskController.swift new file mode 100644 index 000000000..b857b8cb9 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKButtonLogTaskController.swift @@ -0,0 +1,39 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Combine + +/// A task controller that keeps a record of events sorted by their dates. +open class OCKButtonLogTaskController: OCKTaskController { + override func modified(event: OCKAnyEvent) -> OCKAnyEvent { + return event.sortedOutcomeValuesByRecency() + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKChecklistTaskController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKChecklistTaskController.swift new file mode 100644 index 000000000..c96fb7e91 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKChecklistTaskController.swift @@ -0,0 +1,33 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKChecklistTaskController = OCKTaskController diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKGridTaskController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKGridTaskController.swift new file mode 100644 index 000000000..2c57baa82 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKGridTaskController.swift @@ -0,0 +1,33 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKGridTaskController = OCKTaskController diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKInstructionsTaskController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKInstructionsTaskController.swift new file mode 100644 index 000000000..2e30351bc --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKInstructionsTaskController.swift @@ -0,0 +1,33 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKInstructionsTaskController = OCKTaskController diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKSimpleTaskController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKSimpleTaskController.swift new file mode 100644 index 000000000..6a4799d1f --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKSimpleTaskController.swift @@ -0,0 +1,33 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKSimpleTaskController = OCKTaskController diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskController.swift new file mode 100644 index 000000000..4bc2d38ed --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskController.swift @@ -0,0 +1,144 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Combine +import Foundation + +/// A basic controller capable of watching and updating tasks. +open class OCKTaskController: OCKTaskControllerProtocol, ObservableObject { + + // MARK: OCKTaskControllerProtocol + + public let objectWillChange: CurrentValueSubject<OCKTaskEvents?, Never> + public var store: OCKAnyOutcomeStore { storeManager.store } + + // MARK: - Properties + + /// The store manager against which the task will be synchronized. + public let storeManager: OCKSynchronizedStoreManager + + private var cancellables: Set<AnyCancellable> = Set() + + // MARK: - Life Cycle + + /// Initialize with a store manager. + public required init(storeManager: OCKSynchronizedStoreManager) { + self.storeManager = storeManager + self.objectWillChange = .init(nil) + } + + // MARK: - Methods + + /// Begin observing a task. + /// + /// - Parameters: + /// - 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) { + fetchAndObserveEvents(forTaskIDs: [task.id], eventQuery: eventQuery, errorHandler: errorHandler) + } + + /// Begin watching events from multiple tasks for changes. + /// + /// - Parameters: + /// - 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) + taskQuery.ids = taskIDs + + // Fetch the tasks, then fetch and subscribe to events for the tasks + storeManager.store.fetchAnyTasks(query: taskQuery, callbackQueue: .main) { [weak self] result in + switch result { + case .failure(let error): errorHandler?(error) + case .success(let tasks): + tasks.forEach { + 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) + } + } + } + } + + /// Begin watching a single task's events for changes. + /// + /// - Parameters: + /// - taskID: The user-chosen unique identifier for the task to be watched. + /// - eventQuery: A query describing the date range over which to watch for changes. + open func fetchAndObserveEvents(forTaskID taskID: String, eventQuery: OCKEventQuery, errorHandler: ((Error) -> Void)? = nil) { + fetchAndObserveEvents(forTaskIDs: [taskID], eventQuery: eventQuery, errorHandler: errorHandler) + } + + func updateViewModel(withEvents events: [OCKAnyEvent]) { + let taskIds = events.map { $0.task.id } + 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 = OCKTaskEvents() + events.map { self.modified(event: $0) } + .sorted(by: { $0.scheduleEvent.start < $1.scheduleEvent.start }) + .forEach { viewModel.addEvent($0) } + self.objectWillChange.value = viewModel + } + + func modified(event: OCKAnyEvent) -> OCKAnyEvent { + return event + } + + // Update the view model when events for a particular task change + func subscribeTo(eventsBelongingToTask task: OCKAnyTask, eventQuery: OCKEventQuery) { + 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) { + storeManager.store.fetchAnyEvents(taskID: task.id, query: query, callbackQueue: .main) { [weak self] result in + switch result { + case .failure(let error): errorHandler?(error) + case .success(let events): + self?.updateViewModel(withEvents: events) + self?.subscribeTo(eventsBelongingToTask: task, eventQuery: query) + } + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskControllerProtocol+Methods.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskControllerProtocol+Methods.swift new file mode 100644 index 000000000..f7463fc46 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskControllerProtocol+Methods.swift @@ -0,0 +1,195 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +public extension OCKTaskControllerProtocol { + + func setEvent(atIndexPath indexPath: IndexPath, isComplete: Bool, completion: ((Result<OCKAnyOutcome, Error>) -> Void)?) { + let event: OCKAnyEvent + do { + _ = try validatedViewModel() + event = try validatedEvent(forIndexPath: indexPath) + } catch { + completion?(.failure(error)) + return + } + + // If the event is complete, create an outcome with a `true` value + if isComplete { + do { + let outcome = try makeOutcomeFor(event: event, withValues: [.init(true)]) + store.addAnyOutcome(outcome) { result in + switch result { + case .failure(let error): completion?(.failure(error)) + case .success(let outcome): completion?(.success(outcome)) + } + } + } catch { + completion?(.failure(error)) + } + + // if the event is incomplete, delete the outcome + } else { + guard let outcome = event.outcome else { return } + store.deleteAnyOutcome(outcome) { result in + switch result { + case .failure(let error): completion?(.failure(error)) + case .success(let outcome): completion?(.success(outcome)) + } + } + } + } + + func appendOutcomeValue(withType underlyingType: OCKOutcomeValueUnderlyingType, at indexPath: IndexPath, + completion: ((Result<OCKAnyOutcome, Error>) -> Void)?) { + let event: OCKAnyEvent + do { + _ = try validatedViewModel() + event = try validatedEvent(forIndexPath: indexPath) + } catch { + completion?(.failure(error)) + return + } + + let value = OCKOutcomeValue(underlyingType) + + // Update the outcome with the new value + if var outcome = event.outcome { + outcome.values.append(value) + store.updateAnyOutcome(outcome, callbackQueue: .main) { result in + completion?(result.mapError { $0 as Error }) + } + + // Else Save a new outcome if one does not exist + } else { + do { + let outcome = try makeOutcomeFor(event: event, withValues: [value]) + store.addAnyOutcome(outcome, callbackQueue: .main) { result in + completion?(result.mapError { $0 as Error }) + } + } catch { + completion?(.failure(error)) + } + } + } + + func makeOutcomeFor(event: OCKAnyEvent, withValues values: [OCKOutcomeValue]) throws -> OCKAnyOutcome { + guard + let task = event.task as? OCKAnyVersionableTask, + let taskID = task.uuid else { throw OCKTaskControllerError.cannotMakeOutcomeFor(event) } + return OCKOutcome(taskUUID: taskID, taskOccurrenceIndex: event.scheduleEvent.occurrence, values: values) + } + + func eventFor(indexPath: IndexPath) -> OCKAnyEvent? { + return objectWillChange.value?.event(forIndexPath: indexPath) + } + + func validatedViewModel() throws -> OCKTaskEvents { + guard let taskEvents = objectWillChange.value else { + throw OCKTaskControllerError.nilTaskEvent + } + return taskEvents + } + + func validatedEvent(forIndexPath indexPath: IndexPath) throws -> OCKAnyEvent { + guard let event = eventFor(indexPath: indexPath) else { + throw OCKTaskControllerError.invalidIndexPath(indexPath) + } + return event + } + + private func deleteOutcomeValue(at index: Int, for outcome: OCKAnyOutcome, completion: ((Result<OCKAnyOutcome, Error>) -> Void)?) { + // delete the whole outcome if there is only one outcome value remaining + guard outcome.values.count > 1 else { + store.deleteAnyOutcome(outcome, callbackQueue: .main) { result in + completion?(result.mapError { $0 as Error }) + } + return + } + + // Else delete the value from the outcome + var newOutcome = outcome + newOutcome.values.remove(at: index) + store.updateAnyOutcome(newOutcome, callbackQueue: .main) { result in + completion?(result.mapError { $0 as Error }) + } + } + + func initiateDeletionForOutcomeValue(atIndex index: Int, eventIndexPath: IndexPath, + deletionCompletion: ((Result<OCKAnyOutcome, Error>) -> Void)?) throws -> UIAlertController { + _ = try validatedViewModel() + let event = try validatedEvent(forIndexPath: eventIndexPath) + + // Make sure there is an outcome value to delete + guard + let outcome = event.outcome, + index < outcome.values.count else { + throw OCKTaskControllerError.noOutcomeValueForEvent(event, index) + } + + // Make an action sheet to delete the outcome value + let actionSheet = UIAlertController(title: loc("LOG_ENTRY"), message: nil, preferredStyle: .actionSheet) + let cancel = UIAlertAction(title: loc("CANCEL"), style: .default, handler: nil) + let delete = UIAlertAction(title: loc("DELETE"), style: .destructive) { [weak self] _ in + self?.deleteOutcomeValue(at: index, for: outcome, completion: deletionCompletion) + } + [delete, cancel].forEach { actionSheet.addAction($0) } + return actionSheet + } + + func initiateDetailsViewController(forIndexPath indexPath: IndexPath) throws -> OCKDetailViewController { + _ = try validatedViewModel() + let task = try validatedEvent(forIndexPath: indexPath).task + + let detailViewController = OCKDetailViewController() + detailViewController.detailView.titleLabel.text = task.title + detailViewController.detailView.instructionsLabel.text = task.instructions + return detailViewController + } +} + +enum OCKTaskControllerError: Error, LocalizedError { + case nilTaskEvent + case invalidIndexPath(_ indexPath: IndexPath) + case noOutcomeValueForEvent(_ event: OCKAnyEvent, _ index: Int) + case cannotMakeOutcomeFor(_ event: OCKAnyEvent) + + var errorDescription: String? { + switch self { + case .nilTaskEvent: return "Task events view model is nil" + case let .noOutcomeValueForEvent(event, index): return "Event has no outcome value at index \(index): \(event)" + case .invalidIndexPath(let indexPath): return "Invalid index path \(indexPath)" + case .cannotMakeOutcomeFor(let event): return "Cannot make outcome for event: \(event)" + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskControllerProtocol.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskControllerProtocol.swift new file mode 100644 index 000000000..ee8a0e856 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskControllerProtocol.swift @@ -0,0 +1,83 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Combine +import Foundation +import UIKit + +/// Describes an object capable of tracking and updating the state of a task. +public protocol OCKTaskControllerProtocol: AnyObject { + + /// A reference to a writable store. + var store: OCKAnyOutcomeStore { get } + + /// A publisher that publishers new values when the watched task changes in the store. + var objectWillChange: CurrentValueSubject<OCKTaskEvents?, Never> { get } + + // MARK: Implementation Provided + + /// Create a detail view that dispays information about a task. + /// - Parameter indexPath: Index path of the event whose task should be displayed. + func initiateDetailsViewController(forIndexPath indexPath: IndexPath) throws -> OCKDetailViewController + + /// Set the completion state for an event. + /// - Parameters: + /// - indexPath: Index path of the event. + /// - isComplete: True if the event is complete. + /// - completion: Result after etting the completion for the event. + func setEvent(atIndexPath indexPath: IndexPath, isComplete: Bool, completion: ((Result<OCKAnyOutcome, Error>) -> Void)?) + + /// Append an outcome value to an event's outcome. + /// - Parameters: + /// - underlyingType: The value for the outcome value that is being created. + /// - indexPath: Index path of the event to which the outcome will be added. + /// - completion: Result after creating the outcome value. + func appendOutcomeValue(withType underlyingType: OCKOutcomeValueUnderlyingType, at indexPath: IndexPath, + completion: ((Result<OCKAnyOutcome, Error>) -> Void)?) + + /// Create a view with an option to delete an outcome value. + /// - Parameters: + /// - index: The index of the outcome value to delete. + /// - eventIndexPath: The index path of the event for which the outcome value will be deleted. + /// - deletionCompletion: The result from attempting to delete the outcome value. + func initiateDeletionForOutcomeValue(atIndex index: Int, eventIndexPath: IndexPath, + deletionCompletion: ((Result<OCKAnyOutcome, Error>) -> Void)?) throws -> UIAlertController + + /// Make an outcome for an event with the given outcome values. + /// - Parameters: + /// - event: The event for which to create the outcome. + /// - values: The outcome values to attach to the outcome. + func makeOutcomeFor(event: OCKAnyEvent, withValues values: [OCKOutcomeValue]) throws -> OCKAnyOutcome + + /// Return an event for a particular index path. Customize this method to define the indec path behavior used by other functions in this protocol. + /// - Parameter indexPath: The index path used to locate a particular event. + func eventFor(indexPath: IndexPath) -> OCKAnyEvent? +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskEvents.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskEvents.swift new file mode 100644 index 000000000..50d2bbd1f --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Controllers/OCKTaskEvents.swift @@ -0,0 +1,108 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Foundation + +private extension OCKAnyEvent { + + // Check if an event matches another event + func matches(_ other: OCKAnyEvent) -> Bool { + return + other.scheduleEvent.occurrence == scheduleEvent.occurrence && + other.task.id == task.id + } +} + +/// A data structure that holds events from a multiplicity of tasks. +public struct OCKTaskEvents { + + private var events: [String: [OCKAnyEvent]] = [:] // Maps a task identifier to a list of events belonging to that task + private var sortedKeys: [String] = [] // Task identifiers used to index `events`. Sorted by most recently added. + + /// Returns the first event, if any, among the events for all tasks. + public var firstEvent: OCKAnyEvent? { + return event(forIndexPath: .init(row: 0, section: 0)) + } + + /// Returns the events for the first task. + public var firstEvents: [OCKAnyEvent]? { + return events(forSection: 0) + } + + /// Adds an event. + public mutating func addEvent(_ event: OCKAnyEvent) { + let id = event.task.id + let modifiedEvent = event + + // Add the event to the dictionary based on the task identifier + if events[id] != nil { + guard events[id]!.allSatisfy({ !$0.matches(event) }) else { return } // Check for duplicates + events[id]?.append(modifiedEvent) + + // Else create a new dictionary key for the event + } else { + events[id] = [modifiedEvent] + sortedKeys.append(id) + } + } + + /// Updates an event, if it already exists, otherwise does nothing. + public mutating func updateEvent(_ event: OCKAnyEvent) { + let id = event.task.id + guard let index = events[id]?.firstIndex(where: { $0.matches(event) }) else { return } + events[id]?[index] = event + } + + /// Returns the event at an index path, where the section represents the task and the item corresponds to the event index. + public func event(forIndexPath indexPath: IndexPath) -> OCKAnyEvent? { + guard let events = events(forSection: indexPath.section) else { return nil } + guard indexPath.row < events.count else { return nil } + return events[indexPath.row] + } + + /// Returns all the events for the task in a section. + public func events(forSection section: Int) -> [OCKAnyEvent]? { + guard section < sortedKeys.count else { return nil } + guard let events = self.events[sortedKeys[section]] else { fatalError("Sorted key not found in dictionary") } + return events + } + + /// Returns true if the given event is already present. + public func containsEvent(_ event: OCKAnyEvent) -> Bool { + let id = event.task.id + for storedEvent in events[id] ?? [] { + if storedEvent.matches(event) { + return true + } + } + return false + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKButtonLogTaskViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKButtonLogTaskViewSynchronizer.swift new file mode 100644 index 000000000..393f6963e --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKButtonLogTaskViewSynchronizer.swift @@ -0,0 +1,46 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A synchronizer that creates and updates an `OCKButtonLogTaskView`. +open class OCKButtonLogTaskViewSynchronizer: OCKTaskViewSynchronizerProtocol { + public init() {} + + open func updateView(_ view: OCKButtonLogTaskView, context: OCKSynchronizationContext<OCKTaskEvents?>) { + view.updateWith(event: context.viewModel?.firstEvent, animated: context.animated) + } + + open func makeView() -> OCKButtonLogTaskView { + return .init() + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKChecklistTaskViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKChecklistTaskViewSynchronizer.swift new file mode 100644 index 000000000..db998bb23 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKChecklistTaskViewSynchronizer.swift @@ -0,0 +1,46 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A synchronizer specialized for the `OCKChecklistTaskView` +open class OCKChecklistTaskViewSynchronizer: OCKTaskViewSynchronizerProtocol { + public init() {} + + open func updateView(_ view: OCKChecklistTaskView, context: OCKSynchronizationContext<OCKTaskEvents?>) { + view.updateWith(events: context.viewModel?.firstEvents, animated: context.animated) + } + + open func makeView() -> OCKChecklistTaskView { + return .init() + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKGridTaskViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKGridTaskViewSynchronizer.swift new file mode 100644 index 000000000..11de45fe2 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKGridTaskViewSynchronizer.swift @@ -0,0 +1,46 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A view synchronizer that creates and updates an `OCKGridTaskView`. +open class OCKGridTaskViewSynchronizer: OCKTaskViewSynchronizerProtocol { + public init() {} + + open func updateView(_ view: OCKGridTaskView, context: OCKSynchronizationContext<OCKTaskEvents?>) { + view.updateWith(events: context.viewModel?.firstEvents, animated: context.animated) + } + + open func makeView() -> OCKGridTaskView { + return .init() + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKInstructionsTaskViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKInstructionsTaskViewSynchronizer.swift new file mode 100644 index 000000000..21494cfc9 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKInstructionsTaskViewSynchronizer.swift @@ -0,0 +1,46 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A synchronizer specialized for the `OCKInstructionsTaskView` +open class OCKInstructionsTaskViewSynchronizer: OCKTaskViewSynchronizerProtocol { + public init() {} + + open func updateView(_ view: OCKInstructionsTaskView, context: OCKSynchronizationContext<OCKTaskEvents?>) { + view.updateWith(event: context.viewModel?.firstEvent, animated: context.animated) + } + + open func makeView() -> OCKInstructionsTaskView { + return .init() + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKSimpleTaskViewSynchronizer.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKSimpleTaskViewSynchronizer.swift new file mode 100644 index 000000000..dff5523e9 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKSimpleTaskViewSynchronizer.swift @@ -0,0 +1,46 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Foundation + +/// A synchronizer specialized for the `OCKSimpleTaskView` +open class OCKSimpleTaskViewSynchronizer: OCKTaskViewSynchronizerProtocol { + public init() {} + + open func makeView() -> OCKSimpleTaskView { + .init() + } + + open func updateView(_ view: OCKSimpleTaskView, context: OCKSynchronizationContext<OCKTaskEvents?>) { + view.updateWith(event: context.viewModel?.firstEvent, animated: context.animated) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKTaskViewSynchronizerProtocol.swift b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKTaskViewSynchronizerProtocol.swift new file mode 100644 index 000000000..718c6f6ef --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/Synchronizers/OCKTaskViewSynchronizerProtocol.swift @@ -0,0 +1,74 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +/// Describes a type erased view synchronizer for tasks. +public protocol OCKAnyTaskViewSynchronizerProtocol { + + /// Initialize a view to be synchronized. + func makeAnyView() -> UIView & OCKTaskDisplayable + + /// Update a view using the given context + /// - Parameters: + /// - view: The view to be updated + /// - context: Information about the update that is occurring. + func updateAnyView(_ view: UIView & OCKTaskDisplayable, context: OCKSynchronizationContext<OCKTaskEvents?>) +} + +/// Describes a view synchronizer for tasks. +public protocol OCKTaskViewSynchronizerProtocol: OCKAnyTaskViewSynchronizerProtocol { + + /// The type of the view that will be synchronized + associatedtype View: UIView & OCKTaskDisplayable + + /// Initialize a view to be synchronized. + func makeView() -> View + + /// Update a view using the given context. + /// - Parameters: + /// - view: The view to be updated. + /// - context: Information about the update that is occurring. + func updateView(_ view: View, context: OCKSynchronizationContext<OCKTaskEvents?>) +} + +public extension OCKTaskViewSynchronizerProtocol { + func makeAnyView() -> UIView & OCKTaskDisplayable { + let view: View = makeView() + return view + } + + func updateAnyView(_ view: UIView & OCKTaskDisplayable, context: OCKSynchronizationContext<OCKTaskEvents?>) { + guard let typedView = view as? View else { fatalError("Type mismatch") } + updateView(typedView, context: context) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKButtonLogViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKButtonLogViewController.swift new file mode 100644 index 000000000..538e40418 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKButtonLogViewController.swift @@ -0,0 +1,61 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKButtonLogTaskViewController = OCKTaskViewController<OCKButtonLogTaskController, OCKButtonLogTaskViewSynchronizer> + +public extension OCKTaskViewController where Controller: OCKTaskController, ViewSynchronizer == OCKButtonLogTaskViewSynchronizer { + + /// Initialize a view controller that displays a task. Fetches and stays synchronized with events for the task. + /// - Parameter task: The task to display. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the events to fetch. + convenience init(task: OCKAnyTask, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), task: task, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays tasks. Fetches and stays synchronized with events for the tasks. + /// - Parameter taskIDs: User defined ids for the tasks to fetch. + /// - Parameter eventQuery: Used to fetch events for the tasks. + /// - Parameter storeManager: Wraps the store that contains the tasks and events to fetch. + convenience init(taskIDs: [String], eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskIDs: taskIDs, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays task. Fetches and stays synchronized with events for the task. + /// - Parameter viewSynchronizer: Manages the task view. + /// - Parameter taskID: User defined id of the task to fetch. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the task and events to fetch. + convenience init(taskID: String, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskID: taskID, eventQuery: eventQuery, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKChecklistTaskViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKChecklistTaskViewController.swift new file mode 100644 index 000000000..f583757ff --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKChecklistTaskViewController.swift @@ -0,0 +1,61 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKChecklistTaskViewController = OCKTaskViewController<OCKChecklistTaskController, OCKChecklistTaskViewSynchronizer> + +public extension OCKTaskViewController where Controller: OCKTaskController, ViewSynchronizer == OCKChecklistTaskViewSynchronizer { + + /// Initialize a view controller that displays a task. Fetches and stays synchronized with events for the task. + /// - Parameter task: The task to display. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the events to fetch. + convenience init(task: OCKAnyTask, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), task: task, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays tasks. Fetches and stays synchronized with events for the tasks. + /// - Parameter taskIDs: User defined ids for the tasks to fetch. + /// - Parameter eventQuery: Used to fetch events for the tasks. + /// - Parameter storeManager: Wraps the store that contains the tasks and events to fetch. + convenience init(taskIDs: [String], eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskIDs: taskIDs, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays task. Fetches and stays synchronized with events for the task. + /// - Parameter viewSynchronizer: Manages the task view. + /// - Parameter taskID: User defined id of the task to fetch. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the task and events to fetch. + convenience init(taskID: String, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskID: taskID, eventQuery: eventQuery, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKGridTaskViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKGridTaskViewController.swift new file mode 100644 index 000000000..fa47dc411 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKGridTaskViewController.swift @@ -0,0 +1,82 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitUI +import Combine +import UIKit + +/// A task view controller that displays multiple events in a grid collection view. +open class OCKGridTaskViewController: OCKTaskViewController<OCKGridTaskController, OCKGridTaskViewSynchronizer>, UICollectionViewDataSource { + + override open func viewDidLoad() { + super.viewDidLoad() + taskView.collectionView.dataSource = self + } + + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return controller.objectWillChange.value?.firstEvents?.count ?? 0 + } + + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OCKGridTaskView.defaultCellIdentifier, for: indexPath) + guard let typedCell = cell as? OCKGridTaskView.DefaultCellType else { return cell } + let event = controller.objectWillChange.value?.event(forIndexPath: indexPath) + typedCell.updateWith(event: event, animated: false) + return cell + } +} + +public extension OCKTaskViewController where Controller: OCKTaskController, ViewSynchronizer == OCKGridTaskViewSynchronizer { + + /// Initialize a view controller that displays a task. Fetches and stays synchronized with events for the task. + /// - Parameter task: The task to display. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the events to fetch. + convenience init(task: OCKAnyTask, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), task: task, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays tasks. Fetches and stays synchronized with events for the tasks. + /// - Parameter taskIDs: User defined ids for the tasks to fetch. + /// - Parameter eventQuery: Used to fetch events for the tasks. + /// - Parameter storeManager: Wraps the store that contains the tasks and events to fetch. + convenience init(taskIDs: [String], eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskIDs: taskIDs, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays task. Fetches and stays synchronized with events for the task. + /// - Parameter viewSynchronizer: Manages the task view. + /// - Parameter taskID: User defined id of the task to fetch. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the task and events to fetch. + convenience init(taskID: String, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskID: taskID, eventQuery: eventQuery, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKInstructionsTaskViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKInstructionsTaskViewController.swift new file mode 100644 index 000000000..b53ea4f13 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKInstructionsTaskViewController.swift @@ -0,0 +1,61 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +public typealias OCKInstructionsTaskViewController = OCKTaskViewController<OCKInstructionsTaskController, OCKInstructionsTaskViewSynchronizer> + +public extension OCKTaskViewController where Controller: OCKTaskController, ViewSynchronizer == OCKInstructionsTaskViewSynchronizer { + + /// Initialize a view controller that displays a task. Fetches and stays synchronized with events for the task. + /// - Parameter task: The task to display. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the events to fetch. + convenience init(task: OCKAnyTask, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), task: task, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays tasks. Fetches and stays synchronized with events for the tasks. + /// - Parameter taskIDs: User defined ids for the tasks to fetch. + /// - Parameter eventQuery: Used to fetch events for the tasks. + /// - Parameter storeManager: Wraps the store that contains the tasks and events to fetch. + convenience init(taskIDs: [String], eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskIDs: taskIDs, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays task. Fetches and stays synchronized with events for the task. + /// - Parameter viewSynchronizer: Manages the task view. + /// - Parameter taskID: User defined id of the task to fetch. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the task and events to fetch. + convenience init(taskID: String, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskID: taskID, eventQuery: eventQuery, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKSimpleTaskViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKSimpleTaskViewController.swift new file mode 100644 index 000000000..4d4298770 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKSimpleTaskViewController.swift @@ -0,0 +1,62 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +/// A view controller that display and updates a single event that can be completed or uncompleted by tapping a large button. +open class OCKSimpleTaskViewController: OCKTaskViewController<OCKSimpleTaskController, OCKSimpleTaskViewSynchronizer> {} + +public extension OCKTaskViewController where Controller: OCKTaskController, ViewSynchronizer == OCKSimpleTaskViewSynchronizer { + + /// Initialize a view controller that displays a task. Fetches and stays synchronized with events for the task. + /// - Parameter task: The task to display. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the events to fetch. + convenience init(task: OCKAnyTask, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), task: task, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays tasks. Fetches and stays synchronized with events for the tasks. + /// - Parameter taskIDs: User defined ids for the tasks to fetch. + /// - Parameter eventQuery: Used to fetch events for the tasks. + /// - Parameter storeManager: Wraps the store that contains the tasks and events to fetch. + convenience init(taskIDs: [String], eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskIDs: taskIDs, eventQuery: eventQuery, storeManager: storeManager) + } + + /// Initialize a view controller that displays task. Fetches and stays synchronized with events for the task. + /// - Parameter viewSynchronizer: Manages the task view. + /// - Parameter taskID: User defined id of the task to fetch. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the task and events to fetch. + convenience init(taskID: String, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + self.init(viewSynchronizer: ViewSynchronizer(), taskID: taskID, eventQuery: eventQuery, storeManager: storeManager) + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift new file mode 100644 index 000000000..402875664 --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Task/View Controllers/OCKTaskViewController.swift @@ -0,0 +1,212 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import Combine +import UIKit + +/// Types wishing to receive updates from task view controllers can conform to this protocol. +public protocol OCKTaskViewControllerDelegate: AnyObject { + + /// Called when an unhandled error is encountered in a task view controller. + /// - Parameters: + /// - viewController: The view controller in which the error was encountered. + /// - didEncounterError: The error that was unhandled. + func taskViewController<C: OCKTaskControllerProtocol, VS: OCKTaskViewSynchronizerProtocol>( + _ viewController: OCKTaskViewController<C, VS>, didEncounterError: Error) +} + +/// A view controller that displays a task view and keep it synchronized with a store. +open class OCKTaskViewController<Controller: OCKTaskControllerProtocol, ViewSynchronizer: OCKTaskViewSynchronizerProtocol>: +UIViewController, OCKTaskViewDelegate { + + // MARK: Properties + + /// If set, the delegate will receive updates when import events happen + public weak var delegate: OCKTaskViewControllerDelegate? + + /// Handles the responsibility of updating the view when data in the store changes. + public let viewSynchronizer: ViewSynchronizer + + /// Handles the responsibility of interacting with data from the store. + public let controller: Controller + + /// The view that is being synchronized against the store. + public var taskView: ViewSynchronizer.View { + guard let view = self.view as? ViewSynchronizer.View else { fatalError("View should be of type \(ViewSynchronizer.View.self)") } + return view + } + + private var viewModelSubscription: AnyCancellable? + private var viewDidLoadCompletion: (() -> Void)? + + // MARK: - Life Cycle + + /// Initialize with a controller and synchronizer. + public init(controller: Controller, viewSynchronizer: ViewSynchronizer) { + self.controller = controller + self.viewSynchronizer = viewSynchronizer + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @available(*, unavailable) + override open func loadView() { + view = viewSynchronizer.makeView() + } + + override open func viewDidLoad() { + super.viewDidLoad() + taskView.delegate = self + + // Begin listening for changes in the view model. Note, when we subscribe to the view model, it sends its current value through the stream + startObservingViewModel() + + viewDidLoadCompletion?() + } + + // MARK: - Methods + + // Create a subscription that updates the view when the view model is updated. + private func startObservingViewModel() { + viewModelSubscription?.cancel() + viewModelSubscription = controller.objectWillChange + .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 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 + } + + func notifyDelegateAndResetViewOnError<Success, Error>(result: Result<Success, Error>) { + if case let .failure(error) = result { + delegate?.taskViewController(self, didEncounterError: error) + resetViewState() + } + } + + // MARK: - OCKTaskViewDelegate + + open func taskView(_ taskView: UIView & OCKTaskDisplayable, didCompleteEvent isComplete: Bool, at indexPath: IndexPath, sender: Any?) { + controller.setEvent(atIndexPath: indexPath, isComplete: isComplete, completion: notifyDelegateAndResetViewOnError) + } + + open func taskView(_ taskView: UIView & OCKTaskDisplayable, didSelectOutcomeValueAt index: Int, eventIndexPath: IndexPath, sender: Any?) { + 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) + } + } + + open func taskView(_ taskView: UIView & OCKTaskDisplayable, didCreateOutcomeValueAt index: Int, eventIndexPath: IndexPath, sender: Any?) { + controller.appendOutcomeValue(withType: true, at: eventIndexPath, completion: notifyDelegateAndResetViewOnError) + } + + open func didSelectTaskView(_ taskView: UIView & OCKTaskDisplayable, eventIndexPath: IndexPath) { + do { + let detailsViewController = try controller.initiateDetailsViewController(forIndexPath: eventIndexPath) + let navigationController = UINavigationController(rootViewController: detailsViewController) + present(navigationController, animated: true, completion: nil) + } catch { + delegate?.taskViewController(self, didEncounterError: error) + } + } +} + +public extension OCKTaskViewController where Controller: OCKTaskController { + + /// Initialize a view controller that displays a task. Fetches and stays synchronized with events for the task. + /// - Parameter viewSynchronizer: Manages the task view. + /// - Parameter task: The task to display. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the events to fetch. + convenience init(viewSynchronizer: ViewSynchronizer, task: OCKAnyTask, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + let controller = Controller(storeManager: storeManager) + self.init(controller: controller, viewSynchronizer: viewSynchronizer) + viewDidLoadCompletion = { [weak self] in + self?.controller.fetchAndObserveEvents(forTask: task, eventQuery: eventQuery, errorHandler: { [weak self] error in + guard let self = self else { return } + self.delegate?.taskViewController(self, didEncounterError: error) + }) + } + } + + /// Initialize a view controller that displays tasks. Fetches and stays synchronized with events for the tasks. + /// - Parameter viewSynchronizer: Manages the task view. + /// - Parameter taskIDs: User defined ids for the tasks to fetch. + /// - Parameter eventQuery: Used to fetch events for the tasks. + /// - Parameter storeManager: Wraps the store that contains the tasks and events to fetch. + convenience init(viewSynchronizer: ViewSynchronizer, taskIDs: [String], eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + let controller = Controller(storeManager: storeManager) + self.init(controller: controller, viewSynchronizer: viewSynchronizer) + viewDidLoadCompletion = { [weak self] in + self?.controller.fetchAndObserveEvents(forTaskIDs: taskIDs, eventQuery: eventQuery, errorHandler: { [weak self] error in + guard let self = self else { return } + self.delegate?.taskViewController(self, didEncounterError: error) + }) + } + } + + /// Initialize a view controller that displays task. Fetches and stays synchronized with events for the task. + /// - Parameter viewSynchronizer: Manages the task view. + /// - Parameter taskID: User defined id of the task to fetch. + /// - Parameter eventQuery: Used to fetch events for the task. + /// - Parameter storeManager: Wraps the store that contains the task and events to fetch. + convenience init(viewSynchronizer: ViewSynchronizer, taskID: String, eventQuery: OCKEventQuery, storeManager: OCKSynchronizedStoreManager) { + let controller = Controller(storeManager: storeManager) + self.init(controller: controller, viewSynchronizer: viewSynchronizer) + viewDidLoadCompletion = { [weak self] in + self?.controller.fetchAndObserveEvents(forTaskID: taskID, eventQuery: eventQuery, errorHandler: { [weak self] error in + guard let self = self else { return } + self.delegate?.taskViewController(self, didEncounterError: error) + }) + } + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Utilities/OCKContactUtility.swift b/CareKit/CareKit/Synchronized View Controllers/Utilities/OCKContactUtility.swift new file mode 100644 index 000000000..afd707f1f --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Utilities/OCKContactUtility.swift @@ -0,0 +1,68 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Contacts +import MapKit +import UIKit + +struct OCKContactUtility { + private static let addressFormatter: CNPostalAddressFormatter = { + let formatter = CNPostalAddressFormatter() + formatter.style = .mailingAddress + return formatter + }() + + private static let nameFormatter: PersonNameComponentsFormatter = { + let nameFormatter = PersonNameComponentsFormatter() + nameFormatter.style = .medium + return nameFormatter + }() + + static func string(from address: CNPostalAddress?) -> String? { + guard let address = address else { return nil } + return addressFormatter.string(from: address) + } + + static func string(from components: PersonNameComponents?) -> String? { + guard let components = components else { return nil } + return nameFormatter.string(from: components) + } + + static func image(from asset: String?) -> UIImage? { + guard let asset = asset else { return nil } + + // We can't be sure if the image they provide is in the assets folder, in the bundle, or in a directory. + // We can check all 3 possibilities and then choose whichever is non-nil. + let symbol = UIImage(systemName: asset) + let appAssetsImage = UIImage(named: asset) + let otherUrlImage = UIImage(contentsOfFile: asset) + return otherUrlImage ?? appAssetsImage ?? symbol + } +} diff --git a/CareKit/CareKit/Synchronized View Controllers/Utilities/OCKScheduleUtility.swift b/CareKit/CareKit/Synchronized View Controllers/Utilities/OCKScheduleUtility.swift new file mode 100644 index 000000000..340fe0a3a --- /dev/null +++ b/CareKit/CareKit/Synchronized View Controllers/Utilities/OCKScheduleUtility.swift @@ -0,0 +1,132 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Foundation + +struct OCKScheduleUtility { + + private static let timeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .short + return formatter + }() + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "M/d" + return formatter + }() + + static func scheduleLabel(for events: [OCKAnyEvent]) -> String? { + let result = [completionLabel(for: events), dateLabel(for: events)] + .compactMap { $0 } + .joined(separator: " ") + return !result.isEmpty ? result : nil + } + + static func scheduleLabel(for event: OCKAnyEvent) -> String? { + let result = [ + timeLabel(for: event), + dateLabel(forStart: event.scheduleEvent.start, end: event.scheduleEvent.end) + ] + .compactMap { $0 } + .joined(separator: " ") + + return !result.isEmpty ? result : nil + } + + static func timeLabel(for event: OCKAnyEvent, includesEnd: Bool = true) -> String { + switch event.scheduleEvent.element.duration { + + case .allDay: return "all day" + case .seconds: + if includesEnd && event.scheduleEvent.start != event.scheduleEvent.end { + let start = event.scheduleEvent.start + let end = event.scheduleEvent.end + return "\(timeFormatter.string(from: start)) to \(timeFormatter.string(from: end))" + } + } + let label = timeFormatter.string(from: event.scheduleEvent.start).description + return label + } + + static func completedTimeLabel(for event: OCKAnyEvent) -> String? { + guard let completedDate = event.outcome?.values + .max(by: { isMoreRecent(lhs: $0.createdDate, rhs: $1.createdDate) })? + .createdDate + else { return nil } + return timeFormatter.string(from: completedDate) + } + + private static func dateLabel(for events: [OCKAnyEvent]) -> String? { + guard !events.isEmpty else { return nil } + if events.count > 1 { + let schedule = events.first!.scheduleEvent + return dateLabel(forStart: schedule.start, end: schedule.end) + } + return dateLabel(forStart: events.first!.scheduleEvent.start, end: events.last!.scheduleEvent.end) + } + + private static func isMoreRecent(lhs: Date?, rhs: Date?) -> Bool { + guard let lhs = lhs else { return false } + guard let rhs = rhs else { return true } + return lhs > rhs + } + + private static func dateLabel(forStart start: Date, end: Date) -> String? { + let datesAreInSameDay = Calendar.current.isDate(start, inSameDayAs: end) + if datesAreInSameDay { + let datesAreToday = Calendar.current.isDateInToday(start) + return !datesAreToday ? "on \(label(for: start))" : nil + } + return "from \(label(for: start)) to \(label(for: end))" + } + + private static func label(for date: Date) -> String { + if Calendar.current.isDateInToday(date) { + return loc("TODAY") + } + let label = dateFormatter.string(from: date) + return label + } + + private static func completionLabel(for events: [OCKAnyEvent]) -> String? { + guard !events.isEmpty else { return nil } + let completed = events.filter { $0.outcome != nil }.count + let remaining = events.count - completed + let format = OCKLocalization.localized("EVENTS_REMAINING", + tableName: "Localizable", + bundle: nil, + value: "", + comment: "The number of events that the user has not yet marked completed") + return String.localizedStringWithFormat(format, remaining) + } +} diff --git a/CareKit/CareKit/View Updaters/Calendar/OCKWeekCalendarView+Updatable.swift b/CareKit/CareKit/View Updaters/Calendar/OCKWeekCalendarView+Updatable.swift new file mode 100644 index 000000000..ac3194ce2 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Calendar/OCKWeekCalendarView+Updatable.swift @@ -0,0 +1,70 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitUI +import Foundation + +extension OCKWeekCalendarView: OCKCompletionStatesUpdatable { + func updateWith(states: [OCKCompletionRingButton.CompletionState], animated: Bool) { + // clear the view + guard !states.isEmpty else { + completionRingButtons.forEach { + $0.setState(.dimmed, animated: true) + $0.accessibilityLabel = nil + $0.accessibilityValue = nil + } + return + } + + // Else update the ring states + guard states.count == completionRingButtons.count else { + fatalError("Number of states and completions rings do not match") + } + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + + completionRingButtons.enumerated().forEach { index, ring in + let date = Calendar.current.date(byAdding: .day, value: index, to: dateInterval.start)! + ring.setState(states[index], animated: true) + ring.accessibilityValue = makeAccessibilityValue(for: states[index]) + ring.accessibilityLabel = dateFormatter.string(from: date) + ring.accessibilityTraits = ring.isSelected ? [.button, .selected] : [.button] + } + } + + private func makeAccessibilityValue(for state: OCKCompletionRingButton.CompletionState) -> String { + switch state { + case .dimmed: return loc("NO_TASKS") + case .empty: return loc("NO_EVENTS") + case .zero: return "0" + case .progress(let percent): return "\(Int(percent * 100))%" + } + } +} diff --git a/CareKit/CareKit/View Updaters/Chart/OCKCartesianChartView+Updatable.swift b/CareKit/CareKit/View Updaters/Chart/OCKCartesianChartView+Updatable.swift new file mode 100644 index 000000000..59188b998 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Chart/OCKCartesianChartView+Updatable.swift @@ -0,0 +1,38 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI + +extension OCKCartesianChartView: OCKDataSeriesUpdatable { + func updateWith(dataSeries: [OCKDataSeries], animated: Bool) { + graphView.dataSeries = dataSeries + } +} diff --git a/CareKit/CareKit/View Updaters/Contact/OCKDetailedContactView+Updatable.swift b/CareKit/CareKit/View Updaters/Contact/OCKDetailedContactView+Updatable.swift new file mode 100644 index 000000000..cfeaa57b0 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Contact/OCKDetailedContactView+Updatable.swift @@ -0,0 +1,66 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +extension OCKDetailedContactView: OCKContactUpdatable { + func updateWith(contact: OCKAnyContact?, animated: Bool) { + headerView.updateWith(contact: contact, animated: animated) + + guard let contact = contact else { + clearView(animated: animated) + return + } + + instructionsLabel.text = contact.role + addressButton.detailLabel.text = OCKContactUtility.string(from: contact.address) + + // Show the contact buttons if the contact has relevant data + addressButton.setIsHiddenIfNeeded(contact.address == nil) + emailButton.setIsHiddenIfNeeded(contact.emailAddresses?.isEmpty ?? true) + messageButton.setIsHiddenIfNeeded(contact.messagingNumbers?.isEmpty ?? true) + callButton.setIsHiddenIfNeeded(contact.phoneNumbers?.isEmpty ?? true) + } + + private func clearView(animated: Bool) { + instructionsLabel.text = nil + addressButton.detailLabel.text = nil + [addressButton, callButton, emailButton, messageButton].forEach { $0.isHidden = true } + } +} + +private extension UIView { + func setIsHiddenIfNeeded(_ isHidden: Bool) { + guard self.isHidden != isHidden else { return } + self.isHidden = isHidden + } +} diff --git a/CareKit/CareKit/View Updaters/Contact/OCKSimpleContactView+Updatable.swift b/CareKit/CareKit/View Updaters/Contact/OCKSimpleContactView+Updatable.swift new file mode 100644 index 000000000..976f26e94 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Contact/OCKSimpleContactView+Updatable.swift @@ -0,0 +1,38 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI + +extension OCKSimpleContactView: OCKContactUpdatable { + func updateWith(contact: OCKAnyContact?, animated: Bool) { + headerView.updateWith(contact: contact, animated: animated) + } +} diff --git a/CareKit/CareKit/View Updaters/OCKHeaderView+Updatable.swift b/CareKit/CareKit/View Updaters/OCKHeaderView+Updatable.swift new file mode 100644 index 000000000..b65dd4953 --- /dev/null +++ b/CareKit/CareKit/View Updaters/OCKHeaderView+Updatable.swift @@ -0,0 +1,79 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +extension OCKHeaderView: OCKEventUpdatable, OCKTaskUpdatable, OCKContactUpdatable { + func updateWith(event: OCKAnyEvent?, animated: Bool) { + guard let event = event else { + clearView(animated: animated) + return + } + + titleLabel.text = event.task.title + detailLabel.text = OCKScheduleUtility.scheduleLabel(for: event) + updateAccessibilityLabel() + } + + func updateWith(events: [OCKAnyEvent]?, animated: Bool) { + guard let events = events, !events.isEmpty else { + clearView(animated: animated) + return + } + + titleLabel.text = events.first!.task.title + detailLabel.text = OCKScheduleUtility.scheduleLabel(for: events) + updateAccessibilityLabel() + } + + func updateWith(contact: OCKAnyContact?, animated: Bool) { + guard let contact = contact else { + clearView(animated: animated) + return + } + + titleLabel.text = OCKContactUtility.string(from: contact.name) + detailLabel.text = contact.title + iconImageView?.image = OCKContactUtility.image(from: contact.asset) ?? UIImage(systemName: "person.crop.circle") + updateAccessibilityLabel() + } + + private func clearView(animated: Bool) { + [titleLabel, detailLabel].forEach { $0.text = nil } + iconImageView?.image = UIImage(systemName: "person.crop.circle") + accessibilityLabel = nil + } + + private func updateAccessibilityLabel() { + accessibilityLabel = "\(titleLabel.text ?? ""), \(detailLabel.text ?? "")" + } +} diff --git a/CareKit/CareKit/View Updaters/Task/OCKButtonLogTaskView+Updatable.swift b/CareKit/CareKit/View Updaters/Task/OCKButtonLogTaskView+Updatable.swift new file mode 100644 index 000000000..6ebf75bb2 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Task/OCKButtonLogTaskView+Updatable.swift @@ -0,0 +1,50 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +extension OCKButtonLogTaskView: OCKEventUpdatable { + func updateWith(event: OCKAnyEvent?, animated: Bool) { + headerView.updateWith(event: event, animated: animated) + + guard let event = event else { + clearView(animated: animated) + return + } + + instructionsLabel.text = event.task.instructions + updateItems(withOutcomeValues: event.outcome?.values ?? [], animated: animated) + } + + private func clearView(animated: Bool) { + instructionsLabel.text = nil + clearItems(animated: animated) + } +} diff --git a/CareKit/CareKit/View Updaters/Task/OCKChecklistTaskView+Updatable.swift b/CareKit/CareKit/View Updaters/Task/OCKChecklistTaskView+Updatable.swift new file mode 100644 index 000000000..5c20ff2a0 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Task/OCKChecklistTaskView+Updatable.swift @@ -0,0 +1,73 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI + +extension OCKChecklistTaskView: OCKTaskUpdatable { + func updateWith(events: [OCKAnyEvent]?, animated: Bool) { + headerView.updateWith(events: events, animated: animated) + + guard let events = events, !events.isEmpty else { + clearView(animated: animated) + return + } + + instructionsLabel.text = events.first!.task.instructions + + // Update the items based on the new events + for (index, event) in events.enumerated() { + let title = event.scheduleEvent.element.text ?? OCKScheduleUtility.timeLabel(for: event) + if index < items.count { + let item = updateItem(at: index, withTitle: title) + item?.isSelected = event.outcome != nil + } else { + let item = appendItem(withTitle: title, animated: animated) + item.isSelected = event.outcome != nil + } + } + + // Remove any extraneous items + trimItems(given: events, animated: animated) + } + + private func clearView(animated: Bool) { + instructionsLabel.text = nil + clearItems(animated: animated) + } + + // Remove any items that aren't needed + private func trimItems(given events: [OCKAnyEvent], animated: Bool) { + let countToRemove = items.count - events.count + for _ in 0..<countToRemove { + removeItem(at: items.count - 1, animated: animated) + } + } +} diff --git a/CareKit/CareKit/View Updaters/Task/OCKGridTaskView+Updatable.swift b/CareKit/CareKit/View Updaters/Task/OCKGridTaskView+Updatable.swift new file mode 100644 index 000000000..f0bce3741 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Task/OCKGridTaskView+Updatable.swift @@ -0,0 +1,70 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +extension OCKGridTaskView: OCKTaskUpdatable { + func updateWith(events: [OCKAnyEvent]?, animated: Bool) { + headerView.updateWith(events: events, animated: animated) + + guard let events = events, !events.isEmpty else { + clearView(animated: animated) + return + } + + instructionsLabel.text = events.first!.task.instructions + collectionView.reloadData() + } + + private func clearView(animated: Bool) { + instructionsLabel.text = nil + } +} + +extension OCKGridTaskCell: OCKEventUpdatable { + func updateWith(event: OCKAnyEvent?, animated: Bool) { + guard let event = event else { + prepareForReuse() + return + } + + let isComplete = event.outcome != nil + let title = isComplete ? + OCKScheduleUtility.completedTimeLabel(for: event) : + OCKScheduleUtility.timeLabel(for: event, includesEnd: false) + + completionButton.label.text = title + completionButton.isSelected = isComplete + accessibilityLabel = title + accessibilityValue = loc(isComplete ? "COMPLETED" : "INCOMPLETE") + } +} diff --git a/CareKit/CareKit/View Updaters/Task/OCKInstructionsTaskView+Updatable.swift b/CareKit/CareKit/View Updaters/Task/OCKInstructionsTaskView+Updatable.swift new file mode 100644 index 000000000..b87bdcf05 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Task/OCKInstructionsTaskView+Updatable.swift @@ -0,0 +1,56 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI + +extension OCKInstructionsTaskView: OCKEventUpdatable { + func updateWith(event: OCKAnyEvent?, animated: Bool) { + headerView.updateWith(event: event, animated: animated) + + guard let event = event else { + clearView(animated: animated) + return + } + + let isComplete = event.outcome != nil + + completionButton.isSelected = isComplete + completionButton.accessibilityLabel = loc(isComplete ? "COMPLETED" : "INCOMPLETE") + completionButton.accessibilityHint = loc(isComplete ? "DOUBLE_TAP_TO_COMPLETE" : "DOUBLE_TAP_TO_INCOMPLETE") + + instructionsLabel.text = event.task.instructions + } + + private func clearView(animated: Bool) { + instructionsLabel.text = nil + completionButton.isSelected = false + } +} diff --git a/CareKit/CareKit/View Updaters/Task/OCKLogTaskView+Updatable.swift b/CareKit/CareKit/View Updaters/Task/OCKLogTaskView+Updatable.swift new file mode 100644 index 000000000..40dcaa481 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Task/OCKLogTaskView+Updatable.swift @@ -0,0 +1,76 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI +import UIKit + +extension OCKLogTaskView { + + private static let timeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .short + return formatter + }() + + /// Update the stack by updating each item, or adding a new one if necessary based on the number of `outcomeValues`. + func updateItems(withOutcomeValues outcomeValues: [OCKOutcomeValue], animated: Bool) { + if outcomeValues.isEmpty { + clearItems(animated: animated) + } else { + for (index, outcomeValue) in outcomeValues.enumerated() { + guard let date = outcomeValue.updatedDate ?? outcomeValue.createdDate else { break } + let dateString = OCKLogTaskView.timeFormatter.string(from: date).description + + _ = index < items.count ? + updateItem(at: index, withTitle: outcomeValue.stringValue, detail: dateString) : + appendItem(withTitle: outcomeValue.stringValue, detail: dateString, animated: animated) + } + } + trimItems(given: outcomeValues, animated: animated) + } + + // Remove any items that aren't needed + private func trimItems(given outcomeValues: [OCKOutcomeValue], animated: Bool) { + let countToRemove = items.count - outcomeValues.count + for _ in 0..<countToRemove { + removeItem(at: items.count - 1, animated: animated) + } + } +} + +private extension OCKOutcomeValue { + var stringValue: String { + switch type { + case .boolean: return booleanValue! ? loc("COMPLETED") : loc("INCOMPLETE") + default: return String(describing: value).capitalized + } + } +} diff --git a/CareKit/CareKit/View Updaters/Task/OCKSimpleTaskView+Updatable.swift b/CareKit/CareKit/View Updaters/Task/OCKSimpleTaskView+Updatable.swift new file mode 100644 index 000000000..256d99789 --- /dev/null +++ b/CareKit/CareKit/View Updaters/Task/OCKSimpleTaskView+Updatable.swift @@ -0,0 +1,52 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import CareKitUI + +extension OCKSimpleTaskView: OCKEventUpdatable { + func updateWith(event: OCKAnyEvent?, animated: Bool) { + headerView.updateWith(event: event, animated: animated) + + guard let event = event else { + clearView(animated: animated) + return + } + + let isComplete = event.outcome != nil + completionButton.isSelected = isComplete + accessibilityLabel = (headerView.titleLabel.text ?? "") + ", " + (headerView.detailLabel.text ?? "") + accessibilityValue = loc(isComplete ? "COMPLETED" : "INCOMPLETE") + } + + private func clearView(animated: Bool) { + completionButton.setSelected(false, animated: animated) + } +} diff --git a/CareKit/CareKit/View Updaters/Updatable.swift b/CareKit/CareKit/View Updaters/Updatable.swift new file mode 100644 index 000000000..b6263056c --- /dev/null +++ b/CareKit/CareKit/View Updaters/Updatable.swift @@ -0,0 +1,57 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKitStore +import Foundation + +/// Can be updated with a list of events. +protocol OCKTaskUpdatable { + func updateWith(events: [OCKAnyEvent]?, animated: Bool) +} + +/// Can be updated with an event. +protocol OCKEventUpdatable { + func updateWith(event: OCKAnyEvent?, animated: Bool) +} + +/// Can be updated with a contact. +protocol OCKContactUpdatable { + func updateWith(contact: OCKAnyContact?, animated: Bool) +} + +/// Can be updated with a data series. +protocol OCKDataSeriesUpdatable { + func updateWith(dataSeries: [OCKDataSeries], animated: Bool) +} + +/// Can be updated with a list of completion states. +protocol OCKCompletionStatesUpdatable { + func updateWith(states: [OCKCompletionRingButton.CompletionState], animated: Bool) +} diff --git a/CareKit/CareKit/iOS/Calendar/Paging/OCKWeekCalendarPageViewController.swift b/CareKit/CareKit/iOS/Calendar/Paging/OCKWeekCalendarPageViewController.swift index 6f4b03596..062b07eef 100644 --- a/CareKit/CareKit/iOS/Calendar/Paging/OCKWeekCalendarPageViewController.swift +++ b/CareKit/CareKit/iOS/Calendar/Paging/OCKWeekCalendarPageViewController.swift @@ -59,7 +59,7 @@ UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelega /// The currently selected date in the calendar. public var selectedDate: Date { - return currentViewController?.calendarView.selectedDate ?? Date() + return currentViewController?.calendarView.selectedDate ?? startingDate } /// The date interval currently being displayed. @@ -67,11 +67,7 @@ UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelega return currentViewController?.calendarView.dateInterval } - private let aggregator: OCKAdherenceAggregator - private var previouslySelectedDate = Date() - private let storeManager: OCKSynchronizedStoreManager - - var currentViewController: OCKWeekCalendarViewController? { + private var currentViewController: OCKWeekCalendarViewController? { guard let viewControllers = viewControllers, !viewControllers.isEmpty else { return nil } @@ -80,6 +76,17 @@ UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelega return viewController } + private let aggregator: OCKAdherenceAggregator + + // Many of the methods in this class get called when the selected date did change. During those times, this property helps access + // the previous value. + private(set) var cachedSelectedDate = Date() + + /// The initial date displayed when the view controller is loaded. + private let startingDate = Date() + + let storeManager: OCKSynchronizedStoreManager + // MARK: - Life Cycle public init(storeManager: OCKSynchronizedStoreManager, aggregator: OCKAdherenceAggregator) { @@ -99,9 +106,9 @@ UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelega delegate = self // Create the first view controller - let viewController = makeViewController(forDate: previouslySelectedDate) + let viewController = makeViewController(forDate: startingDate) viewController.calendarView.delegate = self - viewController.calendarView.selectDate(previouslySelectedDate) + viewController.calendarView.selectDate(startingDate) setViewControllers([viewController], direction: .forward, animated: false, completion: nil) } @@ -109,9 +116,8 @@ UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelega private func makeViewController(forDate date: Date) -> OCKWeekCalendarViewController { let viewController = OCKWeekCalendarViewController(weekOfDate: date, aggregator: aggregator, storeManager: storeManager) - - let interval = Calendar.current.dateInterval(of: .weekOfYear, for: date)! - viewController.calendarView.showDate(interval.start) + viewController.calendarView.showDate(date) + viewController.calendarView.delegate = self return viewController } @@ -127,65 +133,80 @@ UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelega } } + private func makePage(beside page: OCKWeekCalendarViewController, addingWeeks value: Int) -> OCKWeekCalendarViewController { + let baseDate = page.calendarView.selectedDate + let nextDate = Calendar.current.date(byAdding: .weekOfYear, value: value, to: baseDate)! + let page = makeViewController(forDate: nextDate) + return page + } + /// Select a date in the calendar. If the date is not in the current date interval being displayed, the view controller will automatically /// page to the date interval that contains the new date. /// - Parameter date: The new date to select. /// - Parameter animated: True to animate selection of the new date. open func selectDate(_ date: Date, animated: Bool) { - guard let currentVC = currentViewController else { return } - if currentVC.calendarView.dateInterval.contains(date) { - currentVC.calendarView.selectDate(date) + guard + !Calendar.current.isDate(date, inSameDayAs: selectedDate), + let dateInterval = dateInterval + else { return } + + // Always make sure to update the cached selected date. Note that in this context, `cachedSelectedDate` is the current value + // for the selected date since this method is called on `willSelect`. + defer { + cachedSelectedDate = date + } + + // If the new date is within the currently displayed week, select it + if dateInterval.contains(date) { + currentViewController?.calendarView.selectDate(date) return } - // Create the next view controller + // Else create a new calendar view that contains the new date let nextVC = makeViewController(forDate: date) - nextVC.calendarView.delegate = self - let isLeft = currentVC.calendarView.dateInterval.start > date - nextVC.calendarView.selectDate(date) - setViewControllers([nextVC], direction: isLeft ? .reverse : .forward, animated: animated, completion: nil ) + let isLeft = dateInterval.start > date + setViewControllers([nextVC], direction: isLeft ? .reverse : .forward, animated: animated, completion: nil) } // MARK: UIPageViewController DataSource & Delegate open func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - guard let typedViewController = viewController as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } - let dateInterval = typedViewController.calendarView.dateInterval - let previousDate = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: dateInterval.start)! - let previousPage = makeViewController(forDate: previousDate) - previousPage.calendarView.delegate = self - return previousPage + guard let page = viewController as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } + return makePage(beside: page, addingWeeks: -1) } open func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - guard let typedViewController = viewController as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } - let dateInterval = typedViewController.calendarView.dateInterval - let nextDate = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: dateInterval.start)! - let nextPage = makeViewController(forDate: nextDate) - nextPage.calendarView.delegate = self - return nextPage + guard let page = viewController as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } + return makePage(beside: page, addingWeeks: 1) + } + + open func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { + // Update each calendar to select the ring that is on the same weekday as the current selected ring. That way when transitioning to a + // new page, the correct ring will be displayed immediately. + let weekday = Calendar.current.component(.weekday, from: selectedDate) + pendingViewControllers + .compactMap { $0 as? OCKWeekCalendarViewController } + .forEach { + let newSelectedDate = Calendar.current.date(bySetting: .weekday, value: weekday, of: $0.calendarView.dateInterval.start)! + $0.calendarView.selectDate(newSelectedDate) + } } open func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { guard completed, - let previousViewController = previousViewControllers.first, - let currentViewController = currentViewController, - let currentWeek = dateInterval - else { return } - guard let typedPreviousViewController = previousViewController as? OCKWeekCalendarViewController else { fatalError("Unsupported type") } - - let didMoveForwards = typedPreviousViewController.calendarView.dateInterval < currentViewController.calendarView.dateInterval - let offset = didMoveForwards ? 1 : -1 - let previousSelectedDate = typedPreviousViewController.calendarView.selectedDate - let newSelectedDate = Calendar.current.date(byAdding: .weekOfYear, value: offset, - to: typedPreviousViewController.calendarView.selectedDate)! - currentViewController.calendarView.selectDate(newSelectedDate) - calendarDelegate?.weekCalendarPageViewController(self, didChangeDateInterval: currentWeek) - calendarDelegate?.weekCalendarPageViewController(self, didSelectDate: newSelectedDate, previousDate: previousSelectedDate) + let dateInterval = dateInterval + else { return } + + // Notify the delegate to update the displayed tasks. Note that in this context, `cachedSelectedDate` is the old value + // for the selected date since this method is called on `didFinish`. + calendarDelegate?.weekCalendarPageViewController(self, didChangeDateInterval: dateInterval) + calendarDelegate?.weekCalendarPageViewController(self, didSelectDate: selectedDate, previousDate: cachedSelectedDate) + + cachedSelectedDate = selectedDate } // MARK: OCKCalendarViewDelegate @@ -198,11 +219,18 @@ UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelega open func calendarView(_ calendarView: UIView & OCKCalendarDisplayable, didSelectDate date: Date, at index: Int, sender: Any?) { guard let startOfWeek = dateInterval?.start, - let dateInterval = dateInterval else { return } + let dateInterval = dateInterval + else { return } + + // Make sure the selected date exists in the current calendar page let comparison = Calendar.current.compare(dateInterval.start, to: startOfWeek, toGranularity: .weekOfYear) guard comparison == .orderedSame else { return } - calendarDelegate?.weekCalendarPageViewController(self, didSelectDate: date, previousDate: previouslySelectedDate) - previouslySelectedDate = date + + // Notify the delegate to update the displayed tasks. Note that in this context, `cachedSelectedDate` is the old value + // for the selected date since this method is called on `didSelect`. + calendarDelegate?.weekCalendarPageViewController(self, didSelectDate: date, previousDate: cachedSelectedDate) + + cachedSelectedDate = date } } #endif diff --git a/CareKit/CareKit/iOS/Higher Order/ViewController/OCKDailyPageViewController.swift b/CareKit/CareKit/iOS/Higher Order/ViewController/OCKDailyPageViewController.swift index 144bf211d..d0c97419e 100644 --- a/CareKit/CareKit/iOS/Higher Order/ViewController/OCKDailyPageViewController.swift +++ b/CareKit/CareKit/iOS/Higher Order/ViewController/OCKDailyPageViewController.swift @@ -74,7 +74,7 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { public weak var delegate: OCKDailyPageViewControllerDelegate? public var selectedDate: Date { - return calendarWeekPageViewController.selectedDate + return weekCalendarPageViewController.selectedDate } /// The store manager the view controller uses for synchronization @@ -84,7 +84,7 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { private let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) /// The calendar view controller in the header. - private let calendarWeekPageViewController: OCKWeekCalendarPageViewController + private let weekCalendarPageViewController: OCKWeekCalendarPageViewController // MARK: - Life cycle @@ -95,9 +95,9 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { /// - Parameter adherenceAggregator: An aggregator that will be used to compute the adherence values shown at the top of the view. public init(storeManager: OCKSynchronizedStoreManager, adherenceAggregator: OCKAdherenceAggregator = .compareTargetValues) { self.storeManager = storeManager - self.calendarWeekPageViewController = .init(storeManager: storeManager, aggregator: adherenceAggregator) + self.weekCalendarPageViewController = .init(storeManager: storeManager, aggregator: adherenceAggregator) super.init(nibName: nil, bundle: nil) - self.calendarWeekPageViewController.dataSource = self + self.weekCalendarPageViewController.dataSource = self self.pageViewController.dataSource = self self.pageViewController.delegate = self self.dataSource = self @@ -109,13 +109,33 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { fatalError("init(coder:) has not been implemented") } - // MARK: - Properties + /// The page that is currently being displayed. + open var currentPage: OCKListViewController? { + pageViewController.viewControllers?.first as? OCKListViewController + } + + /// Reload the contents at the currently selected date. + open func reload() { + guard let current = currentPage else { + return + } + preparePage(current, date: selectedDate) + } + /// Selects the given date, updating both the calendar view and the daily content. + /// + /// - Parameters: + /// - date: The date to be selected. + /// - animated: A flag that determines if the selection will be animated or not. 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) + guard !Calendar.current.isDate(selectedDate, inSameDayAs: date) else { return } + + // Load the page of tasks for the new date. This must be done before selecting the date in the calendar so that + // the `selectedDate` is correct. + showPage(forDate: date, previousDate: selectedDate, animated: animated) + + // Select the correct ring in the calendar + weekCalendarPageViewController.selectDate(date, animated: animated) } override open func viewSafeAreaInsetsDidChange() { @@ -123,16 +143,16 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { } override open func loadView() { - [calendarWeekPageViewController, pageViewController].forEach { addChild($0) } - view = OCKHeaderBodyView(headerView: calendarWeekPageViewController.view, bodyView: pageViewController.view) - [calendarWeekPageViewController, pageViewController].forEach { $0.didMove(toParent: self) } + [weekCalendarPageViewController, pageViewController].forEach { addChild($0) } + view = OCKHeaderBodyView(headerView: weekCalendarPageViewController.view, bodyView: pageViewController.view) + [weekCalendarPageViewController, pageViewController].forEach { $0.didMove(toParent: self) } } override open func viewDidLoad() { super.viewDidLoad() let now = Date() - calendarWeekPageViewController.calendarDelegate = self - calendarWeekPageViewController.selectDate(now, animated: false) + weekCalendarPageViewController.calendarDelegate = self + weekCalendarPageViewController.selectDate(now, animated: false) pageViewController.setViewControllers([makePage(date: now)], direction: .forward, animated: false, completion: nil) pageViewController.accessibilityHint = loc("THREE_FINGER_SWIPE_DAY") navigationItem.leftBarButtonItem = UIBarButtonItem(title: loc("TODAY"), style: .plain, target: self, action: #selector(pressedToday(sender:))) @@ -140,15 +160,31 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { private func makePage(date: Date) -> OCKDatedListViewController { let listViewController = OCKDatedListViewController(date: date) + + let refresher = UIRefreshControl() + refresher.addTarget(self, action: #selector(handleRefresh), for: .valueChanged) + listViewController.listView.scrollView.refreshControl = refresher + + preparePage(listViewController, date: date) + return listViewController + } + + private func preparePage(_ list: OCKListViewController, date: Date) { + list.clear() + let dateLabel = OCKDateLabel(textStyle: .title2, weight: .bold) dateLabel.setDate(date) dateLabel.accessibilityTraits = .header + list.insertView(dateLabel, at: 0, animated: false) - listViewController.insertView(dateLabel, at: 0, animated: false) + setInsets(for: list) + dataSource?.dailyPageViewController(self, prepare: list, for: date) + } - setInsets(for: listViewController) - dataSource?.dailyPageViewController(self, prepare: listViewController, for: date) - return listViewController + @objc + private func handleRefresh(_ control: UIRefreshControl) { + control.endRefreshing() + reload() } @objc @@ -171,15 +207,17 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { listView.scrollView.scrollIndicatorInsets = insets } + /// Show the page for a particular date. + private func showPage(forDate date: Date, previousDate: Date, animated: Bool) { + let moveLeft = date < previousDate + let listViewController = makePage(date: date) + pageViewController.setViewControllers([listViewController], direction: moveLeft ? .reverse : .forward, animated: animated, completion: nil) + } + // MARK: - OCKCalendarPageViewControllerDelegate public func weekCalendarPageViewController(_ viewController: OCKWeekCalendarPageViewController, didSelectDate date: Date, previousDate: Date) { - let newComponents = Calendar.current.dateComponents([.weekday, .weekOfYear, .year], from: date) - let oldComponents = Calendar.current.dateComponents([.weekday, .weekOfYear, .year], from: previousDate) - guard newComponents != oldComponents else { return } // do nothing if we have selected a date for the same day of the year - let moveLeft = date < previousDate - let listViewController = makePage(date: date) - pageViewController.setViewControllers([listViewController], direction: moveLeft ? .reverse : .forward, animated: true, completion: nil) + showPage(forDate: date, previousDate: previousDate, animated: true) } public func weekCalendarPageViewController(_ viewController: OCKWeekCalendarPageViewController, didChangeDateInterval interval: DateInterval) {} @@ -220,7 +258,7 @@ UIPageViewControllerDataSource, UIPageViewControllerDelegate { previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { guard completed else { return } guard let listViewController = pageViewController.viewControllers?.first as? OCKDatedListViewController else { fatalError("Unexpected type") } - calendarWeekPageViewController.selectDate(listViewController.date, animated: true) + weekCalendarPageViewController.selectDate(listViewController.date, animated: true) } } diff --git a/CareKit/CareKit/iOS/Task/Controllers/OCKNumericProgressTaskController.swift b/CareKit/CareKit/iOS/Task/Controllers/OCKNumericProgressTaskController.swift index 7e3693aec..eb79e2e3c 100644 --- a/CareKit/CareKit/iOS/Task/Controllers/OCKNumericProgressTaskController.swift +++ b/CareKit/CareKit/iOS/Task/Controllers/OCKNumericProgressTaskController.swift @@ -74,12 +74,6 @@ open class OCKNumericProgressTaskController: OCKTaskController { } } -private extension Double { - func test() { - - } -} - private extension Optional where Wrapped == Double { func logIfNil(message: String) -> Self { switch self { diff --git a/CareKit/CareKitTests/Higher Order/TestWeekCalendarPageViewController.swift b/CareKit/CareKitTests/Higher Order/TestWeekCalendarPageViewController.swift new file mode 100644 index 000000000..4982e4935 --- /dev/null +++ b/CareKit/CareKitTests/Higher Order/TestWeekCalendarPageViewController.swift @@ -0,0 +1,91 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@testable import CareKit +import CareKitStore +import Foundation +import XCTest + +class TestWeekCalendarPageViewController: XCTestCase { + + private var viewController: MockWeekCalendarPageViewController! + + var storeManager: OCKSynchronizedStoreManager! + + let today = Calendar.current.startOfDay(for: Date()) + + override func setUp() { + super.setUp() + let store = OCKStore(name: "test-store", type: .inMemory) + self.storeManager = .init(wrapping: store) + } + + func testCachedSelectedDateStartsAsToday() { + viewController = .init(storeManager: storeManager, aggregator: .outcomeExists, selectedDate: today) + XCTAssertTrue(Calendar.current.isDate(viewController.cachedSelectedDate, inSameDayAs: today)) + } + + func testCachedSelectedDateUpdatesOnManualSelection() { + viewController = .init(storeManager: storeManager, aggregator: .outcomeExists, selectedDate: today) + + let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)! + viewController.selectDate(tomorrow, animated: false) + XCTAssertTrue(Calendar.current.isDate(tomorrow, inSameDayAs: viewController.cachedSelectedDate)) + } + + func testPreviousSelectedDateUpdatesOnPageUpdate() { + let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)! + viewController = .init(storeManager: storeManager, aggregator: .outcomeExists, selectedDate: tomorrow) + + // Simulate the end of the transition to the next page + viewController.pageViewController(viewController, didFinishAnimating: true, previousViewControllers: [], + transitionCompleted: true) + + XCTAssertTrue(Calendar.current.isDate(tomorrow, inSameDayAs: viewController.cachedSelectedDate)) + } +} + +private class MockWeekCalendarPageViewController: OCKWeekCalendarPageViewController { + + override var selectedDate: Date { + _selectedDate + } + + override var dateInterval: DateInterval? { + Calendar.current.dateIntervalOfWeek(for: _selectedDate) + } + + private let _selectedDate: Date + + init(storeManager: OCKSynchronizedStoreManager, aggregator: OCKAdherenceAggregator, selectedDate: Date) { + self._selectedDate = selectedDate + super.init(storeManager: storeManager, aggregator: aggregator) + } +} diff --git a/CareKit/CareKitTests/Task/TestTaskEvents.swift b/CareKit/CareKitTests/Task/TestTaskEvents.swift index a01116474..61ff3f1c1 100644 --- a/CareKit/CareKitTests/Task/TestTaskEvents.swift +++ b/CareKit/CareKitTests/Task/TestTaskEvents.swift @@ -349,10 +349,6 @@ private extension OCKAnyEvent { } } -private extension OCKAnyTask { - var stableID: String? { uuid?.uuidString } -} - private extension ArraySlice { func array() -> [Element] { return Array(self) diff --git a/CareKit/CareKitTests/TestDailyTasksPageViewController.swift b/CareKit/CareKitTests/TestDailyTasksPageViewController.swift new file mode 100644 index 000000000..e183004fc --- /dev/null +++ b/CareKit/CareKitTests/TestDailyTasksPageViewController.swift @@ -0,0 +1,107 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@testable import CareKit +import CareKitStore +import CareKitUI +import Foundation +import XCTest + +private extension OCKTask { + static func mockWithEvents(forDate date: Date, impactsAdherence: Bool, eventCount: Int) -> (OCKTask, [OCKAnyEvent]) { + let startOfDay = Calendar.current.startOfDay(for: date) + let schedules = (1...eventCount).map { + OCKSchedule.dailyAtTime(hour: $0, minutes: 0, start: startOfDay, end: nil, text: nil) + } + var task = OCKTask(id: "doxylamine", title: "Doxylamine", carePlanUUID: nil, schedule: .init(composing: schedules)) + task.impactsAdherence = impactsAdherence + let events = schedules.enumerated().map { + return OCKAnyEvent(task: task, outcome: nil, scheduleEvent: $1.event(forOccurrenceIndex: $0)!) + } + return (task, events) + } +} + +class TestDailyTasksPageViewController: XCTestCase { + + var viewController: OCKDailyTasksPageViewController! + + override func setUp() { + super.setUp() + let store = OCKStore(name: "test-store", type: .inMemory) + viewController = .init(storeManager: .init(wrapping: store)) + } + + func testDelegate() { + viewController.loadViewIfNeeded() + XCTAssertTrue(viewController.tasksDelegate === viewController) + } + + func testViewControllerForTask() { + // Expecting a grid filled with the correct data + var (task, events) = OCKTask.mockWithEvents(forDate: Date(), impactsAdherence: true, eventCount: 2) + var observedViewController = viewController.dailyTasksPageViewController(viewController, viewControllerForTask: task, events: events, + eventQuery: .init(for: Date())) + let expectedViewController1 = observedViewController as? OCKGridTaskViewController + XCTAssertNotNil(expectedViewController1) + XCTAssertEqual(events.count, expectedViewController1?.controller.objectWillChange.value?.firstEvents?.count) + events.enumerated().forEach { offset, expectedEvent in + let observedEvent = expectedViewController1?.controller.objectWillChange.value?.event(forIndexPath: .init(row: offset, section: 0)) + XCTAssertEqual(expectedEvent.task.id, observedEvent?.task.id) + XCTAssertEqual(expectedEvent.scheduleEvent.occurrence, observedEvent?.scheduleEvent.occurrence) + } + + // Expecting a button log filled with the correct data + (task, events) = OCKTask.mockWithEvents(forDate: Date(), impactsAdherence: false, eventCount: 2) + observedViewController = viewController.dailyTasksPageViewController(viewController, viewControllerForTask: task, events: events, + eventQuery: .init(for: Date())) + let expectedViewController2 = observedViewController as? OCKButtonLogTaskViewController + XCTAssertNotNil(expectedViewController2) + XCTAssertEqual(events.count, expectedViewController2?.controller.objectWillChange.value?.firstEvents?.count) + events.enumerated().forEach { offset, expectedEvent in + let observedEvent = expectedViewController2?.controller.objectWillChange.value?.event(forIndexPath: .init(row: offset, section: 0)) + XCTAssertEqual(expectedEvent.task.id, observedEvent?.task.id) + XCTAssertEqual(expectedEvent.scheduleEvent.occurrence, observedEvent?.scheduleEvent.occurrence) + } + + // Expecting a simple log filled with the correct data + (task, events) = OCKTask.mockWithEvents(forDate: Date(), impactsAdherence: true, eventCount: 1) + observedViewController = viewController.dailyTasksPageViewController(viewController, viewControllerForTask: task, events: events, + eventQuery: .init(for: Date())) + let expectedViewController3 = observedViewController as? OCKSimpleTaskViewController + XCTAssertNotNil(expectedViewController3) + XCTAssertEqual(events.count, expectedViewController3?.controller.objectWillChange.value?.firstEvents?.count) + events.enumerated().forEach { offset, expectedEvent in + let observedEvent = expectedViewController3?.controller.objectWillChange.value?.event(forIndexPath: .init(row: offset, section: 0)) + XCTAssertEqual(expectedEvent.task.id, observedEvent?.task.id) + XCTAssertEqual(expectedEvent.scheduleEvent.occurrence, observedEvent?.scheduleEvent.occurrence) + } + } +} 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/CareKit/CareKitTests/TestSynchronizedContext.swift b/CareKit/CareKitTests/TestSynchronizedContext.swift new file mode 100644 index 000000000..ef139712b --- /dev/null +++ b/CareKit/CareKitTests/TestSynchronizedContext.swift @@ -0,0 +1,103 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@testable import CareKit +import Combine +import XCTest + +class TestSynchronizedContext: XCTestCase { + + var subscription: AnyCancellable? + var subject: CurrentValueSubject<Int?, Never>! + + override func setUp() { + super.setUp() + subject = CurrentValueSubject<Int?, Never>(nil) + } + + // Old value should be nil, current value should be nil + func testInitialState() { + let initialState = XCTestExpectation(description: "Initial state is valid") + subscription = subject.context().sink { context in + XCTAssertNil(context.viewModel) + XCTAssertNil(context.oldViewModel) + XCTAssertFalse(context.animated) + initialState.fulfill() + } + wait(for: [initialState], timeout: 2) + } + + // Old value should be nil, current value should be set + func testUpdatedOnce() { + let updatedState = XCTestExpectation(description: "Updated state is valid") + updatedState.expectedFulfillmentCount = 2 + var updateCount = 0 + subscription = subject.context().sink { context in + updateCount += 1 + updatedState.fulfill() + if updateCount == 2 { + XCTAssertEqual(context.viewModel, 3) + XCTAssertNil(context.oldViewModel) + XCTAssertFalse(context.animated) + } + } + subject.value = 3 + wait(for: [updatedState], timeout: 2) + } + + // Old value should be set, current value should be set + func testUpdatedTwice() { + let updatedState = XCTestExpectation(description: "Updated state is valid") + updatedState.expectedFulfillmentCount = 3 + var updateCount = 0 + subscription = subject.context().sink { context in + updateCount += 1 + updatedState.fulfill() + if updateCount == 3 { + XCTAssertEqual(context.viewModel, 4) + XCTAssertEqual(context.oldViewModel, 3) + XCTAssertTrue(context.animated) + } + } + subject.value = 3 + subject.value = 4 + wait(for: [updatedState], timeout: 2) + } + + func testNonNilViewModelAnimates() { + let subject = CurrentValueSubject<Int, Never>(1) + let animates = XCTestExpectation(description: "Animates updates") + subscription = subject.context().sink { context in + XCTAssertTrue(context.animated) + animates.fulfill() + } + wait(for: [animates], timeout: 2) + } +} diff --git a/CareKitFHIR/CareKitFHIR.xcodeproj/project.pbxproj b/CareKitFHIR/CareKitFHIR.xcodeproj/project.pbxproj index 025b06ad0..472bde0a5 100644 --- a/CareKitFHIR/CareKitFHIR.xcodeproj/project.pbxproj +++ b/CareKitFHIR/CareKitFHIR.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 031B45692474A1100063E717 /* OCKFHIRContentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031B45682474A1100063E717 /* OCKFHIRContentType.swift */; }; 031B456B2474A1380063E717 /* OCKFHIRResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031B456A2474A1380063E717 /* OCKFHIRResource.swift */; }; 031D4B662374E1D900199EFC /* FHIRModels+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031D4B652374E1D900199EFC /* FHIRModels+Extensions.swift */; }; + 033FF82024B51A2600070941 /* ModelsR4 in Frameworks */ = {isa = PBXBuildFile; productRef = 033FF81F24B51A2600070941 /* ModelsR4 */; }; + 033FF82224B51A2600070941 /* ModelsDSTU2 in Frameworks */ = {isa = PBXBuildFile; productRef = 033FF82124B51A2600070941 /* ModelsDSTU2 */; }; 0364BB6423C421E30047F952 /* CareKitStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0364BB6323C421E30047F952 /* CareKitStore.framework */; }; 0396EF40233D187800C28FC0 /* CareKitFHIR.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0396EF36233D187800C28FC0 /* CareKitFHIR.framework */; }; 03AFA92B233E697C0091DD45 /* OCKFHIRCodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AFA92A233E697C0091DD45 /* OCKFHIRCodingError.swift */; }; diff --git a/CareKitFHIR/CareKitFHIR/Patients/OCKDSTU2PatientConverter.swift b/CareKitFHIR/CareKitFHIR/Patients/OCKDSTU2PatientConverter.swift deleted file mode 100644 index 4fb685278..000000000 --- a/CareKitFHIR/CareKitFHIR/Patients/OCKDSTU2PatientConverter.swift +++ /dev/null @@ -1,122 +0,0 @@ -/* -Copyright (c) 2019, Apple Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -3. Neither the name of the copyright holder(s) nor the names of any contributors -may be used to endorse or promote products derived from this software without -specific prior written permission. No license is granted to the trademarks of -the copyright holders even if such marks are included in this software. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -import CareKitStore -import Foundation -import ModelsDSTU2 - -extension ModelsDSTU2.Patient: OCKFHIRResource { - public typealias Release = DSTU2 -} - -/// Converts a FHIR DSTU2 `Patient` to an `OCKPatient`. -/// -/// The mapping is predefined to use reasonable defaults, but it is possible to configure -/// the behavior by setting properties on `OCKDSTU2PatientCoder`. -public struct OCKDSTU2PatientCoder: OCKPatientConverterTraits { - - public typealias Resource = ModelsDSTU2.Patient - public typealias Release = Resource.Release - public typealias Entity = OCKPatient - - /// Initialize an `OCKDSTU2PatientCoder` that uses default mappings between FHIR and - /// CareKit patient models. - public init() {} - - // MARK: Convert FHIR Patient to OCKPatient - - public var getId: (Patient) throws -> String = { - guard let id = $0.id?.string else { - throw OCKConversionError.missingRequiredField("id") - } - return id - } - - public var getName: (Patient) throws -> PersonNameComponents = { - guard let fhirName = $0.name?.first else { - throw OCKConversionError.missingRequiredField("name") - } - var components = PersonNameComponents() - components.familyName = fhirName.family?.first?.string - components.givenName = fhirName.given?.first?.string - components.namePrefix = fhirName.prefix?.first?.string - components.nameSuffix = fhirName.suffix?.first?.string - return components - } - - public var getSex: (Patient) throws -> OCKBiologicalSex? = { - switch $0.gender { - case .male: return .male - case .female: return .female - case .other: return .other("other") - case .unknown: return nil - case .none: return nil - } - } - - public var getBirthday: (Patient) throws -> Date? = { - guard - let birthday = $0.birthDate, - let day = birthday.day, - let month = birthday.month - else { return nil } - - var components = DateComponents() - components.year = birthday.year - components.month = Int(month) - components.day = Int(day) - - return Calendar.current.date(from: components) - } - - public var getAllergies: (Patient) throws -> [String]? = { _ in - nil - } - - // MARK: Convert OCKPatient to FHIR Patient - - func newResource() -> Resource { - Patient() - } - - public var putId: (String, Patient) throws -> Void = { id, patient in - patient.id = FHIRString(id) - } - - public var putName: (PersonNameComponents, Patient) throws -> Void = { name, patient in - let humanName = HumanName() - humanName.family = name.familyName == nil ? [] : [name.familyName!.fhirString()] - humanName.given = name.givenName == nil ? [] : [name.givenName!.fhirString()] - humanName.prefix = name.namePrefix == nil ? [] : [name.namePrefix!.fhirString()] - humanName.suffix = name.nameSuffix == nil ? [] : [name.nameSuffix!.fhirString()] - - patient.name = [humanName] - } -} diff --git a/CareKitStore.podspec b/CareKitStore.podspec new file mode 100644 index 000000000..8b271d70b --- /dev/null +++ b/CareKitStore.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'CareKitStore' + s.version = '2.0' + s.summary = 'CareKit is an open source software framework for creating apps that help people better understand and manage their health.' + s.homepage = 'https://github.com/carekit-apple/CareKit/' + s.documentation_url = 'https://developer.apple.com/documentation/carekit' + s.screenshots = [ 'https://user-images.githubusercontent.com/51756298/69096972-66de0b00-0a0a-11ea-96f0-4605d04ab396.gif', + 'https://user-images.githubusercontent.com/51756298/69107801-7586eb00-0a27-11ea-8aa2-eca687602c76.gif'] + s.license = { :type => 'BSD', :file => 'LICENSE' } + s.author = { 'researchandcare.org' => 'https://www.researchandcare.org' } + s.platform = :ios + s.ios.deployment_target = '13.0' + s.watchos.deployment_target = '6.0' + s.swift_versions = '5.0' + s.source = { :git => 'https://github.com/carekit-apple/carekit.git', :tag => s.version.to_s, :submodules => true } + + s.source_files = 'CareKitStore/CareKitStore/**/*' + s.exclude_files = 'CareKitStore/CareKitStore/**/*.plist' + s.frameworks = 'HealthKit', 'CoreData' + +end diff --git a/CareKitStore/CareKitStore.xcodeproj/xcshareddata/xcschemes/CareKitStore.xcscheme b/CareKitStore/CareKitStore.xcodeproj/xcshareddata/xcschemes/CareKitStore.xcscheme new file mode 100644 index 000000000..b7c98790b --- /dev/null +++ b/CareKitStore/CareKitStore.xcodeproj/xcshareddata/xcschemes/CareKitStore.xcscheme @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1140" + version = "1.7"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "E784B8F72232EED600736CA5" + BuildableName = "CareKitStore.framework" + BlueprintName = "CareKitStore iOS" + ReferencedContainer = "container:CareKitStore.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "E784B8F72232EED600736CA5" + BuildableName = "CareKitStore.framework" + BlueprintName = "CareKitStore iOS" + ReferencedContainer = "container:CareKitStore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <TestPlans> + <TestPlanReference + reference = "container:CareKitStoreTests/CareKitStore.xctestplan" + default = "YES"> + </TestPlanReference> + </TestPlans> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "E784B9002232EED600736CA5" + BuildableName = "CareKitStoreTests iOS.xctest" + BlueprintName = "CareKitStoreTests iOS" + ReferencedContainer = "container:CareKitStore.xcodeproj"> + </BuildableReference> + <SkippedTests> + <Test + Identifier = "TestSchedule/testEventGenerationPerformanceBasicSchedule()"> + </Test> + <Test + Identifier = "TestSchedule/testEventGenerationPerformanceHeavySchedule()"> + </Test> + </SkippedTests> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "E784B8F72232EED600736CA5" + BuildableName = "CareKitStore.framework" + BlueprintName = "CareKitStore iOS" + ReferencedContainer = "container:CareKitStore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "E784B8F72232EED600736CA5" + BuildableName = "CareKitStore.framework" + BlueprintName = "CareKitStore iOS" + ReferencedContainer = "container:CareKitStore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/CareKitStore/CareKitStore.xcodeproj/xcshareddata/xcschemes/CareKitStoreTests.xcscheme b/CareKitStore/CareKitStore.xcodeproj/xcshareddata/xcschemes/CareKitStoreTests.xcscheme new file mode 100644 index 000000000..702bb2c0f --- /dev/null +++ b/CareKitStore/CareKitStore.xcodeproj/xcshareddata/xcschemes/CareKitStoreTests.xcscheme @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1140" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "E784B9002232EED600736CA5" + BuildableName = "CareKitStoreTests iOS.xctest" + BlueprintName = "CareKitStoreTests iOS" + ReferencedContainer = "container:CareKitStore.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + <CodeCoverageTargets> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "E784B9002232EED600736CA5" + BuildableName = "CareKitStoreTests iOS.xctest" + BlueprintName = "CareKitStoreTests iOS" + ReferencedContainer = "container:CareKitStore.xcodeproj"> + </BuildableReference> + </CodeCoverageTargets> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "E784B9002232EED600736CA5" + BuildableName = "CareKitStoreTests iOS.xctest" + BlueprintName = "CareKitStoreTests iOS" + ReferencedContainer = "container:CareKitStore.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/CareKitStore/CareKitStore/CoreData/OCKManagedObjectModel.swift b/CareKitStore/CareKitStore/CoreData/OCKManagedObjectModel.swift index 040c1ef8c..6271511fe 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKManagedObjectModel.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKManagedObjectModel.swift @@ -381,7 +381,7 @@ private func makeManagedObjectModel() -> NSManagedObjectModel { noteToTask.destinationEntity = task noteToTask.isOptional = true noteToTask.minCount = 0 - noteToTask.maxCount = 0 + noteToTask.maxCount = 1 noteToTask.deleteRule = .nullifyDeleteRule let noteToOutcome = NSRelationshipDescription() diff --git a/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift b/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift index cf865d472..b709777aa 100644 --- a/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift +++ b/CareKitStore/CareKitStore/CoreData/OCKStore+Outcomes.swift @@ -151,6 +151,7 @@ extension OCKStore { currentOutcomes.forEach { $0.deletedDate = Date() $0.values = Set() + $0.logicalClock = Int64(context.clockTime) } let newOutcomes = outcomes.map(self.createOutcome) diff --git a/CareKitStore/CareKitStore/CoreData/Synchronization/OCKPeer.swift b/CareKitStore/CareKitStore/CoreData/Synchronization/OCKPeer.swift new file mode 100644 index 000000000..4fb59f346 --- /dev/null +++ b/CareKitStore/CareKitStore/CoreData/Synchronization/OCKPeer.swift @@ -0,0 +1,72 @@ +/* + 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 + +/// An `OCKStore` peer that can be used when the peer's store is available locally. +/// This class is intended to be used for testing purposes. +public class OCKLocalPeer: OCKRemoteSynchronizable { + + public weak var delegate: OCKRemoteSynchronizationDelegate? + public weak var store: OCKStore! + public let peerStore: OCKStore + + public init(peerStore: OCKStore) { + self.peerStore = peerStore + } + + public var conflictPolicy: OCKMergeConflictResolutionPolicy = .keepDevice + public var automaticallySynchronizes: Bool = true + + public func pullRevisions( + since knowledgeVector: OCKRevisionRecord.KnowledgeVector, + mergeRevision: @escaping (OCKRevisionRecord, @escaping (Error?) -> Void) -> Void, + completion: @escaping (Error?) -> Void) { + + let clock = knowledgeVector.clock(for: peerStore.context.clockID) + let revision = peerStore.computeRevision(since: clock) + mergeRevision(revision, completion) + } + + public func pushRevisions( + deviceRevision: OCKRevisionRecord, + overwriteRemote: Bool, + completion: @escaping (Error?) -> Void) { + + peerStore.mergeRevision(deviceRevision, completion: completion) + } + + public func chooseConflictResolutionPolicy( + _ conflict: OCKMergeConflictDescription, + completion: @escaping (OCKMergeConflictResolutionPolicy) -> Void) { + + completion(conflictPolicy) + } +} diff --git a/CareKitStore/CareKitStore/HealthKit/OCKHealthKitStore+Outcomes.swift b/CareKitStore/CareKitStore/HealthKit/OCKHealthKitStore+Outcomes.swift index 582c3980b..e54fdad71 100644 --- a/CareKitStore/CareKitStore/HealthKit/OCKHealthKitStore+Outcomes.swift +++ b/CareKitStore/CareKitStore/HealthKit/OCKHealthKitStore+Outcomes.swift @@ -66,13 +66,13 @@ public extension OCKHealthKitPassthroughStore { else { throw OCKStoreError.addFailed(reason: "Cannot persist an OCKHealthKitOutcome that is not owned by this app!") } guard outcome.values.count == 1 else { throw OCKStoreError.addFailed(reason: "OCKHealthKitOutcomes must have exactly 1 value, but got \(outcome.values.count).") } - guard let value = outcome.values.first?.doubleValue + guard let valueString = outcome.values.first?.doubleValue else { throw OCKStoreError.addFailed(reason: "OCKHealthKitOutcome's value must be of type Double, but was not.") } guard let task = tasks.first(where: { $0.uuid == outcome.taskUUID }) else { throw OCKStoreError.addFailed(reason: "No task could be for outcome") } let unit = HKUnit(from: task.healthKitLinkage.unitString) - let quantity = HKQuantity(unit: unit, doubleValue: value) + let quantity = HKQuantity(unit: unit, doubleValue: valueString) let type = HKObjectType.quantityType(forIdentifier: task.healthKitLinkage.quantityIdentifier)! let event = task.schedule.event(forOccurrenceIndex: outcome.taskOccurrenceIndex)! let eventInterval = DateInterval(start: event.start, end: event.end) @@ -93,8 +93,8 @@ public extension OCKHealthKitPassthroughStore { // Copy the newly assigned HealthKit UUID from the HKSample objects to the saves outcomes. var saved = outcomes - samples.enumerated().forEach { index, value in - saved[index].healthKitUUIDs = Set([value.uuid]) + samples.enumerated().forEach { index, valueString in + saved[index].healthKitUUIDs = Set([valueString.uuid]) } callbackQueue.async { diff --git a/CareKitStore/CareKitStore/OCKAdherenceAggregator.swift b/CareKitStore/CareKitStore/OCKAdherenceAggregator.swift index 01f4073e5..fbb12b521 100644 --- a/CareKitStore/CareKitStore/OCKAdherenceAggregator.swift +++ b/CareKitStore/CareKitStore/OCKAdherenceAggregator.swift @@ -97,9 +97,11 @@ public enum OCKAdherenceAggregator { guard let outcome = event.outcome else { return 0 } let targetValues = event.scheduleEvent.element.targetValues guard targetValues.count <= outcome.values.count else { return 0 } - let indiciesToCheck = Array(0..<targetValues.count) - for index in indiciesToCheck { - if !outcomeFulfillsTarget(outcomeValue: outcome.values[index], target: targetValues[index]) { return 0 } + let indicesToCheck = Array(0..<targetValues.count) + for index in indicesToCheck { + if !outcomeFulfillsTarget(outcomeValue: outcome.values[index], target: targetValues[index]) { + return 0 + } } return 1 } @@ -120,31 +122,29 @@ public enum OCKAdherenceAggregator { return Double(numberComplete) / Double(totalTargets) } - private func outcomeFulfillsTarget(outcomeValue value: OCKOutcomeValue, target: OCKOutcomeValue) -> Bool { - assert(value.type == target.type, "Actual outcome value and target value should not have different types!") - guard value.type == target.type else { return false } + private func outcomeFulfillsTarget( + outcomeValue value: OCKOutcomeValue, + target: OCKOutcomeValue) -> Bool { - switch value.type { - case .binary: return checkEquality(lhs: value, rhs: target, keyPath: \.dateValue) - case .boolean: return checkEquality(lhs: value, rhs: target, keyPath: \.booleanValue) - case .text: return checkEquality(lhs: value, rhs: target, keyPath: \.stringValue) - case .double: return compare(lhs: value, greaterThanOrEqualTo: target, keyPath: \.doubleValue) - case .date: return compare(lhs: value, greaterThanOrEqualTo: target, keyPath: \.dateValue) - case .integer: return compare(lhs: value, greaterThanOrEqualTo: target, keyPath: \.integerValue) + // Comparing two numerals (int, double, bool) + if let number = value.numberValue, let targetNumber = target.numberValue { + let comparison = number.compare(targetNumber) + return comparison == .orderedDescending || comparison == .orderedSame } - } - private func checkEquality<T: Equatable>(lhs: OCKOutcomeValue, - rhs: OCKOutcomeValue, - keyPath: KeyPath<OCKOutcomeValue, T?>) -> Bool { - guard let lhsValue = lhs[keyPath: keyPath], let rhsValue = rhs[keyPath: keyPath] else { return false } - return lhsValue == rhsValue - } + // Comparing non-numerals (date, text, binary) + assert(value.type == target.type, "Actual outcome value and target value should not have different types!") + guard value.type == target.type else { + return false + } - private func compare<T: Comparable>(lhs: OCKOutcomeValue, - greaterThanOrEqualTo rhs: OCKOutcomeValue, - keyPath: KeyPath<OCKOutcomeValue, T?>) -> Bool { - guard let lhsValue = lhs[keyPath: keyPath], let rhsValue = rhs[keyPath: keyPath] else { return false } - return lhsValue >= rhsValue + switch value.type { + case .binary: return true + case .text: return true + case .date: return value.dateValue! >= target.dateValue! + default: + assertionFailure("Unexpected value type: \(value.type)") + return false + } } } diff --git a/CareKitStore/CareKitStore/Protocols/CarePlans/OCKAnyCarePlanCategory.swift b/CareKitStore/CareKitStore/Protocols/CarePlans/OCKAnyCarePlanCategory.swift new file mode 100644 index 000000000..26faeb448 --- /dev/null +++ b/CareKitStore/CareKitStore/Protocols/CarePlans/OCKAnyCarePlanCategory.swift @@ -0,0 +1,50 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +/// Conforming a type to `OCKAnyCarePlanCategory` allows it to be queried and displayed by CareKit. +public protocol OCKAnyCarePlanCategory { + + /// A user-defined unique identifier, typically human readable. + var id: String { get } + + /// A title describing this care plan. + var title: String { get } + + /// An identifier for this care plan in a remote store. + var remoteID: String? { get } + + /// An identifier that can be uesd to group this care plan with others. + var groupIdentifier: String? { get } + + /// Any array of notes associated with this object. + var notes: [OCKNote]? { get } +} diff --git a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataStoreProtocol.swift b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataStoreProtocol.swift index 712c4ad46..9e3286eb7 100644 --- a/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataStoreProtocol.swift +++ b/CareKitStore/CareKitStore/Protocols/CoreData/OCKCoreDataStoreProtocol.swift @@ -178,6 +178,7 @@ extension OCKCoreDataStoreProtocol { guard let current = currentVersions.first(where: { $0.id == value.id }) else { throw OCKStoreError.invalidValue(reason: "No matching object could be found for id: \(value.id)") } + current.logicalClock = Int64(context.clockTime) let newVersion = addNewVersion(value) newVersion.previous = current newVersion.uuid = UUID() diff --git a/CareKitStore/CareKitStore/Protocols/Events/OCKAnyEvent.swift b/CareKitStore/CareKitStore/Protocols/Events/OCKAnyEvent.swift index 45130672e..7588b7d62 100644 --- a/CareKitStore/CareKitStore/Protocols/Events/OCKAnyEvent.swift +++ b/CareKitStore/CareKitStore/Protocols/Events/OCKAnyEvent.swift @@ -65,7 +65,7 @@ public struct OCKAnyEvent { self.scheduleEvent = scheduleEvent var hasher = Hasher() - hasher.combine(task.stableID) + hasher.combine(task.uuid) hasher.combine(scheduleEvent.occurrence) id = "\(hasher.finalize())" } diff --git a/CareKitStore/CareKitStore/Protocols/OCKStoreProtocol+Synchronous.swift b/CareKitStore/CareKitStore/Protocols/OCKStoreProtocol+Synchronous.swift index 9dd7d6d27..72d904c67 100644 --- a/CareKitStore/CareKitStore/Protocols/OCKStoreProtocol+Synchronous.swift +++ b/CareKitStore/CareKitStore/Protocols/OCKStoreProtocol+Synchronous.swift @@ -314,7 +314,7 @@ extension OCKStore { private func performSynchronously<T>( _ closure: @escaping (@escaping (Result<T, OCKStoreError>) -> Void) -> Void) throws -> T { - let timeout: TimeInterval = 10.0 + let timeout: TimeInterval = 30.0 let dispatchGroup = DispatchGroup() var closureResult: Result<T, OCKStoreError> = .failure(.timedOut( reason: "Timed out after \(timeout) seconds.")) diff --git a/CareKitStore/CareKitStore/Protocols/Tasks/OCKAnyTask.swift b/CareKitStore/CareKitStore/Protocols/Tasks/OCKAnyTask.swift index 72e69d599..8b48263f9 100644 --- a/CareKitStore/CareKitStore/Protocols/Tasks/OCKAnyTask.swift +++ b/CareKitStore/CareKitStore/Protocols/Tasks/OCKAnyTask.swift @@ -67,12 +67,6 @@ public protocol OCKAnyTask { func belongs(to plan: OCKAnyCarePlan) -> Bool } -extension OCKAnyTask { - - /// The stable identifier that can be used for the `Identifiable` protocol. - var stableID: String? { uuid?.uuidString } -} - internal protocol OCKAnyMutableTask: OCKAnyTask { var title: String? { get set } var instructions: String? { get set } diff --git a/CareKitStore/CareKitStore/StoreCoordinator/OCKPersistentStoreCoordinator+Tasks.swift b/CareKitStore/CareKitStore/StoreCoordinator/OCKPersistentStoreCoordinator+Tasks.swift index a31c3a02f..5a131b33d 100644 --- a/CareKitStore/CareKitStore/StoreCoordinator/OCKPersistentStoreCoordinator+Tasks.swift +++ b/CareKitStore/CareKitStore/StoreCoordinator/OCKPersistentStoreCoordinator+Tasks.swift @@ -57,7 +57,7 @@ extension OCKStoreCoordinator { open func updateAnyTasks(_ tasks: [OCKAnyTask], callbackQueue: DispatchQueue = .main, completion: ((Result<[OCKAnyTask], OCKStoreError>) -> Void)? = nil) { do { - try findStore(forTasks: tasks).addAnyTasks(tasks, callbackQueue: callbackQueue, completion: completion) + try findStore(forTasks: tasks).updateAnyTasks(tasks, callbackQueue: callbackQueue, completion: completion) } catch { callbackQueue.async { completion?(.failure(.updateFailed( diff --git a/CareKitStore/CareKitStore/Structs/OCKCarePlanCategory.swift b/CareKitStore/CareKitStore/Structs/OCKCarePlanCategory.swift new file mode 100644 index 000000000..5285f36e0 --- /dev/null +++ b/CareKitStore/CareKitStore/Structs/OCKCarePlanCategory.swift @@ -0,0 +1,71 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +/// Represents a patient +public struct OCKCarePlanCategory: Codable, Equatable, Identifiable, OCKAnyCarePlanCategory, OCKVersionedObjectCompatible { + + // MARK: OCKAnyCarePlanCategory + public var title: String + public let id: String + + // MARK: OCKVersionedObjectCompatible + public var effectiveDate: Date + public var deletedDate: Date? + public var uuid: UUID? + public var nextVersionUUID: UUID? + public var previousVersionUUID: UUID? + + // MARK: OCKObjectCompatible + public internal(set) var createdDate: Date? + public internal(set) var updatedDate: Date? + public internal(set) var schemaVersion: OCKSemanticVersion? + public var groupIdentifier: String? + public var tags: [String]? + public var remoteID: String? + public var source: String? + public var userInfo: [String: String]? + public var asset: String? + public var notes: [OCKNote]? + public var timezone: TimeZone + + /// Initialize a patient with an id, a first name, and a last name. + /// + /// - Parameters: + /// - id: A user-defined id unique to this category. + /// - title: The category title + public init(id: String, title: String) { + self.title = title + self.id = id + self.effectiveDate = Date() + self.timezone = TimeZone.current + } +} diff --git a/CareKitStore/CareKitStore/Structs/OCKEvent.swift b/CareKitStore/CareKitStore/Structs/OCKEvent.swift index 1323c18ff..bb4c22c0a 100644 --- a/CareKitStore/CareKitStore/Structs/OCKEvent.swift +++ b/CareKitStore/CareKitStore/Structs/OCKEvent.swift @@ -58,7 +58,7 @@ public struct OCKEvent<Task: OCKAnyTask, Outcome: OCKAnyOutcome>: Identifiable { self.scheduleEvent = scheduleEvent var hasher = Hasher() - hasher.combine(task.stableID) + hasher.combine(task.uuid) hasher.combine(scheduleEvent.occurrence) id = "\(hasher.finalize())" } diff --git a/CareKitStore/CareKitStore/Structs/OCKOutcomeValue.swift b/CareKitStore/CareKitStore/Structs/OCKOutcomeValue.swift index c41471029..f623551b9 100644 --- a/CareKitStore/CareKitStore/Structs/OCKOutcomeValue.swift +++ b/CareKitStore/CareKitStore/Structs/OCKOutcomeValue.swift @@ -58,51 +58,53 @@ public struct OCKOutcomeValue: Codable, Equatable, OCKObjectCompatible, CustomSt case kind, units, uuid, value, type, index, createdDate, updatedDate, schemaVersion, tags, - group, remoteID, userInfo, source, timezone + groupIdentifier, remoteID, userInfo, source, timezone, asset, notes } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let valueType = try container.decode(OCKOutcomeValueType.self, forKey: CodingKeys.type) + let valueType = try container.decode(OCKOutcomeValueType.self, forKey: .type) switch valueType { case .integer: - value = try container.decode(Int.self, forKey: CodingKeys.value) + value = try container.decode(Int.self, forKey: .value) case .double: - value = try container.decode(Double.self, forKey: CodingKeys.value) + value = try container.decode(Double.self, forKey: .value) case .boolean: - value = try container.decode(Bool.self, forKey: CodingKeys.value) + value = try container.decode(Bool.self, forKey: .value) case .text: - value = try container.decode(String.self, forKey: CodingKeys.value) + value = try container.decode(String.self, forKey: .value) case .binary: - value = try container.decode(Data.self, forKey: CodingKeys.value) + value = try container.decode(Data.self, forKey: .value) case .date: - value = try container.decode(Date.self, forKey: CodingKeys.value) + value = try container.decode(Date.self, forKey: .value) } - kind = try container.decode(String?.self, forKey: CodingKeys.kind) - units = try container.decode(String?.self, forKey: CodingKeys.units) - index = try container.decode(Int?.self, forKey: CodingKeys.index) - uuid = try container.decode(UUID?.self, forKey: CodingKeys.uuid) - createdDate = try container.decode(Date?.self, forKey: CodingKeys.createdDate) - updatedDate = try container.decode(Date?.self, forKey: CodingKeys.updatedDate) - schemaVersion = try container.decode(OCKSemanticVersion?.self, forKey: CodingKeys.schemaVersion) - groupIdentifier = try container.decode(String?.self, forKey: CodingKeys.group) - tags = try container.decode([String]?.self, forKey: CodingKeys.tags) - remoteID = try container.decode(String?.self, forKey: CodingKeys.remoteID) - source = try container.decode(String?.self, forKey: CodingKeys.source) - userInfo = try container.decode([String: String]?.self, forKey: CodingKeys.userInfo) - timezone = try container.decode(TimeZone.self, forKey: CodingKeys.timezone) + kind = try container.decodeIfPresent(String.self, forKey: .kind) + units = try container.decodeIfPresent(String.self, forKey: .units) + index = try container.decodeIfPresent(Int.self, forKey: .index) + uuid = try container.decodeIfPresent(UUID.self, forKey: .uuid) + createdDate = try container.decodeIfPresent(Date.self, forKey: .createdDate) + updatedDate = try container.decodeIfPresent(Date.self, forKey: .updatedDate) + schemaVersion = try container.decodeIfPresent(OCKSemanticVersion.self, forKey: .schemaVersion) + groupIdentifier = try container.decodeIfPresent(String.self, forKey: .groupIdentifier) + tags = try container.decodeIfPresent([String].self, forKey: .tags) + remoteID = try container.decodeIfPresent(String.self, forKey: .remoteID) + source = try container.decodeIfPresent(String.self, forKey: .source) + userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo) + timezone = try container.decode(TimeZone.self, forKey: .timezone) + asset = try container.decodeIfPresent(String.self, forKey: .asset) + notes = try container.decodeIfPresent([OCKNote].self, forKey: .notes) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type, forKey: .type) - try container.encode(kind, forKey: .kind) - try container.encode(units, forKey: .units) - try container.encode(index, forKey: .index) - try container.encode(uuid, forKey: .uuid) + try container.encodeIfPresent(kind, forKey: .kind) + try container.encodeIfPresent(units, forKey: .units) + try container.encodeIfPresent(index, forKey: .index) + try container.encodeIfPresent(uuid, forKey: .uuid) var encodedValue = false if let value = integerValue { try container.encode(value, forKey: .value); encodedValue = true } else @@ -117,14 +119,16 @@ public struct OCKOutcomeValue: Codable, Equatable, OCKObjectCompatible, CustomSt throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [CodingKeys.value], debugDescription: message)) } - try container.encode(updatedDate, forKey: .updatedDate) - try container.encode(createdDate, forKey: .createdDate) - try container.encode(schemaVersion, forKey: .schemaVersion) - try container.encode(groupIdentifier, forKey: .group) - try container.encode(tags, forKey: .tags) - try container.encode(remoteID, forKey: .remoteID) - try container.encode(source, forKey: .source) - try container.encode(userInfo, forKey: .userInfo) + try container.encodeIfPresent(updatedDate, forKey: .updatedDate) + try container.encodeIfPresent(createdDate, forKey: .createdDate) + try container.encodeIfPresent(schemaVersion, forKey: .schemaVersion) + try container.encodeIfPresent(groupIdentifier, forKey: .groupIdentifier) + try container.encodeIfPresent(tags, forKey: .tags) + try container.encodeIfPresent(remoteID, forKey: .remoteID) + try container.encodeIfPresent(source, forKey: .source) + try container.encodeIfPresent(userInfo, forKey: .userInfo) + try container.encodeIfPresent(asset, forKey: .asset) + try container.encodeIfPresent(notes, forKey: .notes) try container.encode(timezone, forKey: .timezone) } @@ -218,7 +222,7 @@ public struct OCKOutcomeValue: Codable, Equatable, OCKObjectCompatible, CustomSt } /// Checks if two `OCKOutcomeValue`s have equal value properties, without checking their other properties. - func hasSameValueAs(_ other: OCKOutcomeValue) -> Bool { + private func hasSameValueAs(_ other: OCKOutcomeValue) -> Bool { switch type { case .binary: return dataValue == other.dataValue case .boolean: return booleanValue == other.booleanValue @@ -228,4 +232,15 @@ public struct OCKOutcomeValue: Codable, Equatable, OCKObjectCompatible, CustomSt case .text: return stringValue == other.stringValue } } + + // The value as an `NSNumber`. This property can be useful when comparing outcome values with an underlying + // type of Bool, Double, or Int against one another. + public var numberValue: NSNumber? { + switch type { + case .boolean: return NSNumber(value: booleanValue!) + case .double: return NSNumber(value: doubleValue!) + case .integer: return NSNumber(value: integerValue!) + default: return nil + } + } } diff --git a/CareKitStore/CareKitStore/Structs/OCKTask.swift b/CareKitStore/CareKitStore/Structs/OCKTask.swift index b1b4f9ea7..e41c2b42c 100644 --- a/CareKitStore/CareKitStore/Structs/OCKTask.swift +++ b/CareKitStore/CareKitStore/Structs/OCKTask.swift @@ -33,7 +33,7 @@ import Foundation /// An `OCKTask` represents some task or action that a patient is supposed to perform. Tasks are optionally associable with an `OCKCarePlan` /// and must have a unique id and schedule. The schedule determines when and how often the task should be performed, and the /// `impactsAdherence` flag may be used to specify whether or not the patients adherence to this task will affect their daily completion rings. -public struct OCKTask: Codable, Equatable, OCKAnyVersionableTask, OCKAnyMutableTask, OCKVersionedObjectCompatible { +public struct OCKTask: Codable, Equatable, OCKAnyVersionableTask, OCKAnyMutableTask, OCKVersionedObjectCompatible, Identifiable { /// The UUID of the care plan to which this task belongs. public var carePlanUUID: UUID? diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+BuildRevisions.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+BuildRevisions.swift index 9a92cd970..f80cda9c5 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+BuildRevisions.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+BuildRevisions.swift @@ -94,7 +94,7 @@ class TestStoreBuildRevisions: XCTestCase { try store.deleteTasksAndWait([taskA2]) let revision = store.computeRevision(since: store.context.clockTime) - XCTAssert(revision.entities.count == 1) + XCTAssert(revision.entities.count == 2) XCTAssert(revision.entities.first?.deletedDate != nil) } diff --git a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Sync.swift b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Sync.swift index 7eb101094..fbabb10c6 100644 --- a/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Sync.swift +++ b/CareKitStore/CareKitStoreTests/OCKStore/TestStore+Sync.swift @@ -266,6 +266,90 @@ class TestStoreSync: XCTestCase { XCTAssert(localOutcomes == remoteOutcomes) XCTAssert(localOutcomes.count == 1) } + + func testTombstoningOutcomePushedToRemote() throws { + let dummy = DummyEndpoint() + let testStore = OCKStore(name: "test", type: .inMemory, remote: dummy) + dummy.automaticallySynchronizes = false + + let schedule = OCKSchedule.mealTimesEachDay(start: Date(), end: nil) + let task = try testStore.addTaskAndWait(OCKTask(id: "A", title: "A", carePlanUUID: nil, schedule: schedule)) + let taskUUID = try task.getUUID() + + let outcome = try testStore.addOutcomeAndWait(OCKOutcome(taskUUID: taskUUID, taskOccurrenceIndex: 0, values: [OCKOutcomeValue("test")])) + let outcomeUUID = try outcome.getUUID() + XCTAssertNoThrow(try testStore.syncAndWait()) //Sync original outcome + + try testStore.deleteOutcomeAndWait(outcome) + XCTAssertNoThrow(try testStore.syncAndWait()) //Sync tombstoned outcome + let latestRevisions = dummy.revisionsPushedInLastSynch + XCTAssert(latestRevisions.count == 2) + + let tombstonedOutcomes = latestRevisions.compactMap { entity -> OCKOutcome? in + switch entity { + case .outcome(let outcome): + return outcome + default: + return nil + } + } + XCTAssert(tombstonedOutcomes.count == 2) + + guard let tombstonedWithSameUUID = try tombstonedOutcomes.first(where: { try $0.getUUID() == outcomeUUID }) else { + throw OCKStoreError.invalidValue(reason: "Filter doesn't contain UUID") + } + XCTAssert(tombstonedWithSameUUID.values.isEmpty) + XCTAssert(tombstonedWithSameUUID.deletedDate != nil) + + guard let tombstonedWithDifferentUUID = try tombstonedOutcomes.first(where: { try $0.getUUID() != outcomeUUID }) else { + throw OCKStoreError.invalidValue(reason: "Filter doesn't contain UUID") + } + XCTAssert(tombstonedWithDifferentUUID.values.count == 1) + XCTAssert(tombstonedWithDifferentUUID.deletedDate != nil) + } + + func testUpdateTaskVersionPushedToRemote() throws { + let dummy = DummyEndpoint() + let testStore = OCKStore(name: "test", type: .inMemory, remote: dummy) + dummy.automaticallySynchronizes = false + + let schedule = OCKSchedule.mealTimesEachDay(start: Date(), end: nil) + var task = try testStore.addTaskAndWait(OCKTask(id: "A", title: "A", carePlanUUID: nil, schedule: schedule)) + let taskUUID = try task.getUUID() + + XCTAssertNoThrow(try testStore.syncAndWait()) //Sync original outcome + + task.instructions = "Updated instructions" + try testStore.updateTaskAndWait(task) + XCTAssertNoThrow(try testStore.syncAndWait()) //Sync updated outcome + + let latestRevisions = dummy.revisionsPushedInLastSynch + XCTAssert(latestRevisions.count == 2) + + let versionedTasks = latestRevisions.compactMap { entity -> OCKTask? in + switch entity { + case .task(let task): + return task + default: + return nil + } + } + XCTAssert(versionedTasks.count == 2) + + guard let previousVersionTask = try versionedTasks.first(where: { try $0.getUUID() == taskUUID }) else { + throw OCKStoreError.invalidValue(reason: "Filter doesn't contain UUID") + } + + guard let currentVersionTask = try versionedTasks.first(where: { try $0.getUUID() != taskUUID }) else { + throw OCKStoreError.invalidValue(reason: "Filter doesn't contain UUID") + } + XCTAssert(previousVersionTask.instructions == nil) + XCTAssert(try previousVersionTask.getNextVersionUUID() == currentVersionTask.getUUID()) + + XCTAssert(currentVersionTask.instructions != nil) + XCTAssert(try currentVersionTask.getPreviousVersionUUID() == taskUUID) + XCTAssertThrowsError(try currentVersionTask.getNextVersionUUID()) + } } class DummyEndpoint: OCKRemoteSynchronizable { @@ -278,6 +362,9 @@ class DummyEndpoint: OCKRemoteSynchronizable { private(set) var timesPullWasCalled = 0 private(set) var timesPushWasCalled = 0 private(set) var timesForcePushed = 0 + private(set) var uuid = UUID() + private(set) var dummyKnowledgeVector: OCKRevisionRecord.KnowledgeVector? + var revisionsPushedInLastSynch = [OCKEntity]() var conflictPolicy = OCKMergeConflictResolutionPolicy.keepRemote var revision = OCKRevisionRecord(entities: [], knowledgeVector: .init()) @@ -293,6 +380,12 @@ class DummyEndpoint: OCKRemoteSynchronizable { completion(OCKStoreError.remoteSynchronizationFailed(reason: "Failed on purpose")) return } + + guard let dummyVector = self.dummyKnowledgeVector else { + mergeRevision(self.revision, completion) + return + } + self.revision = OCKRevisionRecord(entities: [], knowledgeVector: dummyVector) mergeRevision(self.revision, completion) } } @@ -304,6 +397,18 @@ class DummyEndpoint: OCKRemoteSynchronizable { timesPushWasCalled += 1 timesForcePushed += overwriteRemote ? 1 : 0 + + //Save latest revisions + revisionsPushedInLastSynch.removeAll() + revisionsPushedInLastSynch.append(contentsOf: deviceRevision.entities) + + //Update KnowledgeVector + if dummyKnowledgeVector == nil { + dummyKnowledgeVector = .init([uuid: 0]) + } + dummyKnowledgeVector?.increment(clockFor: uuid) + dummyKnowledgeVector?.merge(with: deviceRevision.knowledgeVector) + completion(nil) } @@ -356,17 +461,17 @@ class DummyEndpoint: OCKRemoteSynchronizable { final class OCKLocalPeer: OCKRemoteSynchronizable { - public weak var delegate: OCKRemoteSynchronizationDelegate? - public let peerStore: OCKStore + weak var delegate: OCKRemoteSynchronizationDelegate? + let peerStore: OCKStore - public init(peerStore: OCKStore) { + init(peerStore: OCKStore) { self.peerStore = peerStore } - public var conflictPolicy: OCKMergeConflictResolutionPolicy = .keepDevice - public var automaticallySynchronizes: Bool = true + var conflictPolicy: OCKMergeConflictResolutionPolicy = .keepDevice + var automaticallySynchronizes: Bool = true - public func pullRevisions( + func pullRevisions( since knowledgeVector: OCKRevisionRecord.KnowledgeVector, mergeRevision: @escaping (OCKRevisionRecord, @escaping (Error?) -> Void) -> Void, completion: @escaping (Error?) -> Void) { @@ -376,7 +481,7 @@ final class OCKLocalPeer: OCKRemoteSynchronizable { mergeRevision(revision, completion) } - public func pushRevisions( + func pushRevisions( deviceRevision: OCKRevisionRecord, overwriteRemote: Bool, completion: @escaping (Error?) -> Void) { @@ -384,7 +489,7 @@ final class OCKLocalPeer: OCKRemoteSynchronizable { peerStore.mergeRevision(deviceRevision, completion: completion) } - public func chooseConflictResolutionPolicy( + func chooseConflictResolutionPolicy( _ conflict: OCKMergeConflictDescription, completion: @escaping (OCKMergeConflictResolutionPolicy) -> Void) { diff --git a/CareKitStore/CareKitStoreTests/Structs/TestOutcomeValue.swift b/CareKitStore/CareKitStoreTests/Structs/TestOutcomeValue.swift index 5656c8647..daa3facac 100644 --- a/CareKitStore/CareKitStoreTests/Structs/TestOutcomeValue.swift +++ b/CareKitStore/CareKitStoreTests/Structs/TestOutcomeValue.swift @@ -116,6 +116,78 @@ class TestOutcomeValue: XCTestCase { testEqualityOfEncodings(outcome1: value1, outcome2: value2) } } + + func testProperDecodingWhenMissingValues() throws { + let valueToDecode = "{\"value\": 10,\"timezone\": {\"identifier\": \"America/New_York\"},\"type\": \"\(OCKOutcomeValueType.integer.rawValue)\"}" + + guard let data = valueToDecode.data(using: .utf8) else { + throw OCKStoreError.invalidValue(reason: "Error: Couldn't get data as utf8") + } + + let decoded = try JSONDecoder().decode(OCKOutcomeValue.self, from: data) + + XCTAssertNil(decoded.asset) + XCTAssertNil(decoded.notes) + XCTAssertEqual(decoded.timezone, TimeZone(identifier: "America/New_York")) + if let decodedUnderValue = decoded.value as? Int { + XCTAssertEqual(decodedUnderValue, 10) + } else { + XCTFail("Should have underlying value") + } + } + + func testCodingAllEntries() throws { + var value = OCKOutcomeValue(10) + let valueNote = OCKNote(author: "myId", title: "hello", content: "world") + + //Value + value.index = 0 + value.kind = "whale" + value.units = "m/s" + + //OCKObjectCompatible + value.uuid = UUID() + value.createdDate = Date().addingTimeInterval(-200) + value.updatedDate = Date().addingTimeInterval(-99) + value.timezone = .current + value.userInfo = ["String": "String"] + value.remoteID = "we" + value.groupIdentifier = "mine" + value.tags = ["one", "two"] + value.schemaVersion = .init(majorVersion: 4) + value.source = "yo" + value.asset = "pic" + value.notes = [valueNote] + + let encoded = try JSONEncoder().encode(value) + let decoded = try JSONDecoder().decode(OCKOutcomeValue.self, from: encoded) + + XCTAssertEqual(decoded.index, value.index) + XCTAssertEqual(decoded.kind, value.kind) + XCTAssertEqual(decoded.units, value.units) + if let decodedUnderValue = decoded.value as? Int, + let currentUnderValue = value.value as? Int { + XCTAssertEqual(decodedUnderValue, currentUnderValue) + } else { + XCTFail("Should have underlying value") + } + + XCTAssertEqual(decoded.uuid, value.uuid) + XCTAssertEqual(decoded.createdDate, value.createdDate) + XCTAssertEqual(decoded.updatedDate, value.updatedDate) + XCTAssertEqual(decoded.timezone, value.timezone) + XCTAssertEqual(decoded.userInfo, value.userInfo) + XCTAssertEqual(decoded.remoteID, value.remoteID) + XCTAssertEqual(decoded.source, value.source) + XCTAssertEqual(decoded.schemaVersion, value.schemaVersion) + XCTAssertEqual(decoded.tags, value.tags) + XCTAssertEqual(decoded.groupIdentifier, value.groupIdentifier) + XCTAssertEqual(decoded.asset, value.asset) + XCTAssertEqual(decoded.notes?.count, 1) + XCTAssertEqual(decoded.notes?.first?.author, "myId") + XCTAssertEqual(decoded.notes?.first?.title, "hello") + XCTAssertEqual(decoded.notes?.first?.content, "world") + } func testEvolvingValue() { var value = OCKOutcomeValue("abc") diff --git a/CareKitStore/CareKitStoreTests/Structs/TestTask.swift b/CareKitStore/CareKitStoreTests/Structs/TestTask.swift index bf3f4899c..a7771a296 100644 --- a/CareKitStore/CareKitStoreTests/Structs/TestTask.swift +++ b/CareKitStore/CareKitStoreTests/Structs/TestTask.swift @@ -158,12 +158,12 @@ class TestTask: XCTestCase { var task1 = OCKTask(id: "doxylamine", title: "Title1", carePlanUUID: nil, schedule: schedule) var task2 = OCKTask(id: "doxylamine", title: "Title2", carePlanUUID: nil, schedule: schedule) - XCTAssertEqual(task1.stableID, task2.stableID) + XCTAssertEqual(task1.uuid, task2.uuid) let uuid = UUID() task1.uuid = uuid task2.uuid = uuid - XCTAssertEqual(task1.stableID, task2.stableID) + XCTAssertEqual(task1.uuid, task2.uuid) } func testIdentitiesDoNotMatch() { @@ -173,6 +173,6 @@ class TestTask: XCTestCase { var task2 = OCKTask(id: "doxylamine", title: "Title2", carePlanUUID: nil, schedule: schedule) task1.uuid = UUID() task2.uuid = UUID() - XCTAssertNotEqual(task1.stableID, task2.stableID) + XCTAssertNotEqual(task1.uuid, task2.uuid) } } diff --git a/CareKitStore/CareKitStoreTests/TestAdherenceAggregator.swift b/CareKitStore/CareKitStoreTests/TestAdherenceAggregator.swift index 3df47c6f6..da32729cb 100644 --- a/CareKitStore/CareKitStoreTests/TestAdherenceAggregator.swift +++ b/CareKitStore/CareKitStoreTests/TestAdherenceAggregator.swift @@ -32,16 +32,40 @@ import Foundation import XCTest class TestAdherenceAggregators: XCTestCase { - func makeEvents(outcomeValues: [OCKOutcomeValueUnderlyingType], targetValues: [OCKOutcomeValueUnderlyingType]) -> [OCKAnyEvent] { - let element = OCKScheduleElement(start: Date(), end: nil, interval: DateComponents(day: 1), - text: nil, targetValues: targetValues.map { OCKOutcomeValue($0) }, - duration: .seconds(100)) - let task = OCKTask(id: "A", title: "A", carePlanUUID: nil, schedule: OCKSchedule(composing: [element])) - let values = outcomeValues.map { OCKOutcomeValue($0) } - let outcome = OCKOutcome(taskUUID: UUID(), taskOccurrenceIndex: 0, values: values) + + func makeEvents( + outcomeValues: [OCKOutcomeValueUnderlyingType], + targetValues: [OCKOutcomeValueUnderlyingType]) -> [OCKAnyEvent] { + + let element = OCKScheduleElement( + start: Date(), end: nil, + interval: DateComponents(day: 1), + text: nil, + targetValues: targetValues.map { OCKOutcomeValue($0) }, + duration: .seconds(100)) + + let task = OCKTask( + id: "A", title: "A", + carePlanUUID: nil, + schedule: OCKSchedule(composing: [element])) + + let outcome = OCKOutcome( + taskUUID: UUID(), + taskOccurrenceIndex: 0, + values: outcomeValues.map { OCKOutcomeValue($0) }) + let scheduleEvent = task.schedule[0] - let event1: OCKStore.Event = OCKEvent(task: task, outcome: outcome, scheduleEvent: scheduleEvent) - let event2: OCKStore.Event = OCKEvent(task: task, outcome: nil, scheduleEvent: scheduleEvent) + + let event1: OCKStore.Event = OCKEvent( + task: task, + outcome: outcome, + scheduleEvent: scheduleEvent) + + let event2: OCKStore.Event = OCKEvent( + task: task, + outcome: nil, + scheduleEvent: scheduleEvent) + return [event1.anyEvent, event2.anyEvent] } @@ -59,7 +83,7 @@ class TestAdherenceAggregators: XCTestCase { XCTAssert(result == .progress(0.25), "Expected 0.25, but got \(result)") } - func testPercentOfOutcomesvaluesThatExistWithNoGoals() { + func testPercentOfOutcomesValuesThatExistWithNoGoals() { let events = makeEvents(outcomeValues: [0], targetValues: []) let aggregator = OCKAdherenceAggregator.percentOfOutcomeValuesThatExist let result = aggregator.aggregate(events: events) @@ -94,6 +118,20 @@ class TestAdherenceAggregators: XCTestCase { XCTAssert(result == .progress(0.5), "Expected 0.5 but got \(result)") } + func testCompareTargetValuesWithMismatchedNumeralTypes() { + let events = makeEvents(outcomeValues: [12.0], targetValues: [10]) + let aggregator = OCKAdherenceAggregator.compareTargetValues + let result = aggregator.aggregate(events: events) + XCTAssert(result == .progress(0.5), "Expected 0.5 but got \(result)") + } + + func testCompareTargetValuesWithMismatchedNumeralAndBooleanTypes() { + let events = makeEvents(outcomeValues: [false], targetValues: [10]) + let aggregator = OCKAdherenceAggregator.compareTargetValues + let result = aggregator.aggregate(events: events) + XCTAssert(result == .progress(0), "Expected 0 but got \(result)") + } + func testTargetCompletionPercentage() { let events = makeEvents(outcomeValues: [10, "fail"], targetValues: [10, "fail"]) let aggregator = OCKAdherenceAggregator.percentOfTargetValuesMet diff --git a/CareKitStore/CareKitStoreTests/TestWatchConnectivityPeer.swift b/CareKitStore/CareKitStoreTests/TestWatchConnectivityPeer.swift index a299a76e7..52b664afc 100644 --- a/CareKitStore/CareKitStoreTests/TestWatchConnectivityPeer.swift +++ b/CareKitStore/CareKitStoreTests/TestWatchConnectivityPeer.swift @@ -172,7 +172,7 @@ class TestWatchConnectivityPeer: XCTestCase { try storeA.syncAndWait() try storeA.deleteOutcomeAndWait(outcomeA) try storeA.syncAndWait() - XCTAssert(peerA.lastRevisionPushedToPeer?.entities.count == 1) + XCTAssert(peerA.lastRevisionPushedToPeer?.entities.count == 2) let tasksA = try storeA.fetchTasksAndWait() let tasksB = try storeB.fetchTasksAndWait() diff --git a/CareKitStore/CareKitStoreTests/Utils.swift b/CareKitStore/CareKitStoreTests/Utils.swift index e14b97a0c..b81ab426d 100644 --- a/CareKitStore/CareKitStoreTests/Utils.swift +++ b/CareKitStore/CareKitStoreTests/Utils.swift @@ -66,3 +66,16 @@ extension OCKObjectCompatible { return uuid } } + +extension OCKVersionedObjectCompatible { + func getPreviousVersionUUID() throws -> UUID { + guard let uuid = previousVersionUUID else { throw OCKStoreError.invalidValue(reason: "Missing previousVersionUUID") } + return uuid + } + + func getNextVersionUUID() throws -> UUID { + guard let uuid = nextVersionUUID else { throw OCKStoreError.invalidValue(reason: "Missing nextVersionUUID") } + return uuid + } +} + diff --git a/CareKitUI.podspec b/CareKitUI.podspec new file mode 100644 index 000000000..9b9de372b --- /dev/null +++ b/CareKitUI.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'CareKitUI' + s.version = '2.0' + s.summary = 'CareKit is an open source software framework for creating apps that help people better understand and manage their health.' + s.homepage = 'https://github.com/carekit-apple/CareKit/' + s.documentation_url = 'https://developer.apple.com/documentation/carekit' + s.screenshots = [ 'https://user-images.githubusercontent.com/51756298/69096972-66de0b00-0a0a-11ea-96f0-4605d04ab396.gif', + 'https://user-images.githubusercontent.com/51756298/69107801-7586eb00-0a27-11ea-8aa2-eca687602c76.gif'] + s.license = { :type => 'BSD', :file => 'LICENSE' } + s.author = { 'researchandcare.org' => 'https://www.researchandcare.org' } + s.platform = :ios + s.ios.deployment_target = '13.0' + s.watchos.deployment_target = '6.0' + s.swift_versions = '5.0' + s.source = { :git => 'https://github.com/carekit-apple/carekit.git', :tag => s.version.to_s} + + s.source_files = 'CareKitUI/CareKitUI/**/*' + #s.resources = ['CareKitUI/CareKitUI/Supporting Files/Localization/*.lproj'] + s.exclude_files = ['CareKitUI/CareKitUI/**/*.plist'] + s.frameworks = 'UIKit', 'SwiftUI' + +end diff --git a/CareKitUI/CareKitUI.xcodeproj/project.pbxproj b/CareKitUI/CareKitUI.xcodeproj/project.pbxproj index bd9c58e52..af916cf0b 100644 --- a/CareKitUI/CareKitUI.xcodeproj/project.pbxproj +++ b/CareKitUI/CareKitUI.xcodeproj/project.pbxproj @@ -1,1310 +1,1316 @@ // !$*UTF8*$! { - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { /* Begin PBXBuildFile section */ - 03089D7F231ED7AF0054EA23 /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03089D7E231ED7AF0054EA23 /* Calendar+Extensions.swift */; }; - 03181064236A158E006D4870 /* OCKCappedSizeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03181063236A158E006D4870 /* OCKCappedSizeLabel.swift */; }; - 03AD384623625A3500F6E7DC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 03AD384423625A3500F6E7DC /* Localizable.stringsdict */; }; - 5103C55222F37B44007A7403 /* Number+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103C55122F37B44007A7403 /* Number+Extensions.swift */; }; - 510466F524A283E500D0FD53 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B225A223EDDCFC00A2D11F /* InstructionsTaskView.swift */; }; - 510466F924A2850700D0FD53 /* OCKContactDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510466F824A2850700D0FD53 /* OCKContactDisplayable.swift */; }; - 5105254724462BCE004483D0 /* TestLinkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105254524462AF5004483D0 /* TestLinkType.swift */; }; - 51052549244639B3004483D0 /* TestLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51052548244639B3004483D0 /* TestLinkView.swift */; }; - 5105911F237651C8004EDC84 /* OCKAccessibleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */; }; - 510A5762234A7B920006C376 /* OCKAnimatedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */; }; - 510CC634243B843C008BD6B3 /* OCKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510CC633243B843C008BD6B3 /* OCKLog.swift */; }; - 510D862D23020D410073776E /* OCKStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D862C23020D410073776E /* OCKStyler.swift */; }; - 51173F3C243FE007004CB150 /* CircularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51173F3B243FE007004CB150 /* CircularCompletionView.swift */; }; - 51173F3E243FE022004CB150 /* RectangularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51173F3D243FE022004CB150 /* RectangularCompletionView.swift */; }; - 51178E1523AAAB2F0068BAB1 /* OCKCompletionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51178E1423AAAB2F0068BAB1 /* OCKCompletionState.swift */; }; - 511BCB5C24365E2E00B4D643 /* OCKDetailedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511BCB5B24365E2E00B4D643 /* OCKDetailedImageView.swift */; }; - 512B013322C2F82900ABCB1D /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5174225B224185290054E97C /* CareKitUI.framework */; }; - 512EE90022975F850052F37C /* OCKDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512EE8FF22975F850052F37C /* OCKDetailView.swift */; }; - 5136794B243BF6EC0026997B /* LinkItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51367946243BF6EA0026997B /* LinkItem.swift */; }; - 5136794C243BF6EC0026997B /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51367947243BF6EB0026997B /* SafariView.swift */; }; - 5136794D243BF6EC0026997B /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51367948243BF6EC0026997B /* LinkButton.swift */; }; - 51367950243BF7040026997B /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5136794F243BF7040026997B /* LinkView.swift */; }; - 51367952243BF7450026997B /* LinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51367951243BF7450026997B /* LinkLabel.swift */; }; - 513FE9FD24803AE10016FCE6 /* LabeledValueTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513FE9FC24803AE10016FCE6 /* LabeledValueTaskView.swift */; }; - 515C3C8022F8AB9A007AC906 /* OCKSimpleContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515C3C7F22F8AB9A007AC906 /* OCKSimpleContactView.swift */; }; - 51637EA322F3A68400BAF65C /* OCKLogTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51637EA222F3A68400BAF65C /* OCKLogTaskView.swift */; }; - 51656029234AB47500F2A21F /* OCKCheckmarkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51656028234AB47500F2A21F /* OCKCheckmarkButton.swift */; }; - 516A02BA22F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A02B922F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift */; }; - 516A02BC22F363D900D55AD7 /* OCKView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A02BB22F363D900D55AD7 /* OCKView.swift */; }; - 516A1C46244F703000BBF2D3 /* Number+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103C55122F37B44007A7403 /* Number+Extensions.swift */; }; - 516A1C48244F766A00BBF2D3 /* OSValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A1C47244F766A00BBF2D3 /* OSValue.swift */; }; - 516A1C49244F766A00BBF2D3 /* OSValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A1C47244F766A00BBF2D3 /* OSValue.swift */; }; - 516A1C4F244FA7E500BBF2D3 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A1C4E244FA7E500BBF2D3 /* View+Extension.swift */; }; - 516A1C50244FA7E900BBF2D3 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A1C4E244FA7E500BBF2D3 /* View+Extension.swift */; }; - 51746F2B2448B90B00B647E1 /* TestNumericProgressTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51746F2A2448B90B00B647E1 /* TestNumericProgressTaskView.swift */; }; - 5178FBDA22E253FC00794353 /* OCKChartDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5178FBD922E253FC00794353 /* OCKChartDisplayable.swift */; }; - 518F9D8822961BF5009CAA48 /* OCKAddressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4522961BF5009CAA48 /* OCKAddressButton.swift */; }; - 518F9D8922961BF5009CAA48 /* OCKDetailedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4622961BF5009CAA48 /* OCKDetailedContactView.swift */; }; - 518F9D8A22961BF5009CAA48 /* OCKContactButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4722961BF5009CAA48 /* OCKContactButton.swift */; }; - 518F9D8C22961BF5009CAA48 /* OCKChecklistTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4B22961BF5009CAA48 /* OCKChecklistTaskView.swift */; }; - 518F9D8E22961BF5009CAA48 /* OCKSimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4D22961BF5009CAA48 /* OCKSimpleTaskView.swift */; }; - 518F9D8F22961BF5009CAA48 /* OCKChecklistItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4E22961BF5009CAA48 /* OCKChecklistItemButton.swift */; }; - 518F9D9022961BF5009CAA48 /* OCKInstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4F22961BF5009CAA48 /* OCKInstructionsTaskView.swift */; }; - 518F9D9122961BF5009CAA48 /* OCKLabeledButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5022961BF5009CAA48 /* OCKLabeledButton.swift */; }; - 518F9D9222961BF5009CAA48 /* OCKSelfSizingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5122961BF5009CAA48 /* OCKSelfSizingCollectionView.swift */; }; - 518F9D9322961BF5009CAA48 /* OCKButtonLogTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5222961BF5009CAA48 /* OCKButtonLogTaskView.swift */; }; - 518F9D9422961BF5009CAA48 /* OCKGridTaskCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5322961BF5009CAA48 /* OCKGridTaskCell.swift */; }; - 518F9D9522961BF5009CAA48 /* OCKGridTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5422961BF5009CAA48 /* OCKGridTaskView.swift */; }; - 518F9D9622961BF5009CAA48 /* OCKLogItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5522961BF5009CAA48 /* OCKLogItemButton.swift */; }; - 518F9D9722961BF5009CAA48 /* OCKGradientPlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5722961BF5009CAA48 /* OCKGradientPlotView.swift */; }; - 518F9D9822961BF5009CAA48 /* OCKGraphLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5822961BF5009CAA48 /* OCKGraphLegendView.swift */; }; - 518F9D9922961BF5009CAA48 /* OCKScatterPlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5922961BF5009CAA48 /* OCKScatterPlotView.swift */; }; - 518F9D9A22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5B22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift */; }; - 518F9D9B22961BF5009CAA48 /* OCKScatterLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5C22961BF5009CAA48 /* OCKScatterLayer.swift */; }; - 518F9D9C22961BF5009CAA48 /* OCKGridLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5D22961BF5009CAA48 /* OCKGridLayer.swift */; }; - 518F9D9D22961BF5009CAA48 /* OCKLineLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5E22961BF5009CAA48 /* OCKLineLayer.swift */; }; - 518F9D9E22961BF5009CAA48 /* OCKBarLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5F22961BF5009CAA48 /* OCKBarLayer.swift */; }; - 518F9D9F22961BF5009CAA48 /* OCKGraphAxisView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6022961BF5009CAA48 /* OCKGraphAxisView.swift */; }; - 518F9DA022961BF6009CAA48 /* OCKDataSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6122961BF5009CAA48 /* OCKDataSeries.swift */; }; - 518F9DA122961BF6009CAA48 /* OCKCartesianChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6222961BF5009CAA48 /* OCKCartesianChartView.swift */; }; - 518F9DA222961BF6009CAA48 /* OCKBarPlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6322961BF5009CAA48 /* OCKBarPlotView.swift */; }; - 518F9DA322961BF6009CAA48 /* OCKGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6422961BF5009CAA48 /* OCKGridView.swift */; }; - 518F9DA422961BF6009CAA48 /* OCKLinePlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6522961BF5009CAA48 /* OCKLinePlotView.swift */; }; - 518F9DA522961BF6009CAA48 /* OCKCartesianGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6622961BF5009CAA48 /* OCKCartesianGraphView.swift */; }; - 518F9DA622961BF6009CAA48 /* OCKGraphable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6722961BF5009CAA48 /* OCKGraphable.swift */; }; - 518F9DA722961BF6009CAA48 /* OCKCompletionRingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6922961BF5009CAA48 /* OCKCompletionRingView.swift */; }; - 518F9DA922961BF6009CAA48 /* OCKRingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6B22961BF5009CAA48 /* OCKRingView.swift */; }; - 518F9DAB22961BF6009CAA48 /* UIFont+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6F22961BF5009CAA48 /* UIFont+Extensions.swift */; }; - 518F9DAE22961BF6009CAA48 /* OCKColorStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7422961BF5009CAA48 /* OCKColorStyler.swift */; }; - 518F9DB022961BF6009CAA48 /* OCKAnimationStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7622961BF5009CAA48 /* OCKAnimationStyler.swift */; }; - 518F9DB122961BF6009CAA48 /* OCKAppearanceStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7722961BF5009CAA48 /* OCKAppearanceStyler.swift */; }; - 518F9DB222961BF6009CAA48 /* OCKDimensionStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7822961BF5009CAA48 /* OCKDimensionStyler.swift */; }; - 518F9DB422961BF6009CAA48 /* OCKHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7B22961BF5009CAA48 /* OCKHeaderView.swift */; }; - 518F9DB522961BF6009CAA48 /* OCKWeekCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7C22961BF5009CAA48 /* OCKWeekCalendarView.swift */; }; - 518F9DB722961BF6009CAA48 /* OCKSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7E22961BF5009CAA48 /* OCKSeparatorView.swift */; }; - 518F9DBA22961BF6009CAA48 /* OCKCompletionRingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D8122961BF5009CAA48 /* OCKCompletionRingButton.swift */; }; - 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 */; }; - 51B225A323EDDCFC00A2D11F /* NoHighlightStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259A23EDDCFC00A2D11F /* NoHighlightStyle.swift */; }; - 51B225A923EDDCFC00A2D11F /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259E23EDDCFC00A2D11F /* CardView.swift */; }; - 51B225AB23EDDCFC00A2D11F /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259F23EDDCFC00A2D11F /* HeaderView.swift */; }; - 51B225AD23EDDCFC00A2D11F /* SimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B225A123EDDCFC00A2D11F /* SimpleTaskView.swift */; }; - 51B225AF23EDDCFC00A2D11F /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B225A223EDDCFC00A2D11F /* InstructionsTaskView.swift */; }; - 51D5C97124A2947A00EC45B5 /* OCKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510CC633243B843C008BD6B3 /* OCKLog.swift */; }; - 51E214292444F1520063A121 /* RectangularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51173F3D243FE022004CB150 /* RectangularCompletionView.swift */; }; - 51E2142A2444F1580063A121 /* CircularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51173F3B243FE007004CB150 /* CircularCompletionView.swift */; }; - 51E76F1D24004F19008B09E7 /* NoHighlightStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259A23EDDCFC00A2D11F /* NoHighlightStyle.swift */; }; - 51E76F1E24004F1F008B09E7 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259E23EDDCFC00A2D11F /* CardView.swift */; }; - 51E76F1F24004F21008B09E7 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259F23EDDCFC00A2D11F /* HeaderView.swift */; }; - 51E76F2024004F25008B09E7 /* SimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B225A123EDDCFC00A2D11F /* SimpleTaskView.swift */; }; - 51E77383237D1D8000ED70A2 /* TestGridTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E77382237D1D8000ED70A2 /* TestGridTaskView.swift */; }; - 51EFC75623FCAE6000536266 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EFC75423FCAE6000536266 /* UIColor+Extension.swift */; }; - 51EFC75723FCAE6000536266 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EFC75423FCAE6000536266 /* UIColor+Extension.swift */; }; - 51F12D39229C81E200CA265B /* OCKLabeledCheckmarkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F12D38229C81E200CA265B /* OCKLabeledCheckmarkButton.swift */; }; - 51F75B942447DD0B00978C71 /* NumericProgressTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F75B932447DD0B00978C71 /* NumericProgressTaskView.swift */; }; - 51F78FEE22D7C7C100058858 /* OCKCalendarDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F78FED22D7C7C100058858 /* OCKCalendarDisplayable.swift */; }; - 51F9F13023A9B9F80087C900 /* OCKColorStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7422961BF5009CAA48 /* OCKColorStyler.swift */; }; - 51F9F13123A9B9F80087C900 /* OCKAnimationStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7622961BF5009CAA48 /* OCKAnimationStyler.swift */; }; - 51F9F13223A9B9F80087C900 /* OCKAppearanceStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7722961BF5009CAA48 /* OCKAppearanceStyler.swift */; }; - 51F9F13323A9B9F80087C900 /* OCKDimensionStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7822961BF5009CAA48 /* OCKDimensionStyler.swift */; }; - 51F9F13423A9B9FF0087C900 /* OCKStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D862C23020D410073776E /* OCKStyler.swift */; }; - 51F9F16623A9BEC40087C900 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 64EEC2362318253B00B1012F /* Localizable.strings */; }; - 51F9F16723A9BEC70087C900 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 03AD384423625A3500F6E7DC /* Localizable.stringsdict */; }; - 51F9F16823A9BEC90087C900 /* OCKLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEC23A231825DD00B1012F /* OCKLocalization.swift */; }; - 51FEF4C624325903003CE34C /* OCKFeaturedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FEF4C324325902003CE34C /* OCKFeaturedContentView.swift */; }; - 64699E9922FC7B5000FF624F /* OCKLogButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64699E9822FC7B5000FF624F /* OCKLogButtonCell.swift */; }; - 64E1BD4C2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1BD4B2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift */; }; - 64E1BD4E2309FCF200DFFE52 /* OCKResponsiveLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1BD4D2309FCF200DFFE52 /* OCKResponsiveLayout.swift */; }; - 64EEC2382318253B00B1012F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 64EEC2362318253B00B1012F /* Localizable.strings */; }; - 64EEC23B231825DD00B1012F /* OCKLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEC23A231825DD00B1012F /* OCKLocalization.swift */; }; + 03089D7F231ED7AF0054EA23 /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03089D7E231ED7AF0054EA23 /* Calendar+Extensions.swift */; }; + 03181064236A158E006D4870 /* OCKCappedSizeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03181063236A158E006D4870 /* OCKCappedSizeLabel.swift */; }; + 03AD384623625A3500F6E7DC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 03AD384423625A3500F6E7DC /* Localizable.stringsdict */; }; + 5103C55222F37B44007A7403 /* Number+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103C55122F37B44007A7403 /* Number+Extensions.swift */; }; + 510466F524A283E500D0FD53 /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B225A223EDDCFC00A2D11F /* InstructionsTaskView.swift */; }; + 510466F924A2850700D0FD53 /* OCKContactDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510466F824A2850700D0FD53 /* OCKContactDisplayable.swift */; }; + 5105254724462BCE004483D0 /* TestLinkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105254524462AF5004483D0 /* TestLinkType.swift */; }; + 51052549244639B3004483D0 /* TestLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51052548244639B3004483D0 /* TestLinkView.swift */; }; + 5105911F237651C8004EDC84 /* OCKAccessibleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */; }; + 510A5762234A7B920006C376 /* OCKAnimatedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */; }; + 510CC634243B843C008BD6B3 /* OCKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510CC633243B843C008BD6B3 /* OCKLog.swift */; }; + 510D862D23020D410073776E /* OCKStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D862C23020D410073776E /* OCKStyler.swift */; }; + 51173F3C243FE007004CB150 /* CircularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51173F3B243FE007004CB150 /* CircularCompletionView.swift */; }; + 51173F3E243FE022004CB150 /* RectangularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51173F3D243FE022004CB150 /* RectangularCompletionView.swift */; }; + 51178E1523AAAB2F0068BAB1 /* OCKCompletionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51178E1423AAAB2F0068BAB1 /* OCKCompletionState.swift */; }; + 511BCB5C24365E2E00B4D643 /* OCKDetailedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511BCB5B24365E2E00B4D643 /* OCKDetailedImageView.swift */; }; + 512B013322C2F82900ABCB1D /* CareKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5174225B224185290054E97C /* CareKitUI.framework */; }; + 512EE90022975F850052F37C /* OCKDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512EE8FF22975F850052F37C /* OCKDetailView.swift */; }; + 5136794B243BF6EC0026997B /* LinkItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51367946243BF6EA0026997B /* LinkItem.swift */; }; + 5136794C243BF6EC0026997B /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51367947243BF6EB0026997B /* SafariView.swift */; }; + 5136794D243BF6EC0026997B /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51367948243BF6EC0026997B /* LinkButton.swift */; }; + 51367950243BF7040026997B /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5136794F243BF7040026997B /* LinkView.swift */; }; + 51367952243BF7450026997B /* LinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51367951243BF7450026997B /* LinkLabel.swift */; }; + 513FE9FD24803AE10016FCE6 /* LabeledValueTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513FE9FC24803AE10016FCE6 /* LabeledValueTaskView.swift */; }; + 515C3C8022F8AB9A007AC906 /* OCKSimpleContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515C3C7F22F8AB9A007AC906 /* OCKSimpleContactView.swift */; }; + 51637EA322F3A68400BAF65C /* OCKLogTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51637EA222F3A68400BAF65C /* OCKLogTaskView.swift */; }; + 51656029234AB47500F2A21F /* OCKCheckmarkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51656028234AB47500F2A21F /* OCKCheckmarkButton.swift */; }; + 516A02BA22F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A02B922F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift */; }; + 516A02BC22F363D900D55AD7 /* OCKView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A02BB22F363D900D55AD7 /* OCKView.swift */; }; + 516A1C46244F703000BBF2D3 /* Number+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103C55122F37B44007A7403 /* Number+Extensions.swift */; }; + 516A1C48244F766A00BBF2D3 /* OSValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A1C47244F766A00BBF2D3 /* OSValue.swift */; }; + 516A1C49244F766A00BBF2D3 /* OSValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A1C47244F766A00BBF2D3 /* OSValue.swift */; }; + 516A1C4F244FA7E500BBF2D3 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A1C4E244FA7E500BBF2D3 /* View+Extension.swift */; }; + 516A1C50244FA7E900BBF2D3 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A1C4E244FA7E500BBF2D3 /* View+Extension.swift */; }; + 517309A524AA6D7E00A35C85 /* OCKStyler+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517309A424AA6D7E00A35C85 /* OCKStyler+Extension.swift */; }; + 517309A624AA6D7E00A35C85 /* OCKStyler+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517309A424AA6D7E00A35C85 /* OCKStyler+Extension.swift */; }; + 51746F2B2448B90B00B647E1 /* TestNumericProgressTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51746F2A2448B90B00B647E1 /* TestNumericProgressTaskView.swift */; }; + 5178FBDA22E253FC00794353 /* OCKChartDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5178FBD922E253FC00794353 /* OCKChartDisplayable.swift */; }; + 518F9D8822961BF5009CAA48 /* OCKAddressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4522961BF5009CAA48 /* OCKAddressButton.swift */; }; + 518F9D8922961BF5009CAA48 /* OCKDetailedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4622961BF5009CAA48 /* OCKDetailedContactView.swift */; }; + 518F9D8A22961BF5009CAA48 /* OCKContactButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4722961BF5009CAA48 /* OCKContactButton.swift */; }; + 518F9D8C22961BF5009CAA48 /* OCKChecklistTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4B22961BF5009CAA48 /* OCKChecklistTaskView.swift */; }; + 518F9D8E22961BF5009CAA48 /* OCKSimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4D22961BF5009CAA48 /* OCKSimpleTaskView.swift */; }; + 518F9D8F22961BF5009CAA48 /* OCKChecklistItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4E22961BF5009CAA48 /* OCKChecklistItemButton.swift */; }; + 518F9D9022961BF5009CAA48 /* OCKInstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D4F22961BF5009CAA48 /* OCKInstructionsTaskView.swift */; }; + 518F9D9122961BF5009CAA48 /* OCKLabeledButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5022961BF5009CAA48 /* OCKLabeledButton.swift */; }; + 518F9D9222961BF5009CAA48 /* OCKSelfSizingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5122961BF5009CAA48 /* OCKSelfSizingCollectionView.swift */; }; + 518F9D9322961BF5009CAA48 /* OCKButtonLogTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5222961BF5009CAA48 /* OCKButtonLogTaskView.swift */; }; + 518F9D9422961BF5009CAA48 /* OCKGridTaskCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5322961BF5009CAA48 /* OCKGridTaskCell.swift */; }; + 518F9D9522961BF5009CAA48 /* OCKGridTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5422961BF5009CAA48 /* OCKGridTaskView.swift */; }; + 518F9D9622961BF5009CAA48 /* OCKLogItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5522961BF5009CAA48 /* OCKLogItemButton.swift */; }; + 518F9D9722961BF5009CAA48 /* OCKGradientPlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5722961BF5009CAA48 /* OCKGradientPlotView.swift */; }; + 518F9D9822961BF5009CAA48 /* OCKGraphLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5822961BF5009CAA48 /* OCKGraphLegendView.swift */; }; + 518F9D9922961BF5009CAA48 /* OCKScatterPlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5922961BF5009CAA48 /* OCKScatterPlotView.swift */; }; + 518F9D9A22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5B22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift */; }; + 518F9D9B22961BF5009CAA48 /* OCKScatterLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5C22961BF5009CAA48 /* OCKScatterLayer.swift */; }; + 518F9D9C22961BF5009CAA48 /* OCKGridLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5D22961BF5009CAA48 /* OCKGridLayer.swift */; }; + 518F9D9D22961BF5009CAA48 /* OCKLineLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5E22961BF5009CAA48 /* OCKLineLayer.swift */; }; + 518F9D9E22961BF5009CAA48 /* OCKBarLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D5F22961BF5009CAA48 /* OCKBarLayer.swift */; }; + 518F9D9F22961BF5009CAA48 /* OCKGraphAxisView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6022961BF5009CAA48 /* OCKGraphAxisView.swift */; }; + 518F9DA022961BF6009CAA48 /* OCKDataSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6122961BF5009CAA48 /* OCKDataSeries.swift */; }; + 518F9DA122961BF6009CAA48 /* OCKCartesianChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6222961BF5009CAA48 /* OCKCartesianChartView.swift */; }; + 518F9DA222961BF6009CAA48 /* OCKBarPlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6322961BF5009CAA48 /* OCKBarPlotView.swift */; }; + 518F9DA322961BF6009CAA48 /* OCKGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6422961BF5009CAA48 /* OCKGridView.swift */; }; + 518F9DA422961BF6009CAA48 /* OCKLinePlotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6522961BF5009CAA48 /* OCKLinePlotView.swift */; }; + 518F9DA522961BF6009CAA48 /* OCKCartesianGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6622961BF5009CAA48 /* OCKCartesianGraphView.swift */; }; + 518F9DA622961BF6009CAA48 /* OCKGraphable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6722961BF5009CAA48 /* OCKGraphable.swift */; }; + 518F9DA722961BF6009CAA48 /* OCKCompletionRingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6922961BF5009CAA48 /* OCKCompletionRingView.swift */; }; + 518F9DA922961BF6009CAA48 /* OCKRingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6B22961BF5009CAA48 /* OCKRingView.swift */; }; + 518F9DAB22961BF6009CAA48 /* UIFont+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D6F22961BF5009CAA48 /* UIFont+Extensions.swift */; }; + 518F9DAE22961BF6009CAA48 /* OCKColorStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7422961BF5009CAA48 /* OCKColorStyler.swift */; }; + 518F9DB022961BF6009CAA48 /* OCKAnimationStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7622961BF5009CAA48 /* OCKAnimationStyler.swift */; }; + 518F9DB122961BF6009CAA48 /* OCKAppearanceStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7722961BF5009CAA48 /* OCKAppearanceStyler.swift */; }; + 518F9DB222961BF6009CAA48 /* OCKDimensionStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7822961BF5009CAA48 /* OCKDimensionStyler.swift */; }; + 518F9DB422961BF6009CAA48 /* OCKHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7B22961BF5009CAA48 /* OCKHeaderView.swift */; }; + 518F9DB522961BF6009CAA48 /* OCKWeekCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7C22961BF5009CAA48 /* OCKWeekCalendarView.swift */; }; + 518F9DB722961BF6009CAA48 /* OCKSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7E22961BF5009CAA48 /* OCKSeparatorView.swift */; }; + 518F9DBA22961BF6009CAA48 /* OCKCompletionRingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D8122961BF5009CAA48 /* OCKCompletionRingButton.swift */; }; + 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 */; }; + 51B225A323EDDCFC00A2D11F /* NoHighlightStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259A23EDDCFC00A2D11F /* NoHighlightStyle.swift */; }; + 51B225A923EDDCFC00A2D11F /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259E23EDDCFC00A2D11F /* CardView.swift */; }; + 51B225AB23EDDCFC00A2D11F /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259F23EDDCFC00A2D11F /* HeaderView.swift */; }; + 51B225AD23EDDCFC00A2D11F /* SimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B225A123EDDCFC00A2D11F /* SimpleTaskView.swift */; }; + 51B225AF23EDDCFC00A2D11F /* InstructionsTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B225A223EDDCFC00A2D11F /* InstructionsTaskView.swift */; }; + 51D5C97124A2947A00EC45B5 /* OCKLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510CC633243B843C008BD6B3 /* OCKLog.swift */; }; + 51E214292444F1520063A121 /* RectangularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51173F3D243FE022004CB150 /* RectangularCompletionView.swift */; }; + 51E2142A2444F1580063A121 /* CircularCompletionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51173F3B243FE007004CB150 /* CircularCompletionView.swift */; }; + 51E76F1D24004F19008B09E7 /* NoHighlightStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259A23EDDCFC00A2D11F /* NoHighlightStyle.swift */; }; + 51E76F1E24004F1F008B09E7 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259E23EDDCFC00A2D11F /* CardView.swift */; }; + 51E76F1F24004F21008B09E7 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B2259F23EDDCFC00A2D11F /* HeaderView.swift */; }; + 51E76F2024004F25008B09E7 /* SimpleTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B225A123EDDCFC00A2D11F /* SimpleTaskView.swift */; }; + 51E77383237D1D8000ED70A2 /* TestGridTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E77382237D1D8000ED70A2 /* TestGridTaskView.swift */; }; + 51EFC75623FCAE6000536266 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EFC75423FCAE6000536266 /* UIColor+Extension.swift */; }; + 51EFC75723FCAE6000536266 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EFC75423FCAE6000536266 /* UIColor+Extension.swift */; }; + 51F12D39229C81E200CA265B /* OCKLabeledCheckmarkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F12D38229C81E200CA265B /* OCKLabeledCheckmarkButton.swift */; }; + 51F75B942447DD0B00978C71 /* NumericProgressTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F75B932447DD0B00978C71 /* NumericProgressTaskView.swift */; }; + 51F78FEE22D7C7C100058858 /* OCKCalendarDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F78FED22D7C7C100058858 /* OCKCalendarDisplayable.swift */; }; + 51F9F13023A9B9F80087C900 /* OCKColorStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7422961BF5009CAA48 /* OCKColorStyler.swift */; }; + 51F9F13123A9B9F80087C900 /* OCKAnimationStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7622961BF5009CAA48 /* OCKAnimationStyler.swift */; }; + 51F9F13223A9B9F80087C900 /* OCKAppearanceStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7722961BF5009CAA48 /* OCKAppearanceStyler.swift */; }; + 51F9F13323A9B9F80087C900 /* OCKDimensionStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F9D7822961BF5009CAA48 /* OCKDimensionStyler.swift */; }; + 51F9F13423A9B9FF0087C900 /* OCKStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D862C23020D410073776E /* OCKStyler.swift */; }; + 51F9F16623A9BEC40087C900 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 64EEC2362318253B00B1012F /* Localizable.strings */; }; + 51F9F16723A9BEC70087C900 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 03AD384423625A3500F6E7DC /* Localizable.stringsdict */; }; + 51F9F16823A9BEC90087C900 /* OCKLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEC23A231825DD00B1012F /* OCKLocalization.swift */; }; + 51FEF4C624325903003CE34C /* OCKFeaturedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FEF4C324325902003CE34C /* OCKFeaturedContentView.swift */; }; + 64699E9922FC7B5000FF624F /* OCKLogButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64699E9822FC7B5000FF624F /* OCKLogButtonCell.swift */; }; + 64E1BD4C2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1BD4B2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift */; }; + 64E1BD4E2309FCF200DFFE52 /* OCKResponsiveLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1BD4D2309FCF200DFFE52 /* OCKResponsiveLayout.swift */; }; + 64EEC2382318253B00B1012F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 64EEC2362318253B00B1012F /* Localizable.strings */; }; + 64EEC23B231825DD00B1012F /* OCKLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEC23A231825DD00B1012F /* OCKLocalization.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 512B013422C2F82900ABCB1D /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 51742252224185290054E97C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 5174225A224185290054E97C; - remoteInfo = CareKitUI; - }; + 512B013422C2F82900ABCB1D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 51742252224185290054E97C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5174225A224185290054E97C; + remoteInfo = CareKitUI; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 03089D7E231ED7AF0054EA23 /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; }; - 03181063236A158E006D4870 /* OCKCappedSizeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCappedSizeLabel.swift; sourceTree = "<group>"; }; - 03AD384723625A4100F6E7DC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; - 05A6B74C237F49F7009D7D1F /* CareKitUI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CareKitUI.xctestplan; sourceTree = "<group>"; }; - 5103C55122F37B44007A7403 /* Number+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Extensions.swift"; sourceTree = "<group>"; }; - 510466F824A2850700D0FD53 /* OCKContactDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKContactDisplayable.swift; sourceTree = "<group>"; }; - 5105254524462AF5004483D0 /* TestLinkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLinkType.swift; sourceTree = "<group>"; }; - 51052548244639B3004483D0 /* TestLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLinkView.swift; sourceTree = "<group>"; }; - 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAccessibleValue.swift; sourceTree = "<group>"; }; - 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAnimatedButton.swift; sourceTree = "<group>"; }; - 510CC633243B843C008BD6B3 /* OCKLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLog.swift; sourceTree = "<group>"; }; - 510D862C23020D410073776E /* OCKStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKStyler.swift; sourceTree = "<group>"; }; - 51173F3B243FE007004CB150 /* CircularCompletionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularCompletionView.swift; sourceTree = "<group>"; }; - 51173F3D243FE022004CB150 /* RectangularCompletionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangularCompletionView.swift; sourceTree = "<group>"; }; - 51178E1423AAAB2F0068BAB1 /* OCKCompletionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCompletionState.swift; sourceTree = "<group>"; }; - 511A854723E0CAA2002A2AFB /* TestColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestColorExtension.swift; sourceTree = "<group>"; }; - 511BCB5B24365E2E00B4D643 /* OCKDetailedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailedImageView.swift; sourceTree = "<group>"; }; - 512B012E22C2F82900ABCB1D /* CareKitUITests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CareKitUITests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 512B013222C2F82900ABCB1D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; - 512EE8FF22975F850052F37C /* OCKDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailView.swift; sourceTree = "<group>"; }; - 51367946243BF6EA0026997B /* LinkItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkItem.swift; sourceTree = "<group>"; }; - 51367947243BF6EB0026997B /* SafariView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; }; - 51367948243BF6EC0026997B /* LinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = "<group>"; }; - 5136794F243BF7040026997B /* LinkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = "<group>"; }; - 51367951243BF7450026997B /* LinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkLabel.swift; sourceTree = "<group>"; }; - 513FE9FC24803AE10016FCE6 /* LabeledValueTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledValueTaskView.swift; sourceTree = "<group>"; }; - 515C3C7F22F8AB9A007AC906 /* OCKSimpleContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleContactView.swift; sourceTree = "<group>"; }; - 51637EA222F3A68400BAF65C /* OCKLogTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLogTaskView.swift; sourceTree = "<group>"; }; - 51656028234AB47500F2A21F /* OCKCheckmarkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCheckmarkButton.swift; sourceTree = "<group>"; }; - 516A02B922F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extensions.swift"; sourceTree = "<group>"; }; - 516A02BB22F363D900D55AD7 /* OCKView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKView.swift; sourceTree = "<group>"; }; - 516A1C47244F766A00BBF2D3 /* OSValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSValue.swift; sourceTree = "<group>"; }; - 516A1C4E244FA7E500BBF2D3 /* View+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; }; - 5174225B224185290054E97C /* CareKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CareKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5174225F224185290054E97C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; - 51746F2A2448B90B00B647E1 /* TestNumericProgressTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNumericProgressTaskView.swift; sourceTree = "<group>"; }; - 5178FBD922E253FC00794353 /* OCKChartDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChartDisplayable.swift; sourceTree = "<group>"; }; - 518F9D4522961BF5009CAA48 /* OCKAddressButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKAddressButton.swift; sourceTree = "<group>"; }; - 518F9D4622961BF5009CAA48 /* OCKDetailedContactView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDetailedContactView.swift; sourceTree = "<group>"; }; - 518F9D4722961BF5009CAA48 /* OCKContactButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKContactButton.swift; sourceTree = "<group>"; }; - 518F9D4B22961BF5009CAA48 /* OCKChecklistTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskView.swift; sourceTree = "<group>"; }; - 518F9D4D22961BF5009CAA48 /* OCKSimpleTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSimpleTaskView.swift; sourceTree = "<group>"; }; - 518F9D4E22961BF5009CAA48 /* OCKChecklistItemButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKChecklistItemButton.swift; sourceTree = "<group>"; }; - 518F9D4F22961BF5009CAA48 /* OCKInstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKInstructionsTaskView.swift; sourceTree = "<group>"; }; - 518F9D5022961BF5009CAA48 /* OCKLabeledButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLabeledButton.swift; sourceTree = "<group>"; }; - 518F9D5122961BF5009CAA48 /* OCKSelfSizingCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSelfSizingCollectionView.swift; sourceTree = "<group>"; }; - 518F9D5222961BF5009CAA48 /* OCKButtonLogTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKButtonLogTaskView.swift; sourceTree = "<group>"; }; - 518F9D5322961BF5009CAA48 /* OCKGridTaskCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGridTaskCell.swift; sourceTree = "<group>"; }; - 518F9D5422961BF5009CAA48 /* OCKGridTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGridTaskView.swift; sourceTree = "<group>"; }; - 518F9D5522961BF5009CAA48 /* OCKLogItemButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLogItemButton.swift; sourceTree = "<group>"; }; - 518F9D5722961BF5009CAA48 /* OCKGradientPlotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGradientPlotView.swift; sourceTree = "<group>"; }; - 518F9D5822961BF5009CAA48 /* OCKGraphLegendView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGraphLegendView.swift; sourceTree = "<group>"; }; - 518F9D5922961BF5009CAA48 /* OCKScatterPlotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKScatterPlotView.swift; sourceTree = "<group>"; }; - 518F9D5B22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCartesianCoordinatesLayer.swift; sourceTree = "<group>"; }; - 518F9D5C22961BF5009CAA48 /* OCKScatterLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKScatterLayer.swift; sourceTree = "<group>"; }; - 518F9D5D22961BF5009CAA48 /* OCKGridLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGridLayer.swift; sourceTree = "<group>"; }; - 518F9D5E22961BF5009CAA48 /* OCKLineLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLineLayer.swift; sourceTree = "<group>"; }; - 518F9D5F22961BF5009CAA48 /* OCKBarLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKBarLayer.swift; sourceTree = "<group>"; }; - 518F9D6022961BF5009CAA48 /* OCKGraphAxisView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGraphAxisView.swift; sourceTree = "<group>"; }; - 518F9D6122961BF5009CAA48 /* OCKDataSeries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDataSeries.swift; sourceTree = "<group>"; }; - 518F9D6222961BF5009CAA48 /* OCKCartesianChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCartesianChartView.swift; sourceTree = "<group>"; }; - 518F9D6322961BF5009CAA48 /* OCKBarPlotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKBarPlotView.swift; sourceTree = "<group>"; }; - 518F9D6422961BF5009CAA48 /* OCKGridView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGridView.swift; sourceTree = "<group>"; }; - 518F9D6522961BF5009CAA48 /* OCKLinePlotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLinePlotView.swift; sourceTree = "<group>"; }; - 518F9D6622961BF5009CAA48 /* OCKCartesianGraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCartesianGraphView.swift; sourceTree = "<group>"; }; - 518F9D6722961BF5009CAA48 /* OCKGraphable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGraphable.swift; sourceTree = "<group>"; }; - 518F9D6922961BF5009CAA48 /* OCKCompletionRingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCompletionRingView.swift; sourceTree = "<group>"; }; - 518F9D6B22961BF5009CAA48 /* OCKRingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKRingView.swift; sourceTree = "<group>"; }; - 518F9D6F22961BF5009CAA48 /* UIFont+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Extensions.swift"; sourceTree = "<group>"; }; - 518F9D7422961BF5009CAA48 /* OCKColorStyler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKColorStyler.swift; sourceTree = "<group>"; }; - 518F9D7622961BF5009CAA48 /* OCKAnimationStyler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKAnimationStyler.swift; sourceTree = "<group>"; }; - 518F9D7722961BF5009CAA48 /* OCKAppearanceStyler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKAppearanceStyler.swift; sourceTree = "<group>"; }; - 518F9D7822961BF5009CAA48 /* OCKDimensionStyler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDimensionStyler.swift; sourceTree = "<group>"; }; - 518F9D7B22961BF5009CAA48 /* OCKHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKHeaderView.swift; sourceTree = "<group>"; }; - 518F9D7C22961BF5009CAA48 /* OCKWeekCalendarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarView.swift; sourceTree = "<group>"; }; - 518F9D7E22961BF5009CAA48 /* OCKSeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSeparatorView.swift; sourceTree = "<group>"; }; - 518F9D8122961BF5009CAA48 /* OCKCompletionRingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCompletionRingButton.swift; sourceTree = "<group>"; }; - 518F9D8222961BF5009CAA48 /* OCKLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLabel.swift; sourceTree = "<group>"; }; - 518F9D8322961BF5009CAA48 /* OCKCardable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCardable.swift; sourceTree = "<group>"; }; - 518F9D8722961BF5009CAA48 /* OCKStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKStackView.swift; sourceTree = "<group>"; }; - 519288722427E2DC00D0AF43 /* CATransaction+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATransaction+Extension.swift"; sourceTree = "<group>"; }; - 5196B54222D5872C00800706 /* OCKTaskDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKTaskDisplayable.swift; sourceTree = "<group>"; }; - 51AB06BF22FE539400B73FC2 /* OCKStylable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKStylable.swift; sourceTree = "<group>"; }; - 51AB06C122FE53E300B73FC2 /* TestStylableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStylableView.swift; sourceTree = "<group>"; }; - 51B2259A23EDDCFC00A2D11F /* NoHighlightStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoHighlightStyle.swift; sourceTree = "<group>"; }; - 51B2259E23EDDCFC00A2D11F /* CardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; }; - 51B2259F23EDDCFC00A2D11F /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; }; - 51B225A123EDDCFC00A2D11F /* SimpleTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleTaskView.swift; sourceTree = "<group>"; }; - 51B225A223EDDCFC00A2D11F /* InstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskView.swift; sourceTree = "<group>"; }; - 51E77382237D1D8000ED70A2 /* TestGridTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGridTaskView.swift; sourceTree = "<group>"; }; - 51EFC75423FCAE6000536266 /* UIColor+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; }; - 51F12D38229C81E200CA265B /* OCKLabeledCheckmarkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLabeledCheckmarkButton.swift; sourceTree = "<group>"; }; - 51F75B932447DD0B00978C71 /* NumericProgressTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericProgressTaskView.swift; sourceTree = "<group>"; }; - 51F78FED22D7C7C100058858 /* OCKCalendarDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCalendarDisplayable.swift; sourceTree = "<group>"; }; - 51F9F11A23A9B8F00087C900 /* CareKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CareKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 51FEF4C324325902003CE34C /* OCKFeaturedContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKFeaturedContentView.swift; sourceTree = "<group>"; }; - 64699E9822FC7B5000FF624F /* OCKLogButtonCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLogButtonCell.swift; sourceTree = "<group>"; }; - 64E1BD4B2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKResponsiveLayoutTests.swift; sourceTree = "<group>"; }; - 64E1BD4D2309FCF200DFFE52 /* OCKResponsiveLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKResponsiveLayout.swift; sourceTree = "<group>"; }; - 64EEC2372318253B00B1012F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; - 64EEC23A231825DD00B1012F /* OCKLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLocalization.swift; sourceTree = "<group>"; }; + 03089D7E231ED7AF0054EA23 /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; }; + 03181063236A158E006D4870 /* OCKCappedSizeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCappedSizeLabel.swift; sourceTree = "<group>"; }; + 03AD384723625A4100F6E7DC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; + 05A6B74C237F49F7009D7D1F /* CareKitUI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CareKitUI.xctestplan; sourceTree = "<group>"; }; + 5103C55122F37B44007A7403 /* Number+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Extensions.swift"; sourceTree = "<group>"; }; + 510466F824A2850700D0FD53 /* OCKContactDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKContactDisplayable.swift; sourceTree = "<group>"; }; + 5105254524462AF5004483D0 /* TestLinkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLinkType.swift; sourceTree = "<group>"; }; + 51052548244639B3004483D0 /* TestLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLinkView.swift; sourceTree = "<group>"; }; + 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAccessibleValue.swift; sourceTree = "<group>"; }; + 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAnimatedButton.swift; sourceTree = "<group>"; }; + 510CC633243B843C008BD6B3 /* OCKLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLog.swift; sourceTree = "<group>"; }; + 510D862C23020D410073776E /* OCKStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKStyler.swift; sourceTree = "<group>"; }; + 51173F3B243FE007004CB150 /* CircularCompletionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularCompletionView.swift; sourceTree = "<group>"; }; + 51173F3D243FE022004CB150 /* RectangularCompletionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangularCompletionView.swift; sourceTree = "<group>"; }; + 51178E1423AAAB2F0068BAB1 /* OCKCompletionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCompletionState.swift; sourceTree = "<group>"; }; + 511A854723E0CAA2002A2AFB /* TestColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestColorExtension.swift; sourceTree = "<group>"; }; + 511BCB5B24365E2E00B4D643 /* OCKDetailedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailedImageView.swift; sourceTree = "<group>"; }; + 512B012E22C2F82900ABCB1D /* CareKitUITests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CareKitUITests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 512B013222C2F82900ABCB1D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 512EE8FF22975F850052F37C /* OCKDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKDetailView.swift; sourceTree = "<group>"; }; + 51367946243BF6EA0026997B /* LinkItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkItem.swift; sourceTree = "<group>"; }; + 51367947243BF6EB0026997B /* SafariView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; }; + 51367948243BF6EC0026997B /* LinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = "<group>"; }; + 5136794F243BF7040026997B /* LinkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = "<group>"; }; + 51367951243BF7450026997B /* LinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkLabel.swift; sourceTree = "<group>"; }; + 513FE9FC24803AE10016FCE6 /* LabeledValueTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledValueTaskView.swift; sourceTree = "<group>"; }; + 515C3C7F22F8AB9A007AC906 /* OCKSimpleContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKSimpleContactView.swift; sourceTree = "<group>"; }; + 51637EA222F3A68400BAF65C /* OCKLogTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLogTaskView.swift; sourceTree = "<group>"; }; + 51656028234AB47500F2A21F /* OCKCheckmarkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKCheckmarkButton.swift; sourceTree = "<group>"; }; + 516A02B922F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extensions.swift"; sourceTree = "<group>"; }; + 516A02BB22F363D900D55AD7 /* OCKView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKView.swift; sourceTree = "<group>"; }; + 516A1C47244F766A00BBF2D3 /* OSValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSValue.swift; sourceTree = "<group>"; }; + 516A1C4E244FA7E500BBF2D3 /* View+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; }; + 517309A424AA6D7E00A35C85 /* OCKStyler+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKStyler+Extension.swift"; sourceTree = "<group>"; }; + 5174225B224185290054E97C /* CareKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CareKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5174225F224185290054E97C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 51746F2A2448B90B00B647E1 /* TestNumericProgressTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNumericProgressTaskView.swift; sourceTree = "<group>"; }; + 5178FBD922E253FC00794353 /* OCKChartDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKChartDisplayable.swift; sourceTree = "<group>"; }; + 518F9D4522961BF5009CAA48 /* OCKAddressButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKAddressButton.swift; sourceTree = "<group>"; }; + 518F9D4622961BF5009CAA48 /* OCKDetailedContactView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDetailedContactView.swift; sourceTree = "<group>"; }; + 518F9D4722961BF5009CAA48 /* OCKContactButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKContactButton.swift; sourceTree = "<group>"; }; + 518F9D4B22961BF5009CAA48 /* OCKChecklistTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKChecklistTaskView.swift; sourceTree = "<group>"; }; + 518F9D4D22961BF5009CAA48 /* OCKSimpleTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSimpleTaskView.swift; sourceTree = "<group>"; }; + 518F9D4E22961BF5009CAA48 /* OCKChecklistItemButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKChecklistItemButton.swift; sourceTree = "<group>"; }; + 518F9D4F22961BF5009CAA48 /* OCKInstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKInstructionsTaskView.swift; sourceTree = "<group>"; }; + 518F9D5022961BF5009CAA48 /* OCKLabeledButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLabeledButton.swift; sourceTree = "<group>"; }; + 518F9D5122961BF5009CAA48 /* OCKSelfSizingCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSelfSizingCollectionView.swift; sourceTree = "<group>"; }; + 518F9D5222961BF5009CAA48 /* OCKButtonLogTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKButtonLogTaskView.swift; sourceTree = "<group>"; }; + 518F9D5322961BF5009CAA48 /* OCKGridTaskCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGridTaskCell.swift; sourceTree = "<group>"; }; + 518F9D5422961BF5009CAA48 /* OCKGridTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGridTaskView.swift; sourceTree = "<group>"; }; + 518F9D5522961BF5009CAA48 /* OCKLogItemButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLogItemButton.swift; sourceTree = "<group>"; }; + 518F9D5722961BF5009CAA48 /* OCKGradientPlotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGradientPlotView.swift; sourceTree = "<group>"; }; + 518F9D5822961BF5009CAA48 /* OCKGraphLegendView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGraphLegendView.swift; sourceTree = "<group>"; }; + 518F9D5922961BF5009CAA48 /* OCKScatterPlotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKScatterPlotView.swift; sourceTree = "<group>"; }; + 518F9D5B22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCartesianCoordinatesLayer.swift; sourceTree = "<group>"; }; + 518F9D5C22961BF5009CAA48 /* OCKScatterLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKScatterLayer.swift; sourceTree = "<group>"; }; + 518F9D5D22961BF5009CAA48 /* OCKGridLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGridLayer.swift; sourceTree = "<group>"; }; + 518F9D5E22961BF5009CAA48 /* OCKLineLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLineLayer.swift; sourceTree = "<group>"; }; + 518F9D5F22961BF5009CAA48 /* OCKBarLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKBarLayer.swift; sourceTree = "<group>"; }; + 518F9D6022961BF5009CAA48 /* OCKGraphAxisView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGraphAxisView.swift; sourceTree = "<group>"; }; + 518F9D6122961BF5009CAA48 /* OCKDataSeries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDataSeries.swift; sourceTree = "<group>"; }; + 518F9D6222961BF5009CAA48 /* OCKCartesianChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCartesianChartView.swift; sourceTree = "<group>"; }; + 518F9D6322961BF5009CAA48 /* OCKBarPlotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKBarPlotView.swift; sourceTree = "<group>"; }; + 518F9D6422961BF5009CAA48 /* OCKGridView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGridView.swift; sourceTree = "<group>"; }; + 518F9D6522961BF5009CAA48 /* OCKLinePlotView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLinePlotView.swift; sourceTree = "<group>"; }; + 518F9D6622961BF5009CAA48 /* OCKCartesianGraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCartesianGraphView.swift; sourceTree = "<group>"; }; + 518F9D6722961BF5009CAA48 /* OCKGraphable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKGraphable.swift; sourceTree = "<group>"; }; + 518F9D6922961BF5009CAA48 /* OCKCompletionRingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCompletionRingView.swift; sourceTree = "<group>"; }; + 518F9D6B22961BF5009CAA48 /* OCKRingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKRingView.swift; sourceTree = "<group>"; }; + 518F9D6F22961BF5009CAA48 /* UIFont+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Extensions.swift"; sourceTree = "<group>"; }; + 518F9D7422961BF5009CAA48 /* OCKColorStyler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKColorStyler.swift; sourceTree = "<group>"; }; + 518F9D7622961BF5009CAA48 /* OCKAnimationStyler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKAnimationStyler.swift; sourceTree = "<group>"; }; + 518F9D7722961BF5009CAA48 /* OCKAppearanceStyler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKAppearanceStyler.swift; sourceTree = "<group>"; }; + 518F9D7822961BF5009CAA48 /* OCKDimensionStyler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKDimensionStyler.swift; sourceTree = "<group>"; }; + 518F9D7B22961BF5009CAA48 /* OCKHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKHeaderView.swift; sourceTree = "<group>"; }; + 518F9D7C22961BF5009CAA48 /* OCKWeekCalendarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKWeekCalendarView.swift; sourceTree = "<group>"; }; + 518F9D7E22961BF5009CAA48 /* OCKSeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSeparatorView.swift; sourceTree = "<group>"; }; + 518F9D8122961BF5009CAA48 /* OCKCompletionRingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCompletionRingButton.swift; sourceTree = "<group>"; }; + 518F9D8222961BF5009CAA48 /* OCKLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLabel.swift; sourceTree = "<group>"; }; + 518F9D8322961BF5009CAA48 /* OCKCardable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCardable.swift; sourceTree = "<group>"; }; + 518F9D8722961BF5009CAA48 /* OCKStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKStackView.swift; sourceTree = "<group>"; }; + 519288722427E2DC00D0AF43 /* CATransaction+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATransaction+Extension.swift"; sourceTree = "<group>"; }; + 5196B54222D5872C00800706 /* OCKTaskDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKTaskDisplayable.swift; sourceTree = "<group>"; }; + 51AB06BF22FE539400B73FC2 /* OCKStylable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKStylable.swift; sourceTree = "<group>"; }; + 51AB06C122FE53E300B73FC2 /* TestStylableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStylableView.swift; sourceTree = "<group>"; }; + 51B2259A23EDDCFC00A2D11F /* NoHighlightStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoHighlightStyle.swift; sourceTree = "<group>"; }; + 51B2259E23EDDCFC00A2D11F /* CardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; }; + 51B2259F23EDDCFC00A2D11F /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; }; + 51B225A123EDDCFC00A2D11F /* SimpleTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleTaskView.swift; sourceTree = "<group>"; }; + 51B225A223EDDCFC00A2D11F /* InstructionsTaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstructionsTaskView.swift; sourceTree = "<group>"; }; + 51E77382237D1D8000ED70A2 /* TestGridTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGridTaskView.swift; sourceTree = "<group>"; }; + 51EFC75423FCAE6000536266 /* UIColor+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; }; + 51F12D38229C81E200CA265B /* OCKLabeledCheckmarkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLabeledCheckmarkButton.swift; sourceTree = "<group>"; }; + 51F75B932447DD0B00978C71 /* NumericProgressTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericProgressTaskView.swift; sourceTree = "<group>"; }; + 51F78FED22D7C7C100058858 /* OCKCalendarDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKCalendarDisplayable.swift; sourceTree = "<group>"; }; + 51F9F11A23A9B8F00087C900 /* CareKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CareKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 51FEF4C324325902003CE34C /* OCKFeaturedContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKFeaturedContentView.swift; sourceTree = "<group>"; }; + 64699E9822FC7B5000FF624F /* OCKLogButtonCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLogButtonCell.swift; sourceTree = "<group>"; }; + 64E1BD4B2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKResponsiveLayoutTests.swift; sourceTree = "<group>"; }; + 64E1BD4D2309FCF200DFFE52 /* OCKResponsiveLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKResponsiveLayout.swift; sourceTree = "<group>"; }; + 64EEC2372318253B00B1012F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; + 64EEC23A231825DD00B1012F /* OCKLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKLocalization.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 512B012B22C2F82900ABCB1D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 512B013322C2F82900ABCB1D /* CareKitUI.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51742258224185290054E97C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51F9F11723A9B8F00087C900 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 512B012B22C2F82900ABCB1D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 512B013322C2F82900ABCB1D /* CareKitUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51742258224185290054E97C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51F9F11723A9B8F00087C900 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 510466E924A26B0A00D0FD53 /* iOS */ = { - isa = PBXGroup; - children = ( - 518F9D8322961BF5009CAA48 /* OCKCardable.swift */, - 64E1BD4D2309FCF200DFFE52 /* OCKResponsiveLayout.swift */, - 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */, - 510466F724A2848800D0FD53 /* Views */, - 51F12D3E229CA8DE00CA265B /* Controls */, - 51F12D41229CA92400CA265B /* Labels */, - 51E7643A22C2ED7300160C22 /* Authentication */, - 518F9D4422961BF5009CAA48 /* Contact */, - 518F9D5622961BF5009CAA48 /* Charts */, - 51B225E223EE263F00A2D11F /* Calendar Events */, - 518F9D4A22961BF5009CAA48 /* Task */, - 51F12D4A229CAB7C00CA265B /* Detail View */, - 640B6A1923D90979009490D6 /* Picker */, - 51F12D49229CAB3600CA265B /* Calendar */, - 515888C62437AED100E65C73 /* Featured Content */, - 51367939243BF69D0026997B /* Link */, - 510466ED24A26B9900D0FD53 /* Style */, - 510466F024A278FC00D0FD53 /* Extensions */, - ); - path = iOS; - sourceTree = "<group>"; - }; - 510466EB24A26B1600D0FD53 /* Shared */ = { - isa = PBXGroup; - children = ( - 510CC633243B843C008BD6B3 /* OCKLog.swift */, - 51B2259A23EDDCFC00A2D11F /* NoHighlightStyle.swift */, - 516A1C47244F766A00BBF2D3 /* OSValue.swift */, - 510466F624A2842A00D0FD53 /* Components */, - 510466EE24A26C1800D0FD53 /* Task */, - 518F9D7022961BF5009CAA48 /* Style */, - 510466EF24A278EF00D0FD53 /* Extensions */, - 64EEC2352318252B00B1012F /* Localization */, - ); - path = Shared; - sourceTree = "<group>"; - }; - 510466ED24A26B9900D0FD53 /* Style */ = { - isa = PBXGroup; - children = ( - 51AB06BF22FE539400B73FC2 /* OCKStylable.swift */, - ); - path = Style; - sourceTree = "<group>"; - }; - 510466EE24A26C1800D0FD53 /* Task */ = { - isa = PBXGroup; - children = ( - 51B225A123EDDCFC00A2D11F /* SimpleTaskView.swift */, - 51B225A223EDDCFC00A2D11F /* InstructionsTaskView.swift */, - ); - path = Task; - sourceTree = "<group>"; - }; - 510466EF24A278EF00D0FD53 /* Extensions */ = { - isa = PBXGroup; - children = ( - 516A1C4E244FA7E500BBF2D3 /* View+Extension.swift */, - 51EFC75423FCAE6000536266 /* UIColor+Extension.swift */, - 5103C55122F37B44007A7403 /* Number+Extensions.swift */, - ); - path = Extensions; - sourceTree = "<group>"; - }; - 510466F024A278FC00D0FD53 /* Extensions */ = { - isa = PBXGroup; - children = ( - 519288722427E2DC00D0AF43 /* CATransaction+Extension.swift */, - 516A02B922F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift */, - 518F9D6F22961BF5009CAA48 /* UIFont+Extensions.swift */, - 03089D7E231ED7AF0054EA23 /* Calendar+Extensions.swift */, - ); - path = Extensions; - sourceTree = "<group>"; - }; - 510466F224A282D400D0FD53 /* Link */ = { - isa = PBXGroup; - children = ( - 5105254524462AF5004483D0 /* TestLinkType.swift */, - 51052548244639B3004483D0 /* TestLinkView.swift */, - ); - path = Link; - sourceTree = "<group>"; - }; - 510466F324A2830500D0FD53 /* Task */ = { - isa = PBXGroup; - children = ( - 51E77382237D1D8000ED70A2 /* TestGridTaskView.swift */, - 51746F2A2448B90B00B647E1 /* TestNumericProgressTaskView.swift */, - ); - path = Task; - sourceTree = "<group>"; - }; - 510466F424A2831200D0FD53 /* Calendar */ = { - isa = PBXGroup; - children = ( - ); - path = Calendar; - sourceTree = "<group>"; - }; - 510466F624A2842A00D0FD53 /* Components */ = { - isa = PBXGroup; - children = ( - 51B2259E23EDDCFC00A2D11F /* CardView.swift */, - 51B2259F23EDDCFC00A2D11F /* HeaderView.swift */, - 51173F3D243FE022004CB150 /* RectangularCompletionView.swift */, - 51173F3B243FE007004CB150 /* CircularCompletionView.swift */, - ); - path = Components; - sourceTree = "<group>"; - }; - 510466F724A2848800D0FD53 /* Views */ = { - isa = PBXGroup; - children = ( - 516A02BB22F363D900D55AD7 /* OCKView.swift */, - 518F9D8722961BF5009CAA48 /* OCKStackView.swift */, - 518F9D7E22961BF5009CAA48 /* OCKSeparatorView.swift */, - 518F9D7B22961BF5009CAA48 /* OCKHeaderView.swift */, - ); - path = Views; - sourceTree = "<group>"; - }; - 512B012F22C2F82900ABCB1D /* CareKitUITests */ = { - isa = PBXGroup; - children = ( - 05A6B74C237F49F7009D7D1F /* CareKitUI.xctestplan */, - 64E1BD4B2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift */, - 51AB06C122FE53E300B73FC2 /* TestStylableView.swift */, - 511A854723E0CAA2002A2AFB /* TestColorExtension.swift */, - 510466F224A282D400D0FD53 /* Link */, - 510466F324A2830500D0FD53 /* Task */, - 510466F424A2831200D0FD53 /* Calendar */, - 512B013222C2F82900ABCB1D /* Info.plist */, - ); - path = CareKitUITests; - sourceTree = "<group>"; - }; - 51367939243BF69D0026997B /* Link */ = { - isa = PBXGroup; - children = ( - 51367946243BF6EA0026997B /* LinkItem.swift */, - 5136794F243BF7040026997B /* LinkView.swift */, - 51367948243BF6EC0026997B /* LinkButton.swift */, - 51367951243BF7450026997B /* LinkLabel.swift */, - 51367947243BF6EB0026997B /* SafariView.swift */, - ); - path = Link; - sourceTree = "<group>"; - }; - 5143E39D22F2740C00FE6773 /* Slider */ = { - isa = PBXGroup; - children = ( - ); - path = Slider; - sourceTree = "<group>"; - }; - 515888C62437AED100E65C73 /* Featured Content */ = { - isa = PBXGroup; - children = ( - 51FEF4C324325902003CE34C /* OCKFeaturedContentView.swift */, - ); - path = "Featured Content"; - sourceTree = "<group>"; - }; - 51742251224185290054E97C = { - isa = PBXGroup; - children = ( - 5174225D224185290054E97C /* CareKitUI */, - 512B012F22C2F82900ABCB1D /* CareKitUITests */, - 5174225C224185290054E97C /* Products */, - ); - sourceTree = "<group>"; - }; - 5174225C224185290054E97C /* Products */ = { - isa = PBXGroup; - children = ( - 5174225B224185290054E97C /* CareKitUI.framework */, - 512B012E22C2F82900ABCB1D /* CareKitUITests iOS.xctest */, - 51F9F11A23A9B8F00087C900 /* CareKitUI.framework */, - ); - name = Products; - sourceTree = "<group>"; - }; - 5174225D224185290054E97C /* CareKitUI */ = { - isa = PBXGroup; - children = ( - 510466EB24A26B1600D0FD53 /* Shared */, - 510466E924A26B0A00D0FD53 /* iOS */, - 5174225F224185290054E97C /* Info.plist */, - ); - path = CareKitUI; - sourceTree = "<group>"; - }; - 518F9D4422961BF5009CAA48 /* Contact */ = { - isa = PBXGroup; - children = ( - 510466F824A2850700D0FD53 /* OCKContactDisplayable.swift */, - 515C3C7F22F8AB9A007AC906 /* OCKSimpleContactView.swift */, - 518F9D4622961BF5009CAA48 /* OCKDetailedContactView.swift */, - 518F9D4522961BF5009CAA48 /* OCKAddressButton.swift */, - 518F9D4722961BF5009CAA48 /* OCKContactButton.swift */, - ); - path = Contact; - sourceTree = "<group>"; - }; - 518F9D4A22961BF5009CAA48 /* Task */ = { - isa = PBXGroup; - children = ( - 5196B54222D5872C00800706 /* OCKTaskDisplayable.swift */, - 518F9D4B22961BF5009CAA48 /* OCKChecklistTaskView.swift */, - 518F9D5422961BF5009CAA48 /* OCKGridTaskView.swift */, - 518F9D4D22961BF5009CAA48 /* OCKSimpleTaskView.swift */, - 518F9D4F22961BF5009CAA48 /* OCKInstructionsTaskView.swift */, - 51637EA222F3A68400BAF65C /* OCKLogTaskView.swift */, - 518F9D5222961BF5009CAA48 /* OCKButtonLogTaskView.swift */, - 51F75B932447DD0B00978C71 /* NumericProgressTaskView.swift */, - 513FE9FC24803AE10016FCE6 /* LabeledValueTaskView.swift */, - 51F12D43229CA99400CA265B /* Buttons */, - 51F12D44229CA9CF00CA265B /* Collection */, - ); - path = Task; - sourceTree = "<group>"; - }; - 518F9D5622961BF5009CAA48 /* Charts */ = { - isa = PBXGroup; - children = ( - 51F12D46229CAA7100CA265B /* Gradient Plots */, - 51F12D45229CAA3E00CA265B /* Protocols */, - 518F9D5A22961BF5009CAA48 /* Layers */, - 518F9D6222961BF5009CAA48 /* OCKCartesianChartView.swift */, - 518F9D5822961BF5009CAA48 /* OCKGraphLegendView.swift */, - 518F9D6022961BF5009CAA48 /* OCKGraphAxisView.swift */, - 518F9D6122961BF5009CAA48 /* OCKDataSeries.swift */, - 518F9D6422961BF5009CAA48 /* OCKGridView.swift */, - 518F9D6622961BF5009CAA48 /* OCKCartesianGraphView.swift */, - ); - path = Charts; - sourceTree = "<group>"; - }; - 518F9D5A22961BF5009CAA48 /* Layers */ = { - isa = PBXGroup; - children = ( - 518F9D5B22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift */, - 518F9D5D22961BF5009CAA48 /* OCKGridLayer.swift */, - 518F9D5C22961BF5009CAA48 /* OCKScatterLayer.swift */, - 518F9D5E22961BF5009CAA48 /* OCKLineLayer.swift */, - 518F9D5F22961BF5009CAA48 /* OCKBarLayer.swift */, - ); - path = Layers; - sourceTree = "<group>"; - }; - 518F9D6822961BF5009CAA48 /* Ring */ = { - isa = PBXGroup; - children = ( - 518F9D6922961BF5009CAA48 /* OCKCompletionRingView.swift */, - 518F9D6B22961BF5009CAA48 /* OCKRingView.swift */, - 518F9D8122961BF5009CAA48 /* OCKCompletionRingButton.swift */, - 51178E1423AAAB2F0068BAB1 /* OCKCompletionState.swift */, - ); - path = Ring; - sourceTree = "<group>"; - }; - 518F9D7022961BF5009CAA48 /* Style */ = { - isa = PBXGroup; - children = ( - 510D862C23020D410073776E /* OCKStyler.swift */, - 518F9D7422961BF5009CAA48 /* OCKColorStyler.swift */, - 518F9D7622961BF5009CAA48 /* OCKAnimationStyler.swift */, - 518F9D7722961BF5009CAA48 /* OCKAppearanceStyler.swift */, - 518F9D7822961BF5009CAA48 /* OCKDimensionStyler.swift */, - ); - path = Style; - sourceTree = "<group>"; - }; - 51B225E223EE263F00A2D11F /* Calendar Events */ = { - isa = PBXGroup; - children = ( - ); - path = "Calendar Events"; - sourceTree = "<group>"; - }; - 51E7643A22C2ED7300160C22 /* Authentication */ = { - isa = PBXGroup; - children = ( - ); - path = Authentication; - sourceTree = "<group>"; - }; - 51F12D3E229CA8DE00CA265B /* Controls */ = { - isa = PBXGroup; - children = ( - 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */, - 51656028234AB47500F2A21F /* OCKCheckmarkButton.swift */, - 518F9D5022961BF5009CAA48 /* OCKLabeledButton.swift */, - 5143E39D22F2740C00FE6773 /* Slider */, - ); - path = Controls; - sourceTree = "<group>"; - }; - 51F12D41229CA92400CA265B /* Labels */ = { - isa = PBXGroup; - children = ( - 518F9D8222961BF5009CAA48 /* OCKLabel.swift */, - 03181063236A158E006D4870 /* OCKCappedSizeLabel.swift */, - ); - path = Labels; - sourceTree = "<group>"; - }; - 51F12D43229CA99400CA265B /* Buttons */ = { - isa = PBXGroup; - children = ( - 51F12D38229C81E200CA265B /* OCKLabeledCheckmarkButton.swift */, - 518F9D5522961BF5009CAA48 /* OCKLogItemButton.swift */, - 518F9D4E22961BF5009CAA48 /* OCKChecklistItemButton.swift */, - ); - path = Buttons; - sourceTree = "<group>"; - }; - 51F12D44229CA9CF00CA265B /* Collection */ = { - isa = PBXGroup; - children = ( - 64699E9822FC7B5000FF624F /* OCKLogButtonCell.swift */, - 518F9D5122961BF5009CAA48 /* OCKSelfSizingCollectionView.swift */, - 518F9D5322961BF5009CAA48 /* OCKGridTaskCell.swift */, - ); - path = Collection; - sourceTree = "<group>"; - }; - 51F12D45229CAA3E00CA265B /* Protocols */ = { - isa = PBXGroup; - children = ( - 518F9D6722961BF5009CAA48 /* OCKGraphable.swift */, - 5178FBD922E253FC00794353 /* OCKChartDisplayable.swift */, - ); - path = Protocols; - sourceTree = "<group>"; - }; - 51F12D46229CAA7100CA265B /* Gradient Plots */ = { - isa = PBXGroup; - children = ( - 518F9D5722961BF5009CAA48 /* OCKGradientPlotView.swift */, - 518F9D6522961BF5009CAA48 /* OCKLinePlotView.swift */, - 518F9D5922961BF5009CAA48 /* OCKScatterPlotView.swift */, - 518F9D6322961BF5009CAA48 /* OCKBarPlotView.swift */, - ); - path = "Gradient Plots"; - sourceTree = "<group>"; - }; - 51F12D49229CAB3600CA265B /* Calendar */ = { - isa = PBXGroup; - children = ( - 51F78FED22D7C7C100058858 /* OCKCalendarDisplayable.swift */, - 518F9D7C22961BF5009CAA48 /* OCKWeekCalendarView.swift */, - 518F9D6822961BF5009CAA48 /* Ring */, - ); - path = Calendar; - sourceTree = "<group>"; - }; - 51F12D4A229CAB7C00CA265B /* Detail View */ = { - isa = PBXGroup; - children = ( - 511BCB5B24365E2E00B4D643 /* OCKDetailedImageView.swift */, - 512EE8FF22975F850052F37C /* OCKDetailView.swift */, - ); - path = "Detail View"; - sourceTree = "<group>"; - }; - 640B6A1923D90979009490D6 /* Picker */ = { - isa = PBXGroup; - children = ( - ); - path = Picker; - sourceTree = "<group>"; - }; - 64EEC2352318252B00B1012F /* Localization */ = { - isa = PBXGroup; - children = ( - 64EEC2362318253B00B1012F /* Localizable.strings */, - 03AD384423625A3500F6E7DC /* Localizable.stringsdict */, - 64EEC23A231825DD00B1012F /* OCKLocalization.swift */, - ); - path = Localization; - sourceTree = "<group>"; - }; + 510466E924A26B0A00D0FD53 /* iOS */ = { + isa = PBXGroup; + children = ( + 518F9D8322961BF5009CAA48 /* OCKCardable.swift */, + 64E1BD4D2309FCF200DFFE52 /* OCKResponsiveLayout.swift */, + 5105911E237651C8004EDC84 /* OCKAccessibleValue.swift */, + 510466F724A2848800D0FD53 /* Views */, + 51F12D3E229CA8DE00CA265B /* Controls */, + 51F12D41229CA92400CA265B /* Labels */, + 51E7643A22C2ED7300160C22 /* Authentication */, + 518F9D4422961BF5009CAA48 /* Contact */, + 518F9D5622961BF5009CAA48 /* Charts */, + 51B225E223EE263F00A2D11F /* Calendar Events */, + 518F9D4A22961BF5009CAA48 /* Task */, + 51F12D4A229CAB7C00CA265B /* Detail View */, + 640B6A1923D90979009490D6 /* Picker */, + 51F12D49229CAB3600CA265B /* Calendar */, + 515888C62437AED100E65C73 /* Featured Content */, + 51367939243BF69D0026997B /* Link */, + 510466ED24A26B9900D0FD53 /* Style */, + 510466F024A278FC00D0FD53 /* Extensions */, + ); + path = iOS; + sourceTree = "<group>"; + }; + 510466EB24A26B1600D0FD53 /* Shared */ = { + isa = PBXGroup; + children = ( + 510CC633243B843C008BD6B3 /* OCKLog.swift */, + 51B2259A23EDDCFC00A2D11F /* NoHighlightStyle.swift */, + 516A1C47244F766A00BBF2D3 /* OSValue.swift */, + 510466F624A2842A00D0FD53 /* Components */, + 510466EE24A26C1800D0FD53 /* Task */, + 518F9D7022961BF5009CAA48 /* Style */, + 510466EF24A278EF00D0FD53 /* Extensions */, + 64EEC2352318252B00B1012F /* Localization */, + ); + path = Shared; + sourceTree = "<group>"; + }; + 510466ED24A26B9900D0FD53 /* Style */ = { + isa = PBXGroup; + children = ( + 51AB06BF22FE539400B73FC2 /* OCKStylable.swift */, + ); + path = Style; + sourceTree = "<group>"; + }; + 510466EE24A26C1800D0FD53 /* Task */ = { + isa = PBXGroup; + children = ( + 51B225A123EDDCFC00A2D11F /* SimpleTaskView.swift */, + 51B225A223EDDCFC00A2D11F /* InstructionsTaskView.swift */, + ); + path = Task; + sourceTree = "<group>"; + }; + 510466EF24A278EF00D0FD53 /* Extensions */ = { + isa = PBXGroup; + children = ( + 516A1C4E244FA7E500BBF2D3 /* View+Extension.swift */, + 51EFC75423FCAE6000536266 /* UIColor+Extension.swift */, + 5103C55122F37B44007A7403 /* Number+Extensions.swift */, + 517309A424AA6D7E00A35C85 /* OCKStyler+Extension.swift */, + ); + path = Extensions; + sourceTree = "<group>"; + }; + 510466F024A278FC00D0FD53 /* Extensions */ = { + isa = PBXGroup; + children = ( + 519288722427E2DC00D0AF43 /* CATransaction+Extension.swift */, + 516A02B922F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift */, + 518F9D6F22961BF5009CAA48 /* UIFont+Extensions.swift */, + 03089D7E231ED7AF0054EA23 /* Calendar+Extensions.swift */, + ); + path = Extensions; + sourceTree = "<group>"; + }; + 510466F224A282D400D0FD53 /* Link */ = { + isa = PBXGroup; + children = ( + 5105254524462AF5004483D0 /* TestLinkType.swift */, + 51052548244639B3004483D0 /* TestLinkView.swift */, + ); + path = Link; + sourceTree = "<group>"; + }; + 510466F324A2830500D0FD53 /* Task */ = { + isa = PBXGroup; + children = ( + 51E77382237D1D8000ED70A2 /* TestGridTaskView.swift */, + 51746F2A2448B90B00B647E1 /* TestNumericProgressTaskView.swift */, + ); + path = Task; + sourceTree = "<group>"; + }; + 510466F424A2831200D0FD53 /* Calendar */ = { + isa = PBXGroup; + children = ( + ); + path = Calendar; + sourceTree = "<group>"; + }; + 510466F624A2842A00D0FD53 /* Components */ = { + isa = PBXGroup; + children = ( + 51B2259E23EDDCFC00A2D11F /* CardView.swift */, + 51B2259F23EDDCFC00A2D11F /* HeaderView.swift */, + 51173F3D243FE022004CB150 /* RectangularCompletionView.swift */, + 51173F3B243FE007004CB150 /* CircularCompletionView.swift */, + ); + path = Components; + sourceTree = "<group>"; + }; + 510466F724A2848800D0FD53 /* Views */ = { + isa = PBXGroup; + children = ( + 516A02BB22F363D900D55AD7 /* OCKView.swift */, + 518F9D8722961BF5009CAA48 /* OCKStackView.swift */, + 518F9D7E22961BF5009CAA48 /* OCKSeparatorView.swift */, + 518F9D7B22961BF5009CAA48 /* OCKHeaderView.swift */, + ); + path = Views; + sourceTree = "<group>"; + }; + 512B012F22C2F82900ABCB1D /* CareKitUITests */ = { + isa = PBXGroup; + children = ( + 05A6B74C237F49F7009D7D1F /* CareKitUI.xctestplan */, + 64E1BD4B2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift */, + 51AB06C122FE53E300B73FC2 /* TestStylableView.swift */, + 511A854723E0CAA2002A2AFB /* TestColorExtension.swift */, + 510466F224A282D400D0FD53 /* Link */, + 510466F324A2830500D0FD53 /* Task */, + 510466F424A2831200D0FD53 /* Calendar */, + 512B013222C2F82900ABCB1D /* Info.plist */, + ); + path = CareKitUITests; + sourceTree = "<group>"; + }; + 51367939243BF69D0026997B /* Link */ = { + isa = PBXGroup; + children = ( + 51367946243BF6EA0026997B /* LinkItem.swift */, + 5136794F243BF7040026997B /* LinkView.swift */, + 51367948243BF6EC0026997B /* LinkButton.swift */, + 51367951243BF7450026997B /* LinkLabel.swift */, + 51367947243BF6EB0026997B /* SafariView.swift */, + ); + path = Link; + sourceTree = "<group>"; + }; + 5143E39D22F2740C00FE6773 /* Slider */ = { + isa = PBXGroup; + children = ( + ); + path = Slider; + sourceTree = "<group>"; + }; + 515888C62437AED100E65C73 /* Featured Content */ = { + isa = PBXGroup; + children = ( + 51FEF4C324325902003CE34C /* OCKFeaturedContentView.swift */, + ); + path = "Featured Content"; + sourceTree = "<group>"; + }; + 51742251224185290054E97C = { + isa = PBXGroup; + children = ( + 5174225D224185290054E97C /* CareKitUI */, + 512B012F22C2F82900ABCB1D /* CareKitUITests */, + 5174225C224185290054E97C /* Products */, + ); + sourceTree = "<group>"; + }; + 5174225C224185290054E97C /* Products */ = { + isa = PBXGroup; + children = ( + 5174225B224185290054E97C /* CareKitUI.framework */, + 512B012E22C2F82900ABCB1D /* CareKitUITests iOS.xctest */, + 51F9F11A23A9B8F00087C900 /* CareKitUI.framework */, + ); + name = Products; + sourceTree = "<group>"; + }; + 5174225D224185290054E97C /* CareKitUI */ = { + isa = PBXGroup; + children = ( + 510466EB24A26B1600D0FD53 /* Shared */, + 510466E924A26B0A00D0FD53 /* iOS */, + 5174225F224185290054E97C /* Info.plist */, + ); + path = CareKitUI; + sourceTree = "<group>"; + }; + 518F9D4422961BF5009CAA48 /* Contact */ = { + isa = PBXGroup; + children = ( + 510466F824A2850700D0FD53 /* OCKContactDisplayable.swift */, + 515C3C7F22F8AB9A007AC906 /* OCKSimpleContactView.swift */, + 518F9D4622961BF5009CAA48 /* OCKDetailedContactView.swift */, + 518F9D4522961BF5009CAA48 /* OCKAddressButton.swift */, + 518F9D4722961BF5009CAA48 /* OCKContactButton.swift */, + ); + path = Contact; + sourceTree = "<group>"; + }; + 518F9D4A22961BF5009CAA48 /* Task */ = { + isa = PBXGroup; + children = ( + 5196B54222D5872C00800706 /* OCKTaskDisplayable.swift */, + 518F9D4B22961BF5009CAA48 /* OCKChecklistTaskView.swift */, + 518F9D5422961BF5009CAA48 /* OCKGridTaskView.swift */, + 518F9D4D22961BF5009CAA48 /* OCKSimpleTaskView.swift */, + 518F9D4F22961BF5009CAA48 /* OCKInstructionsTaskView.swift */, + 51637EA222F3A68400BAF65C /* OCKLogTaskView.swift */, + 518F9D5222961BF5009CAA48 /* OCKButtonLogTaskView.swift */, + 51F75B932447DD0B00978C71 /* NumericProgressTaskView.swift */, + 513FE9FC24803AE10016FCE6 /* LabeledValueTaskView.swift */, + 51F12D43229CA99400CA265B /* Buttons */, + 51F12D44229CA9CF00CA265B /* Collection */, + ); + path = Task; + sourceTree = "<group>"; + }; + 518F9D5622961BF5009CAA48 /* Charts */ = { + isa = PBXGroup; + children = ( + 51F12D46229CAA7100CA265B /* Gradient Plots */, + 51F12D45229CAA3E00CA265B /* Protocols */, + 518F9D5A22961BF5009CAA48 /* Layers */, + 518F9D6222961BF5009CAA48 /* OCKCartesianChartView.swift */, + 518F9D5822961BF5009CAA48 /* OCKGraphLegendView.swift */, + 518F9D6022961BF5009CAA48 /* OCKGraphAxisView.swift */, + 518F9D6122961BF5009CAA48 /* OCKDataSeries.swift */, + 518F9D6422961BF5009CAA48 /* OCKGridView.swift */, + 518F9D6622961BF5009CAA48 /* OCKCartesianGraphView.swift */, + ); + path = Charts; + sourceTree = "<group>"; + }; + 518F9D5A22961BF5009CAA48 /* Layers */ = { + isa = PBXGroup; + children = ( + 518F9D5B22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift */, + 518F9D5D22961BF5009CAA48 /* OCKGridLayer.swift */, + 518F9D5C22961BF5009CAA48 /* OCKScatterLayer.swift */, + 518F9D5E22961BF5009CAA48 /* OCKLineLayer.swift */, + 518F9D5F22961BF5009CAA48 /* OCKBarLayer.swift */, + ); + path = Layers; + sourceTree = "<group>"; + }; + 518F9D6822961BF5009CAA48 /* Ring */ = { + isa = PBXGroup; + children = ( + 518F9D6922961BF5009CAA48 /* OCKCompletionRingView.swift */, + 518F9D6B22961BF5009CAA48 /* OCKRingView.swift */, + 518F9D8122961BF5009CAA48 /* OCKCompletionRingButton.swift */, + 51178E1423AAAB2F0068BAB1 /* OCKCompletionState.swift */, + ); + path = Ring; + sourceTree = "<group>"; + }; + 518F9D7022961BF5009CAA48 /* Style */ = { + isa = PBXGroup; + children = ( + 510D862C23020D410073776E /* OCKStyler.swift */, + 518F9D7422961BF5009CAA48 /* OCKColorStyler.swift */, + 518F9D7622961BF5009CAA48 /* OCKAnimationStyler.swift */, + 518F9D7722961BF5009CAA48 /* OCKAppearanceStyler.swift */, + 518F9D7822961BF5009CAA48 /* OCKDimensionStyler.swift */, + ); + path = Style; + sourceTree = "<group>"; + }; + 51B225E223EE263F00A2D11F /* Calendar Events */ = { + isa = PBXGroup; + children = ( + ); + path = "Calendar Events"; + sourceTree = "<group>"; + }; + 51E7643A22C2ED7300160C22 /* Authentication */ = { + isa = PBXGroup; + children = ( + ); + path = Authentication; + sourceTree = "<group>"; + }; + 51F12D3E229CA8DE00CA265B /* Controls */ = { + isa = PBXGroup; + children = ( + 510A5761234A7B920006C376 /* OCKAnimatedButton.swift */, + 51656028234AB47500F2A21F /* OCKCheckmarkButton.swift */, + 518F9D5022961BF5009CAA48 /* OCKLabeledButton.swift */, + 5143E39D22F2740C00FE6773 /* Slider */, + ); + path = Controls; + sourceTree = "<group>"; + }; + 51F12D41229CA92400CA265B /* Labels */ = { + isa = PBXGroup; + children = ( + 518F9D8222961BF5009CAA48 /* OCKLabel.swift */, + 03181063236A158E006D4870 /* OCKCappedSizeLabel.swift */, + ); + path = Labels; + sourceTree = "<group>"; + }; + 51F12D43229CA99400CA265B /* Buttons */ = { + isa = PBXGroup; + children = ( + 51F12D38229C81E200CA265B /* OCKLabeledCheckmarkButton.swift */, + 518F9D5522961BF5009CAA48 /* OCKLogItemButton.swift */, + 518F9D4E22961BF5009CAA48 /* OCKChecklistItemButton.swift */, + ); + path = Buttons; + sourceTree = "<group>"; + }; + 51F12D44229CA9CF00CA265B /* Collection */ = { + isa = PBXGroup; + children = ( + 64699E9822FC7B5000FF624F /* OCKLogButtonCell.swift */, + 518F9D5122961BF5009CAA48 /* OCKSelfSizingCollectionView.swift */, + 518F9D5322961BF5009CAA48 /* OCKGridTaskCell.swift */, + ); + path = Collection; + sourceTree = "<group>"; + }; + 51F12D45229CAA3E00CA265B /* Protocols */ = { + isa = PBXGroup; + children = ( + 518F9D6722961BF5009CAA48 /* OCKGraphable.swift */, + 5178FBD922E253FC00794353 /* OCKChartDisplayable.swift */, + ); + path = Protocols; + sourceTree = "<group>"; + }; + 51F12D46229CAA7100CA265B /* Gradient Plots */ = { + isa = PBXGroup; + children = ( + 518F9D5722961BF5009CAA48 /* OCKGradientPlotView.swift */, + 518F9D6522961BF5009CAA48 /* OCKLinePlotView.swift */, + 518F9D5922961BF5009CAA48 /* OCKScatterPlotView.swift */, + 518F9D6322961BF5009CAA48 /* OCKBarPlotView.swift */, + ); + path = "Gradient Plots"; + sourceTree = "<group>"; + }; + 51F12D49229CAB3600CA265B /* Calendar */ = { + isa = PBXGroup; + children = ( + 51F78FED22D7C7C100058858 /* OCKCalendarDisplayable.swift */, + 518F9D7C22961BF5009CAA48 /* OCKWeekCalendarView.swift */, + 518F9D6822961BF5009CAA48 /* Ring */, + ); + path = Calendar; + sourceTree = "<group>"; + }; + 51F12D4A229CAB7C00CA265B /* Detail View */ = { + isa = PBXGroup; + children = ( + 511BCB5B24365E2E00B4D643 /* OCKDetailedImageView.swift */, + 512EE8FF22975F850052F37C /* OCKDetailView.swift */, + ); + path = "Detail View"; + sourceTree = "<group>"; + }; + 640B6A1923D90979009490D6 /* Picker */ = { + isa = PBXGroup; + children = ( + ); + path = Picker; + sourceTree = "<group>"; + }; + 64EEC2352318252B00B1012F /* Localization */ = { + isa = PBXGroup; + children = ( + 64EEC2362318253B00B1012F /* Localizable.strings */, + 03AD384423625A3500F6E7DC /* Localizable.stringsdict */, + 64EEC23A231825DD00B1012F /* OCKLocalization.swift */, + ); + path = Localization; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 51742256224185290054E97C /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51F9F11523A9B8F00087C900 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 51742256224185290054E97C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51F9F11523A9B8F00087C900 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 512B012D22C2F82900ABCB1D /* CareKitUITests iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 512B013822C2F82900ABCB1D /* Build configuration list for PBXNativeTarget "CareKitUITests iOS" */; - buildPhases = ( - 512B012A22C2F82900ABCB1D /* Sources */, - 512B012B22C2F82900ABCB1D /* Frameworks */, - 512B012C22C2F82900ABCB1D /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 512B013522C2F82900ABCB1D /* PBXTargetDependency */, - ); - name = "CareKitUITests iOS"; - productName = CareKitUITests; - productReference = 512B012E22C2F82900ABCB1D /* CareKitUITests iOS.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 5174225A224185290054E97C /* CareKitUI iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 51742263224185290054E97C /* Build configuration list for PBXNativeTarget "CareKitUI iOS" */; - buildPhases = ( - 51742256224185290054E97C /* Headers */, - 51742257224185290054E97C /* Sources */, - 51742258224185290054E97C /* Frameworks */, - 51742259224185290054E97C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CareKitUI iOS"; - productName = CareKitViews; - productReference = 5174225B224185290054E97C /* CareKitUI.framework */; - productType = "com.apple.product-type.framework"; - }; - 51F9F11923A9B8F00087C900 /* CareKitUI Watch */ = { - isa = PBXNativeTarget; - buildConfigurationList = 51F9F12123A9B8F00087C900 /* Build configuration list for PBXNativeTarget "CareKitUI Watch" */; - buildPhases = ( - 51F9F11523A9B8F00087C900 /* Headers */, - 51F9F11623A9B8F00087C900 /* Sources */, - 51F9F11723A9B8F00087C900 /* Frameworks */, - 51F9F11823A9B8F00087C900 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CareKitUI Watch"; - productName = "CareKitUI Watch"; - productReference = 51F9F11A23A9B8F00087C900 /* CareKitUI.framework */; - productType = "com.apple.product-type.framework"; - }; + 512B012D22C2F82900ABCB1D /* CareKitUITests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 512B013822C2F82900ABCB1D /* Build configuration list for PBXNativeTarget "CareKitUITests iOS" */; + buildPhases = ( + 512B012A22C2F82900ABCB1D /* Sources */, + 512B012B22C2F82900ABCB1D /* Frameworks */, + 512B012C22C2F82900ABCB1D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 512B013522C2F82900ABCB1D /* PBXTargetDependency */, + ); + name = "CareKitUITests iOS"; + productName = CareKitUITests; + productReference = 512B012E22C2F82900ABCB1D /* CareKitUITests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 5174225A224185290054E97C /* CareKitUI iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 51742263224185290054E97C /* Build configuration list for PBXNativeTarget "CareKitUI iOS" */; + buildPhases = ( + 51742256224185290054E97C /* Headers */, + 51742257224185290054E97C /* Sources */, + 51742258224185290054E97C /* Frameworks */, + 51742259224185290054E97C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "CareKitUI iOS"; + productName = CareKitViews; + productReference = 5174225B224185290054E97C /* CareKitUI.framework */; + productType = "com.apple.product-type.framework"; + }; + 51F9F11923A9B8F00087C900 /* CareKitUI Watch */ = { + isa = PBXNativeTarget; + buildConfigurationList = 51F9F12123A9B8F00087C900 /* Build configuration list for PBXNativeTarget "CareKitUI Watch" */; + buildPhases = ( + 51F9F11523A9B8F00087C900 /* Headers */, + 51F9F11623A9B8F00087C900 /* Sources */, + 51F9F11723A9B8F00087C900 /* Frameworks */, + 51F9F11823A9B8F00087C900 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "CareKitUI Watch"; + productName = "CareKitUI Watch"; + productReference = 51F9F11A23A9B8F00087C900 /* CareKitUI.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 51742252224185290054E97C /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1100; - LastUpgradeCheck = 1200; - ORGANIZATIONNAME = Apple; - TargetAttributes = { - 512B012D22C2F82900ABCB1D = { - CreatedOnToolsVersion = 11.0; - }; - 5174225A224185290054E97C = { - CreatedOnToolsVersion = 10.1; - LastSwiftMigration = 1020; - }; - 51F9F11923A9B8F00087C900 = { - CreatedOnToolsVersion = 11.3; - }; - }; - }; - buildConfigurationList = 51742255224185290054E97C /* Build configuration list for PBXProject "CareKitUI" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 51742251224185290054E97C; - productRefGroup = 5174225C224185290054E97C /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 5174225A224185290054E97C /* CareKitUI iOS */, - 51F9F11923A9B8F00087C900 /* CareKitUI Watch */, - 512B012D22C2F82900ABCB1D /* CareKitUITests iOS */, - ); - }; + 51742252224185290054E97C /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1100; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + 512B012D22C2F82900ABCB1D = { + CreatedOnToolsVersion = 11.0; + }; + 5174225A224185290054E97C = { + CreatedOnToolsVersion = 10.1; + LastSwiftMigration = 1020; + }; + 51F9F11923A9B8F00087C900 = { + CreatedOnToolsVersion = 11.3; + }; + }; + }; + buildConfigurationList = 51742255224185290054E97C /* Build configuration list for PBXProject "CareKitUI" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 51742251224185290054E97C; + productRefGroup = 5174225C224185290054E97C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5174225A224185290054E97C /* CareKitUI iOS */, + 51F9F11923A9B8F00087C900 /* CareKitUI Watch */, + 512B012D22C2F82900ABCB1D /* CareKitUITests iOS */, + ); + }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 512B012C22C2F82900ABCB1D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51742259224185290054E97C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 64EEC2382318253B00B1012F /* Localizable.strings in Resources */, - 03AD384623625A3500F6E7DC /* Localizable.stringsdict in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51F9F11823A9B8F00087C900 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 51F9F16623A9BEC40087C900 /* Localizable.strings in Resources */, - 51F9F16723A9BEC70087C900 /* Localizable.stringsdict in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 512B012C22C2F82900ABCB1D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51742259224185290054E97C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 64EEC2382318253B00B1012F /* Localizable.strings in Resources */, + 03AD384623625A3500F6E7DC /* Localizable.stringsdict in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51F9F11823A9B8F00087C900 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 51F9F16623A9BEC40087C900 /* Localizable.strings in Resources */, + 51F9F16723A9BEC70087C900 /* Localizable.stringsdict in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 512B012A22C2F82900ABCB1D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 51E77383237D1D8000ED70A2 /* TestGridTaskView.swift in Sources */, - 64E1BD4C2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift in Sources */, - 5105254724462BCE004483D0 /* TestLinkType.swift in Sources */, - 51052549244639B3004483D0 /* TestLinkView.swift in Sources */, - 51746F2B2448B90B00B647E1 /* TestNumericProgressTaskView.swift in Sources */, - 51AB06C222FE53E300B73FC2 /* TestStylableView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51742257224185290054E97C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 510466F924A2850700D0FD53 /* OCKContactDisplayable.swift in Sources */, - 518F9D9E22961BF5009CAA48 /* OCKBarLayer.swift in Sources */, - 518F9DA922961BF6009CAA48 /* OCKRingView.swift in Sources */, - 518F9D9C22961BF5009CAA48 /* OCKGridLayer.swift in Sources */, - 51F78FEE22D7C7C100058858 /* OCKCalendarDisplayable.swift in Sources */, - 511BCB5C24365E2E00B4D643 /* OCKDetailedImageView.swift in Sources */, - 518F9D8822961BF5009CAA48 /* OCKAddressButton.swift in Sources */, - 64699E9922FC7B5000FF624F /* OCKLogButtonCell.swift in Sources */, - 518F9D8A22961BF5009CAA48 /* OCKContactButton.swift in Sources */, - 51B225A323EDDCFC00A2D11F /* NoHighlightStyle.swift in Sources */, - 518F9D8C22961BF5009CAA48 /* OCKChecklistTaskView.swift in Sources */, - 518F9D9422961BF5009CAA48 /* OCKGridTaskCell.swift in Sources */, - 516A1C4F244FA7E500BBF2D3 /* View+Extension.swift in Sources */, - 5105911F237651C8004EDC84 /* OCKAccessibleValue.swift in Sources */, - 518F9D9A22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift in Sources */, - 518F9DBC22961BF6009CAA48 /* OCKCardable.swift in Sources */, - 518F9DBB22961BF6009CAA48 /* OCKLabel.swift in Sources */, - 518F9DA122961BF6009CAA48 /* OCKCartesianChartView.swift in Sources */, - 518F9DA022961BF6009CAA48 /* OCKDataSeries.swift in Sources */, - 513FE9FD24803AE10016FCE6 /* LabeledValueTaskView.swift in Sources */, - 512EE90022975F850052F37C /* OCKDetailView.swift in Sources */, - 51AB06C022FE539400B73FC2 /* OCKStylable.swift in Sources */, - 519288732427E2DC00D0AF43 /* CATransaction+Extension.swift in Sources */, - 51656029234AB47500F2A21F /* OCKCheckmarkButton.swift in Sources */, - 51B225AB23EDDCFC00A2D11F /* HeaderView.swift in Sources */, - 518F9DA422961BF6009CAA48 /* OCKLinePlotView.swift in Sources */, - 518F9DA322961BF6009CAA48 /* OCKGridView.swift in Sources */, - 5136794D243BF6EC0026997B /* LinkButton.swift in Sources */, - 515C3C8022F8AB9A007AC906 /* OCKSimpleContactView.swift in Sources */, - 51B225AF23EDDCFC00A2D11F /* InstructionsTaskView.swift in Sources */, - 518F9DA522961BF6009CAA48 /* OCKCartesianGraphView.swift in Sources */, - 518F9D9F22961BF5009CAA48 /* OCKGraphAxisView.swift in Sources */, - 64EEC23B231825DD00B1012F /* OCKLocalization.swift in Sources */, - 51FEF4C624325903003CE34C /* OCKFeaturedContentView.swift in Sources */, - 51367950243BF7040026997B /* LinkView.swift in Sources */, - 518F9D9622961BF5009CAA48 /* OCKLogItemButton.swift in Sources */, - 518F9DBA22961BF6009CAA48 /* OCKCompletionRingButton.swift in Sources */, - 518F9D9522961BF5009CAA48 /* OCKGridTaskView.swift in Sources */, - 51173F3E243FE022004CB150 /* RectangularCompletionView.swift in Sources */, - 518F9D9B22961BF5009CAA48 /* OCKScatterLayer.swift in Sources */, - 510CC634243B843C008BD6B3 /* OCKLog.swift in Sources */, - 518F9D8922961BF5009CAA48 /* OCKDetailedContactView.swift in Sources */, - 518F9D9722961BF5009CAA48 /* OCKGradientPlotView.swift in Sources */, - 51F12D39229C81E200CA265B /* OCKLabeledCheckmarkButton.swift in Sources */, - 518F9DAE22961BF6009CAA48 /* OCKColorStyler.swift in Sources */, - 510D862D23020D410073776E /* OCKStyler.swift in Sources */, - 518F9D9D22961BF5009CAA48 /* OCKLineLayer.swift in Sources */, - 5136794C243BF6EC0026997B /* SafariView.swift in Sources */, - 51173F3C243FE007004CB150 /* CircularCompletionView.swift in Sources */, - 516A02BA22F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift in Sources */, - 51F75B942447DD0B00978C71 /* NumericProgressTaskView.swift in Sources */, - 518F9D9322961BF5009CAA48 /* OCKButtonLogTaskView.swift in Sources */, - 518F9DB522961BF6009CAA48 /* OCKWeekCalendarView.swift in Sources */, - 03089D7F231ED7AF0054EA23 /* Calendar+Extensions.swift in Sources */, - 51367952243BF7450026997B /* LinkLabel.swift in Sources */, - 518F9D9822961BF5009CAA48 /* OCKGraphLegendView.swift in Sources */, - 518F9DC022961BF6009CAA48 /* OCKStackView.swift in Sources */, - 51EFC75623FCAE6000536266 /* UIColor+Extension.swift in Sources */, - 518F9D9922961BF5009CAA48 /* OCKScatterPlotView.swift in Sources */, - 5196B54322D5872C00800706 /* OCKTaskDisplayable.swift in Sources */, - 518F9DA722961BF6009CAA48 /* OCKCompletionRingView.swift in Sources */, - 518F9D9122961BF5009CAA48 /* OCKLabeledButton.swift in Sources */, - 518F9DB222961BF6009CAA48 /* OCKDimensionStyler.swift in Sources */, - 51178E1523AAAB2F0068BAB1 /* OCKCompletionState.swift in Sources */, - 518F9D9022961BF5009CAA48 /* OCKInstructionsTaskView.swift in Sources */, - 518F9DA622961BF6009CAA48 /* OCKGraphable.swift in Sources */, - 5136794B243BF6EC0026997B /* LinkItem.swift in Sources */, - 518F9DB022961BF6009CAA48 /* OCKAnimationStyler.swift in Sources */, - 03181064236A158E006D4870 /* OCKCappedSizeLabel.swift in Sources */, - 518F9DB422961BF6009CAA48 /* OCKHeaderView.swift in Sources */, - 518F9D9222961BF5009CAA48 /* OCKSelfSizingCollectionView.swift in Sources */, - 51B225AD23EDDCFC00A2D11F /* SimpleTaskView.swift in Sources */, - 518F9D8E22961BF5009CAA48 /* OCKSimpleTaskView.swift in Sources */, - 51637EA322F3A68400BAF65C /* OCKLogTaskView.swift in Sources */, - 516A1C48244F766A00BBF2D3 /* OSValue.swift in Sources */, - 5178FBDA22E253FC00794353 /* OCKChartDisplayable.swift in Sources */, - 518F9D8F22961BF5009CAA48 /* OCKChecklistItemButton.swift in Sources */, - 518F9DB722961BF6009CAA48 /* OCKSeparatorView.swift in Sources */, - 516A02BC22F363D900D55AD7 /* OCKView.swift in Sources */, - 518F9DB122961BF6009CAA48 /* OCKAppearanceStyler.swift in Sources */, - 518F9DA222961BF6009CAA48 /* OCKBarPlotView.swift in Sources */, - 510A5762234A7B920006C376 /* OCKAnimatedButton.swift in Sources */, - 518F9DAB22961BF6009CAA48 /* UIFont+Extensions.swift in Sources */, - 5103C55222F37B44007A7403 /* Number+Extensions.swift in Sources */, - 64E1BD4E2309FCF200DFFE52 /* OCKResponsiveLayout.swift in Sources */, - 51B225A923EDDCFC00A2D11F /* CardView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51F9F11623A9B8F00087C900 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 51F9F13023A9B9F80087C900 /* OCKColorStyler.swift in Sources */, - 51E2142A2444F1580063A121 /* CircularCompletionView.swift in Sources */, - 51F9F16823A9BEC90087C900 /* OCKLocalization.swift in Sources */, - 516A1C50244FA7E900BBF2D3 /* View+Extension.swift in Sources */, - 51F9F13323A9B9F80087C900 /* OCKDimensionStyler.swift in Sources */, - 516A1C46244F703000BBF2D3 /* Number+Extensions.swift in Sources */, - 51E76F1D24004F19008B09E7 /* NoHighlightStyle.swift in Sources */, - 51F9F13423A9B9FF0087C900 /* OCKStyler.swift in Sources */, - 51F9F13123A9B9F80087C900 /* OCKAnimationStyler.swift in Sources */, - 510466F524A283E500D0FD53 /* InstructionsTaskView.swift in Sources */, - 51E214292444F1520063A121 /* RectangularCompletionView.swift in Sources */, - 516A1C49244F766A00BBF2D3 /* OSValue.swift in Sources */, - 51D5C97124A2947A00EC45B5 /* OCKLog.swift in Sources */, - 51F9F13223A9B9F80087C900 /* OCKAppearanceStyler.swift in Sources */, - 51E76F1E24004F1F008B09E7 /* CardView.swift in Sources */, - 51E76F2024004F25008B09E7 /* SimpleTaskView.swift in Sources */, - 51EFC75723FCAE6000536266 /* UIColor+Extension.swift in Sources */, - 51E76F1F24004F21008B09E7 /* HeaderView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 512B012A22C2F82900ABCB1D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 51E77383237D1D8000ED70A2 /* TestGridTaskView.swift in Sources */, + 64E1BD4C2309FCDC00DFFE52 /* OCKResponsiveLayoutTests.swift in Sources */, + 5105254724462BCE004483D0 /* TestLinkType.swift in Sources */, + 51052549244639B3004483D0 /* TestLinkView.swift in Sources */, + 51746F2B2448B90B00B647E1 /* TestNumericProgressTaskView.swift in Sources */, + 51AB06C222FE53E300B73FC2 /* TestStylableView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51742257224185290054E97C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 510466F924A2850700D0FD53 /* OCKContactDisplayable.swift in Sources */, + 518F9D9E22961BF5009CAA48 /* OCKBarLayer.swift in Sources */, + 518F9DA922961BF6009CAA48 /* OCKRingView.swift in Sources */, + 518F9D9C22961BF5009CAA48 /* OCKGridLayer.swift in Sources */, + 51F78FEE22D7C7C100058858 /* OCKCalendarDisplayable.swift in Sources */, + 511BCB5C24365E2E00B4D643 /* OCKDetailedImageView.swift in Sources */, + 518F9D8822961BF5009CAA48 /* OCKAddressButton.swift in Sources */, + 64699E9922FC7B5000FF624F /* OCKLogButtonCell.swift in Sources */, + 518F9D8A22961BF5009CAA48 /* OCKContactButton.swift in Sources */, + 51B225A323EDDCFC00A2D11F /* NoHighlightStyle.swift in Sources */, + 518F9D8C22961BF5009CAA48 /* OCKChecklistTaskView.swift in Sources */, + 518F9D9422961BF5009CAA48 /* OCKGridTaskCell.swift in Sources */, + 516A1C4F244FA7E500BBF2D3 /* View+Extension.swift in Sources */, + 5105911F237651C8004EDC84 /* OCKAccessibleValue.swift in Sources */, + 518F9D9A22961BF5009CAA48 /* OCKCartesianCoordinatesLayer.swift in Sources */, + 518F9DBC22961BF6009CAA48 /* OCKCardable.swift in Sources */, + 518F9DBB22961BF6009CAA48 /* OCKLabel.swift in Sources */, + 518F9DA122961BF6009CAA48 /* OCKCartesianChartView.swift in Sources */, + 518F9DA022961BF6009CAA48 /* OCKDataSeries.swift in Sources */, + 513FE9FD24803AE10016FCE6 /* LabeledValueTaskView.swift in Sources */, + 512EE90022975F850052F37C /* OCKDetailView.swift in Sources */, + 51AB06C022FE539400B73FC2 /* OCKStylable.swift in Sources */, + 519288732427E2DC00D0AF43 /* CATransaction+Extension.swift in Sources */, + 51656029234AB47500F2A21F /* OCKCheckmarkButton.swift in Sources */, + 51B225AB23EDDCFC00A2D11F /* HeaderView.swift in Sources */, + 518F9DA422961BF6009CAA48 /* OCKLinePlotView.swift in Sources */, + 518F9DA322961BF6009CAA48 /* OCKGridView.swift in Sources */, + 5136794D243BF6EC0026997B /* LinkButton.swift in Sources */, + 515C3C8022F8AB9A007AC906 /* OCKSimpleContactView.swift in Sources */, + 51B225AF23EDDCFC00A2D11F /* InstructionsTaskView.swift in Sources */, + 518F9DA522961BF6009CAA48 /* OCKCartesianGraphView.swift in Sources */, + 518F9D9F22961BF5009CAA48 /* OCKGraphAxisView.swift in Sources */, + 64EEC23B231825DD00B1012F /* OCKLocalization.swift in Sources */, + 51FEF4C624325903003CE34C /* OCKFeaturedContentView.swift in Sources */, + 51367950243BF7040026997B /* LinkView.swift in Sources */, + 518F9D9622961BF5009CAA48 /* OCKLogItemButton.swift in Sources */, + 518F9DBA22961BF6009CAA48 /* OCKCompletionRingButton.swift in Sources */, + 518F9D9522961BF5009CAA48 /* OCKGridTaskView.swift in Sources */, + 51173F3E243FE022004CB150 /* RectangularCompletionView.swift in Sources */, + 518F9D9B22961BF5009CAA48 /* OCKScatterLayer.swift in Sources */, + 510CC634243B843C008BD6B3 /* OCKLog.swift in Sources */, + 518F9D8922961BF5009CAA48 /* OCKDetailedContactView.swift in Sources */, + 518F9D9722961BF5009CAA48 /* OCKGradientPlotView.swift in Sources */, + 51F12D39229C81E200CA265B /* OCKLabeledCheckmarkButton.swift in Sources */, + 518F9DAE22961BF6009CAA48 /* OCKColorStyler.swift in Sources */, + 510D862D23020D410073776E /* OCKStyler.swift in Sources */, + 518F9D9D22961BF5009CAA48 /* OCKLineLayer.swift in Sources */, + 5136794C243BF6EC0026997B /* SafariView.swift in Sources */, + 51173F3C243FE007004CB150 /* CircularCompletionView.swift in Sources */, + 516A02BA22F363AE00D55AD7 /* NSLayoutConstraint+Extensions.swift in Sources */, + 51F75B942447DD0B00978C71 /* NumericProgressTaskView.swift in Sources */, + 518F9D9322961BF5009CAA48 /* OCKButtonLogTaskView.swift in Sources */, + 518F9DB522961BF6009CAA48 /* OCKWeekCalendarView.swift in Sources */, + 03089D7F231ED7AF0054EA23 /* Calendar+Extensions.swift in Sources */, + 51367952243BF7450026997B /* LinkLabel.swift in Sources */, + 518F9D9822961BF5009CAA48 /* OCKGraphLegendView.swift in Sources */, + 518F9DC022961BF6009CAA48 /* OCKStackView.swift in Sources */, + 51EFC75623FCAE6000536266 /* UIColor+Extension.swift in Sources */, + 518F9D9922961BF5009CAA48 /* OCKScatterPlotView.swift in Sources */, + 5196B54322D5872C00800706 /* OCKTaskDisplayable.swift in Sources */, + 518F9DA722961BF6009CAA48 /* OCKCompletionRingView.swift in Sources */, + 518F9D9122961BF5009CAA48 /* OCKLabeledButton.swift in Sources */, + 518F9DB222961BF6009CAA48 /* OCKDimensionStyler.swift in Sources */, + 51178E1523AAAB2F0068BAB1 /* OCKCompletionState.swift in Sources */, + 518F9D9022961BF5009CAA48 /* OCKInstructionsTaskView.swift in Sources */, + 518F9DA622961BF6009CAA48 /* OCKGraphable.swift in Sources */, + 5136794B243BF6EC0026997B /* LinkItem.swift in Sources */, + 518F9DB022961BF6009CAA48 /* OCKAnimationStyler.swift in Sources */, + 517309A524AA6D7E00A35C85 /* OCKStyler+Extension.swift in Sources */, + 03181064236A158E006D4870 /* OCKCappedSizeLabel.swift in Sources */, + 518F9DB422961BF6009CAA48 /* OCKHeaderView.swift in Sources */, + 518F9D9222961BF5009CAA48 /* OCKSelfSizingCollectionView.swift in Sources */, + 51B225AD23EDDCFC00A2D11F /* SimpleTaskView.swift in Sources */, + 518F9D8E22961BF5009CAA48 /* OCKSimpleTaskView.swift in Sources */, + 51637EA322F3A68400BAF65C /* OCKLogTaskView.swift in Sources */, + 516A1C48244F766A00BBF2D3 /* OSValue.swift in Sources */, + 5178FBDA22E253FC00794353 /* OCKChartDisplayable.swift in Sources */, + 518F9D8F22961BF5009CAA48 /* OCKChecklistItemButton.swift in Sources */, + 518F9DB722961BF6009CAA48 /* OCKSeparatorView.swift in Sources */, + 516A02BC22F363D900D55AD7 /* OCKView.swift in Sources */, + 518F9DB122961BF6009CAA48 /* OCKAppearanceStyler.swift in Sources */, + 518F9DA222961BF6009CAA48 /* OCKBarPlotView.swift in Sources */, + 510A5762234A7B920006C376 /* OCKAnimatedButton.swift in Sources */, + 518F9DAB22961BF6009CAA48 /* UIFont+Extensions.swift in Sources */, + 5103C55222F37B44007A7403 /* Number+Extensions.swift in Sources */, + 64E1BD4E2309FCF200DFFE52 /* OCKResponsiveLayout.swift in Sources */, + 51B225A923EDDCFC00A2D11F /* CardView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 51F9F11623A9B8F00087C900 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 51F9F13023A9B9F80087C900 /* OCKColorStyler.swift in Sources */, + 51E2142A2444F1580063A121 /* CircularCompletionView.swift in Sources */, + 517309A624AA6D7E00A35C85 /* OCKStyler+Extension.swift in Sources */, + 51F9F16823A9BEC90087C900 /* OCKLocalization.swift in Sources */, + 516A1C50244FA7E900BBF2D3 /* View+Extension.swift in Sources */, + 51F9F13323A9B9F80087C900 /* OCKDimensionStyler.swift in Sources */, + 516A1C46244F703000BBF2D3 /* Number+Extensions.swift in Sources */, + 51E76F1D24004F19008B09E7 /* NoHighlightStyle.swift in Sources */, + 51F9F13423A9B9FF0087C900 /* OCKStyler.swift in Sources */, + 51F9F13123A9B9F80087C900 /* OCKAnimationStyler.swift in Sources */, + 510466F524A283E500D0FD53 /* InstructionsTaskView.swift in Sources */, + 51E214292444F1520063A121 /* RectangularCompletionView.swift in Sources */, + 516A1C49244F766A00BBF2D3 /* OSValue.swift in Sources */, + 51D5C97124A2947A00EC45B5 /* OCKLog.swift in Sources */, + 51F9F13223A9B9F80087C900 /* OCKAppearanceStyler.swift in Sources */, + 51E76F1E24004F1F008B09E7 /* CardView.swift in Sources */, + 51E76F2024004F25008B09E7 /* SimpleTaskView.swift in Sources */, + 51EFC75723FCAE6000536266 /* UIColor+Extension.swift in Sources */, + 51E76F1F24004F21008B09E7 /* HeaderView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 512B013522C2F82900ABCB1D /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 5174225A224185290054E97C /* CareKitUI iOS */; - targetProxy = 512B013422C2F82900ABCB1D /* PBXContainerItemProxy */; - }; + 512B013522C2F82900ABCB1D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5174225A224185290054E97C /* CareKitUI iOS */; + targetProxy = 512B013422C2F82900ABCB1D /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 03AD384423625A3500F6E7DC /* Localizable.stringsdict */ = { - isa = PBXVariantGroup; - children = ( - 03AD384723625A4100F6E7DC /* en */, - ); - name = Localizable.stringsdict; - sourceTree = "<group>"; - }; - 64EEC2362318253B00B1012F /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - 64EEC2372318253B00B1012F /* en */, - ); - name = Localizable.strings; - sourceTree = "<group>"; - }; + 03AD384423625A3500F6E7DC /* Localizable.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 03AD384723625A4100F6E7DC /* en */, + ); + name = Localizable.stringsdict; + sourceTree = "<group>"; + }; + 64EEC2362318253B00B1012F /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 64EEC2372318253B00B1012F /* en */, + ); + name = Localizable.strings; + sourceTree = "<group>"; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 512B013622C2F82900ABCB1D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEBUG_INFORMATION_FORMAT = dwarf; - INFOPLIST_FILE = CareKitUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = Apple.CareKitUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 512B013722C2F82900ABCB1D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = CareKitUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = Apple.CareKitUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 51742261224185290054E97C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; - CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; - CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - 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_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; - CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; - CLANG_WARN_ASSIGN_ENUM = 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = 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; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_MISSING_NEWLINE = YES; - GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_SHADOW = YES; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNKNOWN_PRAGMAS = YES; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_LABEL = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - RUN_CLANG_STATIC_ANALYZER = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 51742262224185290054E97C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; - CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; - CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - 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_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; - CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; - CLANG_WARN_ASSIGN_ENUM = 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = 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; - CURRENT_PROJECT_VERSION = 1; - 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_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_MISSING_NEWLINE = YES; - GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_SHADOW = YES; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNKNOWN_PRAGMAS = YES; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_LABEL = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - RUN_CLANG_STATIC_ANALYZER = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 51742264224185290054E97C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - INFOPLIST_FILE = "$(SRCROOT)/CareKitUI/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.carekit.CareKitUI; - PRODUCT_NAME = "$(PROJECT_NAME)"; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Debug; - }; - 51742265224185290054E97C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - INFOPLIST_FILE = "$(SRCROOT)/CareKitUI/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.carekit.CareKitUI; - PRODUCT_NAME = "$(PROJECT_NAME)"; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Release; - }; - 51F9F11F23A9B8F00087C900 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_STYLE = Automatic; - DEBUG_INFORMATION_FORMAT = dwarf; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = CareKitUI/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "org.carekit.CareKitUI-Watch"; - PRODUCT_NAME = "$(PROJECT_NAME)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.0; - }; - name = Debug; - }; - 51F9F12023A9B8F00087C900 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = CareKitUI/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "org.carekit.CareKitUI-Watch"; - PRODUCT_NAME = "$(PROJECT_NAME)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.0; - }; - name = Release; - }; + 512B013622C2F82900ABCB1D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + INFOPLIST_FILE = CareKitUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = Apple.CareKitUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 512B013722C2F82900ABCB1D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = CareKitUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = Apple.CareKitUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 51742261224185290054E97C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + 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_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; + CLANG_WARN_ASSIGN_ENUM = 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = 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; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + RUN_CLANG_STATIC_ANALYZER = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 51742262224185290054E97C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + 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_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; + CLANG_WARN_ASSIGN_ENUM = 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = 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; + CURRENT_PROJECT_VERSION = 1; + 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_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + RUN_CLANG_STATIC_ANALYZER = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 51742264224185290054E97C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + INFOPLIST_FILE = "$(SRCROOT)/CareKitUI/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.carekit.CareKitUI; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 51742265224185290054E97C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_TREAT_WARNINGS_AS_ERRORS = NO; + INFOPLIST_FILE = "$(SRCROOT)/CareKitUI/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.carekit.CareKitUI; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 51F9F11F23A9B8F00087C900 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = CareKitUI/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.carekit.CareKitUI-Watch"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 6.0; + }; + name = Debug; + }; + 51F9F12023A9B8F00087C900 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = CareKitUI/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.carekit.CareKitUI-Watch"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 6.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 512B013822C2F82900ABCB1D /* Build configuration list for PBXNativeTarget "CareKitUITests iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 512B013622C2F82900ABCB1D /* Debug */, - 512B013722C2F82900ABCB1D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 51742255224185290054E97C /* Build configuration list for PBXProject "CareKitUI" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 51742261224185290054E97C /* Debug */, - 51742262224185290054E97C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 51742263224185290054E97C /* Build configuration list for PBXNativeTarget "CareKitUI iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 51742264224185290054E97C /* Debug */, - 51742265224185290054E97C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 51F9F12123A9B8F00087C900 /* Build configuration list for PBXNativeTarget "CareKitUI Watch" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 51F9F11F23A9B8F00087C900 /* Debug */, - 51F9F12023A9B8F00087C900 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; + 512B013822C2F82900ABCB1D /* Build configuration list for PBXNativeTarget "CareKitUITests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 512B013622C2F82900ABCB1D /* Debug */, + 512B013722C2F82900ABCB1D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 51742255224185290054E97C /* Build configuration list for PBXProject "CareKitUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 51742261224185290054E97C /* Debug */, + 51742262224185290054E97C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 51742263224185290054E97C /* Build configuration list for PBXNativeTarget "CareKitUI iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 51742264224185290054E97C /* Debug */, + 51742265224185290054E97C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 51F9F12123A9B8F00087C900 /* Build configuration list for PBXNativeTarget "CareKitUI Watch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 51F9F11F23A9B8F00087C900 /* Debug */, + 51F9F12023A9B8F00087C900 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ - }; - rootObject = 51742252224185290054E97C /* Project object */; + }; + rootObject = 51742252224185290054E97C /* Project object */; } diff --git a/CareKitUI/CareKitUI.xcodeproj/xcshareddata/xcschemes/CareKitUI.xcscheme b/CareKitUI/CareKitUI.xcodeproj/xcshareddata/xcschemes/CareKitUI.xcscheme new file mode 100644 index 000000000..024de8549 --- /dev/null +++ b/CareKitUI/CareKitUI.xcodeproj/xcshareddata/xcschemes/CareKitUI.xcscheme @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1140" + version = "1.7"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5174225A224185290054E97C" + BuildableName = "CareKitUI.framework" + BlueprintName = "CareKitUI iOS" + ReferencedContainer = "container:CareKitUI.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5174225A224185290054E97C" + BuildableName = "CareKitUI.framework" + BlueprintName = "CareKitUI iOS" + ReferencedContainer = "container:CareKitUI.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <TestPlans> + <TestPlanReference + reference = "container:CareKitUITests/CareKitUI.xctestplan" + default = "YES"> + </TestPlanReference> + </TestPlans> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5174225A224185290054E97C" + BuildableName = "CareKitUI.framework" + BlueprintName = "CareKitUI iOS" + ReferencedContainer = "container:CareKitUI.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5174225A224185290054E97C" + BuildableName = "CareKitUI.framework" + BlueprintName = "CareKitUI iOS" + ReferencedContainer = "container:CareKitUI.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/CareKitUI/CareKitUI/Common/Controls/OCKAnimatedButton.swift b/CareKitUI/CareKitUI/Common/Controls/OCKAnimatedButton.swift new file mode 100644 index 000000000..27ec95895 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Controls/OCKAnimatedButton.swift @@ -0,0 +1,327 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +private extension UIColor { + func inverted() -> UIColor? { + var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 + guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { return nil } + return .init(red: 1 - red, green: 1 - green, blue: 1 - blue, alpha: alpha) + } +} + +/// A highlightable and selectable button that can be animated. The `isHighlighted` state can be specified with options, and the `isSelected` state +/// can be handled automatically when the button is tapped. +/// +/// The content view can be injected into the button. By default, the content is contrained to the button's `layoutMarginGuide`. +open class OCKAnimatedButton<Content: UIView>: UIControl, OCKStylable { + + /// Options for displaying the the highlighted state of an `OCKAnimatedButton`. + public enum HighlightOption: Hashable { + + /// Draw an overlay over the button's content. + case overlay(alpha: CGFloat) + + /// Lower the alpha of the button. + case fade(alpha: CGFloat) + + /// Delay showing the highlighted state after the button is tapped. + case delayOnSelect(delay: TimeInterval) + + /// Draw an overlay over the button's content with a default alpha. + public static var defaultOverlay: HighlightOption { .overlay(alpha: 0.05) } + + /// Delay showing the highlighted state after the button is tapped by a default value. + public static var defaultFade: HighlightOption { .fade(alpha: 0.6) } + + /// Delay showing the highlighted state after the button is tapped by a default value. + public static var defaultDelayOnSelect: HighlightOption { .delayOnSelect(delay: 0.05) } + + public func hash(into hasher: inout Hasher) { + // Hash self without taking into account the associated values. + switch self { + case .overlay: hasher.combine(2) + case .fade: hasher.combine(3) + case .delayOnSelect: hasher.combine(4) + } + } + + // Check if self is `.overlay` and ignore the associated value. + var isOverlay: Bool { + switch self { + case .overlay: return true + default: return false + } + } + + // Check if self is `.delayOnSelect` and ignore the associated value. + var isDelayOnSelect: Bool { + switch self { + case .delayOnSelect: return true + default: return false + } + } + + // Check if self is `.fade` and ignore the associated value. + var isFade: Bool { + switch self { + case .fade: return true + default: return false + } + } + } + + // MARK: Properties + + /// Set the selected state for the button. By default the selected state is not animated. + override open var isSelected: Bool { + get { return super.isSelected } + set { return setSelected(newValue, animated: false) } + } + + /// Set the highlighted state for the button. By default the highlighted state is animated. + override open var isHighlighted: Bool { + get { return super.isHighlighted } + set { + if showsHighlight { + // Note: We animate by default because `isHighlighted` is set and managed internally by `UIControl`. + setHighlighted(newValue, animated: true, buttonWasTapped: true) + } else { + super.isHighlighted = newValue + } + } + } + + /// Handle the selection state automatically when the button is tapped. + public var handlesSelection: Bool = true + + /// The highlighted state options. + public let highlightOptions: Set<HighlightOption> + + /// The content inside the button. + public let contentView: Content? + + public var customStyle: OCKStyler? { + didSet { styleChildren() } + } + + // Overlay over the content during the highlighted state. + private lazy var overlayView: UIView = { + let view = UIView() + view.alpha = 0 + view.isUserInteractionEnabled = false + return view + }() + + private let highlightAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut, animations: nil) + private let selectAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut, animations: nil) + private var showsHighlight: Bool { !highlightOptions.isEmpty } + + // MARK: - Life Cycle + + /// Creat an animated button with injected content. The content will automatically be constrained to the button's `layoutMarginGuide`. + /// - Parameter contentView: The content to be constrained to the button's `layoutMarginGuide`. + /// - Parameter highlightOptions: Options for displaying the highlighted state. Suppplying no options cause the button to show no highlighted + /// state + /// - Parameter handlesSelection: Automatically sets the selected state when the button is tapped. + public init(contentView: Content?, + highlightOptions: Set<HighlightOption> = [], + handlesSelection: Bool = true) { + self.contentView = contentView + self.handlesSelection = handlesSelection + self.highlightOptions = Set(highlightOptions) + super.init(frame: .zero) + setup() + } + + public required init?(coder: NSCoder) { + contentView = nil + highlightOptions = [] + super.init(coder: coder) + setup() + } + + // MARK: - Methods + + private func setup() { + addSubviews() + constrainSubviews() + styleDidChange() + + contentView?.isUserInteractionEnabled = false + } + + private func addSubviews() { + if let contentView = contentView { + addSubview(contentView) + } + + if highlightOptions.contains(where: { $0.isOverlay }) { + addSubview(overlayView) + } + } + + private func constrainSubviews() { + if highlightOptions.contains(where: { $0.isOverlay }) { + overlayView.frame = bounds + overlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + } + + if let contentView = contentView { + contentView.frame = bounds + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + } + } + + private func highlightDelayValue(for options: Set<HighlightOption>) -> TimeInterval { + guard let index = options.firstIndex(where: { $0.isDelayOnSelect }) else { return 0 } + if case let HighlightOption.delayOnSelect(delay) = options[index] { + return delay + } + return 0 + } + + private func setHighlighted(_ isHighlighted: Bool, animated: Bool, buttonWasTapped: Bool) { + guard isHighlighted != super.isHighlighted else { return } + super.isHighlighted = isHighlighted + + // Ensure the button should show a highlighted state. If it should not, clear the state. + guard showsHighlight else { + if isHighlighted { + setStyleForSelectedState(false) + } + return + } + + // Set the state without an animation. + guard animated else { + highlightAnimator.stopAnimation(true) + setStyleForHighlightedState(isHighlighted) + return + } + + // Compute the delay value for showing the highlighted state. Only delay if the highlighted state is the result of a tap, and the highlight + // options specify a delay. + let delay: TimeInterval = buttonWasTapped && isHighlighted ? highlightDelayValue(for: highlightOptions) : 0 + + // Animate the highlighted state. + highlightAnimator.stopAnimation(true) + highlightAnimator.addAnimations { [unowned self] in + self.setStyleForHighlightedState(isHighlighted) + } + highlightAnimator.startAnimation(afterDelay: delay) + } + + private func isTouchInside(_ touches: Set<UITouch>, event: UIEvent?) -> Bool { + guard let touch = touches.first else { return false } + let position = touch.location(in: self) + return point(inside: position, with: event) + } + + override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + + // Automatically set the selected state when the button is tapped + guard handlesSelection && isTouchInside(touches, event: event) else { return } + setSelected(!isSelected, animated: true) + } + + /// Set the style for the highlighted state. This function may be called in an animation block if the state is being animated. + /// - Parameter isHighlighted: True if the button is in the highlighted state. + open func setStyleForHighlightedState(_ isHighlighted: Bool) { + for option in highlightOptions { + switch option { + case .overlay(let alpha): + overlayView.alpha = isHighlighted ? alpha : 0 + case .fade(let alpha): + self.alpha = isHighlighted ? alpha : 1 + case .delayOnSelect: + break + } + } + } + + /// Set the highlighted state and update the `isHighlighted` property. + /// - Parameter isHighlighted: True if the button is in the highlighted state. + /// - Parameter animated: Animate the highlighted state. + open func setHighlighted(_ isHighlighted: Bool, animated: Bool) { + setHighlighted(isHighlighted, animated: animated, buttonWasTapped: false) + } + + /// 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. + open func setStyleForSelectedState(_ isSelected: Bool) { + assert(false, "Should override setStyleForSelectedState(isSelected:)") + } + + /// Set the selected state and update the `isSelected` property. + /// - Parameter isSelected: True if the button is in the selected state. + /// - Parameter animated: Animate the selected state. + open func setSelected(_ isSelected: Bool, animated: Bool) { + guard isSelected != self.isSelected else { return } + super.isSelected = isSelected + + // Set the state without an animation. + guard animated else { + selectAnimator.stopAnimation(true) + setStyleForSelectedState(isSelected) + return + } + + // Animate the selected state. + selectAnimator.stopAnimation(true) + selectAnimator.addAnimations { [unowned self] in + self.setStyleForSelectedState(isSelected) + } + selectAnimator.startAnimation() + } + + // MARK: - OCKStylable + + open func styleDidChange() { + let style = self.style() + overlayView.backgroundColor = UIColor { _ in + let customBackground = style.color.customBackground + return customBackground.inverted() ?? customBackground + } + directionalLayoutMargins = style.dimension.directionalInsets1 + } + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + styleDidChange() + } + + override open func removeFromSuperview() { + super.removeFromSuperview() + styleChildren() + } +} diff --git a/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift b/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift new file mode 100644 index 000000000..ff7964966 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Controls/OCKCheckmarkButton.swift @@ -0,0 +1,162 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A button that shows a checkmark in the selected state, and an empty ring in the deselected state. +open class OCKCheckmarkButton: OCKAnimatedButton<UIView> { + + override open var intrinsicContentSize: CGSize { + return .init(width: height.scaledValue, height: height.scaledValue) + } + + /// Checkmark image in the center of the view. + public let imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "checkmark") + return imageView + }() + + lazy var height = OCKAccessibleValue(container: style(), keyPath: \.dimension.buttonHeight2) { [weak self] _ in + self?.invalidateIntrinsicContentSize() + } + + 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 borderLayer = CAShapeLayer() + private let fillLayer = CAShapeLayer() + + // MARK: Life cycle + + public init() { + super.init(contentView: imageView) + setup() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func layoutSubviews() { + super.layoutSubviews() + updateLayers(for: bounds, borderWidth: lineWidth.scaledValue) + } + + // MARK: Methods + + private func setup() { + constrainSubviews() + styleSubviews() + + layer.insertSublayer(borderLayer, below: imageView.layer) + layer.insertSublayer(fillLayer, below: imageView.layer) + } + + private func styleSubviews() { + clipsToBounds = true + applyTintColor() + setStyleForSelectedState(false) + } + + private func constrainSubviews() { + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } + + 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() { + super.styleDidChange() + let style = self.style() + height.update(withContainer: style) + lineWidth.update(withContainer: style) + imageViewPointSize.update(withContainer: style) + if isSelected { + imageView.tintColor = style.color.customBackground + } + } + + override open func tintColorDidChange() { + super.tintColorDidChange() + applyTintColor() + } + + override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { + [lineWidth, height, imageViewPointSize].forEach { $0.apply() } + } + } + + 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/Extensions/Calendar+Extensions.swift b/CareKitUI/CareKitUI/Common/Extensions/Calendar+Extensions.swift new file mode 100644 index 000000000..a62b3fda7 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Extensions/Calendar+Extensions.swift @@ -0,0 +1,39 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +extension Calendar { + func dateIntervalOfWeek(for date: Date) -> DateInterval { + var interval = Calendar.current.dateInterval(of: .weekOfYear, for: date)! + interval.duration -= 1 // The default interval contains 1 second of the next day after the interval. Subtract that off + return interval + } +} diff --git a/CareKitUI/CareKitUI/Common/Extensions/NSLayoutConstraint+Extensions.swift b/CareKitUI/CareKitUI/Common/Extensions/NSLayoutConstraint+Extensions.swift new file mode 100644 index 000000000..35449bf0f --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Extensions/NSLayoutConstraint+Extensions.swift @@ -0,0 +1,110 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +struct LayoutDirection: OptionSet { + let rawValue: Int + + static let top = LayoutDirection(rawValue: 1 << 0) + static let bottom = LayoutDirection(rawValue: 1 << 1) + static let leading = LayoutDirection(rawValue: 1 << 2) + static let trailing = LayoutDirection(rawValue: 1 << 3) + + static let horizontal: LayoutDirection = [.leading, .trailing] + static let vertical: LayoutDirection = [.top, .bottom] + + static let all: LayoutDirection = [.horizontal, .vertical] +} + +extension UILayoutPriority { + static var almostRequired: UILayoutPriority { + return .required - 1 + } +} + +extension NSLayoutConstraint { + func withPriority(_ new: UILayoutPriority) -> NSLayoutConstraint { + priority = new + return self + } +} + +extension UIView { + func setContentHuggingPriorities(_ new: UILayoutPriority) { + setContentHuggingPriority(new, for: .horizontal) + setContentHuggingPriority(new, for: .vertical) + } + + func setContentCompressionResistancePriorities(_ new: UILayoutPriority) { + setContentCompressionResistancePriority(new, for: .horizontal) + setContentCompressionResistancePriority(new, for: .vertical) + } + + func constraints(equalTo other: UIView, directions: LayoutDirection = .all, + priority: UILayoutPriority = .required) -> [NSLayoutConstraint] { + var constraints: [NSLayoutConstraint] = [] + if directions.contains(.top) { + constraints.append(topAnchor.constraint(equalTo: other.topAnchor).withPriority(priority)) + } + if directions.contains(.leading) { + constraints.append(leadingAnchor.constraint(equalTo: other.leadingAnchor).withPriority(priority)) + } + if directions.contains(.bottom) { + constraints.append(bottomAnchor.constraint(equalTo: other.bottomAnchor).withPriority(priority)) + } + if directions.contains(.trailing) { + constraints.append(trailingAnchor.constraint(equalTo: other.trailingAnchor).withPriority(priority)) + } + return constraints + } + + func constraints(equalTo layoutGuide: UILayoutGuide, directions: LayoutDirection = .all, + priority: UILayoutPriority = .required) -> [NSLayoutConstraint] { + var constraints: [NSLayoutConstraint] = [] + if directions.contains(.top) { + constraints.append(topAnchor.constraint(equalTo: layoutGuide.topAnchor).withPriority(priority)) + } + if directions.contains(.leading) { + constraints.append(leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).withPriority(priority)) + } + if directions.contains(.bottom) { + constraints.append(bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).withPriority(priority)) + } + if directions.contains(.trailing) { + constraints.append(trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).withPriority(priority)) + } + return constraints + } + + var isRightToLeft: Bool { + return UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) == .rightToLeft + } +} diff --git a/CareKitUI/CareKitUI/Common/Extensions/Number+Extensions.swift b/CareKitUI/CareKitUI/Common/Extensions/Number+Extensions.swift new file mode 100644 index 000000000..52a9770dc --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Extensions/Number+Extensions.swift @@ -0,0 +1,43 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +extension Double { + var normalized: Double { + return max(0, min(self, 1)) + } +} + +extension CGFloat { + func scaled() -> CGFloat { + UIFontMetrics.default.scaledValue(for: self) + } +} diff --git a/CareKitUI/CareKitUI/Common/Extensions/UIFont+Extensions.swift b/CareKitUI/CareKitUI/Common/Extensions/UIFont+Extensions.swift new file mode 100644 index 000000000..59f97af7d --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Extensions/UIFont+Extensions.swift @@ -0,0 +1,50 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +extension UIFont { + static func preferredCustomFont(forTextStyle textStyle: TextStyle, weight: Weight) -> UIFont { + let defaultDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) + let size = defaultDescriptor.pointSize + let fontDescriptor = UIFontDescriptor(fontAttributes: [ + UIFontDescriptor.AttributeName.size: size, + UIFontDescriptor.AttributeName.family: UIFont.systemFont(ofSize: size).familyName + ]) + + // Add the font weight to the descriptor + let weightedFontDescriptor = fontDescriptor.addingAttributes([ + UIFontDescriptor.AttributeName.traits: [ + UIFontDescriptor.TraitKey.weight: weight + ] + ]) + return UIFont(descriptor: weightedFontDescriptor, size: 0) + } +} diff --git a/CareKitUI/CareKitUI/Common/Labels/OCKCappedSizeLabel.swift b/CareKitUI/CareKitUI/Common/Labels/OCKCappedSizeLabel.swift new file mode 100644 index 000000000..95caf9557 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Labels/OCKCappedSizeLabel.swift @@ -0,0 +1,46 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// This is an internal subclass of `OCKLabel` that has a max font size that will be respected even if +/// the user increases font size or uses accessible sizes. It should be used sparingly and exposed +/// in public API's as its superclass `OCKLabel` +class OCKCappedSizeLabel: OCKLabel { + + var maxFontSize: CGFloat = 20 + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if font.pointSize > maxFontSize { + font = font.withSize(maxFontSize) + } + } +} diff --git a/CareKitUI/CareKitUI/Common/Labels/OCKLabel.swift b/CareKitUI/CareKitUI/Common/Labels/OCKLabel.swift new file mode 100644 index 000000000..9e196b8f5 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Labels/OCKLabel.swift @@ -0,0 +1,123 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A label that handles animating state changes and provides accessibilty features. +/// +/// To animate changes to the text, set `animatesTextChanges` to true. +/// +/// To have the label automatically change its text size whenever the accessibility content size changes, +/// use the initialzer that takes a `textStyle` and `weight`. +open class OCKLabel: UILabel, OCKStylable { + + // MARK: Properties + + public var customStyle: OCKStyler? { + didSet { styleChildren() } + } + + /// Flag determining whether to animate text changes. + public var animatesTextChanges = false + + override open var text: String? { + get { + return super.text + } set { + guard animatesTextChanges else { super.text = newValue; return; } + UIView.transition(with: self, duration: style().animation.stateChangeDuration, options: .transitionCrossDissolve, animations: { + super.text = newValue + }, completion: nil) + } + } + + private let textStyle: UIFont.TextStyle? + private let weight: UIFont.Weight? + + // MARK: Life Cycle + + /// Create an instance of and `OCKLabel`. By default, the label will not animate text changes and will not scale with + /// accessibility content size changes. + public init() { + textStyle = nil + weight = nil + super.init(frame: .zero) + setup() + } + + /// Create an instance of and `OCKLabel`. By default, the label will not animate text changes and will scale with + /// accessibility content size changes. + /// + /// - Parameters: + /// - textStyle: The style of the font. + /// - weight: The weight of the font. + public init(textStyle: UIFont.TextStyle, weight: UIFont.Weight) { + self.textStyle = textStyle + self.weight = weight + super.init(frame: .zero) + font = UIFont.preferredCustomFont(forTextStyle: textStyle, weight: weight) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + textStyle = nil + weight = nil + super.init(coder: aDecoder) + setup() + } + + override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + guard + let textStyle = textStyle, let weight = weight, + traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory + else { return } + font = UIFont.preferredCustomFont(forTextStyle: textStyle, weight: weight) + } + + // MARK: Methods + + private func setup() { + preservesSuperviewLayoutMargins = true + adjustsFontForContentSizeCategory = false + styleDidChange() + } + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + styleDidChange() + } + + override open func removeFromSuperview() { + super.removeFromSuperview() + styleChildren() + } + + open func styleDidChange() {} +} diff --git a/CareKitUI/CareKitUI/Common/Layout/OCKResponsiveLayout.swift b/CareKitUI/CareKitUI/Common/Layout/OCKResponsiveLayout.swift new file mode 100644 index 000000000..178d8c0dd --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Layout/OCKResponsiveLayout.swift @@ -0,0 +1,183 @@ +/* +Copyright (c) 2019, Apple Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder(s) nor the names of any contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. No license is granted to the trademarks of +the copyright holders even if such marks are included in this software. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import Foundation +import UIKit + +/// Accessible view configuration that contains layout information for different `UserInterfaceSizeClass` +/// and `UIContentSizeCategory` combinations. +public struct OCKResponsiveLayout<LayoutOption> { + + // MARK: - Instance Properties + + /// A default `SizeClassRuleSet` to apply for different `UIContentSizeCategory`'s when the + /// exact `UserInterfaceSizeClass` is not important. + public let defaultRuleSet: SizeClassRuleSet<LayoutOption> + + /// A set of `UserInterfaceSizeClass` specific rule sets to provide different accessible layouts + /// for specific size class combinations. + public let sizeClassSpecificRuleSets: [SizeClassRuleSet<LayoutOption>] + + /// A lightweight typealias for a horizontal and vertical `UIUserInterfaceSizeClass` definition. + /// + /// See [Adaptivity and Layout](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/) + /// on the Human Interface Guidelines for all possible combinations. + public typealias SizeClass = (horizontal: UIUserInterfaceSizeClass, vertical: UIUserInterfaceSizeClass) + + // MARK: - Initializers + + /// Initialize a `OCKResponsiveLayout` with default and specific size class rules. + /// + /// - Parameter defaultRuleSet: The default layout rules for when no matching size class rule is + /// provided, usually when layouts need to support dynamic type, but not size class. + /// - Parameter sizeClassSpecificRuleSets: Size class specific layout rule sets + public init( + defaultLayout: LayoutOption, + anySizeClassRuleSet: [OCKResponsiveLayout.Rule<LayoutOption>], + sizeClassSpecificRuleSets: [SizeClassRuleSet<LayoutOption>] = []) { + + let defaultRule = Rule(layout: defaultLayout, greaterThanOrEqualToContentSizeCategory: .unspecified) + + self.defaultRuleSet = SizeClassRuleSet<LayoutOption>( + sizeClasses: [(horizontal: .unspecified, vertical: .unspecified)], + rules: [defaultRule] + anySizeClassRuleSet + ) + + self.sizeClassSpecificRuleSets = sizeClassSpecificRuleSets + } + + // MARK: - Nested Definitions + + /// A layout rule for a given `UIContentSizeCategory`. + public struct Rule<Layout> { + + /// A selected `UIContentSizeCategory` for the given layout. + public let contentSizeCategory: UIContentSizeCategory + + /// A provided layout to display at or above the given `UIContentSizeCategory`. + public let layout: Layout + + /// Initialize a rule with a `Layout` defined by the user and a `UIContentSizeCategory` to display + /// the layout at. + /// - Parameter layout: The `Layout` to display at or above the given `UIContentSizeCategory` + /// - Parameter contentSizeCategory: The `UIContentSizeCategory` that this layout will display at + /// or above + public init( + layout: Layout, + greaterThanOrEqualToContentSizeCategory contentSizeCategory: UIContentSizeCategory = .extraSmall + ) { + self.contentSizeCategory = contentSizeCategory + self.layout = layout + } + } + + /// A set of `UIContentSizeCategory` specific rules for a given size class. + public struct SizeClassRuleSet<LayoutOption> { + + /// A set of rules (combinations of `UIUserInterFaceSizeClass` combinations and user defined layouts. + public let rules: [OCKResponsiveLayout<LayoutOption>.Rule<LayoutOption>] + + /// The valid size class combinations for this set of rules + public let sizeClasses: [SizeClass] + + /// Initialize a `SizeClassRuleSet` with a set of `UIUserInterfaceSizeClass` combinations and + /// `UIContentSizeCategory` specific rules. + /// - Parameter sizeClasses: The `SizeClass` horizontal and vertical definitions for the rules + /// - Parameter rules: The `UIContentSizeCategory` rules for these size classes + public init( + sizeClasses: [SizeClass], + rules: [OCKResponsiveLayout<LayoutOption>.Rule<LayoutOption>] + ) { + self.sizeClasses = sizeClasses + self.rules = rules + } + + /// Initialize a `SizeClassRuleSet` with a `UIUserInterfaceSizeClass` combination and + /// `UIContentSizeCategory` specific rules. + /// - Parameter sizeClass: The `SizeClass` horizontal and vertical definition for the rules + /// - Parameter rules: The `UIContentSizeCategory` rules for this size class + public init( + sizeClass: SizeClass = (horizontal: .unspecified, vertical: .unspecified), + rules: [OCKResponsiveLayout<LayoutOption>.Rule<LayoutOption>] + ) { + self.init(sizeClasses: [sizeClass], rules: rules) + } + } + + // MARK: - Instance Methods + + /// Get a generic `LayoutOption` that has been mapped to a `UserInterfaceSizeClass` and + /// `UIContentSizeCategory` for comparison provided by a given `UITraitCollection`. + /// - Parameter traitCollection: The trait collection to extract device and accessility information. + /// + /// A UITraitCollection contains additional size and accessibility information beyond + /// `UserInterfaceSizeClass` and `UIContentSizeCategory`. This class and method could be extended + /// to respond to other changes beyond these two, however these are `Comparable` and provide a balance + /// between convenient and flexibility. Consider creating additional factory methods instead of + /// extending this method to respond to `contentSize` or `contentInsets` to maintain this convenience. + public func responsiveLayoutRule(traitCollection: UITraitCollection) -> LayoutOption { + + func setContainsCurrentSizeClass(set: SizeClassRuleSet<LayoutOption>) -> Bool { + return set.sizeClasses.contains { width, height -> Bool in + return width == traitCollection.horizontalSizeClass && height == traitCollection.verticalSizeClass + } + } + + func largestMatchingRule(rules: [Rule<LayoutOption>]) -> Rule<LayoutOption>? { + return rules.last { rule -> Bool in + return rule.contentSizeCategory <= traitCollection.preferredContentSizeCategory + } + } + + func layoutOptionForLayoutRuleSet(set: SizeClassRuleSet<LayoutOption>) -> LayoutOption { + let sorted = set.rules.sorted(by: { $0.contentSizeCategory < $1.contentSizeCategory }) + + guard let layout = + largestMatchingRule(rules: sorted)?.layout + ?? largestMatchingRule(rules: self.defaultRuleSet.rules)?.layout else { + fatalError( + """ + A layout could not be determined which should be impossible due to `defaultLayout: LayoutOption` in the + OCKResponsiveLayout class being non-optional. + """ + ) + } + + return layout + } + + let ruleSet = self.sizeClassSpecificRuleSets + .first(where: { setContainsCurrentSizeClass(set: $0) }) + ?? defaultRuleSet + + return layoutOptionForLayoutRuleSet(set: ruleSet) + + } +} diff --git a/CareKitUI/CareKitUI/Common/OCKAccessibleValue.swift b/CareKitUI/CareKitUI/Common/OCKAccessibleValue.swift new file mode 100644 index 000000000..ffec61541 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/OCKAccessibleValue.swift @@ -0,0 +1,68 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A value that scales with the current content size category. +struct OCKAccessibleValue<Container> { + + /// The value before scaling for the content size category. + internal private(set) var rawValue: CGFloat { + didSet { apply() } + } + + /// The value after scaling for the content size category. + var scaledValue: CGFloat { return rawValue.scaled() } + + private var keyPath: KeyPath<Container, CGFloat> + private let applyScaledValue: (_ scaledValue: CGFloat) -> Void + + init(container: Container, keyPath: KeyPath<Container, CGFloat>, apply: @escaping (_ scaledValue: CGFloat) -> Void) { + self.keyPath = keyPath + rawValue = container[keyPath: keyPath] + self.applyScaledValue = apply + } + + /// Update the raw value with a new container. Will use the existing keypath to set the raw value. + mutating func update(withContainer container: Container) { + rawValue = container[keyPath: keyPath] + } + + /// Update the raw value with a new container and keypath to access the raw value. + mutating func update(withContainer container: Container, keyPath: KeyPath<Container, CGFloat>) { + self.keyPath = keyPath + rawValue = container[keyPath: keyPath] + } + + /// Apply the scaled value. + func apply() { + self.applyScaledValue(scaledValue) + } +} diff --git a/CareKitUI/CareKitUI/Common/Protocols/OCKCardable.swift b/CareKitUI/CareKitUI/Common/Protocols/OCKCardable.swift new file mode 100644 index 000000000..b4079ed8e --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Protocols/OCKCardable.swift @@ -0,0 +1,69 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Has the ability to display one's self as a card. A card has a particular corner +/// radius and shadow. +public protocol OCKCardable { + /// View that will be styled as a card. Should hold the `contentView`. + var cardView: UIView { get } + + /// Holds the main content. Subviews should be added to this view. + var contentView: UIView { get } +} + +public extension OCKCardable { + /// Turn the card styling on/off. If this view is stylable, this method should be called from the `styleDidChange` method. Note that shadow + /// rastering is set on by default, and consequently a shadow cannot be set over a clear background. + /// - Parameter enabled: true to turn the card styling on. + /// - Parameter style: The style to use for the card. + func enableCardStyling(_ enabled: Bool, style: OCKStyler) { + cardView.backgroundColor = style.color.secondaryCustomGroupedBackground + cardView.layer.masksToBounds = false + cardView.layer.cornerCurve = .continuous + cardView.layer.cornerRadius = enabled ? style.appearance.cornerRadius2 : 0 + cardView.layer.shadowColor = enabled ? style.color.black.cgColor : UIColor.clear.cgColor + cardView.layer.shadowOffset = style.appearance.shadowOffset1 + cardView.layer.shadowRadius = enabled ? style.appearance.shadowRadius1 : 0 + cardView.layer.shadowOpacity = enabled ? style.appearance.shadowOpacity1 : 0 + cardView.layer.rasterizationScale = enabled ? UIScreen.main.scale : 0 + cardView.layer.shouldRasterize = enabled + + contentView.layer.cornerCurve = .continuous + contentView.layer.cornerRadius = cardView.layer.cornerRadius + } +} + +/// Auxiliary object to handle the `OCKCardable` protocol. +struct OCKCardBuilder: OCKCardable { + let cardView: UIView + let contentView: UIView +} diff --git a/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift b/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift new file mode 100644 index 000000000..f7899dc75 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Style/OCKStylable.swift @@ -0,0 +1,94 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +// An object that can be styled. +public protocol OCKStylable { + + /// Used to override the style. + var customStyle: OCKStyler? { get set } + + /// Returns in order of existence: This object's custom style, the first parent with a custom style, or the default style. + func style() -> OCKStyler + + /// Called when the style changes. + func styleDidChange() +} + +/// In order to propogate style through the view hierearchy: +/// 1. Call `styleChildren()` from a `didSet` observer on `customStyle`. +/// 2. Call `styleChildren()` from `removeFromSuperView()`. +/// 3. Call `styleDidChange()` from `didMoveToSuperview()`. +public extension OCKStylable where Self: UIView { + /// Returns in order of existence: This object's custom style, the first parent with a custom style, or the default style. + func style() -> OCKStyler { + return customStyle ?? getParentCustomStyle() ?? OCKStyle() + } + + /// Notify this view and subviews that the style has changed. Guarantees that the outermost view's `styleDidChange` method will be called after + /// that of inner views. + func styleChildren() { + recursiveStyleChildren() + styleDidChange() + } +} + +private extension UIView { + // Find the first custom style in the superview hierarchy. + func getParentCustomStyle() -> OCKStyler? { + guard let superview = superview else { return nil } + + // if the view has a custom style, return it + if let typedSuperview = superview as? OCKStylable, let customStyle = typedSuperview.customStyle { + return customStyle + } + + // else check if the superview has a custom style + return superview.getParentCustomStyle() + } + + // Recursively notify subviews that the style has changed. + func recursiveStyleChildren() { + for view in subviews { + // Propogate style through any `UIView`s + guard let typedView = view as? OCKStylable & UIView else { + view.recursiveStyleChildren() + continue + } + + // Propogate style to subviews that are not the child of a view that has set a custom style + if typedView.customStyle == nil { + typedView.recursiveStyleChildren() + typedView.styleDidChange() + } + } + } +} diff --git a/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift b/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift new file mode 100644 index 000000000..d4a47f8e5 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Style/OCKStyler.swift @@ -0,0 +1,75 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation +import SwiftUI + +/// Defines styling constants. +public protocol OCKStyler { + var color: OCKColorStyler { get } + var animation: OCKAnimationStyler { get } + var appearance: OCKAppearanceStyler { get } + var dimension: OCKDimensionStyler { get } +} + +/// Defines default values for style constants. +public extension OCKStyler { + var color: OCKColorStyler { OCKColorStyle() } + var animation: OCKAnimationStyler { OCKAnimationStyle() } + var appearance: OCKAppearanceStyler { OCKAppearanceStyle() } + var dimension: OCKDimensionStyler { OCKDimensionStyle() } +} + +// Concrete object that contains style constants. +public struct OCKStyle: OCKStyler { + public init() {} +} + +private struct StyleEnvironmentKey: EnvironmentKey { + static var defaultValue: OCKStyler = OCKStyle() +} + +public extension EnvironmentValues { + + /// Style constants that can be used by a view. + var careKitStyle: OCKStyler { + get { self[StyleEnvironmentKey.self] } + set { self[StyleEnvironmentKey.self] = newValue } + } +} + +public extension View { + + /// Provide style constants that can be used by a view. + /// - Parameter style: Style constants that can be used by a view. + func careKitStyle(_ style: OCKStyler) -> some View { + return self.environment(\.careKitStyle, style) + } +} diff --git a/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAnimationStyler.swift b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAnimationStyler.swift new file mode 100644 index 000000000..990eebe96 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAnimationStyler.swift @@ -0,0 +1,46 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Defines constants for values used during animations. +public protocol OCKAnimationStyler { + var stateChangeDuration: Double { get } +} + +/// Default animation values. +public extension OCKAnimationStyler { + var stateChangeDuration: Double { 0.2 } +} + +/// Concrete object for animation constants. +public struct OCKAnimationStyle: OCKAnimationStyler { + public init() {} +} diff --git a/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAppearanceStyler.swift b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAppearanceStyler.swift new file mode 100644 index 000000000..5cfccd965 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKAppearanceStyler.swift @@ -0,0 +1,66 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Defines constants for view appearance styling. +public protocol OCKAppearanceStyler { + var shadowOpacity1: Float { get } + var shadowRadius1: CGFloat { get } + var shadowOffset1: CGSize { get } + var opacity1: CGFloat { get } + var lineWidth1: CGFloat { get } + + var cornerRadius1: CGFloat { get } + var cornerRadius2: CGFloat { get } + + var borderWidth1: CGFloat { get } + var borderWidth2: CGFloat { get } +} + +/// Default appearance values. +public extension OCKAppearanceStyler { + var shadowOpacity1: Float { 0.15 } + var shadowRadius1: CGFloat { 8 } + var shadowOffset1: CGSize { CGSize(width: 0, height: 2) } + var opacity1: CGFloat { 0.45 } + var lineWidth1: CGFloat { 4 } + + var cornerRadius1: CGFloat { 15 } + var cornerRadius2: CGFloat { 12 } + + var borderWidth1: CGFloat { 2 } + var borderWidth2: CGFloat { 1 } +} + +/// Concrete object for appearance constants. +public struct OCKAppearanceStyle: OCKAppearanceStyler { + public init() {} +} diff --git a/CareKitUI/CareKitUI/Common/Style/Stylers/OCKColorStyler.swift b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKColorStyler.swift new file mode 100644 index 000000000..7308a9c5c --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKColorStyler.swift @@ -0,0 +1,102 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Defines color constants. +public protocol OCKColorStyler { + var label: UIColor { get } + var secondaryLabel: UIColor { get } + var tertiaryLabel: UIColor { get } + + var customBackground: UIColor { get } + var secondaryCustomBackground: UIColor { get } + + var customGroupedBackground: UIColor { get } + var secondaryCustomGroupedBackground: UIColor { get } + var tertiaryCustomGroupedBackground: UIColor { get } + + var separator: UIColor { get } + + var customFill: UIColor { get } + var secondaryCustomFill: UIColor { get } + var tertiaryCustomFill: UIColor { get } + var quaternaryCustomFill: UIColor { get } + + var customBlue: UIColor { get } + + var customGray: UIColor { get } + var customGray2: UIColor { get } + var customGray3: UIColor { get } + var customGray4: UIColor { get } + var customGray5: UIColor { get } + + var black: UIColor { get } + var white: UIColor { get } + var clear: UIColor { get } +} + +/// Defines default values for color constants. +public extension OCKColorStyler { + var label: UIColor { .label } + var secondaryLabel: UIColor { .secondaryLabel } + var tertiaryLabel: UIColor { .tertiaryLabel } + + var customBackground: UIColor { .systemBackground } + var secondaryCustomBackground: UIColor { .secondarySystemBackground } + + var customGroupedBackground: UIColor { .systemGroupedBackground } + var secondaryCustomGroupedBackground: UIColor { .secondarySystemGroupedBackground } + var tertiaryCustomGroupedBackground: UIColor { .tertiarySystemGroupedBackground } + + var separator: UIColor { .separator } + + var customFill: UIColor { .tertiarySystemFill } + var secondaryCustomFill: UIColor { .secondarySystemFill } + var tertiaryCustomFill: UIColor { .tertiarySystemFill } + var quaternaryCustomFill: UIColor { .quaternarySystemFill } + + var customBlue: UIColor { .systemBlue } + + var customGray: UIColor { .systemGray } + var customGray2: UIColor { .systemGray2 } + var customGray3: UIColor { .systemGray3 } + var customGray4: UIColor { .systemGray4 } + var customGray5: UIColor { .systemGray5 } + + var white: UIColor { .white } + var black: UIColor { .black } + var clear: UIColor { .clear } +} + +/// Concrete object for color constants. +public struct OCKColorStyle: OCKColorStyler { + public init() {} +} diff --git a/CareKitUI/CareKitUI/Common/Style/Stylers/OCKDimensionStyler.swift b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKDimensionStyler.swift new file mode 100644 index 000000000..9816e111b --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Style/Stylers/OCKDimensionStyler.swift @@ -0,0 +1,92 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A configurator that defines constants view sizes. +public protocol OCKDimensionStyler { + var separatorHeight: CGFloat { get } + + var lineWidth1: CGFloat { get } + var stackSpacing1: CGFloat { get } + + var imageHeight2: CGFloat { get } + var imageHeight1: CGFloat { get } + + var pointSize3: CGFloat { get } + var pointSize2: CGFloat { get } + var pointSize1: CGFloat { get } + + var buttonHeight3: CGFloat { get } + var buttonHeight2: CGFloat { get } + var buttonHeight1: CGFloat { get } + + var symbolPointSize5: CGFloat { get } + var symbolPointSize4: CGFloat { get } + var symbolPointSize3: CGFloat { get } + var symbolPointSize2: CGFloat { get } + var symbolPointSize1: CGFloat { get } + + var directionalInsets2: NSDirectionalEdgeInsets { get } + var directionalInsets1: NSDirectionalEdgeInsets { get } +} + +/// Default dimension values. +public extension OCKDimensionStyler { + var separatorHeight: CGFloat { 1.0 / UIScreen.main.scale } + + var lineWidth1: CGFloat { 4 } + var stackSpacing1: CGFloat { 8 } + + var imageHeight2: CGFloat { 40 } + var imageHeight1: CGFloat { 150 } + + var pointSize3: CGFloat { 11 } + var pointSize2: CGFloat { 14 } + var pointSize1: CGFloat { 17 } + + var buttonHeight3: CGFloat { 20 } + var buttonHeight2: CGFloat { 50 } + var buttonHeight1: CGFloat { 60 } + + var symbolPointSize5: CGFloat { 8 } + var symbolPointSize4: CGFloat { 12 } + var symbolPointSize3: CGFloat { 16 } + var symbolPointSize2: CGFloat { 20 } + var symbolPointSize1: CGFloat { 30 } + + var directionalInsets2: NSDirectionalEdgeInsets { .init(top: 8, leading: 9, bottom: 8, trailing: 8) } + var directionalInsets1: NSDirectionalEdgeInsets { .init(top: 16, leading: 16, bottom: 16, trailing: 16) } +} + +/// Concrete object for cdimesnion constants. +public struct OCKDimensionStyle: OCKDimensionStyler { + public init() {} +} diff --git a/CareKitUI/CareKitUI/Common/Views/OCKHeaderView.swift b/CareKitUI/CareKitUI/Common/Views/OCKHeaderView.swift new file mode 100644 index 000000000..50c4f5a66 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Views/OCKHeaderView.swift @@ -0,0 +1,264 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A title and detail label. The view can also be configured to show a separator, +/// icon image, and a detail disclosure arrow. +/// +/// +----------------------------------------+ +/// | +----+ | +/// | |icon| Title [detail | +/// | |img | Detail disclosure] | +/// | +----+ | +/// | | +/// | ------------------------------------ | +/// | | +/// +----------------------------------------+ +/// +open class OCKHeaderView: OCKView { + + private enum Constants { + static let spacing: CGFloat = 4 + } + + /// Configuration for a header view. + public struct Configuration { + /// Flag to show a separator under the text in the view. + public var showsSeparator: Bool = false + + /// Flag to show an image on the trailing end of the view. The default image is an arrow. + public var showsDetailDisclosure: Bool = false + + /// Flag to show an image on the leading side of the text in the view. + public var showsIconImage: Bool = false + } + + // MARK: Properties + + /// Vertical stack view that holds the main content. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView(style: .plain) + stackView.axis = .vertical + return stackView + }() + + /// The image on the trialing end of the view. The default image is an arrow. Depending on the configuration, this may be nil. + public let detailDisclosureImage: UIImageView? + + /// Multi-line title label above `detailLabel` + public let titleLabel: OCKLabel = { + let label = OCKLabel(textStyle: .headline, weight: .bold) + label.numberOfLines = 0 + label.animatesTextChanges = true + return label + }() + + /// Multi-line detail label below `titleLabel`. + public let detailLabel: OCKLabel = { + let label = OCKLabel(textStyle: .caption1, weight: .medium) + label.numberOfLines = 0 + label.animatesTextChanges = true + return label + }() + + /// The image on the leading end of the text in the view. Depending on the configuration, this may be ni. + public let iconImageView: UIImageView? + + /// The configuration for the view. + private let configuration: Configuration + + /// Stack view that holds the text content in the header. + private let headerTextStackView = OCKStackView.vertical() + + /// Stack view that holds the content in the header. + private let headerStackView: OCKStackView = { + let stackView = OCKStackView.horizontal() + stackView.alignment = .center + return stackView + }() + + private var iconImageHeightConstraint: NSLayoutConstraint? + + private lazy var iconHeight = OCKAccessibleValue(container: style(), keyPath: \.dimension.imageHeight2) { [weak self] newHeight in + self?.iconImageHeightConstraint?.constant = newHeight + } + + private lazy var detailDisclosurePointSize = OCKAccessibleValue(container: style(), + keyPath: \.dimension.symbolPointSize4) { [detailDisclosureImage] newPointSize in + detailDisclosureImage?.preferredSymbolConfiguration = .init(pointSize: newPointSize) + } + + /// Separator between the header and the body. + private let separatorView: OCKSeparatorView? + + // MARK: Life Cycle + + /// Create the view with a configuration block. The configuration block determines which views the header should show. + public init(configurationHandler: (inout Configuration) -> Void = { _ in }) { + var configuration = Configuration() + configurationHandler(&configuration) + self.configuration = configuration + + iconImageView = configuration.showsIconImage ? OCKHeaderView.makeIconImageView() : nil + detailDisclosureImage = configuration.showsDetailDisclosure ? OCKHeaderView.makeDetailDisclosureImage() : nil + separatorView = configuration.showsSeparator ? OCKSeparatorView() : nil + super.init() + + accessibilityTraits = configuration.showsDetailDisclosure ? [.header, .button] : [.header] + } + + public required init?(coder aDecoder: NSCoder) { + self.configuration = Configuration() + iconImageView = nil + detailDisclosureImage = nil + separatorView = nil + super.init(coder: aDecoder) + } + + // MARK: Methods + + override func setup() { + super.setup() + addSubviews() + constrainSubviews() + styleSubviews() + isAccessibilityElement = true + } + + private func addSubviews() { + [titleLabel, detailLabel].forEach { headerTextStackView.addArrangedSubview($0) } + [headerTextStackView].forEach { headerStackView.addArrangedSubview($0) } + [headerStackView].forEach { contentStackView.addArrangedSubview($0) } + + // Setup dynamic views based on the configuration + if let separatorView = separatorView { contentStackView.addArrangedSubview(separatorView) } + if let detailDisclosureImage = detailDisclosureImage { headerStackView.addArrangedSubview(detailDisclosureImage) } + if let iconImageView = iconImageView { headerStackView.insertArrangedSubview(iconImageView, at: 0) } + + addSubview(contentStackView) + } + + private func styleSubviews() { + let margin = style().dimension.directionalInsets1.top + contentStackView.spacing = margin + headerStackView.spacing = margin / 2.0 + headerTextStackView.spacing = margin / 4.0 + contentStackView.setCustomSpacing(margin, after: headerStackView) + } + + private static func makeIconImageView() -> UIImageView { + let imageView = OCKCircleImageView() + imageView.contentMode = .scaleAspectFill + return imageView + } + + private static func makeDetailDisclosureImage() -> UIImageView { + let image = UIImage(systemName: "chevron.right") + let imageView = UIImageView(image: image) + return imageView + } + + private func constrainSubviews() { + contentStackView.translatesAutoresizingMaskIntoConstraints = false + detailDisclosureImage?.setContentHuggingPriority(.defaultHigh, for: .horizontal) + iconImageView?.setContentHuggingPriority(.defaultHigh, for: .horizontal) + var constraints = contentStackView.constraints(equalTo: self) + + if let imageView = iconImageView { + imageView.translatesAutoresizingMaskIntoConstraints = false + iconImageHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: iconHeight.scaledValue) + constraints += [ + imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), + iconImageHeightConstraint! + ] + } + + NSLayoutConstraint.activate(constraints) + } + + override open func styleDidChange() { + super.styleDidChange() + let style = self.style() + titleLabel.textColor = style.color.label + detailLabel.textColor = style.color.label + + detailDisclosureImage?.tintColor = style.color.customGray3 + iconImageView?.tintColor = style.color.customGray3 + + iconHeight.update(withContainer: style) + detailDisclosurePointSize.update(withContainer: style) + } + + override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { + [iconHeight, detailDisclosurePointSize].forEach { $0.apply() } + } + } +} + +private class OCKCircleImageView: UIImageView { + + private let maskLayer = CAShapeLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + override init(image: UIImage?) { + super.init(image: image) + setup() + } + + override init(image: UIImage?, highlightedImage: UIImage?) { + super.init(image: image, highlightedImage: highlightedImage) + setup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + maskLayer.path = CGPath(ellipseIn: bounds, transform: nil) + maskLayer.backgroundColor = UIColor.black.cgColor + + layer.mask = maskLayer + clipsToBounds = true + } + + override func layoutSubviews() { + super.layoutSubviews() + maskLayer.path = CGPath(ellipseIn: bounds, transform: nil) + } +} diff --git a/CareKitUI/CareKitUI/Common/Views/OCKSeparatorView.swift b/CareKitUI/CareKitUI/Common/Views/OCKSeparatorView.swift new file mode 100644 index 000000000..57c85c477 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Views/OCKSeparatorView.swift @@ -0,0 +1,70 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation +import UIKit + +/// Horizontal separator view. +open class OCKSeparatorView: OCKView { + // MARK: Properties + + private var heightConstraint: NSLayoutConstraint? + + // MARK: Methods + + override func setup() { + super.setup() + constrainSubviews() + styleDidChange() + } + + private func constrainSubviews() { + translatesAutoresizingMaskIntoConstraints = false + heightConstraint = heightAnchor.constraint(equalToConstant: 0) + heightConstraint?.isActive = true + } + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + styleDidChange() + } + + override open func removeFromSuperview() { + super.removeFromSuperview() + styleChildren() + } + + override open func styleDidChange() { + super.styleDidChange() + let cachedStyle = style() + backgroundColor = cachedStyle.color.separator + heightConstraint?.constant = cachedStyle.dimension.separatorHeight + } +} diff --git a/CareKitUI/CareKitUI/Common/Views/OCKStackView.swift b/CareKitUI/CareKitUI/Common/Views/OCKStackView.swift new file mode 100644 index 000000000..79e5161f8 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Views/OCKStackView.swift @@ -0,0 +1,352 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A stack view that supports animating showing/hiding arranged subviews, +/// and has the option of dynamically creating separators when arranged subviews are added. +open class OCKStackView: UIStackView, OCKStylable { + + /// Types of animations that are applied to the stack view. + /// + /// - fade: Animate opacity. + /// - hidden: Animate the isHidden property. + enum Animation { + case fade, hidden + } + + // MARK: Properties + + public var customStyle: OCKStyler? { + didSet { styleChildren() } + } + + /// The style for the stack view. + /// + /// - plain: A normal stack view. + /// - separated: Creates separators between arranges subview. + public enum Style { + case plain, separated + } + + /// The style of the stack view. + public let style: Style + + /// Flag determines if the top and bottom separators are shown when the style of the stack view is separated + public var showsOuterSeparators = true { + didSet { + guard style == .separated else { return } + subviews.first?.removeFromSuperview() + subviews.last?.removeFromSuperview() + } + } + + // MARK: Life Cycle + + /// Create the stack view with a style. A plain style is a typical stack view. A separated + /// style will automatically create separators between arranged subviews whenever they're + /// added. + /// + /// - Parameter style: The style for the stack view. + public init(style: Style = .plain) { + self.style = style + super.init(frame: .zero) + if style == .separated { axis = .vertical } + preservesSuperviewLayoutMargins = true + styleDidChange() + } + + @available(*, unavailable) + public required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + static func vertical() -> OCKStackView { + let stackView = OCKStackView() + stackView.axis = .vertical + return stackView + } + + static func horizontal() -> OCKStackView { + let stackView = OCKStackView() + stackView.axis = .horizontal + return stackView + } + + // MARK: Methods + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + styleDidChange() + } + + override open func removeFromSuperview() { + super.removeFromSuperview() + styleChildren() + } + + open func styleDidChange() {} + + /// The ordered subviews. When the stack view is a separated style, this will not + /// include the separators. + override open var arrangedSubviews: [UIView] { + switch style { + case .plain: + return super.arrangedSubviews + case .separated: + return showsOuterSeparators ? + super.arrangedSubviews.enumerated().filter { $0.0 % 2 != 0 }.map { $0.1 } : // all odd subviews + super.arrangedSubviews.enumerated().filter { $0.0 % 2 == 0 }.map { $0.1 } // all even subviews + } + } + + /// Add a subview to the end of the stack view. If the style is separated, + /// separators will automatically be added. + /// + /// - Parameter view: The view to add. + override open func addArrangedSubview(_ view: UIView) { + switch style { + case .plain: + super.addArrangedSubview(view) + case .separated: + let viewsToAdd = makeSeparators(withView: view) + viewsToAdd.forEach { super.addArrangedSubview($0) } + } + } + + /// Add a subview to the end of the stack view. If the style is separated, + /// separators will automatically be added. Provides the option to animate + /// showing the new view. + /// + /// - Parameters: + /// - view: The view to add. + /// - animated: Flag that determines if the view should animated on screen. + open func addArrangedSubview(_ view: UIView, animated: Bool) { + guard animated else { + addArrangedSubview(view) + return + } + + let viewsToAdd = makeSeparators(withView: view) + viewsToAdd.forEach { + $0.isHidden = true + super.addArrangedSubview($0) + } + toggleViews(viewsToAdd, shouldShow: true, animated: animated) + } + + /// Insert an arranged subview at a particular index in the stack view. If the style is + /// separated, separators will be automatically added. Provides the option to animate + /// showing the new view. + /// + /// - Parameters: + /// - view: The view to add. + /// - stackIndex: Index in the stack view to add the view. + /// - animated: Flag that determines if the view should animated on screen. + open func insertArrangedSubview(_ view: UIView, at stackIndex: Int, animated: Bool) { + guard animated else { + insertArrangedSubview(view, at: stackIndex) + return + } + + let viewsToAdd = makeSeparators(withView: view) + for (index, view) in viewsToAdd.enumerated() { + view.isHidden = true + super.insertArrangedSubview(view, at: index + stackIndex) + } + toggleViews(viewsToAdd, shouldShow: true, animated: animated) + } + + /// Insert an arranged subview at a particular index in the stack view. If the style is + /// separated, separators will automatically be added. + /// + /// - Parameters: + /// - view: The view to add. + /// - stackIndex: Index in the stack view to add the view. + override open func insertArrangedSubview(_ view: UIView, at stackIndex: Int) { + switch style { + case .plain: + super.insertArrangedSubview(view, at: stackIndex) + case .separated: + let viewsToAdd = makeSeparators(withView: view) + for (index, view) in viewsToAdd.enumerated() { + super.insertArrangedSubview(view, at: index + stackIndex) + } + } + } + + /// Remove an arranged subview from the stack view. If the style is separated, + /// the separators will be automatically removed. + /// + /// - Parameter view: The view to remove. + override open func removeArrangedSubview(_ view: UIView) { + switch style { + case .plain: + super.removeArrangedSubview(view) + case .separated: + let viewsToRemove = getSeparators(withView: view) + viewsToRemove.forEach { $0.removeFromSuperview() } + } + } + + /// Remove an arranged subview from the stack view. If the style is separated, + /// the separators will be automatically removed. Option to animated the removal of the + /// view. + /// + /// - Parameters: + /// - view: The view to remove. + /// - animated: Flag that determines if the view removal should be animated. + open func removeArrangedSubview(_ view: UIView, animated: Bool) { + let viewsToRemove = getSeparators(withView: view) + + let removeBlock = { + viewsToRemove.forEach { + $0.removeFromSuperview() + $0.isHidden = false + $0.alpha = 1 + } + } + + guard UIView.areAnimationsEnabled && animated else { + removeBlock() + return + } + + toggleViews(viewsToRemove, shouldShow: false, animated: animated) { complete in + guard complete else { return } + removeBlock() + } + } + + /// Batch view plus any needed separators + private func makeSeparators(withView view: UIView) -> [UIView] { + switch style { + case .plain: return [view] + case .separated: + var views: [UIView] = [] + if super.arrangedSubviews.isEmpty && showsOuterSeparators { + views.append(OCKSeparatorView()) + } else if !super.arrangedSubviews.isEmpty && !showsOuterSeparators { + views.append(OCKSeparatorView()) + } + views.append(view) + if showsOuterSeparators { + views.append(OCKSeparatorView()) + } + return views + } + } + + /// Get the separators included with a view + private func getSeparators(withView view: UIView) -> [UIView] { + switch style { + case .plain: + return [view] + case .separated: + guard let index = subviews.firstIndex(of: view) else { return [] } + + if showsOuterSeparators && index == 0 { + return Array(subviews[..<3]) + } else if !showsOuterSeparators && index == 0 { + return [subviews[0]] + } else { + return Array(subviews[index - 1 ..< index + 1]) + } + } + } + + /// Clear the views in the stack view. + /// + /// - Parameter animated: Flag to animate the removal of the views. + public func clear(animated: Bool = false) { + let removeViewsBlock = { [weak self] in + self?.subviews.forEach { $0.removeFromSuperview() } + } + + guard UIView.areAnimationsEnabled && animated else { + removeViewsBlock() + return + } + + toggleViews(subviews, shouldShow: false, animated: true) { complete in + guard complete else { return } + removeViewsBlock() + } + } + + /// Hide or show the specified views in the stack view. + /// + /// - Parameters: + /// - views: The views to hide or show. + /// - shouldShow: True if the views should be shown, false to hide them. + /// - animated: Animate the visibility of the views. + /// - animations: The particular animations to use when toggling the visibility. + /// - completion: Block to run when the visibility toggling and any animations are complete. + func toggleViews( + _ views: [UIView], + shouldShow: Bool, + animated: Bool, + animations: [Animation] = [.fade, .hidden], + completion: ((Bool) -> Void)? = nil) { + views.forEach { guard $0.superview == self else { return } } + + // skip animation + guard animated else { + views.forEach { $0.isHidden = !shouldShow } + return + } + + var completionWillBeCalled = false + + if animations.contains(.hidden) { + let filteredViews = views.filter { $0.isHidden == shouldShow } // only animated views that are not yet animated + filteredViews.forEach { $0.isHidden = shouldShow } + + UIView.animate(withDuration: style().animation.stateChangeDuration, delay: 0, options: .curveEaseOut, animations: { + filteredViews.forEach { $0.isHidden = !shouldShow } + }, completion: { complete in + if !completionWillBeCalled { completion?(complete) } + completionWillBeCalled = true + }) + } + + if animations.contains(.fade) { + let filteredViews = views.filter { $0.alpha == (shouldShow ? 0 : 1) } // only animated views that are not yet animated + filteredViews.forEach { $0.alpha = shouldShow ? 0 : 1 } + + UIView.animate(withDuration: style().animation.stateChangeDuration, delay: 0, options: .curveEaseOut, animations: { + filteredViews.forEach { $0.alpha = shouldShow ? 1 : 0 } + }, completion: { complete in + if !completionWillBeCalled { completion?(complete) } + completionWillBeCalled = true + }) + } + } +} diff --git a/CareKitUI/CareKitUI/Common/Views/OCKView.swift b/CareKitUI/CareKitUI/Common/Views/OCKView.swift new file mode 100644 index 000000000..8013fad52 --- /dev/null +++ b/CareKitUI/CareKitUI/Common/Views/OCKView.swift @@ -0,0 +1,64 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +open class OCKView: UIView, OCKStylable { + public var customStyle: OCKStyler? { + didSet { styleChildren() } + } + + public init() { + super.init(frame: .zero) + setup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + func setup() { + preservesSuperviewLayoutMargins = true + styleDidChange() + } + + open func styleDidChange() {} + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + styleDidChange() + } + + override open func removeFromSuperview() { + super.removeFromSuperview() + styleChildren() + } +} diff --git a/CareKitUI/CareKitUI/Components/Calendar/OCKCalendarDisplayable.swift b/CareKitUI/CareKitUI/Components/Calendar/OCKCalendarDisplayable.swift new file mode 100644 index 000000000..b6d5a5aaa --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Calendar/OCKCalendarDisplayable.swift @@ -0,0 +1,47 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Any object that can display and handle interactions with a calendar. A calendar displays information about a specified range of days. +public protocol OCKCalendarDisplayable: AnyObject { + /// Handles events related to an `OCKCalendarDisplayable` object. + var delegate: OCKCalendarViewDelegate? { get set } +} + +/// Handles events related to an `OCKCalendarDisplayable` object. +public protocol OCKCalendarViewDelegate: AnyObject { + /// Called when a particular date in the calendar was selected. + /// - Parameter calendarView: The view displaying the calendar. + /// - Parameter date: The date that was selected. + /// - Parameter index: The index of the date that was selected with respect to the collection of days in the current `dateInterval`. + /// - Parameter sender: The sender that initiated the selection. + func calendarView(_ calendarView: UIView & OCKCalendarDisplayable, didSelectDate date: Date, at index: Int, sender: Any?) +} diff --git a/CareKitUI/CareKitUI/Components/Calendar/OCKWeekCalendarView.swift b/CareKitUI/CareKitUI/Components/Calendar/OCKWeekCalendarView.swift new file mode 100644 index 000000000..6f20f56c2 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Calendar/OCKWeekCalendarView.swift @@ -0,0 +1,175 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A horizontal row of seven selectable completion rings and corresponding labels. +/// +/// +--------------------------------------+ +/// | [title] [title] [title] [title] | +/// | | +/// | o o o o o o o o | +/// | o o o o o o ... o o | +/// | o o o o o o o o | +/// +--------------------------------------+ +/// +open class OCKWeekCalendarView: OCKView, OCKCalendarDisplayable { + + // MARK: Properties + + /// The currently selected date in the calendar. + public private (set) var selectedDate = Date() + + /// Handles events related to an `OCKCalendarDisplayable` object. + public weak var delegate: OCKCalendarViewDelegate? + + /// The date interval of the week currently being displayed. + public private (set) var dateInterval = Calendar.current.dateIntervalOfWeek(for: Date()) + + /// The completion ring buttons in the view. There will be one ring for each day in the `dateInterval`. + public private (set) lazy var completionRingButtons: [OCKCompletionRingButton] = { + var rings = [OCKCompletionRingButton]() + let numberOfDays = Calendar.current.dateComponents([.day], from: dateInterval.start, to: dateInterval.end).day! + for _ in 0...numberOfDays { + let ringButton = OCKCompletionRingButton() + ringButton.handlesSelection = false + ringButton.setState(.dimmed, animated: false) + ringButton.addTarget(self, action: #selector(handleSelection(sender:)), for: .touchUpInside) + rings.append(ringButton) + } + return rings + }() + + /// Holds the completion ring buttons in the view. + private let stackView: OCKStackView = { + let stackView = OCKStackView.horizontal() + stackView.distribution = .fillEqually + return stackView + }() + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "dd" + return formatter + }() + + // MARK: - Life Cycle + + /// A view that displays interactable completion rings for each day in a week. The week is computed based on the provided date parameter. + /// - Parameters: + /// - date: Will display the week of the provided date. + public init(weekOfDate date: Date) { + self.dateInterval = Calendar.current.dateIntervalOfWeek(for: date) + selectedDate = date + super.init() + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + // MARK: - Methods + + override func setup() { + super.setup() + addSubview(stackView) + + stackView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate( + stackView.constraints(equalTo: layoutMarginsGuide, directions: [.horizontal]) + + stackView.constraints(equalTo: self, directions: [.vertical]) + ) + + completionRingButtons.forEach { stackView.addArrangedSubview($0) } + completionRingButtons.first?.sendActions(for: .touchUpInside) + updateRingLabels() + } + + private func updateRingLabels() { + let numberOfDays = Calendar.current.dateComponents([.day], from: dateInterval.start, to: dateInterval.end).day! + for index in 0...numberOfDays { + let date = Calendar.current.date(byAdding: .day, value: index, to: dateInterval.start)! + completionRingButtons[index].label.text = dateFormatter.string(from: date) + } + } + + private func dateAt(index: Int) -> Date { + return Calendar.current.date(byAdding: .day, value: index, to: dateInterval.start)! + } + + @objc + private func handleSelection(sender: UIControl) { + for ring in completionRingButtons where ring != sender { + ring.isSelected = false + } + sender.isSelected = true + guard let ringIndex = (completionRingButtons as [UIControl]).firstIndex(of: sender) else { fatalError("Unexpected button") } + selectedDate = dateAt(index: ringIndex) + delegate?.calendarView(self, didSelectDate: selectedDate, at: ringIndex, sender: sender) + } + + /// Select the completion ring that corresponds to the given date. + /// - Parameter date: The date of the ring to select. + public func selectDate(_ date: Date) { + completionRingButtons.first(where: { $0.isSelected })?.isSelected = false + if let ring = completionRingFor(date: date) { + ring.isSelected = true + selectedDate = date + } else { + showDate(date) + selectDate(date) + } + } + + /// Get the completion ring that corresponds to a particular date. + /// - Parameter date: The date that corresponds to the desired completion ring. + /// - Returns: The completion ring that matches the given date. + public func completionRingFor(date: Date) -> OCKCompletionRingButton? { + let offset = abs(Calendar.current.dateComponents([.day], from: dateInterval.start, to: date).day!) + guard offset < completionRingButtons.count else { return nil } + return completionRingButtons[offset] + } + + /// Display the week for the given date. Each ring will correspond to one day in the week. + /// - Parameter date: The date to display. + public func showDate(_ date: Date) { + dateInterval = Calendar.current.dateIntervalOfWeek(for: date) + updateRingLabels() + } + + override open func styleDidChange() { + super.styleDidChange() + let cachedStyle = style() + directionalLayoutMargins = cachedStyle.dimension.directionalInsets1 + stackView.spacing = cachedStyle.dimension.directionalInsets1.leading + } +} diff --git a/CareKitUI/CareKitUI/Components/Calendar/Ring/Buttons/OCKCompletionRingButton.swift b/CareKitUI/CareKitUI/Components/Calendar/Ring/Buttons/OCKCompletionRingButton.swift new file mode 100644 index 000000000..05701f9ef --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Calendar/Ring/Buttons/OCKCompletionRingButton.swift @@ -0,0 +1,135 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A selectable completion ring with an inner check view and a title label. +open class OCKCompletionRingButton: OCKAnimatedButton<OCKStackView> { + + // MARK: Properties + + /// Label above the completion ring. + public let label: OCKLabel = { + let label = OCKCappedSizeLabel(textStyle: .caption1, weight: .semibold) + label.maxFontSize = 20 + return label + }() + + /// A fillable ring view. + public let ring = OCKCompletionRingView() + + /// The completion state of the ring + public private (set) var completionState = CompletionState.empty + + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.alignment = .center + stackView.distribution = .fillProportionally + return stackView + }() + + public enum CompletionState: Equatable { + case dimmed + case empty + case zero + case progress(_ value: CGFloat) + } + + // MARK: - Life cycle + + public init() { + super.init(contentView: contentStackView, handlesSelection: true) + setup() + } + + public required init?(coder: NSCoder) { + super.init(contentView: contentStackView, handlesSelection: true) + setup() + } + + // MARK: - Methods + + override open func styleDidChange() { + super.styleDidChange() + updateRingColors() + ring.lineWidth = style().appearance.lineWidth1 + } + + override open func setStyleForSelectedState(_ isSelected: Bool) { + updateRingColors() + } + + /// Called when the tint color of the view changes. + override open func tintColorDidChange() { + super.tintColorDidChange() + updateRingColors() + applyTintColor() + } + + /// Changes the display state of the button + /// + /// - Parameters: + /// - state: The state that the completion ring button will be set to. + /// - animated: Determines if the change will be animated or instantaneous. + public func setState(_ state: CompletionState, animated: Bool) { + completionState = state + switch state { + case .dimmed: ring.setProgress(0, animated: animated) + case .empty: ring.setProgress(0, animated: animated) + case .zero: ring.setProgress(0.001, animated: animated) + case .progress(let value): ring.setProgress(value, animated: animated) + } + updateRingColors() + } + + private func setup() { + addSubviews() + applyTintColor() + } + + private func updateRingColors() { + let cachedStyle = style() + let grooveStrokeColor = completionState == .dimmed ? cachedStyle.color.customGray3 : cachedStyle.color.customGray + let deselectedLabelColor = completionState == .dimmed ? cachedStyle.color.tertiaryLabel : cachedStyle.color.label + + label.textColor = isSelected ? tintColor : deselectedLabelColor + ring.grooveView.strokeColor = grooveStrokeColor + ring.strokeColor = tintColor + } + + private func addSubviews() { + addSubview(contentStackView) + [label, ring].forEach { contentStackView.addArrangedSubview($0) } + } + + private func applyTintColor() { + ring.strokeColor = tintColor + } +} diff --git a/CareKitUI/CareKitUI/Components/Calendar/Ring/Views/OCKCompletionRingView.swift b/CareKitUI/CareKitUI/Components/Calendar/Ring/Views/OCKCompletionRingView.swift new file mode 100644 index 000000000..d4bcab412 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Calendar/Ring/Views/OCKCompletionRingView.swift @@ -0,0 +1,151 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A fillable ring with an inner checkmark. +open class OCKCompletionRingView: OCKView { + + // MARK: Properties + + override open var intrinsicContentSize: CGSize { + let height = style().dimension.buttonHeight2 + return CGSize(width: height, height: height) + } + + /// The progress value of the ring view. + public var progress: CGFloat { + return ringView.progress + } + + /// The duration for the ring and check view animations. + public var duration: TimeInterval { + get { return ringView.duration } + set { ringView.duration = newValue } + } + + /// The line width of the ring and check views. + public var lineWidth: CGFloat { + get { return ringView.lineWidth } + set { + grooveView.lineWidth = newValue + ringView.lineWidth = newValue + } + } + + /// The stroke color of the ring and check views. + public var strokeColor: UIColor = OCKStyle().color.customBlue { + didSet { + ringView.strokeColor = strokeColor + checkmarkImageView.tintColor = strokeColor + } + } + + private lazy var checkmarkAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.7) + + /// The fillable ring view. + let ringView = OCKRingView() + + /// The groove in which the fillable ring resides. + let grooveView = OCKRingView() + + /// The checkmark image view inside of the ring view. + let checkmarkImageView: UIImageView = { + let image = UIImage(systemName: "checkmark") + let imageView = UIImageView(image: image) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + // MARK: - Methods + + /// Set the progress value for the ring view. The ring will fill accordingly, and if full + /// the checkmark will display. + /// + /// - Parameters: + /// - value: The progress value. + /// - animated: Flag for the ring and check view animations. + public func setProgress(_ value: CGFloat, animated: Bool = true) { + let isComplete = value >= 1.0 + if checkmarkAnimator.isRunning { + checkmarkAnimator.stopAnimation(true) + } + + let animationHandler: () -> Void = { [weak self] in + self?.checkmarkImageView.transform = isComplete ? CGAffineTransform(scaleX: 1, y: 1) : CGAffineTransform(scaleX: 0.1, y: 0.1) + self?.checkmarkImageView.alpha = isComplete ? 1 : 0 + } + + if animated { + checkmarkAnimator.addAnimations(animationHandler) + checkmarkAnimator.startAnimation() + } else { + animationHandler() + } + + ringView.setProgress(value, animated: animated) + } + + override open func styleDidChange() { + super.styleDidChange() + invalidateIntrinsicContentSize() + let style = self.style() + strokeColor = tintColor + grooveView.strokeColor = style.color.customGray3 + checkmarkImageView.preferredSymbolConfiguration = .init(pointSize: style.dimension.symbolPointSize4, weight: .bold) + } + + override func setup() { + super.setup() + grooveView.alpha = 0.25 + grooveView.setProgress(1.0, animated: false) + + setProgress(0, animated: false) + + checkmarkImageView.tintColor = strokeColor + ringView.strokeColor = strokeColor + + [grooveView, ringView, checkmarkImageView].forEach { + addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + var constraints = + grooveView.constraints(equalTo: self) + + ringView.constraints(equalTo: self) + + constraints += [ + checkmarkImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + checkmarkImageView.centerYAnchor.constraint(equalTo: centerYAnchor) + ] + + NSLayoutConstraint.activate(constraints) + } +} diff --git a/CareKitUI/CareKitUI/Components/Calendar/Ring/Views/OCKRingView.swift b/CareKitUI/CareKitUI/Components/Calendar/Ring/Views/OCKRingView.swift new file mode 100644 index 000000000..c4bbd7d93 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Calendar/Ring/Views/OCKRingView.swift @@ -0,0 +1,131 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A fillable progress ring drawing. +class OCKRingView: OCKView { + + // MARK: Properties + + /// The progress of the ring between 0 and 1. The ring will fill based on the value. + private(set) var progress: CGFloat = 1.0 + + private let ringLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.lineCap = .round + layer.fillColor = nil + layer.strokeStart = 0 + return layer + }() + + /// The line width of the ring. + var lineWidth: CGFloat = 10 { + didSet { ringLayer.lineWidth = lineWidth } + } + + /// The stroke color of the ring. + var strokeColor: UIColor = OCKStyle().color.customBlue { + didSet { ringLayer.strokeColor = strokeColor.cgColor } + } + + /// The start angle of the ring to begin drawing. + var startAngle: CGFloat = -.pi / 2 { + didSet { ringLayer.path = ringPath() } + } + + /// The end angle of the ring to end drawing. + var endAngle: CGFloat = 1.5 * .pi { + didSet { ringLayer.path = ringPath() } + } + + /// Duration of the ring's fill animation. + var duration: TimeInterval = 1.0 + + /// The radius oof the ring. + var radius: CGFloat { + return min(bounds.height, bounds.width) / 2 - lineWidth / 2 + } + + // MARK: Life Cycle + + override func layoutSubviews() { + super.layoutSubviews() + configureRing() + } + + // MARK: Methods + + override func setup() { + super.setup() + layer.addSublayer(ringLayer) + styleRingLayer() + } + + /// Set the progress value of the ring. The ring will fill based on the value. + /// + /// - Parameters: + /// - value: Progress value between 0 and 1. + /// - animated: Flag for the fill ring's animation. + func setProgress(_ value: CGFloat, animated: Bool) { + layoutIfNeeded() + + let oldValue = ringLayer.presentation()?.strokeEnd ?? progress + progress = value + ringLayer.strokeEnd = progress + guard animated else { return } + + let path = #keyPath(CAShapeLayer.strokeEnd) + let fill = CABasicAnimation(keyPath: path) + fill.fromValue = oldValue + fill.toValue = value + fill.duration = duration + fill.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + ringLayer.add(fill, forKey: "fill") + } + + private func styleRingLayer() { + strokeColor = tintColor + ringLayer.strokeColor = strokeColor.cgColor + ringLayer.strokeEnd = min(progress, 1.0) + ringLayer.lineWidth = lineWidth + } + + private func configureRing() { + ringLayer.frame = bounds + ringLayer.path = ringPath() + } + + private func ringPath() -> CGPath { + let center = CGPoint(x: bounds.origin.x + frame.width / 2.0, y: bounds.origin.y + frame.height / 2.0) + let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + return circlePath.cgPath + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKBarPlotView.swift b/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKBarPlotView.swift new file mode 100644 index 000000000..ce7d6d14c --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKBarPlotView.swift @@ -0,0 +1,77 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A graph that displays one or more vertical bar plots. +class OCKBarPlotView: OCKGradientPlotView<OCKBarLayer> { + + override func resetLayers() { + let graphRect = graphBounds() + let offsets = computeBarOffsets() + resolveNumberOfLayers() + dataSeries.enumerated().forEach { index, series in + let layer = seriesLayers[index] + layer.dataPoints = series.dataPoints + layer.horizontalOffset = offsets[index] + layer.startColor = series.gradientStartColor ?? tintColor + layer.endColor = series.gradientEndColor ?? tintColor + layer.barWidth = series.size + layer.setPlotBounds(rect: graphRect) + layer.frame = bounds + } + } + + // Adjust the x coordinates of the data series so that the bar charts line up next to one another. + // This does take into account that bars might have different widths. + private func computeBarOffsets() -> [CGFloat] { + let barSizes = dataSeries.map { $0.size } + let groupWidth = barSizes.reduce(0, +) + let offset = -groupWidth / 2 + let adjustments = barSizes.enumerated().map { seriesIndex, size -> CGFloat in + let combinedWidthOfPreviousBars = barSizes[0..<seriesIndex].reduce(0, +) + let shift = offset + combinedWidthOfPreviousBars + size / 2 + return shift + } + return adjustments + } + + private func resolveNumberOfLayers() { + while seriesLayers.count < dataSeries.count { + let newLayer = OCKBarLayer() + seriesLayers.append(newLayer) + layer.addSublayer(newLayer) + } + while seriesLayers.count > dataSeries.count { + let oldLayer = seriesLayers.removeLast() + oldLayer.removeFromSuperlayer() + } + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKGradientPlotView.swift b/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKGradientPlotView.swift new file mode 100644 index 000000000..eaab20e19 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKGradientPlotView.swift @@ -0,0 +1,106 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +private let accessibilityElementBoundingBoxSize = CGSize(width: 10, height: 10) + +/// This is an abstract base class for plots that use a gradient mask. +class OCKGradientPlotView<LayerType: OCKCartesianCoordinatesLayer> : UIView, OCKGradientPlotable, OCKMultiPlotable { + + let gradientLayer = CAGradientLayer() + let pointsLayer = CAShapeLayer() + + func makePath(points: [CGPoint]) -> CGPath { + return UIBezierPath().cgPath + } + + var dataSeries: [OCKDataSeries] = [] { + didSet { resetLayers() } + } + + var xMinimum: CGFloat? { + didSet { seriesLayers.forEach { $0.xMinimum = xMinimum } } + } + + var xMaximum: CGFloat? { + didSet { seriesLayers.forEach { $0.xMaximum = xMaximum } } + } + + var yMinimum: CGFloat? { + didSet { seriesLayers.forEach { $0.yMinimum = yMinimum } } + } + + var yMaximum: CGFloat? { + didSet { seriesLayers.forEach { $0.yMaximum = yMaximum } } + } + + var seriesLayers: [LayerType] = [] + + override var intrinsicContentSize: CGSize { + return CGSize(width: 200, height: 75) + } + + override func layoutSubviews() { + super.layoutSubviews() + seriesLayers.forEach { $0.frame = bounds } + resetAccessibilityElements() + } + + func resetLayers() { + fatalError("This method must be overridden in subclasses!") + } + + func resetAccessibilityElements() { + accessibilityElements = [] + + dataSeries.enumerated().forEach { seriesIndex, series in + series.dataPoints.enumerated().forEach { pointIndex, point in + + let pointInViewSpace = seriesLayers[seriesIndex].convert(graphSpacePoints: [point]).first! + let axOrigin = CGPoint(x: pointInViewSpace.x - accessibilityElementBoundingBoxSize.width / 2, + y: pointInViewSpace.y - accessibilityElementBoundingBoxSize.height / 2) + let axFrame = CGRect(origin: axOrigin, size: accessibilityElementBoundingBoxSize) + + // Create the labels for this data point + let useProvidedLabel = pointIndex < series.accessibilityLabels.count + let label = useProvidedLabel ? series.accessibilityLabels[pointIndex] : "\(series.title), \(point.x), \(point.y)" + + // Create an accessibility element for this singular data point + let element = UIAccessibilityElement(accessibilityContainer: self) + element.accessibilityFrameInContainerSpace = axFrame + element.accessibilityLabel = label + element.accessibilityTraits = UIAccessibilityTraits.staticText + + accessibilityElements?.append(element) + } + } + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKLinePlotView.swift b/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKLinePlotView.swift new file mode 100644 index 000000000..8ff6ccc3b --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKLinePlotView.swift @@ -0,0 +1,77 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Interactable line graph. The difference between this and `OCKLineGraphLayer` is that this has the +/// ability to respond to touches +class OCKLinePlotView: OCKGradientPlotView<OCKLineLayer> { + + override func resetLayers() { + let graphRect = graphBounds() + let offsets = computeLineOffsets() + resolveNumberOfLayers() + dataSeries.enumerated().forEach { index, series in + let layer = seriesLayers[index] + layer.dataPoints = series.dataPoints + layer.offset = offsets[index] + layer.startColor = series.gradientStartColor ?? tintColor + layer.endColor = series.gradientEndColor ?? tintColor + layer.lineWidth = series.size + layer.setPlotBounds(rect: graphRect) + layer.frame = bounds + } + } + + // Adjust the x coordinates of the data series so that two identical lines are slightly offset, so as to be distinguishable. + private func computeLineOffsets() -> [CGSize] { + guard !dataSeries.isEmpty else { return [] } + let spacing: CGFloat = 1.0 + let totalWidth = spacing * CGFloat(dataSeries.count - 1) + let startOffset = -totalWidth / 2 + var offsets = [CGSize]() + for index in 0..<dataSeries.count { + offsets.append(CGSize(width: startOffset + spacing * CGFloat(index), height: 0)) + } + return offsets + } + + private func resolveNumberOfLayers() { + while seriesLayers.count < dataSeries.count { + let newLayer = OCKLineLayer() + seriesLayers.append(newLayer) + layer.addSublayer(newLayer) + } + while seriesLayers.count > dataSeries.count { + let oldLayer = seriesLayers.removeLast() + oldLayer.removeFromSuperlayer() + } + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKScatterPlotView.swift b/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKScatterPlotView.swift new file mode 100644 index 000000000..6f1fb78c4 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Gradient Plots/OCKScatterPlotView.swift @@ -0,0 +1,76 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A graph view that shows one or more scatter plots +class OCKScatterPlotView: OCKGradientPlotView<OCKScatterLayer> { + + override func resetLayers() { + let graphRect = graphBounds() + let offsets = computeLineOffsets() + resolveNumberOfLayers() + dataSeries.enumerated().forEach { index, series in + let layer = seriesLayers[index] + layer.dataPoints = series.dataPoints + layer.offset = offsets[index] + layer.startColor = series.gradientStartColor ?? tintColor + layer.endColor = series.gradientEndColor ?? tintColor + layer.markerSize = series.size + layer.setPlotBounds(rect: graphRect) + layer.frame = bounds + } + } + + // Adjust the x coordinates of the data series so that two identical lines are slightly offset, so as to be distinguishable. + private func computeLineOffsets() -> [CGSize] { + guard !dataSeries.isEmpty else { return [] } + let spacing: CGFloat = 1.0 + let totalWidth = spacing * CGFloat(dataSeries.count - 1) + let startOffset = -totalWidth / 2 + var offsets = [CGSize]() + for index in 0..<dataSeries.count { + offsets.append(CGSize(width: startOffset + spacing * CGFloat(index), height: 0)) + } + return offsets + } + + private func resolveNumberOfLayers() { + while seriesLayers.count < dataSeries.count { + let newLayer = OCKScatterLayer() + seriesLayers.append(newLayer) + layer.addSublayer(newLayer) + } + while seriesLayers.count > dataSeries.count { + let oldLayer = seriesLayers.removeLast() + oldLayer.removeFromSuperlayer() + } + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Layers/OCKBarLayer.swift b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKBarLayer.swift new file mode 100644 index 000000000..f6c0114fa --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKBarLayer.swift @@ -0,0 +1,105 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +class OCKBarLayer: OCKCartesianCoordinatesLayer, OCKGradientPlotable { + let gradientLayer = CAGradientLayer() + let pointsLayer = CAShapeLayer() + + var startColor: UIColor = OCKStyle().color.customGray { + didSet { gradientLayer.colors = [startColor.cgColor, endColor.cgColor] } + } + + var endColor: UIColor = OCKStyle().color.customGray { + didSet { gradientLayer.colors = [startColor.cgColor, endColor.cgColor] } + } + + var barWidth: CGFloat = 10 { + didSet { setNeedsLayout() } + } + + var horizontalOffset: CGFloat = 0 { + didSet { setNeedsLayout() } + } + + override init() { + super.init() + setupSublayers() + } + + override init(layer: Any) { + super.init(layer: layer) + setupSublayers() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSublayers() + } + + private func setupSublayers() { + gradientLayer.colors = [startColor.cgColor, endColor.cgColor] + gradientLayer.startPoint = CGPoint(x: 0.5, y: 1) + gradientLayer.endPoint = CGPoint(x: 0.5, y: 0) + gradientLayer.mask = pointsLayer + addSublayer(gradientLayer) + + pointsLayer.fillColor = OCKStyle().color.customGray.cgColor + pointsLayer.strokeColor = nil + } + + override func layoutSublayers() { + super.layoutSublayers() + drawPoints(points) + } + + override func animateInGraphCoordinates(from oldPoints: [CGPoint], to newPoints: [CGPoint]) { + let grow = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path)) + grow.fromValue = pointsLayer.presentation()?.path ?? makePath(points: oldPoints) + grow.toValue = makePath(points: newPoints) + grow.timingFunction = CAMediaTimingFunction(name: .easeOut) + grow.duration = 1.0 + pointsLayer.add(grow, forKey: "grow") + } + + func makePath(points: [CGPoint]) -> CGPath { + let path = UIBezierPath() + for point in points { + let origin = CGPoint(x: point.x - barWidth / 2 + horizontalOffset, y: point.y) + let size = CGSize(width: barWidth, height: bounds.height - point.y) + let rectangle = CGRect(origin: origin, size: size) + let cornerRadii = CGSize(width: barWidth / 4, height: barWidth / 4) + let rectPath = UIBezierPath(roundedRect: rectangle, byRoundingCorners: [.topLeft, .topRight], cornerRadii: cornerRadii) + path.append(rectPath) + } + return path.cgPath + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Layers/OCKCartesianCoordinatesLayer.swift b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKCartesianCoordinatesLayer.swift new file mode 100644 index 000000000..f685a131f --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKCartesianCoordinatesLayer.swift @@ -0,0 +1,207 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Base class that provides graph coordinates for use in plotting numeric data. +class OCKCartesianCoordinatesLayer: CALayer, OCKSinglePlotable { + static var defaultWidth: CGFloat { return 10.0 } + static var defaultHeight: CGFloat { return 10.0 } + + /// Data points for the graph. + var dataPoints: [CGPoint] = [] { + didSet { + orderedDataPoints = dataPoints.sorted { $0.x < $1.x } + let oldPoints = points + points = convert(graphSpacePoints: orderedDataPoints) + setNeedsLayout() + + // Don't animate if the data sets don't match. Prevents weird scaling animation the very first time data is set. + if oldPoints.count == points.count { + animateInGraphCoordinates(from: oldPoints, to: points) + } + } + } + + func animateInGraphCoordinates(from oldPoints: [CGPoint], to newPoints: [CGPoint]) {} + + func setPlotBounds(rect: CGRect) { + xMinimum = rect.minX + xMaximum = rect.maxX + yMinimum = rect.minY + yMaximum = rect.maxY + } + + /// Minimum x value dislpayed in the graph. + var xMinimum: CGFloat? { + didSet { setNeedsLayout() } + } + + /// Maximum x value displayed in the graph. + var xMaximum: CGFloat? { + didSet { setNeedsLayout() } + } + + /// Minimum y values displayed in the graph. + var yMinimum: CGFloat? { + didSet { setNeedsLayout() } + } + + /// Maximum y value displayed in the graph. + var yMaximum: CGFloat? { + didSet { setNeedsLayout() } + } + + /// Default width of the graph. + var defaultWidth: CGFloat { + return 100 + } + + /// Default height of the graph. + var defaultHeight: CGFloat { + return 100 + } + + private (set) var points: [CGPoint] = [] + private (set) var orderedDataPoints: [CGPoint] = [] + + /// Create an instance if a graoh layer. + override init() { + super.init() + setNeedsLayout() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setNeedsLayout() + } + + override init(layer: Any) { + super.init(layer: layer) + setNeedsLayout() + } + + override func layoutSublayers() { + super.layoutSublayers() + points = convert(graphSpacePoints: orderedDataPoints) + } + + /// Get the rectangle that will be displayed in graph space. + /// + /// - Returns: The rectangle in graph space. + func graphBounds() -> CGRect { + let xCoords = dataPoints.map { $0.x } + let xMin = xMinimum ?? xCoords.min() ?? 0 + let xMax = xMaximum ?? xCoords.max() ?? defaultWidth + let width = xMax - xMin + + let yCoords = dataPoints.map { $0.y } + let yMin = yMinimum ?? yCoords.min() ?? 0 + let yMax = yMaximum ?? yCoords.max() ?? defaultHeight + let height = yMax - yMin + + return CGRect(x: xMin, y: yMin, width: width, height: height) + } + + /// Convert a CGPoint to graph coordinates. + /// + /// - Parameter point: The point in screen space. + /// - Returns: The point in graph space. + func graphCoordinates(at point: CGPoint) -> CGPoint { + return convert(viewSpacePoints: [point]).first! + } + + /// Distance is calculated only in the horizontal direction + + /// Get the closest graph coordinates for a given view coordinate. + /// + /// - Parameter location: The coordinate in screen space. + /// - Returns: The closest coordinate in graph space, and the correspoing screen coordinate. + func closestDataPoint(toViewCoordinates location: CGPoint) -> (viewCoordinates: CGPoint, graphCoordinates: CGPoint)? { + guard !orderedDataPoints.isEmpty else { return nil } + let graphCoords = graphCoordinates(at: location) + let distances = orderedDataPoints.map { point -> CGFloat in + let dx = graphCoords.x - point.x + let distance = sqrt(pow(dx, 2)) + return distance + } + let minDistance = distances.min()! + let indexOfMin = distances.firstIndex(of: minDistance)! + let closestViewCoords = points[indexOfMin] + let closestGraphCoords = orderedDataPoints[indexOfMin] + return (closestViewCoords, closestGraphCoords) + } + + private struct Bounds { + let lower: CGFloat + let upper: CGFloat + } + + /// Converts points from graph space to view space + func convert(graphSpacePoints points: [CGPoint]) -> [CGPoint] { + let graphRect = graphBounds() + let viewRect = bounds + + let graphXBounds = Bounds(lower: graphRect.minX, upper: graphRect.maxX) + let viewXBounds = Bounds(lower: viewRect.minX, upper: viewRect.maxX) + let xMapper = make2DMapper(from: graphXBounds, to: viewXBounds) + + let graphYBounds = Bounds(lower: graphRect.minY, upper: graphRect.maxY) + let viewYBounds = Bounds(lower: viewRect.minY, upper: viewRect.maxY) + let yMapper = make2DMapper(from: graphYBounds, to: viewYBounds) + + return points.map { CGPoint(x: xMapper($0.x), y: viewRect.height - yMapper($0.y)) } + } + + /// Converts points from view space to graph space + private func convert(viewSpacePoints points: [CGPoint]) -> [CGPoint] { + let graphRect = graphBounds() + let viewRect = bounds + + let graphXBounds = Bounds(lower: graphRect.minX, upper: graphRect.maxX) + let viewXBounds = Bounds(lower: viewRect.minX, upper: viewRect.maxX) + let xMapper = make2DMapper(from: viewXBounds, to: graphXBounds) + + let graphYBounds = Bounds(lower: graphRect.minY, upper: graphRect.maxY) + let viewYBounds = Bounds(lower: viewRect.maxY, upper: viewRect.minY) + let yMapper = make2DMapper(from: viewYBounds, to: graphYBounds) + + return points.map { CGPoint(x: xMapper($0.x), y: yMapper($0.y)) } + } + + /// Creates a method that linearly interpolate between two sets of bounds + private func make2DMapper(from start: Bounds, to end: Bounds) -> ((CGFloat) -> CGFloat) { + let rise = end.upper - end.lower + let run = start.upper - start.lower + let slope = rise / run + let intercept = end.lower - start.lower * slope + return { slope * $0 + intercept } + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Layers/OCKGridLayer.swift b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKGridLayer.swift new file mode 100644 index 000000000..61175c4ab --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKGridLayer.swift @@ -0,0 +1,175 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// This layer shows horizontal grid lines and is intended to be added as a background to various kinds of graphs. +class OCKGridLayer: OCKCartesianCoordinatesLayer { + private enum Constants { + static let margin: CGFloat = 16 + } + + /// The number of vertical lines in the grid. + var numberOfVerticalDivisions = 4 { + didSet { setNeedsLayout() } + } + + /// The color of the grid lines. + var gridLineColor: UIColor = OCKStyle().color.customGray { + didSet { + bottomGridLine.strokeColor = gridLineColor.cgColor + gridLines.strokeColor = gridLineColor.cgColor + } + } + + var gridLineWidth: CGFloat = 0.7 { + didSet { + gridLines.lineWidth = gridLineWidth + bottomGridLine.lineWidth = gridLineWidth + } + } + + var gridLineOpacity: CGFloat = 0.25 { + didSet { + gridLines.opacity = Float(gridLineOpacity) + bottomGridLine.opacity = Float(gridLineOpacity) + } + } + + var fontSize: CGFloat = 10 { + didSet { setNeedsLayout() } + } + + let gridLines = CAShapeLayer() + let bottomGridLine = CAShapeLayer() + let topValueLayer = CATextLayer() + let middleValueLayer = CATextLayer() + + /// Create an instance of a grid layer. + override init() { + super.init() + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + /// Create an instance of a grid layer by specifying the layer class. + /// + /// - Parameter layer: Layer class to use as this object's layer. + override init(layer: Any) { + super.init(layer: layer) + setup() + } + + func setup() { + addSublayer(gridLines) + addSublayer(bottomGridLine) + addSublayer(topValueLayer) + addSublayer(middleValueLayer) + } + + override func layoutSublayers() { + super.layoutSublayers() + redraw() + } + + private func redraw() { + drawBottomGridLine() + drawMiddleGridLines() + drawTopValue() + drawMiddleValue() + } + + private func drawBottomGridLine() { + bottomGridLine.path = pathForBottomLine().cgPath + bottomGridLine.lineCap = .round + bottomGridLine.lineWidth = gridLineWidth + bottomGridLine.strokeColor = gridLineColor.cgColor + bottomGridLine.opacity = Float(gridLineOpacity) + bottomGridLine.fillColor = nil + bottomGridLine.frame = bounds + } + + private func drawMiddleGridLines() { + gridLines.path = pathForMiddleLines().cgPath + gridLines.lineDashPattern = [2, 2] + gridLines.lineWidth = gridLineWidth + gridLines.strokeColor = gridLineColor.cgColor + gridLines.opacity = Float(gridLineOpacity) + gridLines.fillColor = nil + gridLines.frame = bounds + } + + private func drawTopValue() { + topValueLayer.contentsScale = UIScreen.main.scale + topValueLayer.string = "\(graphBounds().height)" + topValueLayer.foregroundColor = gridLineColor.cgColor + topValueLayer.fontSize = fontSize + topValueLayer.frame = CGRect(origin: CGPoint(x: Constants.margin, y: 0), size: CGSize(width: 100, height: 44)) + } + + private func drawMiddleValue() { + middleValueLayer.contentsScale = UIScreen.main.scale + middleValueLayer.string = "\(graphBounds().height / 2)" + middleValueLayer.foregroundColor = gridLineColor.cgColor + middleValueLayer.fontSize = fontSize + middleValueLayer.frame = CGRect(origin: CGPoint(x: Constants.margin, y: bounds.height / 2), size: CGSize(width: 100, height: 44)) + } + + private func pathForBottomLine() -> UIBezierPath { + let bottomLeft = CGPoint(x: 0, y: bounds.height - gridLineWidth / 2) + let bottomRight = CGPoint(x: bounds.width, y: bounds.height - gridLineWidth / 2) + let path = UIBezierPath() + path.move(to: bottomLeft) + path.addLine(to: bottomRight) + return path + } + + private func pathForMiddleLines() -> UIBezierPath { + let path = UIBezierPath() + for heigth in middleLineHeights() { + path.move(to: CGPoint(x: 0, y: heigth)) + path.addLine(to: CGPoint(x: bounds.width, y: heigth)) + } + return path + } + + private func middleLineHeights() -> [CGFloat] { + let spacing = bounds.height / CGFloat(numberOfVerticalDivisions) + var heights = [CGFloat](repeating: 0, count: numberOfVerticalDivisions) + for index in 0..<numberOfVerticalDivisions { + heights[index] = spacing * CGFloat(index) + } + return heights + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Layers/OCKLineLayer.swift b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKLineLayer.swift new file mode 100644 index 000000000..3dcaa1fc0 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKLineLayer.swift @@ -0,0 +1,153 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// This layer displays a single line graph. Multiple line graph layers can be stacked to +/// generate plots with more than one data series. +class OCKLineLayer: OCKCartesianCoordinatesLayer { + var startColor: UIColor = OCKStyle().color.customGray { + didSet { gradient.colors = [startColor.cgColor, endColor.cgColor] } + } + + var endColor: UIColor = OCKStyle().color.customGray { + didSet { gradient.colors = [startColor.cgColor, endColor.cgColor] } + } + + var outlineColor: UIColor? = nil { + didSet { outline.strokeColor = outlineColor?.cgColor } + } + + var lineWidth: CGFloat = 4 { + didSet { line.lineWidth = lineWidth } + } + + var offset: CGSize = .zero { + didSet { setNeedsLayout() } + } + + let gradient = CAGradientLayer() + let line = CAShapeLayer() + + /// The layer for the ooutline around the line connecting the data points. + let outline = CAShapeLayer() + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSublayers() + } + + override init() { + super.init() + setupSublayers() + } + + override init(layer: Any) { + super.init(layer: layer) + setupSublayers() + } + + override func layoutSublayers() { + super.layoutSublayers() + drawLine() + } + + override func animateInGraphCoordinates(from oldPoints: [CGPoint], to newPoints: [CGPoint]) { + animateLine(from: oldPoints, to: newPoints) + animateOutline(from: oldPoints, to: newPoints) + } + + private func setupSublayers() { + addSublayer(outline) + addSublayer(gradient) + } + + private func drawLine() { + // The gradient must be made wider so that the line doesn't get clipped if at the edge + let offset = lineWidth * 2 + gradient.frame = CGRect(x: -offset, y: 0, width: bounds.width + 2 * offset, height: bounds.height) + gradient.mask = line + gradient.startPoint = CGPoint(x: 0.5, y: 1) + gradient.endPoint = CGPoint(x: 0.5, y: 0) + gradient.colors = [startColor.cgColor, endColor.cgColor] + + // The line is a sublayer of the gradient, so it needs to be shifted right as far as the gradient is + // shifted to the left so that it lines up properly with the outline layer, which is not a sublayer + // of the gradient layer. + line.path = linePath(for: points) + line.lineWidth = lineWidth + line.lineCap = .round + line.lineJoin = .round + line.strokeColor = OCKStyle().color.customGray.cgColor + line.fillColor = nil + line.frame = bounds.applying(CGAffineTransform(translationX: offset, y: 0)) + + outline.path = linePath(for: points) + outline.lineWidth = lineWidth + 2 + outline.lineCap = .round + outline.lineJoin = .round + outline.strokeColor = outlineColor?.cgColor + outline.fillColor = nil + outline.frame = bounds + } + + /// Points should be given in view coordinates + private func linePath(for points: [CGPoint]) -> CGPath { + let path = UIBezierPath() + guard let firstPoint = points.first else { return path.cgPath } + path.move(to: firstPoint) + for (index, point) in points.enumerated() { + guard index > 0 else { continue } + let adjustedPoint = CGPoint(x: point.x + offset.width, y: point.y + offset.height) + path.addLine(to: adjustedPoint) + + if index == points.count - 1 { + path.addArc(withCenter: adjustedPoint, radius: lineWidth, startAngle: 0, endAngle: 2 * .pi, clockwise: true) + } + } + return path.cgPath + } + + private func animateLine(from oldPoints: [CGPoint], to newPoints: [CGPoint]) { + let grow = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path)) + grow.fromValue = line.presentation()?.path ?? linePath(for: oldPoints) + grow.toValue = linePath(for: newPoints) + grow.duration = 1.0 + line.add(grow, forKey: "grow") + } + + private func animateOutline(from oldPoints: [CGPoint], to newPoints: [CGPoint]) { + let grow = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path)) + grow.fromValue = outline.presentation()?.path ?? linePath(for: oldPoints) + grow.toValue = linePath(for: newPoints) + grow.duration = 1.0 + outline.add(grow, forKey: "grow") + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Layers/OCKScatterLayer.swift b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKScatterLayer.swift new file mode 100644 index 000000000..d4bbfca33 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Layers/OCKScatterLayer.swift @@ -0,0 +1,101 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +class OCKScatterLayer: OCKCartesianCoordinatesLayer, OCKGradientPlotable { + let gradientLayer = CAGradientLayer() + let pointsLayer = CAShapeLayer() + + var markerSize: CGFloat = 3.0 { + didSet { pointsLayer.path = makePath(points: points) } + } + + var startColor: UIColor = OCKStyle().color.customGray { + didSet { gradientLayer.colors = [startColor.cgColor, endColor.cgColor] } + } + + var endColor: UIColor = OCKStyle().color.customGray { + didSet { gradientLayer.colors = [startColor.cgColor, endColor.cgColor] } + } + + var offset: CGSize = .zero { + didSet { setNeedsLayout() } + } + + override init() { + super.init() + setupSublayers() + } + + override init(layer: Any) { + super.init(layer: layer) + setupSublayers() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSublayers() + } + + private func setupSublayers() { + pointsLayer.strokeColor = nil + addSublayer(pointsLayer) + + gradientLayer.colors = [startColor.cgColor, endColor.cgColor] + gradientLayer.startPoint = CGPoint(x: 0.5, y: 1) + gradientLayer.endPoint = CGPoint(x: 0.5, y: 0) + gradientLayer.mask = pointsLayer + addSublayer(gradientLayer) + } + + override func layoutSublayers() { + super.layoutSublayers() + drawPoints(points) + } + + override func animateInGraphCoordinates(from oldPoints: [CGPoint], to newPoints: [CGPoint]) { + let grow = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path)) + grow.fromValue = pointsLayer.presentation()?.path ?? makePath(points: oldPoints) + grow.toValue = makePath(points: newPoints) + grow.duration = 1.0 + pointsLayer.add(grow, forKey: "grow") + } + + func makePath(points: [CGPoint]) -> CGPath { + let path = UIBezierPath() + points.forEach { point in + let adjustedPoint = CGPoint(x: point.x + offset.width, y: point.y + offset.height) + path.move(to: adjustedPoint) + path.addArc(withCenter: adjustedPoint, radius: markerSize, startAngle: 0, endAngle: 2 * .pi, clockwise: true) + } + return path.cgPath + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/OCKCartesianChartView.swift b/CareKitUI/CareKitUI/Components/Charts/OCKCartesianChartView.swift new file mode 100644 index 000000000..59afcddd7 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/OCKCartesianChartView.swift @@ -0,0 +1,119 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation +import UIKit + +open class OCKCartesianChartView: OCKView, OCKChartDisplayable { + + // MARK: Properties + + private let contentView = OCKView() + + private let headerContainerView = UIView() + + /// Handles events related to an `OCKChartDisplayable` object. + public weak var delegate: OCKChartViewDelegate? + + /// Vertical stack view that + public let contentStackView: OCKStackView = { + let stackView = OCKStackView() + stackView.axis = .vertical + return stackView + }() + + /// A default `OCKHeaderView`. + public let headerView = OCKHeaderView() + + /// The main content of the view. + public let graphView: OCKCartesianGraphView + + // MARK: - Life Cycle + + /// Create a chart with a specified type. Available charts include bar, plot, and scatter. + /// + /// - Parameter type: The type of the chart. + public init(type: OCKCartesianGraphView.PlotType) { + graphView = OCKCartesianGraphView(type: type) + super.init() + setup() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Methods + + override func setup() { + super.setup() + addSubviews() + constrainSubviews() + setupGestures() + } + + private func setupGestures() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapHeader)) + headerView.addGestureRecognizer(tapGesture) + } + + private func addSubviews() { + addSubview(contentView) + contentView.addSubview(contentStackView) + headerContainerView.addSubview(headerView) + [headerContainerView, graphView].forEach { contentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [contentView, contentStackView, headerView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate( + contentStackView.constraints(equalTo: self, directions: [.horizontal]) + + contentStackView.constraints(equalTo: layoutMarginsGuide, directions: [.vertical]) + + headerView.constraints(equalTo: headerContainerView.layoutMarginsGuide, directions: [.horizontal]) + + headerView.constraints(equalTo: headerContainerView, directions: [.vertical]) + + contentStackView.constraints(equalTo: contentView)) + } + + @objc + private func didTapHeader() { + delegate?.didSelectChartView(self) + } + + 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 + headerContainerView.directionalLayoutMargins = cachedStyle.dimension.directionalInsets1 + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/OCKCartesianGraphView.swift b/CareKitUI/CareKitUI/Components/Charts/OCKCartesianGraphView.swift new file mode 100644 index 000000000..f220613fc --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/OCKCartesianGraphView.swift @@ -0,0 +1,201 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Displays a `OCKMultiGraphableView` above an axis. The initializer takes an enum `PlotType` that allows you to choose from +/// several common graph types. +/// +/// +-------------------------------------------------------+ +/// | | +/// | [title] | +/// | [detail] | +/// | | +/// | [graph] | +/// | | +/// +-------------------------------------------------------+ +/// +open class OCKCartesianGraphView: OCKView, OCKMultiPlotable { + + /// An enumerator specifying the types of plots this view can display. + public enum PlotType: String, CaseIterable { + case line + case scatter + case bar + } + + // MARK: Properties + + /// The data points displayed in the graph. + public var dataSeries: [OCKDataSeries] { + get { return plotView.dataSeries } + set { + updateScaling(for: newValue) + plotView.dataSeries = newValue + legend.setDataSeries(newValue) + } + } + + /// The labels for the horizontal axis. + public var horizontalAxisMarkers: [String] = [] { + didSet { axisView.axisMarkers = horizontalAxisMarkers } + } + + /// Get the bounds of the graph. + /// + /// - Returns: The bounds of the graph. + public func graphBounds() -> CGRect { + return plotView.graphBounds() + } + + /// The minimum x value in the graph. + public var xMinimum: CGFloat? { + get { return plotView.xMinimum } + set { + plotView.xMinimum = newValue + gridView.xMinimum = newValue + } + } + + /// The maximum x value in the graph. + public var xMaximum: CGFloat? { + get { return plotView.xMaximum } + set { + plotView.xMaximum = newValue + gridView.xMaximum = newValue + } + } + + /// The minimum y value in the graph. + public var yMinimum: CGFloat? { + get { return plotView.yMinimum } + set { + plotView.yMinimum = newValue + gridView.yMinimum = newValue + } + } + + /// The maximum y value in the graph. + public var yMaximum: CGFloat? { + get { return plotView.yMaximum } + set { + plotView.yMaximum = newValue + gridView.yMaximum = newValue + } + } + + /// The index of the selected label in the x-axis. + public var selectedIndex: Int? { + get { return axisView.selectedIndex } + set { axisView.selectedIndex = newValue } + } + + private let gridView: OCKGridView + private let plotView: UIView & OCKMultiPlotable + private let axisView: OCKGraphAxisView + private let axisHeight: CGFloat = 44 + private let horizontalPlotPadding: CGFloat = 50 + private let legend = OCKGraphLegendView() + + // MARK: - Life Cycle + + /// Create a graph view with the specified style. + /// + /// - Parameter plotType: The style of the graph view. + public init(type: PlotType) { + self.gridView = OCKGridView() + self.axisView = OCKGraphAxisView() + self.plotView = { + switch type { + case .line: return OCKLinePlotView() + case .scatter: return OCKScatterPlotView() + case .bar: return OCKBarPlotView() + } + }() + super.init() + setup() + } + + @available(*, unavailable) + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Methods + + override open func tintColorDidChange() { + super.tintColorDidChange() + applyTintColor() + } + + private func updateScaling(for dataSeries: [OCKDataSeries]) { + let maxValue = max(CGFloat(gridView.numberOfDivisions), dataSeries.flatMap { $0.dataPoints }.map { $0.y }.max() ?? 0) + let chartMax = ceil(maxValue / CGFloat(gridView.numberOfDivisions)) * CGFloat(gridView.numberOfDivisions) + plotView.yMaximum = chartMax + gridView.yMaximum = chartMax + } + + override func setup() { + super.setup() + [gridView, plotView, axisView, legend].forEach { addSubview($0) } + + gridView.xMinimum = plotView.xMinimum + gridView.xMaximum = plotView.xMaximum + gridView.yMinimum = plotView.yMinimum + gridView.yMaximum = plotView.yMaximum + + [gridView, plotView, axisView, legend].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + gridView.topAnchor.constraint(equalTo: plotView.topAnchor), + gridView.leadingAnchor.constraint(equalTo: leadingAnchor), + gridView.trailingAnchor.constraint(equalTo: trailingAnchor), + gridView.bottomAnchor.constraint(equalTo: plotView.bottomAnchor), + plotView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: horizontalPlotPadding), + plotView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -horizontalPlotPadding), + plotView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + plotView.bottomAnchor.constraint(equalTo: axisView.topAnchor), + axisView.leadingAnchor.constraint(equalTo: plotView.leadingAnchor), + axisView.trailingAnchor.constraint(equalTo: plotView.trailingAnchor), + axisView.heightAnchor.constraint(equalToConstant: axisHeight), + axisView.bottomAnchor.constraint(equalTo: legend.topAnchor), + legend.leadingAnchor.constraint(greaterThanOrEqualTo: safeAreaLayoutGuide.leadingAnchor), + legend.trailingAnchor.constraint(lessThanOrEqualTo: safeAreaLayoutGuide.trailingAnchor), + 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/OCKDataSeries.swift b/CareKitUI/CareKitUI/Components/Charts/OCKDataSeries.swift new file mode 100644 index 000000000..113b56d71 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/OCKDataSeries.swift @@ -0,0 +1,125 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Representa a single group of data to be plotted. In most cases, CareKit plots accept multiple data +/// series, allowing for for several data series to be plotted on a single axis for easy comparison. +/// - SeeAlso: `OCKGraphView` +public struct OCKDataSeries: Equatable { + /// An array of points given in plot space Cartesian coordinates + /// The plot origin (0, 0) is in the bottom left hand corner. + public var dataPoints: [CGPoint] + + /// A title for this data series that will be displayed in the plot legend. + public var title: String + + /// The start color of the gradient this data series will be plotted in. + public var gradientStartColor: UIColor? + + /// The end color of the gradient this data series will be plotted in. + public var gradientEndColor: UIColor? + + /// A size specifying how large this data series should appear on the plot. + /// Its precise interpretation may vary depending on plot type used. + public var size: CGFloat + + /// Used to set the accessibility labels of each of the data points. + /// This array should either be empty or contain the same number of elements as the data series array. + public var accessibilityLabels: [String] = [] + + /// Creates a new data series that can be passed to chart to be plotted. The series will be plotted in a single + /// solid color. Use this initialize if you wish to plot data at precise or irregular intervals. + /// + /// - Parameters: + /// - dataPoints: An array of points in graph space Cartesian coordinates. The origin is in bottom left corner. + /// - title: A title that will be used to represent this data series in the plot legend. + /// - color: A solid color to be used when plotting the data series. + /// - size: A size specifying how large this data series should appear on the plot. + public init(dataPoints: [CGPoint], title: String, size: CGFloat = 10, color: UIColor? = nil) { + self.dataPoints = dataPoints + self.title = title + self.gradientStartColor = color + self.gradientEndColor = color + self.size = size + } + + /// Creates a new data series that can be passed to chart to be plotted. The series will be plotted with a gradient + /// color scheme. Use this initialize if you wish to plot data at precise or irregular intervals. + /// + /// - Parameters: + /// - dataPoints: An array of points in graph space Cartesian coordinates. The origin is in bottom left corner. + /// - title: A title that will be used to represent this data series in the plot legend. + /// - gradientStartColor: The start color for the gradient. + /// - gradientEndColor: The end color for the gradient. + /// - size: A size specifying how large this data series should appear on the plot. + public init(dataPoints: [CGPoint], title: String, gradientStartColor: UIColor, gradientEndColor: UIColor, size: CGFloat = 10) { + self.dataPoints = dataPoints + self.title = title + self.size = size + self.gradientStartColor = gradientStartColor + self.gradientEndColor = gradientEndColor + } + + /// Creates a new data series that can be passed to chart to be plotted. The series will be plotted in a single solid color. + /// Values will be evenly spaced when displayed on a chart. Use this option when the x coordinate is not particularly + /// meaningful, such as when creating bar charts. + /// + /// - Parameters: + /// - values: An array of values in graph space Cartesian coordinates. + /// - title: A title that will be used to represent this data series in the plot legend. + /// - size: A size specifying how large this data series should appear on the plot. + /// - color: The color that this data series will be plotted in. + public init(values: [CGFloat], title: String, size: CGFloat = 10, color: UIColor? = nil) { + self.dataPoints = values.enumerated().map { CGPoint(x: CGFloat($0), y: $1) } + self.title = title + self.size = size + self.gradientStartColor = color + self.gradientEndColor = color + } + + /// Creates a new data series that can be passed to chart to be plotted. The series will be plotted with a gradient + /// color scheme. Values will be evenly spaced when displayed on a chart. Use this option when the x coordinate is not + /// particularly meaningful, such as when creating bar charts. + /// + /// - Parameters: + /// - values: An array of values in graph space Cartesian coordinates. + /// - title: A title that will be used to represent this data series in the plot legend. + /// - gradientStartColor: The start color for the gradient. + /// - gradientEndColor: The end color for the gradient. + /// - size: A size specifying how large this data series should appear on the plot. + public init(values: [CGFloat], title: String, gradientStartColor: UIColor, gradientEndColor: UIColor, size: CGFloat = 10) { + self.dataPoints = values.enumerated().map { CGPoint(x: CGFloat($0), y: $1) } + self.title = title + self.size = size + self.gradientStartColor = gradientStartColor + self.gradientEndColor = gradientEndColor + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/OCKGraphAxisView.swift b/CareKitUI/CareKitUI/Components/Charts/OCKGraphAxisView.swift new file mode 100644 index 000000000..97025556a --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/OCKGraphAxisView.swift @@ -0,0 +1,153 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +class OCKGraphAxisView: UIView { + var axisMarkers = [String]() { + didSet { redrawLabels() } + } + + var selectedIndex: Int? { + didSet { redrawLabels() } + } + + private var tickViews = [OCKCircleLabelView]() + + private func redrawLabels() { + tickViews.forEach { $0.removeFromSuperview() } + tickViews = axisMarkers.enumerated().map { index, text in + let view = OCKCircleLabelView(textStyle: .callout) + view.frame = frameForMarker(atIndex: index) + view.label.text = text + view.label.textAlignment = .center + view.label.isAccessibilityElement = false + view.isSelected = index == selectedIndex + return view + } + tickViews.forEach(addSubview) + } + + private func frameForMarker(atIndex index: Int) -> CGRect { + guard !axisMarkers.isEmpty else { return .zero } + guard axisMarkers.count > 1 else { return bounds } + let spacing = bounds.width / CGFloat(axisMarkers.count - 1) + let centerX = spacing * CGFloat(index) + let origin = CGPoint(x: centerX - spacing / 2, y: 0) + let size = CGSize(width: spacing, height: bounds.height) + let rect = CGRect(origin: origin, size: size) + return rect + } + + override func layoutSubviews() { + super.layoutSubviews() + tickViews.enumerated().forEach { index, view in + view.frame = frameForMarker(atIndex: index) + } + } +} + +private class OCKCircleLabelView: OCKView { + let label: OCKLabel + + var circleLayer: CAShapeLayer { + guard let layer = layer as? CAShapeLayer else { fatalError("Unsupported type.") } + return layer + } + + override class var layerClass: AnyClass { + return CAShapeLayer.self + } + + override func tintColorDidChange() { + super.tintColorDidChange() + applyTintColor() + } + + var isSelected: Bool = false { + didSet { + updateLabelColor() + circleLayer.fillColor = isSelected ? tintColor.cgColor : nil + } + } + + init(textStyle: UIFont.TextStyle) { + label = OCKCappedSizeLabel(textStyle: .caption1, weight: .medium) + super.init() + setup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("Unsupported initializer") + } + + override func setup() { + super.setup() + addSubview(label) + updateLabelColor() + applyTintColor() + + label.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: centerXAnchor), + label.centerYAnchor.constraint(equalTo: centerYAnchor), + label.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + label.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ]) + } + + override func layoutSubviews() { + super.layoutSubviews() + let padding: CGFloat = 4.0 + let maxDimension = max(label.intrinsicContentSize.width, label.intrinsicContentSize.height) + padding + let size = CGSize(width: maxDimension, height: maxDimension) + let origin = CGPoint(x: label.center.x - maxDimension / 2, y: label.center.y - maxDimension / 2) + + circleLayer.path = UIBezierPath(ovalIn: CGRect(origin: origin, size: size)).cgPath + circleLayer.fillColor = isSelected ? tintColor.cgColor : nil + } + + private func updateLabelColor() { + label.textColor = isSelected ? style().color.customBackground : style().color.label + } + + override func styleDidChange() { + 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/Charts/OCKGraphLegendView.swift b/CareKitUI/CareKitUI/Components/Charts/OCKGraphLegendView.swift new file mode 100644 index 000000000..b7d744a9d --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/OCKGraphLegendView.swift @@ -0,0 +1,120 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +class OCKGraphLegendView: UIStackView { + private enum Constants { + static let iconCornerRadius: CGFloat = 4.0 + static let iconPadding: CGFloat = 6.0 + static let keySpacing: CGFloat = 10.0 + } + + init() { + super.init(frame: .zero) + setup() + } + + required init(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + func setDataSeries(_ dataSeries: [OCKDataSeries]) { + arrangedSubviews.forEach { $0.removeFromSuperview() } + dataSeries.map(makeKey).forEach(addArrangedSubview) + } + + private func setup() { + axis = .horizontal + distribution = .fillEqually + spacing = Constants.keySpacing + } + + private func makeKey(for series: OCKDataSeries) -> UIView { + let icon = makeIcon(startColor: series.gradientStartColor ?? tintColor, endColor: series.gradientEndColor ?? tintColor) + let label = makeLabel(title: series.title, color: series.gradientStartColor ?? tintColor) + let stack = UIStackView(arrangedSubviews: [icon, label]) + stack.axis = .horizontal + stack.spacing = Constants.iconPadding + return stack + } + + private func makeLabel(title: String, color: UIColor) -> UIView { + let label = OCKCappedSizeLabel(textStyle: .caption1, weight: .regular) + label.textAlignment = .left + label.textColor = color + label.text = "\(title)" + label.clipsToBounds = true + label.isAccessibilityElement = false + return label + } + + private func makeIcon(startColor: UIColor, endColor: UIColor) -> UIView { + let icon = OCKGradientView() + icon.startColor = startColor + icon.endColor = endColor + icon.clipsToBounds = true + icon.layer.cornerRadius = Constants.iconCornerRadius + icon.translatesAutoresizingMaskIntoConstraints = false + icon.heightAnchor.constraint(equalTo: icon.widthAnchor).isActive = true + return icon + } +} + +private class OCKGradientView: OCKView { + var startColor: UIColor = OCKStyle().color.customGray2 { + didSet { gradient.colors = [startColor.cgColor, endColor.cgColor] } + } + + var endColor: UIColor = OCKStyle().color.customGray2 { + didSet { gradient.colors = [startColor.cgColor, endColor.cgColor] } + } + + private var gradient: CAGradientLayer { + guard let layer = layer as? CAGradientLayer else { fatalError("Unsupported type") } + return layer + } + + override class var layerClass: AnyClass { + return CAGradientLayer.self + } + + override func setup() { + super.setup() + setupGradient() + } + + private func setupGradient() { + gradient.colors = [startColor.cgColor, endColor.cgColor] + gradient.startPoint = CGPoint(x: 0.5, y: 1) + gradient.endPoint = CGPoint(x: 0.5, y: 0) + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/OCKGridView.swift b/CareKitUI/CareKitUI/Components/Charts/OCKGridView.swift new file mode 100644 index 000000000..bf2619de6 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/OCKGridView.swift @@ -0,0 +1,103 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +class OCKGridView: UIView, OCKCartesianGridProtocol { + override class var layerClass: AnyClass { + return OCKGridLayer.self + } + + private let layout = OCKResponsiveLayout<CGFloat>( + defaultLayout: 4, + anySizeClassRuleSet: [ + .init(layout: 6, greaterThanOrEqualToContentSizeCategory: .small), + .init(layout: 8, greaterThanOrEqualToContentSizeCategory: .medium), + .init(layout: 10, greaterThanOrEqualToContentSizeCategory: .large), + .init(layout: 12, greaterThanOrEqualToContentSizeCategory: .extraLarge), + .init(layout: 14, greaterThanOrEqualToContentSizeCategory: .accessibilityMedium), + .init(layout: 16, greaterThanOrEqualToContentSizeCategory: .accessibilityLarge), + .init(layout: 18, greaterThanOrEqualToContentSizeCategory: .accessibilityExtraLarge) + ] + ) + + private var gridLayer: OCKGridLayer { + return layer as! OCKGridLayer + } + + var numberOfDivisions: Int { + return gridLayer.numberOfVerticalDivisions + } + + var xMinimum: CGFloat? { + get { return gridLayer.xMinimum } + set { gridLayer.xMinimum = newValue } + } + + var xMaximum: CGFloat? { + get { return gridLayer.xMaximum } + set { gridLayer.xMaximum = newValue } + } + + var yMinimum: CGFloat? { + get { return gridLayer.yMinimum } + set { gridLayer.yMinimum = newValue } + } + + var yMaximum: CGFloat? { + get { return gridLayer.yMaximum } + set { gridLayer.yMaximum = newValue } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + + let fontSize = layout.responsiveLayoutRule(traitCollection: traitCollection) + gridLayer.fontSize = fontSize + + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + let fontSize = layout.responsiveLayoutRule(traitCollection: traitCollection) + gridLayer.fontSize = fontSize + + } +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Protocols/OCKChartDisplayable.swift b/CareKitUI/CareKitUI/Components/Charts/Protocols/OCKChartDisplayable.swift new file mode 100644 index 000000000..c8b556a7b --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Protocols/OCKChartDisplayable.swift @@ -0,0 +1,44 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Any object that can display and handle interactions with a chart. +public protocol OCKChartDisplayable: AnyObject { + /// Handles events related to an `OCKChartDisplayable` object. + var delegate: OCKChartViewDelegate? { get set } +} + +/// Handles events related to an `OCKChartDisplayable` object. +public protocol OCKChartViewDelegate: AnyObject { + /// Called when the view displaying the chart was selected. + /// - Parameter chartView: The view displaying the chart. + func didSelectChartView(_ chartView: UIView & OCKChartDisplayable) +} diff --git a/CareKitUI/CareKitUI/Components/Charts/Protocols/OCKGraphable.swift b/CareKitUI/CareKitUI/Components/Charts/Protocols/OCKGraphable.swift new file mode 100644 index 000000000..a3111b895 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Charts/Protocols/OCKGraphable.swift @@ -0,0 +1,108 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Any view or layer that can be plotted on conforms to this protocol. +protocol OCKCartesianGridProtocol: AnyObject { + /// The smallest value shown on the x-axis. If not set, a reasonable default will be used. + var xMinimum: CGFloat? { get set } + + /// The largest value shown on the x-axis. If not set, a reasonable default will be used. + var xMaximum: CGFloat? { get set } + + /// The smallest value shown on the y-axis. If not set, a reasonable default will be used. + var yMinimum: CGFloat? { get set } + + /// The largest value shown on the y-axis. If not set, a reasonable default will be used. + var yMaximum: CGFloat? { get set } +} + +extension OCKCartesianGridProtocol { + /// The default width of the graph in plot space coordinates. + static var defaultWidth: CGFloat { return 100 } + + /// The default height of the graph area in plot space coordinates. + static var defaultHeight: CGFloat { return 100 } +} + +/// Any view that only supports plotting a single data series should conform to this protocol. +protocol OCKSinglePlotable: OCKCartesianGridProtocol { + var dataPoints: [CGPoint] { get set } +} + +/// Any view that supports plotting multiple data series should conform to this protocol. +protocol OCKMultiPlotable: OCKCartesianGridProtocol { + var dataSeries: [OCKDataSeries] { get set } +} + +extension OCKMultiPlotable { + /// Computes the bounds of the area shown on the graph in graph coordinate space. + func graphBounds() -> CGRect { + let xCoords = dataSeries.flatMap { $0.dataPoints }.map { $0.x } + let xMin = xMinimum ?? xCoords.min() ?? 0 + let xMax = xMaximum ?? xCoords.max() ?? Self.defaultWidth + let width = xMax - xMin + + let yCoords = dataSeries.flatMap { $0.dataPoints }.map { $0.y } + let yMin = yMinimum ?? yCoords.min() ?? 0 + let yMax = yMaximum ?? yCoords.max() ?? Self.defaultHeight + let height = yMax - yMin + + return CGRect(x: xMin, y: yMin, width: width, height: height) + } +} + +protocol OCKGradientPlotable { + var gradientLayer: CAGradientLayer { get } + var pointsLayer: CAShapeLayer { get } + + func makePath(points: [CGPoint]) -> CGPath +} + +extension OCKGradientPlotable where Self: CALayer { + func drawPoints(_ points: [CGPoint]) { + let path = makePath(points: points) + pointsLayer.path = path + + // Adjust the gradient's frame to capture the entire shape. It should clip top and bottom, but not sides. + let minX = min(0, path.boundingBoxOfPath.minX) + let maxX = max(bounds.width, path.boundingBoxOfPath.maxX) + let gradientFrame = CGRect(x: minX, y: 0, width: maxX - minX, height: bounds.height) + + gradientLayer.frame = gradientFrame + + // The points layer is positioned as a sublayer in the gradient, but it needs to appear + // in the same position it would if the gradient layer were exactly the same size as its parent. + let translation = CGAffineTransform(translationX: -gradientFrame.minX, y: 0) + let adjustedFrame = bounds.applying(translation) + pointsLayer.frame = adjustedFrame + } +} diff --git a/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKAddressButton.swift b/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKAddressButton.swift new file mode 100644 index 000000000..f1ef41f30 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKAddressButton.swift @@ -0,0 +1,143 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +open class OCKAddressButton: OCKAnimatedButton<OCKStackView> { + // MARK: Properties + + private enum Constants { + static let textSpacing: CGFloat = 2 + } + + private lazy var imageViewPointSize = OCKAccessibleValue(container: style(), keyPath: \.dimension.symbolPointSize3) { [imageView] scaledValue in + imageView.preferredSymbolConfiguration = .init(pointSize: scaledValue, weight: .regular) + } + + private let textStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.spacing = Constants.textSpacing + return stackView + }() + + /// Holds the main content in the button. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.horizontal() + stackView.alignment = .top + return stackView + }() + + /// Image in the corner of the view. The default image is set to the location icon. + public let imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "location") + return imageView + }() + + /// The main text in the button. + public let titleLabel: OCKLabel = { + let label = OCKLabel(textStyle: .footnote, weight: .semibold) + label.numberOfLines = 0 + label.text = loc("ADDRESS") + return label + }() + + /// Below the main text in the button. + public let detailLabel: OCKLabel = { + let label = OCKLabel(textStyle: .footnote, weight: .regular) + label.numberOfLines = 0 + return label + }() + + // MARK: - Life Cycle + + public init() { + super.init(contentView: contentStackView, handlesSelection: false) + setup() + } + + public required init?(coder: NSCoder) { + super.init(contentView: contentStackView, handlesSelection: false) + setup() + } + + // MARK: Methods + + private func setup() { + styleSubviews() + addSubviews() + constrainSubviews() + } + + private func styleSubviews() { + accessibilityLabel = titleLabel.text + accessibilityHint = loc("DOUBLE_TAP_MAP") + applyTintColor() + } + + private func applyTintColor() { + titleLabel.textColor = tintColor + } + + private func addSubviews() { + [titleLabel, detailLabel].forEach { textStackView.addArrangedSubview($0) } + [textStackView, imageView].forEach { contentStackView.addArrangedSubview($0) } + addSubview(contentStackView) + } + + private func constrainSubviews() { + [contentStackView, imageView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) + NSLayoutConstraint.activate(contentStackView.constraints(equalTo: layoutMarginsGuide)) + } + + override open func styleDidChange() { + super.styleDidChange() + let style = self.style() + detailLabel.textColor = style.color.label + backgroundColor = style.color.quaternaryCustomFill + directionalLayoutMargins = style.dimension.directionalInsets1 + layer.cornerRadius = style.appearance.cornerRadius2 + imageViewPointSize.update(withContainer: style) + contentStackView.spacing = style.dimension.directionalInsets1.leading + } + + override open func tintColorDidChange() { + super.tintColorDidChange() + applyTintColor() + } + + override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { + imageViewPointSize.apply() + } + } +} diff --git a/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKContactButton.swift b/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKContactButton.swift new file mode 100644 index 000000000..205cbc508 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Contact/Buttons/OCKContactButton.swift @@ -0,0 +1,158 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +open class OCKContactButton: OCKAnimatedButton<OCKStackView> { + // MARK: Properties + + /// Determines the label text and image shown in the button. + public enum `Type`: String { + + /// Button for phone calls. + case call = "Call" + + /// Button for text messages. + case message = "Message" + + /// Button for emails. + case email = "E-mail" + + var image: UIImage? { + let image: UIImage? + switch self { + case .call: image = UIImage(systemName: "phone") + case .message: image = UIImage(systemName: "text.bubble") + case .email: image = UIImage(systemName: "envelope") + } + return image + } + } + + /// Image above the title label. + public let imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// Title label under the image. + public let label: OCKLabel = { + let label = OCKLabel(textStyle: .footnote, weight: .semibold) + label.textAlignment = .center + return label + }() + + /// Holds the main content for the view. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.spacing = 2 + stackView.isUserInteractionEnabled = false + return stackView + }() + + let type: Type + + private lazy var imageViewPointSize = OCKAccessibleValue(container: style(), keyPath: \.dimension.symbolPointSize2) { [imageView] scaledValue in + imageView.preferredSymbolConfiguration = .init(pointSize: scaledValue, weight: .regular) + } + + // MARK: Life cycle + + /// Initialize the button with a type. The type determines the label text and image. + /// - Parameter type: The type of the button. + public init(type: Type) { + self.type = type + super.init(contentView: contentStackView, handlesSelection: false) + setup() + } + + public required init?(coder: NSCoder) { + self.type = .call + super.init(contentView: contentStackView, handlesSelection: false) + setup() + } + + // MARK: Methods + + private func setup() { + styleSubviews() + addSubviews() + constrainSubviews() + } + + private func styleSubviews() { + imageView.image = type.image + label.text = type.rawValue + + switch type { + case .call: accessibilityLabel = loc("CALL") + case .email: accessibilityLabel = loc("EMAIL") + case .message: accessibilityLabel = loc("MESSAGE") + } + + applyTintColor() + } + + private func addSubviews() { + [imageView, label].forEach { contentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [contentStackView, imageView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate(contentStackView.constraints(equalTo: layoutMarginsGuide)) + } + + 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() + backgroundColor = style.color.quaternaryCustomFill + layer.cornerRadius = style.appearance.cornerRadius2 + imageViewPointSize.update(withContainer: style) + directionalLayoutMargins = style.dimension.directionalInsets1 + } + + override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { + imageViewPointSize.apply() + } + } +} diff --git a/CareKitUI/CareKitUI/Components/Contact/OCKContactDisplayable.swift b/CareKitUI/CareKitUI/Components/Contact/OCKContactDisplayable.swift new file mode 100644 index 000000000..db7449d25 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Contact/OCKContactDisplayable.swift @@ -0,0 +1,64 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Any object that can display and handle interactions with a contact. +public protocol OCKContactDisplayable: AnyObject { + /// Handles events related to an `OCKContactDisplayable` object. + var delegate: OCKContactViewDelegate? { get set } +} + +/// Handles events related to an `OCKContactDisplayable` object. +public protocol OCKContactViewDelegate: AnyObject { + /// Called when the user would like to call the contact. + /// - Parameter contactView: The view that displays the contact. + /// - Parameter sender: The sender that is initiating the call process. + func contactView(_ contactView: UIView & OCKContactDisplayable, senderDidInitiateCall sender: Any?) + + /// Called when the user would like to message the contact. + /// - Parameter contactView: The view that displays the contact. + /// - Parameter sender: The sender that is initiating the messaging process. + func contactView(_ contactView: UIView & OCKContactDisplayable, senderDidInitiateMessage sender: Any?) + + /// Called when the user would like to email the contact. + /// - Parameter contactView: The view that displays the contact. + /// - Parameter sender: The sender that is initiating the email process. + func contactView(_ contactView: UIView & OCKContactDisplayable, senderDidInitiateEmail sender: Any?) + + /// Called when the user would like to view the address of the contact. + /// - Parameter contactView: The view that displays the contact. + /// - Parameter sender: The sender that is initiating the address lookup process. + func contactView(_ contactView: UIView & OCKContactDisplayable, senderDidInitiateAddressLookup sender: Any?) + + /// Called when the view displaying the contact was selected. + /// - Parameter contactView: The view displaying the contact. + func didSelectContactView(_ contactView: UIView & OCKContactDisplayable) +} diff --git a/CareKitUI/CareKitUI/Components/Contact/OCKDetailedContactView.swift b/CareKitUI/CareKitUI/Components/Contact/OCKDetailedContactView.swift new file mode 100644 index 000000000..20d67859e --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Contact/OCKDetailedContactView.swift @@ -0,0 +1,203 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A card that displays information for a contact. The header is an `OCKHeaderView` +/// The body contains a multi-line istructions label, and four buttons; call, message, +/// email, and address. The first three buttons have title labels and image views that can +/// be modified, while the last has a title label, body label, and image view. +/// +/// +-------------------------------------------------------+ +/// | +------+ | +/// | | icon | [title] | +/// | | img | [detail] | +/// | +------+ | +/// | | +/// | -------------------------------------------------- | +/// | | +/// | [Instructions] | +/// | | +/// | +------------+ +------------+ +------------+ | +/// | | [title] | | [title] | | [title] | | +/// | | | | | | | | +/// | +------------+ +------------+ +------------+ | +/// | | +/// | +---------------------------------------------------+ | +/// | | [title] | | +/// | | [detail] | | +/// | | | | +/// | +---------------------------------------------------+ | +/// +-------------------------------------------------------+ +/// +open class OCKDetailedContactView: OCKView, OCKContactDisplayable { + + // MARK: Properties + + /// Handles events related to an `OCKContactDisplayable` object. + public weak var delegate: OCKContactViewDelegate? + + /// A vertical stack view that holds the main content in the view. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.distribution = .fill + return stackView + }() + + /// Header stack view that shows an `iconImageView` and a separator. + public let headerView = OCKHeaderView { + $0.showsIconImage = true + $0.showsSeparator = true + } + + /// Multi-line label under the `headerView`. + public let instructionsLabel: OCKLabel = { + let label = OCKLabel(textStyle: .subheadline, weight: .medium) + label.numberOfLines = 0 + return label + }() + + /// Button with a phone image and title label. + /// Set the `isHidden` property to `false` to hide the button. + public let callButton = OCKContactButton(type: .call) + + /// Button with a messages images and title label. + /// Set the `isHidden` property to `false` to hide the button. + public let messageButton = OCKContactButton(type: .message) + + /// Button with an email image and title label. + /// Set the `isHidden` property to `false` to hide the button. + public let emailButton = OCKContactButton(type: .email) + + /// Button with a location image, title and detail labels. + /// Set the `isHidden` property to `false` to hide the button. + public let addressButton = OCKAddressButton() + + /// The default image that can be used as a placeholder for the `iconImageView` in the `headerView`. + public static let defaultImage = UIImage(systemName: "person.crop.circle")! + + let contentView = OCKView() + + private var buttons: [OCKAnimatedButton<OCKStackView>] { + return [addressButton, callButton, messageButton, emailButton] + } + + private var contactButtons: [OCKContactButton] { + return buttons.compactMap { $0 as? OCKContactButton } + } + + /// Stack view that holds phone, message, and email contact action buttons. + private lazy var contactStackView: OCKStackView = { + let stackView = OCKStackView() + stackView.axis = self.contactStackAxisDirection() + stackView.distribution = .fillEqually + return stackView + }() + + /// Stack view that holds buttons in `contactStack` and `directionsButton`. + /// You may choose to add or hide buttons + private let buttonStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.distribution = .equalSpacing + return stackView + }() + + // MARK: - Methods + + /// Prepares interface after initialization + override func setup() { + super.setup() + addSubviews() + constrainSubviews() + styleSubviews() + setupGestures() + } + + private func setupGestures() { + [messageButton, callButton, addressButton, emailButton].forEach { + $0.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside) + } + } + + private func addSubviews() { + addSubview(contentView) + contentView.addSubview(contentStackView) + [headerView, instructionsLabel, buttonStackView].forEach { contentStackView.addArrangedSubview($0) } + [callButton, messageButton, emailButton].forEach { contactStackView.addArrangedSubview($0) } + [contactStackView, addressButton].forEach { buttonStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [contentView, contentStackView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate( + contentView.constraints(equalTo: layoutMarginsGuide) + + contentStackView.constraints(equalTo: contentView) + ) + } + + private func styleSubviews() { + headerView.iconImageView?.image = OCKDetailedContactView.defaultImage // set default profile picture + } + + @objc + private func didTapButton(_ sender: UIControl) { + sender.isSelected = false // Immediately deselect since these buttons aren't intended to be toggleable + switch sender { + case messageButton: delegate?.contactView(self, senderDidInitiateMessage: sender) + case callButton: delegate?.contactView(self, senderDidInitiateCall: sender) + case addressButton: delegate?.contactView(self, senderDidInitiateAddressLookup: sender) + case emailButton: delegate?.contactView(self, senderDidInitiateEmail: sender) + default: fatalError("Target not set up properly") + } + } + + 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 + let topInset = cachedStyle.dimension.directionalInsets1.top + contentStackView.spacing = topInset + [contactStackView, buttonStackView].forEach { $0.spacing = topInset / 2.0 } + } + + // MARK: Accessibility Scaling + + override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + contactStackView.axis = contactStackAxisDirection() + } + + private func contactStackAxisDirection() -> NSLayoutConstraint.Axis { + traitCollection.preferredContentSizeCategory < .extraExtraLarge ? .horizontal : .vertical + } +} diff --git a/CareKitUI/CareKitUI/Components/Contact/OCKSimpleContactView.swift b/CareKitUI/CareKitUI/Components/Contact/OCKSimpleContactView.swift new file mode 100644 index 000000000..70ca3be97 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Contact/OCKSimpleContactView.swift @@ -0,0 +1,118 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A card that displays information for a contact. The header is an `OCKHeaderView`. +/// +/// +-------------------------------------------------------+ +/// | +------+ | +/// | | icon | [title] [detail | +/// | | img | [detail] disclosure] | +/// | +------+ | +/// +-------------------------------------------------------+ +/// +open class OCKSimpleContactView: OCKView, OCKContactDisplayable { + + // MARK: Properties + + /// Handles events related to an `OCKContactDisplayable` object. + public weak var delegate: OCKContactViewDelegate? + + /// A vertical stack view that holds the main content in the view. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.distribution = .fill + return stackView + }() + + /// Header stack view that shows an `iconImageView` and a separator. + public let headerView = OCKHeaderView { + $0.showsIconImage = true + $0.showsDetailDisclosure = true + } + + let contentView: OCKView = { + let view = OCKView() + view.clipsToBounds = true + return view + }() + + // Button that displays the highlighted state for the view. + private lazy var backgroundButton = OCKAnimatedButton(contentView: contentStackView, highlightOptions: [.defaultOverlay, .defaultDelayOnSelect], + handlesSelection: false) + + // MARK: - Methods + + /// Prepares interface after initialization + override func setup() { + super.setup() + addSubviews() + constrainSubviews() + setupGestures() + styleSubviews() + } + + private func setupGestures() { + backgroundButton.addTarget(self, action: #selector(viewTapped(_:)), for: .touchUpInside) + } + + private func addSubviews() { + addSubview(contentView) + contentView.addSubview(backgroundButton) + contentStackView.addArrangedSubview(headerView) + } + + @objc + private func viewTapped(_ sender: UIControl) { + delegate?.didSelectContactView(self) + } + + private func styleSubviews() { + headerView.iconImageView?.image = UIImage(systemName: "person.crop.circle") + } + + private func constrainSubviews() { + [contentView, backgroundButton, contentStackView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate( + contentView.constraints(equalTo: self) + + backgroundButton.constraints(equalTo: contentView) + + contentStackView.constraints(equalTo: backgroundButton.layoutMarginsGuide)) + } + + 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/OCKChecklistItemButton.swift b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKChecklistItemButton.swift new file mode 100644 index 000000000..2eb1a74a2 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKChecklistItemButton.swift @@ -0,0 +1,120 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation +import UIKit + +/// A button with an icon and title label. +/// +/// +--------------------------+ +/// | [Title] [Icon] | +/// +--------------------------+ +/// +open class OCKChecklistItemButton: OCKAnimatedButton<OCKStackView> { + + // MARK: Properties + + /// The label on the leading side of the button. + public let label: OCKLabel = { + let label = OCKLabel(textStyle: .subheadline, weight: .regular) + label.text = loc("EVENT") + return label + }() + + /// The checkmark button on the trailing end of the view. + public let checkmarkButton: OCKCheckmarkButton = { + let button = OCKCheckmarkButton() + button.isUserInteractionEnabled = false + return button + }() + + /// Holds the main content in the button. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.horizontal() + stackView.alignment = .center + return stackView + }() + + // MARK: - Life Cycle + + public init() { + super.init(contentView: contentStackView, handlesSelection: true) + setup() + } + + public required init?(coder: NSCoder) { + super.init(contentView: contentStackView, handlesSelection: true) + setup() + } + + // MARK: Methods + + private func setup() { + addSubviews() + constrainSubviews() + setupAccessibility() + } + + private func addSubviews() { + [label, checkmarkButton].forEach { contentStackView.addArrangedSubview($0) } + addSubview(contentStackView) + } + + private func constrainSubviews() { + [contentStackView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + checkmarkButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + NSLayoutConstraint.activate( + contentStackView.constraints(equalTo: self, directions: [.horizontal]) + + contentStackView.constraints(equalTo: layoutMarginsGuide, directions: [.vertical]) + ) + } + + private func setupAccessibility() { + accessibilityValue = isSelected ? loc("COMPLETED") : loc("INCOMPLETE") + accessibilityHint = isSelected ? loc("DOUBLE_TAP_TO_COMPLETE") : loc("DOUBLE_TAP_TO_INCOMPLETE") + } + + override open func styleDidChange() { + super.styleDidChange() + let style = self.style() + directionalLayoutMargins = style.dimension.directionalInsets1 + checkmarkButton.height.update(withContainer: style, keyPath: \.dimension.buttonHeight3) + checkmarkButton.imageViewPointSize.update(withContainer: style, keyPath: \.dimension.symbolPointSize5) + label.textColor = style.color.label + } + + override open func setSelected(_ isSelected: Bool, animated: Bool) { + super.setSelected(isSelected, animated: animated) + checkmarkButton.setSelected(isSelected, animated: animated) + setupAccessibility() + } + + override open func setStyleForSelectedState(_ isSelected: Bool) {} +} diff --git a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledButton.swift b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledButton.swift new file mode 100644 index 000000000..eaaf96cae --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledButton.swift @@ -0,0 +1,114 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A button with a filled background color. +/// +/// +--------------------------+ +/// | [Title] | +/// +--------------------------+ +/// +open class OCKLabeledButton: OCKAnimatedButton<OCKLabel> { + + // MARK: Properties + + /// Label in the center of the buttton. + public let label: OCKLabel = { + let label = OCKLabel(textStyle: .subheadline, weight: .medium) + label.text = loc("MARK_COMPLETE") + label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + // MARK: Life Cycle + + public init() { + super.init(contentView: label, handlesSelection: true) + constrainSubviews() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Methods + + override open func setStyleForSelectedState(_ isSelected: Bool) { + let completionString = isSelected ? loc("COMPLETED") : loc("MARK_COMPLETE") + let attributedText = NSMutableAttributedString(string: completionString) + + // Set a checkmark next to the text if the button is in the completed state + if isSelected, let checkmark = UIImage.from(systemName: "checkmark") { + let attachment = NSTextAttachment(image: checkmark) + let checkmarkString = NSAttributedString(attachment: attachment) + attributedText.append(.init(string: " ")) + attributedText.append(checkmarkString) + } + + label.attributedText = attributedText + + updateColors() + } + + override open func styleDidChange() { + super.styleDidChange() + let style = self.style() + updateColors() + layer.cornerRadius = style.appearance.cornerRadius2 + directionalLayoutMargins = style.dimension.directionalInsets1 + } + + override open func tintColorDidChange() { + super.tintColorDidChange() + updateColors() + } + + private func constrainSubviews() { + NSLayoutConstraint.activate(label.constraints(equalTo: layoutMarginsGuide)) + } + + private func updateColors() { + let style = self.style() + backgroundColor = isSelected ? style.color.tertiaryCustomFill : tintColor + label.textColor = isSelected ? tintColor : style.color.white + } +} + +private extension UIImage { + static func from(systemName: String) -> UIImage? { + let image = UIImage(systemName: systemName) + assert(image != nil, "Unable to locate symbol for system name: \(systemName)") + return image + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift new file mode 100644 index 000000000..3279763ed --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLabeledCheckmarkButton.swift @@ -0,0 +1,110 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// OCKCheckmarkButton button with a label. +open class OCKLabeledCheckmarkButton: OCKAnimatedButton<OCKStackView> { + + // MARK: Properties + + /// The label underneath the checkmark button. + public let label: OCKLabel = { + let label = OCKLabel(textStyle: .caption1, weight: .medium) + label.textAlignment = .center + return label + }() + + /// The checkmark button above the label. + public let checkmarkButton: OCKCheckmarkButton = { + let button = OCKCheckmarkButton() + button.isUserInteractionEnabled = false + return button + }() + + /// Holds the main content in the button. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.alignment = .center + return stackView + }() + + // MARK: - Life Cycle + + public init() { + super.init(contentView: contentStackView, handlesSelection: true) + setup() + } + + public required init?(coder: NSCoder) { + super.init(contentView: contentStackView, handlesSelection: true) + setup() + } + + // MARK: - Methods + + private func setup() { + addSubviews() + constrainSubviews() + applyTintColor() + } + + private func addSubviews() { + [checkmarkButton, label].forEach { contentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + contentStackView.translatesAutoresizingMaskIntoConstraints = false + checkmarkButton.setContentHuggingPriority(.defaultHigh, for: .vertical) + NSLayoutConstraint.activate(contentStackView.constraints(equalTo: self)) + } + + private func applyTintColor() { + label.textColor = tintColor + } + + override open func styleDidChange() { + super.styleDidChange() + let style = self.style() + contentStackView.spacing = style.dimension.directionalInsets2.bottom + } + + override open func tintColorDidChange() { + super.tintColorDidChange() + applyTintColor() + } + + override open func setSelected(_ isSelected: Bool, animated: Bool) { + super.setSelected(isSelected, animated: animated) + checkmarkButton.setSelected(isSelected, animated: animated) + } + + override open func setStyleForSelectedState(_ isSelected: Bool) {} +} diff --git a/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLogItemButton.swift b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLogItemButton.swift new file mode 100644 index 000000000..a22a1fc4c --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/Buttons/OCKLogItemButton.swift @@ -0,0 +1,125 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +open class OCKLogItemButton: OCKAnimatedButton<OCKStackView> { + + private enum Constants { + static let spacing: CGFloat = 3 + } + + // MARK: Properties + + /// The icon on the leading end of the button. + public let imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "clock") + imageView.preferredSymbolConfiguration = .init(textStyle: .caption1) + return imageView + }() + + /// Main content label. + public let titleLabel: OCKLabel = { + let label = OCKLabel(textStyle: .caption1, weight: .regular) + return label + }() + + /// Tinted accessory label. + public let detailLabel: OCKLabel = { + let label = OCKLabel(textStyle: .caption1, weight: .regular) + return label + }() + + /// Holds the main content in the button. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.horizontal() + stackView.alignment = .center + stackView.distribution = .fill + return stackView + }() + + // MARK: - Life cycle + + public init() { + super.init(contentView: contentStackView, handlesSelection: false) + setup() + } + + public required init?(coder: NSCoder) { + super.init(contentView: contentStackView, handlesSelection: false) + setup() + } + + // MARK: Methods + + private func setup() { + addSubviews() + constrainSubviews() + styleSubviews() + } + + private func styleSubviews() { + contentStackView.setCustomSpacing(Constants.spacing, after: imageView) + applyTintColor() + } + + private func addSubviews() { + addSubview(contentStackView) + [imageView, detailLabel, titleLabel].forEach { contentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [contentStackView].forEach { $0?.translatesAutoresizingMaskIntoConstraints = false } + detailLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) + NSLayoutConstraint.activate( + contentStackView.constraints(equalTo: self, directions: [.horizontal]) + + contentStackView.constraints(equalTo: layoutMarginsGuide, directions: [.vertical]) + ) + } + + private func applyTintColor() { + detailLabel.textColor = tintColor + } + + override open func styleDidChange() { + super.styleDidChange() + let style = self.style() + titleLabel.textColor = style.color.label + contentStackView.setCustomSpacing(style.dimension.directionalInsets1.top, after: detailLabel) + directionalLayoutMargins = style.dimension.directionalInsets1 + } + + override open func tintColorDidChange() { + super.tintColorDidChange() + applyTintColor() + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/Collection View/OCKGridTaskCell.swift b/CareKitUI/CareKitUI/Components/Task/Collection View/OCKGridTaskCell.swift new file mode 100644 index 000000000..0d4054081 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/Collection View/OCKGridTaskCell.swift @@ -0,0 +1,84 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A cell used in the `collectionView` of the `OCKGridTaskView`. The cell shows a circular `completionButton` that has an image and a +/// `titleLabel`. The default image is a checkmark. +open class OCKGridTaskCell: UICollectionViewCell { + + // MARK: Properties + + /// Circular button that shows an image and label. The default image is a checkmark when selected. + /// The text for the deselected state will automatically adapt to the `tintColor`. + public let completionButton = OCKLabeledCheckmarkButton() + + // MARK: Life cycle + + override public init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + // MARK: Methods + + override open func prepareForReuse() { + super.prepareForReuse() + completionButton.isSelected = false + completionButton.label.text = nil + accessibilityLabel = nil + accessibilityValue = nil + } + + private func setup() { + addSubviews() + constrainSubviews() + completionButton.isEnabled = false + isAccessibilityElement = true + accessibilityTraits = .button + } + + private func addSubviews() { + contentView.addSubview(completionButton) + } + + private func constrainSubviews() { + completionButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate( + completionButton.constraints(equalTo: contentView, directions: [.top, .leading]) + + completionButton.constraints(equalTo: contentView, directions: [.bottom, .trailing], priority: .almostRequired) + ) + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/Collection View/OCKLogButtonCell.swift b/CareKitUI/CareKitUI/Components/Task/Collection View/OCKLogButtonCell.swift new file mode 100644 index 000000000..4e0d9813c --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/Collection View/OCKLogButtonCell.swift @@ -0,0 +1,93 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +private class OCKLogButton: OCKLabeledButton { + override init() { + super.init() + handlesSelection = false + label.text = loc("Log") + } +} + +open class OCKLogButtonCell: UICollectionViewCell { + + // MARK: Properties + + public let logButton: OCKLabeledButton = { + let button = OCKLogButton() + OCKLogButtonCell.resetLogButton(button) + return button + }() + + // MARK: - Life cycle + + override public init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override open func prepareForReuse() { + super.prepareForReuse() + OCKLogButtonCell.resetLogButton(logButton) + } + + // MARK: - Methods + + /// Reset a log button to its original state + private static func resetLogButton(_ button: OCKLabeledButton) { + button.label.text = loc("LOG") + } + + private func setup() { + styleSubviews() + addSubviews() + constrainSubviews() + } + + private func styleSubviews() { + preservesSuperviewLayoutMargins = true + } + + private func addSubviews() { + contentView.addSubview(logButton) + } + + private func constrainSubviews() { + logButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(logButton.constraints(equalTo: contentView)) + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/Collection View/OCKSelfSizingCollectionView.swift b/CareKitUI/CareKitUI/Components/Task/Collection View/OCKSelfSizingCollectionView.swift new file mode 100644 index 000000000..43b196a10 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/Collection View/OCKSelfSizingCollectionView.swift @@ -0,0 +1,64 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +class OCKSelfSizingCollectionView: UICollectionView { + + // MARK: Properties + + private var collectionViewHeightConstraint: NSLayoutConstraint? + + // MARK: Life cycle + + override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + constrainSubviews() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Methods + + override func layoutSubviews() { + super.layoutSubviews() + let height = collectionViewLayout.collectionViewContentSize.height + collectionViewHeightConstraint?.constant = max(1, height) + } + + private func constrainSubviews() { + translatesAutoresizingMaskIntoConstraints = false + collectionViewHeightConstraint = heightAnchor.constraint(equalToConstant: 1).withPriority(.almostRequired) + collectionViewHeightConstraint?.isActive = true + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/OCKButtonLogTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKButtonLogTaskView.swift new file mode 100644 index 000000000..24bdcf02c --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/OCKButtonLogTaskView.swift @@ -0,0 +1,157 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A card that displays a header, multi-line label, a collection of log buttons, and a dynamic vertical stack of logged items. +/// In CareKit, this view is intended to display a particular event for a task. When the log button is presses, +/// a new outcome is created for the event. +/// +/// See `logButtonsCollectionView` to customize the layout or number of log buttons. +/// +/// To insert custom views vertically the view, see `contentStack`. To modify the logged items, see +/// `updateItem`, `appendItem`, `insertItem`, `removeItem` and `clearItems`. +/// +/// +-------------------------------------------------------+ +/// | | +/// | [title] [detail | +/// | [detail] disclosure] | +/// | | +/// | | +/// | -------------------------------------------------- | +/// | | +/// | [instructions] | +/// | | +/// | +-----------------------------------------------+ | +/// | | [log button] | | +/// | +-----------------------------------------------+ | +/// | | +/// | [img] [detail] [title] | +/// | ------------------------------------------------- | +/// | [img] [detail] [title] | +/// | ------------------------------------------------- | +/// | ... | +/// | ... | +/// | ------------------------------------------------- | +/// | [img] [detail] [title] | +/// | | +/// +-------------------------------------------------------+ +/// +open class OCKButtonLogTaskView: OCKLogTaskView, UICollectionViewDelegate, UICollectionViewDataSource { + + private enum Constants { + static let spacing: CGFloat = 16 + static let estimatedCellHeight: CGFloat = 44 + } + + // MARK: Properties + + /// The default cell type used for the `logButtonCollectionView`. + public typealias DefaultCellType = OCKLogButtonCell + + /// The identifier used for the default cell in the `logButtonCollectionView`. + public static let defaultCellIdentifier = "log-button-cell" + + /// Collection view holding the log buttons. + public private (set) var logButtonsCollectionView: UICollectionView! + + /// Multi-line label below the header. + public let instructionsLabel: OCKLabel = { + let label = OCKLabel(textStyle: .subheadline, weight: .medium) + label.numberOfLines = 0 + return label + }() + + // MARK: Methods + + private func makeCollectionView() -> UICollectionView { + let collectionView = OCKSelfSizingCollectionView(frame: .zero, collectionViewLayout: makeLayout()) + collectionView.backgroundColor = nil + collectionView.register(OCKButtonLogTaskView.DefaultCellType.self, forCellWithReuseIdentifier: OCKButtonLogTaskView.defaultCellIdentifier) + return collectionView + } + + private func makeLayout() -> UICollectionViewCompositionalLayout { + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(Constants.estimatedCellHeight)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(Constants.estimatedCellHeight)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = directionalLayoutMargins.top + + let layout = UICollectionViewCompositionalLayout(section: section) + return layout + } + + @objc + private func didTapLogButton(_ sender: UIControl) { + delegate?.taskView(self, didCreateOutcomeValueAt: 0, eventIndexPath: .init(row: 0, section: 0), sender: sender) + } + + override func setup() { + logButtonsCollectionView = makeCollectionView() + logButtonsCollectionView.delegate = self + logButtonsCollectionView.dataSource = self + + super.setup() + } + + override func addSubviews() { + super.addSubviews() + [logButtonsCollectionView, instructionsLabel].forEach { contentStackView.insertArrangedSubview($0, at: 0) } + } + + override open func styleDidChange() { + super.styleDidChange() + let cachedStyle = style() + instructionsLabel.textColor = cachedStyle.color.label + contentStackView.setCustomSpacing(cachedStyle.dimension.directionalInsets2.top, after: logButtonsCollectionView) + } + + // MARK: - UICollectionViewDelegate & DataSource + + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return 1 + } + + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OCKButtonLogTaskView.defaultCellIdentifier, for: indexPath) + guard let typedCell = cell as? OCKButtonLogTaskView.DefaultCellType else { return cell } + typedCell.logButton.addTarget(self, action: #selector(didTapLogButton(_:)), for: .touchUpInside) + typedCell.logButton.label.text = loc("LOG") + typedCell.isAccessibilityElement = true + typedCell.accessibilityLabel = loc("LOG") + typedCell.accessibilityHint = loc("DOUBLE_TAP_TO_RECORD_EVENT") + typedCell.accessibilityTraits = .button + return cell + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/OCKChecklistTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKChecklistTaskView.swift new file mode 100644 index 000000000..64caf8c18 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/OCKChecklistTaskView.swift @@ -0,0 +1,243 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A card that displays a vertically stacked checklist of items. In CareKit, this view is intended to display +/// multiple events for a particular task. +/// +/// To insert custom views vertically the view, see `contentStack`. The header is an `OCKHeaderView`. The body has a +/// stack of checklist items and an instructions label underneath. To access the checklist item buttons, for instance +/// to hook them up to target actions, see the `items` array. To modify the checklist, see +/// `updateItem`, `appendItem`, `insertItem`, `removeItem` and `clearItems`. +/// +/// +-------------------------------------------------------+ +/// | +------+ | +/// | | icon | [title] [detail | +/// | | img | [detail] disclosure] | +/// | +------+ | +/// | | +/// | -------------------------------------------------- | +/// | | +/// | +-------------------------------------------------+ | +/// | | [title] [img] | | +/// | +-------------------------------------------------+ | +/// | +-------------------------------------------------+ | +/// | | [title] [img] | | +/// | +-------------------------------------------------+ | +/// | . | +/// | . | +/// | . | +/// | +-------------------------------------------------+ | +/// | | [title] [img] | | +/// | +-------------------------------------------------+ | +/// | | +/// | [instructions] | +/// +-------------------------------------------------------+ +/// +open class OCKChecklistTaskView: OCKView, OCKTaskDisplayable { + + // MARK: Properties + + let contentView: OCKView = { + let view = OCKView() + view.clipsToBounds = true + return view + }() + + private let checklistItemsStackView: OCKStackView = { + let stackView = OCKStackView(style: .separated) + stackView.axis = .vertical + return stackView + }() + + private let headerStackView = OCKStackView.vertical() + + private lazy var headerButton = OCKAnimatedButton(contentView: headerView, highlightOptions: [.defaultDelayOnSelect, .defaultOverlay], + handlesSelection: false) + + /// The vertical stack view that holds the main content in the view. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.isLayoutMarginsRelativeArrangement = true + return stackView + }() + + /// Handles events related to an `OCKTaskDisplayable` object. + public weak var delegate: OCKTaskViewDelegate? + + /// The header that shows a `detailDisclosureImage`. + public let headerView = OCKHeaderView { + $0.showsDetailDisclosure = true + } + + /// Multi-line label beneath the checklist items. + public let instructionsLabel: OCKLabel = { + let label = OCKLabel(textStyle: .caption1, weight: .regular) + label.numberOfLines = 0 + return label + }() + + /// The buttons in the checklist. + public var items: [OCKChecklistItemButton] { + guard let items = checklistItemsStackView.arrangedSubviews as? [OCKChecklistItemButton] else { fatalError("Unsupported type.") } + return items + } + + // MARK: Methods + + override func setup() { + super.setup() + addSubviews() + constrainSubviews() + styleSubviews() + setupGestures() + } + + private func setupGestures() { + headerButton.addTarget(self, action: #selector(didTapView), for: .touchUpInside) + } + + private func styleSubviews() { + contentStackView.setCustomSpacing(0, after: instructionsLabel) + } + + private func addSubviews() { + addSubview(contentView) + contentView.addSubview(headerStackView) + [headerButton, contentStackView].forEach { headerStackView.addArrangedSubview($0) } + [checklistItemsStackView, instructionsLabel].forEach { contentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [contentView, headerStackView, headerView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate( + contentView.constraints(equalTo: self) + + headerStackView.constraints(equalTo: contentView) + + headerView.constraints(equalTo: headerButton.layoutMarginsGuide)) + } + + @objc + private func didTapView() { + delegate?.didSelectTaskView(self, eventIndexPath: .init(row: 0, section: 0)) + } + + @objc + private func eventButtonTapped(_ sender: UIControl) { + guard let row = checklistItemsStackView.arrangedSubviews.firstIndex(of: sender) else { + fatalError("Invalid index") + } + delegate?.taskView(self, didCompleteEvent: !sender.isSelected, at: .init(row: row, section: 0), sender: sender) + } + + private func makeItem(withTitle title: String) -> OCKChecklistItemButton { + let button = OCKChecklistItemButton() + button.addTarget(self, action: #selector(eventButtonTapped(_:)), for: .touchUpInside) + button.label.text = title + button.accessibilityLabel = title + return button + } + + /// Update an item with text. + /// + /// - Parameters: + /// - index: The index of the item to update. + /// - title: The new text for the item. + /// - Returns: The item that was modified. + @discardableResult + public func updateItem(at index: Int, withTitle title: String) -> OCKChecklistItemButton? { + guard index < checklistItemsStackView.arrangedSubviews.count else { return nil } + let button = items[index] + button.label.text = title + return button + } + + /// Insert an item in the checklist. + /// + /// - Parameters: + /// - title: The text displayed in the item. + /// - index: The index at which to insert the item. + /// - animated: Animate the insertion of the view. + /// - Returns: The item that was inserted. + @discardableResult + public func insertItem(withTitle title: String, at index: Int, animated: Bool) -> OCKChecklistItemButton { + let button = makeItem(withTitle: title) + checklistItemsStackView.insertArrangedSubview(button, at: index, animated: animated) + return button + } + + /// Append an item to the checklist. + /// + /// - Parameters: + /// - title: The text displayed in the item. + /// - animated: Animate the appending of the view. + /// - Returns: The view that was appended. + @discardableResult + public func appendItem(withTitle title: String, animated: Bool) -> OCKChecklistItemButton { + let button = makeItem(withTitle: title) + checklistItemsStackView.addArrangedSubview(button, animated: animated) + return button + } + + /// Remove an item from the checkliist. + /// + /// - Parameters: + /// - index: The index for which to remove the item. + /// - animated: Animate the removal of the item. + /// - Returns: The item that was removed from the checklist. + @discardableResult + public func removeItem(at index: Int, animated: Bool) -> OCKChecklistItemButton? { + guard index < checklistItemsStackView.arrangedSubviews.count else { return nil } + let button = items[index] + checklistItemsStackView.removeArrangedSubview(button, animated: animated) + return button + } + + /// Clear all items from the checklist. + /// + /// - Parameter animated: Animate the removal of the items from the checklist. + public func clearItems(animated: Bool) { + checklistItemsStackView.clear(animated: animated) + } + + 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 + contentStackView.spacing = style.dimension.directionalInsets1.top + contentStackView.directionalLayoutMargins = .init(top: 0, + leading: style.dimension.directionalInsets1.leading, + bottom: style.dimension.directionalInsets1.bottom, + trailing: style.dimension.directionalInsets1.trailing) + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/OCKGridTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKGridTaskView.swift new file mode 100644 index 000000000..c1a920dbe --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/OCKGridTaskView.swift @@ -0,0 +1,252 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A card that displays a header, grid of buttons, and a multi-line label In CareKit, this view is intended to display +/// multiple events for a particular task. The state of each button in the grid indicates the completion state of the +/// corresponding event. +/// +/// To insert custom views vertically the view, see `contentStack`. To modify the data in the grid, set a `dataSource` +/// for the `collectionView`. +/// +/// +-------------------------------------------------------+ +/// | | +/// | [title] [detail | +/// | [detail] disclosure] | +/// | | +/// | | +/// | -------------------------------------------------- | +/// | o o o o o o o o | +/// | o o o o o o ... o o | +/// | o o o o o o o o | +/// | | +/// | [instructions] | +/// +-------------------------------------------------------+ +/// +open class OCKGridTaskView: OCKView, OCKTaskDisplayable, UICollectionViewDelegate { + + private enum Constants { + static let estimatedItemHeight: CGFloat = 70 + static let estimatedItemWidth: CGFloat = 80 + } + + // MARK: Properties + + /// The vertical stack view that holds the main content in the view. + public let contentStackView: OCKStackView = { + let stackView = OCKStackView.vertical() + stackView.isLayoutMarginsRelativeArrangement = true + return stackView + }() + + /// Handles events related to an `OCKTaskDisplayable` object. + public weak var delegate: OCKTaskViewDelegate? + + /// A header view that shows a separator and a `detailDisclosureImage`. + public let headerView = OCKHeaderView { + $0.showsSeparator = true + $0.showsDetailDisclosure = true + } + + /// The default cell identifier that is registered for the collection view. + public static let defaultCellIdentifier = "outcome-value" + + /// The default cell type that is used for the `collectionView`. + public typealias DefaultCellType = OCKGridTaskCell + + /// A collection view that sizes itself based on the size of its content. Cells used should have a constant width constraint. The + /// default cell that is used is an `OCKGridTaskView.DefaultCellType` (`OCKGridTaskCell`). Set a data source to control the content + /// of the grid. + public private(set) lazy var collectionView: UICollectionView = { + let collectionView = OCKSelfSizingCollectionView(frame: .zero, collectionViewLayout: makeLayout()) + collectionView.register(OCKGridTaskView.DefaultCellType.self, forCellWithReuseIdentifier: OCKGridTaskView.defaultCellIdentifier) + collectionView.showsVerticalScrollIndicator = false + collectionView.delegate = self + collectionView.backgroundColor = nil + return collectionView + }() + + /// Multi-line label below the `collectionView`. + public let instructionsLabel: OCKLabel = { + let label = OCKLabel(textStyle: .caption1, weight: .regular) + label.numberOfLines = 0 + return label + }() + + let contentView: OCKView = { + let view = OCKView() + view.clipsToBounds = true + return view + }() + + private let headerStackView = OCKStackView.vertical() + + private lazy var headerButton = OCKAnimatedButton(contentView: headerView, highlightOptions: [.defaultDelayOnSelect, .defaultOverlay], + handlesSelection: false) + + // MARK: Methods + + override func setup() { + super.setup() + addSubviews() + constrainSubviews() + setupGestures() + } + + // Find the horizontal margin for a grid given the parameters. + func horizontalMargin(forContainerWidth containerWidth: CGFloat, itemWidth: CGFloat, interItemSpacing: CGFloat, itemCount: Int) -> CGFloat { + var margin = containerWidth + var columns: Int = 0 + // Find the optimal column count by increasing the columns while minimizing the margin. + while margin >= 0 { + + // Equation derived from: + // containerWidth = margin + spacingBetweenItems + widthOfAllItems + // Note: This equation ensure the margin decreases at each loop step + var newMargin = -containerWidth + (interItemSpacing * max(columns - 1, 0).float) + (itemWidth * columns.float) + newMargin.negate() + + // If the new margin has gone negative, the current margin is the minimum + if newMargin < 0 { return margin } + + // If we do not have enough items to fill more columns, stop early + if itemCount <= columns { return newMargin } + + margin = newMargin // Store the new minimum + columns += 1 // Increase the columns and try to further minimize the margin + } + return margin + } + + private func setupGestures() { + headerButton.addTarget(self, action: #selector(didTapView), for: .touchUpInside) + } + + private func addSubviews() { + addSubview(contentView) + contentView.addSubview(headerStackView) + [headerButton, contentStackView].forEach { headerStackView.addArrangedSubview($0) } + [collectionView, instructionsLabel].forEach { contentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [contentView, headerStackView, headerView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate( + contentView.constraints(equalTo: self) + + headerStackView.constraints(equalTo: contentView) + + headerView.constraints(equalTo: headerButton.layoutMarginsGuide, directions: [.horizontal, .top]) + + headerView.constraints(equalTo: headerButton, directions: [.bottom])) + } + + private func makeLayout() -> UICollectionViewLayout { + let layout = UICollectionViewCompositionalLayout { [weak self] _, configuration in + guard let self = self else { return nil } + let spacing = self.style().dimension.directionalInsets2.trailing + + // Scale the item dimensions based on the content size category + let scaledHeight = Constants.estimatedItemHeight.scaled() + let scaledWidth = Constants.estimatedItemWidth.scaled() + + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(scaledWidth), heightDimension: .absolute(scaledHeight)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(scaledHeight)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + group.interItemSpacing = .fixed(spacing) + + let containerWidth = configuration.container.effectiveContentSize.width + let itemCount = self.collectionView.dataSource?.collectionView(self.collectionView, numberOfItemsInSection: 0) ?? 0 + // Create margins to center the content in the container + let margin = self.horizontalMargin(forContainerWidth: containerWidth, itemWidth: scaledWidth, + interItemSpacing: spacing, itemCount: itemCount) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = spacing + section.contentInsets = .init(top: 0, leading: margin / 2.0, bottom: 0, trailing: margin / 2.0) + return section + } + return layout + } + + @objc + private func didTapView() { + delegate?.didSelectTaskView(self, eventIndexPath: .init(row: 0, section: 0)) + } + + @objc + private func didTapCompletionButton(_ sender: UIControl) { + // Find the index path for the tapped button + collectionView.indexPathsForVisibleItems.forEach { + let cell = collectionView.cellForItem(at: $0) as? OCKGridTaskView.DefaultCellType + if cell?.completionButton === sender { + delegate?.taskView(self, didCompleteEvent: !sender.isSelected, at: $0, sender: sender) + return + } + } + } + + private func resetCollectionViewSizing() { + collectionView.collectionViewLayout.invalidateLayout() // To reset the margins and item spacing + } + + override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { + resetCollectionViewSizing() + } + } + + 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 + directionalLayoutMargins = style.dimension.directionalInsets1 + contentStackView.directionalLayoutMargins = style.dimension.directionalInsets1 + resetCollectionViewSizing() + } + + // MARK: - UICollectionViewDelegate + + open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let cell = collectionView.cellForItem(at: indexPath) as? OCKGridTaskView.DefaultCellType else { return } + // Assume the action is successfull and toggle the selected state + cell.completionButton.isSelected.toggle() + // Notify the delegate that an event has been toggled + delegate?.taskView(self, didCompleteEvent: cell.completionButton.isSelected, at: indexPath, sender: cell.completionButton) + } +} + +private extension Int { + var float: CGFloat { return CGFloat(self) } +} diff --git a/CareKitUI/CareKitUI/Components/Task/OCKInstructionsTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKInstructionsTaskView.swift new file mode 100644 index 000000000..ee6a1fe09 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/OCKInstructionsTaskView.swift @@ -0,0 +1,152 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A card that displays a header, multi-line label, and a completion button. In CareKit, this view is +/// intended to display a particular event for a task. The state of the completion button indicates +/// the completion state of the event. +/// +/// To insert custom views vertically the view, see `contentStack` +/// +/// +-------------------------------------------------------+ +/// | | +/// | [title] [detail | +/// | [detail] disclosure] | +/// | | +/// | | +/// | -------------------------------------------------- | +/// | | +/// | [instructions] | +/// | | +/// | +-------------------------------------------------+ | +/// | | [title] | | +/// | +-------------------------------------------------+ | +/// | | +/// +-------------------------------------------------------+ +/// +open class OCKInstructionsTaskView: OCKView, OCKTaskDisplayable { + + // MARK: Properties + + let contentView: OCKView = { + let view = OCKView() + view.clipsToBounds = true + return view + }() + + private lazy var headerButton = OCKAnimatedButton(contentView: headerView, highlightOptions: [.defaultDelayOnSelect, .defaultOverlay], + handlesSelection: false) + + private let headerStackView = OCKStackView.vertical() + + /// A vertical stack view that holds the main content for the view. + public let contentStackView: OCKStackView = { + let stack = OCKStackView.vertical() + stack.isLayoutMarginsRelativeArrangement = true + return stack + }() + + /// Handles events related to an `OCKTaskDisplayable` object. + public weak var delegate: OCKTaskViewDelegate? + + /// The button on the bottom of the view. The background color is the `tintColor` when in a normal state. and gray when + /// in a selected state. + public let completionButton = OCKLabeledButton() + + /// Multi-line label over the `completionButton`. + public let instructionsLabel: OCKLabel = { + let label = OCKLabel(textStyle: .subheadline, weight: .medium) + label.numberOfLines = 0 + return label + }() + + /// A header view that tshows a separator and a `detailDisclosureImage`. + public let headerView = OCKHeaderView { + $0.showsSeparator = true + $0.showsDetailDisclosure = true + } + + // MARK: Methods + + override func setup() { + super.setup() + addSubviews() + styleSubviews() + constrainSubviews() + setupGestures() + } + + private func setupGestures() { + headerButton.addTarget(self, action: #selector(didTapView), for: .touchUpInside) + completionButton.addTarget(self, action: #selector(completionButtonTapped(_:)), for: .touchUpInside) + } + + private func styleSubviews() { + contentStackView.setCustomSpacing(0, after: completionButton) + } + + private func addSubviews() { + addSubview(contentView) + contentView.addSubview(headerStackView) + [headerButton, contentStackView].forEach { headerStackView.addArrangedSubview($0) } + [instructionsLabel, completionButton].forEach { contentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [contentView, headerStackView, headerView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate( + contentView.constraints(equalTo: self) + + headerStackView.constraints(equalTo: contentView) + + headerView.constraints(equalTo: headerButton.layoutMarginsGuide, directions: [.horizontal, .top]) + + headerView.constraints(equalTo: headerButton, directions: [.bottom])) + } + + @objc + private func didTapView() { + delegate?.didSelectTaskView(self, eventIndexPath: .init(row: 0, section: 0)) + } + + @objc + private func completionButtonTapped(_ sender: UIControl) { + delegate?.taskView(self, didCompleteEvent: !sender.isSelected, at: .init(row: 0, section: 0), sender: sender) + } + + 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 + directionalLayoutMargins = style.dimension.directionalInsets1 + contentStackView.directionalLayoutMargins = style.dimension.directionalInsets1 + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/OCKLogTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKLogTaskView.swift new file mode 100644 index 000000000..c6934d12d --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/OCKLogTaskView.swift @@ -0,0 +1,205 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Base class for all log view. Shows an `OCKHeaderView` and a dynamic stack view of log items. +open class OCKLogTaskView: OCKView, OCKTaskDisplayable { + + // MARK: Properties + + private let contentView: OCKView = { + let view = OCKView() + view.clipsToBounds = true + return view + }() + + private lazy var headerButton = OCKAnimatedButton(contentView: headerView, highlightOptions: [.defaultDelayOnSelect, .defaultOverlay], + handlesSelection: false) + + private let headerStackView = OCKStackView.vertical() + + let logItemsStackView: OCKStackView = { + var stackView = OCKStackView(style: .separated) + stackView.showsOuterSeparators = false + return stackView + }() + + /// A vertical stack view that holds the main content for the view. + public let contentStackView: OCKStackView = { + let stack = OCKStackView.vertical() + stack.isLayoutMarginsRelativeArrangement = true + return stack + }() + + /// Handles events related to an `OCKTaskDisplayable` object. + public weak var delegate: OCKTaskViewDelegate? + + /// The header view that shows a separator and a `detailDisclosureImage`. + public let headerView = OCKHeaderView { + $0.showsSeparator = true + $0.showsDetailDisclosure = true + } + + /// The list of buttons in the log. + open var items: [OCKLogItemButton] { + guard let buttons = logItemsStackView.arrangedSubviews as? [OCKLogItemButton] else { fatalError("Unsupported type.") } + return buttons + } + + // MARK: - Methods + + override func setup() { + super.setup() + addSubviews() + constrainSubviews() + setupGestures() + } + + func addSubviews() { + addSubview(contentView) + contentView.addSubview(headerStackView) + [headerButton, contentStackView].forEach { headerStackView.addArrangedSubview($0) } + [logItemsStackView].forEach { contentStackView.addArrangedSubview($0) } + } + + func constrainSubviews() { + [contentView, headerStackView, headerView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + NSLayoutConstraint.activate( + contentView.constraints(equalTo: self) + + headerStackView.constraints(equalTo: contentView) + + headerView.constraints(equalTo: headerButton.layoutMarginsGuide, directions: [.horizontal, .top]) + + headerView.constraints(equalTo: headerButton, directions: [.bottom])) + } + + private func setupGestures() { + headerButton.addTarget(self, action: #selector(didTapView), for: .touchUpInside) + } + + private func makeItem(withTitle title: String?, detail: String?) -> OCKLogItemButton { + let button = OCKLogItemButton() + button.addTarget(self, action: #selector(itemTapped(_:)), for: .touchUpInside) + button.titleLabel.text = title + button.detailLabel.text = detail + button.accessibilityLabel = (detail ?? "") + " " + (title ?? "") + button.accessibilityHint = loc("DOUBLE_TAP_TO_REMOVE_EVENT") + return button + } + + @objc + private func didTapView() { + delegate?.didSelectTaskView(self, eventIndexPath: .init(row: 0, section: 0)) + } + + @objc + private func itemTapped(_ sender: UIControl) { + guard let index = logItemsStackView.arrangedSubviews.firstIndex(of: sender) else { + fatalError("Target was not set up properly.") + } + delegate?.taskView(self, didSelectOutcomeValueAt: index, eventIndexPath: .init(row: 0, section: 0), sender: sender) + } + + 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 + contentStackView.directionalLayoutMargins = style.dimension.directionalInsets1 + } + + /// Update the text for an item at a particular index. + /// + /// - Parameters: + /// - index: The index of the item to update. + /// - title: The title text to display in the item. + /// - detail: The detail text to display in the item. The text is tinted by default. + /// - Returns: The item that was updated. + @discardableResult + open func updateItem(at index: Int, withTitle title: String?, detail: String?) -> OCKLogItemButton? { + guard index < logItemsStackView.arrangedSubviews.count else { return nil } + let button = items[index] + button.accessibilityLabel = title + button.titleLabel.text = title + button.detailLabel.text = detail + return button + } + + /// Insert an item in the list of logged items. + /// + /// - Parameters: + /// - title: The title text to display in the item. + /// - detail: The detail text to display in the item. The text is tinted by default. + /// - index: The index to insert the item in the list of logged items. + /// - animated: Animate the insertion of the logged item. + /// - Returns: The item that was inserted. + @discardableResult + open func insertItem(withTitle title: String?, detail: String?, at index: Int, animated: Bool) -> OCKLogItemButton { + let button = makeItem(withTitle: title, detail: detail) + logItemsStackView.insertArrangedSubview(button, at: index, animated: animated) + return button + } + + /// Append an item to the list of logged items. + /// + /// - Parameters: + /// - title: The detail text to display in the item. The text is tinted by default. + /// - detail: The detail text to display in the item. The text is tinted by default. + /// - animated: Animate appending the item. + /// - Returns: The item that was appended. + @discardableResult + open func appendItem(withTitle title: String?, detail: String?, animated: Bool) -> OCKLogItemButton { + let button = makeItem(withTitle: title, detail: detail) + logItemsStackView.addArrangedSubview(button, animated: animated) + return button + } + + /// Remove an item from the list of logged items. + /// + /// - Parameters: + /// - index: The index of the item to remove. + /// - animated: Animate the removal of the item. + /// - Returns: The item that was removed. + @discardableResult + open func removeItem(at index: Int, animated: Bool) -> OCKLogItemButton? { + guard index < logItemsStackView.arrangedSubviews.count else { return nil } + let button = items[index] + logItemsStackView.removeArrangedSubview(button, animated: animated) + return button + } + + /// Clear all items from the list of logged items. + /// + /// - Parameter animated: Animate clearing the items. + open func clearItems(animated: Bool) { + logItemsStackView.clear(animated: animated) + } +} diff --git a/CareKitUI/CareKitUI/Components/Task/OCKSimpleTaskView.swift b/CareKitUI/CareKitUI/Components/Task/OCKSimpleTaskView.swift new file mode 100644 index 000000000..95d97add1 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/OCKSimpleTaskView.swift @@ -0,0 +1,122 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A card that displays an `OCKHeaderView` and a circular checkmark button `completionButton`. +/// In CareKit, this view is intended to display a particular event for a task. The state of the `completionButton` +/// indicates the completion state of the event. +/// +/// To insert custom views vertically the view, see `contentStack` +/// +/// +-------------------------------------------------------+ +/// | | +/// | [title] [completion | +/// | [detail] button] | +/// | | +/// +-------------------------------------------------------+ +/// +open class OCKSimpleTaskView: OCKView, OCKTaskDisplayable { + + // MARK: Properties + + private let contentView: OCKView = { + let view = OCKView() + view.clipsToBounds = true + return view + }() + + // Button that displays the highlighted state for the view. + private lazy var backgroundButton = OCKAnimatedButton(contentView: horizontalContentStackView, handlesSelection: false) + + private let horizontalContentStackView: OCKStackView = { + let stack = OCKStackView.horizontal() + stack.alignment = .center + return stack + }() + + /// The button in the trailing end of the card. Has an image that is defaulted to a checkmark when selected. + public let completionButton: OCKCheckmarkButton = { + let button = OCKCheckmarkButton() + button.isUserInteractionEnabled = false + return button + }() + + /// Handles events related to an `OCKTaskDisplayable` object. + public weak var delegate: OCKTaskViewDelegate? + + /// A default version of an `OCKHeaderView`. + public let headerView = OCKHeaderView() + + // MARK: Methods + + override func setup() { + super.setup() + setupGestures() + addSubviews() + constrainSubviews() + + isAccessibilityElement = true + accessibilityTraits = .button + } + + private func setupGestures() { + backgroundButton.addTarget(self, action: #selector(didCompleteEvent(_:)), for: .touchUpInside) + } + + private func addSubviews() { + addSubview(contentView) + contentView.addSubview(backgroundButton) + [headerView, completionButton].forEach { horizontalContentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [contentView, backgroundButton, horizontalContentStackView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + completionButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + NSLayoutConstraint.activate( + contentView.constraints(equalTo: self) + + backgroundButton.constraints(equalTo: contentView) + + horizontalContentStackView.constraints(equalTo: backgroundButton.layoutMarginsGuide)) + } + + @objc + private func didCompleteEvent(_ sender: UIControl) { + completionButton.setSelected(!completionButton.isSelected, animated: true) + delegate?.taskView(self, didCompleteEvent: completionButton.isSelected, at: .init(row: 0, section: 0), sender: sender) + } + + 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/CareKitUI/CareKitUI/Components/Task/OCKTaskDisplayable.swift b/CareKitUI/CareKitUI/Components/Task/OCKTaskDisplayable.swift new file mode 100644 index 000000000..a33221369 --- /dev/null +++ b/CareKitUI/CareKitUI/Components/Task/OCKTaskDisplayable.swift @@ -0,0 +1,71 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// Any object that can display and handle interactions with a task. +public protocol OCKTaskDisplayable: AnyObject { + /// Handles events related to an `OCKTaskDisplayable` object. + var delegate: OCKTaskViewDelegate? { get set } +} + +/// Handles events related to an `OCKTaskDisplayable` object. +public protocol OCKTaskViewDelegate: AnyObject { + + /// Called when an event is completed. + /// - Parameters: + /// - taskView: View displaying the event. + /// - isComplete: True if the event is complete. + /// - indexPath: Index path of the event. + /// - sender: Sender that initiated the completion. + func taskView(_ taskView: UIView & OCKTaskDisplayable, didCompleteEvent isComplete: Bool, at indexPath: IndexPath, sender: Any?) + + /// Called when an outcome value was selected for a particular event. + /// - Parameters: + /// - taskView: View displaying the outcome value. + /// - index: Indec of the outcome value in the event's oucome. + /// - eventIndexPath: index path of the event. + /// - sender: Sender that initiated the selection. + func taskView(_ taskView: UIView & OCKTaskDisplayable, didSelectOutcomeValueAt index: Int, eventIndexPath: IndexPath, sender: Any?) + + /// Called when an outcome value has been created at a particular index. + /// - Parameters: + /// - taskView: View displaying the outcome. + /// - index: Index of the new outcome value. + /// - eventIndexPath: Index of the event. + /// - sender: Sender that initiated the outcome value creation. + func taskView(_ taskView: UIView & OCKTaskDisplayable, didCreateOutcomeValueAt index: Int, eventIndexPath: IndexPath, sender: Any?) + + /// Called when the task view has been selected. + /// - Parameters: + /// - taskView: The task view that has been selected. + /// - eventIndexPath: The index path of the event displayed by the task view. + func didSelectTaskView(_ taskView: UIView & OCKTaskDisplayable, eventIndexPath: IndexPath) +} diff --git a/CareKitUI/CareKitUI/Detail View/OCKDetailView.swift b/CareKitUI/CareKitUI/Detail View/OCKDetailView.swift new file mode 100644 index 000000000..aefa40a33 --- /dev/null +++ b/CareKitUI/CareKitUI/Detail View/OCKDetailView.swift @@ -0,0 +1,109 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit + +/// A view intended to display fine grained details. The view contains a configurable image, title, and instructions. To add +/// custom views, insert into the `contentStackView`. +open class OCKDetailView: OCKView { + // MARK: Properties + + private var contentStackViewTopConstraint: NSLayoutConstraint? + private var imageViewHeightConstraint: NSLayoutConstraint? + + /// Configurable image that spans the width of the view. + public let imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + return imageView + }() + + /// Primary multi-line label. + public let titleLabel: OCKLabel = { + let titleLabel = OCKLabel(textStyle: .title2, weight: .semibold) + titleLabel.numberOfLines = 0 + return titleLabel + }() + + /// Secondary multi-line label + public let instructionsLabel: OCKLabel = { + let label = OCKLabel(textStyle: .subheadline, weight: .medium) + label.numberOfLines = 0 + return label + }() + + /// The vertical stack view that holds the main content for the view. + public let contentStackView = OCKStackView.vertical() + + // MARK: Methods + + override func setup() { + super.setup() + addSubviews() + constrainSubviews() + } + + private func addSubviews() { + [imageView, contentStackView].forEach { addSubview($0) } + [titleLabel, instructionsLabel].forEach { contentStackView.addArrangedSubview($0) } + } + + private func constrainSubviews() { + [imageView, contentStackView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + imageViewHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: 0) + contentStackViewTopConstraint = contentStackView.topAnchor.constraint(equalTo: imageView.bottomAnchor) + + NSLayoutConstraint.activate([ + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageViewHeightConstraint!, + + contentStackViewTopConstraint!, + contentStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + contentStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + contentStackView.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor) + ]) + } + + override open func styleDidChange() { + super.styleDidChange() + let cachedStyle = style() + backgroundColor = cachedStyle.color.customBackground + directionalLayoutMargins = cachedStyle.dimension.directionalInsets1 + instructionsLabel.textColor = cachedStyle.color.label + imageView.backgroundColor = cachedStyle.color.secondaryCustomFill + titleLabel.textColor = cachedStyle.color.label + imageViewHeightConstraint?.constant = cachedStyle.dimension.imageHeight1 + contentStackViewTopConstraint?.constant = cachedStyle.dimension.directionalInsets1.top + contentStackView.spacing = cachedStyle.dimension.directionalInsets1.top / 2.0 + } +} diff --git a/CareKitUI/CareKitUI/Shared/Components/CircularCompletionView.swift b/CareKitUI/CareKitUI/Shared/Components/CircularCompletionView.swift index 5bf5f5762..5aaff733e 100644 --- a/CareKitUI/CareKitUI/Shared/Components/CircularCompletionView.swift +++ b/CareKitUI/CareKitUI/Shared/Components/CircularCompletionView.swift @@ -56,7 +56,7 @@ public struct CircularCompletionView<Content: View>: View { GeometryReader { geo in ZStack { Circle().fill(self.fillColor) - Circle().strokeBorder(Color.accentColor, lineWidth: self.lineWidth(for: geo.size)) + Circle().strokeBorder(Color.accentColor, lineWidth: style.borderWidth(forContainer: geo.size)) } .inverseMask(self.content) } @@ -84,19 +84,6 @@ public struct CircularCompletionView<Content: View>: View { self.isComplete = isComplete self.content = content() } - - // Scaled line width for the current frame size. - private func lineWidth(for containerSize: CGSize) -> CGFloat { - let border = style.appearance.borderWidth2...style.appearance.borderWidth1 - let dimension = style.dimension.buttonHeight4...style.dimension.buttonHeight1 - let currentDimension = min(containerSize.width, containerSize.height) - - // Get the distance factor between the two dimension values. - let factor = currentDimension.interpolationFactor(for: dimension) - - // Flip the factor because we want a higher border width for a smaller dimension. - return border.lowerBound.interpolated(to: border.upperBound, factor: 1 - factor) - } } #if DEBUG @@ -111,12 +98,14 @@ struct CircularCompletionViewPreviews: PreviewProvider { .frame(height: 90) } - CircularCompletionView(isComplete: true) { - Text("") - .frame(width: 30, height: 30) + CircularCompletionView(isComplete: false) { + Image(systemName: "checkmark") + .resizable() + .padding() + .frame(width: 50, height: 50) } - CircularCompletionView(isComplete: false) { + CircularCompletionView(isComplete: true) { Image(systemName: "checkmark") .resizable() .padding() diff --git a/CareKitUI/CareKitUI/Shared/Extensions/Number+Extensions.swift b/CareKitUI/CareKitUI/Shared/Extensions/Number+Extensions.swift index 7a407006c..64f336da6 100644 --- a/CareKitUI/CareKitUI/Shared/Extensions/Number+Extensions.swift +++ b/CareKitUI/CareKitUI/Shared/Extensions/Number+Extensions.swift @@ -50,14 +50,6 @@ extension CGFloat { .clamped(to: (self...end)) } - /// Get the interpolation distance factor between 0 and 1 of this value in the given range. - func interpolationFactor(for range: ClosedRange<CGFloat>) -> CGFloat { - let denominator = range.upperBound - self - guard denominator > 0 else { return 1 } - return ((self - range.lowerBound) / denominator) - .clamped(to: 0...1) - } - func clamped(to range: ClosedRange<CGFloat>) -> CGFloat { return Swift.max(Swift.min(self, range.upperBound), range.lowerBound) } diff --git a/CareKitUI/CareKitUI/Shared/Extensions/OCKStyler+Extension.swift b/CareKitUI/CareKitUI/Shared/Extensions/OCKStyler+Extension.swift new file mode 100644 index 000000000..ec7eab3c2 --- /dev/null +++ b/CareKitUI/CareKitUI/Shared/Extensions/OCKStyler+Extension.swift @@ -0,0 +1,40 @@ +/* + 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 UIKit + +extension OCKStyler { + + /// Scaled border width for the current frame size. + func borderWidth(forContainer container: CGSize) -> CGFloat { + let currentDimension = min(container.width, container.height) + return currentDimension < dimension.buttonHeight3 ? appearance.borderWidth2.scaled() : appearance.borderWidth1.scaled() + } +} diff --git a/CareKitUI/CareKitUI/Shared/Style/OCKAppearanceStyler.swift b/CareKitUI/CareKitUI/Shared/Style/OCKAppearanceStyler.swift index 80cd3fdc7..d5a3cdec9 100644 --- a/CareKitUI/CareKitUI/Shared/Style/OCKAppearanceStyler.swift +++ b/CareKitUI/CareKitUI/Shared/Style/OCKAppearanceStyler.swift @@ -58,8 +58,8 @@ public extension OCKAppearanceStyler { var cornerRadius1: CGFloat { 15 } var cornerRadius2: CGFloat { 12 } - var borderWidth1: CGFloat { 2 } - var borderWidth2: CGFloat { 1 } + var borderWidth1: CGFloat { 3 } + var borderWidth2: CGFloat { 2 } } /// Concrete object for appearance constants. diff --git a/CareKitUI/CareKitUI/Shared/Task/SimpleTaskView.swift b/CareKitUI/CareKitUI/Shared/Task/SimpleTaskView.swift index 8f77acb23..e01f2656c 100644 --- a/CareKitUI/CareKitUI/Shared/Task/SimpleTaskView.swift +++ b/CareKitUI/CareKitUI/Shared/Task/SimpleTaskView.swift @@ -176,6 +176,7 @@ struct SimpleTaskView_Previews: PreviewProvider { SimpleTaskView(title: Text("Title"), detail: Text("Detail"), isComplete: false, action: {}) SimpleTaskView(title: Text("Title"), detail: Text("Detail"), isComplete: true, action: {}) } + .padding() } } #endif diff --git a/CareKitUI/CareKitUI/Supporting Files/Localization/OCKLocalization.swift b/CareKitUI/CareKitUI/Supporting Files/Localization/OCKLocalization.swift new file mode 100644 index 000000000..89a047d22 --- /dev/null +++ b/CareKitUI/CareKitUI/Supporting Files/Localization/OCKLocalization.swift @@ -0,0 +1,98 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COP + YRIGHT 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 + +/// A hook for the free function `loc` to access the framework bundle for localizations +public class OCKLocalization { + + /// An `NSLocalizedString` wrapper that searches for overrides in a main bundle before falling back to the framework provided strings + public static func localized( + _ key: String, + tableName: String? = nil, + bundle: Bundle? = nil, + value: String = "", + comment: String = "" + ) -> String { + + // Find a specified or main bundle override for the given `key` + let str: String = { + switch bundle { + case .some(let bundle): + return NSLocalizedString( + key, + tableName: tableName, + bundle: bundle, + value: value, + comment: comment + ) + case .none: + return NSLocalizedString( + key, + tableName: tableName, + value: value, + comment: comment + ) + } + }() + + // If the string does not equal the key, there was an override in the main bundle + guard str == key else { return str } + + // Use this framework's localizable strings if an override is not found + return NSLocalizedString( + key, + tableName: tableName, + bundle: Bundle(for: OCKLocalization.self), + value: value, + comment: comment + ) + + } + +} + +/// A localization string wrapper to access framework specific strings +/// - Parameter key: The `NSLocalizedString` key +/// - Parameter comment: The `NSLocalizedString` comment +/// +/// This is a free function for developer convenience. +public func loc(_ key: String, _ comment: String = "") -> String { + + return OCKLocalization.localized( + key, + tableName: nil, + bundle: nil, + value: "", + comment: comment + ) + +} diff --git a/CareKitUI/CareKitUI/Supporting Files/Localization/en.lproj/Localizable.strings b/CareKitUI/CareKitUI/Supporting Files/Localization/en.lproj/Localizable.strings new file mode 100644 index 000000000..189f9c9f9 --- /dev/null +++ b/CareKitUI/CareKitUI/Supporting Files/Localization/en.lproj/Localizable.strings @@ -0,0 +1,63 @@ +/* +Copyright (c) 2019, Apple Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder(s) nor the names of any contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. No license is granted to the trademarks of +the copyright holders even if such marks are included in this software. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +"ADDRESS" = "Address"; +"ANNOUNCE_EVENT_DELETED" = "Logged event was deleted"; +"CANCEL" = "Cancel"; +"CALL" = "Call"; +"COMPLETED" = "Completed"; +"CONTACTS" = "Contacts"; +"DELETE" = "Delete"; +"DONE" = "Done"; +"DOUBLE_TAP_MAP" = "Double-tap to open directions in Maps"; +"DOUBLE_TAP_TO_COMPLETE" = "Double-tap to mark as completed"; +"DOUBLE_TAP_TO_INCOMPLETE" = "Double-tap to mark as incomplete"; +"DOUBLE_TAP_TO_RECORD_EVENT" = "Double-tap to record an event"; +"DOUBLE_TAP_TO_REMOVE_EVENT" = "Double-tap to removed logged event"; +"EMAIL" = "E-mail"; +"EVENT" = "Event"; +"GOAL" = "Goal"; +"HIGH" = "High"; +"INCOMPLETE" = "Incomplete"; +"LOG" = "Log"; +"LOG_ENTRY" = "Log Entry"; +"LOGGING" = "Logging"; +"LOW" = "Low"; +"MARK_COMPLETE" = "Mark as Completed"; +"MESSAGE" = "Message"; +"NO_TASKS" = "No Tasks"; +"NO_EVENTS" = "No Events"; +"PROGRESS" = "Progress"; +"SEARCH" = "Search"; +"SELECTED" = "Selected"; +"TASKS" = "Tasks"; +"THREE_FINGER_SWIPE_DAY" = "Three-finger swipe to go to next or previous day"; +"THREE_FINGER_SWIPE_WEEK" = "Three-finger swipe to go to next or previous week"; +"TODAY" = "Today"; diff --git a/CareKitUI/CareKitUI/Supporting Files/Localization/en.lproj/Localizable.stringsdict b/CareKitUI/CareKitUI/Supporting Files/Localization/en.lproj/Localizable.stringsdict new file mode 100644 index 000000000..d823f17ed --- /dev/null +++ b/CareKitUI/CareKitUI/Supporting Files/Localization/en.lproj/Localizable.stringsdict @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>EVENTS_REMAINING</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@events_remaining@</string> + <key>events_remaining</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>i</string> + <key>zero</key> + <string>0 remaining</string> + <key>one</key> + <string>1 remaining</string> + <key>two</key> + <string></string> + <key>few</key> + <string></string> + <key>many</key> + <string></string> + <key>other</key> + <string>%i remaining</string> + </dict> + </dict> +</dict> +</plist> diff --git a/CareKitUI/CareKitUI/SwiftUI/CardView.swift b/CareKitUI/CareKitUI/SwiftUI/CardView.swift new file mode 100644 index 000000000..47e76f05c --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/CardView.swift @@ -0,0 +1,115 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI + +/// A card whose content can be injected. +/// +/// # Style +/// The card supports styling using `careKitStyle(_:)`. +/// +/// # Composing Cards +/// To combine SwiftUI views with CareKit views that are already inside of a `CardView`, wrap the views in a new `CardView`. A CareKit view inside of +/// a `CardView` is rendered without its border and background, since it inherits those visual affordances from the surrounding `CardView`. +/// +/// ``` +/// CardView { +/// CardView { +/// Text("Only the outer card's visual features are rendered.") +/// } +/// } +/// ``` +public struct CardView<Content: View>: View { + + // MARK: - Properties + + @Environment(\.careKitStyle) private var style + @Environment(\.cardEnabled) private var cardEnabled + + private var stackedContent: some View { + VStack(alignment: .leading, spacing: style.dimension.directionalInsets1.top) { + content + } + } + + private let content: Content + + public var body: some View { + cardEnabled ? + ViewBuilder.buildEither(first: stackedContent.modifier(CardModifier(style: self.style))) : + ViewBuilder.buildEither(second: stackedContent) + } + + // MARK: - Init + + /// Create a card with injected content. + /// - Parameter content: Content view injected into the card. + public init(@ViewBuilder content: () -> Content) { + self.content = content() + } +} + +private struct CardModifier: ViewModifier { + + let style: OCKStyler + + func body(content: Content) -> some View { + content + .padding() + .background(GeometryReader { geometry in + RoundedRectangle(cornerRadius: self.style.appearance.cornerRadius2, style: .continuous) + .frame(width: geometry.size.width, height: geometry.size.height) + .foregroundColor(Color(self.style.color.secondaryCustomGroupedBackground)) + .shadow(color: Color(hue: 0, saturation: 0, brightness: 0, opacity: Double(self.style.appearance.shadowOpacity1)), + radius: self.style.appearance.shadowRadius1, + x: self.style.appearance.shadowOffset1.width, + y: self.style.appearance.shadowOffset1.height) + }).cardEnabled(false) + } +} + +// MARK: - Environment + +private struct CardEnabledEnvironmentKey: EnvironmentKey { + static var defaultValue = true +} + +private extension EnvironmentValues { + var cardEnabled: Bool { + get { self[CardEnabledEnvironmentKey.self] } + set { self[CardEnabledEnvironmentKey.self] = newValue } + } +} + +private extension View { + func cardEnabled(_ enabled: Bool) -> some View { + return self.environment(\.cardEnabled, enabled) + } +} diff --git a/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift b/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift new file mode 100644 index 000000000..8190152cd --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/HeaderView.swift @@ -0,0 +1,77 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI + +/// Header used for most CareKit cards. +/// +/// # Style +/// The card supports styling using `careKitStyle(_:)`. +/// +/// ``` +/// +----------------------------------------+ +/// | | +/// | <Title> | +/// | <Detail> | +/// | | +/// +----------------------------------------+ +/// ``` +public struct HeaderView: View { + + // MARK: - Properties + + @Environment(\.careKitStyle) private var style + + private let title: Text + private let detail: Text? + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: style.dimension.directionalInsets1.top / 4.0) { + title + .font(.headline) + .fontWeight(.bold) + detail? + .font(.caption) + .fontWeight(.medium) + }.foregroundColor(Color.primary) + } + } + + // MARK: - Init + + /// Create an instance. + /// - Parameter title: The title text to display above the detail. + /// - Parameter detail: The detail text to display below the title. + public init(title: Text, detail: Text?) { + self.title = title + self.detail = detail + } +} diff --git a/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift b/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift new file mode 100644 index 000000000..5fc67baff --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/InstructionsTaskView.swift @@ -0,0 +1,163 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +import Foundation +import SwiftUI + +/// A card that displays a header view, multi-line label, and a completion button. +/// +/// In CareKit, this view is intended to display a particular event for a task. The state of the button indicates the completion state of the event. +/// +/// # Style +/// The card supports styling using `careKitStyle(_:)`. +/// +/// ``` +/// +-------------------------------------------------------+ +/// | | +/// | <Title> | +/// | <Detail> | +/// | | +/// | -------------------------------------------------- | +/// | | +/// | <Instructions> | +/// | | +/// | +-------------------------------------------------+ | +/// | | <Completion Button> | | +/// | +-------------------------------------------------+ | +/// | | +/// +-------------------------------------------------------+ +/// ``` +public struct InstructionsTaskView<Header: View, Footer: View>: View { + + // MARK: - Properties + + @Environment(\.careKitStyle) private var style + + private let header: Header + private let footer: Footer + private let instructions: Text? + + public var body: some View { + CardView { + VStack { + header + } + Divider() + instructions? + .font(.subheadline) + .fontWeight(.medium) + .lineLimit(nil) + VStack { + footer + } + } + } + + // MARK: - Init + + /// Create an instance. + /// - Parameter instructions: Instructions text to display under the header. + /// - Parameter header: Header to inject at the top of the card. Specified content will be stacked vertically. + /// - Parameter footer: View to inject under the instructions. Specified content will be stacked vertically. + public init(instructions: Text?, @ViewBuilder header: () -> Header, @ViewBuilder footer: () -> Footer) { + self.instructions = instructions + self.header = header() + self.footer = footer() + } +} + +public extension InstructionsTaskView where Header == HeaderView { + + /// Create an instance. + /// - Parameter title: Title text to display in the header. + /// - Parameter detail: Detail text to display in the header. + /// - Parameter instructions: Instructions text to display under the header. + /// - Parameter footer: View to inject under the instructions. Specified content will be stacked vertically. + init(title: Text, detail: Text?, instructions: Text?, @ViewBuilder footer: () -> Footer) { + self.init(instructions: instructions, header: { + Header(title: title, detail: detail) + }, footer: footer) + } +} + +public extension InstructionsTaskView where Footer == _InstructionsTaskViewFooter { + + /// Create an instance. + /// - Parameter isComplete: True if the button under the instructions is in the completed. + /// - Parameter instructions: Instructions text to display under the header. + /// - Parameter action: Action to perform when the button is tapped. + /// - Parameter header: Header to inject at the top of the card. Specified content will be stacked vertically. + init(isComplete: Bool, instructions: Text?, action: (() -> Void)?, @ViewBuilder header: () -> Header) { + self.init(instructions: instructions, header: header, footer: { + _InstructionsTaskViewFooter(isComplete: isComplete, action: action) + }) + } +} + +public extension InstructionsTaskView where Header == HeaderView, Footer == _InstructionsTaskViewFooter { + + /// Create an instance. + /// - Parameter title: Title text to display in the header. + /// - Parameter detail: Detail text to display in the header. + /// - Parameter instructions: Instructions text to display under the header. + /// - Parameter isComplete: True if the button under the instructions is in the completed state. + /// - Parameter action: Action to perform when the button is tapped. + init(title: Text, detail: Text?, instructions: Text?, isComplete: Bool, action: (() -> Void)?) { + self.init(instructions: instructions, header: { + Header(title: title, detail: detail) + }, footer: { + _InstructionsTaskViewFooter(isComplete: isComplete, action: action) + }) + } +} + +/// The default footer used by an `InstructionsTaskView`. +public struct _InstructionsTaskViewFooter: View { + + @Environment(\.careKitStyle) private var style + + private var text: String { + isComplete ? loc("COMPLETED") : loc("MARK_COMPLETE") + } + + fileprivate let isComplete: Bool + fileprivate let action: (() -> Void)? + + public var body: some View { + Button(action: action ?? {}) { + RectangularCompletionView(isComplete: isComplete) { + HStack { + Spacer() + Text(text) + Spacer() + } + } + }.buttonStyle(NoHighlightStyle()) + } +} diff --git a/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift b/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift new file mode 100644 index 000000000..f462fad5e --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/NoHighlightStyle.swift @@ -0,0 +1,38 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI + +/// Turns off the highlighted (AKA pressed) state. +struct NoHighlightStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + } +} diff --git a/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift b/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift new file mode 100644 index 000000000..caf539b42 --- /dev/null +++ b/CareKitUI/CareKitUI/SwiftUI/RectangularCompletionView.swift @@ -0,0 +1,76 @@ +/* + Copyright (c) 2020, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation +import SwiftUI + +/// A view that denotes a completion state. The style of the view differs based on the completion state. +/// +/// # Style +/// The view supports styling using `careKitStyle(_:)`. +/// +/// ``` +/// +----------------------+ +/// | <Content> | +/// +----------------------+ +/// ``` +public struct RectangularCompletionView<Content: View>: View { + + @Environment(\.careKitStyle) private var style + + private var foregroundColor: Color { + isComplete ? Color.accentColor : Color(style.color.white) + } + + private var backgroundColor: Color { + isComplete ? .init(style.color.tertiaryCustomFill) : .accentColor + } + + private let content: Content + private let isComplete: Bool + + public var body: some View { + VStack { content } + .padding() + .font(Font.subheadline.weight(.medium)) + .foregroundColor(foregroundColor) + .background(backgroundColor) + .cornerRadius(style.appearance.cornerRadius2) + } + + /// Create an instance. + /// - Parameters: + /// - isComplete: The completion state that affects the style of the view. + /// - content: The content of the view. The content will be vertically stacked. + public init(isComplete: Bool, @ViewBuilder content: () -> Content) { + self.isComplete = isComplete + self.content = content() + } +} diff --git a/CareKitUI/CareKitUI/iOS/Calendar/OCKWeekCalendarView.swift b/CareKitUI/CareKitUI/iOS/Calendar/OCKWeekCalendarView.swift index 1bf9e7be6..47c7e2440 100644 --- a/CareKitUI/CareKitUI/iOS/Calendar/OCKWeekCalendarView.swift +++ b/CareKitUI/CareKitUI/iOS/Calendar/OCKWeekCalendarView.swift @@ -46,7 +46,7 @@ open class OCKWeekCalendarView: OCKView, OCKCalendarDisplayable { // MARK: Properties /// The currently selected date in the calendar. - public private (set) var selectedDate = Date() + public private (set) var selectedDate: Date /// Handles events related to an `OCKCalendarDisplayable` object. public weak var delegate: OCKCalendarViewDelegate? @@ -88,12 +88,15 @@ open class OCKWeekCalendarView: OCKView, OCKCalendarDisplayable { /// - date: Will display the week of the provided date. public init(weekOfDate date: Date) { self.dateInterval = Calendar.current.dateIntervalOfWeek(for: date) - selectedDate = date + self.selectedDate = dateInterval.start super.init() + + selectDate(selectedDate, shouldValidateDay: false) } + @available(*, unavailable) public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) + fatalError("Initializer not supported") } // MARK: - Methods @@ -113,6 +116,35 @@ open class OCKWeekCalendarView: OCKView, OCKCalendarDisplayable { updateRingLabels() } + /// If a date is valid, select it + /// - Parameters: + /// - date: The date to select. + /// - shouldValidateDay: True if the date should be validated before selecting. + /// - Returns: True if the date was selected. + @discardableResult + private func selectDate(_ date: Date, shouldValidateDay: Bool) -> Bool { + + // Do not allow re-selection of the current selected date + guard !shouldValidateDay || !Calendar.current.isDate(date, inSameDayAs: selectedDate) else { + return false + } + + selectValidatedDate(date) + return true + } + + /// Select a date that has been validated. + private func selectValidatedDate(_ date: Date) { + completionRingButtons.first(where: { $0.isSelected })?.isSelected = false + if let ring = completionRingFor(date: date) { + ring.isSelected = true + selectedDate = date + } else { + showDate(date) + selectDate(date) + } + } + private func updateRingLabels() { let numberOfDays = Calendar.current.dateComponents([.day], from: dateInterval.start, to: dateInterval.end).day! for index in 0...numberOfDays { @@ -127,26 +159,18 @@ open class OCKWeekCalendarView: OCKView, OCKCalendarDisplayable { @objc private func handleSelection(sender: UIControl) { - for ring in completionRingButtons where ring != sender { - ring.isSelected = false - } - sender.isSelected = true guard let ringIndex = (completionRingButtons as [UIControl]).firstIndex(of: sender) else { fatalError("Unexpected button") } - selectedDate = dateAt(index: ringIndex) - delegate?.calendarView(self, didSelectDate: selectedDate, at: ringIndex, sender: sender) + let dateToSelect = dateAt(index: ringIndex) + + if selectDate(dateToSelect, shouldValidateDay: true) { + self.delegate?.calendarView(self, didSelectDate: dateToSelect, at: ringIndex, sender: sender) + } } /// Select the completion ring that corresponds to the given date. /// - Parameter date: The date of the ring to select. public func selectDate(_ date: Date) { - completionRingButtons.first(where: { $0.isSelected })?.isSelected = false - if let ring = completionRingFor(date: date) { - ring.isSelected = true - selectedDate = date - } else { - showDate(date) - selectDate(date) - } + selectDate(date, shouldValidateDay: true) } /// Get the completion ring that corresponds to a particular date. @@ -163,6 +187,7 @@ open class OCKWeekCalendarView: OCKView, OCKCalendarDisplayable { public func showDate(_ date: Date) { dateInterval = Calendar.current.dateIntervalOfWeek(for: date) updateRingLabels() + selectDate(date) } override open func styleDidChange() { diff --git a/CareKitUI/CareKitUI/iOS/Controls/OCKCheckmarkButton.swift b/CareKitUI/CareKitUI/iOS/Controls/OCKCheckmarkButton.swift index 5bf3218d8..30e4c24ff 100644 --- a/CareKitUI/CareKitUI/iOS/Controls/OCKCheckmarkButton.swift +++ b/CareKitUI/CareKitUI/iOS/Controls/OCKCheckmarkButton.swift @@ -49,11 +49,6 @@ open class OCKCheckmarkButton: OCKAnimatedButton<UIView> { self?.invalidateIntrinsicContentSize() } - 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) } @@ -75,7 +70,7 @@ open class OCKCheckmarkButton: OCKAnimatedButton<UIView> { override open func layoutSubviews() { super.layoutSubviews() - updateLayers(for: bounds, borderWidth: lineWidth.scaledValue) + updateLayers(for: bounds, style: style()) } // MARK: Methods @@ -102,7 +97,9 @@ open class OCKCheckmarkButton: OCKAnimatedButton<UIView> { ]) } - private func updateLayers(for bounds: CGRect, borderWidth: CGFloat) { + private func updateLayers(for bounds: CGRect, style: OCKStyler) { + let borderWidth = style.borderWidth(forContainer: bounds.size) + // Outer mask to make the view a circle let circleMask = UIBezierPath(ovalIn: bounds) @@ -124,7 +121,7 @@ open class OCKCheckmarkButton: OCKAnimatedButton<UIView> { super.styleDidChange() let style = self.style() height.update(withContainer: style) - lineWidth.update(withContainer: style) + updateLayers(for: bounds, style: style) imageViewPointSize.update(withContainer: style) if isSelected { imageView.tintColor = style.color.customBackground @@ -139,7 +136,8 @@ open class OCKCheckmarkButton: OCKAnimatedButton<UIView> { override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { - [lineWidth, height, imageViewPointSize].forEach { $0.apply() } + [height, imageViewPointSize].forEach { $0.apply() } + updateLayers(for: bounds, style: style()) } } diff --git a/CareKitUI/CareKitUI/iOS/Detail View/OCKDetailView.swift b/CareKitUI/CareKitUI/iOS/Detail View/OCKDetailView.swift index f3bfbef5c..3e2950947 100644 --- a/CareKitUI/CareKitUI/iOS/Detail View/OCKDetailView.swift +++ b/CareKitUI/CareKitUI/iOS/Detail View/OCKDetailView.swift @@ -185,19 +185,17 @@ open class OCKDetailView: OCKView, UIScrollViewDelegate { NSLayoutConstraint.activate(constraints) } + // Note: This should always be called on the main thread private func updateBody(with html: StyledHTML?) { - // Note: Attributed text needs to be created on the main thread. - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.bodyLabel.attributedText = html?.attributedText(labelWidth: self.bodyLabel.frame.width, - interfaceStyle: self.traitCollection.userInterfaceStyle) - } + bodyLabel.attributedText = html?.attributedText( + labelWidth: bodyLabel.frame.width, + interfaceStyle: traitCollection.userInterfaceStyle + ) } override open func layoutSubviews() { super.layoutSubviews() bodyLabel.preferredMaxLayoutWidth = frame.inset(by: layoutMargins).size.width - updateBody(with: html) // Reload the html when the label's bounds have changed } override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { diff --git a/CareKitUI/CareKitUI/iOS/Labels/OCKLabel.swift b/CareKitUI/CareKitUI/iOS/Labels/OCKLabel.swift index df8ba6c05..3d201f745 100644 --- a/CareKitUI/CareKitUI/iOS/Labels/OCKLabel.swift +++ b/CareKitUI/CareKitUI/iOS/Labels/OCKLabel.swift @@ -107,6 +107,8 @@ open class OCKLabel: UILabel, OCKStylable { private func setup() { preservesSuperviewLayoutMargins = true adjustsFontForContentSizeCategory = false + lineBreakMode = .byCharWrapping + clipsToBounds = false styleDidChange() } diff --git a/CareKitUI/CareKitUI/iOS/Task/OCKSimpleTaskView.swift b/CareKitUI/CareKitUI/iOS/Task/OCKSimpleTaskView.swift index e70141ab6..7ab981444 100644 --- a/CareKitUI/CareKitUI/iOS/Task/OCKSimpleTaskView.swift +++ b/CareKitUI/CareKitUI/iOS/Task/OCKSimpleTaskView.swift @@ -102,7 +102,9 @@ open class OCKSimpleTaskView: OCKView, OCKTaskDisplayable { private func constrainSubviews() { [contentView, backgroundButton, horizontalContentStackView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } completionButton.setContentHuggingPriority(.required, for: .horizontal) - completionButton.setContentCompressionResistancePriority(.required, for: .horizontal) + completionButton.setContentHuggingPriority(.defaultLow, for: .horizontal) + completionButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + headerView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) NSLayoutConstraint.activate( contentView.constraints(equalTo: self) + backgroundButton.constraints(equalTo: contentView) + diff --git a/CareKitUI/CareKitUI/iOS/Views/OCKHeaderView.swift b/CareKitUI/CareKitUI/iOS/Views/OCKHeaderView.swift index 81572a535..90d25c886 100644 --- a/CareKitUI/CareKitUI/iOS/Views/OCKHeaderView.swift +++ b/CareKitUI/CareKitUI/iOS/Views/OCKHeaderView.swift @@ -81,6 +81,7 @@ open class OCKHeaderView: OCKView { label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping label.animatesTextChanges = true + label.adjustsFontSizeToFitWidth = true return label }() @@ -90,6 +91,7 @@ open class OCKHeaderView: OCKView { label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping label.animatesTextChanges = true + label.adjustsFontSizeToFitWidth = true return label }() diff --git a/CareKitUI/CareKitUITests/TestGridTaskView.swift b/CareKitUI/CareKitUITests/TestGridTaskView.swift new file mode 100644 index 000000000..3b98c9823 --- /dev/null +++ b/CareKitUI/CareKitUITests/TestGridTaskView.swift @@ -0,0 +1,66 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@testable import CareKitUI +import Foundation +import XCTest + +public class TestGridTaskView: XCTestCase { + + var view: OCKGridTaskView! + + override public func setUp() { + super.setUp() + view = .init() + } + + func testHorizontalMargin() { + var observed = view.horizontalMargin(forContainerWidth: 300, itemWidth: 50, interItemSpacing: 0, itemCount: 20) + XCTAssertEqual(observed, 0) + + observed = view.horizontalMargin(forContainerWidth: 300, itemWidth: 40, interItemSpacing: 0, itemCount: 20) + XCTAssertEqual(observed, 20) + + observed = view.horizontalMargin(forContainerWidth: 100, itemWidth: 40, interItemSpacing: 20, itemCount: 20) + XCTAssertEqual(observed, 0) + + observed = view.horizontalMargin(forContainerWidth: 100, itemWidth: 30, interItemSpacing: 20, itemCount: 20) + XCTAssertEqual(observed, 20) + + observed = view.horizontalMargin(forContainerWidth: 100, itemWidth: 40, interItemSpacing: 20, itemCount: 2) + XCTAssertEqual(observed, 0) + + observed = view.horizontalMargin(forContainerWidth: 100, itemWidth: 40, interItemSpacing: 20, itemCount: 1) + XCTAssertEqual(observed, 60) + + observed = view.horizontalMargin(forContainerWidth: 100, itemWidth: 40, interItemSpacing: 20, itemCount: 0) + XCTAssertEqual(observed, 100) + } +} diff --git a/OCKCatalog/OCKCatalog.xcodeproj/xcshareddata/xcschemes/OCKWatchCatalog.xcscheme b/OCKCatalog/OCKCatalog.xcodeproj/xcshareddata/xcschemes/OCKWatchCatalog.xcscheme index 7bc8eb341..58934a5c3 100644 --- a/OCKCatalog/OCKCatalog.xcodeproj/xcshareddata/xcschemes/OCKWatchCatalog.xcscheme +++ b/OCKCatalog/OCKCatalog.xcodeproj/xcshareddata/xcschemes/OCKWatchCatalog.xcscheme @@ -54,8 +54,10 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - <BuildableProductRunnable - runnableDebuggingMode = "0"> + <RemoteRunnable + runnableDebuggingMode = "2" + BundleIdentifier = "com.apple.Carousel" + RemotePath = "/OCKCatalog"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "515327EA2447951800EEB862" @@ -63,7 +65,7 @@ BlueprintName = "OCKWatchCatalog" ReferencedContainer = "container:OCKCatalog.xcodeproj"> </BuildableReference> - </BuildableProductRunnable> + </RemoteRunnable> </LaunchAction> <ProfileAction buildConfiguration = "Release" @@ -71,8 +73,10 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> - <BuildableProductRunnable - runnableDebuggingMode = "0"> + <RemoteRunnable + runnableDebuggingMode = "2" + BundleIdentifier = "com.apple.Carousel" + RemotePath = "/OCKCatalog"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "515327EA2447951800EEB862" @@ -80,7 +84,16 @@ BlueprintName = "OCKWatchCatalog" ReferencedContainer = "container:OCKCatalog.xcodeproj"> </BuildableReference> - </BuildableProductRunnable> + </RemoteRunnable> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "515327EA2447951800EEB862" + BuildableName = "OCKWatchCatalog.app" + BlueprintName = "OCKWatchCatalog" + ReferencedContainer = "container:OCKCatalog.xcodeproj"> + </BuildableReference> + </MacroExpansion> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> diff --git a/OCKCatalog/OCKCatalog/Localizable.strings b/OCKCatalog/OCKCatalog/Localizable.strings new file mode 100644 index 000000000..189f9c9f9 --- /dev/null +++ b/OCKCatalog/OCKCatalog/Localizable.strings @@ -0,0 +1,63 @@ +/* +Copyright (c) 2019, Apple Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder(s) nor the names of any contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. No license is granted to the trademarks of +the copyright holders even if such marks are included in this software. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +"ADDRESS" = "Address"; +"ANNOUNCE_EVENT_DELETED" = "Logged event was deleted"; +"CANCEL" = "Cancel"; +"CALL" = "Call"; +"COMPLETED" = "Completed"; +"CONTACTS" = "Contacts"; +"DELETE" = "Delete"; +"DONE" = "Done"; +"DOUBLE_TAP_MAP" = "Double-tap to open directions in Maps"; +"DOUBLE_TAP_TO_COMPLETE" = "Double-tap to mark as completed"; +"DOUBLE_TAP_TO_INCOMPLETE" = "Double-tap to mark as incomplete"; +"DOUBLE_TAP_TO_RECORD_EVENT" = "Double-tap to record an event"; +"DOUBLE_TAP_TO_REMOVE_EVENT" = "Double-tap to removed logged event"; +"EMAIL" = "E-mail"; +"EVENT" = "Event"; +"GOAL" = "Goal"; +"HIGH" = "High"; +"INCOMPLETE" = "Incomplete"; +"LOG" = "Log"; +"LOG_ENTRY" = "Log Entry"; +"LOGGING" = "Logging"; +"LOW" = "Low"; +"MARK_COMPLETE" = "Mark as Completed"; +"MESSAGE" = "Message"; +"NO_TASKS" = "No Tasks"; +"NO_EVENTS" = "No Events"; +"PROGRESS" = "Progress"; +"SEARCH" = "Search"; +"SELECTED" = "Selected"; +"TASKS" = "Tasks"; +"THREE_FINGER_SWIPE_DAY" = "Three-finger swipe to go to next or previous day"; +"THREE_FINGER_SWIPE_WEEK" = "Three-finger swipe to go to next or previous week"; +"TODAY" = "Today"; diff --git a/OCKCatalog/OCKCatalog/Localizable.stringsdict b/OCKCatalog/OCKCatalog/Localizable.stringsdict new file mode 100644 index 000000000..d823f17ed --- /dev/null +++ b/OCKCatalog/OCKCatalog/Localizable.stringsdict @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>EVENTS_REMAINING</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@events_remaining@</string> + <key>events_remaining</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>i</string> + <key>zero</key> + <string>0 remaining</string> + <key>one</key> + <string>1 remaining</string> + <key>two</key> + <string></string> + <key>few</key> + <string></string> + <key>many</key> + <string></string> + <key>other</key> + <string>%i remaining</string> + </dict> + </dict> +</dict> +</plist> diff --git a/OCKCatalog/OCKCatalog/OCKStore+Extensions.swift b/OCKCatalog/OCKCatalog/OCKStore+Extensions.swift new file mode 100644 index 000000000..3bb5c650c --- /dev/null +++ b/OCKCatalog/OCKCatalog/OCKStore+Extensions.swift @@ -0,0 +1,109 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKit +import Foundation + +extension OCKSchedule { + /// Create a schedule that happens at meal times every day of the week. + static func mealTimesEachDay(start: Date, end: Date?) -> OCKSchedule { + let startDate = Calendar.current.startOfDay(for: start) + let breakfast = OCKSchedule.dailyAtTime(hour: 7, minutes: 30, start: startDate, end: end, text: "Breakfast") + let lunch = OCKSchedule.dailyAtTime(hour: 12, minutes: 0, start: startDate, end: end, text: "Lunch") + let dinner = OCKSchedule.dailyAtTime(hour: 17, minutes: 30, start: startDate, end: end, text: "Dinner") + return OCKSchedule(composing: [breakfast, lunch, dinner]) + } +} + +extension OCKStore { + func fillWithDummyData() { + // Note: If the tasks and contacts already exist in the store, these methods will fail. If you have modified the data and would like the + // changes to be reflected in the app, delete and reinstall the catalog app. + let aFewDaysAgo = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: -10, to: Date())!) + addTasks(makeTasks(on: aFewDaysAgo), callbackQueue: .main) { result in + switch result { + case .failure(let error): print("[ERROR] \(error.localizedDescription)") + case .success: break + } + } + + addContacts(makeContacts(), callbackQueue: .main) { result in + switch result { + case .failure(let error): print("[ERROR] \(error.localizedDescription)") + case .success: break + } + } + } + + private func makeTasks(on start: Date) -> [OCKTask] { + var task1 = OCKTask(id: "nausea", title: "Nausea", carePlanID: nil, + schedule: .dailyAtTime(hour: 7, minutes: 0, start: start, end: nil, text: nil)) + task1.instructions = "Log any time you experience nausea." + task1.impactsAdherence = false + + var task2 = OCKTask(id: "doxylamine", title: "Doxylamine", carePlanID: nil, + schedule: .mealTimesEachDay(start: start, end: nil)) + task2.instructions = "Take the tablet with a full glass of water." + + return [task1, task2] + } + + private func makeContacts() -> [OCKContact] { + var contact1 = OCKContact(id: "lexi-torres", givenName: "Lexi", familyName: "Torres", carePlanID: nil) + contact1.role = "Dr. Torres is a family practice doctor with over 20 years of experience." + let phoneNumbers1 = [OCKLabeledValue(label: "work", value: "2135558479")] + contact1.phoneNumbers = phoneNumbers1 + contact1.title = "Family Practice" + contact1.messagingNumbers = phoneNumbers1 + contact1.emailAddresses = [OCKLabeledValue(label: "work", value: "lexitorres@icloud.com")] + let address1 = OCKPostalAddress() + address1.street = "26 E Centerline Rd" + address1.city = "Victor" + address1.state = "MI" + address1.postalCode = "48848" + contact1.address = address1 + + var contact2 = OCKContact(id: "matthew-reiff", givenName: "Matthew", familyName: "Reiff", carePlanID: nil) + contact2.role = "Dr. Reiff is a family practice doctor with over 20 years of experience." + contact2.title = "Family Practice" + let phoneNumbers2 = [OCKLabeledValue(label: "work", value: "7745550146")] + contact2.phoneNumbers = phoneNumbers2 + contact2.messagingNumbers = phoneNumbers2 + contact2.emailAddresses = [OCKLabeledValue(label: "work", value: "matthewreiff@icloud.com")] + let address2 = OCKPostalAddress() + address2.street = "9391 Burkshire Avenue" + address2.city = "Cardiff" + address2.state = "CA" + address2.postalCode = "92007" + contact2.address = address2 + + return [contact1, contact2] + } +} diff --git a/OCKCatalog/OCKCatalog/RootViewController.swift b/OCKCatalog/OCKCatalog/RootViewController.swift new file mode 100644 index 000000000..37d871dc9 --- /dev/null +++ b/OCKCatalog/OCKCatalog/RootViewController.swift @@ -0,0 +1,229 @@ +/* + Copyright (c) 2019, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. No license is granted to the trademarks of + the copyright holders even if such marks are included in this software. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import CareKit +import UIKit + +class RootViewController: UITableViewController { + private enum Constants { + static let cellID = "cell" + static let untrackedTaskID = "nausea" + static let trackedTaskID = "doxylamine" + static let contactID = "lexi-torres" + } + + private enum Section: String, CaseIterable { + case task, contact, chart, list + } + + private enum TaskStyle: String, CaseIterable { + case grid, checklist + case simple, instructions, buttonLog = "button log" + } + + private enum ContactStyle: String, CaseIterable { + case simple, detailed + } + + private enum List: String, CaseIterable { + case tasks, contacts + } + + private let storeManager: OCKSynchronizedStoreManager + + init(storeManager: OCKSynchronizedStoreManager) { + self.storeManager = storeManager + super.init(style: .grouped) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.cellID) + tableView.tableFooterView = UIView() + clearsSelectionOnViewWillAppear = true + + navigationController?.navigationBar.prefersLargeTitles = true + title = "CareKit Catalog" + + tableView.reloadData() + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return Section.allCases[section].rawValue + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section.allCases[section] { + case .task: return TaskStyle.allCases.count + case .contact: return ContactStyle.allCases.count + case .chart: return OCKCartesianGraphView.PlotType.allCases.count + case .list: return List.allCases.count + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: Constants.cellID, for: indexPath) + let title: String + switch Section.allCases[indexPath.section] { + case .task: title = TaskStyle.allCases[indexPath.row].rawValue + case .contact: title = ContactStyle.allCases[indexPath.row].rawValue + case .chart: title = OCKCartesianGraphView.PlotType.allCases[indexPath.row].rawValue + case .list: title = List.allCases[indexPath.row].rawValue + } + cell.textLabel?.text = title.capitalized + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + var viewController: UIViewController + + let section = Section.allCases[indexPath.section] + switch section { + case .task: + let taskViewController = makeTaskViewController(withStyle: TaskStyle.allCases[indexPath.row], storeManager: storeManager) + let listController = OCKListViewController() + listController.appendViewController(taskViewController, animated: false) + viewController = listController + + case .contact: + let contactViewController = makeContactViewController(withStyle: ContactStyle.allCases[indexPath.row], storeManager: storeManager) + let listController = OCKListViewController() + listController.appendViewController(contactViewController, animated: false) + viewController = listController + + case .chart: + let chartViewController = makeChartViewController(withStyle: OCKCartesianGraphView.PlotType.allCases[indexPath.row], + storeManager: storeManager) + let listController = OCKListViewController() + listController.appendViewController(chartViewController, animated: false) + viewController = listController + + case .list: + switch List.allCases[indexPath.row] { + case .tasks: + let rootViewController = OCKDailyTasksPageViewController(storeManager: storeManager) + rootViewController.isModalInPresentation = true + rootViewController.navigationItem.rightBarButtonItem = .init(barButtonSystemItem: .done, + target: self, action: #selector(dismissViewController)) + viewController = UINavigationController(rootViewController: rootViewController) + + case .contacts: + let rootViewController = OCKContactsListViewController(storeManager: storeManager) + rootViewController.isModalInPresentation = true + rootViewController.navigationItem.rightBarButtonItem = .init(barButtonSystemItem: .done, + target: self, action: #selector(dismissViewController)) + viewController = UINavigationController(rootViewController: rootViewController) + } + } + if section == .list { + present(viewController, animated: true, completion: nil) + } else { + navigationController?.pushViewController(viewController, animated: true) + } + clearSelection() + } + + private func makeTaskViewController(withStyle style: TaskStyle, storeManager: OCKSynchronizedStoreManager) -> UIViewController { + switch style { + case .checklist: + return OCKChecklistTaskViewController(taskID: Constants.trackedTaskID, eventQuery: .init(for: Date()), storeManager: storeManager) + case .grid: + return OCKGridTaskViewController(taskID: Constants.trackedTaskID, eventQuery: .init(for: Date()), storeManager: storeManager) + case .simple: + return OCKSimpleTaskViewController(taskID: Constants.trackedTaskID, eventQuery: .init(for: Date()), storeManager: storeManager) + case .instructions: + return OCKInstructionsTaskViewController(taskID: Constants.trackedTaskID, eventQuery: .init(for: Date()), storeManager: storeManager) + case .buttonLog: + return OCKButtonLogTaskViewController(taskID: Constants.untrackedTaskID, eventQuery: .init(for: Date()), storeManager: storeManager) + } + } + + private func makeContactViewController(withStyle style: ContactStyle, storeManager: OCKSynchronizedStoreManager) -> UIViewController { + switch style { + case .simple: + let viewController = OCKSimpleContactViewController(contactID: Constants.contactID, storeManager: storeManager) + return viewController + + case .detailed: + let viewController = OCKDetailedContactViewController(contactID: Constants.contactID, storeManager: storeManager) + viewController.controller.fetchAndObserveContact(withID: Constants.contactID) + return viewController + } + } + + private func makeChartViewController(withStyle style: OCKCartesianGraphView.PlotType, + storeManager: OCKSynchronizedStoreManager) -> UIViewController { + let gradientStart = UIColor { traitCollection -> UIColor in + return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.9960784314, green: 0.3725490196, blue: 0.368627451, alpha: 1) : #colorLiteral(red: 0.8627432641, green: 0.2630574384, blue: 0.2592858295, alpha: 1) + } + let gradientEnd = UIColor { traitCollection -> UIColor in + return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.9960784314, green: 0.4732026144, blue: 0.368627451, alpha: 1) : #colorLiteral(red: 0.8627432641, green: 0.3598620686, blue: 0.2592858295, alpha: 1) + } + + let markerSize: CGFloat = style == .bar ? 10 : 2 + let startOfDay = Calendar.current.startOfDay(for: Date()) + let configurations = [ + OCKDataSeriesConfiguration( + taskID: Constants.trackedTaskID, + legendTitle: Constants.trackedTaskID.capitalized, + gradientStartColor: gradientStart, + gradientEndColor: gradientEnd, + markerSize: markerSize, + eventAggregator: .countOutcomeValues) + ] + + let chartViewController = OCKCartesianChartViewController(plotType: style, selectedDate: startOfDay, + configurations: configurations, storeManager: storeManager) + chartViewController.controller.fetchAndObserveInsights(forConfigurations: configurations) + chartViewController.chartView.headerView.titleLabel.text = Constants.trackedTaskID.capitalized + return chartViewController + } + + private func clearSelection() { + if let selectionPath = self.tableView.indexPathForSelectedRow { + self.tableView.deselectRow(at: selectionPath, animated: true) + } + } + + @objc + private func dismissViewController() { + dismiss(animated: true, completion: nil) + } +} diff --git a/OCKSample/OCKSample/Supporting Files/Localizable.strings b/OCKSample/OCKSample/Supporting Files/Localizable.strings new file mode 100644 index 000000000..189f9c9f9 --- /dev/null +++ b/OCKSample/OCKSample/Supporting Files/Localizable.strings @@ -0,0 +1,63 @@ +/* +Copyright (c) 2019, Apple Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder(s) nor the names of any contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. No license is granted to the trademarks of +the copyright holders even if such marks are included in this software. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +"ADDRESS" = "Address"; +"ANNOUNCE_EVENT_DELETED" = "Logged event was deleted"; +"CANCEL" = "Cancel"; +"CALL" = "Call"; +"COMPLETED" = "Completed"; +"CONTACTS" = "Contacts"; +"DELETE" = "Delete"; +"DONE" = "Done"; +"DOUBLE_TAP_MAP" = "Double-tap to open directions in Maps"; +"DOUBLE_TAP_TO_COMPLETE" = "Double-tap to mark as completed"; +"DOUBLE_TAP_TO_INCOMPLETE" = "Double-tap to mark as incomplete"; +"DOUBLE_TAP_TO_RECORD_EVENT" = "Double-tap to record an event"; +"DOUBLE_TAP_TO_REMOVE_EVENT" = "Double-tap to removed logged event"; +"EMAIL" = "E-mail"; +"EVENT" = "Event"; +"GOAL" = "Goal"; +"HIGH" = "High"; +"INCOMPLETE" = "Incomplete"; +"LOG" = "Log"; +"LOG_ENTRY" = "Log Entry"; +"LOGGING" = "Logging"; +"LOW" = "Low"; +"MARK_COMPLETE" = "Mark as Completed"; +"MESSAGE" = "Message"; +"NO_TASKS" = "No Tasks"; +"NO_EVENTS" = "No Events"; +"PROGRESS" = "Progress"; +"SEARCH" = "Search"; +"SELECTED" = "Selected"; +"TASKS" = "Tasks"; +"THREE_FINGER_SWIPE_DAY" = "Three-finger swipe to go to next or previous day"; +"THREE_FINGER_SWIPE_WEEK" = "Three-finger swipe to go to next or previous week"; +"TODAY" = "Today"; diff --git a/OCKSample/OCKSample/Supporting Files/Localizable.stringsdict b/OCKSample/OCKSample/Supporting Files/Localizable.stringsdict new file mode 100644 index 000000000..d823f17ed --- /dev/null +++ b/OCKSample/OCKSample/Supporting Files/Localizable.stringsdict @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>EVENTS_REMAINING</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@events_remaining@</string> + <key>events_remaining</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>i</string> + <key>zero</key> + <string>0 remaining</string> + <key>one</key> + <string>1 remaining</string> + <key>two</key> + <string></string> + <key>few</key> + <string></string> + <key>many</key> + <string></string> + <key>other</key> + <string>%i remaining</string> + </dict> + </dict> +</dict> +</plist> diff --git a/README.md b/README.md index bd10ffdf0..36d5fcf98 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ let viewController = TaskButtonViewController(controller: TaskButtonController(s viewController.controller.fetchAndObserveEvents(forTaskID: "Doxylamine", eventQuery: OCKEventQuery(for: Date())) ``` -### SwiftUI <a name="carekit-swiftui"></a> +### SwiftUI <a name="swiftui"></a> A SwiftUI API is currently available for the `InstructionsTaskView`. The API is a starting point to demonstrate the API architecture. We would love to integrate community contributions that follow the API structure!