Compare commits
399 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29950b62e4 | |||
| 65de4b333c | |||
| 63fcf4918f | |||
| c9880b0139 | |||
| 13eabb7720 | |||
| 743b773ea3 | |||
| 249eee5dfb | |||
| 90c68d0d19 | |||
| d10a427911 | |||
| 05755a3213 | |||
| e18a633de1 | |||
| 0e68cdf744 | |||
| 7f119a8d0d | |||
| fde1e7e957 | |||
| 0ad96d505c | |||
| d4ff76fc25 | |||
| 85c1395361 | |||
| 1443e57c57 | |||
| 3b75f6213c | |||
| e9d5de64a5 | |||
| 19c61383f6 | |||
| cb6cc97fa4 | |||
| 0651bf0c2a | |||
| 68f9d29b66 | |||
| 24afb1b1ec | |||
| 47f44c488e | |||
| a9b64253f1 | |||
| 506dc646ad | |||
| b50b759c7e | |||
| ed2602e523 | |||
| bbe376cddc | |||
| 50a2b3427a | |||
| ac7c63fc1b | |||
| b50e1d7aa4 | |||
| 454e501cb6 | |||
| 69cb1b7f2e | |||
| 7c8917deb0 | |||
| e5e96b8db9 | |||
| faffea0008 | |||
| e08711ff4b | |||
| c6c99a41db | |||
| ff709e01f2 | |||
| 666bf54dd5 | |||
| fbcd6adc9e | |||
| 2dc3cfb109 | |||
| ae9b9e57cb | |||
| e19dfbff8d | |||
| 6fc2e6d0fe | |||
| 456c3e4a45 | |||
| eaca80f55a | |||
| d60ab56972 | |||
| 6b55e5ba81 | |||
| 8c16ee46cc | |||
| d56e6d5267 | |||
| 1ff84f4f96 | |||
| 3efb3ae1ae | |||
| d516d0cc0a | |||
| d2794890f8 | |||
| ae70457c56 | |||
| 800e45a27a | |||
| 7b6dae062c | |||
| 5070325d9a | |||
| 84511204d2 | |||
| ad8129efdf | |||
| 5f0f32f824 | |||
| 7347a99520 | |||
| 12b2df63b4 | |||
| 00ac394379 | |||
| 8f8cc4de14 | |||
| 577f612778 | |||
| e0f092decc | |||
| acacacdcce | |||
| fce7172bac | |||
| 264d51f1c9 | |||
| 6b1dd7f73d | |||
| ad5b4cb2fd | |||
| 1faa99a490 | |||
| cf67d09e1b | |||
| 14a87b2067 | |||
| 4f59976114 | |||
| 392dd87881 | |||
| fd8c973004 | |||
| ab400a203a | |||
| 7fbd650fbb | |||
| 01cc7ed79d | |||
| 62d5f17c75 | |||
| d198a8fa67 | |||
| 77aa2444a2 | |||
| 281b2093b1 | |||
| 94c2b68e53 | |||
| 99e3b370d5 | |||
| 093513f01f | |||
| 5e39fffe0d | |||
| 0ddd16b202 | |||
| 13fdd71613 | |||
| d000f5b45e | |||
| 19de603340 | |||
| 61af3f9b19 | |||
| f614fda7b3 | |||
| 63fb484f22 | |||
| 6e61d4f79b | |||
| 54feccdcff | |||
| 5bc0088f16 | |||
| e4f7516b6f | |||
| 61290e42fa | |||
| 82720f2978 | |||
| 08af95d3a7 | |||
| 319b9f189e | |||
| d6f71e2cdb | |||
| 92c2299a9d | |||
| ddb2c50aba | |||
| 4d7c27a1dd | |||
| 2e0bef5490 | |||
| 222855fabc | |||
| 97a503cf18 | |||
| 15b6204a9e | |||
| 6f883b4acb | |||
| 762d0350c4 | |||
| ea01dbdbc3 | |||
| 363a1f7caa | |||
| 81b945a129 | |||
| 16a5da4566 | |||
| 1c2af3c669 | |||
| b412f47578 | |||
| d9cfb03dc6 | |||
| 4baa86df01 | |||
| c0c512b9ad | |||
| 95d82db7cf | |||
| 66dbac3bb7 | |||
| d969d55ab1 | |||
| 95e8250194 | |||
| 2539157e47 | |||
| a51b85622a | |||
| 1e25391b0a | |||
| 9cc6255b45 | |||
| 112bb1b7fa | |||
| 041d015dbf | |||
| 877edca1e6 | |||
| c7b2c33a29 | |||
| ca1a18978f | |||
| 0a962c6822 | |||
| f07ae4b8f0 | |||
| b0d7eff709 | |||
| 390c02d2a8 | |||
| 233142920f | |||
| a4a147332d | |||
| f1fdddf5d4 | |||
| 489de1d8aa | |||
| 52578e21fe | |||
| 92fc374b46 | |||
| c87b189f8a | |||
| b7cf4bc918 | |||
| b803c3aab0 | |||
| 3094ba675c | |||
| 88b7c9a68f | |||
| 06e1e9bbfc | |||
| 36b8a72056 | |||
| 111b983716 | |||
| 599a723dd1 | |||
| 7f37428426 | |||
| 630a431299 | |||
| ab081e28e1 | |||
| 8b912b7d25 | |||
| 375e1380d7 | |||
| fdd3d7c653 | |||
| 18ea1236e2 | |||
| 18842a0e64 | |||
| 7a3d0606d5 | |||
| 528186535c | |||
| bd4c1a1568 | |||
| 08382cf7a3 | |||
| 1080e4f08d | |||
| 81c9fd83e3 | |||
| f65b22993b | |||
| e9059ee42c | |||
| f4feaffe19 | |||
| 176da46dd4 | |||
| e4a023713d | |||
| 7cfc964143 | |||
| b4b363cac6 | |||
| 3deaee1ef0 | |||
| 330587f6f8 | |||
| 7615ed631d | |||
| 567c1802ba | |||
| 400ba05d8e | |||
| 3cd8cd7661 | |||
| 612b411dc2 | |||
| cd216ab1c0 | |||
| 610d30fb0a | |||
| f81825bb78 | |||
| eb5c34b35a | |||
| 7ea163bc14 | |||
| acc45033b7 | |||
| aa5bfa8142 | |||
| 070950b31a | |||
| 981c1af7a8 | |||
| 2cbf211613 | |||
| 77de9ddf05 | |||
| 1dfca472e2 | |||
| 4b9dc94927 | |||
| f3eec95c98 | |||
| 005b4b744f | |||
| 002939a0ec | |||
| 35e5d5aa95 | |||
| 547e1b5cd2 | |||
| 421e90615f | |||
| 3538a50e55 | |||
| 30a758e2b7 | |||
| 9ef1876c45 | |||
| b06ffbb44e | |||
| 60076032b9 | |||
| c2b664f3b8 | |||
| fc61990e03 | |||
| ca81287311 | |||
| db995e3b35 | |||
| 1a902eefdd | |||
| 5c3fe6eaef | |||
| a033e1cc5e | |||
| fbb7892ea6 | |||
| 336096f680 | |||
| f3929e0e12 | |||
| 3736cf573e | |||
| 336ab0e21b | |||
| 6459a4a5a0 | |||
| d04c38b4a0 | |||
| 535cec7f08 | |||
| 843895e1e4 | |||
| fe29159d14 | |||
| 7c883966d0 | |||
| 332c8299fa | |||
| 120612cae0 | |||
| dbb525a469 | |||
| 4dea6d8a7b | |||
| c57d3a1e1f | |||
| 77a15aa734 | |||
| aa482452e0 | |||
| f1f322d442 | |||
| 8191a977b7 | |||
| 54c3333b81 | |||
| ad755bb222 | |||
| 920127d096 | |||
| 2696f2377b | |||
| 81fdbdac57 | |||
| dd04625562 | |||
| 98e8c8e09a | |||
| 2da27dddd6 | |||
| e40be2e5f6 | |||
| 79228e8ab7 | |||
| 161149d2df | |||
| a38561bfed | |||
| 74f44435ef | |||
| 66a754d24f | |||
| 258e2623f3 | |||
| 7043316b90 | |||
| fcb980beb8 | |||
| d186e5c6df | |||
| 5822caa980 | |||
| bb55a603db | |||
| a75938f6bb | |||
| 23ba6e97a8 | |||
| 053625a0a1 | |||
| 682cd83213 | |||
| 55a2833c6e | |||
| c6e2df2a6e | |||
| a3b6732406 | |||
| d2fade561b | |||
| ab6600eaf2 | |||
| 815f5bf60a | |||
| e9ecae2720 | |||
| c9f49d9f18 | |||
| 73faa2e59f | |||
| 70889a0671 | |||
| 26101d0068 | |||
| 340c5abdc0 | |||
| 0c121f283b | |||
| f50587eaf4 | |||
| fad50b20e0 | |||
| 99be8240b0 | |||
| b6028a9eef | |||
| fcad24f94b | |||
| 211aa5a118 | |||
| 53842c3231 | |||
| b6ab7414fd | |||
| e56ac6b2e9 | |||
| 0da343ea6f | |||
| 1fc475e826 | |||
| dc0890233f | |||
| 15a8deba02 | |||
| ac27b6d8fa | |||
| 8625fa19d6 | |||
| 6e0a3a5c4a | |||
| 388a20a79b | |||
| ef0c8d0495 | |||
| 97fb686ad4 | |||
| 2c0a6d7761 | |||
| 0070d3ca04 | |||
| 6d0c331aba | |||
| 0d81a9c696 | |||
| 1e7d2c2460 | |||
| 3197a62a3a | |||
| 4cdffbd061 | |||
| fad570f6e9 | |||
| 05e4011f2a | |||
| b28785d160 | |||
| 2b25f66086 | |||
| 54f7a9b53a | |||
| 0d6bd0e3c4 | |||
| ed3d787972 | |||
| 834e992bf0 | |||
| 4a894d2b3c | |||
| 6848c067d0 | |||
| ff3fe765f1 | |||
| ecc6d2b6f9 | |||
| 8f633dcb43 | |||
| a62b853500 | |||
| 0a12a96af5 | |||
| 62bba0b6c9 | |||
| b7c6772266 | |||
| 224ce04a41 | |||
| 05b6f308e3 | |||
| 3f96750424 | |||
| e7e501c087 | |||
| 3aeb8dd9f9 | |||
| d30331a403 | |||
| 1726f5353d | |||
| e3fad9e1d9 | |||
| 9f2e77d103 | |||
| 0478b552dc | |||
| 19ee558b57 | |||
| 8fde58535b | |||
| c424ada8fd | |||
| ba58b4dbad | |||
| 0ee66f6a26 | |||
| 261ef01f93 | |||
| f1083cf096 | |||
| 4b5a587eb8 | |||
| 4bdd148b53 | |||
| 6bad43fb9f | |||
| 34040ac1cd | |||
| 4a58bff1fd | |||
| d8391522db | |||
| aa2fd02c67 | |||
| acf5f194f0 | |||
| b83684fac9 | |||
| 6ea0e4d248 | |||
| f730ed5a00 | |||
| 30e5a09c0c | |||
| 98e2ae73e4 | |||
| 100f744827 | |||
| 9ea558d233 | |||
| d3ceaabce0 | |||
| cf62288080 | |||
| a8e541a462 | |||
| 3c29db9917 | |||
| 2b682cd197 | |||
| cf753bea67 | |||
| a4e1d13bad | |||
| 268af9b05d | |||
| 9b47a271a4 | |||
| 8608143e40 | |||
| 01534209ac | |||
| ce75fc84bb | |||
| 4f1b8e1b89 | |||
| bb6be3bde1 | |||
| 52046a60bc | |||
| aba6fb5889 | |||
| 239ad2d8c9 | |||
| c220e6dfbd | |||
| 5d0cfbf9de | |||
| 644a563d20 | |||
| c49ff60d73 | |||
| 892713bec4 | |||
| 60ca971d8e | |||
| e8f695ee32 | |||
| 7941f76033 | |||
| f871fafed6 | |||
| fbd02ffc59 | |||
| 371c729a48 | |||
| 9f9cbd9fee | |||
| 1341070411 | |||
| ed26df05ae | |||
| 6583215cda | |||
| c77823dfa8 | |||
| ec62232d9f | |||
| a54a266335 | |||
| 734e85ead6 | |||
| 5182776e1f | |||
| 57f896b86f | |||
| abea94060e | |||
| 8a21932d62 | |||
| 07469cf2c1 | |||
| b684c9f2f8 | |||
| acc5ff2ba9 | |||
| 28d431c664 | |||
| 19a3cd7074 | |||
| fe8f0a2234 | |||
| 5592966027 | |||
| 3133dd38df | |||
| bd8ca78ed1 |
@@ -0,0 +1,5 @@
|
||||
language: objective-c
|
||||
osx_image: xcode12
|
||||
xcode_project: ResearchKit.xcodeproj
|
||||
xcode_scheme: ResearchKit
|
||||
xcode_destination: platform=iOS Simulator,OS=14.0,name=iPhone 11 Pro Max
|
||||
+2
-2
@@ -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>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
ResearchKit Framework
|
||||
===========
|
||||
|
||||
    [](https://github.com/ResearchKit/ResearchKit#license) 
|
||||
|
||||
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
|
||||
|
||||
+5
-2
@@ -1,15 +1,18 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ResearchKit'
|
||||
s.version = '2.0.0'
|
||||
s.version = '2.1.0-beta'
|
||||
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
|
||||
|
||||
+1417
-139
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 "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.
|
||||
*/</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1000"
|
||||
version = "1.3">
|
||||
LastUpgradeVersion = "1200"
|
||||
<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,17 +62,6 @@
|
||||
</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"
|
||||
@@ -70,8 +82,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 = "1200"
|
||||
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 = "1200"
|
||||
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"
|
||||
|
||||
@@ -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));
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+3
-3
@@ -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];
|
||||
@@ -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,181 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 *> *)provideResults;
|
||||
|
||||
@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
|
||||
@@ -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 = YES;
|
||||
_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 *> *)provideResults {
|
||||
[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;
|
||||
@@ -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
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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 "ORK3DModelStep.h"
|
||||
#import "ORK3DModelStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@implementation ORK3DModelStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORK3DModelStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier modelManager:(nonnull ORK3DModelManager *)modelManager {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
|
||||
if (self) {
|
||||
_modelManager = modelManager;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
}
|
||||
|
||||
- (BOOL)startsFinished {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (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(aDecoder, modelManager);
|
||||
}
|
||||
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.modelManager, castObject.modelManager));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
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 provideResults];
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,8 +95,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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -28,29 +28,35 @@
|
||||
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 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;
|
||||
|
||||
- (ORKRingView *)ringView;
|
||||
|
||||
- (void)setProgress:(CGFloat)progress;
|
||||
|
||||
- (void)setProgressCircle:(CGFloat)progress;
|
||||
|
||||
- (void)setDBText:(NSString *)text;
|
||||
|
||||
- (void)setThreshold:(double)threshold;
|
||||
|
||||
@property(nonatomic, strong) ORKRingView *ringView;
|
||||
- (void)reachedOptimumNoiseLevel;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -32,27 +32,70 @@
|
||||
#import "ORKEnvironmentSPLMeterContentView.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 CircleIndicatorMaxDiameter = 150.0;
|
||||
static const CGFloat RingViewTopPadding = 24.0;
|
||||
static const CGFloat InstructionLabelTopPadding = 50.0;
|
||||
static const CGFloat InstructionLabelBottomPadding = 10.0;
|
||||
|
||||
static CGFloat CircleIndicatorViewScaleFactorForProgress(CGFloat progress) {
|
||||
|
||||
CGFloat y1 = 0.5, x1 = 0.8, y2 = 1.4, x2 = 1.2;
|
||||
|
||||
if (progress < x1) // lower limit for diameter
|
||||
{
|
||||
return y1;
|
||||
}
|
||||
else if (progress > x2) // upper limit for diameter
|
||||
{
|
||||
return y2;
|
||||
}
|
||||
else // linear interpolation
|
||||
{
|
||||
return y1 + (y2 - y1)/(x2 - x1) * (progress - x1);
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat CircleIndicatorPulseVarianceForProgress(CGFloat progress) {
|
||||
|
||||
// Linear Interpolation
|
||||
// kMin: Lower bound of interpolation. (Matches above)
|
||||
// kMax: Higher bound of interpolation. (Matches above)
|
||||
// min: Lower bound of variance.
|
||||
// max: Higher bound of variance.
|
||||
CGFloat min = 0.0075, max = 0.025;
|
||||
CGFloat kMin = 0.8, kMax = 1.2;
|
||||
|
||||
if (progress < kMin)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
else if (progress > kMax)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
else
|
||||
{
|
||||
return min + (max - min)/(kMax - kMin) * (progress - kMin);
|
||||
}
|
||||
}
|
||||
|
||||
@interface ORKEnvironmentSPLMeterContentView ()
|
||||
@property(nonatomic, strong) ORKRingView *ringView;
|
||||
@end
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterContentView {
|
||||
NSLayoutConstraint *_topToProgressViewConstraint;
|
||||
UIStackView *stackView;
|
||||
UIStackView *miniStackView;
|
||||
UILabel *_dBValueLabel;
|
||||
UILabel *_unitLabel;
|
||||
UILabel *_thresholdLabel;
|
||||
UIView *_circleIndicatorView;
|
||||
UILabel *_DBInstructionLabel;
|
||||
CGFloat preValue;
|
||||
CGFloat currentValue;
|
||||
CAShapeLayer *circle;
|
||||
ORKProgressView *_loadingView;
|
||||
UIProgressView *_progressView;
|
||||
UIColor *_circleIndicatorNoiseColor;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
@@ -60,232 +103,153 @@ static const CGFloat DBLabelFontSize = 35.0;
|
||||
if (self) {
|
||||
preValue = -M_PI_2;
|
||||
currentValue = 0.0;
|
||||
|
||||
_circleIndicatorNoiseColor = UIColor.systemOrangeColor;
|
||||
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 setupRingView];
|
||||
[self setupCircleIndicatorView];
|
||||
[self setProgressCircle:0.0];
|
||||
[self setupDBInstructionLabel];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) setupDBValueLabel {
|
||||
if (!_dBValueLabel) {
|
||||
_dBValueLabel = [UILabel new];
|
||||
- (void)setupRingView {
|
||||
if (!_ringView) {
|
||||
_ringView = [ORKRingView 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 {
|
||||
_ringView.animationDuration = 0.0;
|
||||
_ringView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_ringView];
|
||||
|
||||
[_progressView setProgress:progress animated:animated];
|
||||
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
|
||||
[_progressView setAlpha:(progress == 0) ? 0 : 1];
|
||||
}];
|
||||
[[_ringView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
|
||||
[[_ringView.topAnchor constraintEqualToAnchor:self.topAnchor constant:RingViewTopPadding] setActive:YES];
|
||||
[_ringView setColor:UIColor.grayColor];
|
||||
}
|
||||
|
||||
- (void)setupCircleIndicatorView {
|
||||
if (!_circleIndicatorView) {
|
||||
_circleIndicatorView = [UIView new];
|
||||
}
|
||||
_circleIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self insertSubview:_circleIndicatorView belowSubview:_ringView];
|
||||
|
||||
[[_circleIndicatorView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
|
||||
[[_circleIndicatorView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
|
||||
[[_circleIndicatorView.heightAnchor constraintEqualToConstant:CircleIndicatorMaxDiameter] setActive:YES];
|
||||
[[_circleIndicatorView.widthAnchor constraintEqualToConstant:CircleIndicatorMaxDiameter] setActive:YES];
|
||||
_circleIndicatorView.layer.cornerRadius = CircleIndicatorMaxDiameter * 0.5;
|
||||
}
|
||||
|
||||
- (void)setupDBInstructionLabel {
|
||||
if (!_DBInstructionLabel) {
|
||||
_DBInstructionLabel = [ORKLabel new];
|
||||
_DBInstructionLabel.numberOfLines = 0;
|
||||
_DBInstructionLabel.textColor = UIColor.systemGrayColor;
|
||||
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
|
||||
}
|
||||
_DBInstructionLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_DBInstructionLabel];
|
||||
|
||||
[[_DBInstructionLabel.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
|
||||
[[_DBInstructionLabel.topAnchor constraintEqualToAnchor:_circleIndicatorView.bottomAnchor constant:InstructionLabelTopPadding] setActive:YES];
|
||||
[[_DBInstructionLabel.bottomAnchor constraintLessThanOrEqualToAnchor:self.bottomAnchor constant:-InstructionLabelBottomPadding] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)setProgressCircle:(CGFloat)progress {
|
||||
[_ringView setValue:progress WithColor:progress < 1.0 ? [[UIColor greenColor] colorWithAlphaComponent:0.5] : [[UIColor redColor] colorWithAlphaComponent:0.5]];
|
||||
|
||||
CGFloat circleDiameter = CircleIndicatorViewScaleFactorForProgress(progress);
|
||||
CGFloat variance = CircleIndicatorPulseVarianceForProgress(progress);
|
||||
|
||||
[self startPulsingWithTranformScaleFactor:circleDiameter variance:variance];
|
||||
|
||||
if (progress >= ORKRingViewMaximumValue)
|
||||
{
|
||||
|
||||
[_ringView setBackgroundLayerStrokeColor:[UIColor.whiteColor colorWithAlphaComponent:0.3] circleStrokeColor:UIColor.whiteColor withAnimationDuration:0.8];
|
||||
}
|
||||
else
|
||||
{
|
||||
[_ringView resetLayerColors];
|
||||
}
|
||||
|
||||
[UIView animateWithDuration:0.8
|
||||
delay:0
|
||||
options:UIViewAnimationOptionCurveLinear
|
||||
animations:^{
|
||||
_circleIndicatorView.transform = CGAffineTransformMakeScale(circleDiameter, circleDiameter);
|
||||
_circleIndicatorView.backgroundColor = progress >= ORKRingViewMaximumValue ? _circleIndicatorNoiseColor : self.tintColor;
|
||||
} completion:nil];
|
||||
|
||||
[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)startPulsingWithTranformScaleFactor:(CGFloat)transformScaleFactor variance:(CGFloat)variance {
|
||||
|
||||
[self stopPulsing];
|
||||
|
||||
CAKeyframeAnimation *pulse = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.xy"];
|
||||
pulse.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
||||
pulse.repeatCount = MAXFLOAT;
|
||||
pulse.duration = 0.6;
|
||||
pulse.values = @[
|
||||
@(transformScaleFactor),
|
||||
@(transformScaleFactor * (1 - variance)),
|
||||
@(transformScaleFactor),
|
||||
@(transformScaleFactor * (1 + variance)),
|
||||
@(transformScaleFactor)
|
||||
];
|
||||
|
||||
[_circleIndicatorView.layer addAnimation:pulse forKey:@"pulse"];
|
||||
}
|
||||
|
||||
}
|
||||
if (_dBValueLabel) {
|
||||
[_dBValueLabel setText:[NSString stringWithFormat:@"%@", text]];
|
||||
[_unitLabel setHidden:NO];
|
||||
}
|
||||
- (void)stopPulsing {
|
||||
[_circleIndicatorView.layer removeAnimationForKey:@"pulse"];
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress {
|
||||
CGFloat value = progress < ORKRingViewMinimumValue ? ORKRingViewMinimumValue : progress;
|
||||
[_ringView setValue:value];
|
||||
}
|
||||
|
||||
- (void)updateInstructionForValue:(CGFloat)progress
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
NSString *currentInstruction = [_DBInstructionLabel.text copy];
|
||||
NSString *newInstruction = progress >= ORKRingViewMaximumValue ? ORKLocalizedString(@"ENVIRONMENTSPL_NOISE", nil) : ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
|
||||
|
||||
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 {
|
||||
[self stopPulsing];
|
||||
_ringView.hidden = YES;
|
||||
_circleIndicatorView.hidden = YES;
|
||||
ORKCompletionCheckmarkView *checkmarkView = [[ORKCompletionCheckmarkView alloc] initWithDimension:_ringView.bounds.size.width];
|
||||
checkmarkView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self insertSubview:checkmarkView aboveSubview:_ringView];
|
||||
[[checkmarkView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
|
||||
[[checkmarkView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
|
||||
[checkmarkView setAnimationPoint:1 animated:YES];
|
||||
}
|
||||
|
||||
- (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],
|
||||
|
||||
];
|
||||
|
||||
[self addConstraints:constraints];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKEnvironmentSPLMeterResult : ORKResult
|
||||
|
||||
@property (nonatomic, assign) double sensitivityOffset;
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
self.thresholdValue = ORKEnvironmentSPLMeterTaskDefaultThresholdValue;
|
||||
self.samplingInterval = ORKEnvironmentSPLMeterTaskMinimumSamplingInterval;
|
||||
self.requiredContiguousSamples = ORKEnvironmentSPLMeterTaskDefaultRequiredContiguousSamples;
|
||||
self.stepDuration = CGFLOAT_MAX;
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
|
||||
@@ -32,8 +32,11 @@
|
||||
#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"
|
||||
@@ -41,12 +44,14 @@
|
||||
#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 () {
|
||||
@interface ORKEnvironmentSPLMeterStepViewController ()<ORKRingViewDelegate, ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> {
|
||||
AVAudioEngine *_audioEngine;
|
||||
AVAudioInputNode *_inputNode;
|
||||
AVAudioUnitEQ *_eqUnit;
|
||||
@@ -64,6 +69,11 @@
|
||||
NSInteger _requiredContiguousSamples;
|
||||
int _counter;
|
||||
NSMutableArray *_recordedSamples;
|
||||
AVAudioSessionCategory _savedSessionCategory;
|
||||
AVAudioSessionMode _savedSessionMode;
|
||||
AVAudioSessionCategoryOptions _savedSessionCategoryOptions;
|
||||
UINotificationFeedbackGenerator *_notificationFeedbackGenerator;
|
||||
dispatch_semaphore_t _voiceOverAnnouncementSemaphore;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) ORKEnvironmentSPLMeterContentView *environmentSPLMeterContentView;
|
||||
@@ -90,21 +100,16 @@
|
||||
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.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
[self requestMicrophoneAuthorization];
|
||||
[self configureAudioSession];
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
@@ -117,7 +122,25 @@
|
||||
[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,14 +151,12 @@
|
||||
_samplingInterval = [self environmentSPLMeterStep].samplingInterval;
|
||||
_requiredContiguousSamples = [self environmentSPLMeterStep].requiredContiguousSamples;
|
||||
_thresholdValue = [self environmentSPLMeterStep].thresholdValue;
|
||||
[_environmentSPLMeterContentView setThreshold:_thresholdValue];
|
||||
[self splWorkBlock];
|
||||
|
||||
}
|
||||
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
[self resetAudioSession];
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
@@ -179,10 +200,36 @@
|
||||
|
||||
- (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];
|
||||
|
||||
// Stop any existing audio
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategorySoloAmbient error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
// Force input/output from iOS device
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
// Override Output (and Input) to use built-in mic and speaker.
|
||||
// We need to make sure audio output is to the Headphones and Audio Input is uing the built-in mic.
|
||||
// Although this forces both to the built-in mic AND Speaker, we need to also override the speaker.
|
||||
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
|
||||
if (error)
|
||||
{
|
||||
ORK_Log_Error("Setting AVAudioSessionPortOverrideSpeaker failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,9 +276,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,7 +317,6 @@
|
||||
[_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];
|
||||
@@ -275,14 +326,13 @@
|
||||
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 +343,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 {
|
||||
[self resetAudioSession];
|
||||
[_audioEngine stop];
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
[super stepDidFinish];
|
||||
|
||||
[self.environmentSPLMeterContentView finishStep:self];
|
||||
[self resetAudioSession];
|
||||
[self goForward];
|
||||
}
|
||||
|
||||
@@ -340,5 +400,37 @@
|
||||
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
|
||||
{
|
||||
[_notificationFeedbackGenerator notificationOccurred:eventType];
|
||||
[_notificationFeedbackGenerator prepare];
|
||||
}
|
||||
|
||||
#pragma mark - ORKEnvironmentSPLMeterContentViewVoiceOverDelegate
|
||||
|
||||
- (void)contentView:(ORKEnvironmentSPLMeterContentView *)contentView shouldAnnounce:(NSString *)inAnnouncement
|
||||
{
|
||||
if ([_audioEngine isRunning] == NO)
|
||||
{
|
||||
// Only make this announcement if the audio engine is not running.
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, inAnnouncement);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
constant:CGFLOAT_MIN];
|
||||
imageSpacerHeightConstraint.priority = UILayoutPriorityDefaultLow - 1;
|
||||
[constraints addObject:imageSpacerHeightConstraint];
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
_hrFormatter = [[NSNumberFormatter alloc] init];
|
||||
_hrFormatter.numberStyle = kCFNumberFormatterNoStyle;
|
||||
_hrFormatter.numberStyle = NSNumberFormatterNoStyle;
|
||||
_contentView.timeLeft = self.fitnessStep.stepDuration;
|
||||
|
||||
}
|
||||
@@ -82,10 +82,8 @@
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
_contentView = [ORKFitnessContentView new];
|
||||
_contentView.image = self.fitnessStep.image;
|
||||
_contentView.timeLeft = self.fitnessStep.stepDuration;
|
||||
self.activeStepView.activeCustomView = _contentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
}
|
||||
|
||||
- (void)updateHeartRateWithQuantity:(HKQuantitySample *)quantity unit:(HKUnit *)unit {
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
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 "ORKFrontFacingCameraStepResult.h"
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@implementation ORKFrontFacingCameraStepResult
|
||||
|
||||
- (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,380 @@
|
||||
/*
|
||||
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.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;
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -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,106 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,16 @@
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -156,7 +165,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
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright (c) 2015, 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/ORKResult.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKFileResult;
|
||||
|
||||
/**
|
||||
The `ORKReactionTimeResult` class represents the result of a single successful attempt within an ORKReactionTimeStep.
|
||||
|
||||
The `timestamp` property is equal to the value of systemUptime (in NSProcessInfo) when the stimulus occurred.
|
||||
Each entry of motion data in this file contains a time interval which may be directly compared to timestamp in order to determine the elapsed time since the stimulus.
|
||||
|
||||
The fileResult property references the motion data recorded from the beginning of the attempt until the threshold acceleration was reached.
|
||||
Using the time taken to reach the threshold acceleration as the reaction time of a participant will yield a rather crude measurement. Rather, you should devise your own method using the data recorded to obtain an accurate approximation of the true reaction time.
|
||||
|
||||
A reaction time result is typically generated by the framework as the task proceeds. When the task
|
||||
completes, it may be appropriate to serialize the sample for transmission to a server
|
||||
or to immediately perform analysis on it.
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKNormalizedReactionTimeResult: ORKResult
|
||||
|
||||
@property (nonatomic, copy) NSDate * timerStartDate;
|
||||
@property (nonatomic, copy) NSDate * timerEndDate;
|
||||
@property (nonatomic, copy, nullable) NSDate * stimulusStartDate;
|
||||
@property (nonatomic, copy, nullable) NSDate * reactionDate;
|
||||
@property (nonatomic) NSNumber *currentInterval;
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright (c) 2015, 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 "ORKNormalizedReactionTimeResult.h"
|
||||
|
||||
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKNormalizedReactionTimeResult
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, timerStartDate);
|
||||
ORK_ENCODE_OBJ(aCoder, timerEndDate);
|
||||
ORK_ENCODE_OBJ(aCoder, stimulusStartDate);
|
||||
ORK_ENCODE_OBJ(aCoder, reactionDate);
|
||||
ORK_ENCODE_OBJ(aCoder, currentInterval);
|
||||
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, timerStartDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, timerEndDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, stimulusStartDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, reactionDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, currentInterval, NSNumber);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualObjects(self.timerStartDate, castObject.timerStartDate) &&
|
||||
ORKEqualObjects(self.timerEndDate, castObject.timerEndDate) &&
|
||||
ORKEqualObjects(self.stimulusStartDate, castObject.stimulusStartDate) &&
|
||||
ORKEqualObjects(self.reactionDate, castObject.reactionDate) &&
|
||||
ORKEqualObjects(self.currentInterval, castObject.currentInterval)) ;
|
||||
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return super.hash ^ _timerStartDate.hash ^ _timerEndDate.hash ^ _stimulusStartDate.hash ^ _reactionDate.hash;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKNormalizedReactionTimeResult *result = [super copyWithZone:zone];
|
||||
result.timerStartDate = [self.timerStartDate copy];
|
||||
result.timerEndDate = [self.timerEndDate copy];
|
||||
result.stimulusStartDate = [self.stimulusStartDate copy];
|
||||
result.reactionDate = [self.reactionDate copy];
|
||||
result.currentInterval = [self.currentInterval copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 Foundation;
|
||||
@import AudioToolbox;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKNormalizedReactionTimeStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval maximumStimulusInterval;
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval minimumStimulusInterval;
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval timeout;
|
||||
|
||||
@property (nonatomic, assign) NSInteger numberOfAttempts;
|
||||
|
||||
@property (nonatomic, assign) double thresholdAcceleration;
|
||||
|
||||
@property (nonatomic, assign) SystemSoundID successSound;
|
||||
|
||||
@property (nonatomic, assign) SystemSoundID timeoutSound;
|
||||
|
||||
@property (nonatomic, assign) SystemSoundID failureSound;
|
||||
|
||||
@property (nonatomic) NSNumber *currentInterval;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
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 "ORKNormalizedReactionTimeStep.h"
|
||||
|
||||
#import "ORKNormalizedReactionTimeViewController.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKNormalizedReactionTimeStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORKNormalizedReactionTimeViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
self.shouldContinueOnFinish = YES;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKNormalizedReactionTimeStep *step = [super copyWithZone:zone];
|
||||
step.maximumStimulusInterval = self.maximumStimulusInterval;
|
||||
step.minimumStimulusInterval = self.minimumStimulusInterval;
|
||||
step.thresholdAcceleration = self.thresholdAcceleration;
|
||||
step.timeout = self.timeout;
|
||||
step.numberOfAttempts = self.numberOfAttempts;
|
||||
step.successSound = self.successSound;
|
||||
step.timeoutSound = self.timeoutSound;
|
||||
step.failureSound = self.failureSound;
|
||||
self.currentInterval = self.currentInterval;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
|
||||
if (self.minimumStimulusInterval <= 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"minimumStimulusInterval must be greater than zero"
|
||||
userInfo:nil];
|
||||
}
|
||||
if (self.maximumStimulusInterval < self.minimumStimulusInterval) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"maximumStimulusInterval cannot be less than minimumStimulusInterval"
|
||||
userInfo:nil];
|
||||
}
|
||||
if (self.thresholdAcceleration <= 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"thresholdAcceleration must be greater than zero"
|
||||
userInfo:nil];
|
||||
}
|
||||
if (self.timeout <= 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"timeout must be greater than zero"
|
||||
userInfo:nil];
|
||||
}
|
||||
if (self.numberOfAttempts <= 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"numberOfAttempts must be greater than zero"
|
||||
userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_DOUBLE(aDecoder, maximumStimulusInterval);
|
||||
ORK_DECODE_DOUBLE(aDecoder, minimumStimulusInterval);
|
||||
ORK_DECODE_DOUBLE(aDecoder, thresholdAcceleration);
|
||||
ORK_DECODE_DOUBLE(aDecoder, timeout);
|
||||
ORK_DECODE_UINT32(aDecoder, successSound);
|
||||
ORK_DECODE_UINT32(aDecoder, timeoutSound);
|
||||
ORK_DECODE_UINT32(aDecoder, failureSound);
|
||||
ORK_DECODE_INTEGER(aDecoder, numberOfAttempts);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, currentInterval, NSNumber);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_DOUBLE(aCoder, maximumStimulusInterval);
|
||||
ORK_ENCODE_DOUBLE(aCoder, minimumStimulusInterval);
|
||||
ORK_ENCODE_DOUBLE(aCoder, thresholdAcceleration);
|
||||
ORK_ENCODE_DOUBLE(aCoder, timeout);
|
||||
ORK_ENCODE_UINT32(aCoder, successSound);
|
||||
ORK_ENCODE_UINT32(aCoder, timeoutSound);
|
||||
ORK_ENCODE_UINT32(aCoder, failureSound);
|
||||
ORK_ENCODE_INTEGER(aCoder, numberOfAttempts);
|
||||
ORK_ENCODE_OBJ(aCoder, currentInterval);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
(self.maximumStimulusInterval == castObject.maximumStimulusInterval) &&
|
||||
(self.minimumStimulusInterval == castObject.minimumStimulusInterval) &&
|
||||
(self.thresholdAcceleration == castObject.thresholdAcceleration) &&
|
||||
(self.timeout == castObject.timeout) &&
|
||||
(self.successSound == castObject.successSound) &&
|
||||
(self.timeoutSound == castObject.timeoutSound) &&
|
||||
(self.failureSound == castObject.failureSound) &&
|
||||
(self.numberOfAttempts == castObject.numberOfAttempts) &&
|
||||
(self.currentInterval == castObject.currentInterval)
|
||||
);}
|
||||
|
||||
- (BOOL)allowsBackNavigation {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)startsFinished {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKNormalizedReactionTimeStimulusView : UIView
|
||||
|
||||
- (void)reset;
|
||||
|
||||
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void(^)(void))completion;
|
||||
|
||||
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void(^)(void))completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
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 "ORKNormalizedReactionTimeStimulusView.h"
|
||||
|
||||
|
||||
@implementation ORKNormalizedReactionTimeStimulusView {
|
||||
CAShapeLayer *_tickLayer;
|
||||
CAShapeLayer *_crossLayer;
|
||||
}
|
||||
|
||||
static const CGFloat RoundReactionTimeViewDiameter = 122;
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.layer.cornerRadius = RoundReactionTimeViewDiameter * 0.5;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize {
|
||||
return CGSizeMake(RoundReactionTimeViewDiameter, RoundReactionTimeViewDiameter);
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
[_tickLayer removeFromSuperlayer];
|
||||
[_crossLayer removeFromSuperlayer];
|
||||
_tickLayer = nil;
|
||||
_crossLayer = nil;
|
||||
self.layer.backgroundColor = self.tintColor.CGColor;
|
||||
}
|
||||
|
||||
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(void(^)(void))completion {
|
||||
if (self.hidden) {
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
[self addTickLayer];
|
||||
[CATransaction begin];
|
||||
[CATransaction setCompletionBlock:completion];
|
||||
CAMediaTimingFunction *timing = [[CAMediaTimingFunction alloc] initWithControlPoints:0.180739998817444 :0 :0.577960014343262 :0.918200016021729];
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
|
||||
[animation setTimingFunction:timing];
|
||||
animation.removedOnCompletion = NO;
|
||||
[animation setFillMode:kCAFillModeForwards];
|
||||
animation.fromValue = @(0);
|
||||
animation.toValue = @(1);
|
||||
animation.duration = duration;
|
||||
[_tickLayer addAnimation:animation forKey:@"strokeEnd"];
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(void(^)(void))completion {
|
||||
self.hidden = NO;
|
||||
|
||||
self.layer.backgroundColor = [UIColor clearColor].CGColor;
|
||||
[self addCrossLayer];
|
||||
[CATransaction begin];
|
||||
[CATransaction setCompletionBlock:completion];
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
|
||||
[animation setFillMode:kCAFillModeForwards];
|
||||
animation.fromValue = @([(CAShapeLayer *)[_crossLayer presentationLayer] strokeEnd]);
|
||||
animation.toValue = @(1);
|
||||
animation.duration = duration;
|
||||
_crossLayer.strokeEnd = 1;
|
||||
[_crossLayer addAnimation:animation forKey:@"strokeEnd"];
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden {
|
||||
[self reset];
|
||||
[super setHidden:hidden];
|
||||
}
|
||||
|
||||
- (void)addCrossLayer {
|
||||
_crossLayer = [self lineDrawingLayer];
|
||||
_crossLayer.strokeColor = [UIColor redColor].CGColor;
|
||||
_crossLayer.path = [self crossPath];
|
||||
[self.layer addSublayer:_crossLayer];
|
||||
}
|
||||
|
||||
- (void)addTickLayer {
|
||||
_tickLayer = [self lineDrawingLayer];
|
||||
_tickLayer.strokeColor = [UIColor whiteColor].CGColor;
|
||||
_tickLayer.path = [self tickPath];
|
||||
[self.layer addSublayer:_tickLayer];
|
||||
}
|
||||
|
||||
- (CGPathRef)concealPath:(CGFloat)radius {
|
||||
return [[UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
|
||||
radius:radius / 2
|
||||
startAngle:M_PI + M_PI_2
|
||||
endAngle:-M_PI_2
|
||||
clockwise:NO] CGPath];
|
||||
}
|
||||
|
||||
- (CGPathRef)tickPath {
|
||||
UIBezierPath *path = [self linePath];
|
||||
[path moveToPoint:(CGPoint){37,65}];
|
||||
[path addLineToPoint:(CGPoint){50,78}];
|
||||
[path addLineToPoint:(CGPoint){87,42}];
|
||||
return path.CGPath;
|
||||
}
|
||||
|
||||
- (CGPathRef)crossPath {
|
||||
UIBezierPath *path = [self linePath];
|
||||
[path moveToPoint:(CGPoint){45,78}];
|
||||
[path addLineToPoint:(CGPoint){82,42}];
|
||||
[path moveToPoint:(CGPoint){45,42}];
|
||||
[path addLineToPoint:(CGPoint){82,78}];
|
||||
return path.CGPath;
|
||||
}
|
||||
|
||||
- (UIBezierPath *)linePath {
|
||||
UIBezierPath *path = [UIBezierPath new];
|
||||
path.lineCapStyle = kCGLineCapRound;
|
||||
path.lineWidth = 5;
|
||||
return path;
|
||||
}
|
||||
|
||||
- (CAShapeLayer *)lineDrawingLayer {
|
||||
CAShapeLayer *shapeLayer = [CAShapeLayer new];
|
||||
shapeLayer.strokeEnd = 0;
|
||||
shapeLayer.lineWidth = 5;
|
||||
shapeLayer.lineCap = kCALineCapRound;
|
||||
shapeLayer.lineJoin = kCALineJoinRound;
|
||||
shapeLayer.frame = self.layer.bounds;
|
||||
shapeLayer.backgroundColor = [UIColor clearColor].CGColor;
|
||||
shapeLayer.fillColor = nil;
|
||||
return shapeLayer;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
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 "ORKDefines.h"
|
||||
#import "ORKActiveStepViewController.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKNormalizedReactionTimeViewController : ORKActiveStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
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 "ORKNormalizedReactionTimeViewController.h"
|
||||
|
||||
#import "ORKBorderedButton.h"
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKNormalizedReactionTimeContentView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKVerticalContainerView_Internal.h"
|
||||
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKNormalizedReactionTimeResult.h"
|
||||
#import "ORKNormalizedReactionTimeStep.h"
|
||||
#import "ORKResult.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import <AudioToolbox/AudioServices.h>
|
||||
|
||||
|
||||
@implementation ORKNormalizedReactionTimeViewController {
|
||||
ORKNormalizedReactionTimeContentView *_reactionTimeContentView;
|
||||
NSMutableArray *_results;
|
||||
NSTimer *_stimulusTimer;
|
||||
NSTimer *_timeoutTimer;
|
||||
NSTimeInterval _stimulusTimestamp;
|
||||
BOOL _validResult;
|
||||
BOOL _timedOut;
|
||||
BOOL _shouldIndicateFailure;
|
||||
|
||||
UIView *_backgroundView;
|
||||
ORKNormalizedReactionTimeStimulusView *_stimulusView;
|
||||
|
||||
NSDate *_timerStartDate;
|
||||
NSDate *_stimulusStartDate;
|
||||
NSDate *_reactionDate;
|
||||
}
|
||||
|
||||
static const NSTimeInterval OutcomeAnimationDuration = 0.3;
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
[self configureTitle];
|
||||
_results = [NSMutableArray new];
|
||||
_reactionTimeContentView = [ORKNormalizedReactionTimeContentView new];
|
||||
[_reactionTimeContentView.button addTarget:self action:@selector(startStimulusTimer) forControlEvents:UIControlEventTouchDown];
|
||||
[_reactionTimeContentView.button addTarget:self action:@selector(startReactionTimer) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
self.activeStepView.activeCustomView = _reactionTimeContentView;
|
||||
|
||||
_backgroundView = [_reactionTimeContentView getBackgroundView];
|
||||
_stimulusView = [_reactionTimeContentView getStimulusView];
|
||||
|
||||
[_backgroundView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected)]];
|
||||
[_stimulusView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected)]];
|
||||
|
||||
[_reactionTimeContentView setStimulusHidden:YES];
|
||||
}
|
||||
|
||||
|
||||
-(void)startReactionTimer {
|
||||
if (_stimulusView.hidden) {
|
||||
_validResult = NO;
|
||||
_timedOut = YES;
|
||||
[self addReactionTimeResult];
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// Device motion recorder won't work, so manually trigger didfinish
|
||||
[self attemptDidFinish];
|
||||
#endif
|
||||
} else {
|
||||
_timerStartDate = [NSDate date];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tapDetected {
|
||||
if ([_stimulusTimer isValid] || [_timeoutTimer isValid]) {
|
||||
_reactionDate = [NSDate date];
|
||||
[self addReactionTimeResult];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
[self start];
|
||||
_shouldIndicateFailure = YES;
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
_shouldIndicateFailure = NO;
|
||||
}
|
||||
|
||||
#pragma mark - ORKActiveStepViewController
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *stepResult = [super result];
|
||||
stepResult.results = [self.addedResults arrayByAddingObjectsFromArray:_results] ? : _results;
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
[super applicationWillResignActive:notification];
|
||||
_validResult = NO;
|
||||
[_stimulusTimer invalidate];
|
||||
[_timeoutTimer invalidate];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||
[super applicationDidBecomeActive:notification];
|
||||
[self resetAfterDelay:0];
|
||||
}
|
||||
|
||||
#pragma mark - ORKRecorderDelegate
|
||||
|
||||
- (void)addReactionTimeResult {
|
||||
ORKNormalizedReactionTimeResult *reactionTimeResult = [[ORKNormalizedReactionTimeResult alloc] initWithIdentifier:self.step.identifier];
|
||||
reactionTimeResult.timerStartDate = _timerStartDate;
|
||||
reactionTimeResult.timerEndDate = [NSDate date];
|
||||
reactionTimeResult.reactionDate = _reactionDate;
|
||||
reactionTimeResult.stimulusStartDate = _stimulusStartDate;
|
||||
reactionTimeResult.currentInterval = [self reactionTimeStep].currentInterval;
|
||||
[_results addObject:reactionTimeResult];
|
||||
_timerStartDate = nil;
|
||||
_reactionDate = nil;
|
||||
_stimulusStartDate = nil;
|
||||
|
||||
[self attemptDidFinish];
|
||||
}
|
||||
|
||||
#pragma mark - ORKReactionTimeStepViewController
|
||||
|
||||
- (ORKNormalizedReactionTimeStep *)reactionTimeStep {
|
||||
return (ORKNormalizedReactionTimeStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)configureTitle {
|
||||
NSString *format = ORKLocalizedString(@"REACTION_TIME_TASK_ATTEMPTS_FORMAT", nil);
|
||||
NSString *text = [[NSString stringWithFormat: @"%@\n",ORKLocalizedString(@"REACTION_TIME_NORMALIZED_TASK_ACTIVE_STEP_TITLE", nil)] stringByAppendingString: [NSString stringWithFormat:format, ORKLocalizedStringFromNumber(@(_results.count + 1)), ORKLocalizedStringFromNumber(@([self reactionTimeStep].numberOfAttempts))]];
|
||||
[self.activeStepView updateTitle:nil text:text];
|
||||
}
|
||||
|
||||
- (void)attemptDidFinish {
|
||||
void (^completion)(void) = ^{
|
||||
if (_results.count == [self reactionTimeStep].numberOfAttempts) {
|
||||
[self finish];
|
||||
} else {
|
||||
[self resetAfterDelay:2];
|
||||
}
|
||||
};
|
||||
if (_validResult) {
|
||||
[self indicateSuccess:completion];
|
||||
} else {
|
||||
[self indicateFailure:completion];
|
||||
}
|
||||
_validResult = NO;
|
||||
_timedOut = NO;
|
||||
[_stimulusTimer invalidate];
|
||||
[_timeoutTimer invalidate];
|
||||
}
|
||||
|
||||
- (void)indicateSuccess:(void(^)(void))completion {
|
||||
[_reactionTimeContentView startSuccessAnimationWithDuration:OutcomeAnimationDuration completion:completion];
|
||||
AudioServicesPlaySystemSound([self reactionTimeStep].successSound);
|
||||
}
|
||||
|
||||
- (void)indicateFailure:(void(^)(void))completion {
|
||||
if (!_shouldIndicateFailure) {
|
||||
return;
|
||||
}
|
||||
[_reactionTimeContentView startFailureAnimationWithDuration:OutcomeAnimationDuration completion:completion];
|
||||
SystemSoundID sound = _timedOut ? [self reactionTimeStep].timeoutSound : [self reactionTimeStep].failureSound;
|
||||
AudioServicesPlayAlertSound(sound);
|
||||
}
|
||||
|
||||
- (void)resetAfterDelay:(NSTimeInterval)delay {
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
[_reactionTimeContentView resetAfterDelay:delay completion:^{
|
||||
[weakSelf configureTitle];
|
||||
[weakSelf start];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)startStimulusTimer {
|
||||
_stimulusTimer = [NSTimer scheduledTimerWithTimeInterval:[self stimulusInterval] target:self selector:@selector(stimulusTimerDidFire) userInfo:nil repeats:NO];
|
||||
}
|
||||
|
||||
- (void)stimulusTimerDidFire {
|
||||
_stimulusStartDate = [NSDate date];
|
||||
|
||||
_stimulusTimestamp = [NSProcessInfo processInfo].systemUptime;
|
||||
[_reactionTimeContentView setStimulusHidden:NO];
|
||||
_validResult = YES;
|
||||
[self startTimeoutTimer];
|
||||
}
|
||||
|
||||
- (void)startTimeoutTimer {
|
||||
NSTimeInterval timeout = [self reactionTimeStep].timeout;
|
||||
if (timeout > 0) {
|
||||
_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:@selector(timeoutTimerDidFire) userInfo:nil repeats:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)timeoutTimerDidFire {
|
||||
_validResult = NO;
|
||||
_timedOut = YES;
|
||||
[self addReactionTimeResult];
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// Device motion recorder won't work, so manually trigger didfinish
|
||||
[self attemptDidFinish];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSTimeInterval)stimulusInterval {
|
||||
ORKNormalizedReactionTimeStep *step = [self reactionTimeStep];
|
||||
NSNumber* interval = [self getRandomInterval];
|
||||
step.currentInterval = interval;
|
||||
return [interval doubleValue];
|
||||
}
|
||||
|
||||
|
||||
- (NSNumber*) getRandomInterval {
|
||||
NSArray* values = @[@2,@4,@6];
|
||||
|
||||
int randIndex = arc4random() % [values count];
|
||||
return (NSNumber*)values[randIndex];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@end
|
||||
@@ -98,7 +98,11 @@
|
||||
|
||||
- (void)setAddition:(NSUInteger)additionIndex forTotal:(NSUInteger)totalAddition withDigit:(NSNumber *)digit {
|
||||
if (digit.integerValue == -1) {
|
||||
self.digitLabel.textColor = [[UIColor blackColor] colorWithAlphaComponent:0.3f];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
self.digitLabel.textColor = [[UIColor labelColor] colorWithAlphaComponent:0.3f];
|
||||
} else {
|
||||
self.digitLabel.textColor = [[UIColor blackColor] colorWithAlphaComponent:0.3f];
|
||||
}
|
||||
self.digitLabel.text = ORKLocalizedString(@"PSAT_NO_DIGIT", nil);
|
||||
} else {
|
||||
[self.keyboardView.selectedAnswerButton setSelected:NO];
|
||||
@@ -129,7 +133,7 @@
|
||||
const CGFloat ORKPSATKeyboardWidth = ORKGetMetricForWindow(ORKScreenMetricPSATKeyboardViewWidth, self.window);
|
||||
const CGFloat ORKPSATKeyboardHeight = ORKGetMetricForWindow(ORKScreenMetricPSATKeyboardViewHeight, self.window);
|
||||
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
NSMutableArray<NSLayoutConstraint *> *constraints = [NSMutableArray array];
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_progressView, _digitLabel, _keyboardView);
|
||||
|
||||
@@ -139,11 +143,20 @@
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_keyboardView(==keyboardWidth)]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:@{ @"keyboardWidth": @(ORKPSATKeyboardWidth) }
|
||||
views:views]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_keyboardView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKPSATKeyboardWidth]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_keyboardView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_keyboardView(==keyboardHeight)]"
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
|
||||
NSTimeInterval const ORKPSATStimulusMinimumDuration = 0.2;
|
||||
|
||||
NSInteger const ORKPSATSerieMinimumLength = 10;
|
||||
NSInteger const ORKPSATSerieMaximumLength = 120;
|
||||
NSInteger const ORKPSATSeriesMinimumLength = 3;
|
||||
NSInteger const ORKPSATSeriesMaximumLength = 120;
|
||||
|
||||
NSTimeInterval totalDuration = (self.seriesLength + 1) * self.interStimulusInterval;
|
||||
if (self.stepDuration != totalDuration) {
|
||||
@@ -83,9 +83,9 @@
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"stimulus duration must be greater than or equal to %@ seconds and less than or equal to %@ seconds.", @(ORKPSATStimulusMinimumDuration), @(self.interStimulusInterval)] userInfo:nil];
|
||||
}
|
||||
|
||||
if (self.seriesLength < ORKPSATSerieMinimumLength ||
|
||||
self.seriesLength > ORKPSATSerieMaximumLength) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"serie length must be greater than or equal to %@ additions and less than or equal to %@ additions.", @(ORKPSATSerieMinimumLength), @(ORKPSATSerieMaximumLength)] userInfo:nil];
|
||||
if (self.seriesLength < ORKPSATSeriesMinimumLength ||
|
||||
self.seriesLength > ORKPSATSeriesMaximumLength) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"series length must be greater than or equal to %@ additions and less than or equal to %@ additions.", @(ORKPSATSeriesMinimumLength), @(ORKPSATSeriesMaximumLength)] userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,11 @@
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
#import "ORKPSATContentView.h"
|
||||
#import "ORKPSATKeyboardView.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
@@ -102,11 +104,11 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
self.psatContentView = [[ORKPSATContentView alloc] initWithPresentationMode:[self psatStep].presentationMode];
|
||||
self.psatContentView.keyboardView.delegate = self;
|
||||
[self.psatContentView setEnabled:NO];
|
||||
self.activeStepView.activeCustomView = self.psatContentView;
|
||||
self.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
|
||||
self.timerUpdateInterval = [self psatStep].interStimulusInterval;
|
||||
}
|
||||
@@ -198,7 +200,7 @@
|
||||
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
|
||||
if (self.currentDigitIndex == 0) {
|
||||
[self.psatContentView setEnabled:YES];
|
||||
[self.activeStepView updateTitle:nil text:ORKLocalizedString(@"PSAT_INSTRUCTION", nil)];
|
||||
[self.activeStepView updateTitle:self.step.title text:ORKLocalizedString(@"PSAT_INSTRUCTION", nil)];
|
||||
} else {
|
||||
[self saveSample];
|
||||
}
|
||||
|
||||
@@ -37,20 +37,35 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/**
|
||||
The `ORKRangeOfMotionResult` class records the results of a range of motion active task.
|
||||
|
||||
An `ORKRangeOfMotionResult` object records the flexion and extension values in degrees.
|
||||
An `ORKRangeOfMotionResult` object records the angle values in degrees.
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKRangeOfMotionResult : ORKResult
|
||||
|
||||
/**
|
||||
The degrees when bent.
|
||||
The angle (degrees) from the device reference position at the start position.
|
||||
*/
|
||||
@property (nonatomic, assign) double flexed;
|
||||
@property (nonatomic, assign) double start;
|
||||
|
||||
/**
|
||||
The degrees when extended.
|
||||
The angle (degrees) from the device reference position when the task finishes recording.
|
||||
*/
|
||||
@property (nonatomic, assign) double extended;
|
||||
@property (nonatomic, assign) double finish;
|
||||
|
||||
/**
|
||||
The angle (degrees) from the device reference position at the minimum angle (e.g. when the knee is most bent, such as at the end of the task).
|
||||
*/
|
||||
@property (nonatomic, assign) double minimum;
|
||||
|
||||
/**
|
||||
The angle (degrees) from the device reference position at the maximum angle (e.g. when the knee is extended).
|
||||
*/
|
||||
@property (nonatomic, assign) double maximum;
|
||||
|
||||
/**
|
||||
The angle (degrees) passed through from the start position to the maximum angle (e.g. from when the knee is flexed to when it is extended).
|
||||
*/
|
||||
@property (nonatomic, assign) double range;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -39,15 +39,21 @@
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_DOUBLE(aCoder, flexed);
|
||||
ORK_ENCODE_DOUBLE(aCoder, extended);
|
||||
ORK_ENCODE_DOUBLE(aCoder, start);
|
||||
ORK_ENCODE_DOUBLE(aCoder, finish);
|
||||
ORK_ENCODE_DOUBLE(aCoder, minimum);
|
||||
ORK_ENCODE_DOUBLE(aCoder, maximum);
|
||||
ORK_ENCODE_DOUBLE(aCoder, range);
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_DOUBLE(aDecoder, flexed);
|
||||
ORK_DECODE_DOUBLE(aDecoder, extended);
|
||||
ORK_DECODE_DOUBLE(aDecoder, start);
|
||||
ORK_DECODE_DOUBLE(aDecoder, finish);
|
||||
ORK_DECODE_DOUBLE(aDecoder, minimum);
|
||||
ORK_DECODE_DOUBLE(aDecoder, maximum);
|
||||
ORK_DECODE_DOUBLE(aDecoder, range);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -60,8 +66,11 @@
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
__typeof(self) castObject = object;
|
||||
return isParentSame &&
|
||||
self.flexed == castObject.flexed &&
|
||||
self.extended == castObject.extended;
|
||||
self.start == castObject.start &&
|
||||
self.finish == castObject.finish &&
|
||||
self.minimum == castObject.minimum &&
|
||||
self.maximum == castObject.maximum &&
|
||||
self.range == castObject.range;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
@@ -70,13 +79,16 @@
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKRangeOfMotionResult *result = [super copyWithZone:zone];
|
||||
result.flexed = self.flexed;
|
||||
result.extended = self.extended;
|
||||
result.start = self.start;
|
||||
result.finish = self.finish;
|
||||
result.minimum = self.minimum;
|
||||
result.maximum = self.maximum;
|
||||
result.range = self.range;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)descriptionWithNumberOfPaddingSpaces:(NSUInteger)numberOfPaddingSpaces {
|
||||
return [NSString stringWithFormat:@"<%@: flexion: %f; extension: %f>", self.class.description, self.flexed, self.extended];
|
||||
return [NSString stringWithFormat:@"<%@: start: %f; finish: %f; minimum: %f; maximum: %f; range: %f>", self.class.description, self.start, self.finish, self.minimum, self.maximum, self.range];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -81,14 +81,14 @@
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_INTEGER(aDecoder, limbOption);
|
||||
ORK_DECODE_ENUM(aDecoder, limbOption);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_INTEGER(aCoder, limbOption);
|
||||
ORK_ENCODE_ENUM(aCoder, limbOption);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
|
||||
@@ -41,8 +41,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKRangeOfMotionStepViewController : ORKActiveStepViewController {
|
||||
double _flexedAngle;
|
||||
double _rangeOfMotionAngle;
|
||||
double _startAngle;
|
||||
double _newAngle;
|
||||
double _minAngle;
|
||||
double _maxAngle;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
|
||||
#define radiansToDegrees(radians) ((radians) * 180.0 / M_PI)
|
||||
#define allOrientationsForPitch(x, w, y, z) (atan2(2.0 * (x*w + y*z), 1.0 - 2.0 * (x*x + z*z)))
|
||||
#define allOrientationsForRoll(x, w, y, z) (atan2(2.0 * (y*w - x*z), 1.0 - 2.0 * (y*y + z*z)))
|
||||
#define allOrientationsForYaw(x, w, y, z) (asin(2.0 * (x*y - w*z)))
|
||||
|
||||
|
||||
@interface ORKRangeOfMotionContentView : ORKActiveStepCustomView {
|
||||
NSLayoutConstraint *_topConstraint;
|
||||
@@ -122,10 +125,8 @@
|
||||
UITapGestureRecognizer *_gestureRecognizer;
|
||||
CMAttitude *_referenceAttitude;
|
||||
UIInterfaceOrientation _orientation;
|
||||
double _highestAngle;
|
||||
double _lowestAngle;
|
||||
double _lastAngle;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -139,20 +140,21 @@
|
||||
_gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||
[self.activeStepView addGestureRecognizer:_gestureRecognizer];
|
||||
}
|
||||
|
||||
//This function records the angle of the device when the screen is tapped
|
||||
- (void)handleTap:(UIGestureRecognizer *)sender {
|
||||
[self calculateAndSetFlexedAndExtendedAngles];
|
||||
[self calculateAndSetAngles];
|
||||
[self finish];
|
||||
}
|
||||
|
||||
- (void)calculateAndSetFlexedAndExtendedAngles {
|
||||
_flexedAngle = fabs([self getDeviceAngleInDegreesFromAttitude:_referenceAttitude]);
|
||||
- (void)calculateAndSetAngles {
|
||||
_startAngle = ([self getDeviceAngleInDegreesFromAttitude:_referenceAttitude]);
|
||||
|
||||
BOOL rangeOfMotionMoreThan180Degrees = _highestAngle > 175 && _lowestAngle < 175;
|
||||
if (rangeOfMotionMoreThan180Degrees) {
|
||||
_rangeOfMotionAngle = 360 - fabs(_lastAngle);
|
||||
} else {
|
||||
_rangeOfMotionAngle = fabs(_lastAngle);
|
||||
//This function calculates maximum and minimum angles recorded by the device
|
||||
if (_newAngle > _maxAngle) {
|
||||
_maxAngle = _newAngle;
|
||||
}
|
||||
if (_minAngle == 0.0 || _newAngle < _minAngle) {
|
||||
_minAngle = _newAngle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,13 +170,15 @@
|
||||
|
||||
double angle = [self getDeviceAngleInDegreesFromAttitude:currentAttitude];
|
||||
|
||||
if (angle > _highestAngle) {
|
||||
_highestAngle = angle;
|
||||
//This function shifts the range of angles reported by the device from +/-180 degrees to -90 to +270 degrees, which should be sufficient to cover all ahievable knee and shoulder ranges of motion
|
||||
BOOL shiftAngleRange = angle > 90 && angle <= 180;
|
||||
if (shiftAngleRange) {
|
||||
_newAngle = fabs(angle) - 360;
|
||||
} else {
|
||||
_newAngle = angle;
|
||||
}
|
||||
if (angle < _lowestAngle) {
|
||||
_lowestAngle = angle;
|
||||
}
|
||||
_lastAngle = angle;
|
||||
|
||||
[self calculateAndSetAngles];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -185,11 +189,15 @@
|
||||
*/
|
||||
- (double)getDeviceAngleInDegreesFromAttitude:(CMAttitude *)attitude {
|
||||
if (!_orientation) {
|
||||
_orientation = [UIApplication sharedApplication].statusBarOrientation;
|
||||
_orientation = self.view.window.windowScene.interfaceOrientation;
|
||||
}
|
||||
double angle;
|
||||
if (UIInterfaceOrientationIsLandscape(_orientation)) {
|
||||
angle = radiansToDegrees(attitude.roll);
|
||||
double x = attitude.quaternion.x;
|
||||
double w = attitude.quaternion.w;
|
||||
double y = attitude.quaternion.y;
|
||||
double z = attitude.quaternion.z;
|
||||
angle = radiansToDegrees(allOrientationsForRoll(x, w, y, z));
|
||||
} else {
|
||||
double x = attitude.quaternion.x;
|
||||
double w = attitude.quaternion.w;
|
||||
@@ -207,8 +215,13 @@
|
||||
ORKStepResult *stepResult = [super result];
|
||||
|
||||
ORKRangeOfMotionResult *result = [[ORKRangeOfMotionResult alloc] initWithIdentifier:self.step.identifier];
|
||||
result.flexed = _flexedAngle;
|
||||
result.extended = result.flexed - _rangeOfMotionAngle;
|
||||
|
||||
result.start = 90.0 - _startAngle;
|
||||
result.finish = result.start - _newAngle;
|
||||
//Because the task uses pitch in the direction opposite to the original CoreMotion device axes (i.e. right hand rule), maximum and minimum angles are reported the 'wrong' way around for the knee and shoulder tasks
|
||||
result.minimum = result.start - _maxAngle;
|
||||
result.maximum = result.start - _minAngle;
|
||||
result.range = fabs(result.maximum - result.minimum);
|
||||
|
||||
stepResult.results = [self.addedResults arrayByAddingObject:result] ? : @[result];
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ static const NSTimeInterval OutcomeAnimationDuration = 0.3;
|
||||
_results = [NSMutableArray new];
|
||||
_reactionTimeContentView = [ORKReactionTimeContentView new];
|
||||
self.activeStepView.activeCustomView = _reactionTimeContentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
[_reactionTimeContentView setStimulusHidden:YES];
|
||||
}
|
||||
|
||||
@@ -232,7 +231,7 @@ static const NSTimeInterval OutcomeAnimationDuration = 0.3;
|
||||
- (NSTimeInterval)stimulusInterval {
|
||||
ORKReactionTimeStep *step = [self reactionTimeStep];
|
||||
NSTimeInterval range = step.maximumStimulusInterval - step.minimumStimulusInterval;
|
||||
NSTimeInterval randomFactor = ((NSTimeInterval)rand() / RAND_MAX) * range;
|
||||
NSTimeInterval randomFactor = ((NSTimeInterval)arc4random() / RAND_MAX) * range;
|
||||
return randomFactor + step.minimumStimulusInterval;
|
||||
}
|
||||
|
||||
|
||||
@@ -192,15 +192,15 @@
|
||||
return [NSString stringWithFormat:@"%@_%@", [self recorderType], _recorderUUID.UUIDString];
|
||||
}
|
||||
|
||||
- (ORKDataLogger *)makeJSONDataLoggerWithError:(NSError **)error {
|
||||
- (ORKDataLogger *)makeJSONDataLoggerWithError:(NSError **)errorOut {
|
||||
NSURL *workingDir = [self recordingDirectoryURL];
|
||||
if (!workingDir) {
|
||||
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 nil;
|
||||
}
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtURL:workingDir withIntermediateDirectories:YES attributes:nil error:error]) {
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtURL:workingDir withIntermediateDirectories:YES attributes:nil error:errorOut]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSError *error = nil;
|
||||
if (! [fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(fileProtection)} ofItemAtPath:[url path] error:&error]) {
|
||||
ORK_Log_Warning(@"Error setting %@ on %@: %@", ORKFileProtectionFromMode(fileProtection), url, error);
|
||||
ORK_Log_Error("Error setting %@ on %@: %@", ORKFileProtectionFromMode(fileProtection), url, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (void)reset NS_REQUIRES_SUPER;
|
||||
|
||||
- (void)reportFileResultWithFile:(NSURL *)fileUrl error:(nullable NSError *)error;
|
||||
- (void)reportFileResultWithFile:(nullable NSURL *)fileUrl error:(nullable NSError *)error;
|
||||
|
||||
- (nullable NSURL *)recordingDirectoryURL;
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
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/ResearchKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKSecondaryTaskStep : ORKInstructionStep
|
||||
|
||||
@property (nonatomic) ORKOrderedTask *secondaryTask;
|
||||
@property (nonatomic) NSString *secondaryTaskButtonTitle;
|
||||
@property (nonatomic) NSString *nextButtonTitle;
|
||||
@property (nonatomic) NSUInteger requiredAttempts;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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 "ORKSecondaryTaskStep.h"
|
||||
#import "ORKSecondaryTaskStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@implementation ORKSecondaryTaskStep
|
||||
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORKSecondaryTaskStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.requiredAttempts = 1;
|
||||
self.optional = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, secondaryTask, ORKOrderedTask);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, secondaryTaskButtonTitle, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, nextButtonTitle, NSString);
|
||||
ORK_DECODE_INTEGER(aDecoder, requiredAttempts);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, secondaryTask);
|
||||
ORK_ENCODE_OBJ(aCoder, secondaryTaskButtonTitle);
|
||||
ORK_ENCODE_OBJ(aCoder, nextButtonTitle);
|
||||
ORK_ENCODE_INTEGER(aCoder, requiredAttempts);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKSecondaryTaskStep *step = [super copyWithZone:zone];
|
||||
step->_secondaryTask = [_secondaryTask copy];
|
||||
step->_secondaryTaskButtonTitle = [_secondaryTaskButtonTitle copy];
|
||||
step->_nextButtonTitle = [_nextButtonTitle copy];
|
||||
step->_requiredAttempts = _requiredAttempts;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame
|
||||
&& ORKEqualObjects(self.secondaryTask, castObject.secondaryTask)
|
||||
&& ORKEqualObjects(self.secondaryTaskButtonTitle, castObject.secondaryTaskButtonTitle)
|
||||
&& ORKEqualObjects(self.nextButtonTitle, castObject.nextButtonTitle)
|
||||
&& self.requiredAttempts == castObject.requiredAttempts);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return super.hash ^ self.secondaryTask.hash ^ self.secondaryTaskButtonTitle.hash ^ self.nextButtonTitle.hash ^ self.requiredAttempts;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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/ResearchKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKSecondaryTaskStepViewController : ORKInstructionStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
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 "ORKSecondaryTaskStepViewController.h"
|
||||
#import "ORKSecondaryTaskStep.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKTaskViewController.h"
|
||||
#import "ORKInstructionStepContainerView.h"
|
||||
#import "ORKInstructionStepViewController_Internal.h"
|
||||
#import "ORKStepView.h"
|
||||
#import "ORKNavigationContainerView.h"
|
||||
|
||||
@interface ORKSecondaryTaskStepViewController ()<ORKTaskViewControllerDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKSecondaryTaskStepViewController {
|
||||
ORKTaskViewController *secondaryTaskViewController;
|
||||
NSUInteger requiredAttempts;
|
||||
NSUInteger numberOfTimesTaskCompleted;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
numberOfTimesTaskCompleted = 0;
|
||||
}
|
||||
|
||||
- (ORKSecondaryTaskStep *)secondaryTaskStep {
|
||||
return (ORKSecondaryTaskStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
[self resetSecondaryTaskViewController];
|
||||
requiredAttempts = [self secondaryTaskStep].requiredAttempts;
|
||||
[self updateButtonState];
|
||||
}
|
||||
|
||||
- (void)setContinueButtonItem:(UIBarButtonItem *)continueButtonItem {
|
||||
[super setContinueButtonItem:continueButtonItem];
|
||||
if ([self secondaryTaskStep].nextButtonTitle) {
|
||||
[continueButtonItem setTitle:[self secondaryTaskStep].nextButtonTitle];
|
||||
}
|
||||
_navigationFooterView.continueButtonItem = continueButtonItem;
|
||||
}
|
||||
|
||||
- (void)setSkipButtonItem:(UIBarButtonItem *)skipButtonItem {
|
||||
[super setSkipButtonItem:skipButtonItem];
|
||||
|
||||
[skipButtonItem setTitle:[self secondaryTaskStep].secondaryTaskButtonTitle ? : ORKLocalizedString(@"SECONDARY_TASK_START_BUTTON", nil)];
|
||||
[skipButtonItem setTarget:self];
|
||||
[skipButtonItem setAction:@selector(startSecondaryTaskHandler:)];
|
||||
}
|
||||
|
||||
- (void)startSecondaryTaskHandler:(id)sender{
|
||||
[self startSecondaryTaskButtonTapped];
|
||||
}
|
||||
|
||||
- (void)startSecondaryTaskButtonTapped {
|
||||
if (secondaryTaskViewController) {
|
||||
[self presentViewController:secondaryTaskViewController animated:YES completion:NULL];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)resetSecondaryTaskViewController {
|
||||
secondaryTaskViewController.delegate = nil;
|
||||
secondaryTaskViewController = nil;
|
||||
secondaryTaskViewController = [[ORKTaskViewController alloc] initWithTask:[self secondaryTaskStep].secondaryTask taskRunUUID:[NSUUID UUID]];
|
||||
secondaryTaskViewController.delegate = self;
|
||||
secondaryTaskViewController.outputDirectory = self.taskViewController.outputDirectory;
|
||||
}
|
||||
|
||||
- (void)taskCompleted {
|
||||
numberOfTimesTaskCompleted = numberOfTimesTaskCompleted + 1;
|
||||
[self updateButtonState];
|
||||
}
|
||||
|
||||
- (void)updateButtonState {
|
||||
|
||||
[self.stepView.navigationFooterView setContinueEnabled:(requiredAttempts == 0) || (numberOfTimesTaskCompleted >= requiredAttempts)];
|
||||
|
||||
[self.stepView.navigationFooterView setSkipEnabled:(numberOfTimesTaskCompleted > requiredAttempts)];
|
||||
}
|
||||
|
||||
#pragma mark ORKTaskViewControllerDelegate
|
||||
|
||||
- (void)taskViewController:(ORKTaskViewController *)taskViewController didFinishWithReason:(ORKTaskViewControllerFinishReason)reason error:(NSError *)error {
|
||||
if (reason == ORKTaskViewControllerFinishReasonCompleted) {
|
||||
|
||||
[self taskCompleted];
|
||||
[self resetSecondaryTaskViewController];
|
||||
}
|
||||
[taskViewController dismissViewControllerAnimated:YES completion:NULL];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKRangeOfMotionStep.h"
|
||||
#import <ResearchKit/ORKRangeOfMotionStep.h>
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
|
||||
|
||||
@implementation ORKShoulderRangeOfMotionStepViewController
|
||||
|
||||
#pragma mark - ORKActiveTaskViewController
|
||||
@@ -44,9 +43,13 @@
|
||||
ORKStepResult *stepResult = [super result];
|
||||
|
||||
ORKRangeOfMotionResult *result = [[ORKRangeOfMotionResult alloc] initWithIdentifier:self.step.identifier];
|
||||
result.flexed = 90.0 - _flexedAngle;
|
||||
result.extended = result.flexed + _rangeOfMotionAngle;
|
||||
|
||||
result.start = 90.0 - _startAngle;
|
||||
result.finish = result.start - _newAngle;
|
||||
//Because the task uses pitch in the direction opposite to the original CoreMotion device axes (i.e. right hand rule), maximum and minimum angles are reported the 'wrong' way around for the knee and shoulder tasks
|
||||
result.minimum = result.start - _maxAngle;
|
||||
result.maximum = result.start - _minAngle;
|
||||
result.range = fabs(result.maximum - result.minimum);
|
||||
|
||||
stepResult.results = [self.addedResults arrayByAddingObject:result] ? : @[result];
|
||||
|
||||
return stepResult;
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
// Note: we will only use the first _sequenceLength elements of this array
|
||||
srandom(_seed);
|
||||
for (NSInteger i = 0; i < _gameSize; i++) {
|
||||
NSInteger rand_i = random() % _gameSize;
|
||||
NSInteger rand_i = arc4random() % _gameSize;
|
||||
NSInteger tmp = _sequence[i];
|
||||
_sequence[i] = _sequence[rand_i];
|
||||
_sequence[rand_i] = tmp;
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
- (void)setButtonItem:(ORKBorderedButton *)buttonItem {
|
||||
_buttonItem = buttonItem;
|
||||
if (buttonItem) {
|
||||
buttonItem.contentEdgeInsets = (UIEdgeInsets){.top = 2, .bottom = 2, .left = 8, .right = 8};
|
||||
[buttonItem updateContentInsets:NSDirectionalEdgeInsetsMake(2, 8, 2, 8)];
|
||||
buttonItem.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_continueView addSubview:buttonItem];
|
||||
[[NSLayoutConstraint constraintWithItem:_buttonItem
|
||||
@@ -318,7 +318,7 @@
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
constant:CGFLOAT_MIN];
|
||||
gameViewHeightConstraint.priority = UILayoutPriorityDefaultLow - 1;
|
||||
[constraints addObject:gameViewHeightConstraint];
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#import "ORKSpatialSpanMemoryStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
#import "ORKSpatialSpanMemoryContentView.h"
|
||||
#import "ORKVerticalContainerView_Internal.h"
|
||||
|
||||
@@ -46,6 +47,7 @@
|
||||
#import "ORKSpatialSpanGame.h"
|
||||
#import "ORKSpatialSpanGameState.h"
|
||||
#import "ORKSpatialSpanMemoryStep.h"
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
@@ -168,17 +170,12 @@ typedef void (^_ORKStateHandler)(ORKState *fromState, ORKState *_toState, id con
|
||||
_contentView.footerHidden = YES;
|
||||
_contentView.gameView.delegate = self;
|
||||
self.activeStepView.activeCustomView = _contentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = NO;
|
||||
self.activeStepView.minimumStepHeaderHeight = ORKGetMetricForWindow(ORKScreenMetricMinimumStepHeaderHeightForMemoryGame, self.view.window);
|
||||
self.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
|
||||
[self resetUI];
|
||||
|
||||
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleUserTap:)];
|
||||
[self.activeStepView addGestureRecognizer:tapGestureRecognizer];
|
||||
|
||||
if (usesDefaultCopyright) {
|
||||
self.activeStepView.headerView.learnMoreButton.alpha = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stepDidChange {
|
||||
@@ -655,9 +652,6 @@ typedef void (^_ORKStateHandler)(ORKState *fromState, ORKState *_toState, id con
|
||||
- (void)showComplete {
|
||||
[self.activeStepView updateTitle:ORKLocalizedString(@"MEMORY_GAME_COMPLETE_TITLE", nil) text:nil];
|
||||
|
||||
// Show the copyright
|
||||
self.activeStepView.headerView.learnMoreButton.alpha = 1;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.75 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
_contentView.buttonItem = [ORKBorderedButton new];
|
||||
[_contentView.buttonItem setTitle:ORKLocalizedString(@"BUTTON_NEXT", nil) forState:UIControlStateNormal];
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@class ORKBorderedButton;
|
||||
@class ORKPlaybackButton;
|
||||
|
||||
@interface ORKSpeechInNoiseContentView : ORKActiveStepCustomView
|
||||
|
||||
@@ -46,11 +46,12 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, copy, nullable) NSArray *samples;
|
||||
|
||||
@property (nonatomic) ORKBorderedButton *playButton;
|
||||
@property (nonatomic) ORKPlaybackButton *playButton;
|
||||
|
||||
// Samples should be in the range of (0, 1).
|
||||
- (void)addSample:(NSNumber *)sample;
|
||||
- (void)removeAllSamples;
|
||||
- (void)setGraphViewHidden:(BOOL)hidden;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -39,11 +39,10 @@
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
#import "ORKPlaybackButton.h"
|
||||
|
||||
@interface ORKSpeechInNoiseContentView () <UITextFieldDelegate>
|
||||
|
||||
@property (nonatomic, strong) ORKHeadlineLabel *alertLabel;
|
||||
@property (nonatomic, strong) ORKAudioGraphView *graphView;
|
||||
@property (nonatomic, strong) ORKSubheadlineLabel *transcriptLabel;
|
||||
|
||||
@@ -89,10 +88,12 @@
|
||||
}
|
||||
|
||||
- (void)setupPlayButton {
|
||||
self.playButton = [[ORKBorderedButton alloc] init];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
self.playButton = [[ORKPlaybackButton alloc] initWithText:ORKLocalizedString(@"SPEECH_IN_NOISE_START_AUDIO_LABEL", nil) image:[UIImage systemImageNamed:@"play.circle"]];
|
||||
} else {
|
||||
self.playButton = [[ORKPlaybackButton alloc] initWithText:ORKLocalizedString(@"SPEECH_IN_NOISE_START_AUDIO_LABEL", nil) image:[UIImage imageNamed:@"play" inBundle:ORKBundle() compatibleWithTraitCollection:nil]];
|
||||
}
|
||||
self.playButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self.playButton setTitle:ORKLocalizedString(@"SPEECH_IN_NOISE_START_AUDIO_LABEL", nil)
|
||||
forState:UIControlStateNormal];
|
||||
self.playButton.enabled = YES;
|
||||
self.playButton.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitStartsMediaSession;
|
||||
[self addSubview:_playButton];
|
||||
@@ -126,7 +127,7 @@
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_textLabel, _graphView, _playButton);
|
||||
const CGFloat graphHeight = 150;
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_textLabel]-(5)-[_graphView(graphHeight)]-buttonGap-[_playButton(50)]-topBottomMargin-|"
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_textLabel]-(5)-[_graphView(graphHeight)]-buttonGap-[_playButton]-(>=topBottomMargin)-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:@{
|
||||
@"graphHeight": @(graphHeight),
|
||||
@@ -153,7 +154,7 @@
|
||||
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-twiceSideMargin-[_playButton(200)]-twiceSideMargin-|"
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-twiceSideMargin-[_playButton(>=200)]-twiceSideMargin-|"
|
||||
options:0
|
||||
metrics: @{@"twiceSideMargin": @(twiceSideMargin)}
|
||||
views:views]];
|
||||
@@ -165,6 +166,10 @@
|
||||
_graphView.values = _samples;
|
||||
}
|
||||
|
||||
- (void)setGraphViewHidden:(BOOL)hidden {
|
||||
[_graphView setHidden:hidden];
|
||||
}
|
||||
|
||||
- (void)addSample:(NSNumber *)sample {
|
||||
NSAssert(sample != nil, @"Sample should be non-nil");
|
||||
if (!_samples) {
|
||||
|
||||
@@ -66,6 +66,8 @@ ORK_CLASS_AVAILABLE
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL willAudioLoop;
|
||||
|
||||
@property (nonatomic) BOOL hideGraphView;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user