Compare commits

...

403 Commits

Author SHA1 Message Date
Louie c8473017ec Merge pull request #1531 from ResearchKit/cocoapod_fix
remove deprecated ORKConsent files
2022-11-29 13:21:51 -08:00
Louis Chatta 2bc0033aa8 remove deprecated ORKConsent files 2022-11-29 11:51:42 -08:00
Pariece McKinney a89059f5dc 2.1.1 Release
-Bug fixes
-New ORKAccuracyStroopStep
-AudioStep now allows for audio recording 
-New Styles for ORKDontKnowButton
-General layout and UI improvements
2022-10-26 16:07:45 -07:00
Will 2cc8f9e7d5 Prevent isPasscodeStoredInKeychain crash, correct warnings (#1505)
* fix: prevent isPasscodeStoredInKeychain from causing a crash

* fix: prevent nskeyedunarchiver warnings, add missing classes
2022-06-16 10:55:09 -07:00
akshay-yadav 29950b62e4 marker 2022-06-15 12:53:26 -07:00
akshay-yadav 65de4b333c README update 2022-06-15 11:44:35 -07:00
akshay-yadav 63fcf4918f Merge branch 'stable' into 'main' for housekeeping 2022-06-15 11:40:04 -07:00
akshay-yadav c9880b0139 merge stable into main
resolve merge conflicts
Bugfixes
Cleanup
merge main back to stable
2022-06-15 11:38:42 -07:00
akshay-yadav 13eabb7720 merging stable back to main
Resolving Merge conflicts
Bugfixes
Cleanup
2022-06-15 11:37:16 -07:00
Pariece McKinney 743b773ea3 Point RK to IOS 13 (#1499) 2022-04-25 09:40:44 -07:00
Pariece McKinney 249eee5dfb IOS15 Fixes (#1487) 2022-02-17 11:29:47 -08:00
aplummer-apple 90c68d0d19 Update project library search paths to compile on apple silicon (#1479)
I believe this line was in here to resolve issues with an early beta of xcode 11 and is unnecessary now
2021-11-29 10:20:27 -08:00
Pariece McKinney d10a427911 Merge pull request #1471 from Pariecemckinney-apple/pmckinney/taskVCDeprecationWarningFix
Fix ORKTaskViewController deprecation warning
2021-10-25 14:50:24 -07:00
Pariecemckinney-apple 05755a3213 initial commit 2021-10-25 11:38:50 -07:00
Corey e18a633de1 Set ORKTaskViewController nav background color (#1469) 2021-10-04 13:58:03 -07:00
Pariece McKinney 0e68cdf744 Merge pull request #1448 from stevemoser/patch-6
Fix broken Apple Forums link
2021-06-28 15:58:31 -07:00
Pariece McKinney 7f119a8d0d Merge pull request #1338 from Hengyu/hengyu
Use implied answer format for cells
2021-06-28 15:54:27 -07:00
hengyu fde1e7e957 Use implied answer format for cells 2021-06-27 14:58:31 +08:00
Erik Hornberger 0ad96d505c Merge pull request #1462 from erik-apple/nullable-return-type
Silence warning about nullable return types
2021-06-21 15:25:36 -07:00
Erik Hornberger d4ff76fc25 Silence warning about nullable return types 2021-06-21 15:08:01 -07:00
Erik Hornberger 85c1395361 Merge pull request #1461 from erik-apple/update-deprecated-method-in-keychain
Update deprecated method call
2021-06-21 14:47:45 -07:00
Erik Hornberger 1443e57c57 Replace deprecated method call 2021-06-21 14:22:10 -07:00
Erik Hornberger 3b75f6213c Updates to the predefined range of motion task (#1459) 2021-05-25 17:05:56 -07:00
gavirawson-apple e9d5de64a5 Clip step image (#1457) 2021-05-19 18:11:15 -07:00
Erik Hornberger 19c61383f6 Permission Type Updates (#1454)
* Ensure that the slider's colors match the view's tint

* New visual style for request permission step

* Add a new notifications permission type

* Add a new motion activity permission type

* Update ORKRequestPermissionButton.m

* Forward declare button

* Update ResearchKit/Common/ORKPermissionType.m

Co-authored-by: joeylabarck-apple <81833193+joeylabarck-apple@users.noreply.github.com>

* Update ResearchKit/Common/ORKPermissionType.h

Co-authored-by: joeylabarck-apple <81833193+joeylabarck-apple@users.noreply.github.com>

* Update ResearchKit/Common/ORKMotionActivityPermissionType.h

Co-authored-by: joeylabarck-apple <81833193+joeylabarck-apple@users.noreply.github.com>

* Update ResearchKit/Common/ORKPermissionType.h

Co-authored-by: joeylabarck-apple <81833193+joeylabarck-apple@users.noreply.github.com>

* Update ResearchKit/Common/ORKPermissionType.h

Co-authored-by: joeylabarck-apple <81833193+joeylabarck-apple@users.noreply.github.com>

* Add missing import

* Add missing import

Co-authored-by: joeylabarck-apple <81833193+joeylabarck-apple@users.noreply.github.com>
2021-04-16 17:02:45 -07:00
Steve Moser cb6cc97fa4 Fix broken Apple Forums link 2021-03-01 08:26:36 -05:00
srinathtm-apple 0651bf0c2a Use wheel date picker style (#1447)
* Use wheel date picker style
2021-02-11 19:16:32 -08:00
Pariece McKinney 68f9d29b66 Merge pull request #1396 from Pariecemckinney-apple/video_instruction_fix_issue_1389
Video Instruction Step Now Presents Play Button (#1389)
2020-08-03 16:29:25 -07:00
Pariecemckinney-apple 24afb1b1ec video instruction step now presents the play button so that the user can present the video intended for them 2020-08-03 16:19:07 -07:00
Kyle Seth Gray 47f44c488e Update ORKSkin.m (#1390)
literally just a misspelling of 'Margin'
2020-07-21 11:13:42 -07:00
Pariece McKinney a9b64253f1 VideoCaptureStep now corrects its state when navigating to it after pressing the back button (#1386)
Co-authored-by: Pariece McKinney <pariecemckinney@gmail.com>
2020-07-17 15:44:23 -07:00
Volodymyr Klymenko 506dc646ad Fix markdown formatting (#1383) 2020-06-24 14:21:05 -07:00
srinathtm-apple b50b759c7e RK 2.1 updates (#1384) 2020-06-24 14:20:11 -07:00
Greg Fiumara ed2602e523 Handle new UITouchPhase enumerations. (#1366)
* Handle new UITouchPhase enumerations.

Added in iOS 13.4.

* Conditionally include new UITouchPhase enums.
2020-04-05 16:36:15 -07:00
srinathtm-apple bbe376cddc Remove scripts folder, this is causing app store submission issues (#1362)
* removing scripts folder

* removing references from build phases
2020-03-26 11:25:16 -07:00
srinathtm-apple 50a2b3427a RK 2.1 updates (#1343)
* updates to RK 2.1

* adding AirPodsPro calibration data
2020-03-09 20:34:15 -07:00
Alex Wald ac7c63fc1b added missing nullability annotation in one of the initialisers (#1323) 2020-02-10 12:41:39 -08:00
srinathtm-apple b50e1d7aa4 Merge pull request #1333 from davwillev/master
Spoken instruction for TouchAnywhereStep changed from from title to text
2020-02-10 12:38:47 -08:00
David Evans 454e501cb6 Reallocated TouchAnywhereStep spoken instructions
Reallocated TouchAnywhereStep spoken instructions from task 'title' to instruction 'text'
2020-01-17 12:44:51 +00:00
David Evans 69cb1b7f2e Merge pull request #7 from ResearchKit/master
Update from master
2020-01-06 14:04:09 +00:00
srinathtm-apple 7c8917deb0 Merge pull request #1329 from xinsight/remove-tests-from-archive-2
remove tests from archive
2019-12-11 10:55:26 -08:00
Jay Moore e5e96b8db9 remove address and undef behaviour sanitizers 2019-12-11 18:04:31 +01:00
Jay Moore faffea0008 remove tests from archive 2019-12-11 13:28:29 +01:00
erik-apple e08711ff4b Retrieve tint color from view's window instead of AppDelegate's window (#1324)
We used to retrieve the view's tint color from a reference to the window held by the AppDelegate, however, iOS 13 introduced a new SceneDelegate pattern in which there may be more than one window. We now have to retrieve the tint color from window that the view belongs to, as there may be multiple windows with different tint colors.
2019-12-03 23:23:57 -05:00
srinathtm-apple c6c99a41db Merge pull request #1321 from erik-apple/2.1
ResearchKit 2.1 Update
2019-11-22 17:11:35 -08:00
erik-apple ff709e01f2 Update .travis.yml for Xcode 11 and iOS 13 2019-11-22 16:58:20 -08:00
erik-apple 666bf54dd5 Update ORKFormStep.h 2019-11-22 16:52:30 -08:00
erik-apple fbcd6adc9e Merge branch 'master' into 2.1 2019-11-22 16:48:50 -08:00
Erik Hornberger 2dc3cfb109 ResearchKit 2.1 Update 2019-11-22 16:40:08 -08:00
akshay-yadav ae9b9e57cb updates 2019-10-24 14:19:09 -07:00
srinathtm-apple e19dfbff8d Merge pull request #1298 from susom/instructionStepView-learnMore-fix
Fix displaying of Close button in ORKLearnMoreStepViewController
2019-10-23 12:03:58 -07:00
srinathtm-apple 6fc2e6d0fe Merge pull request #1308 from chrisortman/master
Fixes podspec in cocoapods 1.8.0
2019-10-23 12:03:24 -07:00
srinathtm-apple 456c3e4a45 Merge pull request #1311 from CareEvolution/sectionBug
Bugfix - Index needs to be _allSections
2019-10-23 12:02:54 -07:00
Eric Schramm eaca80f55a index needs to be _allSections 2019-10-17 08:45:10 -05:00
Chris Ortman d60ab56972 Fixes podspec in cocoapods 1.8.0 2019-10-04 16:56:39 -05:00
Paweł Kowalczyk 6b55e5ba81 Fix displaying of Close button in ORKLearnMoreStepViewController 2019-09-02 17:52:12 +02:00
David Evans 8c16ee46cc Merge pull request #6 from ResearchKit/master
Updating master to latest version
2019-07-24 12:15:53 +01:00
srinathtm-apple d56e6d5267 Merge pull request #1288 from srinathtm-apple/master
adding macro to bypass ios13 only api
2019-06-20 10:37:16 -07:00
Srinath Tupil Muralidharan 1ff84f4f96 adding macro to bypass ios13 only api 2019-06-19 16:08:14 -07:00
srinathtm-apple 3efb3ae1ae Merge pull request #1279 from CareEvolution/hideORKFormItemFix
Fix for hidePredicate changes where an ORKTableSection may contain more than one ORKFormItem
2019-06-17 14:39:02 -07:00
Eric Schramm d516d0cc0a reimplementation of hide ORKFormItem post-RK 2.1 2019-06-16 18:10:06 -05:00
Eric Schramm d2794890f8 Merge branch 'appleMaster' into hideORKFormItemFix
# Conflicts:
#	ResearchKit/Common/ORKFormStepViewController.m
2019-06-16 17:55:51 -05:00
srinathtm-apple ae70457c56 Merge pull request #1282 from ninoguba/bugfix_flash_modes
Fixed crash related to supported flash modes on certain devices
2019-06-04 13:57:27 -07:00
srinathtm-apple 800e45a27a Merge pull request #1280 from gabriel-blanco-apple/embeddedReviewStepIsEmpty-51368434
Missing embedded summary in ORKReviewStep
2019-06-04 13:56:51 -07:00
Akshay Yadav 7b6dae062c ResearchKit 2.1 Beta 2019-06-04 13:45:18 -07:00
Nino Guba 5070325d9a Fixed crash related to supported flash modes on certain devices 2019-06-03 18:48:21 -07:00
Gabriel Blanco 84511204d2 Missing embedded summary in ORKReviewStep
Bug introduced here:

commit 9ef1876c45
Author: Brian Ganninger <coder@infinitenexus.com>
Date:   Sun Nov 25 20:44:22 2018 -0800

    [code cleanup] Review mismatched nil returns (issue #1214)

——

Impact:
Causes embedded ORKReviewStep to return no steps
2019-06-03 14:52:32 -07:00
Eric Schramm ad8129efdf fixes for animation of changes 2019-06-02 22:48:57 -05:00
Eric Schramm 5f0f32f824 hideForm fix for sections which contain >1 forms 2019-05-31 15:17:59 -05:00
srinathtm-apple 7347a99520 Merge pull request #1277 from PersonalTouchProjects/mytouch
Add new activity tasks
2019-05-30 16:59:43 -07:00
Tommy Lin 12b2df63b4 Add missing header files to umbrella headers 2019-05-28 01:54:28 +08:00
Tommy Lin 00ac394379 Inherit ORKTouchAbilityScrollResult from ORKResult instead of ORKStepResult (typo) 2019-05-28 01:51:46 +08:00
srinathtm-apple 8f8cc4de14 Merge pull request #1271 from CareEvolution/Schramm/hideFormItemWithPredicate-2.0
Conditionally Hide an ORKFormItem based on an NSPredicate
2019-05-27 09:17:06 -07:00
Tommy Lin 577f612778 Fix retain cycle warnings 2019-05-27 22:31:10 +08:00
Tommy Lin e0f092decc Fix shadow declaration warning 2019-05-27 22:10:59 +08:00
Tommy Lin acacacdcce Add NSCopying, NSCoding and isEqual to ORKTouchAbility steps 2019-05-27 20:56:28 +08:00
Eric Schramm fce7172bac rename - filtered to hidden 2019-05-08 12:31:39 -05:00
Eric Schramm 264d51f1c9 explicitly compare against nil 2019-05-08 12:13:42 -05:00
Eric Schramm 6b1dd7f73d naming improvements, simplification 2019-05-08 12:08:50 -05:00
Eric Schramm ad5b4cb2fd update for renaming to hidePredicate 2019-04-24 21:44:08 -05:00
Eric Schramm 1faa99a490 rename to hidePredicate 2019-04-24 21:43:46 -05:00
Eric Schramm cf67d09e1b remove null results for filtered formItems 2019-04-24 16:00:29 -05:00
Eric Schramm 14a87b2067 fixed to corrected indexPath due to filtering 2019-04-24 15:56:53 -05:00
Eric Schramm 4f59976114 bypass non-optional formItems that are filtered 2019-04-24 15:36:34 -05:00
Eric Schramm 392dd87881 stop animated scrolling if no section show/hide changes 2019-04-19 16:06:03 -05:00
Eric Schramm fd8c973004 update didSelect to unfiltered indexPath 2019-04-19 15:16:01 -05:00
Eric Schramm ab400a203a implement hideItemPredicate in ORKFormStepVC 2019-04-19 14:43:39 -05:00
Eric Schramm 7fbd650fbb add hideItemPredicate to ORKFormItem 2019-04-19 14:39:42 -05:00
srinathtm-apple 01cc7ed79d Project updates (#1264)
* overall updates to ResearchKit, new textChoiceOtherAnswerFormat, unit tests and more..
2019-04-15 14:42:02 -07:00
Tommy Lin 62d5f17c75 Merge branch 'master' into mytouch 2019-03-28 15:48:25 +08:00
srinathtm-apple d198a8fa67 Merge pull request #1256 from yo1995/ORKSkinFix
Ork skin fix
2019-03-24 11:27:15 -07:00
Duke Mobile Center 77aa2444a2 clean comments 2019-03-24 13:01:44 -04:00
Duke Mobile Center 281b2093b1 #1255 solved the mapHeight metric and other compatibility issues. 2019-03-24 12:55:19 -04:00
srinathtm-apple 94c2b68e53 Merge pull request #1247 from srinathtm-apple/dBHLUpdates
dBHL Tone Audiometry updates
2019-02-26 18:42:30 -08:00
Srinath Tupil Muralidharan 99e3b370d5 if user taps during the preStimulusDelay, do not count it towards threshold calculation 2019-02-26 18:06:06 -08:00
srinathtm-apple 093513f01f Merge pull request #1241 from srinathtm-apple/splMeterUpdates
bugfixes for splmeter
2019-02-23 10:11:33 -08:00
srinathtm-apple 5e39fffe0d Merge pull request #1246 from rsanchezsaez-apple/rss/project-hardening-d
[Misc] Homogenize errorOut argument naming
2019-02-23 10:08:49 -08:00
Ricardo Sanchez-Saez 0ddd16b202 [Misc] Homogenize errorOut argument naming 2019-02-21 16:38:57 -08:00
David Evans 13fdd71613 Merge pull request #5 from ResearchKit/master
merge upstream
2019-02-21 12:26:25 +00:00
srinathtm-apple d000f5b45e Merge pull request #1245 from ResearchKit/rss/project-hardening-c
[Project Hardening] Improvements (III)
2019-02-20 23:40:02 -08:00
Ricardo Sanchez-Saez 19de603340 [ORKTaskViewController] More compact nil UUID check 2019-02-20 18:18:04 -08:00
Ricardo Sanchez-Saez 61af3f9b19 [ORKTaskViewController] Remove unneeded implementation 2019-02-20 18:12:44 -08:00
Ricardo Sanchez-Saez f614fda7b3 [Misc] Add a space before every ‘nil’ 2019-02-20 18:00:38 -08:00
Ricardo Sanchez-Saez 63fb484f22 [ORKSpeechRecognitionResult] Fix typo 2019-02-20 18:00:35 -08:00
Ricardo Sanchez-Saez 6e61d4f79b [ORKConsentSection] Remove unneeded import 2019-02-20 18:00:28 -08:00
Ricardo Sanchez-Saez 54feccdcff [ORKValuePicker] Undo unneeded changes 2019-02-20 17:59:51 -08:00
Ricardo Sanchez-Saez 5bc0088f16 [ORKVisualContentStepViewController] Better nullability handling 2019-02-20 17:59:44 -08:00
Ricardo Sanchez-Saez e4f7516b6f [ORKTintedImageView] Remove unneeded changes 2019-02-20 17:59:34 -08:00
Ricardo Sanchez-Saez 61290e42fa [ORKTintedImageView] Remove unneeded nullability 2019-02-20 17:59:26 -08:00
Ricardo Sanchez-Saez 82720f2978 [ORKPDFViewerStepView] Add default nullability 2019-02-20 17:59:17 -08:00
Ricardo Sanchez-Saez 08af95d3a7 [ORKTaskViewController] Better UUID nullability 2019-02-20 17:59:14 -08:00
Ricardo Sanchez-Saez 319b9f189e [ORKTableStepViewController] Better nullability 2019-02-20 17:59:10 -08:00
Ricardo Sanchez-Saez d6f71e2cdb [ORKStepNavigationRule] Improve comment 2019-02-20 17:59:02 -08:00
Ricardo Sanchez-Saez 92c2299a9d [ORKSkin] Remove unused code 2019-02-20 17:58:50 -08:00
Ricardo Sanchez-Saez ddb2c50aba [ORKReviewStep] Add braces for safety 2019-02-20 17:58:23 -08:00
Ricardo Sanchez-Saez 4d7c27a1dd [ORKReviewStep] Add extra nullable annotations 2019-02-20 17:58:20 -08:00
Ricardo Sanchez-Saez 2e0bef5490 [ORKPicker] Better nullability fix, remove unused function 2019-02-20 17:58:15 -08:00
Ricardo Sanchez-Saez 222855fabc [Misc] Nicer variable names 2019-02-20 17:58:12 -08:00
Ricardo Sanchez-Saez 97a503cf18 [ORKMultiValuePicker] Better nullability fix 2019-02-20 17:57:51 -08:00
Ricardo Sanchez-Saez 15b6204a9e [Misc] Rename findInArrayByKey to ORKFindInArrayByKey 2019-02-20 17:57:47 -08:00
Ricardo Sanchez-Saez 6f883b4acb [ORKHealthAnswerFormat] Better static analyzer fixes 2019-02-20 17:57:42 -08:00
Ricardo Sanchez-Saez 762d0350c4 Update comment to reflect code 2019-02-20 17:57:32 -08:00
Ricardo Sanchez-Saez ea01dbdbc3 Remove unneeded check 2019-02-20 17:57:24 -08:00
Ricardo Sanchez-Saez 363a1f7caa [ORKDateTimePicker] Better nullability test 2019-02-20 17:57:18 -08:00
Ricardo Sanchez-Saez 81b945a129 [ORKHealthAnswerFormat] Better nullability issue fix 2019-02-20 17:57:13 -08:00
Ricardo Sanchez-Saez 16a5da4566 [ORKGraphChartView] Simpler fix for shadowing variable declaration 2019-02-20 17:57:10 -08:00
Ricardo Sanchez-Saez 1c2af3c669 [ORKGraphChartView] Simpler fix for shadowing variable declaration 2019-02-20 17:57:06 -08:00
Ricardo Sanchez-Saez b412f47578 Rename ORKLineGraphAccessibilityElement to ORKGraphChartAccessibilityElement 2019-02-20 17:56:59 -08:00
Ricardo Sanchez-Saez d9cfb03dc6 [ORkGraphChartView] Fix wrong accessibility fix 2019-02-20 17:56:53 -08:00
Ricardo Sanchez-Saez 4baa86df01 [ORKGraphChartView] Remove unneeded change 2019-02-20 17:56:48 -08:00
Ricardo Sanchez-Saez c0c512b9ad [ORKStroopStepViewController] Change NSMutableDictionaries to NSDictionaries 2019-02-20 17:56:43 -08:00
Ricardo Sanchez-Saez 95d82db7cf [ORKAudioContentView] Do no set unneeded _timerLabel initial text 2019-02-20 17:56:24 -08:00
Ricardo Sanchez-Saez 66dbac3bb7 [Localization] Continue retuning ‘nil’ when the slider is not found 2019-02-20 17:56:20 -08:00
srinathtm-apple d969d55ab1 Merge pull request #1244 from rsanchezsaez-apple/rss/project-hardening-b
[Project Hardening] Improvements (II)
2019-02-20 17:51:09 -08:00
srinathtm-apple 95e8250194 Merge pull request #1243 from rsanchezsaez-apple/rss/project-hardening-a
[Project Hardening] Improvements (I)
2019-02-20 17:50:53 -08:00
Ricardo Sanchez-Saez 2539157e47 [Project] Disable “Missing Localization Context Comment” from static analyzer 2019-02-20 17:02:56 -08:00
Ricardo Sanchez-Saez a51b85622a [Project] Update project languages appropriately 2019-02-20 17:02:13 -08:00
Ricardo Sanchez-Saez 1e25391b0a Revert "[code cleanup] Add missing localization comments (issue #1214)"
This reverts commit c2b664f3b8.

# Conflicts:
#	ResearchKit/ActiveTasks/ORKAudioContentView.m
2019-02-20 16:57:14 -08:00
Ricardo Sanchez-Saez 9cc6255b45 [ORKSample] Update to Swift 4 2019-02-20 16:52:48 -08:00
Ricardo Sanchez-Saez 112bb1b7fa [ORKTest] Update to Swift 4 2019-02-20 16:52:38 -08:00
Ricardo Sanchez-Saez 041d015dbf [Project] Run static analyzer, in deep mode, only when building in Debug 2019-02-20 16:50:25 -08:00
Ricardo Sanchez-Saez 877edca1e6 [Project] Remove unsupported Swift 3.0 specifier from project 2019-02-20 16:50:16 -08:00
srinathtm-apple c7b2c33a29 Merge pull request #1216 from bgannin/projectHardening
Project hardening
2019-02-20 16:07:29 -08:00
srinathtm-apple ca1a18978f check if error is non-NULL before assignment 2019-02-18 20:24:30 -08:00
srinathtm-apple 0a962c6822 using NSInteger instead of NSUInteger 2019-02-18 17:56:49 -08:00
srinathtm-apple f07ae4b8f0 logical-or the missing super calls in ORKLocationSelectionView 2019-02-18 17:19:33 -08:00
srinathtm-apple b0d7eff709 logical-or the missing super calls 2019-02-18 17:16:49 -08:00
Srinath Tupil Muralidharan 390c02d2a8 bugfixes for splmeter 2019-02-14 12:25:46 -08:00
Tommy Lin 233142920f Merge master into mytouch 2019-02-14 17:10:44 +08:00
srinathtm-apple a4a147332d Merge pull request #1236 from jayway/email_fix
Fixed email regex to enforce start and end of string.
2019-02-04 08:57:26 -08:00
Gustaf Nilklint f1fdddf5d4 Fixed email regex to enforce start and end of string to eliminate the possibility to accidentally accept white space in the email adress. 2019-02-04 15:09:52 +01:00
Tommy Lin 489de1d8aa Fix ORKBorderedButton background color issue 2019-02-02 22:50:36 +08:00
Tommy Lin 52578e21fe Add internationalization strings 2019-01-28 16:26:08 +08:00
srinathtm-apple 92fc374b46 Merge pull request #1224 from srinathtm-apple/AttributedTextForSurveys
attributed text for surveys
2019-01-18 16:50:47 -08:00
Srinath Tupil Muralidharan c87b189f8a moving the assert to the designated initializer 2019-01-18 16:45:21 -08:00
Srinath Tupil Muralidharan b7cf4bc918 adding an assert if all the input params are nil 2019-01-18 16:39:51 -08:00
srinathtm-apple b803c3aab0 Merge pull request #1232 from aplummer-apple/tidy-onboarding
Minor example project onboarding cleanup
2019-01-18 15:58:56 -08:00
aplummer-apple 3094ba675c Update ORKSpatialSpanMemoryStepViewController.m 2019-01-18 15:51:23 -08:00
Andrew Plummer 88b7c9a68f Minor example project onboarding cleanup
- Tidy up localized strings
- Remove unused language config
- Delegate reference cycle
- Tidy signature access at end of onboarding

Minor syntax tidy

Minor Tidy, remove redundant check

Minor tidy, remove unnecessary variable

Minor tidy, clearer call

Fix broken “Disagree” button in example onboarding consent flow

Previously the flow let you progress as usual even if you select “disagree”

Remove development team

Clean whitespace
2019-01-10 18:38:34 -08:00
Tommy Lin 06e1e9bbfc Remove useless code 2019-01-08 14:50:11 +08:00
Srinath Tupil Muralidharan 36b8a72056 renaming an internal function 2019-01-07 17:44:10 -08:00
Srinath Tupil Muralidharan 111b983716 account for case where text is nil but primaryTextAttributedString is non-nil 2019-01-07 15:49:53 -08:00
Srinath Tupil Muralidharan 599a723dd1 call designated initializer using self 2019-01-07 15:14:03 -08:00
Srinath Tupil Muralidharan 7f37428426 support attributedText in ORKMultipleValuePickerAnswerFormat 2019-01-07 13:59:50 -08:00
Srinath Tupil Muralidharan 630a431299 support attributedText in textScaleAnswerFormat 2019-01-07 13:22:17 -08:00
Srinath Tupil Muralidharan ab081e28e1 ensure that attributedText always overrides text 2019-01-07 12:18:30 -08:00
Tommy Lin 8b912b7d25 Add touch ability instruction art works 2019-01-04 19:02:04 +08:00
Tommy Lin 375e1380d7 Add instructions before every touch ability tasks 2019-01-03 18:05:42 +08:00
Tommy Lin fdd3d7c653 Modify scroll task design 2019-01-02 20:33:42 +08:00
Tommy Lin 18ea1236e2 Rename ORKTouchAbilityScrollStep 2019-01-02 16:34:30 +08:00
Tommy Lin 18842a0e64 Add target hint label to ORKTouchAbilityScrollContentView 2018-12-25 17:13:27 +08:00
Tommy Lin 7a3d0606d5 Reset gesture recognizer events after reset tracks 2018-12-25 15:43:52 +08:00
Tommy Lin 528186535c Rename -[ORKTouchAbilityContentView startTracking] to -[ORKTouchAbilityContentView startTrial]
And also  -[ORKTouchAbilityContentView stopTracking] to  -[ORKTouchAbilityContentView endTrial]
2018-12-25 15:26:08 +08:00
Tommy Lin bd4c1a1568 Add start date and end date to ORKTouchAbilityTrial 2018-12-25 15:20:41 +08:00
Tommy Lin 08382cf7a3 Override some touch ability model descriptions 2018-12-24 19:24:01 +08:00
Tommy Lin 1080e4f08d Add ORKTouchAbilityTaskOption 2018-12-24 17:10:37 +08:00
Tommy Lin 81c9fd83e3 Refine scroll trial view 2018-12-24 16:51:13 +08:00
Tommy Lin f65b22993b Update some constraints 2018-12-24 16:37:11 +08:00
Tommy Lin e9059ee42c Add touch ability scroll ability models, views and view controllers 2018-12-22 16:48:52 +08:00
Tommy Lin f4feaffe19 Rename some protocol name and methods 2018-12-21 17:16:23 +08:00
Tommy Lin 176da46dd4 Replace auto layout codes using anchor API with traditional API 2018-12-21 17:07:26 +08:00
Tommy Lin e4a023713d Refine touch ability arrow view 2018-12-20 18:16:38 +08:00
Tommy Lin 7cfc964143 Refine pinch and rotation content view and step view controller 2018-12-20 17:40:27 +08:00
Tommy Lin b4b363cac6 Refine swipe content view and tap step view controller 2018-12-20 17:10:00 +08:00
Tommy Lin 3deaee1ef0 Refine long press content view and tap step view controller 2018-12-20 16:39:55 +08:00
Tommy Lin 330587f6f8 Refine tap content view and tap step view controller 2018-12-20 16:28:22 +08:00
Tommy Lin 7615ed631d Refine ORKTouchAbilityCustomView 2018-12-20 15:46:27 +08:00
Tommy Lin 567c1802ba Add rotation task models, views and controller 2018-12-19 17:50:21 +08:00
Tommy Lin 400ba05d8e Add pinch task models, views and controller 2018-12-17 14:36:34 +08:00
Srinath Tupil Muralidharan 3cd8cd7661 display attributed strings in picker view 2018-12-13 21:10:53 -08:00
Srinath Tupil Muralidharan 612b411dc2 adding support to provide attributed string for both primary text and detail text in ORKAnswerFormat 2018-12-13 21:10:20 -08:00
Srinath Tupil Muralidharan cd216ab1c0 setting attributed text to nil seems to override other UILabel properties, adding checks before assigning 2018-12-13 16:21:53 -08:00
Srinath Tupil Muralidharan 610d30fb0a adding support for setting attributed string to the primary text label of ORKAnswerFormat 2018-12-12 18:34:34 -08:00
Tommy Lin f81825bb78 Add touch ability swipe task 2018-12-05 18:28:32 +08:00
Tommy Lin eb5c34b35a Complete touch ability long press controller and models 2018-12-05 13:42:17 +08:00
Tommy Lin 7ea163bc14 Add property "success" to ORKTouchAbilityTapTrial 2018-12-05 13:21:38 +08:00
srinathtm-apple acc45033b7 Merge pull request #1222 from srinathtm-apple/CleanupPR1088
Cleanup for knee and shoulder range of motion
2018-12-04 17:33:47 -08:00
Srinath Tupil Muralidharan aa5bfa8142 pointing to the renamed assets for the task 2018-12-04 17:22:27 -08:00
srinathtm-apple 070950b31a Merge pull request #1088 from davwillev/master
Changes to angle calculations in Range of Motion active tasks
2018-12-04 15:16:52 -08:00
srinathtm-apple 981c1af7a8 Update ORKRangeOfMotionResult.h
adding new line spacing..
2018-12-04 13:55:36 -08:00
Tommy Lin 2cbf211613 Refine touch ability touch tracker 2018-12-04 15:33:06 +08:00
Tommy Lin 77de9ddf05 Refine touch ability tap step view controller 2018-12-04 14:58:30 +08:00
srinathtm-apple 1dfca472e2 Merge pull request #1213 from stevebaranski/issue/contributing
Correct link to ResearchKit forum in contributing guidelines
2018-12-03 13:15:23 -08:00
Tommy Lin 4b9dc94927 Add touch ability long press step, view, controller and result 2018-11-30 17:15:57 +08:00
Tommy Lin f3eec95c98 Refine tap task behavior 2018-11-30 16:10:00 +08:00
Tommy Lin 005b4b744f Refine ORKTouchAbility models 2018-11-29 17:16:51 +08:00
Tommy Lin 002939a0ec Refine property attributes 2018-11-28 17:08:00 +08:00
Brian Ganninger 35e5d5aa95 [code cleanup] Review mismatched nil returns, part II (issue #1214) 2018-11-27 22:18:03 -08:00
Brian Ganninger 547e1b5cd2 [code cleanup] Use local variable across autorelease pools (issue #1214) 2018-11-27 22:05:43 -08:00
Brian Ganninger 421e90615f [code cleanup] Resolve semantic issue with navigation buttons (issue #1214) 2018-11-27 20:40:23 -08:00
Tommy Lin 3538a50e55 Implement tap task trial points generator 2018-11-27 18:14:53 +08:00
Brian Ganninger 30a758e2b7 Tweak build settings a bit (issue #1214)
// implicit self retain in blocks, priority inversion are larger issues to tackle
2018-11-27 01:09:52 -08:00
Brian Ganninger 9ef1876c45 [code cleanup] Review mismatched nil returns (issue #1214) 2018-11-27 00:52:51 -08:00
Tommy Lin b06ffbb44e Refine code structure 2018-11-27 16:46:16 +08:00
Brian Ganninger 60076032b9 [code cleanup] Use defined enum values (issue #1214) 2018-11-25 19:12:15 -08:00
Brian Ganninger c2b664f3b8 [code cleanup] Add missing localization comments (issue #1214) 2018-11-25 13:08:51 -08:00
Brian Ganninger fc61990e03 [code cleanup] Address missing prototypes for functions (issue #1214) 2018-11-24 21:39:47 -08:00
Brian Ganninger ca81287311 [code cleanup] Resolve shadow declarations (issue #1214) 2018-11-24 19:33:28 -08:00
Brian Ganninger db995e3b35 [code cleanup] Fix dead code stores (issue #1214) 2018-11-24 17:46:16 -08:00
Brian Ganninger 1a902eefdd [code cleanup] Security fixes for random, loop counter (issue #1214) 2018-11-24 17:32:31 -08:00
Brian Ganninger 5c3fe6eaef [code cleanup] Fix unknown pragmas (issue #1214) 2018-11-24 17:27:06 -08:00
Brian Ganninger a033e1cc5e [code cleanup] Remove extraneous semicolons (issue #1214) 2018-11-24 15:42:54 -08:00
Brian Ganninger fbb7892ea6 [code cleanup] Add missing newlines (issue #1214) 2018-11-24 15:36:33 -08:00
Brian Ganninger 336096f680 [code cleanup] Fix memory management/mutability mismatch (issue #1214) 2018-11-24 01:13:05 -08:00
Brian Ganninger f3929e0e12 [code cleanup] Resolve generics type mismatch (issue #1214) 2018-11-24 01:10:20 -08:00
Brian Ganninger 3736cf573e [code cleanup] Resolve missing super calls (issue #1214) 2018-11-24 00:55:43 -08:00
Brian Ganninger 336ab0e21b Enable a swath of new compiler and analyzer flags and shallow analyze on build 2018-11-24 00:49:40 -08:00
Brian Ganninger 6459a4a5a0 Merge pull request #1 from ResearchKit/master
Merge ORK-latest
2018-11-21 15:38:04 -08:00
Tommy Lin d04c38b4a0 Add target view in ORKTouchAbilityTapContentView 2018-11-20 18:27:48 +08:00
stevebaranski 535cec7f08 Correct link to ResearchKit forum in contributing guidelines 2018-11-19 10:21:41 -08:00
Tommy Lin 843895e1e4 Add ORKTouchAbilityCustomView 2018-11-13 18:30:54 +08:00
Tommy Lin fe29159d14 Add ORKTouchAbilityTouchTracker 2018-11-13 16:31:39 +08:00
srinathtm-apple 7c883966d0 Merge pull request #1200 from bgannin/chartFontCustomization
Chart font customization
2018-11-10 13:12:26 -08:00
srinathtm-apple 332c8299fa Merge pull request #1210 from srinathtm-apple/runUnitTests
run unit tests on iPhone X iOS12.0 simulator
2018-11-08 17:27:42 -08:00
Srinath Tupil Muralidharan 120612cae0 run unit tests on iPhone X iOS12.0 simulator 2018-11-08 17:12:38 -08:00
srinathtm-apple dbb525a469 Merge pull request #1188 from CareEvolution/field-content-types
Add support for textContentType from UITextInputTraits to ORKAnswerFormat
2018-11-08 13:32:51 -08:00
Tommy Lin 4dea6d8a7b Make ORKTouchAbilityTouch, ORKTouchAbilityTrack, ORKTouchAbilityGestureRecognizer and ORKTouchAbilityTrial confirm to NSCopying and NSSecureCoding 2018-11-08 15:52:22 +08:00
srinathtm-apple c57d3a1e1f Merge pull request #1107 from rsanchezsaez/rsanchezsaez/optional-pie-legend
[ORKPieChartView] Allow skipping segment legend labels
2018-11-07 14:30:01 -08:00
Tommy Lin 77a15aa734 Add ORKTouchAbilityTrial class 2018-11-07 16:27:57 +08:00
srinathtm-apple aa482452e0 Merge pull request #1209 from srinathtm-apple/FixForIssue1205
fix for issue #1205
2018-11-06 21:09:17 -08:00
Srinath Tupil Muralidharan f1f322d442 fix for issue #1205 2018-11-06 20:56:55 -08:00
Tommy Lin 8191a977b7 Add basic touch ability data models, views and view controller 2018-11-06 15:39:16 +08:00
srinathtm-apple 54c3333b81 Merge pull request #1204 from srinathtm-apple/badgeForBuildStatus
adding badge for CI build status
2018-10-31 13:12:40 -07:00
Srinath Tupil Muralidharan ad755bb222 adding badge for CI build status 2018-10-31 13:06:52 -07:00
srinathtm-apple 920127d096 Merge pull request #1199 from susom/replace_deprecated_uiwebview
Replace deprecated UIWebView
2018-10-30 17:19:25 -07:00
srinathtm-apple 2696f2377b Merge pull request #1192 from bgannin/documentationTweak
Documentation tweaks
2018-10-30 16:52:02 -07:00
srinathtm-apple 81fdbdac57 Merge pull request #1187 from bgannin/testFixes
ORKTest unit tests fail
2018-10-30 16:44:17 -07:00
Brian Ganninger dd04625562 Updates per PR review 2018-10-30 16:23:46 -07:00
srinathtm-apple 98e8c8e09a Merge pull request #1185 from tobiasjungnickel/ScreenType_iPhone_MAX_XR
ScreenType iPhone Max & XR
2018-10-29 20:20:02 -07:00
srinathtm-apple 2da27dddd6 Merge pull request #1196 from xldrx/patch-1
Fixing a typo/bug in TaskList.
2018-10-29 20:14:28 -07:00
srinathtm-apple e40be2e5f6 Merge pull request #1201 from srinathtm-apple/setupTravisCI
Setup travis ci
2018-10-29 19:52:11 -07:00
Srinath Tupil Muralidharan 79228e8ab7 switching to ResearchKit unit tests 2018-10-29 18:07:37 -07:00
Srinath Tupil Muralidharan 161149d2df adding .travis.yml file 2018-10-29 17:39:03 -07:00
Brian Ganninger a38561bfed Add font changes for all chart types to ORKTest (issue #1158) 2018-10-27 16:55:40 -07:00
Paweł Kowalczyk 74f44435ef Replace deprecated UIWebView in ORKConsentReviewController 2018-10-26 12:56:12 +02:00
Paweł Kowalczyk 66a754d24f Replace deprecated UIWebView in ORKConsentLearnMoreViewController 2018-10-26 12:54:03 +02:00
Paweł Kowalczyk 258e2623f3 Replace deprecated UIWebView in ORKHTMLPDFWriter 2018-10-26 12:51:22 +02:00
Paweł Kowalczyk 7043316b90 Fix unit tests related to Two Finger Tapping Interval Task 2018-10-26 12:51:08 +02:00
Paweł Kowalczyk fcb980beb8 Fix unit tests related to adding new css style to handle signamture image 2018-10-26 12:46:19 +02:00
Brian Ganninger d186e5c6df Expose graph chart view fonts (issue #1158) 2018-10-24 20:18:00 -07:00
Brian Ganninger 5822caa980 Expose pie chart view fonts (issue #1158) 2018-10-23 22:18:19 -07:00
Sayed Hadi Hashemi bb55a603db Fixing a typo/bug in TaskList.
passcodeTask identifier has to be ".passcodeTask" not ".passcodeStep".
2018-10-21 15:47:01 -05:00
Brian Ganninger a75938f6bb Add flair (badge) for DVCS to README for clarity 2018-10-21 12:09:28 -07:00
Brian Ganninger 23ba6e97a8 Add badge flair for project attributes/status on README 2018-10-19 18:16:07 -07:00
Brian Ganninger 053625a0a1 Add prerequisites to the dependency management doc (fixes issue #1184) 2018-10-19 17:41:29 -07:00
Tobias Jungnickel 682cd83213 adding XS term to screen types for XSMax model 2018-10-18 10:30:04 +02:00
Eric DeLabar 55a2833c6e Address code review comments by @srinathtm-apple 2018-10-17 10:15:15 -04:00
Brian Ganninger c6e2df2a6e Additional cleanup 2018-10-16 23:41:46 -07:00
Brian Ganninger a3b6732406 Remove spurious initializer change; silence surfaced serialization failure properly (PR feedback) 2018-10-16 23:35:58 -07:00
Eric DeLabar d2fade561b Add support for textContentType from UITextInputTraits
* Add support for passwordFormat to support automatic secure passwords
2018-10-16 09:45:43 -04:00
Brian Ganninger ab6600eaf2 Pre-PR cleanup 2018-10-13 18:12:31 -07:00
Brian Ganninger 815f5bf60a Ignore shoulder range of motion step for now as it fails deserialization (issue #1151) 2018-10-13 13:43:28 -07:00
Brian Ganninger e9ecae2720 Minor code tweaks 2018-10-12 23:36:28 -07:00
Tobias Jungnickel c9f49d9f18 adding iPhone Max 2018-10-11 10:06:08 +02:00
srinathtm-apple 73faa2e59f Merge pull request #1182 from MacroYau/feature/cancelable-task
Allow task view controller to be canceled directly
2018-10-11 13:28:33 +05:30
srinathtm-apple 70889a0671 Merge pull request #1181 from bgannin/scaleHideValueLabel
Add option to hide selected value in slider scale (#657)
2018-10-11 13:27:55 +05:30
Brian Ganninger 26101d0068 Fix remaining issues for -testSecureCoding (issue #1151) 2018-10-10 22:23:55 -07:00
Brian Ganninger 340c5abdc0 Minor cleanup 2018-10-09 02:49:16 -07:00
Brian Ganninger 0c121f283b Properly init test data for clinical recorder configuration (issue #1151) 2018-10-08 20:53:50 -07:00
Brian Ganninger f50587eaf4 Add missing nullability keywords 2018-10-08 19:10:06 -07:00
Brian Ganninger fad50b20e0 Fix precision of equality check in speech recognition step (issue #1151) 2018-10-08 19:08:24 -07:00
Brian Ganninger 99be8240b0 Fix initialization and serialization issues for numeric answer format (issue #1151) 2018-10-08 18:14:10 -07:00
Brian Ganninger b6028a9eef Update known exceptions for serialization checking (issue #1151) 2018-10-08 18:09:32 -07:00
Brian Ganninger fcad24f94b Update known exceptions for serialization checking (issue #1151) 2018-10-07 23:31:05 -07:00
Brian Ganninger 211aa5a118 Fix serialization equality test failures (issue #1151) 2018-10-07 22:43:19 -07:00
Brian Ganninger 53842c3231 Fix addResult test failures (issue #1151) 2018-10-07 18:38:05 -07:00
Brian Ganninger b6ab7414fd Fix comment misspelling 2018-10-07 12:53:42 -07:00
Brian Ganninger e56ac6b2e9 Fix test failure by marking phone number related properties not serialized (issue #1151) 2018-10-07 12:52:08 -07:00
Brian Ganninger 0da343ea6f Fix serialization test failure for dB HL tone audiometry onboarding step (issue #1151) 2018-10-07 12:42:24 -07:00
Brian Ganninger 1fc475e826 Fix serialization test failure for Amsler grid result (issue #1151) 2018-10-07 12:29:25 -07:00
Brian Ganninger dc0890233f Rename variable for spelling and clarity 2018-10-07 12:17:37 -07:00
Brian Ganninger 15a8deba02 Fix serialization test failure for health clinical recorder configuration [iOS 12+ only] (issue #1151) 2018-10-07 12:16:28 -07:00
Brian Ganninger ac27b6d8fa Fix serialization test failure for Amsler grid step (issue #1151)
// also align whitespace for several entries
2018-10-06 20:31:30 -07:00
Brian Ganninger 8625fa19d6 Fix serialization test failure for speech recognition step (issue #1151) 2018-10-06 20:18:56 -07:00
Brian Ganninger 6e0a3a5c4a Fix serialization test failure for environment SPL meter result (issue #1151) 2018-10-06 19:17:56 -07:00
Brian Ganninger 388a20a79b Fix serialization test failure for streaming audio recorder configuration (issue #1151) 2018-10-06 16:48:55 -07:00
Brian Ganninger ef0c8d0495 Fix serialization test failure for Environment SPL Meter step (issue #1151) 2018-10-06 13:51:31 -07:00
Brian Ganninger 97fb686ad4 Fix serialization test failure for Speech in Noise step (issue #1151) 2018-10-06 13:50:44 -07:00
Brian Ganninger 2c0a6d7761 Fix serialization test failure for PDF Viewer step (issue #1151) 2018-10-06 13:49:57 -07:00
Brian Ganninger 0070d3ca04 Fix serialization test failure for tone audiometry step (issue #1151) 2018-10-05 23:43:48 -07:00
Macro Yau 6d0c331aba Fix typo 2018-10-05 14:30:21 +08:00
Macro Yau 0d81a9c696 Rename cancelable to discardable 2018-10-05 14:23:49 +08:00
srinathtm-apple 1e7d2c2460 Merge pull request #1179 from MacroYau/feature/custom-minute-interval
Allow custom minute interval setting for UIDatePicker with date and time, and time of day answer formats
2018-10-05 11:13:03 +05:30
Brian Ganninger 3197a62a3a Update TaskFactory+QuestionSteps.m
Fix incorrect comment
2018-10-04 14:57:26 -07:00
Brian Ganninger 4cdffbd061 Consistency cleanup from PR feedback. 2018-10-04 12:46:31 -07:00
Brian Ganninger fad570f6e9 Explicitly initialize per PR feedback 2018-10-04 10:26:10 -07:00
Macro Yau 05e4011f2a Remove unused getters 2018-09-29 18:43:49 +08:00
Macro Yau b28785d160 Allow task view controller to be cancelable regardless of step type 2018-09-28 19:15:13 +08:00
Brian Ganninger 2b25f66086 Remove hideSelectedValue from serialization test after initializer changes 2018-09-27 23:20:02 -07:00
Brian Ganninger 54f7a9b53a Update initializer 2018-09-27 23:13:05 -07:00
Brian Ganninger 0d6bd0e3c4 Whitespace tweak again 2018-09-27 23:11:29 -07:00
Brian Ganninger ed3d787972 Flipped parameters accidentally 2018-09-27 23:10:39 -07:00
Brian Ganninger 834e992bf0 Whitespace fix take 2 2018-09-27 23:09:27 -07:00
Brian Ganninger 4a894d2b3c Whitespace fix 2018-09-27 23:07:32 -07:00
Brian Ganninger 6848c067d0 Remove copyright attribution 2018-09-27 23:04:16 -07:00
Brian Ganninger ff3fe765f1 Remove hideSelectedValue from initializers, utilize only via settable property per PR review. 2018-09-27 23:01:24 -07:00
Macro Yau ecc6d2b6f9 Remove minute interval parameter from constructors 2018-09-27 16:45:50 +08:00
Brian Ganninger 8f633dcb43 Add copyright attribution per guidelines 2018-09-26 19:25:16 -07:00
Brian Ganninger a62b853500 Fix whitespace to match coding guidelines for project 2018-09-26 19:20:36 -07:00
Brian Ganninger 0a12a96af5 Add an extra step for testing text choice scale with no selected value; update serialization tests 2018-09-26 19:13:01 -07:00
Brian Ganninger 62bba0b6c9 Fix whitespace to match coding guidelines for project 2018-09-26 18:51:11 -07:00
Brian Ganninger b7c6772266 Add copyright attribution per guidelines 2018-09-25 22:56:27 -07:00
Brian Ganninger 224ce04a41 Redo initializers and Scale section of OKRTest to include hiding the selected value for all variants (#657) 2018-09-25 22:46:48 -07:00
Macro Yau 05b6f308e3 Allow custom minute interval setting for UIDatePicker with date and time, and time of day answer formats 2018-09-25 14:06:59 +08:00
Brian Ganninger 3f96750424 Port changes from pull request #446 to latest master 2018-09-24 20:33:23 -07:00
srinathtm-apple e7e501c087 Merge pull request #1175 from king7532/master
NSHealthShareUsageDescription must be longer than one word
2018-09-24 20:18:37 -06:00
Benjamin King 3aeb8dd9f9 NSHealthShareUsageDescription must be longer than one word, otherwise HealthKit will throw a runtime exception during the on-boarding or profile view controller 2018-09-19 11:05:31 -04:00
David Evans d30331a403 Slight change to RoM instructions
Made a slight improvement to the wording of the knee and shoulder range of motion tasks, in line with the new 'maximum' and 'minimum' angle calculations, following feedback after testing with volunteers.
2018-09-19 10:03:46 +01:00
David Evans 1726f5353d Trying to resolve file conflict by updating to latest RK version 2018-09-05 09:32:13 +01:00
David Evans e3fad9e1d9 Final changes for angle calculations 2018-09-03 16:41:38 +01:00
David Evans 9f2e77d103 Updated version for consistency with latest RK version 2018-09-03 16:39:18 +01:00
David Evans 0478b552dc Made final changes to knee and shoulder angle calculations 2018-09-03 16:34:58 +01:00
David Evans 19ee558b57 Adjusted for phone landscape orientation issue
Changed the 'range' result to the absolute difference between maximum and minimum angles, which deals with the problem of the phone being in opposite landscape orientations, and the device recording 'positive' or 'negative ' results accordingly
2018-08-22 12:08:21 +01:00
David Evans 8fde58535b Changed angle results calculations
This needed changes to allow for the negative direction of pitch during knee and shoulder RoM tasks. Counterintuitively, min and max values needed to be exchanged for these particular tasks
2018-08-22 11:49:17 +01:00
David Evans c424ada8fd Adjusted 'finish' angle calculation in line with ORKRangeOfMotionStepViewController 2018-08-01 11:55:09 +01:00
David Evans ba58b4dbad Removed min and max +90.0 adjustment 2018-07-31 22:27:40 +01:00
David Evans 0ee66f6a26 Removed +90.0 adjustment from min and max results 2018-07-31 22:26:34 +01:00
David Evans 261ef01f93 Adjusting maximum and minimum results by +90.0 2018-07-31 21:57:59 +01:00
David Evans f1083cf096 Adjusted minimum and mximum results by +90.0 2018-07-31 21:26:10 +01:00
David Evans 4b5a587eb8 Removed 'fabs' from rangeOfMotionMoreThan180Degrees function 2018-07-31 20:40:02 +01:00
David Evans 4bdd148b53 Removed 'phonesoundwaves' image 2018-07-31 11:42:27 +01:00
David Evans 6bad43fb9f Delete phonesoundwaves_inverted@3x.png 2018-07-31 11:36:29 +01:00
David Evans 34040ac1cd Delete phonesoundwaves_inverted@2x~ipad.png 2018-07-31 11:36:19 +01:00
David Evans 4a58bff1fd Delete phonesoundwaves_inverted@2x.png 2018-07-31 11:36:09 +01:00
David Evans d8391522db Delete Contents.json 2018-07-31 11:34:34 +01:00
Ricardo Sanchez-Saez aa2fd02c67 Merge branch 'master' of github.com:ResearchKit/ResearchKit into rsanchezsaez/optional-pie-legend 2018-07-30 22:21:09 -07:00
David Evans acf5f194f0 Updated calculation for minimum angle 2018-07-30 23:19:46 +01:00
David Evans b83684fac9 Update ResearchKit.strings
Updated instructions for range of motion tasks
2018-07-27 08:50:01 +01:00
David Evans 6ea0e4d248 Update ORKRangeOfMotionStepViewController.m 2018-07-26 23:06:12 +01:00
David Evans f730ed5a00 Update ORKRangeOfMotionStepViewController.m 2018-07-26 23:04:35 +01:00
David Evans 30e5a09c0c Update ORKRangeOfMotionStepViewController.m 2018-07-26 23:03:51 +01:00
David Evans 98e2ae73e4 Add files via upload 2018-07-26 22:38:12 +01:00
David Evans 100f744827 Add files via upload 2018-07-26 22:03:30 +01:00
David Evans 9ea558d233 Delete shoulder_flexed@3x.png 2018-07-26 21:57:58 +01:00
David Evans d3ceaabce0 Delete shoulder_flexed@2x~ipad.png 2018-07-26 21:57:48 +01:00
David Evans cf62288080 Delete shoulder_flexed@2x.png 2018-07-26 21:57:39 +01:00
David Evans a8e541a462 Delete Contents.json 2018-07-26 21:57:29 +01:00
David Evans 3c29db9917 Delete shoulder_extended@3x.png 2018-07-26 21:57:06 +01:00
David Evans 2b682cd197 Delete shoulder_extended@2x~ipad.png 2018-07-26 21:56:57 +01:00
David Evans cf753bea67 Delete shoulder_extended@2x.png 2018-07-26 21:56:48 +01:00
David Evans a4e1d13bad Delete Contents.json 2018-07-26 21:56:37 +01:00
David Evans 268af9b05d Delete shoulder_extended@3x.png 2018-07-26 21:56:19 +01:00
David Evans 9b47a271a4 Delete shoulder_extended@2x~ipad.png 2018-07-26 21:56:10 +01:00
David Evans 8608143e40 Delete shoulder_extended@2x.png 2018-07-26 21:55:59 +01:00
David Evans 01534209ac Delete Contents.json 2018-07-26 21:55:51 +01:00
David Evans ce75fc84bb Delete knee_flexed@3x.png 2018-07-26 21:55:29 +01:00
David Evans 4f1b8e1b89 Delete knee_flexed@2x~ipad.png 2018-07-26 21:55:20 +01:00
David Evans bb6be3bde1 Delete knee_flexed@2x.png 2018-07-26 21:55:11 +01:00
David Evans 52046a60bc Delete Contents.json 2018-07-26 21:55:02 +01:00
David Evans aba6fb5889 Delete knee_flexed@3x.png 2018-07-26 21:54:47 +01:00
David Evans 239ad2d8c9 Delete knee_flexed@2x~ipad.png 2018-07-26 21:54:37 +01:00
David Evans c220e6dfbd Delete knee_flexed@2x.png 2018-07-26 21:54:28 +01:00
David Evans 5d0cfbf9de Delete Contents.json 2018-07-26 21:54:19 +01:00
David Evans 644a563d20 Delete knee_extended@3x.png 2018-07-26 21:54:03 +01:00
David Evans c49ff60d73 Delete knee_extended@2x~ipad.png 2018-07-26 21:53:54 +01:00
David Evans 892713bec4 Delete knee_extended@2x.png 2018-07-26 21:53:45 +01:00
David Evans 60ca971d8e Delete Contents.json 2018-07-26 21:53:34 +01:00
David Evans e8f695ee32 Delete knee_extended@3x.png 2018-07-26 21:53:17 +01:00
David Evans 7941f76033 Delete knee_extended@2x~ipad.png 2018-07-26 21:53:01 +01:00
David Evans f871fafed6 Delete Contents.json 2018-07-26 21:52:51 +01:00
David Evans fbd02ffc59 Delete knee_extended@2x.png 2018-07-26 21:52:41 +01:00
David Evans 371c729a48 Delete shoulder_flexed@3x.png 2018-07-26 21:51:36 +01:00
David Evans 9f9cbd9fee Delete Contents.json 2018-07-26 21:51:21 +01:00
David Evans 1341070411 Delete shoulder_flexed@2x~ipad.png 2018-07-26 21:51:08 +01:00
David Evans ed26df05ae Delete shoulder_flexed@2x.png 2018-07-26 21:50:58 +01:00
David Evans 6583215cda Add files via upload
Added new results to range of motion tasks, and renamed flexed and extended
2018-07-26 21:41:54 +01:00
David Evans c77823dfa8 Add files via upload
Added a new image (phonesoundwaves_inverted) to range of motion tasks.
Added additional spoken instruction to 'tap anywhere' stage in range of motion tasks.
2018-07-26 21:36:39 +01:00
David Evans ec62232d9f Add files via upload 2018-07-26 21:28:45 +01:00
David Evans a54a266335 Add files via upload 2018-07-26 21:27:29 +01:00
David Evans 734e85ead6 Add files via upload
Added new results
2018-07-26 21:24:04 +01:00
David Evans 5182776e1f Add files via upload 2018-07-26 21:17:44 +01:00
David Evans 57f896b86f Add files via upload 2018-07-26 21:14:56 +01:00
David Evans abea94060e Update ORKRangeOfMotionStepViewController.m 2018-07-26 17:31:26 +01:00
David Evans 8a21932d62 Update ORKRangeOfMotionStepViewController.m 2018-07-26 17:30:48 +01:00
David Evans 07469cf2c1 Update ORKRangeOfMotionStepViewController.m 2018-07-26 17:28:43 +01:00
David Evans b684c9f2f8 Update ORKRangeOfMotionStepViewController.m 2018-07-26 17:27:56 +01:00
David Evans acc5ff2ba9 Update ORKRangeOfMotionStepViewController.m 2018-04-05 22:20:47 +01:00
David Evans 28d431c664 Update ORKRangeOfMotionStepViewController.m 2018-04-05 22:18:03 +01:00
Ricardo Sanchez-Saez 19a3cd7074 [ORKPieChart] Fix delegate optionality 2018-04-04 22:01:37 -07:00
Ricardo Sanchez-Saez fe8f0a2234 [ORKPieChart] Reuse more code 2018-04-04 21:57:27 -07:00
Ricardo Sanchez-Saez 5592966027 [ORKPieChartView] Allow skipping segment legend labels 2018-04-04 03:20:43 -07:00
David Evans 3133dd38df Merge pull request #1 from davwillev/davwillev-patch-1
Changes to angle calculations in Range of Motion active tasks
2018-03-05 13:28:28 +00:00
David Evans bd8ca78ed1 Add files via upload
Added quaternion to Euler definitions for Roll and Yaw.
Replaced the attitude.roll implementation for the device in landscape orientation with the quaternion for Roll.
Modified the calculation for 'flexed' and 'extended' angles.
2018-03-05 13:21:18 +00:00
1269 changed files with 62855 additions and 20175 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ codebase. However, other types of contributions are welcome too, in
keeping with the ResearchKit™ framework [best practices](../../wiki/best-practices). For example,
contributions of original free-to-use survey content, back-end integrations,
validation data, and analysis or processing tools are all welcome. Ask
on [researchkit-dev](https://lists.apple.com/mailman/listinfo/researchkit-dev) or [contact us](https://developer.apple.com/contact/researchkit/) for guidance.
on the [*ResearchKit* Forum](https://forums.developer.apple.com/community/researchkit) or [contact us](https://developer.apple.com/contact/researchkit/) for guidance.
Contributing software
@@ -42,7 +42,7 @@ consider one of the areas where we'd like to extend ResearchKit:
* More consent sections
* Back end integrations
If in doubt, bring your idea up on [researchkit-dev](https://lists.apple.com/mailman/listinfo/researchkit-dev).
If in doubt, bring your idea up on the [*ResearchKit* Forum](https://forums.developer.apple.com/community/researchkit).
Creating a personal fork<a name="fork"></a>
+10 -9
View File
@@ -1,22 +1,22 @@
ResearchKit Framework
===========
![VCS](https://img.shields.io/badge/dvcs-Git%20%2B%20LFS-tomato.svg) ![Platform](https://img.shields.io/cocoapods/p/ResearchKit.svg) ![CocoaPods](https://img.shields.io/cocoapods/v/ResearchKit.svg) ![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-yellow.svg?style=flat) [![License](https://img.shields.io/badge/license-BSD-green.svg?style=flat)](https://github.com/ResearchKit/ResearchKit#license) ![](https://travis-ci.com/ResearchKit/ResearchKit.svg?branch=master)
The *ResearchKit™ framework* is an open source software framework that makes it easy to create apps
for medical research or for other research projects.
* [Getting Started](#gettingstarted)
* Documentation:
* [Programming Guide](http://researchkit.org/docs/docs/Overview/GuideOverview.html)
* [Framework Reference](http://researchkit.org/docs/index.html)
* [Documentation](docs/)
* [Best Practices](../../wiki/best-practices)
* [Contributing to ResearchKit](CONTRIBUTING.md)
* [Website](http://researchkit.org) and [Blog](http://researchkit.org/blog.html)
* [Website](https://www.researchandcare.org)
* [ResearchKit BSD License](#license)
Getting More Information
========================
* Join the [*ResearchKit* Forum](https://forums.developer.apple.com/community/researchkit) for discussing uses of the *ResearchKit framework and* related projects.
* Join the [*ResearchKit* Forum](https://developer.apple.com/forums/tags/researchkit) for discussing uses of the *ResearchKit framework and* related projects.
Use Cases
===========
@@ -30,7 +30,7 @@ Surveys
The *ResearchKit framework* provides a pre-built user interface for surveys, which can be presented
modally on an *iPhone*, *iPod Touch*, or *iPad*. See
*[Creating Surveys](http://researchkit.org/docs/docs/Survey/CreatingSurveys.html)* for more
*[Creating Surveys](docs/Survey/)* for more
information.
@@ -39,7 +39,7 @@ Consent
The *ResearchKit framework* provides visual consent templates that you can customize to explain the
details of your research study and obtain a signature if needed.
See *[Obtaining Consent](http://researchkit.org/docs/docs/InformedConsent/InformedConsent.html)* for
See *[Obtaining Consent](docs/InformedConsent/)* for
more information.
@@ -50,8 +50,9 @@ Some studies may need data beyond survey questions or the passive data collectio
available through use of the *HealthKit* and *CoreMotion* APIs if you are programming for *iOS*.
*ResearchKit*'s active tasks invite users to perform activities under semi-controlled conditions,
while *iPhone* sensors actively collect data. See
*[Active Tasks](http://researchkit.org/docs/docs/ActiveTasks/ActiveTasks.html)* for more
*[Active Tasks](docs/ActiveTasks/)* for more
information.
ResearchKit active tasks are not diagnostic tools nor medical devices of any kind and output from those active tasks may not be used for diagnosis. Developers and researchers are responsible for complying with all applicable laws and regulations with respect to further development and use of the active tasks.
Charts
------------
@@ -81,7 +82,7 @@ The latest stable version of *ResearchKit framework* can be cloned with
git clone -b stable https://github.com/ResearchKit/ResearchKit.git
```
Or, for the latest changes, use the `master` branch:
Or, for the latest changes, use the `main` branch:
```
git clone https://github.com/ResearchKit/ResearchKit.git
-6
View File
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj">
</FileRef>
<FileRef
location = "group:ResearchKit.xcodeproj">
</FileRef>
@@ -13,7 +10,4 @@
<FileRef
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
</FileRef>
<FileRef
location = "group:samples/ORKSample/ORKSample.xcodeproj">
</FileRef>
</Workspace>
+5 -2
View File
@@ -1,15 +1,18 @@
Pod::Spec.new do |s|
s.name = 'ResearchKit'
s.version = '2.0.0'
s.version = '2.1.0'
s.summary = 'ResearchKit is an open source software framework that makes it easy to create apps for medical research or for other research projects.'
s.homepage = 'https://www.github.com/ResearchKit/ResearchKit'
s.documentation_url = 'http://researchkit.github.io/docs/'
s.license = { :type => 'BSD', :file => 'LICENSE' }
s.author = { 'researchkit.org' => 'http://researchkit.org' }
s.source = { :git => 'https://github.com/ResearchKit/ResearchKit.git', :tag => s.version.to_s }
s.public_header_files = `./scripts/find_headers.rb --public --private`.split("\n")
s.public_header_files = `./scripts/find_headers.rb --public`.split("\n")
s.private_header_files = `./scripts/find_headers.rb --private`.split("\n")
s.source_files = 'ResearchKit/**/*.{h,m,swift}'
s.resources = 'ResearchKit/**/*.{fsh,vsh}', 'ResearchKit/Animations/**/*.m4v', 'ResearchKit/Artwork.xcassets', 'ResearchKit/Localized/*.lproj'
s.platform = :ios, '11.0'
s.requires_arc = true
s.swift_version = '5'
s.module_map = "ResearchKit/ResearchKit.modulemap"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,37 @@
<?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>FILEHEADER</key>
<string>
/*
Copyright (c) ___YEAR___, 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 &quot;AS IS&quot;
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.
*/</string>
</dict>
</plist>
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
version = "1.3">
LastUpgradeVersion = "1250"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
buildImplicitDependencies = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -20,6 +20,20 @@
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
BuildableName = "ResearchKitTests.xctest"
BlueprintName = "ResearchKitTests"
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
@@ -27,6 +41,15 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B183A4731A8535D100C76870"
BuildableName = "ResearchKit.framework"
BlueprintName = "ResearchKit"
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
@@ -39,22 +62,13 @@
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B183A4731A8535D100C76870"
BuildableName = "ResearchKit.framework"
BlueprintName = "ResearchKit"
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
enableUBSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -70,8 +84,6 @@
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "YES"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
BuildableName = "ResearchKitTests.xctest"
BlueprintName = "ResearchKitTests"
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B183A4731A8535D100C76870"
BuildableName = "ResearchKit.framework"
BlueprintName = "ResearchKit"
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
BuildableName = "ResearchKitTests.xctest"
BlueprintName = "ResearchKitTests"
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</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">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -29,8 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -51,8 +49,6 @@
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
+24
View File
@@ -0,0 +1,24 @@
{
"configurations" : [
{
"id" : "A9C8689E-BBEE-4643-8C5D-DBA6A5AF01B2",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:ResearchKit.xcodeproj",
"identifier" : "86CC8E991AC09332001CCD89",
"name" : "ResearchKitTests"
}
}
],
"version" : 1
}
+3 -3
View File
@@ -30,6 +30,6 @@
// Shared header for accessibility functionality.
#import "ORKAccessibilityFunctions.h"
#import "ORKLineGraphAccessibilityElement.h"
#import "UIView+ORKAccessibility.h"
#import <ResearchKit/ORKAccessibilityFunctions.h>
#import <ResearchKit/ORKGraphChartAccessibilityElement.h>
#import <ResearchKit/UIView+ORKAccessibility.h>
@@ -29,8 +29,8 @@
*/
#import "ORKDefines.h"
#import "ORKHelpers_Internal.h"
#import <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKHelpers_Internal.h>
NS_ASSUME_NONNULL_BEGIN
@@ -38,8 +38,8 @@ NS_ASSUME_NONNULL_BEGIN
@class ORKScaleSlider;
// Used to properly format values from the ORKScaleSlider.
ORK_EXTERN NSString *ORKAccessibilityFormatScaleSliderValue(CGFloat value, ORKScaleSlider *slider);
ORK_EXTERN NSString *ORKAccessibilityFormatContinuousScaleSliderValue(CGFloat value, ORKScaleSlider *slider);
ORK_EXTERN NSString * _Nullable ORKAccessibilityFormatScaleSliderValue(CGFloat value, ORKScaleSlider *slider);
ORK_EXTERN NSString * _Nullable ORKAccessibilityFormatContinuousScaleSliderValue(CGFloat value, ORKScaleSlider *slider);
// Performs a block on the main thread after a delay. If Voice Over is not running, the block is performed immediately.
ORK_EXTERN void ORKAccessibilityPerformBlockAfterDelay(NSTimeInterval delay, void(^block)(void));
@@ -34,7 +34,7 @@
NS_ASSUME_NONNULL_BEGIN
@interface ORKLineGraphAccessibilityElement : UIAccessibilityElement
@interface ORKGraphChartAccessibilityElement : UIAccessibilityElement
- (nonnull instancetype)initWithAccessibilityContainer:(nonnull UIView *)container index:(NSInteger)index maxIndex:(NSInteger)maxIndex;
@@ -28,10 +28,10 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKLineGraphAccessibilityElement.h"
#import "ORKGraphChartAccessibilityElement.h"
@interface ORKLineGraphAccessibilityElement()
@interface ORKGraphChartAccessibilityElement()
@property (assign, nonatomic) NSInteger index;
@property (assign, nonatomic) NSInteger maxIndex;
@@ -39,7 +39,7 @@
@end
@implementation ORKLineGraphAccessibilityElement
@implementation ORKGraphChartAccessibilityElement
- (nonnull instancetype)initWithAccessibilityContainer:(nonnull UIView *)container index:(NSInteger)index maxIndex:(NSInteger)maxIndex {
self = [super initWithAccessibilityContainer:container];
+283
View File
@@ -0,0 +1,283 @@
/*
Copyright (c) 2019, Novartis.
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.
*/
public enum CircleSliderOption {
case startAngle(Double)
case barColor(UIColor)
case trackingColor(UIColor)
case thumbColor(UIColor)
case thumbImage(UIImage)
case barWidth(CGFloat)
case thumbWidth(CGFloat)
case maxValue(Float)
case minValue(Float)
case sliderEnabled(Bool)
case viewInset(CGFloat)
case minMaxSwitchTreshold(Float)
}
open class CircleSlider: UISlider {
private let minThumbTouchAreaWidth: CGFloat = 44
private var latestDegree: Double = 0
private var startValue: Float = 0
open var sliderValue: Float {
get {
return startValue
}
set {
var value = newValue
let significantChange = (maxValue - minValue) * (1.0 - minMaxSwitchTreshold)
let isSignificantChangeOccured = abs(newValue - startValue) > significantChange
if isSignificantChangeOccured {
if startValue < newValue {
value = minValue
} else {
value = maxValue
}
} else {
value = newValue
}
startValue = value
sendActions(for: .valueChanged)
var degree = Math.degreeFromValue(startAngle, value: sliderValue, maxValue: maxValue, minValue: minValue)
if startValue == maxValue {
degree -= degree / (360 * 100)
}
layout(degree)
}
}
private var trackLayer: TrackLayer! {
didSet {
layer.addSublayer(trackLayer)
}
}
private var thumbView: UIView! {
didSet {
if sliderEnabled {
thumbView.backgroundColor = thumbColor
thumbView.center = thumbCenter(startAngle)
thumbView.layer.cornerRadius = thumbView!.bounds.size.width * 0.5
addSubview(thumbView)
if let thumbImage = thumbImage {
let thumbImageView = UIImageView(frame: thumbView.bounds)
thumbImageView.image = thumbImage
thumbView.addSubview(thumbImageView)
thumbView.backgroundColor = UIColor.clear
}
} else {
thumbView.isHidden = true
}
}
}
private var startAngle: Double = -90
private var barColor = UIColor.lightGray
private var trackingColor = UIColor.blue
private var thumbColor = UIColor.black
private var barWidth: CGFloat = 20
private var maxValue: Float = 101
private var minValue: Float = 0
private var sliderEnabled = true
private var viewInset: CGFloat = 20
private var minMaxSwitchTreshold: Float = 0.0
private var thumbImage: UIImage?
private var _thumbWidth: CGFloat?
private var thumbWidth: CGFloat {
get {
if let retValue = _thumbWidth {
return retValue
}
return (thumbImage?.size.height)!
}
set {
_thumbWidth = newValue
}
}
override open func awakeFromNib() {
super.awakeFromNib()
backgroundColor = UIColor.clear
}
public init(frame: CGRect, options: [CircleSliderOption]?) {
super.init(frame: frame)
if let options = options {
build(options)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapHandle(sender:)))
tapGesture.numberOfTouchesRequired = 1
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(tapHandle(sender:)))
addGestureRecognizer(tapGesture)
addGestureRecognizer(panGesture)
}
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func layoutSublayers(of layer: CALayer) {
if trackLayer == nil {
trackLayer = TrackLayer(bounds: bounds.insetBy(dx: viewInset, dy: viewInset), setting: createLayerSetting())
}
if thumbView == nil {
if let image = thumbImage {
thumbView = UIView(frame: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
} else {
thumbView = UIView(frame: CGRect(x: 0, y: 0, width: thumbWidth, height: thumbWidth))
}
}
}
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !sliderEnabled {
return nil
}
return self
}
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds = self.bounds
bounds = bounds.insetBy(dx: 100.0, dy: 100.0)
return bounds.contains(point)
}
override open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let degree = Math.pointPairToBearingDegrees(center, endPoint: touch.location(in: self))
latestDegree = degree
layout(degree)
let value = Float(Math.adjustValue(startAngle, degree: degree, maxValue: maxValue, minValue: minValue))
thumbView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(degree)))
sliderValue = value
return true
}
@objc
func tapHandle(sender: UIGestureRecognizer) {
if isUserInteractionEnabled {
let degree = Math.pointPairToBearingDegrees(center, endPoint: sender.location(in: self))
latestDegree = degree
layout(degree)
let value = Float(Math.adjustValue(startAngle, degree: degree, maxValue: maxValue, minValue: minValue))
thumbView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(degree)))
sliderValue = value
}
}
open func changeOptions(_ options: [CircleSliderOption]) {
build(options)
redraw()
}
private func redraw() {
if trackLayer != nil {
trackLayer.removeFromSuperlayer()
}
trackLayer = TrackLayer(bounds: bounds.insetBy(dx: viewInset, dy: viewInset), setting: createLayerSetting())
if thumbView != nil {
thumbView.removeFromSuperview()
}
if let image = thumbImage {
thumbView = UIView(frame: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
} else {
thumbView = UIView(frame: CGRect(x: 0, y: 0, width: thumbWidth, height: thumbWidth))
}
self.layout(self.latestDegree)
}
func build(_ options: [CircleSliderOption]) {
for option in options {
switch option {
case let .startAngle(value):
startAngle = value
latestDegree = startAngle
case let .barColor(value):
barColor = value
case let .trackingColor(value):
trackingColor = value
case let .thumbColor(value):
thumbColor = value
case let .barWidth(value):
barWidth = value
case let .thumbWidth(value):
thumbWidth = value
case let .maxValue(value):
maxValue = value
maxValue += 1
case let .minValue(value):
minValue = value
startValue = minValue
case let .sliderEnabled(value):
sliderEnabled = value
case let .viewInset(value):
viewInset = value
case let .minMaxSwitchTreshold(value):
minMaxSwitchTreshold = value
case let .thumbImage(value):
thumbImage = value
}
}
}
private func layout(_ degree: Double) {
if let trackLayer = trackLayer, let thumbView = self.thumbView {
trackLayer.degree = degree
thumbView.center = thumbCenter(degree)
thumbView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(degree)))
trackLayer.setNeedsDisplay()
}
}
private func createLayerSetting() -> TrackLayer.Setting {
var setting = TrackLayer.Setting()
setting.startAngle = startAngle
setting.barColor = barColor
setting.trackingColor = trackingColor
setting.barWidth = barWidth
return setting
}
private func thumbCenter(_ degree: Double) -> CGPoint {
let radius = (bounds.insetBy(dx: viewInset, dy: viewInset).width * 0.5) - (barWidth * 0.5) + 5
return Math.pointFromAngle(frame, angle: degree, radius: Double(radius))
}
}
@@ -0,0 +1,126 @@
/*
Copyright (c) 2019, Novartis.
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.
*/
public enum DeviceType: String {
case iPhone5 = "iPhone5"
case iPhone5C = "iPhone5C"
case iPhone5S = "iPhone5S"
case iPhone6Plus = "iPhone6Plus"
case iPhone6 = "iPhone6"
case iPhone6S = "iPhone6S"
case iPhone6SPlus = "iPhone6SPlus"
case iPhone7 = "iPhone7"
case iPhone7Plus = "iPhone7Plus"
case iPhoneSE = "iPhoneSE"
case IPodTouch5 = "iPod5,1"
case IPodTouch6 = "iPod7,1"
}
func parseDeviceType(_ identifier: String) -> DeviceType {
switch identifier {
case "iPhone5,1", "iPhone5,2": return .iPhone5
case "iPhone5,3", "iPhone5,4": return .iPhone5C
case "iPhone6,1", "iPhone6,2": return .iPhone5S
case "iPhone7,1": return .iPhone6Plus
case "iPhone7,2": return .iPhone6
case "iPhone8,2": return .iPhone6SPlus
case "iPhone8,1": return .iPhone6S
case "iPhone9,1", "iPhone9,3": return .iPhone7
case "iPhone9,2", "iPhone9,4": return .iPhone7Plus
case "iPhone8,4": return .iPhoneSE
case "iPod5,1": return .IPodTouch5
case "iPod7,1": return .IPodTouch6
default:
if UIDevice.iPhonePlus {
return .iPhone7Plus
} else {
return .iPhone7
}
}
}
var pixelPerInchIphonePlus: CGFloat = 401
var pixelPerInchIphone: CGFloat = 326
var inchPerMm: CGFloat = 25.4
var renderedPixels: CGFloat = 1.15
func parsePixelPerInch(deviceType: DeviceType) -> CGFloat {
switch deviceType {
case .iPhone5, .iPhone5C, .iPhone5S, .iPhoneSE, .iPhone6, .iPhone6S, .iPhone7, .IPodTouch5, .IPodTouch6: return pixelPerInchIphone
case .iPhone6Plus, .iPhone6SPlus, .iPhone7Plus: return pixelPerInchIphonePlus
}
}
public extension UIDevice {
class var deviceType: DeviceType {
var systemInfo = utsname()
uname(&systemInfo)
let machine = systemInfo.machine
let mirror = Mirror(reflecting: machine)
var identifier = ""
for child in mirror.children {
if let value = child.value as? Int8, value != 0 {
identifier.append(String(UnicodeScalar(UInt8(value))))
}
}
return parseDeviceType(identifier)
}
class var pixelsPerMm: CGFloat {
return parsePixelPerInch(deviceType: UIDevice.deviceType) / inchPerMm
}
class var iPhonePlus: Bool {
if UIDevice.current.userInterfaceIdiom != .phone {
return false
}
if UIScreen.main.scale > 2.9 {
return true
}
return false
}
}
@@ -0,0 +1,182 @@
/*
Copyright (c) 2019, Novartis.
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.
*/
internal class EyeActivitySlider: UIView {
private var testType: VisionStepType?
private var incorrectAnswers = 0
private let contentGap: CGFloat = 20.0
private let toleranceAngle = 22.5
private let letterAngles = [0, 45, 90, 135, 180, 225, 270, 315]
private var letterSize: CGFloat {
var letterSize: CGFloat!
if self.testType == .visualAcuity {
letterSize = letterMmSizes[currentStep] * UIDevice.pixelsPerMm / UIScreen.main.nativeScale
} else {
letterSize = 20 * UIDevice.pixelsPerMm / UIScreen.main.nativeScale
}
return letterSize
}
private var currentStep = 0
private var letterMmSizes: [CGFloat] = [5.82, 4.65, 3.72, 2.91, 2.33, 1.86, 1.45, 1.16, 0.93, 0.73, 0.58, 0.47, 0.37]
private var contrastLevels: [CGFloat] = [0.9, 0.92, 0.937, 0.95, 0.96, 0.968, 0.975, 0.98, 0.984, 0.9875, 0.99]
private var stepScores: [Int] = [50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110]
private var letterAngle = 0.0
private lazy var letterImageView: UIImageView = {
let letterImage = UIImage(named: "iCNLandoltC",
in: Bundle(for: type(of: self)),
compatibleWith: nil)
let imageView = UIImageView(image: letterImage!)
return imageView
}()
private lazy var circleImageView: UIImageView = {
let circleImage = UIImage(named: "orangeGrayCircle",
in: Bundle(for: type(of: self)),
compatibleWith: nil)
return UIImageView(image: circleImage!)
}()
private var slider: CircleSlider?
internal init(testType: VisionStepType) {
super.init(frame: CGRect())
self.testType = testType
commonInit()
}
required internal init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
addSubview(letterImageView)
circleImageView.contentMode = .scaleAspectFit
addSubview(circleImageView)
let thumbImage = UIImage(named: "iCNDialPointerWithShadow",
in: Bundle(for: type(of: self)),
compatibleWith: nil)
slider = CircleSlider(frame: bounds, options: [
CircleSliderOption.barColor(UIColor.clear),
CircleSliderOption.trackingColor(UIColor.clear),
CircleSliderOption.startAngle(0),
CircleSliderOption.maxValue(360),
CircleSliderOption.minValue(0),
CircleSliderOption.thumbImage(thumbImage!)
])
addSubview(slider!)
updateSliderAndLetter()
}
override public func layoutSubviews() {
super.layoutSubviews()
letterAngle = Double(letterAngles[Int(arc4random_uniform(7))])
letterImageView.transform = CGAffineTransform.identity
letterImageView.frame = CGRect(origin: CGPoint(), size: CGSize(width: letterSize, height: letterSize))
letterImageView.center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
letterImageView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(letterAngle)))
letterImageView.alpha = getAlpha()
slider?.frame = bounds
var frame = contentFrame()
circleImageView.frame = frame
let labelMargin: CGFloat = 30.0
frame.origin.x += labelMargin
frame.origin.y += labelMargin
frame.size.width -= labelMargin * 2
frame.size.height -= labelMargin * 2
}
private func updateSliderAndLetter() {
guard incorrectAnswers < 2 else { return }
letterImageView.isHidden = false
slider?.sliderValue = 0
slider?.isUserInteractionEnabled = true
slider?.isHidden = false
setNeedsLayout()
}
private func contentFrame() -> CGRect {
let sideLength = min(bounds.size.width, bounds.size.height) - contentGap
let contentFrame = CGRect(x: (bounds.size.width - sideLength) / 2, y: (bounds.size.height - sideLength) / 2, width: sideLength, height: sideLength)
return contentFrame
}
private func getAlpha() -> CGFloat {
return testType == .visualAcuity ? 1.0 : (1 - contrastLevels[currentStep])
}
private func getResult() -> Bool {
let sliderValue = Double((slider?.sliderValue)!)
let leftMargin = letterAngle - toleranceAngle
let rightMargin = letterAngle + toleranceAngle
let result = sliderValue > leftMargin && sliderValue < rightMargin
if result == false {
incorrectAnswers += 1
} else {
currentStep += 1
}
return result
}
internal func hideLetter() {
letterImageView.isHidden = true
}
// swiftlint:disable large_tuple
internal func fetchResultDataAndUpdateSlider() -> (outcome: Bool, letterAngle: Double, sliderAngle: Double, score: Int, incorrectAnswers: Int, maxScore: Int) {
let outcome = getResult()
let score = stepScores[currentStep]
let currentSliderValue = Double((slider?.sliderValue)!)
let currentLetterAngle = letterAngle
let maxScore = testType == .visualAcuity ? stepScores.last! : stepScores[contrastLevels.count - 1]
updateSliderAndLetter()
return (outcome, currentLetterAngle, currentSliderValue, score, incorrectAnswers, maxScore)
}
}
+76
View File
@@ -0,0 +1,76 @@
/*
Copyright (c) 2019, Novartis.
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.
*/
internal class Math {
internal class func degreesToRadians(_ angle: Double) -> Double {
return angle / 180 * .pi
}
internal class func pointFromAngle(_ frame: CGRect, angle: Double, radius: Double) -> CGPoint {
let radian = degreesToRadians(angle)
let xPoint = Double(frame.midX) + cos(radian) * radius
let yPoint = Double(frame.midY) + sin(radian) * radius
return CGPoint(x: xPoint, y: yPoint)
}
internal class func pointPairToBearingDegrees(_ startPoint: CGPoint, endPoint: CGPoint) -> Double {
let originPoint = CGPoint(x: endPoint.x - startPoint.x, y: endPoint.y - startPoint.y)
let bearingRadians = atan2(Double(originPoint.y), Double(originPoint.x))
var bearingDegrees = bearingRadians * (180.0 / .pi)
bearingDegrees = (bearingDegrees > 0.0 ? bearingDegrees : (360.0 + bearingDegrees))
return bearingDegrees
}
internal class func adjustValue(_ startAngle: Double, degree: Double, maxValue: Float, minValue: Float) -> Double {
let ratio = Double((maxValue - minValue) / 360)
let ratioStart = ratio * startAngle
let ratioDegree = ratio * degree
let adjustValue: Double
if startAngle < 0 {
adjustValue = (360 + startAngle) > degree ? (ratioDegree - ratioStart) : (ratioDegree - ratioStart) - (360 * ratio)
} else {
adjustValue = (360 - (360 - startAngle)) < degree ? (ratioDegree - ratioStart) : (ratioDegree - ratioStart) + (360 * ratio)
}
return adjustValue + (Double(minValue))
}
internal class func adjustDegree(_ startAngle: Double, degree: Double) -> Double {
return (360 + startAngle) > degree ? degree : -(360 - degree)
}
internal class func degreeFromValue(_ startAngle: Double, value: Float, maxValue: Float, minValue: Float) -> Double {
let ratio = Double((maxValue - minValue) / 360)
let angle = Double(value) / ratio
return angle + startAngle - (Double(minValue) / ratio)
}
}
@@ -0,0 +1,78 @@
/*
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 <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKResult.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ORK3DModelManagerProtocol <NSObject>
@required
/**
This method is called within the ORK3DModelStepViewController's viewDidLoad method.
You are passed the contentView of the step so that you can add whatever visuals you choose.
*/
- (void)addContentToView:(UIView *)view;
/**
This method provides the ORK3DModelManager sublass the opportunity for cleanup before step is deallocated.
*/
- (void)stepWillEnd;
/**
This method is called by the ORK3DModelStepViewController's after the user taps the continue button or after the 3DModelManager subclass calls the endStep method.
This method signifies that the step is about to end so any necessary clean up before deallocation should be done here.
You can also optionally pass back an array of ORKResults.
*/
- (nullable NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier;
@end
ORK_CLASS_AVAILABLE
@interface ORK3DModelManager : NSObject <ORK3DModelManagerProtocol, NSSecureCoding, NSCopying>
- (instancetype)init;
@property (nonatomic, assign) BOOL allowsSelection;
@property (nonatomic, nullable) UIColor *highlightColor;
@property (nonatomic, nullable) NSArray<NSString *> *identifiersOfObjectsToHighlight;
- (void)setContinueEnabled:(BOOL)enabled;
- (void)endStep;
@end
NS_ASSUME_NONNULL_END
+125
View File
@@ -0,0 +1,125 @@
/*
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 "ORK3DModelManager.h"
#import "ORK3DModelManager_Internal.h"
#import "ORKHelpers_Internal.h"
NSNotificationName const ORK3DModelEnableContinueButtonNotification = @"ORK3DModelEnableContinueButtonNotification";
NSNotificationName const ORK3DModelDisableContinueButtonNotification = @"ORK3DModelDisableContinueButtonNotification";
NSNotificationName const ORK3DModelEndStepNotification = @"ORK3DModelEndStepNotification";
@implementation ORK3DModelManager
- (instancetype)init {
self = [super init];
if (self) {
_allowsSelection = NO;
_highlightColor = [UIColor yellowColor];
_identifiersOfObjectsToHighlight = nil;
}
return self;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORK3DModelManager *modelManager = [[[self class] allocWithZone:zone] init];
modelManager->_allowsSelection = self.allowsSelection;
modelManager->_highlightColor = [_highlightColor copy];
modelManager->_identifiersOfObjectsToHighlight = [self.identifiersOfObjectsToHighlight copy];
return modelManager;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self ) {
ORK_DECODE_BOOL(aDecoder, allowsSelection);
ORK_DECODE_OBJ_ARRAY(aDecoder, identifiersOfObjectsToHighlight, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, highlightColor, UIColor);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
ORK_ENCODE_BOOL(aCoder, allowsSelection);
ORK_ENCODE_OBJ(aCoder, identifiersOfObjectsToHighlight);
ORK_ENCODE_OBJ(aCoder, highlightColor);
}
- (BOOL)isEqual:(id)object {
if ([self class] != [object class]) {
return NO;
}
__typeof(self) castObject = object;
return ((self.allowsSelection == castObject.allowsSelection) &&
(ORKEqualObjects(self.highlightColor, castObject.highlightColor)) &&
(ORKEqualObjects(self.identifiersOfObjectsToHighlight, castObject.identifiersOfObjectsToHighlight)));
}
- (NSUInteger)hash
{
return [_identifiersOfObjectsToHighlight hash] ^ (_allowsSelection ? 0xf : 0x0) ^ [_highlightColor hash];
}
#pragma mark - Instance Methods
- (void)setContinueEnabled:(BOOL)enabled {
if (enabled) {
[[NSNotificationCenter defaultCenter] postNotificationName:ORK3DModelEnableContinueButtonNotification object:self];
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:ORK3DModelDisableContinueButtonNotification object:self];
}
}
- (void)endStep {
[[NSNotificationCenter defaultCenter] postNotificationName:ORK3DModelEndStepNotification object:self];
}
#pragma mark - ORK3DModelManagerProtocol
- (void)addContentToView:(UIView *)view {
[NSException raise:@"addContentToView not overwitten" format:@"Subclasses must overwrite the addContentToView function"];
}
- (void)stepWillEnd {
[NSException raise:@"stepWillEnd not overwitten" format:@"Subclasses must overwrite the stepWillEnd function"];
}
- (NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier {
[NSException raise:@"provideResults not overwitten" format:@"Subclasses must overwrite the provideResults function"];
return nil;
}
@end
@@ -0,0 +1,35 @@
/*
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;
extern NSNotificationName const ORK3DModelDisableContinueButtonNotification;
extern NSNotificationName const ORK3DModelEnableContinueButtonNotification;
extern NSNotificationName const ORK3DModelEndStepNotification;
+47
View File
@@ -0,0 +1,47 @@
/*
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 <ResearchKit/ORK3DModelManager.h>
#import <ResearchKit/ORKActiveStep.h>
#import <ResearchKit/ORKDefines.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORK3DModelStep : ORKActiveStep
- (instancetype)initWithIdentifier:(NSString *)identifier modelManager:(ORK3DModelManager *)modelManager;
@property (nonatomic) ORK3DModelManager *modelManager;
@end
NS_ASSUME_NONNULL_END
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
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:
@@ -28,68 +28,72 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKVisualConsentStep.h"
#import "ORKVisualConsentStepViewController.h"
#import "ORKConsentDocument_Internal.h"
#import "ORKStep_Private.h"
#import "ORK3DModelStep.h"
#import "ORK3DModelStepViewController.h"
#import "ORKHelpers_Internal.h"
@implementation ORKVisualConsentStep
@implementation ORK3DModelStep
+ (Class)stepViewControllerClass {
return [ORKVisualConsentStepViewController class];
return [ORK3DModelStepViewController class];
}
- (instancetype)initWithIdentifier:(NSString *)identifier document:(ORKConsentDocument *)consentDocument {
- (instancetype)initWithIdentifier:(NSString *)identifier modelManager:(nonnull ORK3DModelManager *)modelManager {
self = [super initWithIdentifier:identifier];
if (self) {
self.consentDocument = consentDocument;
_modelManager = modelManager;
}
return self;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKVisualConsentStep *step = [super copyWithZone:zone];
step.consentDocument = self.consentDocument;
return step;
- (void)validateParameters {
[super validateParameters];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, consentDocument, ORKConsentDocument);
}
return self;
- (BOOL)startsFinished {
return NO;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_OBJ(aCoder, consentDocument);
- (BOOL)allowsBackNavigation {
return NO;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORK3DModelStep *step = [super copyWithZone:zone];
step->_modelManager = [self.modelManager copy];
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self ) {
ORK_DECODE_OBJ_CLASS(aDecoder, modelManager, ORK3DModelManager);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_OBJ(aCoder, modelManager);
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
ORKEqualObjects(self.consentDocument, castObject.consentDocument));
return (isParentSame && ORKEqualObjects(self.modelManager, castObject.modelManager));
}
- (NSUInteger)hash {
return super.hash ^ self.consentDocument.hash;
}
- (BOOL)showsProgress {
return NO;
return [super hash] ^ [_modelManager hash];
}
@end
@@ -0,0 +1,42 @@
/*
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;
#import "ORK3DModelStep.h"
#import "ORKCustomStepView_Internal.h"
NS_ASSUME_NONNULL_BEGIN
@interface ORK3DModelStepContentView : ORKActiveStepCustomView
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,51 @@
/*
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 "ORK3DModelStepContentView.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKUnitLabel.h"
@implementation ORK3DModelStepContentView
- (instancetype)init {
self = [super init];
if (self) {
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
self.translatesAutoresizingMaskIntoConstraints = NO;
}
return self;
}
@end
@@ -0,0 +1,42 @@
/*
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 <ResearchKit/ORKActiveStepViewController.h>
#import <ResearchKit/ORKDefines.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORK3DModelStepViewController : ORKActiveStepViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,145 @@
/*
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 "ORK3DModelManager.h"
#import "ORK3DModelManager_Internal.h"
#import "ORK3DModelStep.h"
#import "ORK3DModelStepContentView.h"
#import "ORK3DModelStepViewController.h"
#import "ORKActiveStepView.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKBorderedButton.h"
#import "ORKCollectionResult_Private.h"
#import "ORKHelpers_Internal.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKResult_Private.h"
#import "ORKStepContainerView_Private.h"
#import "ORKStepViewController_Internal.h"
@implementation ORK3DModelStepViewController {
ORK3DModelManager *_modelManager;
ORK3DModelStepContentView *_stepContentView;
ORK3DModelStep *_step;
}
- (instancetype)initWithStep:(ORKStep *)step {
self = [super initWithStep:step];
if (self) {
_step = [self threeDimensionalModelStep];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
_stepContentView = [ORK3DModelStepContentView new];
_stepContentView.translatesAutoresizingMaskIntoConstraints = NO;
self.activeStepView.activeCustomView = _stepContentView;
self.activeStepView.customContentFillsAvailableSpace = NO;
self.activeStepView.navigationFooterView.neverHasContinueButton = NO;
[[_stepContentView.bottomAnchor constraintEqualToAnchor:self.activeStepView.navigationFooterView.topAnchor] setActive:YES];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(disableContinueButton:)
name:ORK3DModelDisableContinueButtonNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(enableContinueButton:)
name:ORK3DModelEnableContinueButtonNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(endStep:)
name:ORK3DModelEndStepNotification
object:nil];
[self activate3DModelManager];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:ORK3DModelDisableContinueButtonNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:ORK3DModelEnableContinueButtonNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:ORK3DModelEndStepNotification object:nil];
}
- (void)activate3DModelManager {
_modelManager = _step.modelManager;
[_modelManager addContentToView:_stepContentView];
}
- (void)stepDidFinish {
[super stepDidFinish];
if (_modelManager) {
[_modelManager stepWillEnd];
}
[self goForward];
}
- (ORK3DModelStep *)threeDimensionalModelStep {
return (ORK3DModelStep *)self.step;
}
- (ORKStepResult *)result {
ORKStepResult *stepResult = [super result];
if (_modelManager) {
NSArray<ORKResult *> *managerResults = [_modelManager provideResultsWithIdentifier:self.step.identifier];
if (managerResults) {
stepResult.results = [managerResults copy];
}
}
return stepResult;
}
#pragma mark - Notification Methods
- (void)disableContinueButton:(NSNotification *)notification {
self.activeStepView.navigationFooterView.continueEnabled = NO;
}
- (void)enableContinueButton:(NSNotification *)notification {
self.activeStepView.navigationFooterView.continueEnabled = YES;
}
- (void)endStep:(NSNotification *)notification {
[self finish];
}
@end
@@ -0,0 +1,56 @@
/*
Copyright (c) 2021, 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 <ResearchKit/ORKStroopResult.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKAccuracyStroopResult : ORKStroopResult
/**
A value that indicates whether the user selected the correct color (i.e. the base display color).
*/
@property (nonatomic, readonly) BOOL didSelectCorrectColor;
/**
A value that indicates how long it took for the user to make a selection.
*/
@property (nonatomic, assign) NSTimeInterval timeTakenToSelect;
/**
A value that indicates how far away (in pixels) that the user selected away from the center
of the correct circle.
*/
@property (nonatomic) double distanceToClosestCenter;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,99 @@
/*
Copyright (c) 2021, 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 "ORKAccuracyStroopResult.h"
#import "ORKResult_Private.h"
#import "ORKHelpers_Internal.h"
@interface ORKAccuracyStroopResult ()
@property (readwrite) BOOL didSelectCorrectColor;
@end
@implementation ORKAccuracyStroopResult
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
ORK_DECODE_DOUBLE(coder, distanceToClosestCenter);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
ORK_ENCODE_BOOL(coder, didSelectCorrectColor);
ORK_ENCODE_DOUBLE(coder, timeTakenToSelect);
ORK_ENCODE_DOUBLE(coder, distanceToClosestCenter);
}
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAccuracyStroopResult *result = [super copyWithZone:zone];
result.distanceToClosestCenter = self.distanceToClosestCenter;
return result;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
self.distanceToClosestCenter == castObject.distanceToClosestCenter);
}
- (NSUInteger)hash {
return [super hash] ^ @(self.didSelectCorrectColor).hash ^ @(self.timeTakenToSelect).hash ^ @(self.distanceToClosestCenter).hash;
}
#pragma mark - ResearchKit
- (BOOL)didSelectCorrectColor {
_didSelectCorrectColor = [self.color isEqualToString:self.colorSelected];
return _didSelectCorrectColor;
}
- (NSString *)descriptionWithNumberOfPaddingSpaces:(NSUInteger)numberOfPaddingSpaces {
return [NSString stringWithFormat:@"%@; didSelectCorrectColor: %i; timeTakenToSelect: %.3f; distanceToClosestCenter: %.0f %@",
[self descriptionPrefixWithNumberOfPaddingSpaces:numberOfPaddingSpaces],
self.didSelectCorrectColor,
self.timeTakenToSelect,
self.distanceToClosestCenter,
self.descriptionSuffix];
}
@end
@@ -0,0 +1,68 @@
/*
Copyright (c) 2021, 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 <ResearchKit/ORKActiveStep.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKAccuracyStroopStep : ORKActiveStep
/**
The color of the label.
The base display color is the color that the user must tap on to be correct. The text of
the label may match the base display color depending on the `isColorMatching` property.
*/
@property (nonatomic) UIColor *baseDisplayColor;
/**
Whether the text and base display color are matching.
If this value is true, the text of the label will spell out the same color as the base display
color, making the task easier for the user. If this value is false, the label color and label text
will represent different colors, which adds complexity to the puzzle task.
*/
@property (nonatomic) BOOL isColorMatching;
/**
The text of the label. (read-only)
The value of this property is generated based on the `baseDisplayColor` and `isColorMatching`
properties. If `isColorMatching` is false, the actual display color will be randomly generated
to be a color that is not the base display color.
*/
@property (nonatomic, readonly) UIColor *actualDisplayColor;
+ (NSArray <UIColor *> *)colors;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,107 @@
/*
Copyright (c) 2021, 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 "ORKAccuracyStroopStep.h"
#import "ORKAccuracyStroopStepViewController.h"
#import "ORKHelpers_Internal.h"
@implementation ORKAccuracyStroopStep
+ (Class)stepViewControllerClass {
return ORKAccuracyStroopStepViewController.class;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super initWithIdentifier:identifier];
if (self) {
self.baseDisplayColor = ORKAccuracyStroopStep.colors[arc4random_uniform(ORKAccuracyStroopStep.colors.count)];
self.isColorMatching = YES;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_BOOL(aDecoder, isColorMatching);
ORK_DECODE_OBJ_CLASS(aDecoder, baseDisplayColor, UIColor);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_BOOL(aCoder, isColorMatching);
ORK_ENCODE_OBJ(aCoder, baseDisplayColor);
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAccuracyStroopStep *step = [super copyWithZone:zone];
step.isColorMatching = self.isColorMatching;
step.baseDisplayColor = [self.baseDisplayColor copy];
return step;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return isParentSame
&& self.isColorMatching == castObject.isColorMatching
&& ORKEqualObjects(self.baseDisplayColor, castObject.baseDisplayColor);
}
- (NSUInteger)hash {
return super.hash
^ (self.isColorMatching ? 0xf : 0x0)
^ (self.baseDisplayColor ? 0xf : 0x0);
}
+ (NSArray<UIColor *> *)colors {
return @[ UIColor.systemRedColor,
UIColor.systemGreenColor,
UIColor.systemBlueColor,
UIColor.systemYellowColor,
UIColor.systemOrangeColor ];
}
- (UIColor *)actualDisplayColor {
return self.isColorMatching ?
self.baseDisplayColor :
ORKAccuracyStroopStep.colors[arc4random_uniform(ORKAccuracyStroopStep.colors.count)];
}
@end
@@ -0,0 +1,41 @@
/*
Copyright (c) 2021, 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 <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKStepViewController.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKAccuracyStroopStepViewController : ORKStepViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,283 @@
/*
Copyright (c) 2021, 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 "ORKAccuracyStroopStepViewController.h"
#import "ORKAccuracyStroopStep.h"
#import "ORKAccuracyStroopResult.h"
#import "ORKCollectionResult.h"
#import "ORKHelpers_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "UIColor+String.h"
@interface ORKAccuracyStroopStepViewController () <UIGestureRecognizerDelegate>
@property (nonatomic) NSMutableArray <UIView *> *circles;
@property (nonatomic, strong) UILabel *colorLabel;
@property (nonatomic) UIView *circlesView;
@property (nonatomic) NSArray<NSLayoutConstraint *> *constraints;
@property (nonatomic) double distanceToClosestCenter;
@property (nonatomic) UIColor *selectedColor;
@end
@implementation ORKAccuracyStroopStepViewController
- (ORKAccuracyStroopStep *)accuracyStroopStep {
return (ORKAccuracyStroopStep *)self.step;
}
- (void)stepDidChange {
[super stepDidChange];
if (self.step && [self isViewLoaded]) {
[self setupColorLabel];
[self setupCirclesView];
[self setupConstraints];
dispatch_async(dispatch_get_main_queue(), ^{
[self setupCircles];
[self setupViewTap];
});
}
}
- (void)setupViewTap {
for (UIGestureRecognizer *recognizer in self.circlesView.gestureRecognizers) {
[self.circlesView removeGestureRecognizer:recognizer];
}
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tapGestureRecognizer.delegate = self;
[self.circlesView addGestureRecognizer:tapGestureRecognizer];
}
- (void)setupCirclesView {
[self.circlesView removeFromSuperview];
self.circlesView = nil;
self.circlesView = UIView.new;
[self.view addSubview:self.circlesView];
}
- (void)setupColorLabel {
[self.colorLabel removeFromSuperview];
self.colorLabel = nil;
self.colorLabel = UILabel.new;
self.colorLabel.text = self.accuracyStroopStep.actualDisplayColor.textRepresentation;
self.colorLabel.textColor = self.accuracyStroopStep.baseDisplayColor;
self.colorLabel.font = [UIFont systemFontOfSize:35.0 weight:UIFontWeightMedium];
[self.view addSubview:self.colorLabel];
}
- (void)setupConstraints {
if (self.constraints) {
[NSLayoutConstraint deactivateConstraints:self.constraints];
}
self.colorLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.circlesView.translatesAutoresizingMaskIntoConstraints = NO;
self.constraints = nil;
self.constraints = @[
[NSLayoutConstraint constraintWithItem:self.colorLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:10.0],
[NSLayoutConstraint constraintWithItem:self.colorLabel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:self.circlesView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.colorLabel
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:self.circlesView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:self.circlesView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:self.circlesView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0]
];
[NSLayoutConstraint activateConstraints:self.constraints];
}
- (void)setupCircles {
for (UIView *circle in self.circles) {
[circle removeFromSuperview];
}
[self.circles removeAllObjects];
self.circles = NSMutableArray.array;
// Constants to use for ball and grid
int ballSize = 50;
int padding = 10;
int cellSize = ballSize + padding * 2;
// Calculating number of rows/columns in grid to layout color circles
uint32_t numRows = (self.circlesView.bounds.size.height) / cellSize;
uint32_t numColumns = (self.circlesView.bounds.size.width) / cellSize;
// Extra padding to ensure that the grid spans the whole screen width
int extraHorizontalSpaceForCell = ((int)self.circlesView.bounds.size.width % cellSize) / numColumns;
// Matrix to keep track of cells that already have a circle --> avoid overlap in O(n)
bool cellTakenMatrix[numRows][numColumns];
for (uint32_t r = 0; r < numRows; r++) {
for (uint32_t c = 0; c < numColumns; c++) {
cellTakenMatrix[r][c] = false;
}
}
for (int colorIndex = 0; colorIndex < ORKAccuracyStroopStep.colors.count; colorIndex++) {
// Obtain random location for color circle within bounds
int randomR = (int)arc4random_uniform(numRows);
int randomC = (int)arc4random_uniform(numColumns);
ORK_Log_Debug("Trying placement for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
// If cell is already taken, look at 8 spots around for a free spot
if (cellTakenMatrix[randomR][randomC]) {
ORK_Log_Debug("Position (r, c): (%d, %d) already taken", randomR, randomC);
// Loops through the 3x3 grid with randomR,randomC as the center
bool shouldBreak = false;
for (int r = randomR - 1; !shouldBreak && r <= randomR + 1; r++) {
for (int c = randomC - 1; !shouldBreak && c <= randomC + 1; c++) {
// If r/c are out of circleView's bounds, then don't consider
if ((r < 0 || r >= numRows) || (c < 0 || c >= numColumns)) { continue; }
// If cell is not taken, then can assign to there and break out of for-loops
if (!cellTakenMatrix[r][c]) {
randomR = r;
randomC = c;
shouldBreak = true;
}
}
}
}
ORK_Log_Info("Final position for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
cellTakenMatrix[randomR][randomC] = true;
CGFloat circleX = (randomC * (cellSize + extraHorizontalSpaceForCell)) + padding + extraHorizontalSpaceForCell / 2;
CGFloat circleY = (randomR * cellSize) + padding;
CGRect frame = CGRectMake(circleX, circleY, ballSize, ballSize);
UIView *newCircle = [[UIView alloc] initWithFrame:frame];
newCircle.backgroundColor = ORKAccuracyStroopStep.colors[colorIndex];
newCircle.clipsToBounds = YES;
newCircle.layer.cornerRadius = ballSize / 2;
newCircle.tag = colorIndex;
[self.circles addObject:newCircle];
[self.circlesView addSubview:newCircle];
}
}
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
CGPoint touchPoint = [recognizer locationInView:self.circlesView];
double minDistance = INFINITY;
for (UIView *circle in self.circles) {
double dx = (touchPoint.x - circle.center.x);
double dy = (touchPoint.y - circle.center.y);
double distance = sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
minDistance = distance;
}
if (CGRectContainsPoint(circle.frame, touchPoint)) {
self.selectedColor = ORKAccuracyStroopStep.colors[circle.tag];
self.distanceToClosestCenter = distance;
[super goForward];
return;
}
}
self.distanceToClosestCenter = minDistance;
[super goForward];
}
- (BOOL)hasPreviousStep {
return NO;
}
- (ORKStepResult *)result {
ORKStepResult *stepResult = [super result];
ORKAccuracyStroopResult *result = [[ORKAccuracyStroopResult alloc] initWithIdentifier:self.accuracyStroopStep.identifier];
result.color = self.accuracyStroopStep.baseDisplayColor.textRepresentation;
result.colorSelected = self.selectedColor.textRepresentation;
result.distanceToClosestCenter = self.distanceToClosestCenter;
result.startDate = stepResult.startDate;
result.endDate = stepResult.endDate;
result.timeTakenToSelect = [result.endDate timeIntervalSinceDate:result.startDate];
NSMutableArray *results = [[NSMutableArray alloc] init];
if (stepResult.results) {
results = [stepResult.results mutableCopy];
}
[results addObject:result];
stepResult.results = [results copy];
return stepResult;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self stepDidChange];
}
@end
+9 -8
View File
@@ -190,14 +190,6 @@ The default value of this property is `NO`.
*/
@property (nonatomic, copy, nullable) NSString *finishedSpokenInstruction;
/**
An image to be displayed below the instructions for the step.
The image can be stretched to fit the available space. When choosing a size
for this asset, be sure to take into account the variations in device form factors.
*/
@property (nonatomic, strong, nullable) UIImage *image;
/**
An array of recorder configurations that define the parameters for recorders to be
run during a step to collect sensor or other data.
@@ -214,6 +206,15 @@ The default value of this property is `NO`.
*/
@property (nonatomic, copy, nullable) NSArray<ORKRecorderConfiguration *> *recorderConfigurations;
/**
A Boolean value that determines if a step is a practice step or not.
When the value of this property is `YES`, the ResearchKit framework sets the allowsBackNavigation property to 'YES'
The default value of this property is `NO`.
*/
@property (nonatomic, assign) BOOL isPractice;
@end
NS_ASSUME_NONNULL_END
+8 -5
View File
@@ -102,7 +102,6 @@
step.spokenInstruction = self.spokenInstruction;
step.finishedSpokenInstruction = self.finishedSpokenInstruction;
step.recorderConfigurations = [self.recorderConfigurations copy];
step.image = self.image;
return step;
}
@@ -122,8 +121,8 @@
ORK_DECODE_BOOL(aDecoder, shouldContinueOnFinish);
ORK_DECODE_OBJ_CLASS(aDecoder, spokenInstruction, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, finishedSpokenInstruction, NSString);
ORK_DECODE_IMAGE(aDecoder, image);
ORK_DECODE_OBJ_ARRAY(aDecoder, recorderConfigurations, ORKRecorderConfiguration);
ORK_DECODE_BOOL(aDecoder, isPractice);
}
return self;
}
@@ -141,10 +140,10 @@
ORK_ENCODE_BOOL(aCoder, shouldVibrateOnFinish);
ORK_ENCODE_BOOL(aCoder, shouldUseNextAsSkipButton);
ORK_ENCODE_BOOL(aCoder, shouldContinueOnFinish);
ORK_ENCODE_IMAGE(aCoder, image);
ORK_ENCODE_OBJ(aCoder, spokenInstruction);
ORK_ENCODE_OBJ(aCoder, finishedSpokenInstruction);
ORK_ENCODE_OBJ(aCoder, recorderConfigurations);
ORK_ENCODE_BOOL(aCoder, isPractice);
}
- (BOOL)isEqual:(id)object {
@@ -155,7 +154,6 @@
ORKEqualObjects(self.spokenInstruction, castObject.spokenInstruction) &&
ORKEqualObjects(self.finishedSpokenInstruction, castObject.finishedSpokenInstruction) &&
ORKEqualObjects(self.recorderConfigurations, castObject.recorderConfigurations) &&
ORKEqualObjects(self.image, castObject.image) &&
(self.stepDuration == castObject.stepDuration) &&
(self.shouldShowDefaultTimer == castObject.shouldShowDefaultTimer) &&
(self.shouldStartTimerAutomatically == castObject.shouldStartTimerAutomatically) &&
@@ -166,7 +164,8 @@
(self.shouldVibrateOnStart == castObject.shouldVibrateOnStart) &&
(self.shouldVibrateOnFinish == castObject.shouldVibrateOnFinish) &&
(self.shouldContinueOnFinish == castObject.shouldContinueOnFinish) &&
(self.shouldUseNextAsSkipButton == castObject.shouldUseNextAsSkipButton));
(self.shouldUseNextAsSkipButton == castObject.shouldUseNextAsSkipButton) &&
(self.isPractice == castObject.isPractice));
}
- (NSSet<HKObjectType *> *)requestedHealthKitTypesForReading {
@@ -188,4 +187,8 @@
return mask;
}
- (BOOL)allowsBackNavigation {
return self.isPractice;
}
@end
@@ -30,7 +30,7 @@
@import UIKit;
#import "ORKLabel.h"
#import <ResearchKit/ORKLabel.h>
NS_ASSUME_NONNULL_BEGIN
@@ -62,4 +62,5 @@ NS_ASSUME_NONNULL_BEGIN
@end
NS_ASSUME_NONNULL_END
NS_ASSUME_NONNULL_END
+3 -49
View File
@@ -30,30 +30,26 @@
#import "ORKActiveStepTimer.h"
#import "ORKHelpers_Internal.h"
@import UIKit;
#include <mach/mach.h>
#include <mach/mach_time.h>
static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
static mach_timebase_info_data_t sTimebaseInfo;
if ( sTimebaseInfo.denom == 0 ) {
(void) mach_timebase_info(&sTimebaseInfo);
static mach_timebase_info_data_t sTimebaseInfo;
if (sTimebaseInfo.denom == 0) {
(void)mach_timebase_info(&sTimebaseInfo);
}
uint64_t elapsedNano = delta * sTimebaseInfo.numer / sTimebaseInfo.denom;
return elapsedNano * 1.0 / NSEC_PER_SEC;
}
@implementation ORKActiveStepTimer {
uint64_t _startTime;
NSTimeInterval _preExistingRuntime;
dispatch_queue_t _queue;
dispatch_source_t _timer;
UIBackgroundTaskIdentifier _backgroundTaskIdentifier;
uint32_t _isRunning;
}
@@ -68,7 +64,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
_interval = interval;
_handler = [handler copy];
_preExistingRuntime = runtime;
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
_queue = dispatch_queue_create("active_step", DISPATCH_QUEUE_SERIAL);
@@ -128,7 +123,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
}
- (void)queue_event {
[self queue_assertBackgroundTask];
NSTimeInterval runtime = [self queue_runtime];
BOOL finished = (runtime >= _duration);
@@ -138,13 +132,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
dispatch_async(dispatch_get_main_queue(), ^{
_handler(self, finished);
dispatch_sync(_queue, ^{
// If the timer is still NULL here, we can safely release the background task.
if (_timer == NULL) {
[self queue_releaseBackgroundTask];
}
});
});
}
@@ -156,29 +143,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
}
}
- (void)queue_releaseBackgroundTask {
if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
return;
}
UIBackgroundTaskIdentifier identifier = _backgroundTaskIdentifier;
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] endBackgroundTask:identifier];
});
}
- (void)queue_assertBackgroundTask {
if (_backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
return;
}
_backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
// This is guaranteed to be called synchronously on the main queue, switch to our queue to invalidate the identifier
dispatch_sync(_queue, ^{
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
});
}];
}
- (void)queue_resume {
if (_timer != NULL) {
// Already resumed
@@ -190,11 +154,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
return;
}
// We want to run in the background if we can, so voice can be played, etc.
assert(_backgroundTaskIdentifier == UIBackgroundTaskInvalid);
[self queue_assertBackgroundTask];
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
if (_timer == NULL) {
@@ -228,16 +187,11 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
_preExistingRuntime += timeIntervalFromMachTime(now - _startTime);
_startTime = 0;
if (!atFinish) {
// If we are atFinish, the task will be released after the handler completes
[self queue_releaseBackgroundTask];
}
}
- (void)queue_reset {
[self queue_clearTimer];
_preExistingRuntime = 0;
[self queue_releaseBackgroundTask];
}
@end
@@ -34,6 +34,7 @@
#import "ORKActiveStepTimer.h"
#import "ORKActiveStepTimerView.h"
#import "ORKActiveStepView.h"
#import "ORKStepContainerView_Private.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKStepHeaderView_Internal.h"
#import "ORKVerticalContainerView.h"
@@ -118,30 +119,30 @@
- (void)setActiveStepView {
if (!_activeStepView) {
_activeStepView = [[ORKActiveStepView alloc] initWithFrame:self.view.bounds];
_activeStepView = [ORKActiveStepView new];
[_activeStepView placeNavigationContainerInsideScrollView];
}
if (_customView) {
_activeStepView.customContentView = _customView;
}
[_activeStepView setCustomView:_customView];
_activeStepView.headerView.learnMoreButtonItem = self.learnMoreButtonItem;
[self.view addSubview:_activeStepView];
}
- (void)setNavigationFooterView {
if (!_navigationFooterView) {
_navigationFooterView = [ORKNavigationContainerView new];
_navigationFooterView = _activeStepView.navigationFooterView;
}
_navigationFooterView.skipButtonItem = self.skipButtonItem;
_navigationFooterView.continueEnabled = _finished;
ORKActiveStep *step = [self activeStep];
_navigationFooterView.useNextForSkip = step.shouldUseNextAsSkipButton;
_navigationFooterView.optional = step.optional;
_navigationFooterView.cancelButtonItem = self.cancelButtonItem;
BOOL neverHasContinueButton = (step.shouldContinueOnFinish && !step.startsFinished);
[_navigationFooterView setNeverHasContinueButton:neverHasContinueButton];
[_navigationFooterView updateContinueAndSkipEnabled];
[self updateContinueButtonItem];
[self.view addSubview:_navigationFooterView];
}
- (void)setupConstraints {
@@ -150,61 +151,38 @@
}
_constraints = nil;
UIView *viewForiPad = [self viewForiPadLayoutConstraints];
_activeStepView.translatesAutoresizingMaskIntoConstraints = NO;
_navigationFooterView.translatesAutoresizingMaskIntoConstraints = NO;
_constraints = @[
[NSLayoutConstraint constraintWithItem:_activeStepView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:viewForiPad ? : self.view.safeAreaLayoutGuide
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_activeStepView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:viewForiPad ? : self.view.safeAreaLayoutGuide
toItem:self.view
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_activeStepView
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:viewForiPad ? : self.view.safeAreaLayoutGuide
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_navigationFooterView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:viewForiPad ? : self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_navigationFooterView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:viewForiPad ? : self.view
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_navigationFooterView
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:viewForiPad ? : self.view
toItem:self.view
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_activeStepView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:_navigationFooterView
attribute:NSLayoutAttributeTop
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0],
constant:0.0]
];
[NSLayoutConstraint activateConstraints:_constraints];
@@ -217,31 +195,25 @@
[self prepareStep];
}
- (UIView *)customViewContainer {
__unused UIView *view = [self view];
return _activeStepView.customViewContainer;
}
- (ORKTintedImageView *)imageView {
__unused UIView *view = [self view];
return _activeStepView.imageView;
}
- (void)setCustomView:(UIView *)customView {
_customView = customView;
[_activeStepView setStepView:_customView];
if (_customView) {
[_activeStepView setCustomContentView:_customView];
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
ORK_Log_Debug(@"%@",self);
[self.taskViewController setRegisteredScrollView:_activeStepView];
if (_activeStepView.navigationFooterView) {
[_activeStepView.navigationFooterView flattenIfNeeded];
}
ORK_Log_Debug("%@",self);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
ORK_Log_Debug(@"%@",self);
ORK_Log_Debug("%@",self);
// Wait for animation complete
dispatch_async(dispatch_get_main_queue(), ^{
@@ -256,7 +228,7 @@
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
ORK_Log_Debug(@"%@",self);
ORK_Log_Debug("%@",self);
[self suspend];
}
@@ -272,7 +244,6 @@
- (void)setLearnMoreButtonItem:(UIBarButtonItem *)learnMoreButtonItem {
[super setLearnMoreButtonItem:learnMoreButtonItem];
_activeStepView.headerView.learnMoreButtonItem = self.learnMoreButtonItem;
}
- (void)setSkipButtonItem:(UIBarButtonItem *)skipButtonItem {
@@ -282,7 +253,6 @@
- (void)setCancelButtonItem:(UIBarButtonItem *)cancelButtonItem {
[super setCancelButtonItem:cancelButtonItem];
_navigationFooterView.cancelButtonItem = cancelButtonItem;
}
- (void)setFinished:(BOOL)finished {
@@ -345,7 +315,7 @@
self.finished = [[self activeStep] startsFinished];
ORK_Log_Debug(@"%@", self);
ORK_Log_Debug("%@", self);
_activeStepView.activeStep = self.activeStep;
if ([self.activeStep hasCountDown]) {
@@ -386,7 +356,7 @@
}
- (void)start {
ORK_Log_Debug(@"%@",self);
ORK_Log_Debug("%@",self);
self.started = YES;
[self startTimer];
[_activeStepView.activeCustomView startStep:self];
@@ -412,7 +382,7 @@
}
- (void)suspend {
ORK_Log_Debug(@"%@",self);
ORK_Log_Debug("%@",self);
if (self.finished || !self.started) {
return;
}
@@ -424,7 +394,7 @@
}
- (void)resume {
ORK_Log_Debug(@"%@",self);
ORK_Log_Debug("%@",self);
if (self.finished || !self.started) {
return;
}
@@ -436,7 +406,7 @@
}
- (void)finish {
ORK_Log_Debug(@"%@",self);
ORK_Log_Debug("%@",self);
if (self.finished) {
return;
}
@@ -520,7 +490,15 @@
BOOL isHalfway = !_hasSpokenHalfwayCountdown && timer.runtime > timer.duration / 2.0;
if (!finished && self.activeStep.shouldSpeakRemainingTimeAtHalfway && !UIAccessibilityIsVoiceOverRunning() && isHalfway) {
_hasSpokenHalfwayCountdown = YES;
NSString *text = [NSString localizedStringWithFormat:ORKLocalizedString(@"COUNTDOWN_SPOKEN_REMAINING_%@", nil), @(countDownValue)];
NSDateComponentsFormatter *secondsFormatter = [NSDateComponentsFormatter new];
secondsFormatter.unitsStyle = NSDateComponentsFormatterUnitsStyleSpellOut;
secondsFormatter.allowedUnits = NSCalendarUnitSecond;
secondsFormatter.formattingContext = NSFormattingContextDynamic;
secondsFormatter.maximumUnitCount = 1;
NSString *seconds = [secondsFormatter stringFromTimeInterval:countDownValue];
NSString *text = [NSString localizedStringWithFormat:ORKLocalizedString(@"COUNTDOWN_SPOKEN_REMAINING_%@", nil), seconds];
[voice speakText:text];
}
}
@@ -29,7 +29,7 @@
*/
#import "ORKActiveStepViewController.h"
#import <ResearchKit/ORKActiveStepViewController.h>
NS_ASSUME_NONNULL_BEGIN
@@ -56,8 +56,6 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, getter=isStarted) BOOL started;
@property (nonatomic, strong, readonly) ORKNavigationContainerView *navigationFooterView;
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished; // Let subclass receive timer fires
- (void)applicationWillResignActive:(NSNotification *)notification;
@@ -29,7 +29,7 @@
*/
#import "ORKActiveStep.h"
#import <ResearchKit/ORKActiveStep.h>
NS_ASSUME_NONNULL_BEGIN
@@ -32,25 +32,28 @@
@interface ORKAmslerGridContentView() {
UIBezierPath *path;
CGFloat dimension;
}
@end
@implementation ORKAmslerGridContentView
- (void)plotAmslerGrid {
dimension = MIN(self.bounds.size.width, self.bounds.size.height);
path = [[UIBezierPath alloc] init];
path.lineWidth = _lineWidth;
CGFloat cellSize = MIN(self.bounds.size.width, self.bounds.size.height)/_numberOfCellsPerSide;
CGFloat cellSize = dimension/_numberOfCellsPerSide;
for (int index = 0; index < _numberOfCellsPerSide; index ++) {
CGPoint startVertical = CGPointMake((CGFloat)index * cellSize, 0);
CGPoint endVertical = CGPointMake((CGFloat)index * cellSize, self.bounds.size.height);
CGPoint endVertical = CGPointMake((CGFloat)index * cellSize, dimension);
[path moveToPoint:startVertical];
[path addLineToPoint:endVertical];
CGPoint startHorizontal = CGPointMake(0, (CGFloat)index * cellSize);
CGPoint endHorizontal = CGPointMake(self.bounds.size.width, (CGFloat)index * cellSize);
CGPoint endHorizontal = CGPointMake(dimension, (CGFloat)index * cellSize);
[path moveToPoint:startHorizontal];
[path addLineToPoint:endHorizontal];
}
@@ -65,15 +68,11 @@
[self plotAmslerGrid];
[_lineColor setStroke];
[path stroke];
UIBezierPath *circleInTheCenter = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2) radius:self.bounds.size.width/_ratioOfWidthToRadius startAngle:0 endAngle:360 clockwise:YES];
UIBezierPath *circleInTheCenter = [UIBezierPath bezierPathWithArcCenter:CGPointMake(dimension/2, dimension/2) radius:dimension/_ratioOfWidthToRadius startAngle:0 endAngle:360 clockwise:YES];
[_lineColor setFill];
[circleInTheCenter fill];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
- (instancetype)init
{
self = [super init];
@@ -83,8 +82,21 @@
_ratioOfWidthToRadius = 75;
_lineColor = [UIColor blackColor];
_backgroundColor = [UIColor whiteColor];
[self setDimensionConstraint];
}
return self;
}
- (void)setDimensionConstraint {
[NSLayoutConstraint activateConstraints:@[
[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0]
]];
}
@end
@@ -76,12 +76,10 @@
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
[self.navigationController setNavigationBarHidden:YES animated:NO];
[self.navigationFooterView setHidden:YES];
_amslerGridView = [ORKAmslerGridContentView new];
_amslerGridView.translatesAutoresizingMaskIntoConstraints = NO;
self.activeStepView.activeCustomView = _amslerGridView;
self.activeStepView.stepViewFillsAvailableSpace = YES;
[self.activeStepView removeCustomContentPadding];
_freehandDrawingView = [ORKFreehandDrawingView new];
@@ -91,9 +89,8 @@
[_amslerGridView addSubview:_freehandDrawingView];
UISwipeGestureRecognizer *r = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
r.direction = UISwipeGestureRecognizerDirectionLeft;
[self.activeStepView addGestureRecognizer:r];
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
[self.activeStepView addGestureRecognizer:panGestureRecognizer];
self.activeStepView.isAccessibilityElement = YES;
self.activeStepView.accessibilityLabel = ORKLocalizedString(@"AX_AMSLER_GRID_LABEL", nil);
@@ -102,42 +99,15 @@
[self setupContraints];
}
- (void)handleSingleTap:(UISwipeGestureRecognizer *)recognizer {
[self finish];
- (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateChanged) {
[self finish];
}
}
- (void)setupContraints {
CGFloat width = MIN(self.view.bounds.size.width, self.view.bounds.size.height);
NSArray *constraints = @[
[NSLayoutConstraint constraintWithItem:_amslerGridView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:width],
[NSLayoutConstraint constraintWithItem:_amslerGridView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:width],
[NSLayoutConstraint constraintWithItem:_amslerGridView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_amslerGridView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_freehandDrawingView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
@@ -35,6 +35,15 @@
NS_ASSUME_NONNULL_BEGIN
@class ORKRecordButton;
typedef NS_ENUM(NSUInteger, ORKAudioContentViewEvent) {
ORKAudioContentViewEventStartRecording = 0,
ORKAudioContentViewEventStopRecording
};
typedef void (^ORKAudioStepContentViewEventHandler)(ORKAudioContentViewEvent);
@interface ORKAudioContentView : ORKActiveStepCustomView
@property (nonatomic, copy, nullable) UIColor *keyColor;
@@ -48,6 +57,10 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy, nullable) NSArray *samples;
@property (nonatomic) BOOL useRecordButton;
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler;
// Samples should be in the range of (0, 1).
- (void)addSample:(NSNumber *)sample;
- (void)removeAllSamples;
@@ -55,3 +68,4 @@ NS_ASSUME_NONNULL_BEGIN
@end
NS_ASSUME_NONNULL_END
+91 -17
View File
@@ -29,8 +29,9 @@
*/
#import "ORKAudioContentView.h"
#import "ORKAudioGraphView.h"
#import "ORKAudioMeteringView.h"
#import "ORKHeadlineLabel.h"
#import "ORKLabel.h"
@@ -38,6 +39,7 @@
#import "ORKAccessibility.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKRecordButton.h"
// The central blue region.
@@ -46,6 +48,8 @@ static const CGFloat GraphViewBlueZoneHeight = 170;
// The two bands at top and bottom which are "loud" each have this height.
static const CGFloat GraphViewRedZoneHeight = 25;
static const CGFloat ORKAudioStepContentRecordButtonVerticalSpacing = 20.0;
@interface ORKAudioTimerLabel : ORKLabel
@end
@@ -61,11 +65,12 @@ static const CGFloat GraphViewRedZoneHeight = 25;
@end
@interface ORKAudioContentView ()
@interface ORKAudioContentView () <ORKRecordButtonDelegate>
@property (nonatomic, strong) ORKHeadlineLabel *alertLabel;
@property (nonatomic, strong) UILabel *timerLabel;
@property (nonatomic, strong) ORKAudioGraphView *graphView;
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
@property (nonatomic, copy, nullable) ORKAudioStepContentViewEventHandler viewEventhandler;
@end
@@ -73,19 +78,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
@implementation ORKAudioContentView {
NSMutableArray *_samples;
UIColor *_keyColor;
ORKRecordButton *_recordButton;
BOOL _checkAudioLevel;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
_checkAudioLevel = YES;
_useRecordButton = NO;
self.alertLabel = [ORKHeadlineLabel new];
_alertLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.timerLabel = [ORKAudioTimerLabel new];
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
_timerLabel.textAlignment = NSTextAlignmentRight;
self.graphView = [ORKAudioGraphView new];
self.graphView = [[ORKAudioMeteringView alloc] init];
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
@@ -95,8 +104,8 @@ static const CGFloat GraphViewRedZoneHeight = 25;
[self addSubview:_timerLabel];
[self addSubview:_graphView];
_timerLabel.text = @"06:00";
_alertLabel.text = ORKLocalizedString(@"AUDIO_TOO_LOUD_LABEL", nil);
// _timerLabel.text set in -updateTimerLabel:
self.alertThreshold = GraphViewBlueZoneHeight / ((GraphViewRedZoneHeight * 2) + GraphViewBlueZoneHeight);
@@ -122,10 +131,22 @@ static const CGFloat GraphViewRedZoneHeight = 25;
[self updateAlertLabelHidden];
}
- (void)setUseRecordButton:(BOOL)useRecordButton {
_useRecordButton = useRecordButton;
if (_useRecordButton) {
_checkAudioLevel = NO;
[_timerLabel setHidden: YES];
[self setupRecordButton];
[self setUpConstraints];
}
}
- (void)applyKeyColor {
UIColor *keyColor = [self keyColor];
_timerLabel.textColor = keyColor;
_graphView.keyColor = keyColor;
_graphView.meterColor = keyColor;
}
- (UIColor *)keyColor {
@@ -143,6 +164,44 @@ static const CGFloat GraphViewRedZoneHeight = 25;
_graphView.alertColor = alertColor;
}
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler {
self.viewEventhandler = [handler copy];
}
- (void)invokeViewEventHandlerWithEvent:(ORKAudioContentViewEvent)event {
if (self.viewEventhandler) {
dispatch_async(dispatch_get_main_queue(), ^{
self.viewEventhandler(event);
});
}
}
- (void)buttonPressed:(ORKRecordButton *)recordButton {
switch (recordButton.buttonType) {
case ORKRecordButtonTypeRecord:
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStartRecording];
[_recordButton setButtonType:ORKRecordButtonTypeStop];
break;
default:
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStopRecording];
[_recordButton setButtonState:ORKRecordButtonStateDisabled];
break;
}
}
- (void)setupRecordButton {
if (!_recordButton) {
_recordButton = [[ORKRecordButton alloc] init];
_recordButton.delegate = self;
_recordButton.translatesAutoresizingMaskIntoConstraints = NO;
[_recordButton setButtonType:ORKRecordButtonTypeRecord];
[self addSubview:_recordButton];
}
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
@@ -161,12 +220,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
const CGFloat innerMargin = 2;
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
views:views]];
if (_useRecordButton) {
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin)}
views:views]];
[constraints addObject:[_recordButton.topAnchor constraintEqualToAnchor:_graphView.bottomAnchor constant:ORKAudioStepContentRecordButtonVerticalSpacing]];
[constraints addObject:[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor]];
} else {
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
views:views]];
}
[constraints addObject:[NSLayoutConstraint constraintWithItem:_graphView
attribute:NSLayoutAttributeHeight
@@ -206,18 +276,21 @@ static const CGFloat GraphViewRedZoneHeight = 25;
}
- (void)updateGraphSamples {
_graphView.values = _samples;
_graphView.samples = _samples;
[self updateAlertLabelHidden];
}
- (void)updateAlertLabelHidden {
NSNumber *sample = _samples.lastObject;
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
if (_alertLabel.hidden && show) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
if (_checkAudioLevel) {
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
if (_alertLabel.hidden && show) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
}
_alertLabel.hidden = !show;
}
_alertLabel.hidden = !show;
}
- (void)setSamples:(NSArray *)samples {
@@ -260,3 +333,4 @@ static const CGFloat GraphViewRedZoneHeight = 25;
}
@end
@@ -0,0 +1,64 @@
/*
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 <ResearchKit/ResearchKit.h>
#import <ResearchKit/ORKFitnessStep.h>
NS_ASSUME_NONNULL_BEGIN
@class ORKBundleAsset;
ORK_CLASS_AVAILABLE
@interface ORKVocalCue : NSObject <NSSecureCoding, NSCopying>
@property (atomic) NSTimeInterval time;
@property (atomic, copy) NSString *spokenText;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithTime:(NSTimeInterval) time
spokenText:(NSString *) spokenText;
@end
ORK_CLASS_AVAILABLE
@interface ORKAudioFitnessStep : ORKFitnessStep
@property (nonatomic, copy) ORKBundleAsset *audioAsset;
@property (nonatomic, copy) NSArray<ORKVocalCue *> *vocalCues;
- (instancetype)initWithIdentifier:(NSString *) identifier
audioAsset:(ORKBundleAsset *) audioAsset
vocalCues:(nullable NSArray<ORKVocalCue *> *) vocalCues;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,146 @@
/*
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 "ORKAudioFitnessStep.h"
#import "ORKAudioFitnessStepViewController.h"
#import "ORKBundleAsset.h"
#import "ORKHelpers_Internal.h"
@implementation ORKVocalCue
- (instancetype)initWithTime:(NSTimeInterval)time
spokenText:(NSString *)spokenText {
self = [super init];
if (self) {
self.time = time;
self.spokenText = [spokenText copy];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
if (self) {
ORK_DECODE_DOUBLE(coder, time);
ORK_DECODE_OBJ_CLASS(coder, spokenText, NSString);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
ORK_ENCODE_DOUBLE(coder, time);
ORK_ENCODE_OBJ(coder, spokenText);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
return [[ORKVocalCue alloc] initWithTime:self.time spokenText:self.spokenText];
}
- (BOOL)isEqual:(id)other
{
if ([self class] != [other class]) {
return NO;
}
__typeof(self) castObject = other;
return (self.time == castObject.time &&
ORKEqualObjects(self.spokenText, castObject.spokenText));
}
@end
@implementation ORKAudioFitnessStep
- (Class)stepViewControllerClass {
return [ORKAudioFitnessStepViewController class];
}
- (instancetype)initWithIdentifier:(NSString *)identifier
audioAsset:(ORKBundleAsset *)audioAsset
vocalCues:(nullable NSArray<ORKVocalCue *> *)vocalCues {
self = [super initWithIdentifier:identifier];
if (self) {
self.stepDuration = 180;
self.shouldShowDefaultTimer = NO;
self.audioAsset = [audioAsset copy];
self.vocalCues = vocalCues == nil ? [NSArray new] : [vocalCues copy];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
ORK_DECODE_OBJ_CLASS(coder, audioAsset, ORKBundleAsset);
ORK_DECODE_OBJ_ARRAY(coder, vocalCues, ORKVocalCue);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
ORK_ENCODE_OBJ(coder, audioAsset);
ORK_ENCODE_OBJ(coder, vocalCues);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAudioFitnessStep *step = [super copyWithZone:zone];
step.audioAsset = [self.audioAsset copy];
step.vocalCues = [self.vocalCues copy];
return step;
}
- (BOOL)isEqual:(id)other
{
BOOL superIsEqual = [super isEqual:other];
__typeof(self) castObject = other;
return (superIsEqual &&
ORKEqualObjects(self.audioAsset, castObject.audioAsset) &&
ORKEqualObjects(self.vocalCues, castObject.vocalCues));
}
- (NSUInteger)hash
{
return super.hash ^ self.audioAsset.hash ^ self.vocalCues.hash;
}
@end
@@ -0,0 +1,50 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <ResearchKit/ResearchKit.h>
#import <ResearchKit/ORKFitnessStepViewController.h>
NS_ASSUME_NONNULL_BEGIN
// Test Seam
@protocol ORKAudioPlayer
- (BOOL)prepareToPlay;
- (BOOL)play;
- (void)pause;
- (void)stop;
@end
@interface ORKAudioFitnessStepViewController : ORKFitnessStepViewController
@property (nonatomic) id<ORKAudioPlayer> audioPlayer;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,150 @@
/*
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 "ORKActiveStepTimer.h"
#import "ORKAudioFitnessStep.h"
#import "ORKAudioFitnessStepViewController.h"
#import "ORKVoiceEngine.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKHelpers_Internal.h"
#import <AVFoundation/AVFoundation.h>
@interface ORKAVAudioPlayer : AVAudioPlayer <ORKAudioPlayer>
@end
@implementation ORKAVAudioPlayer
@end
@interface ORKAudioFitnessStepViewController ()
@property (nonatomic) BOOL appHasAudioBackgroundMode;
@property (nonatomic) NSMutableSet<ORKVocalCue *> *playedCues;
@end
@implementation ORKAudioFitnessStepViewController
- (ORKAudioFitnessStep *)audioStep {
return (ORKAudioFitnessStep *)self.step;
}
- (NSMutableSet *)playedCues {
if (!_playedCues) {
_playedCues = [NSMutableSet new];
}
return _playedCues;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.audioPlayer prepareToPlay];
}
- (void)start {
[super start];
if (self.appHasAudioBackgroundMode) {
[self enableBackgroundAudioSession:YES];
}
[self.audioPlayer play];
}
- (void)suspend {
[super suspend];
[self.audioPlayer pause];
}
- (void)resume {
[super resume];
[self.audioPlayer play];
}
- (void)finish {
[super finish];
[self.audioPlayer stop];
if (self.appHasAudioBackgroundMode) {
[self enableBackgroundAudioSession:NO];
}
}
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
[super countDownTimerFired:timer finished:finished];
ORKVoiceEngine *voice = [ORKVoiceEngine sharedVoiceEngine];
NSTimeInterval timeRemaining = [timer duration] - [timer runtime];
for (ORKVocalCue *cue in [self audioStep].vocalCues) {
if (cue.time >= timeRemaining && ![self.playedCues containsObject:cue]) {
[self.playedCues addObject:cue];
[voice speakText: cue.spokenText];
}
}
}
- (BOOL)appHasAudioBackgroundMode {
NSArray<NSString *> *backgroundModes = (NSArray<NSString *> *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
BOOL hasBackgroundAudioMode = [backgroundModes containsObject:@"audio"];
return hasBackgroundAudioMode;
}
- (void)enableBackgroundAudioSession:(BOOL)enabled {
NSError *error;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
mode:AVAudioSessionModeDefault
routeSharingPolicy:AVAudioSessionRouteSharingPolicyLongFormAudio
options:0
error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessStepViewController failed to setup audio session: %@", error);
return;
}
[[AVAudioSession sharedInstance] setActive:enabled error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessViewController failed to start audio session: %@", error);
return;
}
}
- (id<ORKAudioPlayer>)audioPlayer {
if (!_audioPlayer) {
ORKAudioFitnessStep *step = [self audioStep];
NSError *error;
_audioPlayer = [[ORKAVAudioPlayer alloc] initWithContentsOfURL:step.audioAsset.url error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessStepViewController Failed to load audio file: %@", error.localizedFailureReason);
}
}
return _audioPlayer;
}
@end
+6 -6
View File
@@ -77,12 +77,12 @@
const double ORKSineWaveToneGeneratorAmplitudeDefault = 0.03f;
const double ORKSineWaveToneGeneratorSampleRateDefault = 44100.0f;
OSStatus ORKAudioGeneratorRenderTone(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
static OSStatus ORKAudioGeneratorRenderTone(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
// Fixed amplitude is good enough for our purposes
const double amplitude = ORKSineWaveToneGeneratorAmplitudeDefault;
+4 -9
View File
@@ -28,20 +28,15 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import UIKit;
#import <ResearchKit/ORKAudioMeteringView.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKAudioGraphView : UIView
@property (nonatomic, strong) UIColor *keyColor;
@property (nonatomic, strong) UIColor *alertColor;
@property (nonatomic, copy) NSArray *values;
@property (nonatomic) CGFloat alertThreshold;
@interface ORKAudioGraphView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
@end
NS_ASSUME_NONNULL_END
+46 -26
View File
@@ -32,11 +32,21 @@
#import "ORKAudioGraphView.h"
#import "ORKSkin.h"
static const CGFloat ValueLineWidth = 4.5;
static const CGFloat ValueLineMargin = 1.5;
static const CGFloat GraphHeight = 150.0;
@interface ORKAudioGraphView ()
/// ORKAudioMetering
@property (nonatomic, copy, nullable) NSArray<NSNumber *> *samples;
@property (nonatomic, assign) float alertThreshold;
/// ORKAudioMeteringView
@property (nonatomic, strong) UIColor *meterColor;
@property (nonatomic, strong, nullable) UIColor *alertColor;
@end
@implementation ORKAudioGraphView
@@ -46,7 +56,7 @@ static const CGFloat GraphHeight = 150.0;
[self setUpConstraints];
#if TARGET_IPHONE_SIMULATOR
_values = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
_samples = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
#endif
}
return self;
@@ -65,26 +75,6 @@ static const CGFloat GraphHeight = 150.0;
[NSLayoutConstraint activateConstraints:@[heightConstraint]];
}
- (void)setValues:(NSArray *)values {
_values = [values copy];
[self setNeedsDisplay];
}
- (void)setKeyColor:(UIColor *)keyColor {
_keyColor = [keyColor copy];
[self setNeedsDisplay];
}
- (void)setAlertColor:(UIColor *)alertColor {
_alertColor = [alertColor copy];
[self setNeedsDisplay];
}
- (void)setAlertThreshold:(CGFloat)alertThreshold {
_alertThreshold = alertThreshold;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
@@ -104,7 +94,7 @@ static const CGFloat GraphHeight = 150.0;
[centerLine addLineToPoint:(CGPoint){.x = maxX, .y = midY}];
CGContextSetLineWidth(context, 1.0 / scale);
[_keyColor setStroke];
[_meterColor setStroke];
CGFloat lengths[2] = {3, 3};
CGContextSetLineDash(context, 0, lengths, 2);
@@ -125,7 +115,7 @@ static const CGFloat GraphHeight = 150.0;
path1.lineWidth = ValueLineWidth;
UIBezierPath *path2 = [path1 copy];
for (NSNumber *value in [_values reverseObjectEnumerator]) {
for (NSNumber *value in [_samples reverseObjectEnumerator]) {
CGFloat floatValue = value.doubleValue;
UIBezierPath *path = nil;
@@ -134,7 +124,7 @@ static const CGFloat GraphHeight = 150.0;
[_alertColor setStroke];
} else {
path = path2;
[_keyColor setStroke];
[_meterColor setStroke];
}
[path moveToPoint:(CGPoint){.x = x, .y = midY - floatValue*halfHeight}];
[path addLineToPoint:(CGPoint){.x = x, .y = midY + floatValue*halfHeight}];
@@ -150,11 +140,41 @@ static const CGFloat GraphHeight = 150.0;
[_alertColor setStroke];
[path1 stroke];
[_keyColor setStroke];
[_meterColor setStroke];
[path2 stroke];
}
CGContextRestoreGState(context);
}
#pragma mark - ORKAudioMetering
- (void)setSamples:(NSArray<NSNumber *> *)samples
{
_samples = [samples copy];
[self setNeedsDisplay];
}
- (void)setAlertThreshold:(float)threshold
{
_alertThreshold = threshold;
[self setNeedsDisplay];
}
#pragma mark = ORKAudioMeteringView
- (void)setMeterColor:(UIColor *)meterColor
{
_meterColor = [meterColor copy];
[self setNeedsDisplay];
}
- (void)setAlertColor:(UIColor *)alertColor
{
_alertColor = [alertColor copy];
[self setNeedsDisplay];
}
@end
@@ -75,3 +75,4 @@ ORK_CLASS_AVAILABLE
@end
NS_ASSUME_NONNULL_END
@@ -148,7 +148,7 @@ Float32 const VolumeClamp = 60.0;
// Setup reader
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
if (urlAsset.tracks.count == 0) {
NSLog(@"No tracks found for urlAsset: %@", fileURL);
ORK_Log_Info("No tracks found for urlAsset: %@", fileURL);
return NO;
}
@@ -216,3 +216,4 @@ Float32 const VolumeClamp = 60.0;
@end
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
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:
@@ -29,26 +29,30 @@
*/
@import UIKit;
@import UIKit;
NS_ASSUME_NONNULL_BEGIN
@class ORKConsentSection;
extern NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit);
@interface ORKConsentSceneViewController : UIViewController
@protocol ORKAudioMetering <NSObject>
- (instancetype)initWithSection:(ORKConsentSection *)section;
- (void)setSamples:(nullable NSArray<NSNumber *> *)samples;
@property (nonatomic, readonly, nullable) ORKConsentSection *section;
- (void)setAlertThreshold:(float)threshold;
@property (nonatomic, strong, nullable) UIBarButtonItem *continueButtonItem;
@end
@property (nonatomic, strong, nullable) UIBarButtonItem *cancelButtonItem;
@protocol ORKAudioMeteringDisplay
@property (nonatomic, strong, nullable) NSString *learnMoreButtonTitle;
- (void)setMeterColor:(nonnull UIColor *)meterColor;
@property (nonatomic, assign) BOOL imageHidden;
- (void)setAlertColor:(nonnull UIColor *)alertColor;
@end
@interface ORKAudioMeteringView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
@end
@@ -0,0 +1,146 @@
/*
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 "ORKAudioMeteringView.h"
#import "ORKAudioGraphView.h"
NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit) {
if (samples.count > limit) {
return [samples subarrayWithRange:(NSRange){samples.count - limit, samples.count - 1}];
}
return [samples copy];
}
@interface ORKAudioMeteringView ()
@property (nonatomic, strong) UIView<ORKAudioMetering, ORKAudioMeteringDisplay> *meteringView;
@end
@implementation ORKAudioMeteringView
- (instancetype)init
{
self = [super init];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (void)configureMeteringView
{
if (!_meteringView) {
[self setMeteringView:[[ORKAudioGraphView alloc] init]];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[_meteringView setFrame:[self bounds]];
}
- (void)setHidden:(BOOL)hidden
{
[super setHidden:hidden];
[_meteringView setHidden:hidden];
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if ([self superview] == nil)
{
[_meteringView removeFromSuperview];
}
else
{
[self addSubview:_meteringView];
}
}
#pragma mark - ORKAudioMetering
- (void)setSamples:(NSArray<NSNumber *> *)samples
{
[_meteringView setSamples:samples];
}
- (void)setAlertThreshold:(float)threshold
{
[_meteringView setAlertThreshold:threshold];
}
#pragma mark - ORKAudioMeteringDisplay
- (void)setMeterColor:(UIColor *)meterColor
{
[_meteringView setMeterColor:meterColor];
}
- (void)setAlertColor:(UIColor *)alertColor
{
[_meteringView setAlertColor:alertColor];
}
#pragma mark - UIAccessibility
- (BOOL)isAccessibilityElement {
return NO;
}
@end
+9 -9
View File
@@ -50,7 +50,7 @@
@implementation ORKAudioRecorder
- (void)dealloc {
ORK_Log_Debug(@"Remove audiorecorder %p", self);
ORK_Log_Debug("Remove audiorecorder %p", self);
[_audioRecorder stop];
_audioRecorder = nil;
}
@@ -85,7 +85,7 @@
if (_savedSessionCategory) {
NSError *error;
if (![[AVAudioSession sharedInstance] setCategory:_savedSessionCategory error:&error]) {
ORK_Log_Error(@"Failed to restore the audio session category: %@", [error localizedDescription]);
ORK_Log_Error("Failed to restore the audio session category: %@", [error localizedDescription]);
}
_savedSessionCategory = nil;
}
@@ -113,7 +113,7 @@
return;
}
ORK_Log_Debug(@"Create audioRecorder %p", self);
ORK_Log_Debug("Create audioRecorder %p", self);
_audioRecorder = [[AVAudioRecorder alloc]
initWithURL:soundFileURL
settings:self.recorderSettings
@@ -245,29 +245,29 @@
return [[self recordingDirectoryURL] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", [self logName], [self extension]]];
}
- (BOOL)recreateFileWithError:(NSError **)error {
- (BOOL)recreateFileWithError:(NSError **)errorOut {
NSURL *url = [self recordingFileURL];
if (!url) {
if (error) {
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteInvalidFileNameError userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"ERROR_RECORDER_NO_OUTPUT_DIRECTORY", nil)}];
if (errorOut != NULL) {
*errorOut = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteInvalidFileNameError userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"ERROR_RECORDER_NO_OUTPUT_DIRECTORY", nil)}];
}
return NO;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:error]) {
if (![fileManager createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:errorOut]) {
return NO;
}
if ([fileManager fileExistsAtPath:[url path]]) {
if (![fileManager removeItemAtPath:[url path] error:error]) {
if (![fileManager removeItemAtPath:[url path] error:errorOut]) {
return NO;
}
}
[fileManager createFileAtPath:[url path] contents:nil attributes:nil];
[fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(ORKFileProtectionCompleteUnlessOpen)} ofItemAtPath:[url path] error:error];
[fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(ORKFileProtectionCompleteUnlessOpen)} ofItemAtPath:[url path] error:errorOut];
return YES;
}
+12
View File
@@ -38,6 +38,18 @@ NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKAudioStep : ORKActiveStep
/**
A Boolean value that determines if audio recording will start and stop
automatcially or be controlled via a ORKRecordButton
When set to YES the user will be able to start and stop the audio recording
by the ORKRecordButton
The default value of this property is `NO`.
*/
@property (nonatomic) BOOL useRecordButton;
@end
NS_ASSUME_NONNULL_END
+43 -1
View File
@@ -49,16 +49,27 @@
if (self) {
self.shouldShowDefaultTimer = NO;
self.shouldStartTimerAutomatically = YES;
self.useRecordButton = NO;
}
return self;
}
- (void)setUseRecordButton:(BOOL)useRecordButton {
_useRecordButton = useRecordButton;
[self setShouldStartTimerAutomatically:!_useRecordButton];
if (_useRecordButton) {
self.stepDuration = 0;
}
}
- (void)validateParameters {
[super validateParameters];
NSTimeInterval const ORKAudioTaskMinimumDuration = 5.0;
if ( self.stepDuration < ORKAudioTaskMinimumDuration) {
if ( self.stepDuration < ORKAudioTaskMinimumDuration && !self.useRecordButton) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKAudioTaskMinimumDuration)] userInfo:nil];
}
}
@@ -67,4 +78,35 @@
return NO;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAudioStep *step = [super copyWithZone:zone];
step.useRecordButton = self.useRecordButton;
return step;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_BOOL(aDecoder, useRecordButton);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_BOOL(aCoder, useRecordButton);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame && self.useRecordButton == castObject.useRecordButton);
}
@end
@@ -29,6 +29,7 @@
*/
@import Foundation;
#import <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKActiveStepViewController.h>
@@ -44,3 +45,4 @@ ORK_CLASS_AVAILABLE
@end
NS_ASSUME_NONNULL_END
@@ -59,6 +59,7 @@
ORKAudioContentView *_audioContentView;
ORKAudioRecorder *_audioRecorder;
ORKActiveStepTimer *_timer;
NSTimer *_intervalTimer;
NSError *_audioRecorderError;
}
@@ -83,6 +84,12 @@
// Do any additional setup after loading the view.
_audioContentView = [ORKAudioContentView new];
_audioContentView.timeLeft = self.audioStep.stepDuration;
_audioContentView.useRecordButton = self.audioStep.useRecordButton && self.audioStep.stepDuration == 0;
__weak typeof(self) weakSelf = self;
[_audioContentView setViewEventHandler:^(ORKAudioContentViewEvent event) {
[weakSelf handleContentViewEvent:event];
}];
if (self.alertThreshold > 0) {
_audioContentView.alertThreshold = self.alertThreshold;
@@ -91,6 +98,19 @@
self.activeStepView.activeCustomView = _audioContentView;
}
- (void)handleContentViewEvent:(ORKAudioContentViewEvent)event {
switch (event) {
case ORKAudioContentViewEventStartRecording:
[self start];
break;
case ORKAudioContentViewEventStopRecording:
[self finish];
break;
}
}
- (void)audioRecorderDidChange {
_audioRecorder.audioRecorder.meteringEnabled = YES;
[self setAvAudioRecorder:_audioRecorder.audioRecorder];
@@ -116,42 +136,72 @@
if (_audioRecorderError) {
return;
}
[_avAudioRecorder updateMeters];
float value = [_avAudioRecorder averagePowerForChannel:0];
// Assume value is in range roughly -60dB to 0dB
float clampedValue = MAX(value / 60.0, -1) + 1;
[_audioContentView addSample:@(clampedValue)];
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
if (!self.audioStep.useRecordButton) {
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
}
}
- (void)startNewTimerIfNeeded {
if (!_timer) {
NSTimeInterval duration = self.audioStep.stepDuration;
ORKWeakTypeOf(self) weakSelf = self;
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
ORKStrongTypeOf(self) strongSelf = weakSelf;
[strongSelf doSample];
if (finished) {
[strongSelf finish];
}
}];
[_timer resume];
if (self.audioStep.useRecordButton) {
if (!_intervalTimer) {
_intervalTimer = [NSTimer scheduledTimerWithTimeInterval: 20 / 100
target:self selector:@selector(doSample)
userInfo:nil
repeats:YES];
}
} else {
if (!_timer) {
NSTimeInterval duration = self.audioStep.stepDuration;
ORKWeakTypeOf(self) weakSelf = self;
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
ORKStrongTypeOf(self) strongSelf = weakSelf;
[strongSelf doSample];
if (finished) {
[strongSelf finish];
}
}];
[_timer resume];
}
}
_audioContentView.finished = NO;
}
- (void)start {
[super start];
[self audioRecorderDidChange];
[_timer reset];
_timer = nil;
[self startNewTimerIfNeeded];
if (!self.audioStep.useRecordButton) {
[_timer reset];
_timer = nil;
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
[self startNewTimerIfNeeded];
}
- (void)suspend {
[super suspend];
[_timer pause];
if (!self.audioStep.useRecordButton) {
[_timer pause];
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
if (_avAudioRecorder) {
[_audioContentView addSample:@(0)];
}
@@ -160,8 +210,12 @@
- (void)resume {
[super resume];
[self audioRecorderDidChange];
[self startNewTimerIfNeeded];
[_timer resume];
if (!self.audioStep.useRecordButton) {
[_timer resume];
}
}
- (void)finish {
@@ -169,8 +223,14 @@
return;
}
[super finish];
[_timer reset];
_timer = nil;
if (!self.audioStep.useRecordButton) {
[_timer reset];
_timer = nil;
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
}
- (void)stepDidFinish {
@@ -189,3 +249,4 @@
}
@end
@@ -0,0 +1,53 @@
/*
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 AVFoundation;
#import <ResearchKit/ORKRecorder.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ORKAudioStreamingDelegate <ORKRecorderDelegate>
- (void)audioAvailable:(AVAudioPCMBuffer *)buffer;
@end
@class ORKStep;
@interface ORKAudioStreamer : ORKRecorder
- (instancetype)initWithIdentifier:(NSString *)identifier step:(nullable ORKStep *)step NS_DESIGNATED_INITIALIZER;
@property (nonatomic, strong, readonly, nullable) AVAudioEngine *audioEngine;
@end
NS_ASSUME_NONNULL_END
+222
View File
@@ -0,0 +1,222 @@
/*
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 "ORKAudioStreamer.h"
#import "ORKHelpers_Internal.h"
#import "ORKRecorder_Internal.h"
#import "ORKStep.h"
#pragma mark - ORKAudioStreamerConfiguration
@implementation ORKAudioStreamerConfiguration
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super initWithIdentifier:identifier];
return self;
}
- (ORKRecorder *)recorderForStep:(ORKStep *)step outputDirectory:(NSURL *)outputDirectory {
ORKAudioStreamer *obj = [[ORKAudioStreamer alloc] initWithIdentifier:self.identifier step:step];
return obj;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (BOOL)isEqual:(id)object {
return [super isEqual:object];
}
- (ORKPermissionMask)requestedPermissionMask {
return ORKPermissionAudioRecording;
}
@end
#pragma mark - ORKAudioStreamer
@implementation ORKAudioStreamer
{
NSString *_savedSessionCategory;
}
- (instancetype)initWithIdentifier:(NSString *)identifier step:(ORKStep *)step
{
self = [super initWithIdentifier:identifier step:step outputDirectory:nil];
if (self)
{
self.continuesInBackground = YES;
}
return self;
}
- (void)restoreSavedAudioSessionCategory
{
if (_savedSessionCategory)
{
NSError *error;
if (![[AVAudioSession sharedInstance] setCategory:_savedSessionCategory error:&error])
{
ORK_Log_Error("Failed to restore the audio session category: %@", [error localizedDescription]);
}
_savedSessionCategory = nil;
}
}
- (BOOL)isRecording
{
return [_audioEngine isRunning];
}
- (NSString *)recorderType
{
return @"audioStreaming";
}
- (void)start
{
if (!_audioEngine)
{
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
_savedSessionCategory = audioSession.category;
NSError *error = nil;
BOOL success =
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error] &&
[[AVAudioSession sharedInstance] setMode:AVAudioSessionModeMeasurement error:&error] &&
[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
if (!success && error)
{
[self finishRecordingWithError:error];
return;
}
ORK_Log_Debug("Create audioEngine recorder %p", self);
_audioEngine = [[AVAudioEngine alloc] init];
AVAudioInputNode *inputnode = _audioEngine.inputNode;
AVAudioFormat *recordingFormat = [inputnode inputFormatForBus:0];
[inputnode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when)
{
id<ORKAudioStreamingDelegate> delegate = (id<ORKAudioStreamingDelegate>)self.delegate;
if (delegate && [delegate respondsToSelector:@selector(audioAvailable:)]) {
[delegate audioAvailable:buffer];
}
}];
[_audioEngine prepare];
[_audioEngine startAndReturnError:&error];
if (error != nil)
{
[self finishRecordingWithError:error];
return;
}
}
[super start];
}
- (void)stop
{
if (!_audioEngine)
{
return;
}
[self doStopRecording];
[super stop];
}
- (void)doStopRecording
{
if (self.isRecording)
{
if ([_audioEngine isRunning])
{
[_audioEngine stop];
[[_audioEngine inputNode] removeTapOnBus:0];
}
_audioEngine = nil;
[self restoreSavedAudioSessionCategory];
}
}
- (void)finishRecordingWithError:(NSError *)error
{
[self doStopRecording];
[super finishRecordingWithError:error];
}
- (void)reset
{
if ([_audioEngine isRunning])
{
[_audioEngine stop];
[[_audioEngine inputNode] removeTapOnBus:0];
}
_audioEngine = nil;
[super reset];
}
- (void)dealloc
{
ORK_Log_Debug("Remove audiorecorder %p", self);
if ([_audioEngine isRunning])
{
[_audioEngine stop];
[[_audioEngine inputNode] removeTapOnBus:0];
}
_audioEngine = nil;
}
@end
@@ -36,7 +36,6 @@
#import "ORKCustomStepView_Internal.h"
#import "ORKLabel.h"
#import "ORKSubheadlineLabel.h"
#import "ORKVerticalContainerView.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
@@ -243,6 +242,14 @@ static const CGFloat ProgressIndicatorOuterMargin = 1.0;
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @(_countDown).stringValue);
[_countdownView startAnimateWithDuration:[(ORKActiveStep *)self.step stepDuration]];
[UIApplication.sharedApplication setIdleTimerDisabled:YES];
}
- (void)viewWillDisappear:(BOOL)animated {
[UIApplication.sharedApplication setIdleTimerDisabled:NO];
[super viewWillDisappear:animated];
}
- (void)updateCountdownLabel {
+97 -91
View File
@@ -108,10 +108,10 @@ static NSString *const ORKDataLoggerManagerConfigurationFilename = @".ORKDataLog
return (string.integerValue != 0);
}
- (BOOL)ork_setUploaded:(BOOL)uploaded error:(NSError **)error {
- (BOOL)ork_setUploaded:(BOOL)uploaded error:(NSError **)errorOut {
NSString *value = (uploaded ? @"1" : @"0");
NSData *encodedString = [value dataUsingEncoding:NSUTF8StringEncoding];
return [self ork_setData:encodedString forAttr:ORKDataLoggerUploadedAttr error:error];
return [self ork_setData:encodedString forAttr:ORKDataLoggerUploadedAttr error:errorOut];
}
- (NSData *)ork_dataForAttr:(const char *)attr {
@@ -132,12 +132,12 @@ static NSString *const ORKDataLoggerManagerConfigurationFilename = @".ORKDataLog
return data;
}
- (BOOL)ork_setData:(NSData *)data forAttr:(const char *)attr error:(NSError **)error {
- (BOOL)ork_setData:(NSData *)data forAttr:(const char *)attr error:(NSError **)errorOut {
const char *path = [self fileSystemRepresentation];
int rc = setxattr(path, attr, data.bytes, data.length, 0, 0);
if (rc != 0) {
if (error) {
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_SET_ATTRIBUTE", nil)}];
if (errorOut != NULL) {
*errorOut = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_SET_ATTRIBUTE", nil)}];
}
}
return (rc == 0);
@@ -238,19 +238,19 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
return [object isKindOfClass:[NSData class]];
}
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
return YES;
}
- (BOOL)writeData:(NSData *)data fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
- (BOOL)writeData:(NSData *)data fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
BOOL result = YES;
@try {
[fileHandle writeData:data];
}
@catch (NSException *exception) {
result = NO;
if (error) {
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorException userInfo:@{@"exception": exception}];
if (errorOut != NULL) {
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorException userInfo:@{@"exception": exception}];
}
}
return result;
@@ -265,20 +265,20 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
[fileHandle truncateFileAtOffset:offset];
}
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
if (![self canAcceptLogObject:object]) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"ORKLogFormatter accepts NSData only" userInfo:nil];
}
return [self writeData:(NSData *)object fileHandle:fileHandle error:error];
return [self writeData:(NSData *)object fileHandle:fileHandle error:errorOut];
}
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
unsigned long long checkpoint = [self checkpointWithFileHandle:fileHandle];
NSError *errorOut = nil;
NSError *error = nil;
BOOL success = YES;
for (NSObject *obj in objects) {
success = [self appendObject:obj fileHandle:fileHandle error:&errorOut];
success = [self appendObject:obj fileHandle:fileHandle error:&error];
if (!success) {
break;
}
@@ -286,8 +286,8 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
if (!success) {
[self rollbackToCheckpoint:checkpoint fileHandle:fileHandle];
if (error) {
*error = errorOut;
if (errorOut != NULL) {
*errorOut = error;
}
}
@@ -335,10 +335,10 @@ static NSInteger _ORKJSON_terminatorLength = 0;
return false;
}
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
// Write valid JSON containing no objects
NSData *data = [kJSONLogEmptyLogString dataUsingEncoding:NSUTF8StringEncoding];
return [self writeData:data fileHandle:fileHandle error:error];
return [self writeData:data fileHandle:fileHandle error:errorOut];
}
- (unsigned long long)checkpointWithFileHandle:(NSFileHandle *)fileHandle {
@@ -356,8 +356,8 @@ static NSInteger _ORKJSON_terminatorLength = 0;
}
}
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
return [self appendObjects:@[object] fileHandle:fileHandle error:error];
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
return [self appendObjects:@[object] fileHandle:fileHandle error:errorOut];
}
/*
@@ -368,7 +368,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
* before writing. When writing, we write a separator (if needed), the JSON
* object being appended, and the footer bytes.
*/
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
if (!fileHandle) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Filehandle is nil" userInfo:nil];
}
@@ -385,7 +385,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
// Seek to the end of the file; we'll later backtrack
unsigned long long offset = [fileHandle seekToEndOfFile];
if (offset == 0) {
if (![self beginLogWithFileHandle:fileHandle error:error]) {
if (![self beginLogWithFileHandle:fileHandle error:errorOut]) {
return NO;
}
offset = [fileHandle offsetInFile];
@@ -402,12 +402,13 @@ static NSInteger _ORKJSON_terminatorLength = 0;
// Serialize each object separately to the buffer, pending a single write, so the
// objects form part of a single array.
__block BOOL success = YES;
__block NSError *localError;
[objects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSData *data;
if ([obj isKindOfClass:[NSData class]]) {
data = obj;
} else {
data = [NSJSONSerialization dataWithJSONObject:obj options:(NSJSONWritingOptions)0 error:error];
data = [NSJSONSerialization dataWithJSONObject:obj options:(NSJSONWritingOptions)0 error:&localError];
}
if (!data) {
success = NO;
@@ -419,6 +420,9 @@ static NSInteger _ORKJSON_terminatorLength = 0;
}
}
}];
if (errorOut != NULL) {
*errorOut = localError;
}
if (!success) {
return success;
}
@@ -428,7 +432,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
assert(_ORKJSON_terminatorLength < offset);
[fileHandle seekToFileOffset:(offset - _ORKJSON_terminatorLength)];
success = [self writeData:outputData fileHandle:fileHandle error:error];
success = [self writeData:outputData fileHandle:fileHandle error:errorOut];
if (!success) {
[self rollbackToCheckpoint:checkpoint fileHandle:fileHandle];
@@ -530,7 +534,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
- (void)setupDirectorySource {
int dirFD = open([_url fileSystemRepresentation], O_EVTONLY);
if (dirFD < 0) {
ORK_Log_Warning(@"Could not track directory %s (%d)", [_url fileSystemRepresentation], [[NSFileManager defaultManager] fileExistsAtPath:[_url path]]);
ORK_Log_Info("Could not track directory %s (%d)", [_url fileSystemRepresentation], [[NSFileManager defaultManager] fileExistsAtPath:[_url path]]);
} else {
// Dispatch to a concurrent queue, so we don't store up blocks while our
// queue is working.
@@ -597,24 +601,28 @@ static NSInteger _ORKJSON_terminatorLength = 0;
return success;
}
- (BOOL)enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error {
- (BOOL)enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
if (!block) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Block parameter is required" userInfo:nil];
}
__block BOOL success = NO;
__block NSError *localError;
dispatch_sync(_queue, ^{
success = [self queue_enumerateLogsUploaded:uploaded block:block error:error];
success = [self queue_enumerateLogsUploaded:uploaded block:block error:&localError];
});
if (errorOut != NULL) {
*errorOut = localError;
}
return success;
}
- (BOOL)enumerateLogsNeedingUpload:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
return [self enumerateLogsUploaded:NO block:block error:error];
- (BOOL)enumerateLogsNeedingUpload:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
return [self enumerateLogsUploaded:NO block:block error:errorOut];
}
- (BOOL)enumerateLogsAlreadyUploaded:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
return [self enumerateLogsUploaded:YES block:block error:error];
- (BOOL)enumerateLogsAlreadyUploaded:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
return [self enumerateLogsUploaded:YES block:block error:errorOut];
}
- (BOOL)append:(id)object error:(NSError * __autoreleasing *)error {
@@ -701,7 +709,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
});
}
- (BOOL)queue_enumerateLogs:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
- (BOOL)queue_enumerateLogs:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
static NSArray *keys = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@@ -716,7 +724,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
NSDirectoryEnumerationSkipsPackageDescendants)
errorHandler:nil];
NSError *errorOut = nil;
NSError *error = nil;
NSMutableArray *urls = [NSMutableArray array];
for (NSURL *url in enumerator) {
if (![self urlMatchesLogName:url]) {
@@ -726,8 +734,8 @@ static NSInteger _ORKJSON_terminatorLength = 0;
// Don't include the "current" log file
continue;
}
NSDictionary *resources = [url resourceValuesForKeys:keys error:&errorOut];
if (errorOut) {
NSDictionary *resources = [url resourceValuesForKeys:keys error:&error];
if (error) {
// If there's been an error getting the resource values, give up
break;
}
@@ -737,7 +745,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
[urls addObject:url];
}
if (!errorOut) {
if (!error) {
// Sort the URLs before beginning enumeration for the caller
[urls sortUsingComparator:^NSComparisonResult(NSURL *obj1, NSURL *obj2) {
// We can assume all relate to files in the same directory
@@ -753,27 +761,23 @@ static NSInteger _ORKJSON_terminatorLength = 0;
}
}
if (error) {
*error = errorOut;
if (errorOut != NULL) {
*errorOut = error;
}
return (errorOut ? NO : YES);
return (error ? NO : YES);
}
- (BOOL)queue_enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
- (BOOL)queue_enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
return [self queue_enumerateLogs:^(NSURL *logFileUrl, BOOL *stop) {
NSError *errorOut = nil;
BOOL wantUploaded = [logFileUrl ork_isUploaded];
BOOL isWanted = (wantUploaded && uploaded) || (!wantUploaded && !uploaded);
if (isWanted) {
block(logFileUrl, stop);
}
if (errorOut) {
*stop = YES;
}
} error:error];
} error:errorOut];
}
- (NSFileHandle *)queue_makeFileHandleWithError:(NSError **)error {
- (NSFileHandle *)queue_makeFileHandleWithError:(NSError **)errorOut {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *url = [self currentLogFileURL];
@@ -785,7 +789,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
NSFileHandle *fileHandle = nil;
if (!createNewFile) {
fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:error];
fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:errorOut];
if (!fileHandle) {
// Assume it's because we can't open the file, perhaps for security reasons.
// Close and rename the log.
@@ -798,12 +802,12 @@ static NSInteger _ORKJSON_terminatorLength = 0;
NSString *filePath = [url path];
BOOL success = [fileManager createFileAtPath:filePath contents:nil attributes:nil];
if (!success) {
if (error) {
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_CREATE_FILE", nil)}];
if (errorOut != NULL) {
*errorOut = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_CREATE_FILE", nil)}];
}
return nil;
}
fileHandle = [NSFileHandle fileHandleForWritingToURL:[self currentLogFileURL] error:error];
fileHandle = [NSFileHandle fileHandleForWritingToURL:[self currentLogFileURL] error:errorOut];
if (!fileHandle) {
[fileManager removeItemAtURL:url error:nil];
return nil;
@@ -814,10 +818,10 @@ static NSInteger _ORKJSON_terminatorLength = 0;
assert(fileHandle);
// Set file protection after opening the file, so that class B works as expected.
BOOL success = [fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(self.fileProtectionMode)} ofItemAtPath:[url path] error:error];
BOOL success = [fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(self.fileProtectionMode)} ofItemAtPath:[url path] error:errorOut];
// Allow formatter to initialize the log file with header content
success = success && [self.logFormatter beginLogWithFileHandle:fileHandle error:error];
success = success && [self.logFormatter beginLogWithFileHandle:fileHandle error:errorOut];
if (!success) {
[fileHandle closeFile];
@@ -829,9 +833,9 @@ static NSInteger _ORKJSON_terminatorLength = 0;
return _currentFileHandle;
}
- (NSFileHandle *)queue_fileHandleWithError:(NSError **)error {
- (NSFileHandle *)queue_fileHandleWithError:(NSError **)errorOut {
if (!_currentFileHandle) {
_currentFileHandle = [self queue_makeFileHandleWithError:error];
_currentFileHandle = [self queue_makeFileHandleWithError:errorOut];
[_currentFileHandle seekToEndOfFile];
}
@@ -878,14 +882,14 @@ static NSInteger _ORKJSON_terminatorLength = 0;
if (((NSNumber *)parameters[NSURLIsRegularFileKey]).boolValue) {
if (((NSNumber *)parameters[NSURLFileSizeKey]).intValue > 0) {
NSURL *destinationUrl = [ORKDataLogger nextUrlForDirectoryUrl:_url logName:_logName];
ORK_Log_Debug(@"Rollover: %@ to %@", [url lastPathComponent], [destinationUrl lastPathComponent]);
ORK_Log_Debug("Rollover: %@ to %@", [url lastPathComponent], [destinationUrl lastPathComponent]);
[fileManager moveItemAtURL:url toURL:destinationUrl error:nil];
if (self.fileProtectionMode == ORKFileProtectionCompleteUnlessOpen) {
// Upgrade to complete file protection after roll-over
NSError *error = nil;
if (![fileManager setAttributes:@{NSFileProtectionKey: NSFileProtectionComplete}
ofItemAtPath:[destinationUrl path] error:&error]) {
ORK_Log_Warning(@"Error setting NSFileProtectionComplete on %@: %@", destinationUrl, error);
ORK_Log_Error("Error setting NSFileProtectionComplete on %@: %@", destinationUrl, error);
}
}
@@ -922,15 +926,15 @@ static NSInteger _ORKJSON_terminatorLength = 0;
[self queue_closeAndRenameLog];
}
- (BOOL)queue_append:(id)object error:(NSError **)error {
- (BOOL)queue_append:(id)object error:(NSError **)errorOut {
[self queue_rolloverIfNeeded];
NSFileHandle *fileHandle = [self queue_fileHandleWithError:error];
NSFileHandle *fileHandle = [self queue_fileHandleWithError:errorOut];
if (!fileHandle) {
return NO;
}
BOOL result = [self.logFormatter appendObject:object fileHandle:_currentFileHandle error:error];
BOOL result = [self.logFormatter appendObject:object fileHandle:_currentFileHandle error:errorOut];
// Quick check to see if we've run over the maximum log file size
if ((self.maximumCurrentLogFileSize > 0) && ([_currentFileHandle offsetInFile] >= self.maximumCurrentLogFileSize)) {
@@ -940,15 +944,15 @@ static NSInteger _ORKJSON_terminatorLength = 0;
return result;
}
- (BOOL)queue_appendObjects:(NSArray *)objects error:(NSError **)error {
- (BOOL)queue_appendObjects:(NSArray *)objects error:(NSError **)errorOut {
[self queue_rolloverIfNeeded];
NSFileHandle *fileHandle = [self queue_fileHandleWithError:error];
NSFileHandle *fileHandle = [self queue_fileHandleWithError:errorOut];
if (!fileHandle) {
return NO;
}
BOOL result = [self.logFormatter appendObjects:objects fileHandle:_currentFileHandle error:error];
BOOL result = [self.logFormatter appendObjects:objects fileHandle:_currentFileHandle error:errorOut];
// Quick check to see if we've run over the maximum log file size
if ((self.maximumCurrentLogFileSize > 0) && ([_currentFileHandle offsetInFile] >= self.maximumCurrentLogFileSize)) {
@@ -957,23 +961,24 @@ static NSInteger _ORKJSON_terminatorLength = 0;
return result;
}
- (BOOL)queue_markFileUploaded:(BOOL)uploaded atURL:(NSURL *)url error:(NSError **)error {
BOOL success = [url ork_setUploaded:uploaded error:error];
- (BOOL)queue_markFileUploaded:(BOOL)uploaded atURL:(NSURL *)url error:(NSError **)errorOut {
BOOL success = [url ork_setUploaded:uploaded error:errorOut];
[self queue_setNeedsUpdateBytes];
return success;
}
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs withError:(NSError **)error {
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs withError:(NSError **)errorOut {
NSFileManager *fileManager = [NSFileManager defaultManager];
__block NSMutableArray *errors = [NSMutableArray array];
__block NSError *error = nil;
BOOL success = [self queue_enumerateLogs:^(NSURL *logFileUrl, BOOL *stop) {
if ([fileURLs containsObject:logFileUrl]) {
NSError *errorOut = nil;
BOOL uploaded = [logFileUrl ork_isUploaded];
if (uploaded) {
if (![fileManager removeItemAtURL:logFileUrl error:&errorOut]) {
[errors addObject:errorOut];
if (![fileManager removeItemAtURL:logFileUrl error:&error]) {
[errors addObject:error];
error = nil;
}
} else {
// File was requested to be removed, but was not marked uploaded
@@ -982,16 +987,17 @@ static NSInteger _ORKJSON_terminatorLength = 0;
userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_COULD_NOT_MAORK", nil), @"url": logFileUrl}]];
}
}
} error:error];
} error:&error];
if (!success && error) {
[errors addObject:error];
error = nil;
}
// Reporting multiple errors
if (errors.count) {
if (!success && error && *error) {
[errors addObject:*error];
*error = [NSError errorWithDomain:ORKErrorDomain
code:ORKErrorMultipleErrors
userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_MULTIPLE", nil), @"errors": errors}];
}
if (errorOut != NULL) {
*errorOut = [NSError errorWithDomain:ORKErrorDomain
code:ORKErrorMultipleErrors
userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_MULTIPLE", nil), @"errors": errors}];
success = NO;
}
return success;
@@ -1196,14 +1202,14 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
return logNames;
}
- (BOOL)queue_enumerateLogsNeedingUpload:(void (^)(ORKDataLogger *dataLogger, NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
- (BOOL)queue_enumerateLogsNeedingUpload:(void (^)(ORKDataLogger *dataLogger, NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
BOOL success = YES;
NSMutableArray *allFiles = [NSMutableArray array];
// Collect all the log file URLs so we can sort them by date rather than enumerating by logger.
for (ORKDataLogger *logger in _records.allValues) {
success = [logger enumerateLogsNeedingUpload:^(NSURL *logFileUrl, BOOL *stop) {
[allFiles addObject:logFileUrl];
} error:error];
} error:errorOut];
if (!success) {
break;
@@ -1247,7 +1253,7 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
return success;
}
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)error {
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)errorOut {
BOOL success = YES;
NSMutableArray *notRemoved = [NSMutableArray array];
for (NSURL *url in fileURLs) {
@@ -1257,15 +1263,15 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not from a known logger" userInfo:@{@"url":url}];
}
NSError *errorOut = nil;
BOOL itemSuccess = [[NSFileManager defaultManager] removeItemAtURL:url error:&errorOut];
NSError *error = nil;
BOOL itemSuccess = [[NSFileManager defaultManager] removeItemAtURL:url error:&error];
if (!itemSuccess) {
[notRemoved addObject:url];
success = NO;
}
}
if (error && notRemoved.count) {
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
if (errorOut != NULL && notRemoved.count) {
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
}
return success;
}
@@ -1279,7 +1285,7 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
return success;
}
- (BOOL)queue_unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)error {
- (BOOL)queue_unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)errorOut {
BOOL success = YES;
NSMutableArray<NSURL *> *notRemoved = [NSMutableArray array];
for (NSURL *url in fileURLs) {
@@ -1289,15 +1295,15 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not from a known logger" userInfo:@{@"url":url}];
}
NSError *errorOut = nil;
BOOL itemSuccess = [logger markFileUploaded:NO atURL:url error:&errorOut];
NSError *error = nil;
BOOL itemSuccess = [logger markFileUploaded:NO atURL:url error:&error];
if (!itemSuccess) {
[notRemoved addObject:url];
success = NO;
}
}
if (error && notRemoved.count) {
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
if (errorOut != NULL && notRemoved.count) {
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
}
return success;
}
@@ -1310,7 +1316,7 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
return success;
}
- (BOOL)queue_removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError **)error {
- (BOOL)queue_removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError **)errorOut {
if (bytes == 0) {
for (ORKDataLogger *logger in _records) {
[logger removeAllFilesWithError:nil];
@@ -1360,8 +1366,8 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
} error:nil];
}
if (error && (totalBytes > bytes)) {
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorObjectNotFound userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"ERROR_DATALOGGER_COULD_NOT_FREE_SPACE", nil)}];
if (errorOut != NULL && (totalBytes > bytes)) {
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorObjectNotFound userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"ERROR_DATALOGGER_COULD_NOT_FREE_SPACE", nil)}];
}
return (totalBytes <= bytes);
@@ -0,0 +1,42 @@
/*
Copyright (c) 2021, 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/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKEnvironmentSPLMeterBarView : UIView
- (void)setProgress:(CGFloat)progress;
- (void)stopAnimation;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,292 @@
/*
Copyright (c) 2021, 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 "ORKEnvironmentSPLMeterBarView.h"
#import <QuartzCore/QuartzCore.h>
static const CGFloat ORKEnvironmentSPLMeterSquareSize = 8.0;
static const CGFloat ORKEnvironmentSPLMeterSquareDistance = 4.0;
static const int ORKEnvironmentSPLMeterNumberOfRows = 4;
@interface ORKEnvironmentSPLMeterColumnView : UIView {
int _numberOfRows;
CGFloat _squareSize;
CGFloat _cornerRadius;
NSArray<CAShapeLayer*> *_dots;
}
- (void)setColor:(UIColor *)color;
- (void)setOpacity:(CGFloat)opacity;
@end
@implementation ORKEnvironmentSPLMeterColumnView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_numberOfRows = ORKEnvironmentSPLMeterNumberOfRows;
_squareSize = ORKEnvironmentSPLMeterSquareSize;
_cornerRadius = ORKEnvironmentSPLMeterSquareDistance;
[self initRows];
}
return self;
}
- (void)initRows {
CGFloat halfSquareSize = _squareSize * 0.5;
CGFloat spacing = _squareSize + halfSquareSize;
NSMutableArray<CAShapeLayer*> *dots = [[NSMutableArray alloc] init];
for (int i = 0; i < _numberOfRows; i++) {
CAShapeLayer *dot = [CAShapeLayer layer];
CGRect dotRect = CGRectMake(0,
spacing * i,
_squareSize, _squareSize);
[dot setPath:[UIBezierPath bezierPathWithRoundedRect:dotRect
cornerRadius:_cornerRadius].CGPath];
if (@available(iOS 13.0, *)) {
dot.fillColor = [UIColor systemGray6Color].CGColor;
}
[[self layer] addSublayer:dot];
[dots addObject:dot];
}
_dots = [dots copy];
}
- (void)setOpacity:(CGFloat)opacity {
for (NSInteger i = 0 ; i < _dots.count; i++) {
CAShapeLayer *dot = _dots[i];
dot.opacity = opacity;
}
}
- (void)setColor:(UIColor *)color {
[_dots makeObjectsPerformSelector:@selector(setFillColor:) withObject:(id)[color CGColor]];
}
@end
@interface ORKEnvironmentSPLMeterBarView () {
NSArray<ORKEnvironmentSPLMeterColumnView *> *_columnViews;
int _currentIndex;
int _targetIndex;
int _maximumNumberOfDots;
int _greenIndexLimit;
BOOL _didLayoutViews;
BOOL _isAnimating;
NSTimer *_animationTimer;
}
@end
@implementation ORKEnvironmentSPLMeterBarView
- (void)didMoveToSuperview {
[super didMoveToSuperview];
_didLayoutViews = NO;
_isAnimating = NO;
}
- (void)setupView {
CGFloat width = CGRectGetWidth(self.frame);
CGFloat dotSpacing = (ORKEnvironmentSPLMeterSquareSize + ORKEnvironmentSPLMeterSquareDistance);
_maximumNumberOfDots = (int) (floor(width/dotSpacing)) + 1;
NSMutableArray<ORKEnvironmentSPLMeterColumnView*> *columnViews = [[NSMutableArray alloc] init];
_greenIndexLimit = _maximumNumberOfDots * 0.66;
_currentIndex = 0;
_targetIndex = _greenIndexLimit;
for (int i = 1 ; i <= _maximumNumberOfDots; i++) {
CGRect columnRect = CGRectMake((i - 1) * dotSpacing,
0, ORKEnvironmentSPLMeterSquareSize, ORKEnvironmentSPLMeterSquareSize);
ORKEnvironmentSPLMeterColumnView *columnView = [[ORKEnvironmentSPLMeterColumnView alloc] initWithFrame:columnRect];
if (i <= _greenIndexLimit - 1) {
[columnView setColor:[UIColor systemGreenColor]];
} else {
[columnView setColor:[UIColor systemOrangeColor]];
}
[self addSubview:columnView];
[columnViews addObject:columnView];
}
_columnViews = [columnViews copy];
[self updateViewForIndex:_currentIndex];
[self animateColumns];
}
- (void)setProgress:(CGFloat)progress {
CGFloat resultProgress = progress;
if (progress == 20.0) {
return;
}
if(progress < 0) {
resultProgress = 0.0;
}
float inMin = 0.0;
float inMax = 1.0;
float outMin = 0.0;
float outMax = 0.66;
float normalizedIndexValue = outMin + (outMax - outMin) * (resultProgress - inMin) / (inMax - inMin);
if (resultProgress > 1.0) {
inMin = 1.0;
inMax = 1.5;
outMin = 0.66;
outMax = 1.0;
normalizedIndexValue = outMin + (outMax - outMin) * (resultProgress - inMin) / (inMax - inMin);
}
int newTargetIndex = (int) (floor(normalizedIndexValue * _maximumNumberOfDots) + 1);
if (newTargetIndex != _targetIndex) {
[self stopAnimation];
_targetIndex = newTargetIndex;
_currentIndex = _targetIndex + (-1 + arc4random_uniform(3));
[self updateViewForIndex:newTargetIndex];
} else if (!_isAnimating) {
int indexDistance = abs(_currentIndex - newTargetIndex);
for (int i = 0; i < indexDistance; i++) {
int newIndex;
if (newTargetIndex < _currentIndex) {
newIndex = _currentIndex - i;
} else {
newIndex = _currentIndex + i;
}
[self updateViewForIndex:newIndex];
}
[self animateColumns];
}
}
- (void)animateColumns {
[_animationTimer invalidate];
_isAnimating = YES;
_animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(timerTicked) userInfo:nil repeats:YES];
}
- (void)timerTicked {
if (_currentIndex > _targetIndex) {
_currentIndex = _currentIndex - 1;
} else if (_currentIndex < _targetIndex) {
_currentIndex = _currentIndex + 1;
} else {
_currentIndex = _currentIndex + (-1 + arc4random_uniform(3));
}
[self updateViewForIndex:_currentIndex];
}
- (void)updateViewForIndex:(int)index {
for (int i = 0 ; i < _maximumNumberOfDots; i++) {
ORKEnvironmentSPLMeterColumnView *columnView = _columnViews[i];
NSInteger distanceToIndex = i - index;
CGFloat opacityFactor = 0.1 * distanceToIndex;
UIColor *grayColor;
UIColor *greenColor;
UIColor *orangeColor;
if (@available(iOS 13.0, *)) {
greenColor = [UIColor systemGreenColor];
orangeColor = [UIColor systemOrangeColor];
} else {
grayColor = [UIColor grayColor];
greenColor = [UIColor greenColor];
orangeColor = [UIColor orangeColor];
}
if (i <= _greenIndexLimit) {
if (i < index) {
[columnView setColor:greenColor];
[columnView setOpacity:1.0];
} else {
if (distanceToIndex < 3){
[columnView setColor:greenColor];
[columnView setOpacity:0.5 - opacityFactor];
} else {
[columnView setColor:grayColor];
[columnView setOpacity:1.0];
}
}
} else {
if (i < index) {
[columnView setColor:orangeColor];
[columnView setOpacity:1.0];
} else {
if (distanceToIndex < 3){
[columnView setColor:orangeColor];
[columnView setOpacity:0.5 - opacityFactor];
} else {
[columnView setColor:grayColor];
[columnView setOpacity:1.0];
}
}
}
}
}
- (void)stopAnimation {
_isAnimating = NO;
[_animationTimer invalidate];
_animationTimer = nil;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!_didLayoutViews) {
_didLayoutViews = YES;
[self setupView];
}
}
- (void)dealloc {
[_animationTimer invalidate];
_animationTimer = nil;
}
@end
@@ -28,29 +28,40 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import UIKit;
#import "ORKCustomStepView_Internal.h"
#import "ORKUnitLabel.h"
#import "ORKRingView.h"
NS_ASSUME_NONNULL_BEGIN
@class ORKEnvironmentSPLMeterBarView;
@class ORKRingView;
@class ORKRoundTappingButton;
@class ORKNavigationContainerView;
@class ORKEnvironmentSPLMeterContentView;
@protocol ORKEnvironmentSPLMeterContentViewVoiceOverDelegate <NSObject>
- (void)contentView:(ORKEnvironmentSPLMeterContentView * _Nonnull)contentView shouldAnnounce:(NSString * _Nonnull)inAnnouncement;
@end
@interface ORKEnvironmentSPLMeterContentView : ORKActiveStepCustomView
- (void)setProgress:(CGFloat)progress
animated:(BOOL)animated;
@property (nonatomic, strong) ORKNavigationContainerView *navigationFooterView;
@property (nonatomic, weak) id<ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> voiceOverDelegate;
- (ORKEnvironmentSPLMeterBarView *)barView;
- (ORKRingView *)ringView;
- (void)setProgress:(CGFloat)progress;
- (void)setProgressCircle:(CGFloat)progress;
- (void)setDBText:(NSString *)text;
- (void)setProgressBar:(CGFloat)progress;
- (void)setThreshold:(double)threshold;
@property(nonatomic, strong) ORKRingView *ringView;
- (void)reachedOptimumNoiseLevel;
@end
@@ -30,29 +30,34 @@
#import "ORKEnvironmentSPLMeterContentView.h"
#import "ORKEnvironmentSPLMeterBarView.h"
#import "ORKRoundTappingButton.h"
#import "ORKUnitLabel.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKRingView.h"
#import "ORKProgressView.h"
#import "ORKCompletionCheckmarkView.h"
static const CGFloat DBLabelFontSize = 35.0;
static const CGFloat RingViewPadding = 18.0;
static const CGFloat InstructionLabelPadding = 8.0;
static const CGFloat HalfCircleSize = 14.0;
static const CGFloat BarViewHeight = 50.0;
@interface ORKEnvironmentSPLMeterContentView ()
@property(nonatomic, strong) ORKRingView *ringView;
@property(nonatomic, strong) ORKEnvironmentSPLMeterBarView *barView;
@end
@implementation ORKEnvironmentSPLMeterContentView {
NSLayoutConstraint *_topToProgressViewConstraint;
UIStackView *stackView;
UIStackView *miniStackView;
UILabel *_dBValueLabel;
UILabel *_unitLabel;
UILabel *_thresholdLabel;
UIView *_containerView;
UILabel *_DBInstructionLabel;
UIImage *_checkmarkImage;
UIImage *_xmarkImage;
UIImageView *_xmarkView;
CGFloat preValue;
CGFloat currentValue;
CAShapeLayer *circle;
ORKProgressView *_loadingView;
UIProgressView *_progressView;
}
- (instancetype)init {
@@ -62,230 +67,166 @@ static const CGFloat DBLabelFontSize = 35.0;
currentValue = 0.0;
self.translatesAutoresizingMaskIntoConstraints = NO;
_ringView = [ORKRingView new];
_ringView.animationDuration = 0.8;
[self addSubview: _ringView];
[self setupThresholdLabel];
[self setupDBValueLabel];
[self setupUnitLabel];
[_ringView addSubview:_dBValueLabel];
[_ringView addSubview:_unitLabel];
[self addSubview:_thresholdLabel];
_loadingView = [ORKProgressView new];
_loadingView.translatesAutoresizingMaskIntoConstraints = NO;
[_ringView addSubview:_loadingView];
_progressView = [UIProgressView new];
_progressView.translatesAutoresizingMaskIntoConstraints = NO;
_progressView.progressTintColor = [self tintColor];
[_progressView setAlpha:0];
[self addSubview:_progressView];
[self setUpConstraints];
[self setupContainerView];
[self setupDBInstructionLabel];
[self setupRingView];
[self setupBarView];
[self setupXmarkView];
[self setProgressCircle:0.0];
}
return self;
}
- (void) setupDBValueLabel {
if (!_dBValueLabel) {
_dBValueLabel = [UILabel new];
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
_DBInstructionLabel.font = [self title3TextFont];
}
- (UIFont *)title3TextFont {
UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleTitle3];
UIFontDescriptor *fontDescriptor = [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
return [UIFont fontWithDescriptor:fontDescriptor size:[[descriptor objectForKey: UIFontDescriptorSizeAttribute] doubleValue]];
}
- (void)setupContainerView {
if (!_containerView) {
_containerView = [UIView new];
}
_dBValueLabel.translatesAutoresizingMaskIntoConstraints = NO;
_dBValueLabel.numberOfLines = 0;
_dBValueLabel.textColor = [[UIColor blackColor] colorWithAlphaComponent:0.7];
_dBValueLabel.lineBreakMode = NSLineBreakByWordWrapping;
_dBValueLabel.textAlignment = NSTextAlignmentCenter;
[_dBValueLabel setText:ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil)];
[_dBValueLabel setFont:[UIFont systemFontOfSize:DBLabelFontSize weight:UIFontWeightThin]];
}
- (void) setupUnitLabel {
if (!_unitLabel) {
_unitLabel = [UILabel new];
}
_unitLabel.translatesAutoresizingMaskIntoConstraints = NO;
_unitLabel.numberOfLines = 0;
_unitLabel.textColor = [[UIColor grayColor] colorWithAlphaComponent:1.0];
_unitLabel.lineBreakMode = NSLineBreakByWordWrapping;
_unitLabel.textAlignment = NSTextAlignmentCenter;
[_unitLabel setText:ORKLocalizedString(@"ENVIRONMENTSPL_UNIT", nil)];
[_unitLabel setHidden:YES];
[_unitLabel setFont:[UIFont systemFontOfSize:15 weight:UIFontWeightLight]];
}
- (void)setupThresholdLabel {
if (!_thresholdLabel) {
_thresholdLabel = [UILabel new];
}
_thresholdLabel.translatesAutoresizingMaskIntoConstraints = NO;
_thresholdLabel.numberOfLines = 0;
_thresholdLabel.textColor = [[UIColor grayColor] colorWithAlphaComponent:1.0];
_thresholdLabel.lineBreakMode = NSLineBreakByWordWrapping;
_thresholdLabel.textAlignment = NSTextAlignmentCenter;
[_thresholdLabel setFont:[UIFont systemFontOfSize:15 weight:UIFontWeightThin]];
}
- (void)tintColorDidChange {
[super tintColorDidChange];
_progressView.progressTintColor = [self tintColor];
}
- (void)setProgress:(CGFloat)progress
animated:(BOOL)animated {
_containerView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_containerView];
[_progressView setProgress:progress animated:animated];
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
[_progressView setAlpha:(progress == 0) ? 0 : 1];
}];
[[_containerView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:-RingViewPadding] setActive:YES];
[[_containerView.topAnchor constraintGreaterThanOrEqualToAnchor:self.topAnchor] setActive:YES];
[[_containerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
[[_containerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
[[_containerView.topAnchor constraintLessThanOrEqualToAnchor:self.bottomAnchor] setActive:YES];
}
- (void)setupXmarkView {
if (!_xmarkView) {
if (@available(iOS 13.0, *)) {
UIImageConfiguration *configuration = [UIImageSymbolConfiguration configurationWithPointSize:HalfCircleSize weight:UIImageSymbolWeightBold scale:UIImageSymbolScaleDefault];
_xmarkImage = [[UIImage systemImageNamed:@"xmark" withConfiguration:configuration] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
_checkmarkImage = [[UIImage systemImageNamed:@"checkmark" withConfiguration:configuration] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
_xmarkView = [[UIImageView alloc] initWithImage: _xmarkImage];
_xmarkView.tintColor = UIColor.systemOrangeColor;
}
_xmarkView.translatesAutoresizingMaskIntoConstraints = NO;
[_containerView addSubview:_xmarkView];
[[_xmarkView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
[[_xmarkView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
_xmarkView.hidden = YES;
}
- (void)setupBarView {
if (!_barView) {
_barView = [[ORKEnvironmentSPLMeterBarView alloc] initWithFrame:CGRectZero];
}
_barView.translatesAutoresizingMaskIntoConstraints = NO;
[_containerView addSubview:_barView];
[[_barView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor] setActive:YES];
[[_barView.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor] setActive:YES];
[[_barView.heightAnchor constraintEqualToConstant:BarViewHeight] setActive:YES];
[[_barView.topAnchor constraintEqualToAnchor:_DBInstructionLabel.bottomAnchor constant:RingViewPadding] setActive:YES];
[[_barView.bottomAnchor constraintEqualToAnchor:_containerView.bottomAnchor constant:RingViewPadding] setActive:YES];
}
- (void)setupRingView {
if (!_ringView) {
_ringView = [ORKRingView new];
}
_ringView.animationDuration = 0.0;
_ringView.translatesAutoresizingMaskIntoConstraints = NO;
[_containerView addSubview:_ringView];
[[_ringView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor] setActive:YES];
[[_ringView.centerYAnchor constraintEqualToAnchor:_DBInstructionLabel.centerYAnchor] setActive:YES];
[[_ringView.trailingAnchor constraintEqualToAnchor:_DBInstructionLabel.leadingAnchor constant:-InstructionLabelPadding] setActive:YES];
if (@available(iOS 13.0, *)) {
[_ringView setColor:UIColor.systemGray6Color];
}
}
- (void)setupDBInstructionLabel {
if (!_DBInstructionLabel) {
_DBInstructionLabel = [ORKLabel new];
_DBInstructionLabel.numberOfLines = 0;
_DBInstructionLabel.font = [self title3TextFont];
if (@available(iOS 13.0, *)) {
_DBInstructionLabel.textColor = UIColor.labelColor;
}
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil);
}
_DBInstructionLabel.translatesAutoresizingMaskIntoConstraints = NO;
[_containerView addSubview:_DBInstructionLabel];
[[_DBInstructionLabel.topAnchor constraintEqualToAnchor:_containerView.topAnchor constant:InstructionLabelPadding] setActive:YES];
[[_DBInstructionLabel.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor constant:-InstructionLabelPadding] setActive:YES];
}
- (void)setProgressBar:(CGFloat)progress {
[_barView setProgress:progress];
}
- (void)setProgressCircle:(CGFloat)progress {
[_ringView setValue:progress WithColor:progress < 1.0 ? [[UIColor greenColor] colorWithAlphaComponent:0.5] : [[UIColor redColor] colorWithAlphaComponent:0.5]];
if (progress >= ORKRingViewMaximumValue) {
} else {
[_ringView resetLayerColors];
}
[self updateInstructionForValue:progress];
}
- (void)setThreshold:(double)threshold {
if (_thresholdLabel) {
[_thresholdLabel setText:[NSString stringWithFormat:ORKLocalizedString(@"ENVIRONMENTSPL_THRESHOLD", nil), @(threshold)]];
}
- (ORKRingView *)ringView {
return _ringView;
}
- (void)setDBText:(NSString *)text {
if (_loadingView) {
[_loadingView setHidden:YES];
[_loadingView removeFromSuperview];
_loadingView = nil;
- (void)setProgress:(CGFloat)progress {
CGFloat value = progress < ORKRingViewMinimumValue ? ORKRingViewMinimumValue : progress;
[_ringView setValue:value];
}
}
if (_dBValueLabel) {
[_dBValueLabel setText:[NSString stringWithFormat:@"%@", text]];
[_unitLabel setHidden:NO];
}
- (void)updateInstructionForValue:(CGFloat)progress {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *currentInstruction = [_DBInstructionLabel.text copy];
BOOL isNoise = (progress >= ORKRingViewMaximumValue);
NSString *newInstruction = isNoise ? ORKLocalizedString(@"ENVIRONMENTSPL_NOISE", nil) : ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil);
_xmarkView.hidden = !isNoise;
if (![newInstruction isEqualToString:currentInstruction]) {
_DBInstructionLabel.text = newInstruction;
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)]) {
[self.voiceOverDelegate contentView:self shouldAnnounce:_DBInstructionLabel.text];
}
}
});
}
- (void)finishStep:(ORKActiveStepViewController *)viewController {
[super finishStep:viewController];
}
- (void)updateLayoutMargins {
CGFloat margin = ORKStandardHorizontalMarginForView(self);
self.layoutMargins = (UIEdgeInsets){.left = margin * 2, .right = margin * 2};
}
- (void)reachedOptimumNoiseLevel {
_xmarkView.hidden = NO;
_xmarkView.image = _checkmarkImage;
_xmarkView.tintColor = UIColor.systemGreenColor;
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self updateLayoutMargins];
}
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
[self updateLayoutMargins];
}
- (void)setUpConstraints {
NSArray *constraints = @[
[NSLayoutConstraint constraintWithItem:_ringView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterX
multiplier:1.0 constant:0.0],
[NSLayoutConstraint constraintWithItem:_ringView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:-80.0],
[NSLayoutConstraint constraintWithItem:_dBValueLabel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:_ringView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_dBValueLabel
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:_ringView
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_unitLabel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:_ringView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_unitLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_dBValueLabel
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:10.0],
[NSLayoutConstraint constraintWithItem:_thresholdLabel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:_ringView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_thresholdLabel
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:_ringView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:-20.0],
[NSLayoutConstraint constraintWithItem:_loadingView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:_dBValueLabel
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_loadingView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_dBValueLabel
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:5.0],
[NSLayoutConstraint constraintWithItem:_progressView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_ringView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:80.0],
[NSLayoutConstraint constraintWithItem:_progressView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:5.0],
[NSLayoutConstraint constraintWithItem:_progressView
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:-5.0],
];
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
[self addConstraints:constraints];
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)]) {
[self.voiceOverDelegate contentView:self shouldAnnounce:_DBInstructionLabel.text];
}
[NSLayoutConstraint activateConstraints:constraints];
[_ringView setBackgroundLayerStrokeColor:UIColor.systemGreenColor circleStrokeColor:UIColor.systemGreenColor withAnimationDuration:0.0];
[_barView stopAnimation];
}
@end
@@ -31,6 +31,7 @@
#import <ResearchKit/ResearchKit.h>
ORK_CLASS_AVAILABLE
@interface ORKEnvironmentSPLMeterResult : ORKResult
@property (nonatomic, assign) double sensitivityOffset;
@@ -31,7 +31,7 @@
#import "ORKEnvironmentSPLMeterStep.h"
#import "ORKEnvironmentSPLMeterStepViewController.h"
#import "ORKRecorder_Private.h"
#import "ORKHelpers_Internal.h"
#define ORKEnvironmentSPLMeterTaskDefaultThresholdValue 35.0
@@ -56,6 +56,12 @@
self.thresholdValue = ORKEnvironmentSPLMeterTaskDefaultThresholdValue;
self.samplingInterval = ORKEnvironmentSPLMeterTaskMinimumSamplingInterval;
self.requiredContiguousSamples = ORKEnvironmentSPLMeterTaskDefaultRequiredContiguousSamples;
self.stepDuration = CGFLOAT_MAX;
self.shouldShowDefaultTimer = NO;
// This is inserted here because it is required for any task that requires the SPL Meter step
ORKAudioStreamerConfiguration *config = [[ORKAudioStreamerConfiguration alloc] initWithIdentifier:[NSString stringWithFormat:@"%@_streamerConfiguration",self.identifier]];
self.recorderConfigurations = @[config];
}
- (void)validateParameters {
@@ -31,22 +31,32 @@
#import "ORKEnvironmentSPLMeterStepViewController.h"
#import "ORKActiveStepView.h"
#import "ORKStepView.h"
#import "ORKStepContainerView_Private.h"
#import "ORKRoundTappingButton.h"
#import "ORKEnvironmentSPLMeterContentView.h"
#import "ORKRingView.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
#import "ORKCollectionResult_Private.h"
#import "ORKEnvironmentSPLMeterResult.h"
#import "ORKEnvironmentSPLMeterStep.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKSkin.h"
#import "ORKHelpers_Internal.h"
#import <AVFoundation/AVFoundation.h>
#include <sys/sysctl.h>
@interface ORKEnvironmentSPLMeterStepViewController () {
static const NSTimeInterval SPL_METER_PLAY_DELAY_VOICEOVER = 3.0;
@interface ORKEnvironmentSPLMeterStepViewController ()<ORKRingViewDelegate, ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> {
AVAudioEngine *_audioEngine;
AVAudioInputNode *_inputNode;
AVAudioUnitEQ *_eqUnit;
@@ -64,6 +74,12 @@
NSInteger _requiredContiguousSamples;
int _counter;
NSMutableArray *_recordedSamples;
AVAudioSessionCategory _savedSessionCategory;
AVAudioSessionMode _savedSessionMode;
AVAudioSessionCategoryOptions _savedSessionCategoryOptions;
UINotificationFeedbackGenerator *_notificationFeedbackGenerator;
dispatch_semaphore_t _voiceOverAnnouncementSemaphore;
NSTimer *_timeoutTimer;
}
@property (nonatomic, strong) ORKEnvironmentSPLMeterContentView *environmentSPLMeterContentView;
@@ -85,39 +101,45 @@
_requiredContiguousSamples = 1;
_sensitivityOffset = -23.3;
_recordedSamples = [NSMutableArray new];
_audioEngine = [[AVAudioEngine alloc] init];
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
}
return self;
}
- (void)initializeInternalButtonItems {
[super initializeInternalButtonItems];
// Don't show next button
self.internalContinueButtonItem = nil;
self.internalDoneButtonItem = nil;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self saveAudioSession];
_sensitivityOffset = [self sensitivityOffsetForDevice];
_environmentSPLMeterContentView = [ORKEnvironmentSPLMeterContentView new];
[_environmentSPLMeterContentView setProgress:0.01 animated:YES];
[self setNavigationFooterView];
_environmentSPLMeterContentView.voiceOverDelegate = self;
_environmentSPLMeterContentView.ringView.delegate = self;
self.activeStepView.activeCustomView = _environmentSPLMeterContentView;
[self requestMicrophoneAuthorization];
[self requestRecordPermissionIfNeeded];
[self configureAudioSession];
_audioEngine = [[AVAudioEngine alloc] init];
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
_inputNode = [_audioEngine inputNode];
_inputNodeOutputFormat = [_inputNode inputFormatForBus:0];
_sampleRate = (uint32_t)_inputNodeOutputFormat.sampleRate;
_bufferSize = _sampleRate/10;
_countToFetch = _sampleRate/(int)_bufferSize;
[self configureEQ];
[_audioEngine attachNode:_eqUnit];
[_audioEngine connect:_inputNode to:_eqUnit format:_inputNodeOutputFormat];
[self setupFeedbackGenerator];
}
- (void)saveAudioSession {
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
_savedSessionCategory = audioSession.category;
_savedSessionMode = audioSession.mode;
_savedSessionCategoryOptions = audioSession.categoryOptions;
}
- (void)setNavigationFooterView {
self.activeStepView.navigationFooterView.continueButtonItem = self.continueButtonItem;
self.activeStepView.navigationFooterView.continueEnabled = NO;
[self.activeStepView.navigationFooterView updateContinueAndSkipEnabled];
}
- (void)setContinueButtonItem:(UIBarButtonItem *)continueButtonItem {
[super setContinueButtonItem:continueButtonItem];
_navigationFooterView.continueButtonItem = continueButtonItem;
}
- (void)viewDidAppear:(BOOL)animated {
@@ -128,17 +150,25 @@
_samplingInterval = [self environmentSPLMeterStep].samplingInterval;
_requiredContiguousSamples = [self environmentSPLMeterStep].requiredContiguousSamples;
_thresholdValue = [self environmentSPLMeterStep].thresholdValue;
[_environmentSPLMeterContentView setThreshold:_thresholdValue];
[self configureInputNode];
[self splWorkBlock];
if (UIAccessibilityIsVoiceOverRunning()) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SPL_METER_PLAY_DELAY_VOICEOVER * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil));
});
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self resetAudioSession];
[_eqUnit removeTapOnBus:0];
[_audioEngine stop];
[_rmsBuffer removeAllObjects];
[self resetAudioSession];
}
- (NSString *)deviceType {
@@ -173,17 +203,87 @@
return sResult;
}
- (void)requestMicrophoneAuthorization {
[[AVAudioSession sharedInstance] recordPermission];
- (void)requestRecordPermissionIfNeeded
{
[self handleRecordPermission:[[AVAudioSession sharedInstance] recordPermission]];
}
- (void)handleRecordPermission:(AVAudioSessionRecordPermission)recordPermission
{
switch (recordPermission)
{
case AVAudioSessionRecordPermissionGranted:
break;
case AVAudioSessionRecordPermissionDenied:
{
ORK_Log_Error("User has denied record permission for a step which requires microphone access.");
break;
}
case AVAudioSessionRecordPermissionUndetermined:
{
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
[self handleRecordPermission:granted ? AVAudioSessionRecordPermissionGranted : AVAudioSessionRecordPermissionDenied];
}];
break;
}
}
}
- (void)configureAudioSession {
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionMixWithOthers error:&error];
if ([AVAudioSession sharedInstance].isOtherAudioPlaying) {
NSError *activationError = nil;
[[AVAudioSession sharedInstance] setActive:YES error:&activationError];
AVAudioSession * session = [AVAudioSession sharedInstance];
// Stop any existing audio
[session setCategory:AVAudioSessionCategorySoloAmbient error:&error];
if (error) {
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
}
[session setActive:YES error:&error];
if (error) {
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
}
// Force input/output from iOS device
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
if (error) {
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
}
// When setting the input like this, we do not need to set the input AND the output to the iPhone.
NSArray<AVAudioSessionPortDescription *> * inputs = [session availableInputs];
for (AVAudioSessionPortDescription* desc in inputs) {
if ([desc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
// go ahead and set our preferred input to the built-in mic
[session setPreferredInput:desc error:&error];
if (error) {
ORK_Log_Error("Setting AVAudioSession preferred input failed with error message: \"%@\"", error.localizedDescription);
}
}
}
[session setActive:YES error:&error];
if (error) {
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
}
}
- (void)configureInputNode {
_inputNode = [_audioEngine inputNode];
_inputNodeOutputFormat = [_inputNode inputFormatForBus:0];
_sampleRate = (uint32_t)_inputNodeOutputFormat.sampleRate;
_bufferSize = _sampleRate/10;
_countToFetch = _sampleRate > 0 ? _sampleRate/(int)_bufferSize : 0;
[self configureEQ];
[_audioEngine attachNode:_eqUnit];
[_audioEngine connect:_inputNode to:_eqUnit format:_inputNodeOutputFormat];
}
- (void)configureEQ {
@@ -229,9 +329,14 @@
eqCoefficient.bypass = NO;
}
- (void)splWorkBlock {
if (!_audioEngine.isRunning && ![[AVAudioSession sharedInstance] isOtherAudioPlaying]) {
// secondaryAudioShouldBeSilencedHint returns true if VoiceOver is running.
// Since we are killing all audio when configuring the session, here we can make a safe assumption that if VoiceOver is running, allow the user to continue even if the secondaryAudioShouldBeSilencedHint is YES.
// If VoiceOver is not running, we can still gate based on the secondaryAudioShouldBeSilencedHint.
BOOL otherAudioIsProhibitingMeasurement = [[AVAudioSession sharedInstance] secondaryAudioShouldBeSilencedHint] && !UIAccessibilityIsVoiceOverRunning();
if (!_audioEngine.isRunning && !otherAudioIsProhibitingMeasurement) {
[_eqUnit installTapOnBus:0
bufferSize:_bufferSize
format:_inputNodeOutputFormat
@@ -265,24 +370,33 @@
[_recordedSamples addObject:[NSNumber numberWithFloat:_spl]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.environmentSPLMeterContentView setProgressCircle:(_spl/_thresholdValue)];
[self.environmentSPLMeterContentView setDBText:[NSString stringWithFormat:@"%.f", _spl]];
});
[self evaluateThreshold:_spl];
[_rmsBuffer removeAllObjects];
} else {
if (rms > 0.0 && _sampleRate > 0.0) {
float spl = (20 * log10f(sqrtf(rms/(float)_sampleRate))) - _sensitivityOffset + 96;
dispatch_async(dispatch_get_main_queue(), ^{
[self.environmentSPLMeterContentView setProgressBar:(spl/_thresholdValue)];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self.environmentSPLMeterContentView setProgressBar:(_spl/_thresholdValue)];
});
}
}
dispatch_semaphore_signal(_semaphoreRms);
});
dispatch_semaphore_wait(_semaphoreRms, DISPATCH_TIME_FOREVER);
} else if ([AVAudioSession sharedInstance].recordPermission == AVAudioSessionRecordPermissionDenied) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.environmentSPLMeterContentView setDBText:[NSString stringWithFormat:@"N/A"]];
[_eqUnit removeTapOnBus:0];
[_audioEngine stop];
[_rmsBuffer removeAllObjects];
});
}
}];
if (!_audioEngine.isRunning && ![[AVAudioSession sharedInstance] isOtherAudioPlaying]) {
if (!_audioEngine.isRunning && !otherAudioIsProhibitingMeasurement) {
NSError *error = nil;
[_audioEngine startAndReturnError:&error];
} else {
@@ -293,42 +407,52 @@
}
}
- (void) evaluateThreshold: (float)spl {
if (spl < _thresholdValue) {
- (void)evaluateThreshold:(float)spl
{
if (spl < _thresholdValue)
{
_counter += 1;
if (_counter >= _requiredContiguousSamples) {
dispatch_async(dispatch_get_main_queue(), ^{
[self finish];
});
[self.environmentSPLMeterContentView.ringView fillRingWithDuration:(double)_requiredContiguousSamples*_samplingInterval];
if (_counter >= _requiredContiguousSamples)
{
[self reachedOptimumNoiseLevel];
[self sendHapticEvent:UINotificationFeedbackTypeSuccess];
}
} else {
_counter = 0;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.environmentSPLMeterContentView setProgress:((float)_counter/_requiredContiguousSamples) + 0.01 animated:YES];
});
else
{
_counter = 0;
self.environmentSPLMeterContentView.ringView.animationDuration = 0.5;
[self.environmentSPLMeterContentView setProgress:0.0];
[self sendHapticEvent:UINotificationFeedbackTypeError];
}
}
- (void) resetAudioSession {
- (void)resetAudioSession {
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionMixWithOthers error:&error];
[[AVAudioSession sharedInstance] setCategory:_savedSessionCategory mode:_savedSessionMode options:_savedSessionCategoryOptions error:&error];
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error];
if (error) {
ORK_Log_Error(@"Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
}
if ([AVAudioSession sharedInstance].isOtherAudioPlaying) {
NSError *activationError = nil;
[[AVAudioSession sharedInstance] setActive:YES error:&activationError];
if (activationError) {
ORK_Log_Error(@"Activating AVAudioSession failed with error message: \"%@\"", activationError.localizedDescription);
}
[[AVAudioSession sharedInstance] setActive:YES error:&error];
if (error) {
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
}
}
- (void)reachedOptimumNoiseLevel {
[_audioEngine stop];
[self resetAudioSession];
}
- (void)stepDidFinish {
[super stepDidFinish];
[self.environmentSPLMeterContentView finishStep:self];
[self resetAudioSession];
[self goForward];
}
@@ -340,5 +464,35 @@
return (ORKEnvironmentSPLMeterStep *)self.step;
}
#pragma mark - ORKRingViewDelegate
- (void)ringViewDidFinishFillAnimation {
[self.environmentSPLMeterContentView reachedOptimumNoiseLevel];
self.activeStepView.navigationFooterView.continueEnabled = YES;
}
#pragma mark - UINotificationFeedbackGenerator
- (void)setupFeedbackGenerator
{
_notificationFeedbackGenerator = [[UINotificationFeedbackGenerator alloc] init];
[_notificationFeedbackGenerator prepare];
}
- (void)sendHapticEvent:(UINotificationFeedbackType)eventType
{
dispatch_async(dispatch_get_main_queue(), ^{
[_notificationFeedbackGenerator notificationOccurred:eventType];
[_notificationFeedbackGenerator prepare];
});
}
#pragma mark - ORKEnvironmentSPLMeterContentViewVoiceOverDelegate
- (void)contentView:(ORKEnvironmentSPLMeterContentView *)contentView shouldAnnounce:(NSString *)inAnnouncement
{
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, inAnnouncement);
}
@end
+3
View File
@@ -75,6 +75,9 @@ ORK_CLASS_AVAILABLE
*/
@property (nonatomic, copy, nullable) NSURL *fileURL;
@property (nonatomic, copy, nullable) NSString *fileName;
@end
NS_ASSUME_NONNULL_END
+5 -1
View File
@@ -45,6 +45,7 @@
[super encodeWithCoder:aCoder];
ORK_ENCODE_URL(aCoder, fileURL);
ORK_ENCODE_OBJ(aCoder, contentType);
ORK_ENCODE_OBJ(aCoder, fileName);
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
@@ -52,6 +53,7 @@
if (self) {
ORK_DECODE_URL(aDecoder, fileURL);
ORK_DECODE_OBJ_CLASS(aDecoder, contentType, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, fileName, NSString);
}
return self;
}
@@ -66,7 +68,8 @@
__typeof(self) castObject = object;
return (isParentSame &&
ORKEqualFileURLs(self.fileURL, castObject.fileURL) &&
ORKEqualObjects(self.contentType, castObject.contentType));
ORKEqualObjects(self.contentType, castObject.contentType) &&
ORKEqualObjects(self.fileName, castObject.fileName));
}
- (NSUInteger)hash {
@@ -77,6 +80,7 @@
ORKFileResult *result = [super copyWithZone:zone];
result.fileURL = [self.fileURL copy];
result.contentType = [self.contentType copy];
result.fileName = [self.fileName copy];
return result;
}
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
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:
@@ -34,19 +34,38 @@
NS_ASSUME_NONNULL_BEGIN
// Displays a countdown ring and a timer.
//
// ------------------------------
// | |
// | Title Label |
// | |
// | subtitle label |
// | |
// | __________ |
// | / \ |
// | | 2:30 | |
// | \ ________ / |
// | |
// |______________________________|
@interface ORKFitnessContentView : ORKActiveStepCustomView
@property (nonatomic, assign, getter=isFinished) BOOL finished;
@property (nonatomic) BOOL hasHeartRate;
@property (nonatomic) BOOL hasDistance;
/// The total amount of time the active task is supposed to be performed for.
/// For the six minute walk test, this will typically be 360 seconds.
@property (nonatomic) NSTimeInterval duration;
@property (nonatomic, copy, nullable) NSString *heartRate;
@property (nonatomic) double distanceInMeters;
/// The amount of time that still remain.
@property (nonatomic) NSTimeInterval timeLeft;
@property (nonatomic, strong, nullable) UIImage *image;
/// Whether or not the text label is hidden.
@property (nonatomic) BOOL labelHidden;
@property (nonatomic, assign) NSTimeInterval timeLeft;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithDuration:(NSTimeInterval)duration;
@end
+86 -275
View File
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
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:
@@ -31,28 +31,8 @@
#import "ORKFitnessContentView.h"
#import "ORKActiveStepQuantityView.h"
#import "ORKTintedImageView.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
@import CoreMotion;
@import HealthKit;
// #define LAYOUT_TEST 1
// #define LAYOUT_DEBUG 1
@interface ORKFitnessContentView () {
ORKQuantityLabel *_timerLabel;
ORKQuantityPairView *_quantityPairView;
UIView *_imageSpacer1;
UIView *_imageSpacer2;
ORKTintedImageView *_imageView;
NSLengthFormatter *_lengthFormatter;
NSLayoutConstraint *_imageRatioConstraint;
NSLayoutConstraint *_topConstraint;
UILabel *_timerLabel;
}
@end
@@ -60,272 +40,72 @@
@implementation ORKFitnessContentView
- (ORKActiveStepQuantityView *)distanceView {
return _quantityPairView.leftView;
}
- (ORKActiveStepQuantityView *)heartRateView {
return _quantityPairView.rightView;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
- (instancetype)initWithDuration:(NSTimeInterval)duration {
self = [super init];
if (self) {
_timerLabel = [ORKQuantityLabel new];
_quantityPairView = [ORKQuantityPairView new];
_imageSpacer1 = [UIView new];
_imageSpacer1.translatesAutoresizingMaskIntoConstraints = NO;
_imageSpacer2 = [UIView new];
_imageSpacer2.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_imageSpacer1];
[self addSubview:_imageSpacer2];
[self heartRateView].image = [UIImage imageNamed:@"heart-fitness" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
[self updateLengthFormatter];
_imageView = [ORKTintedImageView new];
_imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView.shouldApplyTint = YES;
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
_quantityPairView.translatesAutoresizingMaskIntoConstraints = NO;
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
[self updateKeylineVisible];
_timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently;
_imageView.isAccessibilityElement = NO;
self.hasHeartRate = _hasHeartRate;
self.hasDistance = _hasDistance;
#if LAYOUT_TEST
self.timeLeft = 60 * 5;
self.hasHeartRate = YES;
self.hasDistance = YES;
self.distanceInMeters = 100;
self.heartRate = @"22";
#endif
#if LAYOUT_DEBUG
self.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
_quantityPairView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.2];
#endif
[self setDistanceInMeters:0];
[self heartRateView].title = ORKLocalizedString(@"FITNESS_HEARTRATE_TITLE", nil);
_duration = duration;
_timeLeft = duration;
_timerLabel = [[UILabel alloc] init];
_timerLabel.textAlignment = NSTextAlignmentCenter;
_timerLabel.font = [self labelFont];
_timerLabel.adjustsFontForContentSizeCategory = YES;
_timerLabel.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
[self addSubview:_quantityPairView];
[self addSubview:_imageView];
[self addSubview:_timerLabel];
[self setUpConstraints];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
[self tintColorDidChange];
[self updateTimerLabel];
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
- (void)tintColorDidChange {
[self setNeedsDisplay];
_timerLabel.textColor = self.tintColor;
}
- (void)updateLengthFormatter {
_lengthFormatter = [NSLengthFormatter new];
_lengthFormatter.numberFormatter.maximumFractionDigits = 1;
_lengthFormatter.numberFormatter.maximumSignificantDigits = 3;
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
_timerLabel.font = [self labelFont];
}
- (void)localeDidChange:(NSNotification *)notification {
[self updateLengthFormatter];
[self setDistanceInMeters:_distanceInMeters];
}
- (void)willMoveToWindow:(UIWindow *)newWindow {
[super willMoveToWindow:newWindow];
[self updateConstraintConstantsForWindow:newWindow];
}
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
const CGFloat CaptionBaselineToTimerTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToFitnessTimerTop, window);
const CGFloat CaptionBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
_topConstraint.constant = (CaptionBaselineToTimerTop - CaptionBaselineToStepViewTop);
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
NSDictionary *views = NSDictionaryOfVariableBindings(_timerLabel, _imageView, _quantityPairView, _imageSpacer1, _imageSpacer2);
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_timerLabel][_imageSpacer1(>=0)][_imageView]"
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:views]];
_topConstraint = [NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0];
[constraints addObject:_topConstraint];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_imageView][_imageSpacer2(>=0)][_quantityPairView]|"
options:(NSLayoutFormatOptions)0
metrics:nil
views:views]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer2
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_imageSpacer2
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0.0]];
NSLayoutConstraint *imageSpacerHeightConstraint = [NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:ORKScreenMetricMaxDimension];
imageSpacerHeightConstraint.priority = UILayoutPriorityDefaultLow - 1;
[constraints addObject:imageSpacerHeightConstraint];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_quantityPairView]|"
options:(NSLayoutFormatOptions)0
metrics:nil
views:views]];
NSLayoutConstraint *maxWidthConstraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:ORKScreenMetricMaxDimension];
maxWidthConstraint.priority = UILayoutPriorityRequired - 1;
[constraints addObject:maxWidthConstraint];
[NSLayoutConstraint activateConstraints:constraints];
[self updateConstraintConstantsForWindow:self.window];
}
- (void)setImage:(UIImage *)image {
_image = image;
_imageView.image = image;
_imageRatioConstraint.active = NO;
CGSize size = image.size;
if (size.width > 0 && size.height > 0) {
_imageRatioConstraint = [NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_imageView
attribute:NSLayoutAttributeWidth
multiplier:size.height / size.width
constant:0.0];
_imageRatioConstraint.active = YES;
}
}
- (void)setHasDistance:(BOOL)hasDistance {
_hasDistance = hasDistance;
[self distanceView].enabled = _hasDistance;
[self updateKeylineVisible];
}
- (void)setHasHeartRate:(BOOL)hasHeartRate {
_hasHeartRate = hasHeartRate;
[self heartRateView].enabled = _hasHeartRate;
[self updateKeylineVisible];
}
- (void)setHeartRate:(NSString *)heartRate {
_heartRate = heartRate;
[self heartRateView].value = heartRate;
}
- (void)updateKeylineVisible {
[_quantityPairView setKeylineHidden:!(_hasDistance && _hasHeartRate)];
}
- (void)setDistanceInMeters:(double)distanceInMeters {
_distanceInMeters = distanceInMeters;
double displayDistance = _distanceInMeters;
NSString *distanceString = nil;
NSLengthFormatterUnit unit;
NSString *unitString = [_lengthFormatter unitStringFromMeters:displayDistance usedUnit:&unit];
switch (unit) {
case NSLengthFormatterUnitCentimeter:
case NSLengthFormatterUnitMillimeter:
unit = NSLengthFormatterUnitMeter;
// Force showing 0 meters if the distance is sufficiently short to be displayed in cm or mm
unitString = [_lengthFormatter unitStringFromValue:0 unit:NSLengthFormatterUnitMeter];
displayDistance = 0;
break;
default:
break;
}
// Use HealthKit to convert the unit, so we can use the number formatter directly.
HKUnit *hkUnit = [HKUnit unitFromLengthFormatterUnit:unit];
double conversionFactor = 1.0;
if ([hkUnit isNull] && (unit == NSLengthFormatterUnitYard)) {
hkUnit = [HKUnit footUnit];
conversionFactor = 1.0 / 3.0;
}
HKQuantity *quantity = [HKQuantity quantityWithUnit:[HKUnit meterUnit] doubleValue:displayDistance];
distanceString = [_lengthFormatter.numberFormatter stringFromNumber:@([quantity doubleValueForUnit:hkUnit]*conversionFactor)];
[self distanceView].title = [NSString localizedStringWithFormat:ORKLocalizedString(@"FITNESS_DISTANCE_TITLE_FORMAT", nil), unitString];
[self distanceView].value = distanceString;
- (void)setDuration:(NSTimeInterval)duration {
_duration = duration;
[self setNeedsDisplay];
}
- (void)setTimeLeft:(NSTimeInterval)timeLeft {
_timeLeft = timeLeft;
[self updateTimerLabel];
[self setNeedsDisplay];
}
- (BOOL)labelHidden {
return _timerLabel.isHidden;
}
- (void)setLabelHidden:(BOOL)labelHidden {
[_timerLabel setHidden:labelHidden];
}
- (UIFont*) labelFont {
UIFont* font = [UIFont preferredFontForTextStyle: UIFontTextStyleLargeTitle];
UIFontMetrics* metrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleLargeTitle];
if (@available(iOS 13, *)) {
UIFontDescriptor* round = [[font fontDescriptor] fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
UIFontDescriptor* weighted = [round fontDescriptorByAddingAttributes:@{
UIFontDescriptorTraitsAttribute: @{
UIFontWeightTrait: @1.5
}
}];
font = [UIFont fontWithDescriptor:weighted size:44];
}
UIFont* scaled = [metrics scaledFontForFont:font];
return scaled;
}
- (void)updateTimerLabel {
@@ -334,13 +114,44 @@
dispatch_once(&onceToken, ^{
formatter = [NSDateComponentsFormatter new];
formatter.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorDropLeading;
formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
});
NSString *labelString = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
_timerLabel.text = labelString;
_timerLabel.hidden = (labelString == nil);
_timerLabel.text = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
}
- (void)drawRect:(CGRect)rect {
// The ring should be be centered and fill 1/2 of the view's width
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat strokeWidth = 12;
CGFloat xCenter = self.bounds.size.width / 2;
CGFloat yCenter = self.bounds.size.height / 2;
CGFloat dimension = MIN(self.bounds.size.width, self.bounds.size.height);
CGFloat radius = 0.5 * (dimension * 0.5);
CGFloat percentFilled = _timeLeft / _duration;
CGFloat startAngle = -M_PI_2 - (percentFilled * 2 * M_PI);
CGFloat stopAngle = -M_PI_2;
bool clockwise = NO;
CGContextSetLineWidth(context, strokeWidth);
CGContextSetLineCap(context, kCGLineCapRound);
// Draw a circular track
if (@available(iOS 13.0, *)) {
[[UIColor systemGray5Color] setStroke];
} else {
[[UIColor lightGrayColor] setStroke];
}
CGContextAddArc(context, xCenter, yCenter, radius, 0, 2 * M_PI, clockwise ? 1 : 0);
CGContextStrokePath(context);
// Fill in the track based on progress
[self.tintColor setStroke];
CGContextAddArc(context, xCenter, yCenter, radius, startAngle, stopAngle, clockwise ? 1 : 0);
CGContextStrokePath(context);
}
@end
+2 -8
View File
@@ -36,17 +36,11 @@
NS_ASSUME_NONNULL_BEGIN
/**
Fitness step.
Displays usual header, a counting-up timer, read outs for distance and/or
heart rate if corresponding recorders are attached.
Also displays an image during the task.
*/
ORK_CLASS_AVAILABLE
@interface ORKFitnessStep : ORKActiveStep
@property (nonatomic, copy) NSDictionary *userInfo;
@end
NS_ASSUME_NONNULL_END
+42 -5
View File
@@ -30,6 +30,7 @@
#import "ORKFitnessStep.h"
#import "ORKHelpers_Internal.h"
#import "ORKFitnessStepViewController.h"
@@ -43,6 +44,7 @@
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super initWithIdentifier:identifier];
if (self) {
self.userInfo = [[NSDictionary alloc] init];
self.shouldShowDefaultTimer = NO;
}
return self;
@@ -58,13 +60,48 @@
}
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKFitnessStep *step = [super copyWithZone:zone];
return step;
}
- (BOOL)startsFinished {
return NO;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
ORK_DECODE_OBJ_CLASS(coder, userInfo, NSDictionary);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
ORK_ENCODE_OBJ(coder, userInfo);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKFitnessStep *step = [super copyWithZone:zone];
step.userInfo = [self.userInfo copy];
return step;
}
- (BOOL)isEqual:(id)other
{
if ([self class] != [other class]) {
return NO;
}
__typeof(self) castObject = other;
return ORKEqualObjects(self.userInfo, castObject.userInfo);
}
- (NSUInteger)hash
{
return [super hash] ^ self.userInfo.hash;
}
@end
@@ -30,27 +30,20 @@
#import "ORKFitnessStepViewController.h"
#import "ORKActiveStepTimer.h"
#import "ORKActiveStepView.h"
#import "ORKFitnessContentView.h"
#import "ORKVerticalContainerView.h"
#import "ORKFitnessStep.h"
#import "ORKActiveStepView.h"
#import "ORKActiveStepTimer.h"
#import "ORKStepViewController_Internal.h"
#import "ORKHealthQuantityTypeRecorder.h"
#import "ORKPedometerRecorder.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKFitnessStep.h"
#import "ORKStep_Private.h"
#import "ORKHelpers_Internal.h"
#import "ORKStepContainerView_Private.h"
@interface ORKFitnessStepViewController () <ORKHealthQuantityTypeRecorderDelegate, ORKPedometerRecorderDelegate> {
NSInteger _intendedSteps;
@interface ORKFitnessStepViewController () {
ORKFitnessContentView *_contentView;
NSNumberFormatter *_hrFormatter;
}
@end
@@ -70,84 +63,61 @@
return (ORKFitnessStep *)self.step;
}
- (void)stepDidChange {
[super stepDidChange];
_hrFormatter = [[NSNumberFormatter alloc] init];
_hrFormatter.numberStyle = kCFNumberFormatterNoStyle;
_contentView.timeLeft = self.fitnessStep.stepDuration;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_contentView = [ORKFitnessContentView new];
_contentView.image = self.fitnessStep.image;
_contentView.timeLeft = self.fitnessStep.stepDuration;
_contentView = [[ORKFitnessContentView alloc] initWithDuration:self.fitnessStep.stepDuration];
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
self.activeStepView.activeCustomView = _contentView;
self.activeStepView.stepViewFillsAvailableSpace = YES;
self.activeStepView.customContentFillsAvailableSpace = YES;
self.continueButtonTitle = ORKLocalizedString(@"BUTTON_SKIP_STEP", nil);
}
- (void)updateHeartRateWithQuantity:(HKQuantitySample *)quantity unit:(HKUnit *)unit {
if (quantity != nil) {
_contentView.hasHeartRate = YES;
}
if (quantity) {
_contentView.heartRate = [_hrFormatter stringFromNumber:@([quantity.quantity doubleValueForUnit:unit])];
} else {
_contentView.heartRate = @"--";
}
- (void)finish {
[super finish];
_contentView.labelHidden = YES;
self.continueButtonTitle = ORKLocalizedString(@"BUTTON_NEXT", nil);
}
- (void)updateDistance:(double)distanceInMeters {
_contentView.hasDistance = YES;
_contentView.distanceInMeters = distanceInMeters;
}
- (void)recordersDidChange {
[super recordersDidChange];
ORKPedometerRecorder *pedometerRecorder = nil;
ORKHealthQuantityTypeRecorder *heartRateRecorder = nil;
for (ORKRecorder *recorder in self.recorders) {
if ([recorder isKindOfClass:[ORKPedometerRecorder class]]) {
pedometerRecorder = (ORKPedometerRecorder *)recorder;
} else if ([recorder isKindOfClass:[ORKHealthQuantityTypeRecorder class]]) {
ORKHealthQuantityTypeRecorder *rec1 = (ORKHealthQuantityTypeRecorder *)recorder;
if ([[[rec1 quantityType] identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
heartRateRecorder = (ORKHealthQuantityTypeRecorder *)recorder;
}
}
}
if (heartRateRecorder == nil) {
_contentView.hasHeartRate = NO;
}
_contentView.heartRate = @"--";
_contentView.hasDistance = (pedometerRecorder != nil);
_contentView.distanceInMeters = 0;
- (void)stepDidChange {
[super stepDidChange];
_contentView.duration = self.fitnessStep.stepDuration;
_contentView.timeLeft = self.fitnessStep.stepDuration;
}
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
_contentView.timeLeft = finished ? 0 : (timer.duration - timer.runtime);
_contentView.duration = self.fitnessStep.stepDuration;
[super countDownTimerFired:timer finished:finished];
}
#pragma mark - ORKHealthQuantityTypeRecorderDelegate
- (void)goForward {
- (void)healthQuantityTypeRecorderDidUpdate:(ORKHealthQuantityTypeRecorder *)healthQuantityTypeRecorder {
if ([[healthQuantityTypeRecorder.quantityType identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
[self updateHeartRateWithQuantity:healthQuantityTypeRecorder.lastSample unit:healthQuantityTypeRecorder.unit];
if (self.finished) {
[super goForward];
return;
}
}
#pragma mark - ORKPedometerRecorderDelegate
UIAlertController *alert = [UIAlertController alertControllerWithTitle:ORKLocalizedString(@"FITNESS_STOP_TEST_CONFIRMATION", nil)
message:ORKLocalizedString(@"FITNESS_STOP_TEST_DETAIL", nil)
preferredStyle:UIAlertControllerStyleAlert];
- (void)pedometerRecorderDidUpdate:(ORKPedometerRecorder *)pedometerRecorder {
double distanceInMeters = pedometerRecorder.totalDistance;
[self updateDistance:distanceInMeters];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"FITNESS_RESUME_TEST", nil)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * _Nonnull action) {
[alert dismissViewControllerAnimated:YES completion:nil];
}]];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_SKIP_STEP", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[alert dismissViewControllerAnimated:YES completion:^{
[super goForward];
}];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
@end
@@ -0,0 +1,45 @@
/*
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 <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKActiveStep.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKFrontFacingCameraStep : ORKActiveStep
@property (nonatomic, assign) NSTimeInterval maximumRecordingLimit;
@property (nonatomic, assign) BOOL allowsReview;
@property (nonatomic, assign) BOOL allowsRetry;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,129 @@
/*
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 "ORKFrontFacingCameraStep.h"
#import "ORKFrontFacingCameraStepViewController.h"
#import "ORKHelpers_Internal.h"
static const NSTimeInterval MIN_RECORDING_DURATION = 10.0;
static const NSTimeInterval MAX_RECORDING_DURATION = 300.0;
@implementation ORKFrontFacingCameraStep
+ (Class)stepViewControllerClass
{
return [ORKFrontFacingCameraStepViewController class];
}
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (instancetype)initWithIdentifier:(NSString *)identifier
{
self = [super initWithIdentifier:identifier];
if (self)
{
_maximumRecordingLimit = 60.0;
_allowsRetry = NO;
_allowsReview = NO;
}
return self;
}
- (void)validateParameters
{
[super validateParameters];
if (self.maximumRecordingLimit < MIN_RECORDING_DURATION ||
self.maximumRecordingLimit > MAX_RECORDING_DURATION)
{
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:[NSString stringWithFormat:@"maxRecordingDuration must be greater than %f seconds and less than %f seconds.",
MIN_RECORDING_DURATION,
MAX_RECORDING_DURATION]
userInfo:nil];
}
}
- (BOOL)startsFinished
{
return NO;
}
- (BOOL)allowsBackNavigation
{
return NO;
}
- (instancetype)copyWithZone:(NSZone *)zone
{
ORKFrontFacingCameraStep *step = [super copyWithZone:zone];
step.maximumRecordingLimit = self.maximumRecordingLimit;
step.allowsRetry = self.allowsRetry;
step.allowsReview = self.allowsReview;
return step;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self )
{
ORK_DECODE_DOUBLE(aDecoder, maximumRecordingLimit);
ORK_DECODE_BOOL(aDecoder, allowsRetry);
ORK_DECODE_BOOL(aDecoder, allowsReview);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[super encodeWithCoder:aCoder];
ORK_ENCODE_DOUBLE(aCoder, maximumRecordingLimit);
ORK_ENCODE_BOOL(aCoder, allowsRetry);
ORK_ENCODE_BOOL(aCoder, allowsReview);
}
- (BOOL)isEqual:(id)object
{
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
(self.maximumRecordingLimit == castObject.maximumRecordingLimit) &&
(self.allowsRetry == castObject.allowsRetry) &&
(self.allowsReview == castObject.allowsReview));
}
@end
@@ -0,0 +1,64 @@
/*
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;
NS_ASSUME_NONNULL_BEGIN
@class AVCaptureSession;
typedef NS_ENUM(NSUInteger, ORKFrontFacingCameraStepContentViewEvent) {
ORKFrontFacingCameraStepContentViewEventStartRecording = 0,
ORKFrontFacingCameraStepContentViewEventStopRecording,
ORKFrontFacingCameraStepContentViewEventReviewRecording,
ORKFrontFacingCameraStepContentViewEventRetryRecording,
ORKFrontFacingCameraStepContentViewEventSubmitRecording,
ORKFrontFacingCameraStepContentViewEventError
};
typedef void (^ORKFrontFacingCameraStepContentViewEventHandler)(ORKFrontFacingCameraStepContentViewEvent);
@interface ORKFrontFacingCameraStepContentView : UIView
- (instancetype)initWithTitle:(nullable NSString *)title text:(nullable NSString *)text;
- (void)setViewEventHandler:(ORKFrontFacingCameraStepContentViewEventHandler)handler;
- (void)setPreviewLayerWithSession:(AVCaptureSession *)session;
- (void)startTimerWithMaximumRecordingLimit:(NSTimeInterval)maximumRecordingLimit;
- (void)presentReviewOptionsAllowingReview:(BOOL)allowReview allowRetry:(BOOL)allowRetry;
- (void)handleError:(NSError *)error;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,600 @@
/*
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 "ORKFrontFacingCameraStepContentView.h"
#import "ORKUnitLabel.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKBorderedButton.h"
#import "ORKTitleLabel.h"
#import "ORKBodyLabel.h"
#import "ORKIconButton.h"
#import "ORKStepHeaderView_Internal.h"
#import <AVFoundation/AVFoundation.h>
@interface ORKFrontFacingCameraStepOptionsView : UIVisualEffectView
@property (nonatomic, strong) ORKIconButton *reviewVideoButton;
@property (nonatomic, strong) ORKIconButton *deleteAndRetryVideoButton;
@property (nonatomic, strong) UIButton *submitVideoButton;
@end
@implementation ORKFrontFacingCameraStepOptionsView {
NSMutableArray *_constraints;
ORKTitleLabel *_titleLabel;
}
- (instancetype)initWithEffect:(UIVisualEffect *)effect {
self = [super initWithEffect:effect];
if (self) {
[self setupSubviews];
[self setUpConstraints];
}
return self;
}
- (void)drawRect:(CGRect)rect {
self.layer.cornerRadius = 10.0;
self.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
self.clipsToBounds = YES;
}
- (void)setupSubviews {
_titleLabel = [ORKTitleLabel new];
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
_titleLabel.textAlignment = NSTextAlignmentLeft;
_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
[_titleLabel setTextColor:[UIColor whiteColor]];
_titleLabel.text = ORKLocalizedString(@"FRONT_FACING_CAMERA_REVIEW_OPTIONS_TITLE", nil);
[self.contentView addSubview:_titleLabel];
UIImage *reviewButtonIcon = nil;
if (@available(iOS 13.0, *)) {
reviewButtonIcon = [UIImage systemImageNamed:@"video.fill"];
}
_reviewVideoButton = [[ORKIconButton alloc] initWithButtonText:ORKLocalizedString(@"FRONT_FACING_CAMERA_REVIEW_VIDEO", nil) buttonIcon: reviewButtonIcon];
_reviewVideoButton.tag = 0;
_reviewVideoButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:_reviewVideoButton];
UIImage *deleteAndRetryButtonIcon = nil;
if (@available(iOS 13.0, *)) {
deleteAndRetryButtonIcon = [UIImage systemImageNamed:@"trash.fill"];
}
_deleteAndRetryVideoButton = [[ORKIconButton alloc] initWithButtonText:ORKLocalizedString(@"FRONT_FACING_CAMERA_RETRY_VIDEO", nil) buttonIcon: deleteAndRetryButtonIcon];
_deleteAndRetryVideoButton.tag = 1;
_deleteAndRetryVideoButton.translatesAutoresizingMaskIntoConstraints = NO;
[_deleteAndRetryVideoButton updateTextAndImageColor:[UIColor redColor]];
[self.contentView addSubview:_deleteAndRetryVideoButton];
_submitVideoButton = [UIButton new];
_submitVideoButton.tag = 2;
_submitVideoButton.translatesAutoresizingMaskIntoConstraints = NO;
_submitVideoButton.layer.cornerRadius = 10.0;
_submitVideoButton.clipsToBounds = YES;
_submitVideoButton.titleLabel.font = [UIFont systemFontOfSize:20.0];
[_submitVideoButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[_submitVideoButton setBackgroundColor:[UIColor systemBlueColor]];
[_submitVideoButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_SUBMIT_VIDEO", nil) forState:UIControlStateNormal];
[self.contentView addSubview:_submitVideoButton];
}
- (void)setUpConstraints {
if (_constraints) {
[NSLayoutConstraint deactivateConstraints:_constraints];
}
_constraints = [NSMutableArray array];
[_constraints addObject: [_titleLabel.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:25.0]];
[_constraints addObject: [_titleLabel.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:20.0]];
[_constraints addObject: [_titleLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-20.0]];
//reviewVideoButton constraints
[_constraints addObject:[_reviewVideoButton.topAnchor constraintEqualToAnchor:_titleLabel.bottomAnchor constant:40.0]];
[_constraints addObject:[_reviewVideoButton.leadingAnchor constraintEqualToAnchor:_titleLabel.leadingAnchor]];
[_constraints addObject:[_reviewVideoButton.trailingAnchor constraintEqualToAnchor:_titleLabel.trailingAnchor]];
[_constraints addObject:[_reviewVideoButton.heightAnchor constraintEqualToConstant:50.0]];
//deleteAndRetryButton constraints
[_constraints addObject:[_deleteAndRetryVideoButton.topAnchor constraintEqualToAnchor:_reviewVideoButton.bottomAnchor constant:15.0]];
[_constraints addObject:[_deleteAndRetryVideoButton.leadingAnchor constraintEqualToAnchor:_titleLabel.leadingAnchor]];
[_constraints addObject:[_deleteAndRetryVideoButton.trailingAnchor constraintEqualToAnchor:_titleLabel.trailingAnchor]];
[_constraints addObject:[_deleteAndRetryVideoButton.heightAnchor constraintEqualToConstant:50.0]];
//submitVideoButton constraints
[_constraints addObject:[_submitVideoButton.leadingAnchor constraintEqualToAnchor:_titleLabel.leadingAnchor]];
[_constraints addObject:[_submitVideoButton.trailingAnchor constraintEqualToAnchor:_titleLabel.trailingAnchor]];
[_constraints addObject:[_submitVideoButton.bottomAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.bottomAnchor constant:-20.0]];
[_constraints addObject:[_submitVideoButton.heightAnchor constraintEqualToConstant:50.0]];
[NSLayoutConstraint activateConstraints:_constraints];
}
@end
typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
ORKStartStopButtonStateStartRecording = 0,
ORKStartStopButtonStateStopRecording,
} ORK_ENUM_AVAILABLE;
@interface ORKBlurFooterView : UIVisualEffectView
- (instancetype)initWithTitleText:(nullable NSString *)titleText detailText:(nullable NSString *)detailText;
@property (nonatomic) UIButton *startStopButton;
@property (nonatomic) ORKStartStopButtonState startStopButtonState;
@property (nonatomic) UILabel *timerLabel;
@end
@implementation ORKBlurFooterView {
NSMutableArray<NSLayoutConstraint *> *_heightConstraints;
NSLayoutConstraint *_blurViewTopConstraint;
NSString *_titleText;
NSString *_detailText;
ORKTitleLabel *_titleLabel;
ORKBodyLabel *_detailTextLabel;
UIButton *_collapseButton;
BOOL _isTextCollapsed;
}
- (instancetype)initWithTitleText:(nullable NSString *)titleText detailText:(nullable NSString *)detailText {
self = [super initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];
if (self) {
_titleText = titleText;
_detailText = detailText;
_isTextCollapsed = NO;
_startStopButtonState = ORKStartStopButtonStateStartRecording;
[self setupSubviews];
[self setupConstraints];
[self setStartStopButtonState:ORKStartStopButtonStateStartRecording];
}
return self;
}
- (void)setupSubviews {
_startStopButton = [UIButton new];
if (@available(iOS 15.0, *)) {
UIButtonConfiguration *buttonConfiguration = [UIButtonConfiguration plainButtonConfiguration];
[buttonConfiguration setContentInsets:NSDirectionalEdgeInsetsMake(0, 6, 0, 6)];
[_startStopButton setConfiguration:buttonConfiguration];
} else {
_startStopButton.contentEdgeInsets = (UIEdgeInsets){.left = 6, .right = 6};
}
_startStopButton.layer.cornerRadius = 14.0;
_startStopButton.clipsToBounds = YES;
UIFontDescriptor *descriptorOne = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
_startStopButton.titleLabel.font = [UIFont boldSystemFontOfSize:[[descriptorOne objectForKey: UIFontDescriptorSizeAttribute] doubleValue] + 1.0];
[self.contentView addSubview:_startStopButton];
_timerLabel = [UILabel new];
_timerLabel.font = [UIFont systemFontOfSize:15.0];
_timerLabel.adjustsFontSizeToFitWidth = YES;
[self.contentView addSubview:_timerLabel];
UIImage *collapseButtonImage;
if (@available(iOS 13.0, *)) {
collapseButtonImage = [UIImage systemImageNamed:@"chevron.down"];
}
if (_titleText) {
_titleLabel = [ORKTitleLabel new];
_titleLabel.textAlignment = NSTextAlignmentLeft;
_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
_titleLabel.numberOfLines = 0;
[_titleLabel setTextColor:[UIColor whiteColor]];
_titleLabel.text = _titleText;
[self.contentView addSubview:_titleLabel];
}
if (_detailText) {
_detailTextLabel = [ORKBodyLabel new];
_detailTextLabel.textAlignment = NSTextAlignmentLeft;
_detailTextLabel.lineBreakMode = NSLineBreakByWordWrapping;
_detailTextLabel.numberOfLines = 0;
[_detailTextLabel setTextColor:[UIColor whiteColor]];
_detailTextLabel.text = _detailText ? : @"";
[self.contentView addSubview:_detailTextLabel];
}
if (_titleText || _detailText) {
_collapseButton = [UIButton new];
_collapseButton.translatesAutoresizingMaskIntoConstraints = NO;
[_collapseButton setTintColor:[UIColor whiteColor]];
[_collapseButton setBackgroundImage:collapseButtonImage forState:UIControlStateNormal];
[_collapseButton addTarget:self
action:@selector(collapseButtonPressed)
forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:_collapseButton];
}
}
- (void)setupConstraints {
_startStopButton.translatesAutoresizingMaskIntoConstraints = NO;
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
_detailTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[[_startStopButton.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:20.0] setActive:YES];
[[_startStopButton.trailingAnchor constraintEqualToAnchor:_timerLabel.leadingAnchor constant:-15.0] setActive:YES];
[[_startStopButton.bottomAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.bottomAnchor constant:-20.0] setActive:YES];
[[_startStopButton.heightAnchor constraintEqualToConstant:50.0] setActive:YES];
[[_timerLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-20.0] setActive:YES];
[[_timerLabel.centerYAnchor constraintEqualToAnchor:_startStopButton.centerYAnchor] setActive:YES];
[[_timerLabel.widthAnchor constraintEqualToConstant:40.0] setActive:YES];
if (_titleLabel || _detailTextLabel) {
if (_detailTextLabel) {
[[_detailTextLabel.leadingAnchor constraintEqualToAnchor:_startStopButton.leadingAnchor] setActive:YES];
[[_detailTextLabel.trailingAnchor constraintEqualToAnchor:_timerLabel.trailingAnchor] setActive:YES];
[[_detailTextLabel.bottomAnchor constraintEqualToAnchor:_startStopButton.topAnchor constant:-20.0] setActive:YES];
}
if (_titleLabel) {
[[_titleLabel.leadingAnchor constraintEqualToAnchor:_startStopButton.leadingAnchor] setActive:YES];
[[_titleLabel.trailingAnchor constraintEqualToAnchor:_collapseButton.leadingAnchor constant: -10.0] setActive:YES];
[[_titleLabel.bottomAnchor constraintEqualToAnchor:_detailTextLabel ? _detailTextLabel.topAnchor : _startStopButton.topAnchor constant: -15.0] setActive:YES];
[[_collapseButton.topAnchor constraintEqualToAnchor:_titleLabel.topAnchor] setActive:YES];
_blurViewTopConstraint = [self.contentView.topAnchor constraintEqualToAnchor:_titleLabel.topAnchor constant:-20.0];
} else {
[[_collapseButton.bottomAnchor constraintEqualToAnchor:_detailTextLabel.topAnchor constant:-15.0] setActive:YES];
_blurViewTopConstraint = [self.contentView.topAnchor constraintEqualToAnchor:_collapseButton.topAnchor constant:-20.0];
}
[[_collapseButton.trailingAnchor constraintEqualToAnchor:_timerLabel.trailingAnchor] setActive:YES];
[[_collapseButton.heightAnchor constraintEqualToConstant:25.0] setActive:YES];
[[_collapseButton.widthAnchor constraintEqualToConstant:25.0] setActive:YES];
[_blurViewTopConstraint setActive:YES];
} else {
[[self.contentView.topAnchor constraintEqualToAnchor:_startStopButton.topAnchor constant:-20.0] setActive:YES];
}
}
- (void)setStartStopButtonState:(ORKStartStopButtonState)startStopButtonState
{
_startStopButtonState = startStopButtonState;
if (startStopButtonState == ORKStartStopButtonStateStartRecording)
{
[_startStopButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_START_TITLE", nil) forState:UIControlStateNormal];
[_startStopButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[_startStopButton setBackgroundColor:[UIColor systemBlueColor]];
[_timerLabel setText:ORKLocalizedString(@"FRONT_FACING_CAMERA_START_TIME", nil)];
[_timerLabel setTextColor:[UIColor darkGrayColor]];
}
else
{
[_startStopButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_STOP_TITLE", nil) forState:UIControlStateNormal];
[_startStopButton setTitleColor:[UIColor systemBlueColor] forState:UIControlStateNormal];
[_startStopButton setBackgroundColor:[UIColor systemGrayColor]];
[_timerLabel setTextColor:[UIColor whiteColor]];
}
}
- (void)collapseButtonPressed {
UIImage *collapseButtonImage;
if (_isTextCollapsed) {
[_blurViewTopConstraint setActive:NO];
_blurViewTopConstraint = [self.contentView.topAnchor constraintEqualToAnchor:_titleLabel.topAnchor constant:-20.0];
[_blurViewTopConstraint setActive:YES];
[NSLayoutConstraint deactivateConstraints:_heightConstraints];
_heightConstraints = nil;
} else {
[_blurViewTopConstraint setActive:NO];
_blurViewTopConstraint = [self.contentView.topAnchor constraintEqualToAnchor:_collapseButton.topAnchor constant:-20.0];
[_blurViewTopConstraint setActive:YES];
_heightConstraints = [NSMutableArray new];
[_heightConstraints addObject:[_titleLabel.heightAnchor constraintEqualToConstant:0.0]];
[_heightConstraints addObject:[_detailTextLabel.heightAnchor constraintEqualToConstant:0.0]];
[NSLayoutConstraint activateConstraints:_heightConstraints];
}
if (@available(iOS 13.0, *)) {
collapseButtonImage = _isTextCollapsed ? [UIImage systemImageNamed:@"chevron.down"] : [UIImage systemImageNamed:@"chevron.up"];
}
[_collapseButton setBackgroundImage:collapseButtonImage forState:UIControlStateNormal];
_isTextCollapsed = !_isTextCollapsed;
}
@end
@interface ORKFrontFacingCameraStepContentView ()
@property (nonatomic, copy, nullable) ORKFrontFacingCameraStepContentViewEventHandler viewEventhandler;
@end
@implementation ORKFrontFacingCameraStepContentView {
ORKStepHeaderView *_headerView;
UIView *_cameraView;
AVCaptureVideoPreviewLayer *_previewLayer;
ORKBlurFooterView *_blurFooterView;
NSTimer *_timer;
NSTimeInterval _maxRecordingTime;
CGFloat _recordingTime;
NSDateComponentsFormatter *_dateComponentsFormatter;
ORKFrontFacingCameraStepOptionsView *_optionsView;
NSString *_titleText;
NSString *_bodyText;
}
- (instancetype)initWithTitle:(nullable NSString *)title text:(NSString *)text {
self = [super initWithFrame:CGRectZero];
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
if (self) {
self.translatesAutoresizingMaskIntoConstraints = NO;
_titleText = title;
_bodyText = text;
[self setUpSubviews];
[self setUpConstraints];
}
return self;
}
- (void)setUpSubviews {
_cameraView = [UIView new];
_cameraView.alpha = 1.0;
[self addSubview:_cameraView];
_blurFooterView = [[ORKBlurFooterView alloc] initWithTitleText:_titleText detailText:_bodyText];
_blurFooterView.layer.cornerRadius = 10.0;
_blurFooterView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
_blurFooterView.clipsToBounds = YES;
[_blurFooterView.startStopButton addTarget:self
action:@selector(startStopButtonPressed)
forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_blurFooterView];
}
- (void)layoutSubviews {
if (_previewLayer && _previewLayer.frame.size.height == 0 && _cameraView.frame.size.height != 0) {
_previewLayer.position = CGPointMake(_cameraView.frame.size.width / 2, _cameraView.frame.size.height / 2);
_previewLayer.bounds = CGRectMake(0, 0, _cameraView.frame.size.width, _cameraView.frame.size.height);
}
}
- (void)setUpConstraints {
_cameraView.translatesAutoresizingMaskIntoConstraints = NO;
_blurFooterView.translatesAutoresizingMaskIntoConstraints = NO;
[[_cameraView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
[[_cameraView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
[[_cameraView.topAnchor constraintEqualToAnchor:self.topAnchor] setActive:YES];
[[_cameraView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
[[_blurFooterView.leadingAnchor constraintEqualToAnchor:_cameraView.leadingAnchor] setActive:YES];
[[_blurFooterView.trailingAnchor constraintEqualToAnchor:_cameraView.trailingAnchor] setActive:YES];
[[_blurFooterView.bottomAnchor constraintEqualToAnchor:_cameraView.bottomAnchor] setActive:YES];
}
- (void)setViewEventHandler:(ORKFrontFacingCameraStepContentViewEventHandler)handler
{
self.viewEventhandler = [handler copy];
}
- (void)invokeViewEventHandlerWithEvent:(ORKFrontFacingCameraStepContentViewEvent)event
{
if (self.viewEventhandler)
{
dispatch_async(dispatch_get_main_queue(), ^{
self.viewEventhandler(event);
});
}
}
- (void)setPreviewLayerWithSession:(AVCaptureSession *)session {
_previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
_previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
_previewLayer.needsDisplayOnBoundsChange = YES;
_previewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortrait;
[_cameraView.layer addSublayer:_previewLayer];
}
- (void)handleError:(NSError *)error
{
[_optionsView removeFromSuperview];
[_cameraView removeFromSuperview];
[_blurFooterView removeFromSuperview];
[_previewLayer removeFromSuperlayer];
_optionsView = nil;
_cameraView = nil;
_blurFooterView = nil;
_previewLayer = nil;
if (_headerView)
{
[_headerView removeFromSuperview];
_headerView = nil;
}
_headerView = [[ORKStepHeaderView alloc] init];
_headerView.instructionLabel.text = error.localizedDescription;
[_headerView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:_headerView];
[NSLayoutConstraint activateConstraints:@[
[_headerView.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor],
[_headerView.leftAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leftAnchor],
[_headerView.rightAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.rightAnchor],
]];
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventError];
}
- (void)startStopButtonPressed
{
if (_blurFooterView.startStopButtonState == ORKStartStopButtonStateStartRecording)
{
[_blurFooterView setStartStopButtonState:ORKStartStopButtonStateStopRecording];
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventStartRecording];
}
else
{
[_blurFooterView setStartStopButtonState:ORKStartStopButtonStateStartRecording];
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventStopRecording];
[_timer invalidate];
_timer = nil;
}
}
- (void)startTimerWithMaximumRecordingLimit:(NSTimeInterval)maximumRecordingLimit
{
if (_timer) {
[_timer invalidate];
}
_maxRecordingTime = maximumRecordingLimit;
_recordingTime = 0.0;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateRecordingTime)
userInfo:nil
repeats:YES];
}
- (void)updateRecordingTime {
_recordingTime += _timer.timeInterval;
if (_recordingTime >= _maxRecordingTime) {
[_timer invalidate];
[_blurFooterView setStartStopButtonState:ORKStartStopButtonStateStartRecording];
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventStopRecording];
} else {
_blurFooterView.timerLabel.text = [self formattedTimeFromSeconds:_recordingTime];
}
}
- (NSString *)formattedTimeFromSeconds:(CGFloat)seconds {
if (!_dateComponentsFormatter) {
_dateComponentsFormatter = [NSDateComponentsFormatter new];
_dateComponentsFormatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
_dateComponentsFormatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitNanosecond;
}
return [_dateComponentsFormatter stringFromTimeInterval:seconds];
}
- (void)presentReviewOptionsAllowingReview:(BOOL)allowReview allowRetry:(BOOL)allowRetry
{
if (allowRetry || allowReview)
{
[self presentOptionsView];
[_optionsView.reviewVideoButton setHidden:!allowReview];
[_optionsView.deleteAndRetryVideoButton setHidden:!allowRetry];
}
}
- (void)presentOptionsView
{
if (_optionsView)
{
[_optionsView removeFromSuperview];
_optionsView = nil;
}
_optionsView = [[ORKFrontFacingCameraStepOptionsView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];
_optionsView.translatesAutoresizingMaskIntoConstraints = NO;
[_optionsView.reviewVideoButton addTarget:self
action:@selector(optionsViewButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[_optionsView.deleteAndRetryVideoButton addTarget:self
action:@selector(optionsViewButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[_optionsView.submitVideoButton addTarget:self
action:@selector(optionsViewButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_optionsView];
[self setupOptionsViewConstraints];
}
- (void)setupOptionsViewConstraints {
[[_optionsView.topAnchor constraintEqualToAnchor:self.topAnchor] setActive:YES];
[[_optionsView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
[[_optionsView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
[[_optionsView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
}
- (void)optionsViewButtonPressed:(UIButton *)button {
if (button) {
if (button.tag == 0) {
//review video
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventReviewRecording];
} else if (button.tag == 1) {
//delete and redo recording
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventRetryRecording];
[_optionsView removeFromSuperview];
_optionsView = nil;
} else if (button.tag == 2) {
//submit video
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventSubmitRecording];
}
}
}
@end
@@ -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 <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKFileResult.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKFrontFacingCameraStepResult : ORKFileResult
@property (nonatomic, assign) NSInteger retryCount;
@end
NS_ASSUME_NONNULL_END
@@ -1,5 +1,5 @@
/*
Copyright (c) 2018, Apple Inc. All rights reserved.
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:
@@ -28,37 +28,40 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKFrontFacingCameraStepResult.h"
#import "ORKResult_Private.h"
#import "ORKHelpers_Internal.h"
import Foundation
@implementation ORKFrontFacingCameraStepResult
@available(watchOSApplicationExtension 5.0, *)
class AssessmentManager {
private var manager: CMMovementDisorderManager?
init() {
if CMMovementDisorderManager.isAvailable() {
manager = CMMovementDisorderManager()
monitorForParkinsons()
}
}
func monitorForParkinsons() {
manager?.monitorKinesias(forDuration: 7 * 24 * 3600)
}
func queryNewAssessments() {
let calendar = Calendar.current
let toDate = Date()
let fromDate:Date = calendar.date(byAdding: .day, value: -7, to: toDate)!
manager?.queryTremor(from: fromDate, to: toDate, withHandler: { (results, error) in
})
manager?.queryDyskineticSymptom(from: fromDate, to: toDate, withHandler: { (results, error) in
})
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_INTEGER(aCoder, retryCount);
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_INTEGER(aDecoder, retryCount);
}
return self;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return isParentSame && (castObject.retryCount == self.retryCount);
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKFrontFacingCameraStepResult *result = [super copyWithZone:zone];
result.retryCount = self.retryCount;
return result;
}
@end
@@ -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 <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKActiveStepViewController.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKFrontFacingCameraStepViewController : ORKActiveStepViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,381 @@
/*
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 <AVFoundation/AVFoundation.h>
#import <AVKit/AVKit.h>
#import <CoreImage/CoreImage.h>
#import <MediaPlayer/MediaPlayer.h>
#import "ORKActiveStepView.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKBorderedButton.h"
#import "ORKCollectionResult_Private.h"
#import "ORKFrontFacingCameraStep.h"
#import "ORKFrontFacingCameraStepContentView.h"
#import "ORKFrontFacingCameraStepResult.h"
#import "ORKFrontFacingCameraStepViewController.h"
#import "ORKHelpers_Internal.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKResult_Private.h"
#import "ORKStepContainerView_Private.h"
#import "ORKStepViewController_Internal.h"
@interface ORKFrontFacingCameraStepViewController () <AVCaptureFileOutputRecordingDelegate, AVCaptureVideoDataOutputSampleBufferDelegate>
@property (nonatomic, strong) ORKFrontFacingCameraStepContentView *contentView;
@end
@implementation ORKFrontFacingCameraStepViewController {
NSMutableArray *_results;
ORKFrontFacingCameraStep *_frontFacingCameraStep;
AVCaptureMovieFileOutput *_movieFileOutput;
NSURL *_tempOutputURL;
NSURL *_savedFileURL;
NSString *_savedFileName;
AVCaptureDevice *_frontCameraCaptureDevice;
AVCaptureSession *_captureSession;
NSInteger retryCount;
}
- (instancetype)initWithStep:(ORKStep *)step {
self = [super initWithStep:step];
if (self) {
retryCount = 0;
_frontFacingCameraStep = (ORKFrontFacingCameraStep *)step;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
_results = [NSMutableArray new];
[self setupContentView];
[self setupConstraints];
[self startSession];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[_contentView layoutSubviews];
}
- (void)handleError:(NSError *)error {
// Shut down the session, if running
if (_captureSession.isRunning) {
[_captureSession stopRunning];
}
// Reset the state to before the capture session was setup. Order here is important
_captureSession = nil;
_movieFileOutput = nil;
_tempOutputURL = nil;
_savedFileURL = nil;
// Handle error in the UI.
[_contentView handleError:error];
}
- (void)stepDidFinish {
[super stepDidFinish];
if (_tempOutputURL) {
[self deleteTempVideoFile];
}
[self goForward];
}
- (void)setupContentView {
_contentView = [[ORKFrontFacingCameraStepContentView alloc] initWithTitle:_frontFacingCameraStep.title text:_frontFacingCameraStep.text];
_contentView.layer.cornerRadius = 10.0;
_contentView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
_contentView.clipsToBounds = YES;
__weak typeof(self) weakSelf = self;
[_contentView setViewEventHandler:^(ORKFrontFacingCameraStepContentViewEvent event) {
[weakSelf handleContentViewEvent:event];
}];
[self.view addSubview:_contentView];
}
- (void)handleContentViewEvent:(ORKFrontFacingCameraStepContentViewEvent)event {
switch (event)
{
case ORKFrontFacingCameraStepContentViewEventStartRecording:
[self startVideoRecording];
break;
case ORKFrontFacingCameraStepContentViewEventStopRecording:
[self stopVideoRecording];
break;
case ORKFrontFacingCameraStepContentViewEventReviewRecording:
{
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:_tempOutputURL];
AVPlayer *playVideo = [[AVPlayer alloc] initWithPlayerItem:playerItem];
AVPlayerViewController *playerViewController = [[AVPlayerViewController alloc] init];
playerViewController.player = playVideo;
playerViewController.player.volume = 1.0;
[self presentViewController:playerViewController animated:YES completion:nil];
[playVideo play];
break;
}
case ORKFrontFacingCameraStepContentViewEventRetryRecording:
[self deleteTempVideoFile];
retryCount++;
break;
case ORKFrontFacingCameraStepContentViewEventSubmitRecording:
{
[self submitVideo];
break;
}
case ORKFrontFacingCameraStepContentViewEventError:
break;
}
}
-(void)setupConstraints {
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
[[_contentView.topAnchor constraintEqualToAnchor:self.view.topAnchor] setActive:YES];
[[_contentView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor] setActive:YES];
[[_contentView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor] setActive:YES];
[[_contentView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] setActive:YES];
}
- (void)startSession
{
_captureSession = [AVCaptureSession new];
_frontCameraCaptureDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
if (_frontCameraCaptureDevice)
{
NSError *error = nil;
AVCaptureDevice *captureAudioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:_frontCameraCaptureDevice error:&error];
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:captureAudioDevice error:&error];
[AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVideoRecording options:0 error:&error];
if (error) {
[self handleError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSFeatureUnsupportedError userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"CAPTURE_ERROR_CAMERA_NOT_FOUND", nil)}]];
return;
}
[_captureSession beginConfiguration];
if ([_captureSession canAddInput:deviceInput]) {
[_captureSession addInput:deviceInput];
}
if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
[_captureSession setSessionPreset:AVCaptureSessionPreset640x480];
}
if ([_captureSession canAddInput:audioInput]) {
[_captureSession addInput:audioInput];
}
_movieFileOutput = [AVCaptureMovieFileOutput new];
if ([_captureSession canAddOutput:_movieFileOutput]) {
[_captureSession addOutput:_movieFileOutput];
AVCaptureConnection *captureConnection = [_movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
if (captureConnection && [captureConnection isVideoStabilizationSupported]) {
captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
}
AVCaptureVideoDataOutput *output = [AVCaptureVideoDataOutput new];
NSString* key = (NSString*)kCVPixelBufferPixelFormatTypeKey;
NSNumber* value = [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA];
NSDictionary* videoSettings = [NSDictionary dictionaryWithObject:value forKey:key];
[output setVideoSettings:videoSettings];
output.alwaysDiscardsLateVideoFrames = YES;
if ([_captureSession canAddOutput:output]) {
[_captureSession addOutput:output];
}
AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
if ([connection isVideoOrientationSupported]) {
connection.videoOrientation = AVCaptureVideoOrientationPortrait;
}
if ([connection isVideoMirroringSupported]) {
[connection setVideoMirrored:NO];
}
[_captureSession commitConfiguration];
dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
dispatch_queue_t recordingQueue = dispatch_queue_create("output.queue", qos);
[output setSampleBufferDelegate:self queue:recordingQueue];
[_contentView setPreviewLayerWithSession:_captureSession];
[_captureSession startRunning];
}
[_contentView layoutSubviews];
}
- (void)startVideoRecording {
if (![_movieFileOutput isRecording]) {
AVCaptureConnection *movieFileOutputConnection = [_movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
[movieFileOutputConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
NSArray<AVVideoCodecType> *availableVideoCodecTypes = _movieFileOutput.availableVideoCodecTypes;
if (availableVideoCodecTypes && [availableVideoCodecTypes containsObject:AVVideoCodecTypeHEVC]) {
NSString* key = (NSString*)AVVideoCodecKey;
NSString* value = (NSString*)AVVideoCodecTypeHEVC;
NSDictionary* outputSettings = [NSDictionary dictionaryWithObject:value forKey:key];
[_movieFileOutput setOutputSettings:outputSettings forConnection:movieFileOutputConnection];
}
// Start recording to a temporary file.
NSString *tempVideoFilePath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID new].UUIDString] stringByAppendingPathExtension:@"mov"];
[_movieFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:tempVideoFilePath] recordingDelegate:self];
}
[_contentView layoutSubviews];
}
- (void)stopVideoRecording {
if (_movieFileOutput && [_movieFileOutput isRecording]) {
[_movieFileOutput stopRecording];
}
}
- (void)submitVideo {
if ([self tempVideoFileExists])
{
//Save video to permanant file
NSString *outputFileName = [NSUUID new].UUIDString;
_savedFileName = [outputFileName stringByAppendingPathExtension:@"mov"];
NSURL *docURL = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject;
docURL = [docURL URLByAppendingPathComponent:_savedFileName];
NSData *data = [NSData dataWithContentsOfURL:_tempOutputURL];
BOOL wasDataSavedToURL = [data writeToURL:docURL atomically:YES];
if (wasDataSavedToURL)
{
//remove video saved to temp directory if it was saved successfully in the document directory
_savedFileURL = docURL;
[self deleteTempVideoFile];
[self finish];
}
}
}
- (BOOL)tempVideoFileExists {
if (_tempOutputURL && [NSFileManager.defaultManager fileExistsAtPath:_tempOutputURL.relativePath]) {
return YES;
} else {
return NO;
}
}
- (void)deleteTempVideoFile {
if ([self tempVideoFileExists]) {
NSError *error;
[NSFileManager.defaultManager removeItemAtPath:_tempOutputURL.relativePath error:&error];
if (!error) {
_tempOutputURL = nil;
} else {
@throw [NSException exceptionWithName:NSGenericException reason:[NSString stringWithFormat:@"There was an error encountered while attempting to remove the saved video from the temp directory at path: %@", _tempOutputURL.path] userInfo:nil];
}
} else if (_tempOutputURL) {
_tempOutputURL = nil;
}
}
- (ORKStepResult *)result {
ORKStepResult *stepResult = [super result];
NSDate *now = stepResult.endDate;
NSMutableArray *results = [NSMutableArray arrayWithArray:stepResult.results];
ORKFrontFacingCameraStepResult *frontFacingCameraResult = [[ORKFrontFacingCameraStepResult alloc] initWithIdentifier:self.step.identifier];
frontFacingCameraResult.startDate = stepResult.startDate;
frontFacingCameraResult.endDate = now;
frontFacingCameraResult.contentType = @"video/quicktime";
frontFacingCameraResult.fileURL = _savedFileURL;
frontFacingCameraResult.fileName = _savedFileName;
frontFacingCameraResult.retryCount = retryCount;
[results addObject:frontFacingCameraResult];
stepResult.results = [results copy];
return stepResult;
}
#pragma mark - AVCaptureFileOutputRecordingDelegate methods
- (void)captureOutput:(AVCaptureFileOutput *)output didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections {
[_contentView startTimerWithMaximumRecordingLimit:_frontFacingCameraStep.maximumRecordingLimit];
}
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error
{
if (!error)
{
_tempOutputURL = outputFileURL;
[_contentView presentReviewOptionsAllowingReview:_frontFacingCameraStep.allowsReview
allowRetry:_frontFacingCameraStep.allowsRetry];
if (!_frontFacingCameraStep.allowsRetry && !_frontFacingCameraStep.allowsReview) {
[self submitVideo];
}
}
}
@end
@@ -77,10 +77,10 @@
[super start];
if (!_logger) {
NSError *err = nil;
_logger = [self makeJSONDataLoggerWithError:&err];
NSError *error = nil;
_logger = [self makeJSONDataLoggerWithError:&error];
if (!_logger) {
[self finishRecordingWithError:err];
[self finishRecordingWithError:error];
return;
}
}
@@ -99,18 +99,18 @@
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:_healthClinicalType
predicate:_healthFHIRResourceType ? [HKQuery predicateForClinicalRecordsWithFHIRResourceType:_healthFHIRResourceType] : nil limit:HKObjectQueryNoLimit
sortDescriptors:nil
resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
NSUInteger resultCount = results.count;
resultsHandler:^(HKSampleQuery * _Nonnull sampleQuery, NSArray<__kindof HKSample *> * _Nullable sampleResults, NSError * _Nullable error) {
NSUInteger resultCount = sampleResults.count;
if (resultCount == 0) {
return;
}
[results enumerateObjectsUsingBlock:^(HKClinicalRecord *clinicalRecord, NSUInteger idx, BOOL *stop) {
[sampleResults enumerateObjectsUsingBlock:^(HKClinicalRecord *clinicalRecord, NSUInteger idx, BOOL *stop) {
NSError *error = nil;
[_logger append:clinicalRecord.FHIRResource.data error:&error];
if (error) {
ORK_Log_Warning(@"Failed to add health records object to the logger with error: %@", error);
NSError *logError = nil;
[_logger append:clinicalRecord.FHIRResource.data error:&logError];
if (logError) {
ORK_Log_Error("Failed to add health records object to the logger with error: %@", logError);
return;
}
}];
@@ -205,7 +205,7 @@
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, healthClinicalType, HKClinicalType);
ORK_DECODE_OBJ(aDecoder, healthFHIRResourceType);
ORK_DECODE_OBJ_CLASS(aDecoder, healthFHIRResourceType, NSString);
}
return self;
}
@@ -150,7 +150,7 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
void (^handleResults)(NSArray <__kindof HKSample *> *, HKQueryAnchor *, NSUInteger, NSError *) = ^ (NSArray *results, HKQueryAnchor *newAnchor, NSUInteger newAnchorValue, NSError *error) {
if (error) {
// An error in the query's not the end of the world: we'll probably get another chance. Just log it.
ORK_Log_Warning(@"Anchored query error: %@", error);
ORK_Log_Error("Anchored query error: %@", error);
return;
}
@@ -194,10 +194,10 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
[super start];
if (!_logger) {
NSError *err = nil;
_logger = [self makeJSONDataLoggerWithError:&err];
NSError *error = nil;
_logger = [self makeJSONDataLoggerWithError:&error];
if (!_logger) {
[self finishRecordingWithError:err];
[self finishRecordingWithError:error];
return;
}
}
@@ -141,21 +141,6 @@ static const CGFloat ORKHolePegViewDiameter = 88.0f;
}];
}
- (void)updateLayoutMargins {
CGFloat margin = ORKStandardHorizontalMarginForView(self);
self.layoutMargins = (UIEdgeInsets){.left = margin * 2, .right = margin * 2};
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self updateLayoutMargins];
}
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
[self updateLayoutMargins];
}
- (void)updateConstraints {
if ([self.constraints count]) {
[NSLayoutConstraint deactivateConstraints:self.constraints];
@@ -165,7 +150,6 @@ static const CGFloat ORKHolePegViewDiameter = 88.0f;
NSMutableArray *constraintsArray = [NSMutableArray array];
NSDictionary *views = NSDictionaryOfVariableBindings(_progressView, _pegView, _holeView, _directionView);
NSDictionary *metrics = @{@"diameter": @(ORKHolePegViewDiameter)};
[constraintsArray addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_progressView]-|"
@@ -181,16 +165,6 @@ static const CGFloat ORKHolePegViewDiameter = 88.0f;
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_progressView]"
options:(NSLayoutFormatOptions)0
metrics:nil views:views]];
[constraintsArray addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=0-[_pegView(diameter)]->=0-|"
options:(NSLayoutFormatOptions)0
metrics:metrics views:views]];
[constraintsArray addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=0-[_holeView]->=0-|"
options:(NSLayoutFormatOptions)0
metrics:nil views:views]];
[constraintsArray addObject:[NSLayoutConstraint constraintWithItem:self.pegView
attribute:NSLayoutAttributeCenterY
@@ -33,6 +33,7 @@
#import "ORKActiveStepView.h"
#import "ORKHolePegTestPlaceContentView.h"
#import "ORKStepContainerView_Private.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
@@ -88,8 +89,7 @@
self.holePegTestPlaceContentView.threshold = [self holePegTestPlaceStep].threshold;
self.holePegTestPlaceContentView.delegate = self;
self.activeStepView.activeCustomView = self.holePegTestPlaceContentView;
self.activeStepView.stepViewFillsAvailableSpace = YES;
self.activeStepView.scrollContainerShouldCollapseNavbar = NO;
self.activeStepView.customContentFillsAvailableSpace = YES;
}
#pragma mark - step life cycle methods
@@ -150,7 +150,7 @@
[self start];
}
[self.activeStepView updateTitle:nil
[self.activeStepView updateTitle:self.step.title
text:[[self holePegTestPlaceStep].movingDirection == ORKBodySagittalRight ? ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT_2", nil)]]];
}
@@ -160,7 +160,7 @@
[self saveSampleWithDistance:distance];
[holePegTestPlaceContentView setProgress:((CGFloat)self.successes / [self holePegTestPlaceStep].numberOfPegs) animated:YES];
[self.activeStepView updateTitle:nil
[self.activeStepView updateTitle:self.step.title
text:[[self holePegTestPlaceStep].movingDirection == ORKBodySagittalRight ? ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)]]];
if (self.successes >= [self holePegTestPlaceStep].numberOfPegs) {
@@ -172,7 +172,7 @@
- (void)holePegTestPlaceDidFail:(ORKHolePegTestPlaceContentView *)holePegTestPlaceContentView {
self.failures++;
[self.activeStepView updateTitle:nil
[self.activeStepView updateTitle:self.step.title
text:[[self holePegTestPlaceStep].movingDirection == ORKBodySagittalRight ? ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)]]];
}
@@ -134,21 +134,6 @@ static const CGFloat PegViewSeparatorWidth = 2.0f;
}];
}
- (void)updateLayoutMargins {
CGFloat margin = ORKStandardHorizontalMarginForView(self);
self.layoutMargins = (UIEdgeInsets){.left = margin * 2, .right = margin * 2};
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self updateLayoutMargins];
}
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
[self updateLayoutMargins];
}
- (void)updateConstraints {
if ([self.constraints count]) {
[NSLayoutConstraint deactivateConstraints:self.constraints];
@@ -33,6 +33,7 @@
#import "ORKActiveStepView.h"
#import "ORKHolePegTestRemoveContentView.h"
#import "ORKStepContainerView_Private.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
@@ -86,8 +87,7 @@
self.holePegTestRemoveContentView.threshold = [self holePegTestRemoveStep].threshold;
self.holePegTestRemoveContentView.delegate = self;
self.activeStepView.activeCustomView = self.holePegTestRemoveContentView;
self.activeStepView.stepViewFillsAvailableSpace = YES;
self.activeStepView.scrollContainerShouldCollapseNavbar = NO;
self.activeStepView.customContentFillsAvailableSpace = YES;
NSString *identifier = [[self holePegTestRemoveStep].identifier stringByReplacingOccurrencesOfString:@"remove" withString:@"place"];
NSTimeInterval placeStepDuration = ((ORKHolePegTestResult *)[[self.taskViewController.result stepResultForStepIdentifier:identifier].results firstObject]).totalTime;
@@ -150,7 +150,7 @@
#pragma mark - hole peg test content view delegate
- (void)holePegTestRemoveDidProgress:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView {
[self.activeStepView updateTitle:nil
[self.activeStepView updateTitle:self.step.title
text:[[self holePegTestRemoveStep].movingDirection == ORKBodySagittalLeft ? ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT_2", nil)]]];
}
@@ -160,7 +160,7 @@
[self saveSampleWithDistance:distance];
[holePegTestRemoveContentView setProgress:((CGFloat)self.successes / [self holePegTestRemoveStep].numberOfPegs) animated:YES];
[self.activeStepView updateTitle:nil
[self.activeStepView updateTitle:self.step.title
text:[[self holePegTestRemoveStep].movingDirection == ORKBodySagittalLeft ? ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)]]];
if (self.successes >= [self holePegTestRemoveStep].numberOfPegs) {
@@ -171,7 +171,7 @@
- (void)holePegTestRemoveDidFail:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView {
self.failures++;
[self.activeStepView updateTitle:nil
[self.activeStepView updateTitle:self.step.title
text:[[self holePegTestRemoveStep].movingDirection == ORKBodySagittalLeft ? ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)]]];
}
@@ -0,0 +1,110 @@
/*
Copyright (c) 2019, Novartis.
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 ResearchKit.Private
public class ORKLandoltCResult: ORKResult {
public var outcome: Bool?
public var letterAngle: Double?
public var sliderAngle: Double?
public var score: Int?
enum Keys: String {
case outcome
case letterAngle
case sliderAngle
case score
}
public init(identifier: String, outcome: Bool, letterAngle: Double, sliderAngle: Double, score: Int) {
super.init(identifier: identifier)
self.outcome = outcome
self.letterAngle = letterAngle
self.sliderAngle = sliderAngle
self.score = score
}
override public func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(outcome, forKey: Keys.outcome.rawValue)
aCoder.encode(letterAngle, forKey: Keys.letterAngle.rawValue)
aCoder.encode(sliderAngle, forKey: Keys.sliderAngle.rawValue)
aCoder.encode(score, forKey: Keys.score.rawValue)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
outcome = aDecoder.decodeObject(forKey: Keys.outcome.rawValue) as? Bool ?? false
letterAngle = aDecoder.decodeObject(forKey: Keys.letterAngle.rawValue) as? Double ?? 0.0
sliderAngle = aDecoder.decodeObject(forKey: Keys.sliderAngle.rawValue) as? Double ?? 0.0
score = aDecoder.decodeObject(forKey: Keys.score.rawValue) as? Int ?? 0
}
override public func copy(with zone: NSZone? = nil) -> Any {
let result = super.copy(with: zone) as! ORKLandoltCResult
result.outcome = outcome
result.letterAngle = letterAngle
result.sliderAngle = sliderAngle
result.score = score
return result
}
override public func isEqual(_ object: Any?) -> Bool {
let isParentSame = super.isEqual(object)
if let castObject = object as? ORKLandoltCResult {
return (isParentSame &&
ORKEqualObjects(outcome as Any, castObject.outcome as Any) &&
ORKEqualObjects(letterAngle as Any, castObject.letterAngle as Any) &&
ORKEqualObjects(sliderAngle as Any, castObject.sliderAngle as Any) &&
ORKEqualObjects(score as Any, castObject.score as Any))
}
return true
}
override public func description(withNumberOfPaddingSpaces numberOfPaddingSpaces: UInt) -> String {
let descriptionString = """
\(descriptionPrefix(withNumberOfPaddingSpaces: numberOfPaddingSpaces));
Outcome: \(String(describing: outcome)); LetterAngle: \(String(describing: letterAngle));
SliderAngle: \(String(describing: sliderAngle)); Score: \(String(describing: score))
"""
return descriptionString
}
}
@@ -0,0 +1,105 @@
/*
Copyright (c) 2019, Novartis.
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.
*/
@objc
public enum VisionStepLeftOrRightEye: Int {
case left
case right
}
@objc
public enum VisionStepType: Int {
case visualAcuity
case contrastSensitivity
}
@objc
public class ORKLandoltCStep: ORKActiveStep {
public var testType: VisionStepType?
public var eyeToTest: VisionStepLeftOrRightEye?
enum Key: String {
case testType
case eyeToTest
}
public override class func stepViewControllerClass() -> AnyClass {
return ORKLandoltCStepViewController.self
}
public class func supportsSecureCoding() -> Bool {
return true
}
@objc
public init(identifier: String, testType: VisionStepType, eyeToTest: VisionStepLeftOrRightEye) {
super.init(identifier: identifier)
self.testType = testType
self.eyeToTest = eyeToTest
}
public override var allowsBackNavigation: Bool {
return false
}
public override func copy(with zone: NSZone? = nil) -> Any {
let visionStep: ORKLandoltCStep = super.copy(with: zone) as! ORKLandoltCStep
return visionStep
}
public required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if let typeValue = aDecoder.decodeObject(forKey: "stepType") as? Int {
testType = VisionStepType(rawValue: typeValue)
}
if let eyeValue = aDecoder.decodeObject(forKey: "eyeToTest") as? Int {
eyeToTest = VisionStepLeftOrRightEye(rawValue: eyeValue)
}
}
public override func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(testType, forKey: Key.testType.rawValue)
aCoder.encode(eyeToTest, forKey: Key.eyeToTest.rawValue)
}
public override func isEqual(_ object: Any?) -> Bool {
if let object = object as? ORKLandoltCStep {
return testType == object.testType && eyeToTest == object.eyeToTest
}
return false
}
}
@@ -0,0 +1,81 @@
/*
Copyright (c) 2019, Novartis.
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 ResearchKit.Private
internal class ORKLandoltCStepContentView: UIView {
var eyeActivitySlider: EyeActivitySlider?
private var testType: VisionStepType?
internal init(testType: VisionStepType) {
super.init(frame: CGRect())
translatesAutoresizingMaskIntoConstraints = false
self.testType = testType
setupSubviews()
setupConstraints()
}
internal required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
internal func setupSubviews() {
guard let typeValue = testType else {
return
}
eyeActivitySlider = EyeActivitySlider(testType: typeValue)
addSubview(eyeActivitySlider!)
}
internal func setupConstraints() {
eyeActivitySlider?.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
NSLayoutConstraint(item: eyeActivitySlider!,
attribute: .width,
relatedBy: .equal,
toItem: self,
attribute: .width,
multiplier: 1.0,
constant: 0.0),
NSLayoutConstraint(item: eyeActivitySlider!,
attribute: .height,
relatedBy: .equal,
toItem: self,
attribute: .width,
multiplier: 1.0,
constant: 0.0)
])
}
}
@@ -0,0 +1,268 @@
/*
Copyright (c) 2019, Novartis.
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 ResearchKit.Private
public class ORKLandoltCStepViewController: ORKActiveStepViewController {
private var activityTimer = Timer()
private var results = NSMutableArray()
private var visionStepView: ORKLandoltCStepView
private var eyeToTest: VisionStepLeftOrRightEye?
private var testType: VisionStepType?
public override init(step: ORKStep?) {
if let visionStep = step as? ORKLandoltCStep {
eyeToTest = visionStep.eyeToTest
testType = visionStep.testType
}
visionStepView = ORKLandoltCStepView(testType: testType)
super.init(step: step)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override var result: ORKStepResult? {
let stepResult = super.result
stepResult?.results = results.copy() as? [ORKResult]
return stepResult!
}
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
activeStepView?.customContentView = visionStepView
activeStepView?.removeCustomContentPadding()
activeStepView?.customContentFillsAvailableSpace = true
// TODO: Localize
visionStepView.currentEyeLabel.text = eyeToTest == .left ? "Left Eye" : "Right Eye"
visionStepView.continueButton.addTarget(self, action: #selector(continueButtonWasPressed), for: .touchUpInside)
startTimer()
}
override public func stepDidFinish() {
super.stepDidFinish()
goForward()
}
private func startTimer() {
activityTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(hideCircle), userInfo: nil, repeats: false)
}
@objc
private func hideCircle() {
activityTimer.invalidate()
visionStepView.visionContentView?.eyeActivitySlider?.hideLetter()
visionStepView.topInstructionLabel.isHidden = false
}
@objc
private func continueButtonWasPressed() {
activityTimer.invalidate()
visionStepView.topInstructionLabel.isHidden = true
visionStepView.continueButton.isEnabled = false
if let resultData = visionStepView.visionContentView?.eyeActivitySlider?.fetchResultDataAndUpdateSlider() {
let stepResult: ORKLandoltCResult = ORKLandoltCResult(identifier: step!.identifier,
outcome: resultData.outcome,
letterAngle: resultData.letterAngle,
sliderAngle: resultData.sliderAngle,
score: resultData.score)
results.add(stepResult)
if resultData.incorrectAnswers == 2 || resultData.score == resultData.maxScore {
stepDidFinish()
} else {
visionStepView.continueButton.isEnabled = true
startTimer()
}
}
}
}
public class ORKLandoltCStepView: UIView {
var visionContentView: ORKLandoltCStepContentView?
let continueButtonCornerRadius: CGFloat = 12.0
let eyeLabelTopPadding: CGFloat = 20.0
let instructionLabelTopPadding: CGFloat = 15.0
let visionContentTopPadding: CGFloat = 10.0
let continueButton = ORKRoundTappingButton()
let currentEyeLabel = UILabel()
let topInstructionLabel = UILabel()
init(testType: VisionStepType!) {
super.init(frame: .zero)
setupCurrentEyeLabel()
setupTopInstructionLabel()
setupVisionContentView(testType: testType)
setupContinueButton()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupVisionContentView(testType: VisionStepType!) {
if visionContentView == nil {
visionContentView = ORKLandoltCStepContentView(testType: testType)
}
addSubview(visionContentView!)
}
func setupCurrentEyeLabel() {
currentEyeLabel.isHidden = true
currentEyeLabel.textAlignment = .center
currentEyeLabel.textColor = UIColor.black
currentEyeLabel.numberOfLines = 0
// TODO: set FontDescriptor
currentEyeLabel.font = UIFont(name: "", size: 20.0)
addSubview(currentEyeLabel)
}
func setupTopInstructionLabel() {
topInstructionLabel.textAlignment = .center
topInstructionLabel.numberOfLines = 0
topInstructionLabel.textColor = UIColor.black
// TODO: Localize
topInstructionLabel.text = "Move the dial to where you think the opening in the letter was."
// TODO: set FontDescriptor
topInstructionLabel.font = UIFont(name: "", size: 20.0)
topInstructionLabel.isHidden = true
addSubview(topInstructionLabel)
}
func setupContinueButton() {
// TODO: Localize
continueButton.diameter = 60.0
continueButton.setTitle("Next", for: UIControl.State.normal)
continueButton.backgroundColor = tintColor
continueButton.layer.cornerRadius = continueButtonCornerRadius
addSubview(continueButton)
}
private func setupConstraints() {
currentEyeLabel.translatesAutoresizingMaskIntoConstraints = false
topInstructionLabel.translatesAutoresizingMaskIntoConstraints = false
visionContentView?.translatesAutoresizingMaskIntoConstraints = false
continueButton.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
NSLayoutConstraint(item: currentEyeLabel,
attribute: .top,
relatedBy: .equal,
toItem: self,
attribute: .top,
multiplier: 1.0,
constant: eyeLabelTopPadding),
NSLayoutConstraint(item: currentEyeLabel,
attribute: .centerX,
relatedBy: .equal,
toItem: self,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0),
NSLayoutConstraint(item: topInstructionLabel,
attribute: .top,
relatedBy: .equal,
toItem: currentEyeLabel,
attribute: .bottom,
multiplier: 1.0,
constant: instructionLabelTopPadding),
NSLayoutConstraint(item: topInstructionLabel,
attribute: .centerX,
relatedBy: .equal,
toItem: self,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0),
NSLayoutConstraint(item: topInstructionLabel,
attribute: .width,
relatedBy: .equal,
toItem: self,
attribute: .width,
multiplier: 0.8,
constant: 0.0),
NSLayoutConstraint(item: visionContentView!,
attribute: .top,
relatedBy: .equal,
toItem: topInstructionLabel,
attribute: .bottom,
multiplier: 1.0,
constant: visionContentTopPadding),
NSLayoutConstraint(item: visionContentView!,
attribute: .centerX,
relatedBy: .equal,
toItem: self,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0),
NSLayoutConstraint(item: visionContentView!,
attribute: .width,
relatedBy: .equal,
toItem: self,
attribute: .width,
multiplier: 0.8,
constant: 0.0),
NSLayoutConstraint(item: visionContentView!,
attribute: .height,
relatedBy: .equal,
toItem: self,
attribute: .height,
multiplier: 0.8,
constant: 0.0),
NSLayoutConstraint(item: continueButton,
attribute: .centerX,
relatedBy: .equal,
toItem: self,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0),
NSLayoutConstraint(item: continueButton,
attribute: .bottom,
relatedBy: .equal,
toItem: self,
attribute: .bottom,
multiplier: 1.0,
constant: -20.0)
]
NSLayoutConstraint.activate(constraints)
}
}
+28 -15
View File
@@ -71,8 +71,13 @@
return @"location";
}
// Test Seam - unit tests don't support background updates or pausing.
- (CLLocationManager *)createLocationManager {
return [[CLLocationManager alloc] init];
CLLocationManager *manager = [[CLLocationManager alloc] init];
manager.pausesLocationUpdatesAutomatically = NO;
manager.allowsBackgroundLocationUpdates = YES;
return manager;
}
- (void)start {
@@ -88,20 +93,19 @@
}
self.locationManager = [self createLocationManager];
if ([CLLocationManager authorizationStatus] <= kCLAuthorizationStatusDenied) {
CLAuthorizationStatus status = kCLAuthorizationStatusNotDetermined;
if (@available(iOS 14.0, *)) {
status = self.locationManager.authorizationStatus;
} else {
status = [CLLocationManager authorizationStatus];
}
if (status == kCLAuthorizationStatusRestricted || status == kCLAuthorizationStatusNotDetermined) {
[self.locationManager requestWhenInUseAuthorization];
}
self.locationManager.pausesLocationUpdatesAutomatically = NO;
self.locationManager.delegate = self;
if (!self.locationManager) {
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSFeatureUnsupportedError
userInfo:@{@"recorder": self}];
[self finishRecordingWithError:error];
return;
}
self.uptime = [NSProcessInfo processInfo].systemUptime;
[self.locationManager startUpdatingLocation];
}
@@ -129,7 +133,8 @@
}
- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations {
didUpdateLocations:(NSArray<CLLocation *> *)locations {
BOOL success = YES;
NSParameterAssert(locations.count >= 0);
NSError *error = nil;
@@ -156,7 +161,15 @@
}
- (BOOL)isRecording {
return [CLLocationManager locationServicesEnabled] && (self.locationManager != nil) && ([CLLocationManager authorizationStatus] > kCLAuthorizationStatusDenied);
CLAuthorizationStatus status = kCLAuthorizationStatusNotDetermined;
if (@available(iOS 14.0, *)) {
status = self.locationManager.authorizationStatus;
} else {
status = [CLLocationManager authorizationStatus];
}
return [CLLocationManager locationServicesEnabled] && (self.locationManager != nil) && (status > kCLAuthorizationStatusDenied);
}
- (void)reset {
@@ -0,0 +1,58 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. 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;
#import "ORKCustomStepView_Internal.h"
#import "ORKNormalizedReactionTimeStimulusView.h"
#import "ORKRoundTappingButton.h"
NS_ASSUME_NONNULL_BEGIN
@interface ORKNormalizedReactionTimeContentView : ORKActiveStepCustomView
@property (nonatomic) ORKRoundTappingButton *button;
- (void)setStimulusHidden:(BOOL)hidden;
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void (^)(void))completion;
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void (^)(void))completion;
- (void)resetAfterDelay:(NSTimeInterval)delay completion:(nullable void (^)(void))completion;
- (UIView *)getBackgroundView;
- (ORKNormalizedReactionTimeStimulusView *)getStimulusView;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,213 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. 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 "ORKNormalizedReactionTimeContentView.h"
#import "ORKNavigationContainerView.h"
#import "ORKNormalizedReactionTimeStimulusView.h"
#import "ORKSkin.h"
#import "ORKHelpers_Internal.h"
CGFloat NormalizeButtonSize = 100.0;
CGFloat BackgroundViewSpaceMultiplier = 2.0;
@implementation ORKNormalizedReactionTimeContentView {
ORKNormalizedReactionTimeStimulusView *_stimulusView;
UIView *_backgroundView;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.translatesAutoresizingMaskIntoConstraints = NO;
[self resizeConstraints];
[self addStimulusView];
[self addBackgroundView];
[self addButton];
}
return self;
}
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(void (^)(void))completion {
[_stimulusView startSuccessAnimationWithDuration:duration completion:completion];
}
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(void (^)(void))completion {
[_stimulusView startFailureAnimationWithDuration:duration completion:completion];
}
- (void)resetAfterDelay:(NSTimeInterval)delay completion:(nullable void (^)(void))completion {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
_stimulusView.hidden = YES;
if (completion) {
completion();
}
});
}
-(void)resizeConstraints {
ORKScreenType screenType = ORKGetVerticalScreenTypeForWindow([[[UIApplication sharedApplication] delegate] window]);
if (screenType == ORKScreenTypeiPhone5 ) {
NormalizeButtonSize = 70.0;
BackgroundViewSpaceMultiplier = 1.75;
}
}
-(void)addButton {
_button = [ORKRoundTappingButton new];
_button.translatesAutoresizingMaskIntoConstraints = NO;
[_button setTitle: ORKLocalizedString(@"REACTION_TIME_TASK_NORM_BUTTON_TITLE", nil) forState:UIControlStateNormal];
[_button setDiameter:NormalizeButtonSize];
[self addSubview:_button];
[NSLayoutConstraint activateConstraints: @[
[NSLayoutConstraint constraintWithItem:_button
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_button
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_backgroundView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:1.0],
]];
}
- (void)addStimulusView {
if (!_stimulusView) {
_stimulusView = [ORKNormalizedReactionTimeStimulusView new];
_stimulusView.translatesAutoresizingMaskIntoConstraints = NO;
_stimulusView.backgroundColor = self.tintColor;
[self addSubview:_stimulusView];
[self setUpStimulusViewConstraints];
}
}
- (void)addBackgroundView {
if (!_backgroundView) {
_backgroundView = [UIView new];
}
_backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
_backgroundView.layer.borderWidth = 3.0;
_backgroundView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:0.3];
_backgroundView.layer.borderColor = [UIColor lightGrayColor].CGColor;
[self insertSubview:_backgroundView belowSubview:_stimulusView];
[self setupBackgroundViewConstraints];
}
- (UIView *)getBackgroundView {
return _backgroundView;
}
- (ORKNormalizedReactionTimeStimulusView *)getStimulusView {
return _stimulusView;
}
- (void)setStimulusHidden:(BOOL)hidden {
_stimulusView.hidden = hidden;
}
- (void)setUpStimulusViewConstraints {
NSMutableArray *constraints = [NSMutableArray array];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_stimulusView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_stimulusView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0.0]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_stimulusView]-(>=0)-|"
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:NSDictionaryOfVariableBindings(_stimulusView)]];
[NSLayoutConstraint activateConstraints:constraints];
}
- (void)setupBackgroundViewConstraints {
NSMutableArray *constraints = [NSMutableArray array];
[constraints addObjectsFromArray:@[
[NSLayoutConstraint constraintWithItem:_backgroundView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:_stimulusView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_backgroundView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:_stimulusView
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_backgroundView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:_stimulusView
attribute:NSLayoutAttributeWidth
multiplier:BackgroundViewSpaceMultiplier
constant:0.0],
[NSLayoutConstraint constraintWithItem:_backgroundView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_stimulusView
attribute:NSLayoutAttributeHeight
multiplier:BackgroundViewSpaceMultiplier
constant:0.0],
]];
[NSLayoutConstraint activateConstraints:constraints];
}
@end

Some files were not shown because too many files have changed in this diff Show More