Compare commits
403 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8473017ec | |||
| 2bc0033aa8 | |||
| a89059f5dc | |||
| 2cc8f9e7d5 | |||
| 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 |
+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
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:ResearchKit.xcodeproj">
|
||||
</FileRef>
|
||||
@@ -13,7 +10,4 @@
|
||||
<FileRef
|
||||
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:samples/ORKSample/ORKSample.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
+5
-2
@@ -1,15 +1,18 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ResearchKit'
|
||||
s.version = '2.0.0'
|
||||
s.version = '2.1.0'
|
||||
s.summary = 'ResearchKit is an open source software framework that makes it easy to create apps for medical research or for other research projects.'
|
||||
s.homepage = 'https://www.github.com/ResearchKit/ResearchKit'
|
||||
s.documentation_url = 'http://researchkit.github.io/docs/'
|
||||
s.license = { :type => 'BSD', :file => 'LICENSE' }
|
||||
s.author = { 'researchkit.org' => 'http://researchkit.org' }
|
||||
s.source = { :git => 'https://github.com/ResearchKit/ResearchKit.git', :tag => s.version.to_s }
|
||||
s.public_header_files = `./scripts/find_headers.rb --public --private`.split("\n")
|
||||
s.public_header_files = `./scripts/find_headers.rb --public`.split("\n")
|
||||
s.private_header_files = `./scripts/find_headers.rb --private`.split("\n")
|
||||
s.source_files = 'ResearchKit/**/*.{h,m,swift}'
|
||||
s.resources = 'ResearchKit/**/*.{fsh,vsh}', 'ResearchKit/Animations/**/*.m4v', 'ResearchKit/Artwork.xcassets', 'ResearchKit/Localized/*.lproj'
|
||||
s.platform = :ios, '11.0'
|
||||
s.requires_arc = true
|
||||
s.swift_version = '5'
|
||||
s.module_map = "ResearchKit/ResearchKit.modulemap"
|
||||
end
|
||||
|
||||
+1700
-213
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 = "1250"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -20,6 +20,20 @@
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
|
||||
BuildableName = "ResearchKitTests.xctest"
|
||||
BlueprintName = "ResearchKitTests"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
@@ -27,6 +41,15 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B183A4731A8535D100C76870"
|
||||
BuildableName = "ResearchKit.framework"
|
||||
BlueprintName = "ResearchKit"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
@@ -39,22 +62,13 @@
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B183A4731A8535D100C76870"
|
||||
BuildableName = "ResearchKit.framework"
|
||||
BlueprintName = "ResearchKit"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
enableUBSanitizer = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -70,8 +84,6 @@
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
|
||||
BuildableName = "ResearchKitTests.xctest"
|
||||
BlueprintName = "ResearchKitTests"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "YES">
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B183A4731A8535D100C76870"
|
||||
BuildableName = "ResearchKit.framework"
|
||||
BlueprintName = "ResearchKit"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES"
|
||||
testExecutionOrdering = "random">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
|
||||
BuildableName = "ResearchKitTests.xctest"
|
||||
BlueprintName = "ResearchKitTests"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1000"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -29,8 +29,6 @@
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -51,8 +49,6 @@
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "A9C8689E-BBEE-4643-8C5D-DBA6A5AF01B2",
|
||||
"name" : "Configuration 1",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"target" : {
|
||||
"containerPath" : "container:ResearchKit.xcodeproj",
|
||||
"identifier" : "86CC8E991AC09332001CCD89",
|
||||
"name" : "ResearchKitTests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
}
|
||||
@@ -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,182 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
internal class EyeActivitySlider: UIView {
|
||||
|
||||
private var testType: VisionStepType?
|
||||
|
||||
private var incorrectAnswers = 0
|
||||
private let contentGap: CGFloat = 20.0
|
||||
private let toleranceAngle = 22.5
|
||||
private let letterAngles = [0, 45, 90, 135, 180, 225, 270, 315]
|
||||
private var letterSize: CGFloat {
|
||||
var letterSize: CGFloat!
|
||||
|
||||
if self.testType == .visualAcuity {
|
||||
letterSize = letterMmSizes[currentStep] * UIDevice.pixelsPerMm / UIScreen.main.nativeScale
|
||||
} else {
|
||||
letterSize = 20 * UIDevice.pixelsPerMm / UIScreen.main.nativeScale
|
||||
}
|
||||
|
||||
return letterSize
|
||||
}
|
||||
|
||||
private var currentStep = 0
|
||||
private var letterMmSizes: [CGFloat] = [5.82, 4.65, 3.72, 2.91, 2.33, 1.86, 1.45, 1.16, 0.93, 0.73, 0.58, 0.47, 0.37]
|
||||
private var contrastLevels: [CGFloat] = [0.9, 0.92, 0.937, 0.95, 0.96, 0.968, 0.975, 0.98, 0.984, 0.9875, 0.99]
|
||||
private var stepScores: [Int] = [50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110]
|
||||
private var letterAngle = 0.0
|
||||
|
||||
private lazy var letterImageView: UIImageView = {
|
||||
let letterImage = UIImage(named: "iCNLandoltC",
|
||||
in: Bundle(for: type(of: self)),
|
||||
compatibleWith: nil)
|
||||
let imageView = UIImageView(image: letterImage!)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var circleImageView: UIImageView = {
|
||||
let circleImage = UIImage(named: "orangeGrayCircle",
|
||||
in: Bundle(for: type(of: self)),
|
||||
compatibleWith: nil)
|
||||
return UIImageView(image: circleImage!)
|
||||
}()
|
||||
|
||||
private var slider: CircleSlider?
|
||||
|
||||
internal init(testType: VisionStepType) {
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.testType = testType
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required internal init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
addSubview(letterImageView)
|
||||
|
||||
circleImageView.contentMode = .scaleAspectFit
|
||||
addSubview(circleImageView)
|
||||
|
||||
let thumbImage = UIImage(named: "iCNDialPointerWithShadow",
|
||||
in: Bundle(for: type(of: self)),
|
||||
compatibleWith: nil)
|
||||
|
||||
slider = CircleSlider(frame: bounds, options: [
|
||||
CircleSliderOption.barColor(UIColor.clear),
|
||||
CircleSliderOption.trackingColor(UIColor.clear),
|
||||
CircleSliderOption.startAngle(0),
|
||||
CircleSliderOption.maxValue(360),
|
||||
CircleSliderOption.minValue(0),
|
||||
CircleSliderOption.thumbImage(thumbImage!)
|
||||
])
|
||||
|
||||
addSubview(slider!)
|
||||
updateSliderAndLetter()
|
||||
}
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
letterAngle = Double(letterAngles[Int(arc4random_uniform(7))])
|
||||
letterImageView.transform = CGAffineTransform.identity
|
||||
letterImageView.frame = CGRect(origin: CGPoint(), size: CGSize(width: letterSize, height: letterSize))
|
||||
letterImageView.center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
|
||||
letterImageView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(letterAngle)))
|
||||
letterImageView.alpha = getAlpha()
|
||||
slider?.frame = bounds
|
||||
|
||||
var frame = contentFrame()
|
||||
circleImageView.frame = frame
|
||||
|
||||
let labelMargin: CGFloat = 30.0
|
||||
frame.origin.x += labelMargin
|
||||
frame.origin.y += labelMargin
|
||||
frame.size.width -= labelMargin * 2
|
||||
frame.size.height -= labelMargin * 2
|
||||
}
|
||||
|
||||
private func updateSliderAndLetter() {
|
||||
guard incorrectAnswers < 2 else { return }
|
||||
|
||||
letterImageView.isHidden = false
|
||||
slider?.sliderValue = 0
|
||||
slider?.isUserInteractionEnabled = true
|
||||
slider?.isHidden = false
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
private func contentFrame() -> CGRect {
|
||||
let sideLength = min(bounds.size.width, bounds.size.height) - contentGap
|
||||
let contentFrame = CGRect(x: (bounds.size.width - sideLength) / 2, y: (bounds.size.height - sideLength) / 2, width: sideLength, height: sideLength)
|
||||
return contentFrame
|
||||
}
|
||||
|
||||
private func getAlpha() -> CGFloat {
|
||||
return testType == .visualAcuity ? 1.0 : (1 - contrastLevels[currentStep])
|
||||
}
|
||||
|
||||
private func getResult() -> Bool {
|
||||
let sliderValue = Double((slider?.sliderValue)!)
|
||||
let leftMargin = letterAngle - toleranceAngle
|
||||
let rightMargin = letterAngle + toleranceAngle
|
||||
let result = sliderValue > leftMargin && sliderValue < rightMargin
|
||||
|
||||
if result == false {
|
||||
incorrectAnswers += 1
|
||||
} else {
|
||||
currentStep += 1
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
internal func hideLetter() {
|
||||
letterImageView.isHidden = true
|
||||
}
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
internal func fetchResultDataAndUpdateSlider() -> (outcome: Bool, letterAngle: Double, sliderAngle: Double, score: Int, incorrectAnswers: Int, maxScore: Int) {
|
||||
let outcome = getResult()
|
||||
let score = stepScores[currentStep]
|
||||
let currentSliderValue = Double((slider?.sliderValue)!)
|
||||
let currentLetterAngle = letterAngle
|
||||
let maxScore = testType == .visualAcuity ? stepScores.last! : stepScores[contrastLevels.count - 1]
|
||||
updateSliderAndLetter()
|
||||
|
||||
return (outcome, currentLetterAngle, currentSliderValue, score, incorrectAnswers, maxScore)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
internal class Math {
|
||||
|
||||
internal class func degreesToRadians(_ angle: Double) -> Double {
|
||||
return angle / 180 * .pi
|
||||
}
|
||||
|
||||
internal class func pointFromAngle(_ frame: CGRect, angle: Double, radius: Double) -> CGPoint {
|
||||
let radian = degreesToRadians(angle)
|
||||
let xPoint = Double(frame.midX) + cos(radian) * radius
|
||||
let yPoint = Double(frame.midY) + sin(radian) * radius
|
||||
return CGPoint(x: xPoint, y: yPoint)
|
||||
}
|
||||
|
||||
internal class func pointPairToBearingDegrees(_ startPoint: CGPoint, endPoint: CGPoint) -> Double {
|
||||
let originPoint = CGPoint(x: endPoint.x - startPoint.x, y: endPoint.y - startPoint.y)
|
||||
let bearingRadians = atan2(Double(originPoint.y), Double(originPoint.x))
|
||||
var bearingDegrees = bearingRadians * (180.0 / .pi)
|
||||
bearingDegrees = (bearingDegrees > 0.0 ? bearingDegrees : (360.0 + bearingDegrees))
|
||||
return bearingDegrees
|
||||
}
|
||||
|
||||
internal class func adjustValue(_ startAngle: Double, degree: Double, maxValue: Float, minValue: Float) -> Double {
|
||||
let ratio = Double((maxValue - minValue) / 360)
|
||||
let ratioStart = ratio * startAngle
|
||||
let ratioDegree = ratio * degree
|
||||
let adjustValue: Double
|
||||
if startAngle < 0 {
|
||||
adjustValue = (360 + startAngle) > degree ? (ratioDegree - ratioStart) : (ratioDegree - ratioStart) - (360 * ratio)
|
||||
} else {
|
||||
adjustValue = (360 - (360 - startAngle)) < degree ? (ratioDegree - ratioStart) : (ratioDegree - ratioStart) + (360 * ratio)
|
||||
}
|
||||
return adjustValue + (Double(minValue))
|
||||
}
|
||||
|
||||
internal class func adjustDegree(_ startAngle: Double, degree: Double) -> Double {
|
||||
return (360 + startAngle) > degree ? degree : -(360 - degree)
|
||||
}
|
||||
|
||||
internal class func degreeFromValue(_ startAngle: Double, value: Float, maxValue: Float, minValue: Float) -> Double {
|
||||
let ratio = Double((maxValue - minValue) / 360)
|
||||
let angle = Double(value) / ratio
|
||||
return angle + startAngle - (Double(minValue) / ratio)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKResult.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol ORK3DModelManagerProtocol <NSObject>
|
||||
|
||||
@required
|
||||
|
||||
|
||||
/**
|
||||
This method is called within the ORK3DModelStepViewController's viewDidLoad method.
|
||||
|
||||
You are passed the contentView of the step so that you can add whatever visuals you choose.
|
||||
*/
|
||||
- (void)addContentToView:(UIView *)view;
|
||||
|
||||
/**
|
||||
This method provides the ORK3DModelManager sublass the opportunity for cleanup before step is deallocated.
|
||||
*/
|
||||
- (void)stepWillEnd;
|
||||
|
||||
/**
|
||||
This method is called by the ORK3DModelStepViewController's after the user taps the continue button or after the 3DModelManager subclass calls the endStep method.
|
||||
|
||||
This method signifies that the step is about to end so any necessary clean up before deallocation should be done here.
|
||||
|
||||
You can also optionally pass back an array of ORKResults.
|
||||
*/
|
||||
- (nullable NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier;
|
||||
|
||||
@end
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORK3DModelManager : NSObject <ORK3DModelManagerProtocol, NSSecureCoding, NSCopying>
|
||||
|
||||
- (instancetype)init;
|
||||
|
||||
@property (nonatomic, assign) BOOL allowsSelection;
|
||||
@property (nonatomic, nullable) UIColor *highlightColor;
|
||||
@property (nonatomic, nullable) NSArray<NSString *> *identifiersOfObjectsToHighlight;
|
||||
|
||||
- (void)setContinueEnabled:(BOOL)enabled;
|
||||
- (void)endStep;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORK3DModelManager.h"
|
||||
#import "ORK3DModelManager_Internal.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
NSNotificationName const ORK3DModelEnableContinueButtonNotification = @"ORK3DModelEnableContinueButtonNotification";
|
||||
NSNotificationName const ORK3DModelDisableContinueButtonNotification = @"ORK3DModelDisableContinueButtonNotification";
|
||||
NSNotificationName const ORK3DModelEndStepNotification = @"ORK3DModelEndStepNotification";
|
||||
|
||||
@implementation ORK3DModelManager
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_allowsSelection = NO;
|
||||
_highlightColor = [UIColor yellowColor];
|
||||
_identifiersOfObjectsToHighlight = nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORK3DModelManager *modelManager = [[[self class] allocWithZone:zone] init];
|
||||
modelManager->_allowsSelection = self.allowsSelection;
|
||||
modelManager->_highlightColor = [_highlightColor copy];
|
||||
modelManager->_identifiersOfObjectsToHighlight = [self.identifiersOfObjectsToHighlight copy];
|
||||
return modelManager;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super init];
|
||||
if (self ) {
|
||||
ORK_DECODE_BOOL(aDecoder, allowsSelection);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, identifiersOfObjectsToHighlight, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, highlightColor, UIColor);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
ORK_ENCODE_BOOL(aCoder, allowsSelection);
|
||||
ORK_ENCODE_OBJ(aCoder, identifiersOfObjectsToHighlight);
|
||||
ORK_ENCODE_OBJ(aCoder, highlightColor);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
if ([self class] != [object class]) {
|
||||
return NO;
|
||||
}
|
||||
__typeof(self) castObject = object;
|
||||
return ((self.allowsSelection == castObject.allowsSelection) &&
|
||||
(ORKEqualObjects(self.highlightColor, castObject.highlightColor)) &&
|
||||
(ORKEqualObjects(self.identifiersOfObjectsToHighlight, castObject.identifiersOfObjectsToHighlight)));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [_identifiersOfObjectsToHighlight hash] ^ (_allowsSelection ? 0xf : 0x0) ^ [_highlightColor hash];
|
||||
}
|
||||
|
||||
#pragma mark - Instance Methods
|
||||
|
||||
- (void)setContinueEnabled:(BOOL)enabled {
|
||||
if (enabled) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ORK3DModelEnableContinueButtonNotification object:self];
|
||||
} else {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ORK3DModelDisableContinueButtonNotification object:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)endStep {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ORK3DModelEndStepNotification object:self];
|
||||
}
|
||||
|
||||
#pragma mark - ORK3DModelManagerProtocol
|
||||
|
||||
- (void)addContentToView:(UIView *)view {
|
||||
[NSException raise:@"addContentToView not overwitten" format:@"Subclasses must overwrite the addContentToView function"];
|
||||
}
|
||||
|
||||
- (void)stepWillEnd {
|
||||
[NSException raise:@"stepWillEnd not overwitten" format:@"Subclasses must overwrite the stepWillEnd function"];
|
||||
}
|
||||
|
||||
- (NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier {
|
||||
[NSException raise:@"provideResults not overwitten" format:@"Subclasses must overwrite the provideResults function"];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import Foundation;
|
||||
|
||||
extern NSNotificationName const ORK3DModelDisableContinueButtonNotification;
|
||||
extern NSNotificationName const ORK3DModelEnableContinueButtonNotification;
|
||||
extern NSNotificationName const ORK3DModelEndStepNotification;
|
||||
@@ -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
|
||||
+37
-33
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -28,68 +28,72 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKVisualConsentStep.h"
|
||||
|
||||
#import "ORKVisualConsentStepViewController.h"
|
||||
|
||||
#import "ORKConsentDocument_Internal.h"
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORK3DModelStep.h"
|
||||
#import "ORK3DModelStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKVisualConsentStep
|
||||
@implementation ORK3DModelStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORKVisualConsentStepViewController class];
|
||||
return [ORK3DModelStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier document:(ORKConsentDocument *)consentDocument {
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier modelManager:(nonnull ORK3DModelManager *)modelManager {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
|
||||
if (self) {
|
||||
self.consentDocument = consentDocument;
|
||||
_modelManager = modelManager;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKVisualConsentStep *step = [super copyWithZone:zone];
|
||||
step.consentDocument = self.consentDocument;
|
||||
return step;
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, consentDocument, ORKConsentDocument);
|
||||
}
|
||||
return self;
|
||||
- (BOOL)startsFinished {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, consentDocument);
|
||||
- (BOOL)allowsBackNavigation {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORK3DModelStep *step = [super copyWithZone:zone];
|
||||
step->_modelManager = [self.modelManager copy];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self ) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, modelManager, ORK3DModelManager);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, modelManager);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualObjects(self.consentDocument, castObject.consentDocument));
|
||||
return (isParentSame && ORKEqualObjects(self.modelManager, castObject.modelManager));
|
||||
}
|
||||
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return super.hash ^ self.consentDocument.hash;
|
||||
}
|
||||
|
||||
- (BOOL)showsProgress {
|
||||
return NO;
|
||||
return [super hash] ^ [_modelManager hash];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import UIKit;
|
||||
|
||||
#import "ORK3DModelStep.h"
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORK3DModelStepContentView : ORKActiveStepCustomView
|
||||
|
||||
@end
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORK3DModelStepContentView.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKUnitLabel.h"
|
||||
|
||||
@implementation ORK3DModelStepContentView
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORK3DModelStepViewController : ORKActiveStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORK3DModelManager.h"
|
||||
#import "ORK3DModelManager_Internal.h"
|
||||
#import "ORK3DModelStep.h"
|
||||
#import "ORK3DModelStepContentView.h"
|
||||
#import "ORK3DModelStepViewController.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
@implementation ORK3DModelStepViewController {
|
||||
ORK3DModelManager *_modelManager;
|
||||
ORK3DModelStepContentView *_stepContentView;
|
||||
ORK3DModelStep *_step;
|
||||
}
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
self = [super initWithStep:step];
|
||||
|
||||
if (self) {
|
||||
_step = [self threeDimensionalModelStep];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
_stepContentView = [ORK3DModelStepContentView new];
|
||||
_stepContentView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.activeStepView.activeCustomView = _stepContentView;
|
||||
self.activeStepView.customContentFillsAvailableSpace = NO;
|
||||
self.activeStepView.navigationFooterView.neverHasContinueButton = NO;
|
||||
|
||||
[[_stepContentView.bottomAnchor constraintEqualToAnchor:self.activeStepView.navigationFooterView.topAnchor] setActive:YES];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(disableContinueButton:)
|
||||
name:ORK3DModelDisableContinueButtonNotification
|
||||
object:nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(enableContinueButton:)
|
||||
name:ORK3DModelEnableContinueButtonNotification
|
||||
object:nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(endStep:)
|
||||
name:ORK3DModelEndStepNotification
|
||||
object:nil];
|
||||
|
||||
[self activate3DModelManager];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:ORK3DModelDisableContinueButtonNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:ORK3DModelEnableContinueButtonNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:ORK3DModelEndStepNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)activate3DModelManager {
|
||||
_modelManager = _step.modelManager;
|
||||
|
||||
[_modelManager addContentToView:_stepContentView];
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
[super stepDidFinish];
|
||||
|
||||
if (_modelManager) {
|
||||
[_modelManager stepWillEnd];
|
||||
}
|
||||
|
||||
[self goForward];
|
||||
}
|
||||
|
||||
- (ORK3DModelStep *)threeDimensionalModelStep {
|
||||
return (ORK3DModelStep *)self.step;
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *stepResult = [super result];
|
||||
|
||||
if (_modelManager) {
|
||||
NSArray<ORKResult *> *managerResults = [_modelManager provideResultsWithIdentifier:self.step.identifier];
|
||||
if (managerResults) {
|
||||
stepResult.results = [managerResults copy];
|
||||
}
|
||||
}
|
||||
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
#pragma mark - Notification Methods
|
||||
|
||||
- (void)disableContinueButton:(NSNotification *)notification {
|
||||
self.activeStepView.navigationFooterView.continueEnabled = NO;
|
||||
}
|
||||
|
||||
- (void)enableContinueButton:(NSNotification *)notification {
|
||||
self.activeStepView.navigationFooterView.continueEnabled = YES;
|
||||
}
|
||||
|
||||
- (void)endStep:(NSNotification *)notification {
|
||||
[self finish];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKStroopResult.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAccuracyStroopResult : ORKStroopResult
|
||||
|
||||
/**
|
||||
A value that indicates whether the user selected the correct color (i.e. the base display color).
|
||||
*/
|
||||
@property (nonatomic, readonly) BOOL didSelectCorrectColor;
|
||||
|
||||
/**
|
||||
A value that indicates how long it took for the user to make a selection.
|
||||
*/
|
||||
@property (nonatomic, assign) NSTimeInterval timeTakenToSelect;
|
||||
|
||||
/**
|
||||
A value that indicates how far away (in pixels) that the user selected away from the center
|
||||
of the correct circle.
|
||||
*/
|
||||
@property (nonatomic) double distanceToClosestCenter;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAccuracyStroopResult.h"
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@interface ORKAccuracyStroopResult ()
|
||||
@property (readwrite) BOOL didSelectCorrectColor;
|
||||
@end
|
||||
|
||||
@implementation ORKAccuracyStroopResult
|
||||
|
||||
#pragma mark - NSSecureCoding
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
ORK_DECODE_DOUBLE(coder, distanceToClosestCenter);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[super encodeWithCoder:coder];
|
||||
ORK_ENCODE_BOOL(coder, didSelectCorrectColor);
|
||||
ORK_ENCODE_DOUBLE(coder, timeTakenToSelect);
|
||||
ORK_ENCODE_DOUBLE(coder, distanceToClosestCenter);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - NSCopying
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAccuracyStroopResult *result = [super copyWithZone:zone];
|
||||
result.distanceToClosestCenter = self.distanceToClosestCenter;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
self.distanceToClosestCenter == castObject.distanceToClosestCenter);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return [super hash] ^ @(self.didSelectCorrectColor).hash ^ @(self.timeTakenToSelect).hash ^ @(self.distanceToClosestCenter).hash;
|
||||
}
|
||||
|
||||
#pragma mark - ResearchKit
|
||||
|
||||
- (BOOL)didSelectCorrectColor {
|
||||
_didSelectCorrectColor = [self.color isEqualToString:self.colorSelected];
|
||||
return _didSelectCorrectColor;
|
||||
}
|
||||
|
||||
- (NSString *)descriptionWithNumberOfPaddingSpaces:(NSUInteger)numberOfPaddingSpaces {
|
||||
return [NSString stringWithFormat:@"%@; didSelectCorrectColor: %i; timeTakenToSelect: %.3f; distanceToClosestCenter: %.0f %@",
|
||||
[self descriptionPrefixWithNumberOfPaddingSpaces:numberOfPaddingSpaces],
|
||||
self.didSelectCorrectColor,
|
||||
self.timeTakenToSelect,
|
||||
self.distanceToClosestCenter,
|
||||
self.descriptionSuffix];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAccuracyStroopStep : ORKActiveStep
|
||||
|
||||
/**
|
||||
The color of the label.
|
||||
|
||||
The base display color is the color that the user must tap on to be correct. The text of
|
||||
the label may match the base display color depending on the `isColorMatching` property.
|
||||
*/
|
||||
@property (nonatomic) UIColor *baseDisplayColor;
|
||||
|
||||
/**
|
||||
Whether the text and base display color are matching.
|
||||
|
||||
If this value is true, the text of the label will spell out the same color as the base display
|
||||
color, making the task easier for the user. If this value is false, the label color and label text
|
||||
will represent different colors, which adds complexity to the puzzle task.
|
||||
*/
|
||||
@property (nonatomic) BOOL isColorMatching;
|
||||
|
||||
/**
|
||||
The text of the label. (read-only)
|
||||
|
||||
The value of this property is generated based on the `baseDisplayColor` and `isColorMatching`
|
||||
properties. If `isColorMatching` is false, the actual display color will be randomly generated
|
||||
to be a color that is not the base display color.
|
||||
*/
|
||||
@property (nonatomic, readonly) UIColor *actualDisplayColor;
|
||||
|
||||
+ (NSArray <UIColor *> *)colors;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAccuracyStroopStep.h"
|
||||
#import "ORKAccuracyStroopStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@implementation ORKAccuracyStroopStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return ORKAccuracyStroopStepViewController.class;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.baseDisplayColor = ORKAccuracyStroopStep.colors[arc4random_uniform(ORKAccuracyStroopStep.colors.count)];
|
||||
self.isColorMatching = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_BOOL(aDecoder, isColorMatching);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, baseDisplayColor, UIColor);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_BOOL(aCoder, isColorMatching);
|
||||
ORK_ENCODE_OBJ(aCoder, baseDisplayColor);
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAccuracyStroopStep *step = [super copyWithZone:zone];
|
||||
step.isColorMatching = self.isColorMatching;
|
||||
step.baseDisplayColor = [self.baseDisplayColor copy];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
|
||||
return isParentSame
|
||||
&& self.isColorMatching == castObject.isColorMatching
|
||||
&& ORKEqualObjects(self.baseDisplayColor, castObject.baseDisplayColor);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return super.hash
|
||||
^ (self.isColorMatching ? 0xf : 0x0)
|
||||
^ (self.baseDisplayColor ? 0xf : 0x0);
|
||||
}
|
||||
|
||||
+ (NSArray<UIColor *> *)colors {
|
||||
return @[ UIColor.systemRedColor,
|
||||
UIColor.systemGreenColor,
|
||||
UIColor.systemBlueColor,
|
||||
UIColor.systemYellowColor,
|
||||
UIColor.systemOrangeColor ];
|
||||
}
|
||||
|
||||
- (UIColor *)actualDisplayColor {
|
||||
return self.isColorMatching ?
|
||||
self.baseDisplayColor :
|
||||
ORKAccuracyStroopStep.colors[arc4random_uniform(ORKAccuracyStroopStep.colors.count)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKStepViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKAccuracyStroopStepViewController : ORKStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAccuracyStroopStepViewController.h"
|
||||
#import "ORKAccuracyStroopStep.h"
|
||||
#import "ORKAccuracyStroopResult.h"
|
||||
|
||||
#import "ORKCollectionResult.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "UIColor+String.h"
|
||||
|
||||
@interface ORKAccuracyStroopStepViewController () <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic) NSMutableArray <UIView *> *circles;
|
||||
@property (nonatomic, strong) UILabel *colorLabel;
|
||||
@property (nonatomic) UIView *circlesView;
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *constraints;
|
||||
@property (nonatomic) double distanceToClosestCenter;
|
||||
@property (nonatomic) UIColor *selectedColor;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKAccuracyStroopStepViewController
|
||||
|
||||
- (ORKAccuracyStroopStep *)accuracyStroopStep {
|
||||
return (ORKAccuracyStroopStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
|
||||
if (self.step && [self isViewLoaded]) {
|
||||
[self setupColorLabel];
|
||||
[self setupCirclesView];
|
||||
[self setupConstraints];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self setupCircles];
|
||||
[self setupViewTap];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupViewTap {
|
||||
for (UIGestureRecognizer *recognizer in self.circlesView.gestureRecognizers) {
|
||||
[self.circlesView removeGestureRecognizer:recognizer];
|
||||
}
|
||||
|
||||
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||
tapGestureRecognizer.delegate = self;
|
||||
[self.circlesView addGestureRecognizer:tapGestureRecognizer];
|
||||
}
|
||||
|
||||
- (void)setupCirclesView {
|
||||
[self.circlesView removeFromSuperview];
|
||||
self.circlesView = nil;
|
||||
|
||||
self.circlesView = UIView.new;
|
||||
[self.view addSubview:self.circlesView];
|
||||
}
|
||||
|
||||
- (void)setupColorLabel {
|
||||
[self.colorLabel removeFromSuperview];
|
||||
self.colorLabel = nil;
|
||||
|
||||
self.colorLabel = UILabel.new;
|
||||
self.colorLabel.text = self.accuracyStroopStep.actualDisplayColor.textRepresentation;
|
||||
self.colorLabel.textColor = self.accuracyStroopStep.baseDisplayColor;
|
||||
self.colorLabel.font = [UIFont systemFontOfSize:35.0 weight:UIFontWeightMedium];
|
||||
[self.view addSubview:self.colorLabel];
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
if (self.constraints) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
}
|
||||
self.colorLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.circlesView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.constraints = nil;
|
||||
self.constraints = @[
|
||||
[NSLayoutConstraint constraintWithItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:10.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeLeading
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeLeading
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeTrailing
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeTrailing
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0]
|
||||
];
|
||||
[NSLayoutConstraint activateConstraints:self.constraints];
|
||||
}
|
||||
|
||||
- (void)setupCircles {
|
||||
for (UIView *circle in self.circles) {
|
||||
[circle removeFromSuperview];
|
||||
}
|
||||
|
||||
[self.circles removeAllObjects];
|
||||
self.circles = NSMutableArray.array;
|
||||
|
||||
// Constants to use for ball and grid
|
||||
int ballSize = 50;
|
||||
int padding = 10;
|
||||
int cellSize = ballSize + padding * 2;
|
||||
|
||||
// Calculating number of rows/columns in grid to layout color circles
|
||||
uint32_t numRows = (self.circlesView.bounds.size.height) / cellSize;
|
||||
uint32_t numColumns = (self.circlesView.bounds.size.width) / cellSize;
|
||||
|
||||
// Extra padding to ensure that the grid spans the whole screen width
|
||||
int extraHorizontalSpaceForCell = ((int)self.circlesView.bounds.size.width % cellSize) / numColumns;
|
||||
|
||||
// Matrix to keep track of cells that already have a circle --> avoid overlap in O(n)
|
||||
bool cellTakenMatrix[numRows][numColumns];
|
||||
for (uint32_t r = 0; r < numRows; r++) {
|
||||
for (uint32_t c = 0; c < numColumns; c++) {
|
||||
cellTakenMatrix[r][c] = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int colorIndex = 0; colorIndex < ORKAccuracyStroopStep.colors.count; colorIndex++) {
|
||||
// Obtain random location for color circle within bounds
|
||||
int randomR = (int)arc4random_uniform(numRows);
|
||||
int randomC = (int)arc4random_uniform(numColumns);
|
||||
|
||||
ORK_Log_Debug("Trying placement for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
|
||||
|
||||
// If cell is already taken, look at 8 spots around for a free spot
|
||||
if (cellTakenMatrix[randomR][randomC]) {
|
||||
ORK_Log_Debug("Position (r, c): (%d, %d) already taken", randomR, randomC);
|
||||
|
||||
// Loops through the 3x3 grid with randomR,randomC as the center
|
||||
bool shouldBreak = false;
|
||||
for (int r = randomR - 1; !shouldBreak && r <= randomR + 1; r++) {
|
||||
for (int c = randomC - 1; !shouldBreak && c <= randomC + 1; c++) {
|
||||
// If r/c are out of circleView's bounds, then don't consider
|
||||
if ((r < 0 || r >= numRows) || (c < 0 || c >= numColumns)) { continue; }
|
||||
|
||||
// If cell is not taken, then can assign to there and break out of for-loops
|
||||
if (!cellTakenMatrix[r][c]) {
|
||||
randomR = r;
|
||||
randomC = c;
|
||||
shouldBreak = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ORK_Log_Info("Final position for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
|
||||
|
||||
cellTakenMatrix[randomR][randomC] = true;
|
||||
|
||||
CGFloat circleX = (randomC * (cellSize + extraHorizontalSpaceForCell)) + padding + extraHorizontalSpaceForCell / 2;
|
||||
CGFloat circleY = (randomR * cellSize) + padding;
|
||||
CGRect frame = CGRectMake(circleX, circleY, ballSize, ballSize);
|
||||
UIView *newCircle = [[UIView alloc] initWithFrame:frame];
|
||||
newCircle.backgroundColor = ORKAccuracyStroopStep.colors[colorIndex];
|
||||
newCircle.clipsToBounds = YES;
|
||||
newCircle.layer.cornerRadius = ballSize / 2;
|
||||
newCircle.tag = colorIndex;
|
||||
[self.circles addObject:newCircle];
|
||||
[self.circlesView addSubview:newCircle];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
|
||||
CGPoint touchPoint = [recognizer locationInView:self.circlesView];
|
||||
double minDistance = INFINITY;
|
||||
|
||||
for (UIView *circle in self.circles) {
|
||||
double dx = (touchPoint.x - circle.center.x);
|
||||
double dy = (touchPoint.y - circle.center.y);
|
||||
double distance = sqrt(dx * dx + dy * dy);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
}
|
||||
|
||||
if (CGRectContainsPoint(circle.frame, touchPoint)) {
|
||||
self.selectedColor = ORKAccuracyStroopStep.colors[circle.tag];
|
||||
self.distanceToClosestCenter = distance;
|
||||
[super goForward];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.distanceToClosestCenter = minDistance;
|
||||
[super goForward];
|
||||
}
|
||||
|
||||
- (BOOL)hasPreviousStep {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *stepResult = [super result];
|
||||
|
||||
ORKAccuracyStroopResult *result = [[ORKAccuracyStroopResult alloc] initWithIdentifier:self.accuracyStroopStep.identifier];
|
||||
result.color = self.accuracyStroopStep.baseDisplayColor.textRepresentation;
|
||||
result.colorSelected = self.selectedColor.textRepresentation;
|
||||
result.distanceToClosestCenter = self.distanceToClosestCenter;
|
||||
result.startDate = stepResult.startDate;
|
||||
result.endDate = stepResult.endDate;
|
||||
result.timeTakenToSelect = [result.endDate timeIntervalSinceDate:result.startDate];
|
||||
|
||||
NSMutableArray *results = [[NSMutableArray alloc] init];
|
||||
if (stepResult.results) {
|
||||
results = [stepResult.results mutableCopy];
|
||||
}
|
||||
|
||||
[results addObject:result];
|
||||
|
||||
stepResult.results = [results copy];
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self stepDidChange];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -30,30 +30,26 @@
|
||||
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@import UIKit;
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
|
||||
static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
static mach_timebase_info_data_t sTimebaseInfo;
|
||||
if ( sTimebaseInfo.denom == 0 ) {
|
||||
(void) mach_timebase_info(&sTimebaseInfo);
|
||||
static mach_timebase_info_data_t sTimebaseInfo;
|
||||
if (sTimebaseInfo.denom == 0) {
|
||||
(void)mach_timebase_info(&sTimebaseInfo);
|
||||
}
|
||||
uint64_t elapsedNano = delta * sTimebaseInfo.numer / sTimebaseInfo.denom;
|
||||
return elapsedNano * 1.0 / NSEC_PER_SEC;
|
||||
}
|
||||
|
||||
|
||||
@implementation ORKActiveStepTimer {
|
||||
uint64_t _startTime;
|
||||
NSTimeInterval _preExistingRuntime;
|
||||
dispatch_queue_t _queue;
|
||||
dispatch_source_t _timer;
|
||||
UIBackgroundTaskIdentifier _backgroundTaskIdentifier;
|
||||
uint32_t _isRunning;
|
||||
}
|
||||
|
||||
@@ -68,7 +64,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
_interval = interval;
|
||||
_handler = [handler copy];
|
||||
_preExistingRuntime = runtime;
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
|
||||
_queue = dispatch_queue_create("active_step", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
@@ -128,7 +123,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
}
|
||||
|
||||
- (void)queue_event {
|
||||
[self queue_assertBackgroundTask];
|
||||
|
||||
NSTimeInterval runtime = [self queue_runtime];
|
||||
BOOL finished = (runtime >= _duration);
|
||||
@@ -138,13 +132,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
_handler(self, finished);
|
||||
dispatch_sync(_queue, ^{
|
||||
|
||||
// If the timer is still NULL here, we can safely release the background task.
|
||||
if (_timer == NULL) {
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,29 +143,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)queue_releaseBackgroundTask {
|
||||
if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
|
||||
return;
|
||||
}
|
||||
UIBackgroundTaskIdentifier identifier = _backgroundTaskIdentifier;
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[[UIApplication sharedApplication] endBackgroundTask:identifier];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)queue_assertBackgroundTask {
|
||||
if (_backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
|
||||
return;
|
||||
}
|
||||
_backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
||||
// This is guaranteed to be called synchronously on the main queue, switch to our queue to invalidate the identifier
|
||||
dispatch_sync(_queue, ^{
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)queue_resume {
|
||||
if (_timer != NULL) {
|
||||
// Already resumed
|
||||
@@ -190,11 +154,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We want to run in the background if we can, so voice can be played, etc.
|
||||
assert(_backgroundTaskIdentifier == UIBackgroundTaskInvalid);
|
||||
|
||||
[self queue_assertBackgroundTask];
|
||||
|
||||
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
|
||||
0, 0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
if (_timer == NULL) {
|
||||
@@ -228,16 +187,11 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
_preExistingRuntime += timeIntervalFromMachTime(now - _startTime);
|
||||
_startTime = 0;
|
||||
|
||||
if (!atFinish) {
|
||||
// If we are atFinish, the task will be released after the handler completes
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)queue_reset {
|
||||
[self queue_clearTimer];
|
||||
_preExistingRuntime = 0;
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKActiveStepTimerView.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
#import "ORKStepHeaderView_Internal.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
@@ -118,30 +119,30 @@
|
||||
|
||||
- (void)setActiveStepView {
|
||||
if (!_activeStepView) {
|
||||
_activeStepView = [[ORKActiveStepView alloc] initWithFrame:self.view.bounds];
|
||||
_activeStepView = [ORKActiveStepView new];
|
||||
[_activeStepView placeNavigationContainerInsideScrollView];
|
||||
}
|
||||
if (_customView) {
|
||||
_activeStepView.customContentView = _customView;
|
||||
}
|
||||
[_activeStepView setCustomView:_customView];
|
||||
_activeStepView.headerView.learnMoreButtonItem = self.learnMoreButtonItem;
|
||||
[self.view addSubview:_activeStepView];
|
||||
}
|
||||
|
||||
- (void)setNavigationFooterView {
|
||||
if (!_navigationFooterView) {
|
||||
_navigationFooterView = [ORKNavigationContainerView new];
|
||||
_navigationFooterView = _activeStepView.navigationFooterView;
|
||||
}
|
||||
_navigationFooterView.skipButtonItem = self.skipButtonItem;
|
||||
_navigationFooterView.continueEnabled = _finished;
|
||||
|
||||
|
||||
ORKActiveStep *step = [self activeStep];
|
||||
_navigationFooterView.useNextForSkip = step.shouldUseNextAsSkipButton;
|
||||
_navigationFooterView.optional = step.optional;
|
||||
_navigationFooterView.cancelButtonItem = self.cancelButtonItem;
|
||||
BOOL neverHasContinueButton = (step.shouldContinueOnFinish && !step.startsFinished);
|
||||
[_navigationFooterView setNeverHasContinueButton:neverHasContinueButton];
|
||||
[_navigationFooterView updateContinueAndSkipEnabled];
|
||||
|
||||
|
||||
[self updateContinueButtonItem];
|
||||
[self.view addSubview:_navigationFooterView];
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
@@ -150,61 +151,38 @@
|
||||
}
|
||||
_constraints = nil;
|
||||
|
||||
UIView *viewForiPad = [self viewForiPadLayoutConstraints];
|
||||
|
||||
_activeStepView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_navigationFooterView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
_constraints = @[
|
||||
[NSLayoutConstraint constraintWithItem:_activeStepView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:viewForiPad ? : self.view.safeAreaLayoutGuide
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_activeStepView
|
||||
attribute:NSLayoutAttributeLeft
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:viewForiPad ? : self.view.safeAreaLayoutGuide
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeLeft
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_activeStepView
|
||||
attribute:NSLayoutAttributeRight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:viewForiPad ? : self.view.safeAreaLayoutGuide
|
||||
attribute:NSLayoutAttributeRight
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_navigationFooterView
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:viewForiPad ? : self.view
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_navigationFooterView
|
||||
attribute:NSLayoutAttributeLeft
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:viewForiPad ? : self.view
|
||||
attribute:NSLayoutAttributeLeft
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_navigationFooterView
|
||||
attribute:NSLayoutAttributeRight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:viewForiPad ? : self.view
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeRight
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_activeStepView
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_navigationFooterView
|
||||
attribute:NSLayoutAttributeTop
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
constant:0.0]
|
||||
|
||||
];
|
||||
[NSLayoutConstraint activateConstraints:_constraints];
|
||||
@@ -217,31 +195,25 @@
|
||||
[self prepareStep];
|
||||
}
|
||||
|
||||
- (UIView *)customViewContainer {
|
||||
__unused UIView *view = [self view];
|
||||
return _activeStepView.customViewContainer;
|
||||
}
|
||||
|
||||
- (ORKTintedImageView *)imageView {
|
||||
__unused UIView *view = [self view];
|
||||
return _activeStepView.imageView;
|
||||
}
|
||||
|
||||
- (void)setCustomView:(UIView *)customView {
|
||||
_customView = customView;
|
||||
[_activeStepView setStepView:_customView];
|
||||
if (_customView) {
|
||||
[_activeStepView setCustomContentView:_customView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
|
||||
[self.taskViewController setRegisteredScrollView:_activeStepView];
|
||||
|
||||
if (_activeStepView.navigationFooterView) {
|
||||
[_activeStepView.navigationFooterView flattenIfNeeded];
|
||||
}
|
||||
ORK_Log_Debug("%@",self);
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
ORK_Log_Debug("%@",self);
|
||||
|
||||
// Wait for animation complete
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@@ -256,7 +228,7 @@
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
ORK_Log_Debug("%@",self);
|
||||
|
||||
[self suspend];
|
||||
}
|
||||
@@ -272,7 +244,6 @@
|
||||
|
||||
- (void)setLearnMoreButtonItem:(UIBarButtonItem *)learnMoreButtonItem {
|
||||
[super setLearnMoreButtonItem:learnMoreButtonItem];
|
||||
_activeStepView.headerView.learnMoreButtonItem = self.learnMoreButtonItem;
|
||||
}
|
||||
|
||||
- (void)setSkipButtonItem:(UIBarButtonItem *)skipButtonItem {
|
||||
@@ -282,7 +253,6 @@
|
||||
|
||||
- (void)setCancelButtonItem:(UIBarButtonItem *)cancelButtonItem {
|
||||
[super setCancelButtonItem:cancelButtonItem];
|
||||
_navigationFooterView.cancelButtonItem = cancelButtonItem;
|
||||
}
|
||||
|
||||
- (void)setFinished:(BOOL)finished {
|
||||
@@ -345,7 +315,7 @@
|
||||
|
||||
self.finished = [[self activeStep] startsFinished];
|
||||
|
||||
ORK_Log_Debug(@"%@", self);
|
||||
ORK_Log_Debug("%@", self);
|
||||
_activeStepView.activeStep = self.activeStep;
|
||||
|
||||
if ([self.activeStep hasCountDown]) {
|
||||
@@ -386,7 +356,7 @@
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
ORK_Log_Debug("%@",self);
|
||||
self.started = YES;
|
||||
[self startTimer];
|
||||
[_activeStepView.activeCustomView startStep:self];
|
||||
@@ -412,7 +382,7 @@
|
||||
}
|
||||
|
||||
- (void)suspend {
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
ORK_Log_Debug("%@",self);
|
||||
if (self.finished || !self.started) {
|
||||
return;
|
||||
}
|
||||
@@ -424,7 +394,7 @@
|
||||
}
|
||||
|
||||
- (void)resume {
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
ORK_Log_Debug("%@",self);
|
||||
if (self.finished || !self.started) {
|
||||
return;
|
||||
}
|
||||
@@ -436,7 +406,7 @@
|
||||
}
|
||||
|
||||
- (void)finish {
|
||||
ORK_Log_Debug(@"%@",self);
|
||||
ORK_Log_Debug("%@",self);
|
||||
if (self.finished) {
|
||||
return;
|
||||
}
|
||||
@@ -520,7 +490,15 @@
|
||||
BOOL isHalfway = !_hasSpokenHalfwayCountdown && timer.runtime > timer.duration / 2.0;
|
||||
if (!finished && self.activeStep.shouldSpeakRemainingTimeAtHalfway && !UIAccessibilityIsVoiceOverRunning() && isHalfway) {
|
||||
_hasSpokenHalfwayCountdown = YES;
|
||||
NSString *text = [NSString localizedStringWithFormat:ORKLocalizedString(@"COUNTDOWN_SPOKEN_REMAINING_%@", nil), @(countDownValue)];
|
||||
|
||||
NSDateComponentsFormatter *secondsFormatter = [NSDateComponentsFormatter new];
|
||||
secondsFormatter.unitsStyle = NSDateComponentsFormatterUnitsStyleSpellOut;
|
||||
secondsFormatter.allowedUnits = NSCalendarUnitSecond;
|
||||
secondsFormatter.formattingContext = NSFormattingContextDynamic;
|
||||
secondsFormatter.maximumUnitCount = 1;
|
||||
NSString *seconds = [secondsFormatter stringFromTimeInterval:countDownValue];
|
||||
NSString *text = [NSString localizedStringWithFormat:ORKLocalizedString(@"COUNTDOWN_SPOKEN_REMAINING_%@", nil), seconds];
|
||||
|
||||
[voice speakText:text];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKActiveStepViewController.h"
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@@ -56,8 +56,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, assign, getter=isStarted) BOOL started;
|
||||
|
||||
@property (nonatomic, strong, readonly) ORKNavigationContainerView *navigationFooterView;
|
||||
|
||||
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished; // Let subclass receive timer fires
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKActiveStep.h"
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -32,25 +32,28 @@
|
||||
|
||||
@interface ORKAmslerGridContentView() {
|
||||
UIBezierPath *path;
|
||||
CGFloat dimension;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation ORKAmslerGridContentView
|
||||
|
||||
- (void)plotAmslerGrid {
|
||||
dimension = MIN(self.bounds.size.width, self.bounds.size.height);
|
||||
|
||||
path = [[UIBezierPath alloc] init];
|
||||
path.lineWidth = _lineWidth;
|
||||
|
||||
CGFloat cellSize = MIN(self.bounds.size.width, self.bounds.size.height)/_numberOfCellsPerSide;
|
||||
CGFloat cellSize = dimension/_numberOfCellsPerSide;
|
||||
|
||||
for (int index = 0; index < _numberOfCellsPerSide; index ++) {
|
||||
CGPoint startVertical = CGPointMake((CGFloat)index * cellSize, 0);
|
||||
CGPoint endVertical = CGPointMake((CGFloat)index * cellSize, self.bounds.size.height);
|
||||
CGPoint endVertical = CGPointMake((CGFloat)index * cellSize, dimension);
|
||||
[path moveToPoint:startVertical];
|
||||
[path addLineToPoint:endVertical];
|
||||
|
||||
CGPoint startHorizontal = CGPointMake(0, (CGFloat)index * cellSize);
|
||||
CGPoint endHorizontal = CGPointMake(self.bounds.size.width, (CGFloat)index * cellSize);
|
||||
CGPoint endHorizontal = CGPointMake(dimension, (CGFloat)index * cellSize);
|
||||
[path moveToPoint:startHorizontal];
|
||||
[path addLineToPoint:endHorizontal];
|
||||
}
|
||||
@@ -65,15 +68,11 @@
|
||||
[self plotAmslerGrid];
|
||||
[_lineColor setStroke];
|
||||
[path stroke];
|
||||
UIBezierPath *circleInTheCenter = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2) radius:self.bounds.size.width/_ratioOfWidthToRadius startAngle:0 endAngle:360 clockwise:YES];
|
||||
UIBezierPath *circleInTheCenter = [UIBezierPath bezierPathWithArcCenter:CGPointMake(dimension/2, dimension/2) radius:dimension/_ratioOfWidthToRadius startAngle:0 endAngle:360 clockwise:YES];
|
||||
[_lineColor setFill];
|
||||
[circleInTheCenter fill];
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
@@ -83,8 +82,21 @@
|
||||
_ratioOfWidthToRadius = 75;
|
||||
_lineColor = [UIColor blackColor];
|
||||
_backgroundColor = [UIColor whiteColor];
|
||||
[self setDimensionConstraint];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setDimensionConstraint {
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeWidth
|
||||
multiplier:1.0
|
||||
constant:0.0]
|
||||
]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -76,12 +76,10 @@
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = [UIColor blackColor];
|
||||
[self.navigationController setNavigationBarHidden:YES animated:NO];
|
||||
[self.navigationFooterView setHidden:YES];
|
||||
_amslerGridView = [ORKAmslerGridContentView new];
|
||||
_amslerGridView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.activeStepView.activeCustomView = _amslerGridView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
[self.activeStepView removeCustomContentPadding];
|
||||
|
||||
_freehandDrawingView = [ORKFreehandDrawingView new];
|
||||
|
||||
@@ -91,9 +89,8 @@
|
||||
|
||||
[_amslerGridView addSubview:_freehandDrawingView];
|
||||
|
||||
UISwipeGestureRecognizer *r = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
|
||||
r.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
[self.activeStepView addGestureRecognizer:r];
|
||||
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
|
||||
[self.activeStepView addGestureRecognizer:panGestureRecognizer];
|
||||
|
||||
self.activeStepView.isAccessibilityElement = YES;
|
||||
self.activeStepView.accessibilityLabel = ORKLocalizedString(@"AX_AMSLER_GRID_LABEL", nil);
|
||||
@@ -102,42 +99,15 @@
|
||||
[self setupContraints];
|
||||
}
|
||||
|
||||
- (void)handleSingleTap:(UISwipeGestureRecognizer *)recognizer {
|
||||
[self finish];
|
||||
- (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer {
|
||||
if (recognizer.state == UIGestureRecognizerStateChanged) {
|
||||
[self finish];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupContraints {
|
||||
CGFloat width = MIN(self.view.bounds.size.width, self.view.bounds.size.height);
|
||||
NSArray *constraints = @[
|
||||
|
||||
[NSLayoutConstraint constraintWithItem:_amslerGridView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:width],
|
||||
[NSLayoutConstraint constraintWithItem:_amslerGridView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:width],
|
||||
[NSLayoutConstraint constraintWithItem:_amslerGridView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_amslerGridView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_freehandDrawingView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKRecordButton;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, ORKAudioContentViewEvent) {
|
||||
ORKAudioContentViewEventStartRecording = 0,
|
||||
ORKAudioContentViewEventStopRecording
|
||||
};
|
||||
|
||||
typedef void (^ORKAudioStepContentViewEventHandler)(ORKAudioContentViewEvent);
|
||||
|
||||
@interface ORKAudioContentView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic, copy, nullable) UIColor *keyColor;
|
||||
@@ -48,6 +57,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, copy, nullable) NSArray *samples;
|
||||
|
||||
@property (nonatomic) BOOL useRecordButton;
|
||||
|
||||
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler;
|
||||
|
||||
// Samples should be in the range of (0, 1).
|
||||
- (void)addSample:(NSNumber *)sample;
|
||||
- (void)removeAllSamples;
|
||||
@@ -55,3 +68,4 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
#import "ORKAudioContentView.h"
|
||||
#import "ORKAudioGraphView.h"
|
||||
#import "ORKAudioMeteringView.h"
|
||||
|
||||
#import "ORKHeadlineLabel.h"
|
||||
#import "ORKLabel.h"
|
||||
@@ -38,6 +39,7 @@
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKRecordButton.h"
|
||||
|
||||
|
||||
// The central blue region.
|
||||
@@ -46,6 +48,8 @@ static const CGFloat GraphViewBlueZoneHeight = 170;
|
||||
// The two bands at top and bottom which are "loud" each have this height.
|
||||
static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
|
||||
static const CGFloat ORKAudioStepContentRecordButtonVerticalSpacing = 20.0;
|
||||
|
||||
@interface ORKAudioTimerLabel : ORKLabel
|
||||
|
||||
@end
|
||||
@@ -61,11 +65,12 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKAudioContentView ()
|
||||
@interface ORKAudioContentView () <ORKRecordButtonDelegate>
|
||||
|
||||
@property (nonatomic, strong) ORKHeadlineLabel *alertLabel;
|
||||
@property (nonatomic, strong) UILabel *timerLabel;
|
||||
@property (nonatomic, strong) ORKAudioGraphView *graphView;
|
||||
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
|
||||
@property (nonatomic, copy, nullable) ORKAudioStepContentViewEventHandler viewEventhandler;
|
||||
|
||||
@end
|
||||
|
||||
@@ -73,19 +78,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
@implementation ORKAudioContentView {
|
||||
NSMutableArray *_samples;
|
||||
UIColor *_keyColor;
|
||||
ORKRecordButton *_recordButton;
|
||||
BOOL _checkAudioLevel;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
|
||||
_checkAudioLevel = YES;
|
||||
_useRecordButton = NO;
|
||||
|
||||
self.alertLabel = [ORKHeadlineLabel new];
|
||||
_alertLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.timerLabel = [ORKAudioTimerLabel new];
|
||||
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_timerLabel.textAlignment = NSTextAlignmentRight;
|
||||
self.graphView = [ORKAudioGraphView new];
|
||||
self.graphView = [[ORKAudioMeteringView alloc] init];
|
||||
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
@@ -95,8 +104,8 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
[self addSubview:_timerLabel];
|
||||
[self addSubview:_graphView];
|
||||
|
||||
_timerLabel.text = @"06:00";
|
||||
_alertLabel.text = ORKLocalizedString(@"AUDIO_TOO_LOUD_LABEL", nil);
|
||||
// _timerLabel.text set in -updateTimerLabel:
|
||||
|
||||
self.alertThreshold = GraphViewBlueZoneHeight / ((GraphViewRedZoneHeight * 2) + GraphViewBlueZoneHeight);
|
||||
|
||||
@@ -122,10 +131,22 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
[self updateAlertLabelHidden];
|
||||
}
|
||||
|
||||
- (void)setUseRecordButton:(BOOL)useRecordButton {
|
||||
_useRecordButton = useRecordButton;
|
||||
|
||||
if (_useRecordButton) {
|
||||
_checkAudioLevel = NO;
|
||||
[_timerLabel setHidden: YES];
|
||||
|
||||
[self setupRecordButton];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applyKeyColor {
|
||||
UIColor *keyColor = [self keyColor];
|
||||
_timerLabel.textColor = keyColor;
|
||||
_graphView.keyColor = keyColor;
|
||||
_graphView.meterColor = keyColor;
|
||||
}
|
||||
|
||||
- (UIColor *)keyColor {
|
||||
@@ -143,6 +164,44 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
_graphView.alertColor = alertColor;
|
||||
}
|
||||
|
||||
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler {
|
||||
self.viewEventhandler = [handler copy];
|
||||
}
|
||||
|
||||
- (void)invokeViewEventHandlerWithEvent:(ORKAudioContentViewEvent)event {
|
||||
if (self.viewEventhandler) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
self.viewEventhandler(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)buttonPressed:(ORKRecordButton *)recordButton {
|
||||
switch (recordButton.buttonType) {
|
||||
case ORKRecordButtonTypeRecord:
|
||||
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStartRecording];
|
||||
[_recordButton setButtonType:ORKRecordButtonTypeStop];
|
||||
break;
|
||||
default:
|
||||
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStopRecording];
|
||||
[_recordButton setButtonState:ORKRecordButtonStateDisabled];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupRecordButton {
|
||||
if (!_recordButton) {
|
||||
_recordButton = [[ORKRecordButton alloc] init];
|
||||
_recordButton.delegate = self;
|
||||
_recordButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[_recordButton setButtonType:ORKRecordButtonTypeRecord];
|
||||
|
||||
[self addSubview:_recordButton];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
|
||||
@@ -161,12 +220,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
|
||||
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
|
||||
const CGFloat innerMargin = 2;
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
|
||||
views:views]];
|
||||
|
||||
if (_useRecordButton) {
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:@{@"sideMargin": @(sideMargin)}
|
||||
views:views]];
|
||||
|
||||
[constraints addObject:[_recordButton.topAnchor constraintEqualToAnchor:_graphView.bottomAnchor constant:ORKAudioStepContentRecordButtonVerticalSpacing]];
|
||||
[constraints addObject:[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor]];
|
||||
} else {
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
|
||||
views:views]];
|
||||
}
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_graphView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
@@ -206,18 +276,21 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
}
|
||||
|
||||
- (void)updateGraphSamples {
|
||||
_graphView.values = _samples;
|
||||
_graphView.samples = _samples;
|
||||
[self updateAlertLabelHidden];
|
||||
}
|
||||
|
||||
- (void)updateAlertLabelHidden {
|
||||
NSNumber *sample = _samples.lastObject;
|
||||
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
|
||||
|
||||
if (_alertLabel.hidden && show) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
|
||||
if (_checkAudioLevel) {
|
||||
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
|
||||
|
||||
if (_alertLabel.hidden && show) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
|
||||
}
|
||||
_alertLabel.hidden = !show;
|
||||
}
|
||||
_alertLabel.hidden = !show;
|
||||
}
|
||||
|
||||
- (void)setSamples:(NSArray *)samples {
|
||||
@@ -260,3 +333,4 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
#import <ResearchKit/ORKFitnessStep.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKBundleAsset;
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKVocalCue : NSObject <NSSecureCoding, NSCopying>
|
||||
|
||||
@property (atomic) NSTimeInterval time;
|
||||
|
||||
@property (atomic, copy) NSString *spokenText;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithTime:(NSTimeInterval) time
|
||||
spokenText:(NSString *) spokenText;
|
||||
@end
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAudioFitnessStep : ORKFitnessStep
|
||||
|
||||
@property (nonatomic, copy) ORKBundleAsset *audioAsset;
|
||||
|
||||
@property (nonatomic, copy) NSArray<ORKVocalCue *> *vocalCues;
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *) identifier
|
||||
audioAsset:(ORKBundleAsset *) audioAsset
|
||||
vocalCues:(nullable NSArray<ORKVocalCue *> *) vocalCues;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAudioFitnessStep.h"
|
||||
#import "ORKAudioFitnessStepViewController.h"
|
||||
#import "ORKBundleAsset.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@implementation ORKVocalCue
|
||||
|
||||
- (instancetype)initWithTime:(NSTimeInterval)time
|
||||
spokenText:(NSString *)spokenText {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.time = time;
|
||||
self.spokenText = [spokenText copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if (self) {
|
||||
ORK_DECODE_DOUBLE(coder, time);
|
||||
ORK_DECODE_OBJ_CLASS(coder, spokenText, NSString);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
ORK_ENCODE_DOUBLE(coder, time);
|
||||
ORK_ENCODE_OBJ(coder, spokenText);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
return [[ORKVocalCue alloc] initWithTime:self.time spokenText:self.spokenText];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
if ([self class] != [other class]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__typeof(self) castObject = other;
|
||||
return (self.time == castObject.time &&
|
||||
ORKEqualObjects(self.spokenText, castObject.spokenText));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKAudioFitnessStep
|
||||
|
||||
- (Class)stepViewControllerClass {
|
||||
return [ORKAudioFitnessStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
audioAsset:(ORKBundleAsset *)audioAsset
|
||||
vocalCues:(nullable NSArray<ORKVocalCue *> *)vocalCues {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.stepDuration = 180;
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
self.audioAsset = [audioAsset copy];
|
||||
self.vocalCues = vocalCues == nil ? [NSArray new] : [vocalCues copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(coder, audioAsset, ORKBundleAsset);
|
||||
ORK_DECODE_OBJ_ARRAY(coder, vocalCues, ORKVocalCue);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
[super encodeWithCoder:coder];
|
||||
ORK_ENCODE_OBJ(coder, audioAsset);
|
||||
ORK_ENCODE_OBJ(coder, vocalCues);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAudioFitnessStep *step = [super copyWithZone:zone];
|
||||
step.audioAsset = [self.audioAsset copy];
|
||||
step.vocalCues = [self.vocalCues copy];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
BOOL superIsEqual = [super isEqual:other];
|
||||
|
||||
__typeof(self) castObject = other;
|
||||
return (superIsEqual &&
|
||||
ORKEqualObjects(self.audioAsset, castObject.audioAsset) &&
|
||||
ORKEqualObjects(self.vocalCues, castObject.vocalCues));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return super.hash ^ self.audioAsset.hash ^ self.vocalCues.hash;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
#import <ResearchKit/ORKFitnessStepViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Test Seam
|
||||
@protocol ORKAudioPlayer
|
||||
- (BOOL)prepareToPlay;
|
||||
- (BOOL)play;
|
||||
- (void)pause;
|
||||
- (void)stop;
|
||||
@end
|
||||
|
||||
@interface ORKAudioFitnessStepViewController : ORKFitnessStepViewController
|
||||
|
||||
@property (nonatomic) id<ORKAudioPlayer> audioPlayer;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKAudioFitnessStep.h"
|
||||
#import "ORKAudioFitnessStepViewController.h"
|
||||
#import "ORKVoiceEngine.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface ORKAVAudioPlayer : AVAudioPlayer <ORKAudioPlayer>
|
||||
@end
|
||||
|
||||
@implementation ORKAVAudioPlayer
|
||||
@end
|
||||
|
||||
@interface ORKAudioFitnessStepViewController ()
|
||||
@property (nonatomic) BOOL appHasAudioBackgroundMode;
|
||||
@property (nonatomic) NSMutableSet<ORKVocalCue *> *playedCues;
|
||||
@end
|
||||
|
||||
@implementation ORKAudioFitnessStepViewController
|
||||
|
||||
- (ORKAudioFitnessStep *)audioStep {
|
||||
return (ORKAudioFitnessStep *)self.step;
|
||||
}
|
||||
|
||||
- (NSMutableSet *)playedCues {
|
||||
if (!_playedCues) {
|
||||
_playedCues = [NSMutableSet new];
|
||||
}
|
||||
return _playedCues;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self.audioPlayer prepareToPlay];
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
|
||||
if (self.appHasAudioBackgroundMode) {
|
||||
[self enableBackgroundAudioSession:YES];
|
||||
}
|
||||
|
||||
[self.audioPlayer play];
|
||||
}
|
||||
|
||||
- (void)suspend {
|
||||
[super suspend];
|
||||
[self.audioPlayer pause];
|
||||
}
|
||||
|
||||
- (void)resume {
|
||||
[super resume];
|
||||
[self.audioPlayer play];
|
||||
}
|
||||
|
||||
- (void)finish {
|
||||
[super finish];
|
||||
[self.audioPlayer stop];
|
||||
|
||||
if (self.appHasAudioBackgroundMode) {
|
||||
[self enableBackgroundAudioSession:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
|
||||
[super countDownTimerFired:timer finished:finished];
|
||||
|
||||
ORKVoiceEngine *voice = [ORKVoiceEngine sharedVoiceEngine];
|
||||
NSTimeInterval timeRemaining = [timer duration] - [timer runtime];
|
||||
|
||||
for (ORKVocalCue *cue in [self audioStep].vocalCues) {
|
||||
if (cue.time >= timeRemaining && ![self.playedCues containsObject:cue]) {
|
||||
[self.playedCues addObject:cue];
|
||||
[voice speakText: cue.spokenText];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)appHasAudioBackgroundMode {
|
||||
NSArray<NSString *> *backgroundModes = (NSArray<NSString *> *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
|
||||
BOOL hasBackgroundAudioMode = [backgroundModes containsObject:@"audio"];
|
||||
return hasBackgroundAudioMode;
|
||||
}
|
||||
|
||||
- (void)enableBackgroundAudioSession:(BOOL)enabled {
|
||||
NSError *error;
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
|
||||
mode:AVAudioSessionModeDefault
|
||||
routeSharingPolicy:AVAudioSessionRouteSharingPolicyLongFormAudio
|
||||
options:0
|
||||
error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("ORKAudioFitnessStepViewController failed to setup audio session: %@", error);
|
||||
return;
|
||||
}
|
||||
|
||||
[[AVAudioSession sharedInstance] setActive:enabled error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("ORKAudioFitnessViewController failed to start audio session: %@", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (id<ORKAudioPlayer>)audioPlayer {
|
||||
if (!_audioPlayer) {
|
||||
ORKAudioFitnessStep *step = [self audioStep];
|
||||
NSError *error;
|
||||
_audioPlayer = [[ORKAVAudioPlayer alloc] initWithContentsOfURL:step.audioAsset.url error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("ORKAudioFitnessStepViewController Failed to load audio file: %@", error.localizedFailureReason);
|
||||
}
|
||||
}
|
||||
return _audioPlayer;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -28,20 +28,15 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKAudioMeteringView.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKAudioGraphView : UIView
|
||||
|
||||
@property (nonatomic, strong) UIColor *keyColor;
|
||||
@property (nonatomic, strong) UIColor *alertColor;
|
||||
|
||||
@property (nonatomic, copy) NSArray *values;
|
||||
|
||||
@property (nonatomic) CGFloat alertThreshold;
|
||||
|
||||
@interface ORKAudioGraphView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -32,11 +32,21 @@
|
||||
#import "ORKAudioGraphView.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
static const CGFloat ValueLineWidth = 4.5;
|
||||
static const CGFloat ValueLineMargin = 1.5;
|
||||
static const CGFloat GraphHeight = 150.0;
|
||||
|
||||
@interface ORKAudioGraphView ()
|
||||
|
||||
/// ORKAudioMetering
|
||||
@property (nonatomic, copy, nullable) NSArray<NSNumber *> *samples;
|
||||
@property (nonatomic, assign) float alertThreshold;
|
||||
|
||||
/// ORKAudioMeteringView
|
||||
@property (nonatomic, strong) UIColor *meterColor;
|
||||
@property (nonatomic, strong, nullable) UIColor *alertColor;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKAudioGraphView
|
||||
|
||||
@@ -46,7 +56,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[self setUpConstraints];
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
_values = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
|
||||
_samples = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
@@ -65,26 +75,6 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[NSLayoutConstraint activateConstraints:@[heightConstraint]];
|
||||
}
|
||||
|
||||
- (void)setValues:(NSArray *)values {
|
||||
_values = [values copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setKeyColor:(UIColor *)keyColor {
|
||||
_keyColor = [keyColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor {
|
||||
_alertColor = [alertColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(CGFloat)alertThreshold {
|
||||
_alertThreshold = alertThreshold;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
CGRect bounds = self.bounds;
|
||||
|
||||
@@ -104,7 +94,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[centerLine addLineToPoint:(CGPoint){.x = maxX, .y = midY}];
|
||||
|
||||
CGContextSetLineWidth(context, 1.0 / scale);
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
CGFloat lengths[2] = {3, 3};
|
||||
CGContextSetLineDash(context, 0, lengths, 2);
|
||||
|
||||
@@ -125,7 +115,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
path1.lineWidth = ValueLineWidth;
|
||||
UIBezierPath *path2 = [path1 copy];
|
||||
|
||||
for (NSNumber *value in [_values reverseObjectEnumerator]) {
|
||||
for (NSNumber *value in [_samples reverseObjectEnumerator]) {
|
||||
CGFloat floatValue = value.doubleValue;
|
||||
|
||||
UIBezierPath *path = nil;
|
||||
@@ -134,7 +124,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[_alertColor setStroke];
|
||||
} else {
|
||||
path = path2;
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
}
|
||||
[path moveToPoint:(CGPoint){.x = x, .y = midY - floatValue*halfHeight}];
|
||||
[path addLineToPoint:(CGPoint){.x = x, .y = midY + floatValue*halfHeight}];
|
||||
@@ -150,11 +140,41 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[_alertColor setStroke];
|
||||
[path1 stroke];
|
||||
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
[path2 stroke];
|
||||
|
||||
}
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMetering
|
||||
|
||||
- (void)setSamples:(NSArray<NSNumber *> *)samples
|
||||
{
|
||||
_samples = [samples copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(float)threshold
|
||||
{
|
||||
_alertThreshold = threshold;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
#pragma mark = ORKAudioMeteringView
|
||||
|
||||
- (void)setMeterColor:(UIColor *)meterColor
|
||||
{
|
||||
_meterColor = [meterColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor
|
||||
{
|
||||
_alertColor = [alertColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@@ -75,3 +75,4 @@ ORK_CLASS_AVAILABLE
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ Float32 const VolumeClamp = 60.0;
|
||||
// Setup reader
|
||||
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
|
||||
if (urlAsset.tracks.count == 0) {
|
||||
NSLog(@"No tracks found for urlAsset: %@", fileURL);
|
||||
ORK_Log_Info("No tracks found for urlAsset: %@", fileURL);
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -216,3 +216,4 @@ Float32 const VolumeClamp = 60.0;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
+14
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -29,26 +29,30 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@import UIKit;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKConsentSection;
|
||||
extern NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit);
|
||||
|
||||
@interface ORKConsentSceneViewController : UIViewController
|
||||
@protocol ORKAudioMetering <NSObject>
|
||||
|
||||
- (instancetype)initWithSection:(ORKConsentSection *)section;
|
||||
- (void)setSamples:(nullable NSArray<NSNumber *> *)samples;
|
||||
|
||||
@property (nonatomic, readonly, nullable) ORKConsentSection *section;
|
||||
- (void)setAlertThreshold:(float)threshold;
|
||||
|
||||
@property (nonatomic, strong, nullable) UIBarButtonItem *continueButtonItem;
|
||||
@end
|
||||
|
||||
@property (nonatomic, strong, nullable) UIBarButtonItem *cancelButtonItem;
|
||||
@protocol ORKAudioMeteringDisplay
|
||||
|
||||
@property (nonatomic, strong, nullable) NSString *learnMoreButtonTitle;
|
||||
- (void)setMeterColor:(nonnull UIColor *)meterColor;
|
||||
|
||||
@property (nonatomic, assign) BOOL imageHidden;
|
||||
- (void)setAlertColor:(nonnull UIColor *)alertColor;
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKAudioMeteringView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAudioMeteringView.h"
|
||||
|
||||
|
||||
#import "ORKAudioGraphView.h"
|
||||
|
||||
NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit) {
|
||||
|
||||
if (samples.count > limit) {
|
||||
|
||||
return [samples subarrayWithRange:(NSRange){samples.count - limit, samples.count - 1}];
|
||||
}
|
||||
|
||||
return [samples copy];
|
||||
}
|
||||
|
||||
@interface ORKAudioMeteringView ()
|
||||
@property (nonatomic, strong) UIView<ORKAudioMetering, ORKAudioMeteringDisplay> *meteringView;
|
||||
@end
|
||||
|
||||
@implementation ORKAudioMeteringView
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)configureMeteringView
|
||||
{
|
||||
if (!_meteringView) {
|
||||
[self setMeteringView:[[ORKAudioGraphView alloc] init]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
[_meteringView setFrame:[self bounds]];
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden
|
||||
{
|
||||
[super setHidden:hidden];
|
||||
[_meteringView setHidden:hidden];
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
if ([self superview] == nil)
|
||||
{
|
||||
[_meteringView removeFromSuperview];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self addSubview:_meteringView];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMetering
|
||||
|
||||
- (void)setSamples:(NSArray<NSNumber *> *)samples
|
||||
{
|
||||
[_meteringView setSamples:samples];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(float)threshold
|
||||
{
|
||||
[_meteringView setAlertThreshold:threshold];
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMeteringDisplay
|
||||
|
||||
- (void)setMeterColor:(UIColor *)meterColor
|
||||
{
|
||||
[_meteringView setMeterColor:meterColor];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor
|
||||
{
|
||||
[_meteringView setAlertColor:alertColor];
|
||||
}
|
||||
|
||||
#pragma mark - UIAccessibility
|
||||
|
||||
- (BOOL)isAccessibilityElement {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,18 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAudioStep : ORKActiveStep
|
||||
|
||||
/**
|
||||
A Boolean value that determines if audio recording will start and stop
|
||||
automatcially or be controlled via a ORKRecordButton
|
||||
|
||||
When set to YES the user will be able to start and stop the audio recording
|
||||
by the ORKRecordButton
|
||||
|
||||
The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic) BOOL useRecordButton;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -49,16 +49,27 @@
|
||||
if (self) {
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
self.shouldStartTimerAutomatically = YES;
|
||||
self.useRecordButton = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setUseRecordButton:(BOOL)useRecordButton {
|
||||
_useRecordButton = useRecordButton;
|
||||
|
||||
[self setShouldStartTimerAutomatically:!_useRecordButton];
|
||||
|
||||
if (_useRecordButton) {
|
||||
self.stepDuration = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
|
||||
NSTimeInterval const ORKAudioTaskMinimumDuration = 5.0;
|
||||
|
||||
if ( self.stepDuration < ORKAudioTaskMinimumDuration) {
|
||||
if ( self.stepDuration < ORKAudioTaskMinimumDuration && !self.useRecordButton) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKAudioTaskMinimumDuration)] userInfo:nil];
|
||||
}
|
||||
}
|
||||
@@ -67,4 +78,35 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAudioStep *step = [super copyWithZone:zone];
|
||||
step.useRecordButton = self.useRecordButton;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_BOOL(aDecoder, useRecordButton);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_BOOL(aCoder, useRecordButton);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame && self.useRecordButton == castObject.useRecordButton);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
@@ -44,3 +45,4 @@ ORK_CLASS_AVAILABLE
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
ORKAudioContentView *_audioContentView;
|
||||
ORKAudioRecorder *_audioRecorder;
|
||||
ORKActiveStepTimer *_timer;
|
||||
NSTimer *_intervalTimer;
|
||||
NSError *_audioRecorderError;
|
||||
}
|
||||
|
||||
@@ -83,6 +84,12 @@
|
||||
// Do any additional setup after loading the view.
|
||||
_audioContentView = [ORKAudioContentView new];
|
||||
_audioContentView.timeLeft = self.audioStep.stepDuration;
|
||||
_audioContentView.useRecordButton = self.audioStep.useRecordButton && self.audioStep.stepDuration == 0;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[_audioContentView setViewEventHandler:^(ORKAudioContentViewEvent event) {
|
||||
[weakSelf handleContentViewEvent:event];
|
||||
}];
|
||||
|
||||
if (self.alertThreshold > 0) {
|
||||
_audioContentView.alertThreshold = self.alertThreshold;
|
||||
@@ -91,6 +98,19 @@
|
||||
self.activeStepView.activeCustomView = _audioContentView;
|
||||
}
|
||||
|
||||
- (void)handleContentViewEvent:(ORKAudioContentViewEvent)event {
|
||||
|
||||
switch (event) {
|
||||
case ORKAudioContentViewEventStartRecording:
|
||||
[self start];
|
||||
break;
|
||||
|
||||
case ORKAudioContentViewEventStopRecording:
|
||||
[self finish];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)audioRecorderDidChange {
|
||||
_audioRecorder.audioRecorder.meteringEnabled = YES;
|
||||
[self setAvAudioRecorder:_audioRecorder.audioRecorder];
|
||||
@@ -116,42 +136,72 @@
|
||||
if (_audioRecorderError) {
|
||||
return;
|
||||
}
|
||||
|
||||
[_avAudioRecorder updateMeters];
|
||||
float value = [_avAudioRecorder averagePowerForChannel:0];
|
||||
// Assume value is in range roughly -60dB to 0dB
|
||||
float clampedValue = MAX(value / 60.0, -1) + 1;
|
||||
[_audioContentView addSample:@(clampedValue)];
|
||||
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startNewTimerIfNeeded {
|
||||
if (!_timer) {
|
||||
NSTimeInterval duration = self.audioStep.stepDuration;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf doSample];
|
||||
if (finished) {
|
||||
[strongSelf finish];
|
||||
}
|
||||
}];
|
||||
[_timer resume];
|
||||
if (self.audioStep.useRecordButton) {
|
||||
|
||||
if (!_intervalTimer) {
|
||||
|
||||
_intervalTimer = [NSTimer scheduledTimerWithTimeInterval: 20 / 100
|
||||
target:self selector:@selector(doSample)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
} else {
|
||||
|
||||
if (!_timer) {
|
||||
NSTimeInterval duration = self.audioStep.stepDuration;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf doSample];
|
||||
if (finished) {
|
||||
[strongSelf finish];
|
||||
}
|
||||
}];
|
||||
[_timer resume];
|
||||
}
|
||||
}
|
||||
|
||||
_audioContentView.finished = NO;
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
[self audioRecorderDidChange];
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
[self startNewTimerIfNeeded];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
|
||||
[self startNewTimerIfNeeded];
|
||||
}
|
||||
|
||||
- (void)suspend {
|
||||
[super suspend];
|
||||
[_timer pause];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer pause];
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
|
||||
if (_avAudioRecorder) {
|
||||
[_audioContentView addSample:@(0)];
|
||||
}
|
||||
@@ -160,8 +210,12 @@
|
||||
- (void)resume {
|
||||
[super resume];
|
||||
[self audioRecorderDidChange];
|
||||
|
||||
[self startNewTimerIfNeeded];
|
||||
[_timer resume];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer resume];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finish {
|
||||
@@ -169,8 +223,14 @@
|
||||
return;
|
||||
}
|
||||
[super finish];
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
@@ -189,3 +249,4 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import Foundation;
|
||||
@import AVFoundation;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol ORKAudioStreamingDelegate <ORKRecorderDelegate>
|
||||
|
||||
- (void)audioAvailable:(AVAudioPCMBuffer *)buffer;
|
||||
|
||||
@end
|
||||
|
||||
@class ORKStep;
|
||||
|
||||
@interface ORKAudioStreamer : ORKRecorder
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier step:(nullable ORKStep *)step NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property (nonatomic, strong, readonly, nullable) AVAudioEngine *audioEngine;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAudioStreamer.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKStep.h"
|
||||
|
||||
#pragma mark - ORKAudioStreamerConfiguration
|
||||
|
||||
@implementation ORKAudioStreamerConfiguration
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (ORKRecorder *)recorderForStep:(ORKStep *)step outputDirectory:(NSURL *)outputDirectory {
|
||||
|
||||
ORKAudioStreamer *obj = [[ORKAudioStreamer alloc] initWithIdentifier:self.identifier step:step];
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
return [super isEqual:object];
|
||||
}
|
||||
|
||||
- (ORKPermissionMask)requestedPermissionMask {
|
||||
return ORKPermissionAudioRecording;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - ORKAudioStreamer
|
||||
|
||||
@implementation ORKAudioStreamer
|
||||
{
|
||||
NSString *_savedSessionCategory;
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier step:(ORKStep *)step
|
||||
{
|
||||
self = [super initWithIdentifier:identifier step:step outputDirectory:nil];
|
||||
if (self)
|
||||
{
|
||||
self.continuesInBackground = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)restoreSavedAudioSessionCategory
|
||||
{
|
||||
if (_savedSessionCategory)
|
||||
{
|
||||
NSError *error;
|
||||
if (![[AVAudioSession sharedInstance] setCategory:_savedSessionCategory error:&error])
|
||||
{
|
||||
ORK_Log_Error("Failed to restore the audio session category: %@", [error localizedDescription]);
|
||||
}
|
||||
_savedSessionCategory = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isRecording
|
||||
{
|
||||
return [_audioEngine isRunning];
|
||||
}
|
||||
|
||||
- (NSString *)recorderType
|
||||
{
|
||||
return @"audioStreaming";
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
if (!_audioEngine)
|
||||
{
|
||||
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
|
||||
_savedSessionCategory = audioSession.category;
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success =
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error] &&
|
||||
[[AVAudioSession sharedInstance] setMode:AVAudioSessionModeMeasurement error:&error] &&
|
||||
[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
|
||||
|
||||
if (!success && error)
|
||||
{
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
ORK_Log_Debug("Create audioEngine recorder %p", self);
|
||||
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
AVAudioInputNode *inputnode = _audioEngine.inputNode;
|
||||
AVAudioFormat *recordingFormat = [inputnode inputFormatForBus:0];
|
||||
|
||||
[inputnode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when)
|
||||
{
|
||||
id<ORKAudioStreamingDelegate> delegate = (id<ORKAudioStreamingDelegate>)self.delegate;
|
||||
|
||||
if (delegate && [delegate respondsToSelector:@selector(audioAvailable:)]) {
|
||||
[delegate audioAvailable:buffer];
|
||||
}
|
||||
}];
|
||||
|
||||
[_audioEngine prepare];
|
||||
|
||||
[_audioEngine startAndReturnError:&error];
|
||||
|
||||
if (error != nil)
|
||||
{
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[super start];
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
if (!_audioEngine)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
[self doStopRecording];
|
||||
|
||||
[super stop];
|
||||
}
|
||||
|
||||
- (void)doStopRecording
|
||||
{
|
||||
if (self.isRecording)
|
||||
{
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
_audioEngine = nil;
|
||||
|
||||
[self restoreSavedAudioSessionCategory];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finishRecordingWithError:(NSError *)error
|
||||
{
|
||||
[self doStopRecording];
|
||||
|
||||
[super finishRecordingWithError:error];
|
||||
}
|
||||
|
||||
- (void)reset
|
||||
{
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
_audioEngine = nil;
|
||||
[super reset];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
ORK_Log_Debug("Remove audiorecorder %p", self);
|
||||
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
|
||||
_audioEngine = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -36,7 +36,6 @@
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKLabel.h"
|
||||
#import "ORKSubheadlineLabel.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
@@ -243,6 +242,14 @@ static const CGFloat ProgressIndicatorOuterMargin = 1.0;
|
||||
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @(_countDown).stringValue);
|
||||
[_countdownView startAnimateWithDuration:[(ORKActiveStep *)self.step stepDuration]];
|
||||
|
||||
[UIApplication.sharedApplication setIdleTimerDisabled:YES];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[UIApplication.sharedApplication setIdleTimerDisabled:NO];
|
||||
|
||||
[super viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
- (void)updateCountdownLabel {
|
||||
|
||||
@@ -108,10 +108,10 @@ static NSString *const ORKDataLoggerManagerConfigurationFilename = @".ORKDataLog
|
||||
return (string.integerValue != 0);
|
||||
}
|
||||
|
||||
- (BOOL)ork_setUploaded:(BOOL)uploaded error:(NSError **)error {
|
||||
- (BOOL)ork_setUploaded:(BOOL)uploaded error:(NSError **)errorOut {
|
||||
NSString *value = (uploaded ? @"1" : @"0");
|
||||
NSData *encodedString = [value dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [self ork_setData:encodedString forAttr:ORKDataLoggerUploadedAttr error:error];
|
||||
return [self ork_setData:encodedString forAttr:ORKDataLoggerUploadedAttr error:errorOut];
|
||||
}
|
||||
|
||||
- (NSData *)ork_dataForAttr:(const char *)attr {
|
||||
@@ -132,12 +132,12 @@ static NSString *const ORKDataLoggerManagerConfigurationFilename = @".ORKDataLog
|
||||
return data;
|
||||
}
|
||||
|
||||
- (BOOL)ork_setData:(NSData *)data forAttr:(const char *)attr error:(NSError **)error {
|
||||
- (BOOL)ork_setData:(NSData *)data forAttr:(const char *)attr error:(NSError **)errorOut {
|
||||
const char *path = [self fileSystemRepresentation];
|
||||
int rc = setxattr(path, attr, data.bytes, data.length, 0, 0);
|
||||
if (rc != 0) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_SET_ATTRIBUTE", nil)}];
|
||||
if (errorOut != NULL) {
|
||||
*errorOut = [NSError errorWithDomain:NSCocoaErrorDomain code:rc userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_SET_ATTRIBUTE", nil)}];
|
||||
}
|
||||
}
|
||||
return (rc == 0);
|
||||
@@ -238,19 +238,19 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
|
||||
return [object isKindOfClass:[NSData class]];
|
||||
}
|
||||
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData *)data fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
- (BOOL)writeData:(NSData *)data fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
|
||||
BOOL result = YES;
|
||||
@try {
|
||||
[fileHandle writeData:data];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
result = NO;
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorException userInfo:@{@"exception": exception}];
|
||||
if (errorOut != NULL) {
|
||||
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorException userInfo:@{@"exception": exception}];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -265,20 +265,20 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
|
||||
[fileHandle truncateFileAtOffset:offset];
|
||||
}
|
||||
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
|
||||
if (![self canAcceptLogObject:object]) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"ORKLogFormatter accepts NSData only" userInfo:nil];
|
||||
}
|
||||
return [self writeData:(NSData *)object fileHandle:fileHandle error:error];
|
||||
return [self writeData:(NSData *)object fileHandle:fileHandle error:errorOut];
|
||||
}
|
||||
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
|
||||
unsigned long long checkpoint = [self checkpointWithFileHandle:fileHandle];
|
||||
|
||||
NSError *errorOut = nil;
|
||||
NSError *error = nil;
|
||||
BOOL success = YES;
|
||||
for (NSObject *obj in objects) {
|
||||
success = [self appendObject:obj fileHandle:fileHandle error:&errorOut];
|
||||
success = [self appendObject:obj fileHandle:fileHandle error:&error];
|
||||
if (!success) {
|
||||
break;
|
||||
}
|
||||
@@ -286,8 +286,8 @@ static void *ORKObjectObserverContext = &ORKObjectObserverContext;
|
||||
|
||||
if (!success) {
|
||||
[self rollbackToCheckpoint:checkpoint fileHandle:fileHandle];
|
||||
if (error) {
|
||||
*error = errorOut;
|
||||
if (errorOut != NULL) {
|
||||
*errorOut = error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,10 +335,10 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
- (BOOL)beginLogWithFileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
|
||||
// Write valid JSON containing no objects
|
||||
NSData *data = [kJSONLogEmptyLogString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [self writeData:data fileHandle:fileHandle error:error];
|
||||
return [self writeData:data fileHandle:fileHandle error:errorOut];
|
||||
}
|
||||
|
||||
- (unsigned long long)checkpointWithFileHandle:(NSFileHandle *)fileHandle {
|
||||
@@ -356,8 +356,8 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)error {
|
||||
return [self appendObjects:@[object] fileHandle:fileHandle error:error];
|
||||
- (BOOL)appendObject:(id)object fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
|
||||
return [self appendObjects:@[object] fileHandle:fileHandle error:errorOut];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -368,7 +368,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
* before writing. When writing, we write a separator (if needed), the JSON
|
||||
* object being appended, and the footer bytes.
|
||||
*/
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)appendObjects:(NSArray *)objects fileHandle:(NSFileHandle *)fileHandle error:(NSError **)errorOut {
|
||||
if (!fileHandle) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Filehandle is nil" userInfo:nil];
|
||||
}
|
||||
@@ -385,7 +385,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
// Seek to the end of the file; we'll later backtrack
|
||||
unsigned long long offset = [fileHandle seekToEndOfFile];
|
||||
if (offset == 0) {
|
||||
if (![self beginLogWithFileHandle:fileHandle error:error]) {
|
||||
if (![self beginLogWithFileHandle:fileHandle error:errorOut]) {
|
||||
return NO;
|
||||
}
|
||||
offset = [fileHandle offsetInFile];
|
||||
@@ -402,12 +402,13 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
// Serialize each object separately to the buffer, pending a single write, so the
|
||||
// objects form part of a single array.
|
||||
__block BOOL success = YES;
|
||||
__block NSError *localError;
|
||||
[objects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
||||
NSData *data;
|
||||
if ([obj isKindOfClass:[NSData class]]) {
|
||||
data = obj;
|
||||
} else {
|
||||
data = [NSJSONSerialization dataWithJSONObject:obj options:(NSJSONWritingOptions)0 error:error];
|
||||
data = [NSJSONSerialization dataWithJSONObject:obj options:(NSJSONWritingOptions)0 error:&localError];
|
||||
}
|
||||
if (!data) {
|
||||
success = NO;
|
||||
@@ -419,6 +420,9 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
}
|
||||
}];
|
||||
if (errorOut != NULL) {
|
||||
*errorOut = localError;
|
||||
}
|
||||
if (!success) {
|
||||
return success;
|
||||
}
|
||||
@@ -428,7 +432,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
assert(_ORKJSON_terminatorLength < offset);
|
||||
[fileHandle seekToFileOffset:(offset - _ORKJSON_terminatorLength)];
|
||||
|
||||
success = [self writeData:outputData fileHandle:fileHandle error:error];
|
||||
success = [self writeData:outputData fileHandle:fileHandle error:errorOut];
|
||||
|
||||
if (!success) {
|
||||
[self rollbackToCheckpoint:checkpoint fileHandle:fileHandle];
|
||||
@@ -530,7 +534,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
- (void)setupDirectorySource {
|
||||
int dirFD = open([_url fileSystemRepresentation], O_EVTONLY);
|
||||
if (dirFD < 0) {
|
||||
ORK_Log_Warning(@"Could not track directory %s (%d)", [_url fileSystemRepresentation], [[NSFileManager defaultManager] fileExistsAtPath:[_url path]]);
|
||||
ORK_Log_Info("Could not track directory %s (%d)", [_url fileSystemRepresentation], [[NSFileManager defaultManager] fileExistsAtPath:[_url path]]);
|
||||
} else {
|
||||
// Dispatch to a concurrent queue, so we don't store up blocks while our
|
||||
// queue is working.
|
||||
@@ -597,24 +601,28 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError * __autoreleasing *)error {
|
||||
- (BOOL)enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
|
||||
if (!block) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Block parameter is required" userInfo:nil];
|
||||
}
|
||||
|
||||
__block BOOL success = NO;
|
||||
__block NSError *localError;
|
||||
dispatch_sync(_queue, ^{
|
||||
success = [self queue_enumerateLogsUploaded:uploaded block:block error:error];
|
||||
success = [self queue_enumerateLogsUploaded:uploaded block:block error:&localError];
|
||||
});
|
||||
if (errorOut != NULL) {
|
||||
*errorOut = localError;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)enumerateLogsNeedingUpload:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
return [self enumerateLogsUploaded:NO block:block error:error];
|
||||
- (BOOL)enumerateLogsNeedingUpload:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
|
||||
return [self enumerateLogsUploaded:NO block:block error:errorOut];
|
||||
}
|
||||
|
||||
- (BOOL)enumerateLogsAlreadyUploaded:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
return [self enumerateLogsUploaded:YES block:block error:error];
|
||||
- (BOOL)enumerateLogsAlreadyUploaded:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
|
||||
return [self enumerateLogsUploaded:YES block:block error:errorOut];
|
||||
}
|
||||
|
||||
- (BOOL)append:(id)object error:(NSError * __autoreleasing *)error {
|
||||
@@ -701,7 +709,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)queue_enumerateLogs:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
- (BOOL)queue_enumerateLogs:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
|
||||
static NSArray *keys = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@@ -716,7 +724,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
NSDirectoryEnumerationSkipsPackageDescendants)
|
||||
errorHandler:nil];
|
||||
|
||||
NSError *errorOut = nil;
|
||||
NSError *error = nil;
|
||||
NSMutableArray *urls = [NSMutableArray array];
|
||||
for (NSURL *url in enumerator) {
|
||||
if (![self urlMatchesLogName:url]) {
|
||||
@@ -726,8 +734,8 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
// Don't include the "current" log file
|
||||
continue;
|
||||
}
|
||||
NSDictionary *resources = [url resourceValuesForKeys:keys error:&errorOut];
|
||||
if (errorOut) {
|
||||
NSDictionary *resources = [url resourceValuesForKeys:keys error:&error];
|
||||
if (error) {
|
||||
// If there's been an error getting the resource values, give up
|
||||
break;
|
||||
}
|
||||
@@ -737,7 +745,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
[urls addObject:url];
|
||||
}
|
||||
|
||||
if (!errorOut) {
|
||||
if (!error) {
|
||||
// Sort the URLs before beginning enumeration for the caller
|
||||
[urls sortUsingComparator:^NSComparisonResult(NSURL *obj1, NSURL *obj2) {
|
||||
// We can assume all relate to files in the same directory
|
||||
@@ -753,27 +761,23 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
*error = errorOut;
|
||||
if (errorOut != NULL) {
|
||||
*errorOut = error;
|
||||
}
|
||||
return (errorOut ? NO : YES);
|
||||
return (error ? NO : YES);
|
||||
}
|
||||
|
||||
- (BOOL)queue_enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
- (BOOL)queue_enumerateLogsUploaded:(BOOL)uploaded block:(void (^)(NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
|
||||
return [self queue_enumerateLogs:^(NSURL *logFileUrl, BOOL *stop) {
|
||||
NSError *errorOut = nil;
|
||||
BOOL wantUploaded = [logFileUrl ork_isUploaded];
|
||||
BOOL isWanted = (wantUploaded && uploaded) || (!wantUploaded && !uploaded);
|
||||
if (isWanted) {
|
||||
block(logFileUrl, stop);
|
||||
}
|
||||
if (errorOut) {
|
||||
*stop = YES;
|
||||
}
|
||||
} error:error];
|
||||
} error:errorOut];
|
||||
}
|
||||
|
||||
- (NSFileHandle *)queue_makeFileHandleWithError:(NSError **)error {
|
||||
- (NSFileHandle *)queue_makeFileHandleWithError:(NSError **)errorOut {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSURL *url = [self currentLogFileURL];
|
||||
|
||||
@@ -785,7 +789,7 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
|
||||
NSFileHandle *fileHandle = nil;
|
||||
if (!createNewFile) {
|
||||
fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:error];
|
||||
fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:errorOut];
|
||||
if (!fileHandle) {
|
||||
// Assume it's because we can't open the file, perhaps for security reasons.
|
||||
// Close and rename the log.
|
||||
@@ -798,12 +802,12 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
NSString *filePath = [url path];
|
||||
BOOL success = [fileManager createFileAtPath:filePath contents:nil attributes:nil];
|
||||
if (!success) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_CREATE_FILE", nil)}];
|
||||
if (errorOut != NULL) {
|
||||
*errorOut = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_CREATE_FILE", nil)}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
fileHandle = [NSFileHandle fileHandleForWritingToURL:[self currentLogFileURL] error:error];
|
||||
fileHandle = [NSFileHandle fileHandleForWritingToURL:[self currentLogFileURL] error:errorOut];
|
||||
if (!fileHandle) {
|
||||
[fileManager removeItemAtURL:url error:nil];
|
||||
return nil;
|
||||
@@ -814,10 +818,10 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
assert(fileHandle);
|
||||
|
||||
// Set file protection after opening the file, so that class B works as expected.
|
||||
BOOL success = [fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(self.fileProtectionMode)} ofItemAtPath:[url path] error:error];
|
||||
BOOL success = [fileManager setAttributes:@{NSFileProtectionKey: ORKFileProtectionFromMode(self.fileProtectionMode)} ofItemAtPath:[url path] error:errorOut];
|
||||
|
||||
// Allow formatter to initialize the log file with header content
|
||||
success = success && [self.logFormatter beginLogWithFileHandle:fileHandle error:error];
|
||||
success = success && [self.logFormatter beginLogWithFileHandle:fileHandle error:errorOut];
|
||||
|
||||
if (!success) {
|
||||
[fileHandle closeFile];
|
||||
@@ -829,9 +833,9 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return _currentFileHandle;
|
||||
}
|
||||
|
||||
- (NSFileHandle *)queue_fileHandleWithError:(NSError **)error {
|
||||
- (NSFileHandle *)queue_fileHandleWithError:(NSError **)errorOut {
|
||||
if (!_currentFileHandle) {
|
||||
_currentFileHandle = [self queue_makeFileHandleWithError:error];
|
||||
_currentFileHandle = [self queue_makeFileHandleWithError:errorOut];
|
||||
|
||||
[_currentFileHandle seekToEndOfFile];
|
||||
}
|
||||
@@ -878,14 +882,14 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
if (((NSNumber *)parameters[NSURLIsRegularFileKey]).boolValue) {
|
||||
if (((NSNumber *)parameters[NSURLFileSizeKey]).intValue > 0) {
|
||||
NSURL *destinationUrl = [ORKDataLogger nextUrlForDirectoryUrl:_url logName:_logName];
|
||||
ORK_Log_Debug(@"Rollover: %@ to %@", [url lastPathComponent], [destinationUrl lastPathComponent]);
|
||||
ORK_Log_Debug("Rollover: %@ to %@", [url lastPathComponent], [destinationUrl lastPathComponent]);
|
||||
[fileManager moveItemAtURL:url toURL:destinationUrl error:nil];
|
||||
if (self.fileProtectionMode == ORKFileProtectionCompleteUnlessOpen) {
|
||||
// Upgrade to complete file protection after roll-over
|
||||
NSError *error = nil;
|
||||
if (![fileManager setAttributes:@{NSFileProtectionKey: NSFileProtectionComplete}
|
||||
ofItemAtPath:[destinationUrl path] error:&error]) {
|
||||
ORK_Log_Warning(@"Error setting NSFileProtectionComplete on %@: %@", destinationUrl, error);
|
||||
ORK_Log_Error("Error setting NSFileProtectionComplete on %@: %@", destinationUrl, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -922,15 +926,15 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
[self queue_closeAndRenameLog];
|
||||
}
|
||||
|
||||
- (BOOL)queue_append:(id)object error:(NSError **)error {
|
||||
- (BOOL)queue_append:(id)object error:(NSError **)errorOut {
|
||||
[self queue_rolloverIfNeeded];
|
||||
|
||||
NSFileHandle *fileHandle = [self queue_fileHandleWithError:error];
|
||||
NSFileHandle *fileHandle = [self queue_fileHandleWithError:errorOut];
|
||||
if (!fileHandle) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL result = [self.logFormatter appendObject:object fileHandle:_currentFileHandle error:error];
|
||||
BOOL result = [self.logFormatter appendObject:object fileHandle:_currentFileHandle error:errorOut];
|
||||
|
||||
// Quick check to see if we've run over the maximum log file size
|
||||
if ((self.maximumCurrentLogFileSize > 0) && ([_currentFileHandle offsetInFile] >= self.maximumCurrentLogFileSize)) {
|
||||
@@ -940,15 +944,15 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)queue_appendObjects:(NSArray *)objects error:(NSError **)error {
|
||||
- (BOOL)queue_appendObjects:(NSArray *)objects error:(NSError **)errorOut {
|
||||
[self queue_rolloverIfNeeded];
|
||||
|
||||
NSFileHandle *fileHandle = [self queue_fileHandleWithError:error];
|
||||
NSFileHandle *fileHandle = [self queue_fileHandleWithError:errorOut];
|
||||
if (!fileHandle) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL result = [self.logFormatter appendObjects:objects fileHandle:_currentFileHandle error:error];
|
||||
BOOL result = [self.logFormatter appendObjects:objects fileHandle:_currentFileHandle error:errorOut];
|
||||
|
||||
// Quick check to see if we've run over the maximum log file size
|
||||
if ((self.maximumCurrentLogFileSize > 0) && ([_currentFileHandle offsetInFile] >= self.maximumCurrentLogFileSize)) {
|
||||
@@ -957,23 +961,24 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)queue_markFileUploaded:(BOOL)uploaded atURL:(NSURL *)url error:(NSError **)error {
|
||||
BOOL success = [url ork_setUploaded:uploaded error:error];
|
||||
- (BOOL)queue_markFileUploaded:(BOOL)uploaded atURL:(NSURL *)url error:(NSError **)errorOut {
|
||||
BOOL success = [url ork_setUploaded:uploaded error:errorOut];
|
||||
[self queue_setNeedsUpdateBytes];
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs withError:(NSError **)error {
|
||||
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs withError:(NSError **)errorOut {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
__block NSMutableArray *errors = [NSMutableArray array];
|
||||
__block NSError *error = nil;
|
||||
BOOL success = [self queue_enumerateLogs:^(NSURL *logFileUrl, BOOL *stop) {
|
||||
if ([fileURLs containsObject:logFileUrl]) {
|
||||
NSError *errorOut = nil;
|
||||
BOOL uploaded = [logFileUrl ork_isUploaded];
|
||||
|
||||
if (uploaded) {
|
||||
if (![fileManager removeItemAtURL:logFileUrl error:&errorOut]) {
|
||||
[errors addObject:errorOut];
|
||||
if (![fileManager removeItemAtURL:logFileUrl error:&error]) {
|
||||
[errors addObject:error];
|
||||
error = nil;
|
||||
}
|
||||
} else {
|
||||
// File was requested to be removed, but was not marked uploaded
|
||||
@@ -982,16 +987,17 @@ static NSInteger _ORKJSON_terminatorLength = 0;
|
||||
userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_COULD_NOT_MAORK", nil), @"url": logFileUrl}]];
|
||||
}
|
||||
}
|
||||
} error:error];
|
||||
} error:&error];
|
||||
if (!success && error) {
|
||||
[errors addObject:error];
|
||||
error = nil;
|
||||
}
|
||||
|
||||
// Reporting multiple errors
|
||||
if (errors.count) {
|
||||
if (!success && error && *error) {
|
||||
[errors addObject:*error];
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain
|
||||
code:ORKErrorMultipleErrors
|
||||
userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_MULTIPLE", nil), @"errors": errors}];
|
||||
}
|
||||
if (errorOut != NULL) {
|
||||
*errorOut = [NSError errorWithDomain:ORKErrorDomain
|
||||
code:ORKErrorMultipleErrors
|
||||
userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"ERROR_DATALOGGER_MULTIPLE", nil), @"errors": errors}];
|
||||
success = NO;
|
||||
}
|
||||
return success;
|
||||
@@ -1196,14 +1202,14 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
|
||||
return logNames;
|
||||
}
|
||||
|
||||
- (BOOL)queue_enumerateLogsNeedingUpload:(void (^)(ORKDataLogger *dataLogger, NSURL *logFileUrl, BOOL *stop))block error:(NSError **)error {
|
||||
- (BOOL)queue_enumerateLogsNeedingUpload:(void (^)(ORKDataLogger *dataLogger, NSURL *logFileUrl, BOOL *stop))block error:(NSError **)errorOut {
|
||||
BOOL success = YES;
|
||||
NSMutableArray *allFiles = [NSMutableArray array];
|
||||
// Collect all the log file URLs so we can sort them by date rather than enumerating by logger.
|
||||
for (ORKDataLogger *logger in _records.allValues) {
|
||||
success = [logger enumerateLogsNeedingUpload:^(NSURL *logFileUrl, BOOL *stop) {
|
||||
[allFiles addObject:logFileUrl];
|
||||
} error:error];
|
||||
} error:errorOut];
|
||||
|
||||
if (!success) {
|
||||
break;
|
||||
@@ -1247,7 +1253,7 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)error {
|
||||
- (BOOL)queue_removeUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)errorOut {
|
||||
BOOL success = YES;
|
||||
NSMutableArray *notRemoved = [NSMutableArray array];
|
||||
for (NSURL *url in fileURLs) {
|
||||
@@ -1257,15 +1263,15 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not from a known logger" userInfo:@{@"url":url}];
|
||||
}
|
||||
|
||||
NSError *errorOut = nil;
|
||||
BOOL itemSuccess = [[NSFileManager defaultManager] removeItemAtURL:url error:&errorOut];
|
||||
NSError *error = nil;
|
||||
BOOL itemSuccess = [[NSFileManager defaultManager] removeItemAtURL:url error:&error];
|
||||
if (!itemSuccess) {
|
||||
[notRemoved addObject:url];
|
||||
success = NO;
|
||||
}
|
||||
}
|
||||
if (error && notRemoved.count) {
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
|
||||
if (errorOut != NULL && notRemoved.count) {
|
||||
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -1279,7 +1285,7 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)queue_unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)error {
|
||||
- (BOOL)queue_unmarkUploadedFiles:(NSArray<NSURL *> *)fileURLs error:(NSError **)errorOut {
|
||||
BOOL success = YES;
|
||||
NSMutableArray<NSURL *> *notRemoved = [NSMutableArray array];
|
||||
for (NSURL *url in fileURLs) {
|
||||
@@ -1289,15 +1295,15 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:@"URL is not from a known logger" userInfo:@{@"url":url}];
|
||||
}
|
||||
|
||||
NSError *errorOut = nil;
|
||||
BOOL itemSuccess = [logger markFileUploaded:NO atURL:url error:&errorOut];
|
||||
NSError *error = nil;
|
||||
BOOL itemSuccess = [logger markFileUploaded:NO atURL:url error:&error];
|
||||
if (!itemSuccess) {
|
||||
[notRemoved addObject:url];
|
||||
success = NO;
|
||||
}
|
||||
}
|
||||
if (error && notRemoved.count) {
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
|
||||
if (errorOut != NULL && notRemoved.count) {
|
||||
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorMultipleErrors userInfo:@{@"notRemoved":notRemoved}];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -1310,7 +1316,7 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)queue_removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError **)error {
|
||||
- (BOOL)queue_removeOldAndUploadedLogsToThreshold:(unsigned long long)bytes error:(NSError **)errorOut {
|
||||
if (bytes == 0) {
|
||||
for (ORKDataLogger *logger in _records) {
|
||||
[logger removeAllFilesWithError:nil];
|
||||
@@ -1360,8 +1366,8 @@ static NSString *const LoggerConfigurationsKey = @"loggers";
|
||||
} error:nil];
|
||||
}
|
||||
|
||||
if (error && (totalBytes > bytes)) {
|
||||
*error = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorObjectNotFound userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"ERROR_DATALOGGER_COULD_NOT_FREE_SPACE", nil)}];
|
||||
if (errorOut != NULL && (totalBytes > bytes)) {
|
||||
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorObjectNotFound userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"ERROR_DATALOGGER_COULD_NOT_FREE_SPACE", nil)}];
|
||||
}
|
||||
|
||||
return (totalBytes <= bytes);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKEnvironmentSPLMeterBarView : UIView
|
||||
|
||||
- (void)setProgress:(CGFloat)progress;
|
||||
- (void)stopAnimation;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKEnvironmentSPLMeterBarView.h"
|
||||
|
||||
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
static const CGFloat ORKEnvironmentSPLMeterSquareSize = 8.0;
|
||||
static const CGFloat ORKEnvironmentSPLMeterSquareDistance = 4.0;
|
||||
static const int ORKEnvironmentSPLMeterNumberOfRows = 4;
|
||||
|
||||
@interface ORKEnvironmentSPLMeterColumnView : UIView {
|
||||
int _numberOfRows;
|
||||
CGFloat _squareSize;
|
||||
CGFloat _cornerRadius;
|
||||
|
||||
NSArray<CAShapeLayer*> *_dots;
|
||||
}
|
||||
|
||||
- (void)setColor:(UIColor *)color;
|
||||
- (void)setOpacity:(CGFloat)opacity;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterColumnView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_numberOfRows = ORKEnvironmentSPLMeterNumberOfRows;
|
||||
_squareSize = ORKEnvironmentSPLMeterSquareSize;
|
||||
_cornerRadius = ORKEnvironmentSPLMeterSquareDistance;
|
||||
[self initRows];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initRows {
|
||||
CGFloat halfSquareSize = _squareSize * 0.5;
|
||||
CGFloat spacing = _squareSize + halfSquareSize;
|
||||
NSMutableArray<CAShapeLayer*> *dots = [[NSMutableArray alloc] init];
|
||||
for (int i = 0; i < _numberOfRows; i++) {
|
||||
CAShapeLayer *dot = [CAShapeLayer layer];
|
||||
CGRect dotRect = CGRectMake(0,
|
||||
spacing * i,
|
||||
_squareSize, _squareSize);
|
||||
[dot setPath:[UIBezierPath bezierPathWithRoundedRect:dotRect
|
||||
cornerRadius:_cornerRadius].CGPath];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
dot.fillColor = [UIColor systemGray6Color].CGColor;
|
||||
}
|
||||
[[self layer] addSublayer:dot];
|
||||
|
||||
[dots addObject:dot];
|
||||
}
|
||||
|
||||
_dots = [dots copy];
|
||||
|
||||
}
|
||||
|
||||
- (void)setOpacity:(CGFloat)opacity {
|
||||
for (NSInteger i = 0 ; i < _dots.count; i++) {
|
||||
CAShapeLayer *dot = _dots[i];
|
||||
dot.opacity = opacity;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setColor:(UIColor *)color {
|
||||
[_dots makeObjectsPerformSelector:@selector(setFillColor:) withObject:(id)[color CGColor]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKEnvironmentSPLMeterBarView () {
|
||||
NSArray<ORKEnvironmentSPLMeterColumnView *> *_columnViews;
|
||||
|
||||
int _currentIndex;
|
||||
int _targetIndex;
|
||||
int _maximumNumberOfDots;
|
||||
int _greenIndexLimit;
|
||||
|
||||
BOOL _didLayoutViews;
|
||||
BOOL _isAnimating;
|
||||
|
||||
NSTimer *_animationTimer;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterBarView
|
||||
|
||||
- (void)didMoveToSuperview {
|
||||
[super didMoveToSuperview];
|
||||
|
||||
_didLayoutViews = NO;
|
||||
_isAnimating = NO;
|
||||
}
|
||||
|
||||
- (void)setupView {
|
||||
CGFloat width = CGRectGetWidth(self.frame);
|
||||
CGFloat dotSpacing = (ORKEnvironmentSPLMeterSquareSize + ORKEnvironmentSPLMeterSquareDistance);
|
||||
_maximumNumberOfDots = (int) (floor(width/dotSpacing)) + 1;
|
||||
NSMutableArray<ORKEnvironmentSPLMeterColumnView*> *columnViews = [[NSMutableArray alloc] init];
|
||||
_greenIndexLimit = _maximumNumberOfDots * 0.66;
|
||||
_currentIndex = 0;
|
||||
_targetIndex = _greenIndexLimit;
|
||||
|
||||
for (int i = 1 ; i <= _maximumNumberOfDots; i++) {
|
||||
CGRect columnRect = CGRectMake((i - 1) * dotSpacing,
|
||||
0, ORKEnvironmentSPLMeterSquareSize, ORKEnvironmentSPLMeterSquareSize);
|
||||
|
||||
ORKEnvironmentSPLMeterColumnView *columnView = [[ORKEnvironmentSPLMeterColumnView alloc] initWithFrame:columnRect];
|
||||
|
||||
if (i <= _greenIndexLimit - 1) {
|
||||
[columnView setColor:[UIColor systemGreenColor]];
|
||||
} else {
|
||||
[columnView setColor:[UIColor systemOrangeColor]];
|
||||
}
|
||||
|
||||
[self addSubview:columnView];
|
||||
|
||||
[columnViews addObject:columnView];
|
||||
}
|
||||
|
||||
_columnViews = [columnViews copy];
|
||||
|
||||
[self updateViewForIndex:_currentIndex];
|
||||
|
||||
[self animateColumns];
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress {
|
||||
CGFloat resultProgress = progress;
|
||||
if (progress == 20.0) {
|
||||
return;
|
||||
}
|
||||
if(progress < 0) {
|
||||
resultProgress = 0.0;
|
||||
}
|
||||
|
||||
float inMin = 0.0;
|
||||
float inMax = 1.0;
|
||||
float outMin = 0.0;
|
||||
float outMax = 0.66;
|
||||
|
||||
float normalizedIndexValue = outMin + (outMax - outMin) * (resultProgress - inMin) / (inMax - inMin);
|
||||
|
||||
if (resultProgress > 1.0) {
|
||||
inMin = 1.0;
|
||||
inMax = 1.5;
|
||||
outMin = 0.66;
|
||||
outMax = 1.0;
|
||||
|
||||
normalizedIndexValue = outMin + (outMax - outMin) * (resultProgress - inMin) / (inMax - inMin);
|
||||
}
|
||||
|
||||
int newTargetIndex = (int) (floor(normalizedIndexValue * _maximumNumberOfDots) + 1);
|
||||
|
||||
if (newTargetIndex != _targetIndex) {
|
||||
[self stopAnimation];
|
||||
_targetIndex = newTargetIndex;
|
||||
_currentIndex = _targetIndex + (-1 + arc4random_uniform(3));
|
||||
[self updateViewForIndex:newTargetIndex];
|
||||
} else if (!_isAnimating) {
|
||||
int indexDistance = abs(_currentIndex - newTargetIndex);
|
||||
for (int i = 0; i < indexDistance; i++) {
|
||||
int newIndex;
|
||||
if (newTargetIndex < _currentIndex) {
|
||||
newIndex = _currentIndex - i;
|
||||
} else {
|
||||
newIndex = _currentIndex + i;
|
||||
}
|
||||
[self updateViewForIndex:newIndex];
|
||||
}
|
||||
|
||||
[self animateColumns];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)animateColumns {
|
||||
[_animationTimer invalidate];
|
||||
_isAnimating = YES;
|
||||
_animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(timerTicked) userInfo:nil repeats:YES];
|
||||
}
|
||||
|
||||
- (void)timerTicked {
|
||||
if (_currentIndex > _targetIndex) {
|
||||
_currentIndex = _currentIndex - 1;
|
||||
} else if (_currentIndex < _targetIndex) {
|
||||
_currentIndex = _currentIndex + 1;
|
||||
} else {
|
||||
_currentIndex = _currentIndex + (-1 + arc4random_uniform(3));
|
||||
}
|
||||
[self updateViewForIndex:_currentIndex];
|
||||
}
|
||||
|
||||
- (void)updateViewForIndex:(int)index {
|
||||
for (int i = 0 ; i < _maximumNumberOfDots; i++) {
|
||||
ORKEnvironmentSPLMeterColumnView *columnView = _columnViews[i];
|
||||
NSInteger distanceToIndex = i - index;
|
||||
CGFloat opacityFactor = 0.1 * distanceToIndex;
|
||||
UIColor *grayColor;
|
||||
UIColor *greenColor;
|
||||
UIColor *orangeColor;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
|
||||
greenColor = [UIColor systemGreenColor];
|
||||
orangeColor = [UIColor systemOrangeColor];
|
||||
} else {
|
||||
grayColor = [UIColor grayColor];
|
||||
greenColor = [UIColor greenColor];
|
||||
orangeColor = [UIColor orangeColor];
|
||||
}
|
||||
if (i <= _greenIndexLimit) {
|
||||
if (i < index) {
|
||||
[columnView setColor:greenColor];
|
||||
[columnView setOpacity:1.0];
|
||||
} else {
|
||||
if (distanceToIndex < 3){
|
||||
[columnView setColor:greenColor];
|
||||
[columnView setOpacity:0.5 - opacityFactor];
|
||||
} else {
|
||||
[columnView setColor:grayColor];
|
||||
[columnView setOpacity:1.0];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (i < index) {
|
||||
[columnView setColor:orangeColor];
|
||||
[columnView setOpacity:1.0];
|
||||
} else {
|
||||
if (distanceToIndex < 3){
|
||||
[columnView setColor:orangeColor];
|
||||
[columnView setOpacity:0.5 - opacityFactor];
|
||||
} else {
|
||||
[columnView setColor:grayColor];
|
||||
[columnView setOpacity:1.0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopAnimation {
|
||||
_isAnimating = NO;
|
||||
[_animationTimer invalidate];
|
||||
_animationTimer = nil;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
if (!_didLayoutViews) {
|
||||
_didLayoutViews = YES;
|
||||
[self setupView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_animationTimer invalidate];
|
||||
_animationTimer = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -28,29 +28,40 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKUnitLabel.h"
|
||||
#import "ORKRingView.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKEnvironmentSPLMeterBarView;
|
||||
@class ORKRingView;
|
||||
@class ORKRoundTappingButton;
|
||||
@class ORKNavigationContainerView;
|
||||
@class ORKEnvironmentSPLMeterContentView;
|
||||
|
||||
@protocol ORKEnvironmentSPLMeterContentViewVoiceOverDelegate <NSObject>
|
||||
|
||||
- (void)contentView:(ORKEnvironmentSPLMeterContentView * _Nonnull)contentView shouldAnnounce:(NSString * _Nonnull)inAnnouncement;
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKEnvironmentSPLMeterContentView : ORKActiveStepCustomView
|
||||
|
||||
- (void)setProgress:(CGFloat)progress
|
||||
animated:(BOOL)animated;
|
||||
@property (nonatomic, strong) ORKNavigationContainerView *navigationFooterView;
|
||||
|
||||
@property (nonatomic, weak) id<ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> voiceOverDelegate;
|
||||
|
||||
- (ORKEnvironmentSPLMeterBarView *)barView;
|
||||
|
||||
- (ORKRingView *)ringView;
|
||||
|
||||
- (void)setProgress:(CGFloat)progress;
|
||||
|
||||
- (void)setProgressCircle:(CGFloat)progress;
|
||||
|
||||
- (void)setDBText:(NSString *)text;
|
||||
- (void)setProgressBar:(CGFloat)progress;
|
||||
|
||||
- (void)setThreshold:(double)threshold;
|
||||
|
||||
@property(nonatomic, strong) ORKRingView *ringView;
|
||||
- (void)reachedOptimumNoiseLevel;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -30,29 +30,34 @@
|
||||
|
||||
|
||||
#import "ORKEnvironmentSPLMeterContentView.h"
|
||||
|
||||
#import "ORKEnvironmentSPLMeterBarView.h"
|
||||
#import "ORKRoundTappingButton.h"
|
||||
|
||||
#import "ORKUnitLabel.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKRingView.h"
|
||||
#import "ORKProgressView.h"
|
||||
#import "ORKCompletionCheckmarkView.h"
|
||||
|
||||
static const CGFloat DBLabelFontSize = 35.0;
|
||||
|
||||
static const CGFloat RingViewPadding = 18.0;
|
||||
static const CGFloat InstructionLabelPadding = 8.0;
|
||||
static const CGFloat HalfCircleSize = 14.0;
|
||||
static const CGFloat BarViewHeight = 50.0;
|
||||
|
||||
@interface ORKEnvironmentSPLMeterContentView ()
|
||||
@property(nonatomic, strong) ORKRingView *ringView;
|
||||
@property(nonatomic, strong) ORKEnvironmentSPLMeterBarView *barView;
|
||||
@end
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterContentView {
|
||||
NSLayoutConstraint *_topToProgressViewConstraint;
|
||||
UIStackView *stackView;
|
||||
UIStackView *miniStackView;
|
||||
UILabel *_dBValueLabel;
|
||||
UILabel *_unitLabel;
|
||||
UILabel *_thresholdLabel;
|
||||
UIView *_containerView;
|
||||
UILabel *_DBInstructionLabel;
|
||||
UIImage *_checkmarkImage;
|
||||
UIImage *_xmarkImage;
|
||||
UIImageView *_xmarkView;
|
||||
CGFloat preValue;
|
||||
CGFloat currentValue;
|
||||
CAShapeLayer *circle;
|
||||
ORKProgressView *_loadingView;
|
||||
UIProgressView *_progressView;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
@@ -62,230 +67,166 @@ static const CGFloat DBLabelFontSize = 35.0;
|
||||
currentValue = 0.0;
|
||||
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_ringView = [ORKRingView new];
|
||||
_ringView.animationDuration = 0.8;
|
||||
[self addSubview: _ringView];
|
||||
|
||||
[self setupThresholdLabel];
|
||||
[self setupDBValueLabel];
|
||||
[self setupUnitLabel];
|
||||
[_ringView addSubview:_dBValueLabel];
|
||||
[_ringView addSubview:_unitLabel];
|
||||
[self addSubview:_thresholdLabel];
|
||||
|
||||
_loadingView = [ORKProgressView new];
|
||||
_loadingView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_ringView addSubview:_loadingView];
|
||||
|
||||
_progressView = [UIProgressView new];
|
||||
_progressView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_progressView.progressTintColor = [self tintColor];
|
||||
[_progressView setAlpha:0];
|
||||
[self addSubview:_progressView];
|
||||
|
||||
[self setUpConstraints];
|
||||
[self setupContainerView];
|
||||
[self setupDBInstructionLabel];
|
||||
[self setupRingView];
|
||||
[self setupBarView];
|
||||
[self setupXmarkView];
|
||||
[self setProgressCircle:0.0];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) setupDBValueLabel {
|
||||
if (!_dBValueLabel) {
|
||||
_dBValueLabel = [UILabel new];
|
||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
||||
[super traitCollectionDidChange:previousTraitCollection];
|
||||
_DBInstructionLabel.font = [self title3TextFont];
|
||||
}
|
||||
|
||||
- (UIFont *)title3TextFont {
|
||||
UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleTitle3];
|
||||
UIFontDescriptor *fontDescriptor = [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
|
||||
return [UIFont fontWithDescriptor:fontDescriptor size:[[descriptor objectForKey: UIFontDescriptorSizeAttribute] doubleValue]];
|
||||
}
|
||||
|
||||
- (void)setupContainerView {
|
||||
if (!_containerView) {
|
||||
_containerView = [UIView new];
|
||||
}
|
||||
_dBValueLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_dBValueLabel.numberOfLines = 0;
|
||||
_dBValueLabel.textColor = [[UIColor blackColor] colorWithAlphaComponent:0.7];
|
||||
_dBValueLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
_dBValueLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[_dBValueLabel setText:ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil)];
|
||||
[_dBValueLabel setFont:[UIFont systemFontOfSize:DBLabelFontSize weight:UIFontWeightThin]];
|
||||
}
|
||||
|
||||
- (void) setupUnitLabel {
|
||||
if (!_unitLabel) {
|
||||
_unitLabel = [UILabel new];
|
||||
}
|
||||
_unitLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_unitLabel.numberOfLines = 0;
|
||||
_unitLabel.textColor = [[UIColor grayColor] colorWithAlphaComponent:1.0];
|
||||
_unitLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
_unitLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[_unitLabel setText:ORKLocalizedString(@"ENVIRONMENTSPL_UNIT", nil)];
|
||||
[_unitLabel setHidden:YES];
|
||||
[_unitLabel setFont:[UIFont systemFontOfSize:15 weight:UIFontWeightLight]];
|
||||
}
|
||||
|
||||
- (void)setupThresholdLabel {
|
||||
if (!_thresholdLabel) {
|
||||
_thresholdLabel = [UILabel new];
|
||||
}
|
||||
_thresholdLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_thresholdLabel.numberOfLines = 0;
|
||||
_thresholdLabel.textColor = [[UIColor grayColor] colorWithAlphaComponent:1.0];
|
||||
_thresholdLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
_thresholdLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[_thresholdLabel setFont:[UIFont systemFontOfSize:15 weight:UIFontWeightThin]];
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
_progressView.progressTintColor = [self tintColor];
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress
|
||||
animated:(BOOL)animated {
|
||||
_containerView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_containerView];
|
||||
|
||||
[_progressView setProgress:progress animated:animated];
|
||||
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
|
||||
[_progressView setAlpha:(progress == 0) ? 0 : 1];
|
||||
}];
|
||||
[[_containerView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:-RingViewPadding] setActive:YES];
|
||||
[[_containerView.topAnchor constraintGreaterThanOrEqualToAnchor:self.topAnchor] setActive:YES];
|
||||
[[_containerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
|
||||
[[_containerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
|
||||
[[_containerView.topAnchor constraintLessThanOrEqualToAnchor:self.bottomAnchor] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)setupXmarkView {
|
||||
if (!_xmarkView) {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageConfiguration *configuration = [UIImageSymbolConfiguration configurationWithPointSize:HalfCircleSize weight:UIImageSymbolWeightBold scale:UIImageSymbolScaleDefault];
|
||||
_xmarkImage = [[UIImage systemImageNamed:@"xmark" withConfiguration:configuration] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
_checkmarkImage = [[UIImage systemImageNamed:@"checkmark" withConfiguration:configuration] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
}
|
||||
_xmarkView = [[UIImageView alloc] initWithImage: _xmarkImage];
|
||||
_xmarkView.tintColor = UIColor.systemOrangeColor;
|
||||
}
|
||||
_xmarkView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_containerView addSubview:_xmarkView];
|
||||
|
||||
[[_xmarkView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
|
||||
[[_xmarkView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
|
||||
_xmarkView.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)setupBarView {
|
||||
if (!_barView) {
|
||||
_barView = [[ORKEnvironmentSPLMeterBarView alloc] initWithFrame:CGRectZero];
|
||||
}
|
||||
|
||||
_barView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_containerView addSubview:_barView];
|
||||
|
||||
[[_barView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor] setActive:YES];
|
||||
[[_barView.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor] setActive:YES];
|
||||
|
||||
[[_barView.heightAnchor constraintEqualToConstant:BarViewHeight] setActive:YES];
|
||||
[[_barView.topAnchor constraintEqualToAnchor:_DBInstructionLabel.bottomAnchor constant:RingViewPadding] setActive:YES];
|
||||
[[_barView.bottomAnchor constraintEqualToAnchor:_containerView.bottomAnchor constant:RingViewPadding] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)setupRingView {
|
||||
if (!_ringView) {
|
||||
_ringView = [ORKRingView new];
|
||||
}
|
||||
_ringView.animationDuration = 0.0;
|
||||
_ringView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_containerView addSubview:_ringView];
|
||||
|
||||
[[_ringView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor] setActive:YES];
|
||||
[[_ringView.centerYAnchor constraintEqualToAnchor:_DBInstructionLabel.centerYAnchor] setActive:YES];
|
||||
[[_ringView.trailingAnchor constraintEqualToAnchor:_DBInstructionLabel.leadingAnchor constant:-InstructionLabelPadding] setActive:YES];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
[_ringView setColor:UIColor.systemGray6Color];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupDBInstructionLabel {
|
||||
if (!_DBInstructionLabel) {
|
||||
_DBInstructionLabel = [ORKLabel new];
|
||||
_DBInstructionLabel.numberOfLines = 0;
|
||||
_DBInstructionLabel.font = [self title3TextFont];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_DBInstructionLabel.textColor = UIColor.labelColor;
|
||||
}
|
||||
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil);
|
||||
}
|
||||
_DBInstructionLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_containerView addSubview:_DBInstructionLabel];
|
||||
|
||||
[[_DBInstructionLabel.topAnchor constraintEqualToAnchor:_containerView.topAnchor constant:InstructionLabelPadding] setActive:YES];
|
||||
[[_DBInstructionLabel.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor constant:-InstructionLabelPadding] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)setProgressBar:(CGFloat)progress {
|
||||
[_barView setProgress:progress];
|
||||
}
|
||||
|
||||
- (void)setProgressCircle:(CGFloat)progress {
|
||||
[_ringView setValue:progress WithColor:progress < 1.0 ? [[UIColor greenColor] colorWithAlphaComponent:0.5] : [[UIColor redColor] colorWithAlphaComponent:0.5]];
|
||||
if (progress >= ORKRingViewMaximumValue) {
|
||||
} else {
|
||||
[_ringView resetLayerColors];
|
||||
}
|
||||
|
||||
[self updateInstructionForValue:progress];
|
||||
}
|
||||
|
||||
- (void)setThreshold:(double)threshold {
|
||||
if (_thresholdLabel) {
|
||||
[_thresholdLabel setText:[NSString stringWithFormat:ORKLocalizedString(@"ENVIRONMENTSPL_THRESHOLD", nil), @(threshold)]];
|
||||
}
|
||||
- (ORKRingView *)ringView {
|
||||
return _ringView;
|
||||
}
|
||||
|
||||
- (void)setDBText:(NSString *)text {
|
||||
if (_loadingView) {
|
||||
[_loadingView setHidden:YES];
|
||||
[_loadingView removeFromSuperview];
|
||||
_loadingView = nil;
|
||||
- (void)setProgress:(CGFloat)progress {
|
||||
CGFloat value = progress < ORKRingViewMinimumValue ? ORKRingViewMinimumValue : progress;
|
||||
[_ringView setValue:value];
|
||||
}
|
||||
|
||||
}
|
||||
if (_dBValueLabel) {
|
||||
[_dBValueLabel setText:[NSString stringWithFormat:@"%@", text]];
|
||||
[_unitLabel setHidden:NO];
|
||||
}
|
||||
- (void)updateInstructionForValue:(CGFloat)progress {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSString *currentInstruction = [_DBInstructionLabel.text copy];
|
||||
BOOL isNoise = (progress >= ORKRingViewMaximumValue);
|
||||
NSString *newInstruction = isNoise ? ORKLocalizedString(@"ENVIRONMENTSPL_NOISE", nil) : ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil);
|
||||
_xmarkView.hidden = !isNoise;
|
||||
|
||||
if (![newInstruction isEqualToString:currentInstruction]) {
|
||||
_DBInstructionLabel.text = newInstruction;
|
||||
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)]) {
|
||||
[self.voiceOverDelegate contentView:self shouldAnnounce:_DBInstructionLabel.text];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)finishStep:(ORKActiveStepViewController *)viewController {
|
||||
[super finishStep:viewController];
|
||||
}
|
||||
|
||||
- (void)updateLayoutMargins {
|
||||
CGFloat margin = ORKStandardHorizontalMarginForView(self);
|
||||
self.layoutMargins = (UIEdgeInsets){.left = margin * 2, .right = margin * 2};
|
||||
}
|
||||
- (void)reachedOptimumNoiseLevel {
|
||||
_xmarkView.hidden = NO;
|
||||
_xmarkView.image = _checkmarkImage;
|
||||
_xmarkView.tintColor = UIColor.systemGreenColor;
|
||||
|
||||
- (void)setFrame:(CGRect)frame {
|
||||
[super setFrame:frame];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds {
|
||||
[super setBounds:bounds];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
|
||||
NSArray *constraints = @[
|
||||
|
||||
[NSLayoutConstraint constraintWithItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0 constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0
|
||||
constant:-80.0],
|
||||
[NSLayoutConstraint constraintWithItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_unitLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_unitLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:10.0],
|
||||
[NSLayoutConstraint constraintWithItem:_thresholdLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_thresholdLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:-20.0],
|
||||
[NSLayoutConstraint constraintWithItem:_loadingView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_loadingView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:5.0],
|
||||
[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:80.0],
|
||||
[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeLeft
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeLeft
|
||||
multiplier:1.0
|
||||
constant:5.0],
|
||||
[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeRight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeRight
|
||||
multiplier:1.0
|
||||
constant:-5.0],
|
||||
|
||||
];
|
||||
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
|
||||
|
||||
[self addConstraints:constraints];
|
||||
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)]) {
|
||||
[self.voiceOverDelegate contentView:self shouldAnnounce:_DBInstructionLabel.text];
|
||||
}
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
[_ringView setBackgroundLayerStrokeColor:UIColor.systemGreenColor circleStrokeColor:UIColor.systemGreenColor withAnimationDuration:0.0];
|
||||
|
||||
[_barView stopAnimation];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKEnvironmentSPLMeterResult : ORKResult
|
||||
|
||||
@property (nonatomic, assign) double sensitivityOffset;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
#import "ORKEnvironmentSPLMeterStep.h"
|
||||
#import "ORKEnvironmentSPLMeterStepViewController.h"
|
||||
|
||||
#import "ORKRecorder_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#define ORKEnvironmentSPLMeterTaskDefaultThresholdValue 35.0
|
||||
@@ -56,6 +56,12 @@
|
||||
self.thresholdValue = ORKEnvironmentSPLMeterTaskDefaultThresholdValue;
|
||||
self.samplingInterval = ORKEnvironmentSPLMeterTaskMinimumSamplingInterval;
|
||||
self.requiredContiguousSamples = ORKEnvironmentSPLMeterTaskDefaultRequiredContiguousSamples;
|
||||
self.stepDuration = CGFLOAT_MAX;
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
|
||||
// This is inserted here because it is required for any task that requires the SPL Meter step
|
||||
ORKAudioStreamerConfiguration *config = [[ORKAudioStreamerConfiguration alloc] initWithIdentifier:[NSString stringWithFormat:@"%@_streamerConfiguration",self.identifier]];
|
||||
self.recorderConfigurations = @[config];
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
|
||||
@@ -31,22 +31,32 @@
|
||||
|
||||
#import "ORKEnvironmentSPLMeterStepViewController.h"
|
||||
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKStepView.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
#import "ORKRoundTappingButton.h"
|
||||
#import "ORKEnvironmentSPLMeterContentView.h"
|
||||
#import "ORKRingView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKTaskViewController_Internal.h"
|
||||
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKEnvironmentSPLMeterResult.h"
|
||||
#import "ORKEnvironmentSPLMeterStep.h"
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
@interface ORKEnvironmentSPLMeterStepViewController () {
|
||||
|
||||
static const NSTimeInterval SPL_METER_PLAY_DELAY_VOICEOVER = 3.0;
|
||||
|
||||
@interface ORKEnvironmentSPLMeterStepViewController ()<ORKRingViewDelegate, ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> {
|
||||
AVAudioEngine *_audioEngine;
|
||||
AVAudioInputNode *_inputNode;
|
||||
AVAudioUnitEQ *_eqUnit;
|
||||
@@ -64,6 +74,12 @@
|
||||
NSInteger _requiredContiguousSamples;
|
||||
int _counter;
|
||||
NSMutableArray *_recordedSamples;
|
||||
AVAudioSessionCategory _savedSessionCategory;
|
||||
AVAudioSessionMode _savedSessionMode;
|
||||
AVAudioSessionCategoryOptions _savedSessionCategoryOptions;
|
||||
UINotificationFeedbackGenerator *_notificationFeedbackGenerator;
|
||||
dispatch_semaphore_t _voiceOverAnnouncementSemaphore;
|
||||
NSTimer *_timeoutTimer;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) ORKEnvironmentSPLMeterContentView *environmentSPLMeterContentView;
|
||||
@@ -85,39 +101,45 @@
|
||||
_requiredContiguousSamples = 1;
|
||||
_sensitivityOffset = -23.3;
|
||||
_recordedSamples = [NSMutableArray new];
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initializeInternalButtonItems {
|
||||
[super initializeInternalButtonItems];
|
||||
|
||||
// Don't show next button
|
||||
self.internalContinueButtonItem = nil;
|
||||
self.internalDoneButtonItem = nil;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
|
||||
[super viewDidLoad];
|
||||
[self saveAudioSession];
|
||||
_sensitivityOffset = [self sensitivityOffsetForDevice];
|
||||
_environmentSPLMeterContentView = [ORKEnvironmentSPLMeterContentView new];
|
||||
[_environmentSPLMeterContentView setProgress:0.01 animated:YES];
|
||||
[self setNavigationFooterView];
|
||||
_environmentSPLMeterContentView.voiceOverDelegate = self;
|
||||
_environmentSPLMeterContentView.ringView.delegate = self;
|
||||
self.activeStepView.activeCustomView = _environmentSPLMeterContentView;
|
||||
[self requestMicrophoneAuthorization];
|
||||
|
||||
[self requestRecordPermissionIfNeeded];
|
||||
[self configureAudioSession];
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
|
||||
_inputNode = [_audioEngine inputNode];
|
||||
_inputNodeOutputFormat = [_inputNode inputFormatForBus:0];
|
||||
_sampleRate = (uint32_t)_inputNodeOutputFormat.sampleRate;
|
||||
_bufferSize = _sampleRate/10;
|
||||
_countToFetch = _sampleRate/(int)_bufferSize;
|
||||
[self configureEQ];
|
||||
[_audioEngine attachNode:_eqUnit];
|
||||
[_audioEngine connect:_inputNode to:_eqUnit format:_inputNodeOutputFormat];
|
||||
|
||||
[self setupFeedbackGenerator];
|
||||
}
|
||||
|
||||
- (void)saveAudioSession {
|
||||
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
|
||||
_savedSessionCategory = audioSession.category;
|
||||
_savedSessionMode = audioSession.mode;
|
||||
_savedSessionCategoryOptions = audioSession.categoryOptions;
|
||||
}
|
||||
|
||||
- (void)setNavigationFooterView {
|
||||
|
||||
self.activeStepView.navigationFooterView.continueButtonItem = self.continueButtonItem;
|
||||
self.activeStepView.navigationFooterView.continueEnabled = NO;
|
||||
[self.activeStepView.navigationFooterView updateContinueAndSkipEnabled];
|
||||
}
|
||||
|
||||
- (void)setContinueButtonItem:(UIBarButtonItem *)continueButtonItem {
|
||||
[super setContinueButtonItem:continueButtonItem];
|
||||
_navigationFooterView.continueButtonItem = continueButtonItem;
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
@@ -128,17 +150,25 @@
|
||||
_samplingInterval = [self environmentSPLMeterStep].samplingInterval;
|
||||
_requiredContiguousSamples = [self environmentSPLMeterStep].requiredContiguousSamples;
|
||||
_thresholdValue = [self environmentSPLMeterStep].thresholdValue;
|
||||
[_environmentSPLMeterContentView setThreshold:_thresholdValue];
|
||||
|
||||
[self configureInputNode];
|
||||
[self splWorkBlock];
|
||||
|
||||
if (UIAccessibilityIsVoiceOverRunning()) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SPL_METER_PLAY_DELAY_VOICEOVER * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
[self resetAudioSession];
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
[self resetAudioSession];
|
||||
|
||||
}
|
||||
|
||||
- (NSString *)deviceType {
|
||||
@@ -173,17 +203,87 @@
|
||||
return sResult;
|
||||
}
|
||||
|
||||
- (void)requestMicrophoneAuthorization {
|
||||
[[AVAudioSession sharedInstance] recordPermission];
|
||||
|
||||
- (void)requestRecordPermissionIfNeeded
|
||||
{
|
||||
[self handleRecordPermission:[[AVAudioSession sharedInstance] recordPermission]];
|
||||
}
|
||||
|
||||
- (void)handleRecordPermission:(AVAudioSessionRecordPermission)recordPermission
|
||||
{
|
||||
switch (recordPermission)
|
||||
{
|
||||
case AVAudioSessionRecordPermissionGranted:
|
||||
break;
|
||||
|
||||
case AVAudioSessionRecordPermissionDenied:
|
||||
{
|
||||
ORK_Log_Error("User has denied record permission for a step which requires microphone access.");
|
||||
break;
|
||||
}
|
||||
case AVAudioSessionRecordPermissionUndetermined:
|
||||
{
|
||||
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
|
||||
[self handleRecordPermission:granted ? AVAudioSessionRecordPermissionGranted : AVAudioSessionRecordPermissionDenied];
|
||||
}];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configureAudioSession {
|
||||
NSError *error = nil;
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionMixWithOthers error:&error];
|
||||
if ([AVAudioSession sharedInstance].isOtherAudioPlaying) {
|
||||
NSError *activationError = nil;
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&activationError];
|
||||
|
||||
AVAudioSession * session = [AVAudioSession sharedInstance];
|
||||
|
||||
// Stop any existing audio
|
||||
[session setCategory:AVAudioSessionCategorySoloAmbient error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
[session setActive:YES error:&error];
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
// Force input/output from iOS device
|
||||
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
// When setting the input like this, we do not need to set the input AND the output to the iPhone.
|
||||
NSArray<AVAudioSessionPortDescription *> * inputs = [session availableInputs];
|
||||
for (AVAudioSessionPortDescription* desc in inputs) {
|
||||
if ([desc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
|
||||
// go ahead and set our preferred input to the built-in mic
|
||||
[session setPreferredInput:desc error:&error];
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error("Setting AVAudioSession preferred input failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[session setActive:YES error:&error];
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configureInputNode {
|
||||
_inputNode = [_audioEngine inputNode];
|
||||
_inputNodeOutputFormat = [_inputNode inputFormatForBus:0];
|
||||
_sampleRate = (uint32_t)_inputNodeOutputFormat.sampleRate;
|
||||
_bufferSize = _sampleRate/10;
|
||||
_countToFetch = _sampleRate > 0 ? _sampleRate/(int)_bufferSize : 0;
|
||||
[self configureEQ];
|
||||
[_audioEngine attachNode:_eqUnit];
|
||||
[_audioEngine connect:_inputNode to:_eqUnit format:_inputNodeOutputFormat];
|
||||
}
|
||||
|
||||
- (void)configureEQ {
|
||||
@@ -229,9 +329,14 @@
|
||||
eqCoefficient.bypass = NO;
|
||||
}
|
||||
|
||||
|
||||
- (void)splWorkBlock {
|
||||
if (!_audioEngine.isRunning && ![[AVAudioSession sharedInstance] isOtherAudioPlaying]) {
|
||||
// secondaryAudioShouldBeSilencedHint returns true if VoiceOver is running.
|
||||
// Since we are killing all audio when configuring the session, here we can make a safe assumption that if VoiceOver is running, allow the user to continue even if the secondaryAudioShouldBeSilencedHint is YES.
|
||||
// If VoiceOver is not running, we can still gate based on the secondaryAudioShouldBeSilencedHint.
|
||||
|
||||
BOOL otherAudioIsProhibitingMeasurement = [[AVAudioSession sharedInstance] secondaryAudioShouldBeSilencedHint] && !UIAccessibilityIsVoiceOverRunning();
|
||||
|
||||
if (!_audioEngine.isRunning && !otherAudioIsProhibitingMeasurement) {
|
||||
[_eqUnit installTapOnBus:0
|
||||
bufferSize:_bufferSize
|
||||
format:_inputNodeOutputFormat
|
||||
@@ -265,24 +370,33 @@
|
||||
[_recordedSamples addObject:[NSNumber numberWithFloat:_spl]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.environmentSPLMeterContentView setProgressCircle:(_spl/_thresholdValue)];
|
||||
[self.environmentSPLMeterContentView setDBText:[NSString stringWithFormat:@"%.f", _spl]];
|
||||
});
|
||||
[self evaluateThreshold:_spl];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
} else {
|
||||
if (rms > 0.0 && _sampleRate > 0.0) {
|
||||
float spl = (20 * log10f(sqrtf(rms/(float)_sampleRate))) - _sensitivityOffset + 96;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.environmentSPLMeterContentView setProgressBar:(spl/_thresholdValue)];
|
||||
});
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.environmentSPLMeterContentView setProgressBar:(_spl/_thresholdValue)];
|
||||
});
|
||||
}
|
||||
}
|
||||
dispatch_semaphore_signal(_semaphoreRms);
|
||||
});
|
||||
dispatch_semaphore_wait(_semaphoreRms, DISPATCH_TIME_FOREVER);
|
||||
} else if ([AVAudioSession sharedInstance].recordPermission == AVAudioSessionRecordPermissionDenied) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.environmentSPLMeterContentView setDBText:[NSString stringWithFormat:@"N/A"]];
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
});
|
||||
}
|
||||
}];
|
||||
if (!_audioEngine.isRunning && ![[AVAudioSession sharedInstance] isOtherAudioPlaying]) {
|
||||
if (!_audioEngine.isRunning && !otherAudioIsProhibitingMeasurement) {
|
||||
NSError *error = nil;
|
||||
[_audioEngine startAndReturnError:&error];
|
||||
} else {
|
||||
@@ -293,42 +407,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void) evaluateThreshold: (float)spl {
|
||||
if (spl < _thresholdValue) {
|
||||
- (void)evaluateThreshold:(float)spl
|
||||
{
|
||||
if (spl < _thresholdValue)
|
||||
{
|
||||
_counter += 1;
|
||||
if (_counter >= _requiredContiguousSamples) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self finish];
|
||||
});
|
||||
|
||||
[self.environmentSPLMeterContentView.ringView fillRingWithDuration:(double)_requiredContiguousSamples*_samplingInterval];
|
||||
|
||||
if (_counter >= _requiredContiguousSamples)
|
||||
{
|
||||
[self reachedOptimumNoiseLevel];
|
||||
|
||||
[self sendHapticEvent:UINotificationFeedbackTypeSuccess];
|
||||
}
|
||||
} else {
|
||||
_counter = 0;
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.environmentSPLMeterContentView setProgress:((float)_counter/_requiredContiguousSamples) + 0.01 animated:YES];
|
||||
});
|
||||
else
|
||||
{
|
||||
_counter = 0;
|
||||
self.environmentSPLMeterContentView.ringView.animationDuration = 0.5;
|
||||
[self.environmentSPLMeterContentView setProgress:0.0];
|
||||
|
||||
[self sendHapticEvent:UINotificationFeedbackTypeError];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) resetAudioSession {
|
||||
- (void)resetAudioSession {
|
||||
NSError *error = nil;
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionMixWithOthers error:&error];
|
||||
[[AVAudioSession sharedInstance] setCategory:_savedSessionCategory mode:_savedSessionMode options:_savedSessionCategoryOptions error:&error];
|
||||
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error(@"Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
|
||||
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
if ([AVAudioSession sharedInstance].isOtherAudioPlaying) {
|
||||
NSError *activationError = nil;
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&activationError];
|
||||
if (activationError) {
|
||||
ORK_Log_Error(@"Activating AVAudioSession failed with error message: \"%@\"", activationError.localizedDescription);
|
||||
}
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reachedOptimumNoiseLevel {
|
||||
[_audioEngine stop];
|
||||
[self resetAudioSession];
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
[super stepDidFinish];
|
||||
|
||||
[self.environmentSPLMeterContentView finishStep:self];
|
||||
[self resetAudioSession];
|
||||
[self goForward];
|
||||
}
|
||||
|
||||
@@ -340,5 +464,35 @@
|
||||
return (ORKEnvironmentSPLMeterStep *)self.step;
|
||||
}
|
||||
|
||||
#pragma mark - ORKRingViewDelegate
|
||||
|
||||
- (void)ringViewDidFinishFillAnimation {
|
||||
[self.environmentSPLMeterContentView reachedOptimumNoiseLevel];
|
||||
self.activeStepView.navigationFooterView.continueEnabled = YES;
|
||||
}
|
||||
|
||||
#pragma mark - UINotificationFeedbackGenerator
|
||||
|
||||
- (void)setupFeedbackGenerator
|
||||
{
|
||||
_notificationFeedbackGenerator = [[UINotificationFeedbackGenerator alloc] init];
|
||||
[_notificationFeedbackGenerator prepare];
|
||||
}
|
||||
|
||||
- (void)sendHapticEvent:(UINotificationFeedbackType)eventType
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_notificationFeedbackGenerator notificationOccurred:eventType];
|
||||
[_notificationFeedbackGenerator prepare];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - ORKEnvironmentSPLMeterContentViewVoiceOverDelegate
|
||||
|
||||
- (void)contentView:(ORKEnvironmentSPLMeterContentView *)contentView shouldAnnounce:(NSString *)inAnnouncement
|
||||
{
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, inAnnouncement);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ ORK_CLASS_AVAILABLE
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSURL *fileURL;
|
||||
|
||||
|
||||
@property (nonatomic, copy, nullable) NSString *fileName;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_URL(aCoder, fileURL);
|
||||
ORK_ENCODE_OBJ(aCoder, contentType);
|
||||
ORK_ENCODE_OBJ(aCoder, fileName);
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
@@ -52,6 +53,7 @@
|
||||
if (self) {
|
||||
ORK_DECODE_URL(aDecoder, fileURL);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, contentType, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, fileName, NSString);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -66,7 +68,8 @@
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualFileURLs(self.fileURL, castObject.fileURL) &&
|
||||
ORKEqualObjects(self.contentType, castObject.contentType));
|
||||
ORKEqualObjects(self.contentType, castObject.contentType) &&
|
||||
ORKEqualObjects(self.fileName, castObject.fileName));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
@@ -77,6 +80,7 @@
|
||||
ORKFileResult *result = [super copyWithZone:zone];
|
||||
result.fileURL = [self.fileURL copy];
|
||||
result.contentType = [self.contentType copy];
|
||||
result.fileName = [self.fileName copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -34,19 +34,38 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Displays a countdown ring and a timer.
|
||||
//
|
||||
// ------------------------------
|
||||
// | |
|
||||
// | Title Label |
|
||||
// | |
|
||||
// | subtitle label |
|
||||
// | |
|
||||
// | __________ |
|
||||
// | / \ |
|
||||
// | | 2:30 | |
|
||||
// | \ ________ / |
|
||||
// | |
|
||||
// |______________________________|
|
||||
@interface ORKFitnessContentView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic, assign, getter=isFinished) BOOL finished;
|
||||
|
||||
@property (nonatomic) BOOL hasHeartRate;
|
||||
@property (nonatomic) BOOL hasDistance;
|
||||
/// The total amount of time the active task is supposed to be performed for.
|
||||
/// For the six minute walk test, this will typically be 360 seconds.
|
||||
@property (nonatomic) NSTimeInterval duration;
|
||||
|
||||
@property (nonatomic, copy, nullable) NSString *heartRate;
|
||||
@property (nonatomic) double distanceInMeters;
|
||||
/// The amount of time that still remain.
|
||||
@property (nonatomic) NSTimeInterval timeLeft;
|
||||
|
||||
@property (nonatomic, strong, nullable) UIImage *image;
|
||||
/// Whether or not the text label is hidden.
|
||||
@property (nonatomic) BOOL labelHidden;
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval timeLeft;
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithDuration:(NSTimeInterval)duration;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -31,28 +31,8 @@
|
||||
|
||||
#import "ORKFitnessContentView.h"
|
||||
|
||||
#import "ORKActiveStepQuantityView.h"
|
||||
#import "ORKTintedImageView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
@import CoreMotion;
|
||||
@import HealthKit;
|
||||
|
||||
|
||||
// #define LAYOUT_TEST 1
|
||||
// #define LAYOUT_DEBUG 1
|
||||
|
||||
@interface ORKFitnessContentView () {
|
||||
ORKQuantityLabel *_timerLabel;
|
||||
ORKQuantityPairView *_quantityPairView;
|
||||
UIView *_imageSpacer1;
|
||||
UIView *_imageSpacer2;
|
||||
ORKTintedImageView *_imageView;
|
||||
NSLengthFormatter *_lengthFormatter;
|
||||
NSLayoutConstraint *_imageRatioConstraint;
|
||||
NSLayoutConstraint *_topConstraint;
|
||||
UILabel *_timerLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -60,272 +40,72 @@
|
||||
|
||||
@implementation ORKFitnessContentView
|
||||
|
||||
- (ORKActiveStepQuantityView *)distanceView {
|
||||
return _quantityPairView.leftView;
|
||||
}
|
||||
|
||||
- (ORKActiveStepQuantityView *)heartRateView {
|
||||
return _quantityPairView.rightView;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
- (instancetype)initWithDuration:(NSTimeInterval)duration {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_timerLabel = [ORKQuantityLabel new];
|
||||
_quantityPairView = [ORKQuantityPairView new];
|
||||
_imageSpacer1 = [UIView new];
|
||||
_imageSpacer1.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_imageSpacer2 = [UIView new];
|
||||
_imageSpacer2.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_imageSpacer1];
|
||||
[self addSubview:_imageSpacer2];
|
||||
[self heartRateView].image = [UIImage imageNamed:@"heart-fitness" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
|
||||
[self updateLengthFormatter];
|
||||
_imageView = [ORKTintedImageView new];
|
||||
_imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_imageView.shouldApplyTint = YES;
|
||||
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_quantityPairView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self updateKeylineVisible];
|
||||
|
||||
_timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently;
|
||||
_imageView.isAccessibilityElement = NO;
|
||||
|
||||
self.hasHeartRate = _hasHeartRate;
|
||||
self.hasDistance = _hasDistance;
|
||||
|
||||
#if LAYOUT_TEST
|
||||
self.timeLeft = 60 * 5;
|
||||
self.hasHeartRate = YES;
|
||||
self.hasDistance = YES;
|
||||
self.distanceInMeters = 100;
|
||||
self.heartRate = @"22";
|
||||
#endif
|
||||
#if LAYOUT_DEBUG
|
||||
self.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
|
||||
_quantityPairView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.2];
|
||||
#endif
|
||||
|
||||
[self setDistanceInMeters:0];
|
||||
[self heartRateView].title = ORKLocalizedString(@"FITNESS_HEARTRATE_TITLE", nil);
|
||||
_duration = duration;
|
||||
_timeLeft = duration;
|
||||
|
||||
_timerLabel = [[UILabel alloc] init];
|
||||
_timerLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_timerLabel.font = [self labelFont];
|
||||
_timerLabel.adjustsFontForContentSizeCategory = YES;
|
||||
_timerLabel.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
|
||||
|
||||
[self addSubview:_quantityPairView];
|
||||
[self addSubview:_imageView];
|
||||
[self addSubview:_timerLabel];
|
||||
[self setUpConstraints];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
|
||||
|
||||
[self tintColorDidChange];
|
||||
[self updateTimerLabel];
|
||||
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
- (void)tintColorDidChange {
|
||||
[self setNeedsDisplay];
|
||||
_timerLabel.textColor = self.tintColor;
|
||||
}
|
||||
|
||||
- (void)updateLengthFormatter {
|
||||
_lengthFormatter = [NSLengthFormatter new];
|
||||
_lengthFormatter.numberFormatter.maximumFractionDigits = 1;
|
||||
_lengthFormatter.numberFormatter.maximumSignificantDigits = 3;
|
||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
||||
_timerLabel.font = [self labelFont];
|
||||
}
|
||||
|
||||
- (void)localeDidChange:(NSNotification *)notification {
|
||||
[self updateLengthFormatter];
|
||||
[self setDistanceInMeters:_distanceInMeters];
|
||||
}
|
||||
|
||||
- (void)willMoveToWindow:(UIWindow *)newWindow {
|
||||
[super willMoveToWindow:newWindow];
|
||||
[self updateConstraintConstantsForWindow:newWindow];
|
||||
}
|
||||
|
||||
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
|
||||
const CGFloat CaptionBaselineToTimerTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToFitnessTimerTop, window);
|
||||
const CGFloat CaptionBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
|
||||
_topConstraint.constant = (CaptionBaselineToTimerTop - CaptionBaselineToStepViewTop);
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_timerLabel, _imageView, _quantityPairView, _imageSpacer1, _imageSpacer2);
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_timerLabel][_imageSpacer1(>=0)][_imageView]"
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
_topConstraint = [NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
[constraints addObject:_topConstraint];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self attribute:NSLayoutAttributeWidth
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self attribute:NSLayoutAttributeWidth
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_imageView][_imageSpacer2(>=0)][_quantityPairView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer2
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_imageSpacer2
|
||||
attribute:NSLayoutAttributeHeight
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
NSLayoutConstraint *imageSpacerHeightConstraint = [NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
imageSpacerHeightConstraint.priority = UILayoutPriorityDefaultLow - 1;
|
||||
[constraints addObject:imageSpacerHeightConstraint];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_quantityPairView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
NSLayoutConstraint *maxWidthConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
maxWidthConstraint.priority = UILayoutPriorityRequired - 1;
|
||||
[constraints addObject:maxWidthConstraint];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
[self updateConstraintConstantsForWindow:self.window];
|
||||
}
|
||||
|
||||
- (void)setImage:(UIImage *)image {
|
||||
_image = image;
|
||||
_imageView.image = image;
|
||||
|
||||
_imageRatioConstraint.active = NO;
|
||||
|
||||
CGSize size = image.size;
|
||||
if (size.width > 0 && size.height > 0) {
|
||||
_imageRatioConstraint = [NSLayoutConstraint constraintWithItem:_imageView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_imageView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
multiplier:size.height / size.width
|
||||
constant:0.0];
|
||||
_imageRatioConstraint.active = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHasDistance:(BOOL)hasDistance {
|
||||
_hasDistance = hasDistance;
|
||||
[self distanceView].enabled = _hasDistance;
|
||||
[self updateKeylineVisible];
|
||||
}
|
||||
|
||||
- (void)setHasHeartRate:(BOOL)hasHeartRate {
|
||||
_hasHeartRate = hasHeartRate;
|
||||
[self heartRateView].enabled = _hasHeartRate;
|
||||
[self updateKeylineVisible];
|
||||
}
|
||||
|
||||
- (void)setHeartRate:(NSString *)heartRate {
|
||||
_heartRate = heartRate;
|
||||
[self heartRateView].value = heartRate;
|
||||
}
|
||||
|
||||
- (void)updateKeylineVisible {
|
||||
[_quantityPairView setKeylineHidden:!(_hasDistance && _hasHeartRate)];
|
||||
}
|
||||
|
||||
- (void)setDistanceInMeters:(double)distanceInMeters {
|
||||
_distanceInMeters = distanceInMeters;
|
||||
double displayDistance = _distanceInMeters;
|
||||
NSString *distanceString = nil;
|
||||
NSLengthFormatterUnit unit;
|
||||
NSString *unitString = [_lengthFormatter unitStringFromMeters:displayDistance usedUnit:&unit];
|
||||
|
||||
switch (unit) {
|
||||
case NSLengthFormatterUnitCentimeter:
|
||||
case NSLengthFormatterUnitMillimeter:
|
||||
unit = NSLengthFormatterUnitMeter;
|
||||
// Force showing 0 meters if the distance is sufficiently short to be displayed in cm or mm
|
||||
unitString = [_lengthFormatter unitStringFromValue:0 unit:NSLengthFormatterUnitMeter];
|
||||
displayDistance = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Use HealthKit to convert the unit, so we can use the number formatter directly.
|
||||
HKUnit *hkUnit = [HKUnit unitFromLengthFormatterUnit:unit];
|
||||
double conversionFactor = 1.0;
|
||||
if ([hkUnit isNull] && (unit == NSLengthFormatterUnitYard)) {
|
||||
hkUnit = [HKUnit footUnit];
|
||||
conversionFactor = 1.0 / 3.0;
|
||||
}
|
||||
HKQuantity *quantity = [HKQuantity quantityWithUnit:[HKUnit meterUnit] doubleValue:displayDistance];
|
||||
distanceString = [_lengthFormatter.numberFormatter stringFromNumber:@([quantity doubleValueForUnit:hkUnit]*conversionFactor)];
|
||||
|
||||
[self distanceView].title = [NSString localizedStringWithFormat:ORKLocalizedString(@"FITNESS_DISTANCE_TITLE_FORMAT", nil), unitString];
|
||||
[self distanceView].value = distanceString;
|
||||
- (void)setDuration:(NSTimeInterval)duration {
|
||||
_duration = duration;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setTimeLeft:(NSTimeInterval)timeLeft {
|
||||
_timeLeft = timeLeft;
|
||||
[self updateTimerLabel];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (BOOL)labelHidden {
|
||||
return _timerLabel.isHidden;
|
||||
}
|
||||
|
||||
- (void)setLabelHidden:(BOOL)labelHidden {
|
||||
[_timerLabel setHidden:labelHidden];
|
||||
}
|
||||
|
||||
- (UIFont*) labelFont {
|
||||
|
||||
UIFont* font = [UIFont preferredFontForTextStyle: UIFontTextStyleLargeTitle];
|
||||
UIFontMetrics* metrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleLargeTitle];
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
UIFontDescriptor* round = [[font fontDescriptor] fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
|
||||
UIFontDescriptor* weighted = [round fontDescriptorByAddingAttributes:@{
|
||||
UIFontDescriptorTraitsAttribute: @{
|
||||
UIFontWeightTrait: @1.5
|
||||
}
|
||||
}];
|
||||
font = [UIFont fontWithDescriptor:weighted size:44];
|
||||
}
|
||||
|
||||
UIFont* scaled = [metrics scaledFontForFont:font];
|
||||
return scaled;
|
||||
}
|
||||
|
||||
- (void)updateTimerLabel {
|
||||
@@ -334,13 +114,44 @@
|
||||
dispatch_once(&onceToken, ^{
|
||||
formatter = [NSDateComponentsFormatter new];
|
||||
formatter.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
|
||||
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
|
||||
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorDropLeading;
|
||||
formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
|
||||
});
|
||||
|
||||
NSString *labelString = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
_timerLabel.text = labelString;
|
||||
_timerLabel.hidden = (labelString == nil);
|
||||
_timerLabel.text = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
|
||||
// The ring should be be centered and fill 1/2 of the view's width
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGFloat strokeWidth = 12;
|
||||
CGFloat xCenter = self.bounds.size.width / 2;
|
||||
CGFloat yCenter = self.bounds.size.height / 2;
|
||||
CGFloat dimension = MIN(self.bounds.size.width, self.bounds.size.height);
|
||||
CGFloat radius = 0.5 * (dimension * 0.5);
|
||||
CGFloat percentFilled = _timeLeft / _duration;
|
||||
CGFloat startAngle = -M_PI_2 - (percentFilled * 2 * M_PI);
|
||||
CGFloat stopAngle = -M_PI_2;
|
||||
bool clockwise = NO;
|
||||
|
||||
CGContextSetLineWidth(context, strokeWidth);
|
||||
CGContextSetLineCap(context, kCGLineCapRound);
|
||||
|
||||
// Draw a circular track
|
||||
if (@available(iOS 13.0, *)) {
|
||||
[[UIColor systemGray5Color] setStroke];
|
||||
} else {
|
||||
[[UIColor lightGrayColor] setStroke];
|
||||
}
|
||||
|
||||
CGContextAddArc(context, xCenter, yCenter, radius, 0, 2 * M_PI, clockwise ? 1 : 0);
|
||||
CGContextStrokePath(context);
|
||||
|
||||
// Fill in the track based on progress
|
||||
[self.tintColor setStroke];
|
||||
CGContextAddArc(context, xCenter, yCenter, radius, startAngle, stopAngle, clockwise ? 1 : 0);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -36,17 +36,11 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Fitness step.
|
||||
|
||||
Displays usual header, a counting-up timer, read outs for distance and/or
|
||||
heart rate if corresponding recorders are attached.
|
||||
|
||||
Also displays an image during the task.
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKFitnessStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, copy) NSDictionary *userInfo;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
|
||||
@@ -43,6 +44,7 @@
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.userInfo = [[NSDictionary alloc] init];
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
}
|
||||
return self;
|
||||
@@ -58,13 +60,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKFitnessStep *step = [super copyWithZone:zone];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)startsFinished {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(coder, userInfo, NSDictionary);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
[super encodeWithCoder:coder];
|
||||
ORK_ENCODE_OBJ(coder, userInfo);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKFitnessStep *step = [super copyWithZone:zone];
|
||||
step.userInfo = [self.userInfo copy];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
if ([self class] != [other class]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__typeof(self) castObject = other;
|
||||
return ORKEqualObjects(self.userInfo, castObject.userInfo);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [super hash] ^ self.userInfo.hash;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,27 +30,20 @@
|
||||
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKFitnessContentView.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKActiveStepTimer.h"
|
||||
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKHealthQuantityTypeRecorder.h"
|
||||
#import "ORKPedometerRecorder.h"
|
||||
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
|
||||
@interface ORKFitnessStepViewController () <ORKHealthQuantityTypeRecorderDelegate, ORKPedometerRecorderDelegate> {
|
||||
NSInteger _intendedSteps;
|
||||
@interface ORKFitnessStepViewController () {
|
||||
ORKFitnessContentView *_contentView;
|
||||
NSNumberFormatter *_hrFormatter;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -70,84 +63,61 @@
|
||||
return (ORKFitnessStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
_hrFormatter = [[NSNumberFormatter alloc] init];
|
||||
_hrFormatter.numberStyle = kCFNumberFormatterNoStyle;
|
||||
_contentView.timeLeft = self.fitnessStep.stepDuration;
|
||||
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
_contentView = [ORKFitnessContentView new];
|
||||
_contentView.image = self.fitnessStep.image;
|
||||
_contentView.timeLeft = self.fitnessStep.stepDuration;
|
||||
|
||||
_contentView = [[ORKFitnessContentView alloc] initWithDuration:self.fitnessStep.stepDuration];
|
||||
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
self.activeStepView.activeCustomView = _contentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
self.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
self.continueButtonTitle = ORKLocalizedString(@"BUTTON_SKIP_STEP", nil);
|
||||
}
|
||||
|
||||
- (void)updateHeartRateWithQuantity:(HKQuantitySample *)quantity unit:(HKUnit *)unit {
|
||||
if (quantity != nil) {
|
||||
_contentView.hasHeartRate = YES;
|
||||
}
|
||||
if (quantity) {
|
||||
_contentView.heartRate = [_hrFormatter stringFromNumber:@([quantity.quantity doubleValueForUnit:unit])];
|
||||
} else {
|
||||
_contentView.heartRate = @"--";
|
||||
}
|
||||
- (void)finish {
|
||||
[super finish];
|
||||
_contentView.labelHidden = YES;
|
||||
self.continueButtonTitle = ORKLocalizedString(@"BUTTON_NEXT", nil);
|
||||
}
|
||||
|
||||
- (void)updateDistance:(double)distanceInMeters {
|
||||
_contentView.hasDistance = YES;
|
||||
_contentView.distanceInMeters = distanceInMeters;
|
||||
|
||||
}
|
||||
|
||||
- (void)recordersDidChange {
|
||||
[super recordersDidChange];
|
||||
|
||||
ORKPedometerRecorder *pedometerRecorder = nil;
|
||||
ORKHealthQuantityTypeRecorder *heartRateRecorder = nil;
|
||||
for (ORKRecorder *recorder in self.recorders) {
|
||||
if ([recorder isKindOfClass:[ORKPedometerRecorder class]]) {
|
||||
pedometerRecorder = (ORKPedometerRecorder *)recorder;
|
||||
} else if ([recorder isKindOfClass:[ORKHealthQuantityTypeRecorder class]]) {
|
||||
ORKHealthQuantityTypeRecorder *rec1 = (ORKHealthQuantityTypeRecorder *)recorder;
|
||||
if ([[[rec1 quantityType] identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
|
||||
heartRateRecorder = (ORKHealthQuantityTypeRecorder *)recorder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (heartRateRecorder == nil) {
|
||||
_contentView.hasHeartRate = NO;
|
||||
}
|
||||
_contentView.heartRate = @"--";
|
||||
_contentView.hasDistance = (pedometerRecorder != nil);
|
||||
_contentView.distanceInMeters = 0;
|
||||
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
_contentView.duration = self.fitnessStep.stepDuration;
|
||||
_contentView.timeLeft = self.fitnessStep.stepDuration;
|
||||
}
|
||||
|
||||
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
|
||||
_contentView.timeLeft = finished ? 0 : (timer.duration - timer.runtime);
|
||||
_contentView.duration = self.fitnessStep.stepDuration;
|
||||
[super countDownTimerFired:timer finished:finished];
|
||||
}
|
||||
|
||||
#pragma mark - ORKHealthQuantityTypeRecorderDelegate
|
||||
- (void)goForward {
|
||||
|
||||
- (void)healthQuantityTypeRecorderDidUpdate:(ORKHealthQuantityTypeRecorder *)healthQuantityTypeRecorder {
|
||||
if ([[healthQuantityTypeRecorder.quantityType identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
|
||||
[self updateHeartRateWithQuantity:healthQuantityTypeRecorder.lastSample unit:healthQuantityTypeRecorder.unit];
|
||||
if (self.finished) {
|
||||
[super goForward];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - ORKPedometerRecorderDelegate
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:ORKLocalizedString(@"FITNESS_STOP_TEST_CONFIRMATION", nil)
|
||||
message:ORKLocalizedString(@"FITNESS_STOP_TEST_DETAIL", nil)
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
- (void)pedometerRecorderDidUpdate:(ORKPedometerRecorder *)pedometerRecorder {
|
||||
double distanceInMeters = pedometerRecorder.totalDistance;
|
||||
[self updateDistance:distanceInMeters];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"FITNESS_RESUME_TEST", nil)
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
[alert dismissViewControllerAnimated:YES completion:nil];
|
||||
}]];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_SKIP_STEP", nil)
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
[alert dismissViewControllerAnimated:YES completion:^{
|
||||
[super goForward];
|
||||
}];
|
||||
}]];
|
||||
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKFrontFacingCameraStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval maximumRecordingLimit;
|
||||
@property (nonatomic, assign) BOOL allowsReview;
|
||||
@property (nonatomic, assign) BOOL allowsRetry;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKFrontFacingCameraStep.h"
|
||||
#import "ORKFrontFacingCameraStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
static const NSTimeInterval MIN_RECORDING_DURATION = 10.0;
|
||||
static const NSTimeInterval MAX_RECORDING_DURATION = 300.0;
|
||||
|
||||
@implementation ORKFrontFacingCameraStep
|
||||
|
||||
+ (Class)stepViewControllerClass
|
||||
{
|
||||
return [ORKFrontFacingCameraStepViewController class];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
{
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self)
|
||||
{
|
||||
_maximumRecordingLimit = 60.0;
|
||||
_allowsRetry = NO;
|
||||
_allowsReview = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)validateParameters
|
||||
{
|
||||
[super validateParameters];
|
||||
|
||||
|
||||
if (self.maximumRecordingLimit < MIN_RECORDING_DURATION ||
|
||||
self.maximumRecordingLimit > MAX_RECORDING_DURATION)
|
||||
{
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:[NSString stringWithFormat:@"maxRecordingDuration must be greater than %f seconds and less than %f seconds.",
|
||||
MIN_RECORDING_DURATION,
|
||||
MAX_RECORDING_DURATION]
|
||||
userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)startsFinished
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)allowsBackNavigation
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
ORKFrontFacingCameraStep *step = [super copyWithZone:zone];
|
||||
step.maximumRecordingLimit = self.maximumRecordingLimit;
|
||||
step.allowsRetry = self.allowsRetry;
|
||||
step.allowsReview = self.allowsReview;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self )
|
||||
{
|
||||
ORK_DECODE_DOUBLE(aDecoder, maximumRecordingLimit);
|
||||
ORK_DECODE_BOOL(aDecoder, allowsRetry);
|
||||
ORK_DECODE_BOOL(aDecoder, allowsReview);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder
|
||||
{
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_DOUBLE(aCoder, maximumRecordingLimit);
|
||||
ORK_ENCODE_BOOL(aCoder, allowsRetry);
|
||||
ORK_ENCODE_BOOL(aCoder, allowsReview);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
__typeof(self) castObject = object;
|
||||
|
||||
return (isParentSame &&
|
||||
(self.maximumRecordingLimit == castObject.maximumRecordingLimit) &&
|
||||
(self.allowsRetry == castObject.allowsRetry) &&
|
||||
(self.allowsReview == castObject.allowsReview));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import UIKit;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class AVCaptureSession;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, ORKFrontFacingCameraStepContentViewEvent) {
|
||||
ORKFrontFacingCameraStepContentViewEventStartRecording = 0,
|
||||
ORKFrontFacingCameraStepContentViewEventStopRecording,
|
||||
ORKFrontFacingCameraStepContentViewEventReviewRecording,
|
||||
ORKFrontFacingCameraStepContentViewEventRetryRecording,
|
||||
ORKFrontFacingCameraStepContentViewEventSubmitRecording,
|
||||
ORKFrontFacingCameraStepContentViewEventError
|
||||
};
|
||||
|
||||
typedef void (^ORKFrontFacingCameraStepContentViewEventHandler)(ORKFrontFacingCameraStepContentViewEvent);
|
||||
|
||||
@interface ORKFrontFacingCameraStepContentView : UIView
|
||||
|
||||
- (instancetype)initWithTitle:(nullable NSString *)title text:(nullable NSString *)text;
|
||||
|
||||
- (void)setViewEventHandler:(ORKFrontFacingCameraStepContentViewEventHandler)handler;
|
||||
|
||||
- (void)setPreviewLayerWithSession:(AVCaptureSession *)session;
|
||||
|
||||
- (void)startTimerWithMaximumRecordingLimit:(NSTimeInterval)maximumRecordingLimit;
|
||||
|
||||
- (void)presentReviewOptionsAllowingReview:(BOOL)allowReview allowRetry:(BOOL)allowRetry;
|
||||
|
||||
- (void)handleError:(NSError *)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKFrontFacingCameraStepContentView.h"
|
||||
#import "ORKUnitLabel.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
#import "ORKTitleLabel.h"
|
||||
#import "ORKBodyLabel.h"
|
||||
#import "ORKIconButton.h"
|
||||
#import "ORKStepHeaderView_Internal.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface ORKFrontFacingCameraStepOptionsView : UIVisualEffectView
|
||||
|
||||
@property (nonatomic, strong) ORKIconButton *reviewVideoButton;
|
||||
@property (nonatomic, strong) ORKIconButton *deleteAndRetryVideoButton;
|
||||
@property (nonatomic, strong) UIButton *submitVideoButton;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKFrontFacingCameraStepOptionsView {
|
||||
NSMutableArray *_constraints;
|
||||
ORKTitleLabel *_titleLabel;
|
||||
}
|
||||
|
||||
- (instancetype)initWithEffect:(UIVisualEffect *)effect {
|
||||
self = [super initWithEffect:effect];
|
||||
|
||||
if (self) {
|
||||
[self setupSubviews];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
self.layer.cornerRadius = 10.0;
|
||||
self.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
|
||||
self.clipsToBounds = YES;
|
||||
}
|
||||
|
||||
- (void)setupSubviews {
|
||||
_titleLabel = [ORKTitleLabel new];
|
||||
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_titleLabel.textAlignment = NSTextAlignmentLeft;
|
||||
_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[_titleLabel setTextColor:[UIColor whiteColor]];
|
||||
_titleLabel.text = ORKLocalizedString(@"FRONT_FACING_CAMERA_REVIEW_OPTIONS_TITLE", nil);
|
||||
[self.contentView addSubview:_titleLabel];
|
||||
|
||||
UIImage *reviewButtonIcon = nil;
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
reviewButtonIcon = [UIImage systemImageNamed:@"video.fill"];
|
||||
}
|
||||
|
||||
_reviewVideoButton = [[ORKIconButton alloc] initWithButtonText:ORKLocalizedString(@"FRONT_FACING_CAMERA_REVIEW_VIDEO", nil) buttonIcon: reviewButtonIcon];
|
||||
_reviewVideoButton.tag = 0;
|
||||
_reviewVideoButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self.contentView addSubview:_reviewVideoButton];
|
||||
|
||||
UIImage *deleteAndRetryButtonIcon = nil;
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
deleteAndRetryButtonIcon = [UIImage systemImageNamed:@"trash.fill"];
|
||||
}
|
||||
|
||||
_deleteAndRetryVideoButton = [[ORKIconButton alloc] initWithButtonText:ORKLocalizedString(@"FRONT_FACING_CAMERA_RETRY_VIDEO", nil) buttonIcon: deleteAndRetryButtonIcon];
|
||||
_deleteAndRetryVideoButton.tag = 1;
|
||||
_deleteAndRetryVideoButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_deleteAndRetryVideoButton updateTextAndImageColor:[UIColor redColor]];
|
||||
[self.contentView addSubview:_deleteAndRetryVideoButton];
|
||||
|
||||
_submitVideoButton = [UIButton new];
|
||||
_submitVideoButton.tag = 2;
|
||||
_submitVideoButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_submitVideoButton.layer.cornerRadius = 10.0;
|
||||
_submitVideoButton.clipsToBounds = YES;
|
||||
_submitVideoButton.titleLabel.font = [UIFont systemFontOfSize:20.0];
|
||||
[_submitVideoButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
|
||||
[_submitVideoButton setBackgroundColor:[UIColor systemBlueColor]];
|
||||
[_submitVideoButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_SUBMIT_VIDEO", nil) forState:UIControlStateNormal];
|
||||
[self.contentView addSubview:_submitVideoButton];
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
if (_constraints) {
|
||||
[NSLayoutConstraint deactivateConstraints:_constraints];
|
||||
}
|
||||
|
||||
_constraints = [NSMutableArray array];
|
||||
|
||||
[_constraints addObject: [_titleLabel.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:25.0]];
|
||||
[_constraints addObject: [_titleLabel.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:20.0]];
|
||||
[_constraints addObject: [_titleLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-20.0]];
|
||||
|
||||
//reviewVideoButton constraints
|
||||
[_constraints addObject:[_reviewVideoButton.topAnchor constraintEqualToAnchor:_titleLabel.bottomAnchor constant:40.0]];
|
||||
[_constraints addObject:[_reviewVideoButton.leadingAnchor constraintEqualToAnchor:_titleLabel.leadingAnchor]];
|
||||
[_constraints addObject:[_reviewVideoButton.trailingAnchor constraintEqualToAnchor:_titleLabel.trailingAnchor]];
|
||||
[_constraints addObject:[_reviewVideoButton.heightAnchor constraintEqualToConstant:50.0]];
|
||||
|
||||
//deleteAndRetryButton constraints
|
||||
[_constraints addObject:[_deleteAndRetryVideoButton.topAnchor constraintEqualToAnchor:_reviewVideoButton.bottomAnchor constant:15.0]];
|
||||
[_constraints addObject:[_deleteAndRetryVideoButton.leadingAnchor constraintEqualToAnchor:_titleLabel.leadingAnchor]];
|
||||
[_constraints addObject:[_deleteAndRetryVideoButton.trailingAnchor constraintEqualToAnchor:_titleLabel.trailingAnchor]];
|
||||
[_constraints addObject:[_deleteAndRetryVideoButton.heightAnchor constraintEqualToConstant:50.0]];
|
||||
|
||||
//submitVideoButton constraints
|
||||
[_constraints addObject:[_submitVideoButton.leadingAnchor constraintEqualToAnchor:_titleLabel.leadingAnchor]];
|
||||
[_constraints addObject:[_submitVideoButton.trailingAnchor constraintEqualToAnchor:_titleLabel.trailingAnchor]];
|
||||
[_constraints addObject:[_submitVideoButton.bottomAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.bottomAnchor constant:-20.0]];
|
||||
[_constraints addObject:[_submitVideoButton.heightAnchor constraintEqualToConstant:50.0]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:_constraints];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
|
||||
ORKStartStopButtonStateStartRecording = 0,
|
||||
ORKStartStopButtonStateStopRecording,
|
||||
} ORK_ENUM_AVAILABLE;
|
||||
|
||||
|
||||
@interface ORKBlurFooterView : UIVisualEffectView
|
||||
- (instancetype)initWithTitleText:(nullable NSString *)titleText detailText:(nullable NSString *)detailText;
|
||||
|
||||
@property (nonatomic) UIButton *startStopButton;
|
||||
@property (nonatomic) ORKStartStopButtonState startStopButtonState;
|
||||
@property (nonatomic) UILabel *timerLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKBlurFooterView {
|
||||
NSMutableArray<NSLayoutConstraint *> *_heightConstraints;
|
||||
NSLayoutConstraint *_blurViewTopConstraint;
|
||||
|
||||
NSString *_titleText;
|
||||
NSString *_detailText;
|
||||
|
||||
ORKTitleLabel *_titleLabel;
|
||||
ORKBodyLabel *_detailTextLabel;
|
||||
|
||||
UIButton *_collapseButton;
|
||||
|
||||
BOOL _isTextCollapsed;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTitleText:(nullable NSString *)titleText detailText:(nullable NSString *)detailText {
|
||||
self = [super initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];
|
||||
if (self) {
|
||||
_titleText = titleText;
|
||||
_detailText = detailText;
|
||||
_isTextCollapsed = NO;
|
||||
_startStopButtonState = ORKStartStopButtonStateStartRecording;
|
||||
[self setupSubviews];
|
||||
[self setupConstraints];
|
||||
[self setStartStopButtonState:ORKStartStopButtonStateStartRecording];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupSubviews {
|
||||
_startStopButton = [UIButton new];
|
||||
|
||||
if (@available(iOS 15.0, *)) {
|
||||
UIButtonConfiguration *buttonConfiguration = [UIButtonConfiguration plainButtonConfiguration];
|
||||
[buttonConfiguration setContentInsets:NSDirectionalEdgeInsetsMake(0, 6, 0, 6)];
|
||||
[_startStopButton setConfiguration:buttonConfiguration];
|
||||
} else {
|
||||
_startStopButton.contentEdgeInsets = (UIEdgeInsets){.left = 6, .right = 6};
|
||||
}
|
||||
|
||||
_startStopButton.layer.cornerRadius = 14.0;
|
||||
_startStopButton.clipsToBounds = YES;
|
||||
|
||||
UIFontDescriptor *descriptorOne = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
|
||||
_startStopButton.titleLabel.font = [UIFont boldSystemFontOfSize:[[descriptorOne objectForKey: UIFontDescriptorSizeAttribute] doubleValue] + 1.0];
|
||||
[self.contentView addSubview:_startStopButton];
|
||||
|
||||
_timerLabel = [UILabel new];
|
||||
_timerLabel.font = [UIFont systemFontOfSize:15.0];
|
||||
_timerLabel.adjustsFontSizeToFitWidth = YES;
|
||||
[self.contentView addSubview:_timerLabel];
|
||||
|
||||
UIImage *collapseButtonImage;
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
collapseButtonImage = [UIImage systemImageNamed:@"chevron.down"];
|
||||
}
|
||||
|
||||
if (_titleText) {
|
||||
_titleLabel = [ORKTitleLabel new];
|
||||
_titleLabel.textAlignment = NSTextAlignmentLeft;
|
||||
_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
_titleLabel.numberOfLines = 0;
|
||||
[_titleLabel setTextColor:[UIColor whiteColor]];
|
||||
_titleLabel.text = _titleText;
|
||||
[self.contentView addSubview:_titleLabel];
|
||||
}
|
||||
|
||||
if (_detailText) {
|
||||
_detailTextLabel = [ORKBodyLabel new];
|
||||
_detailTextLabel.textAlignment = NSTextAlignmentLeft;
|
||||
_detailTextLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
_detailTextLabel.numberOfLines = 0;
|
||||
[_detailTextLabel setTextColor:[UIColor whiteColor]];
|
||||
_detailTextLabel.text = _detailText ? : @"";
|
||||
[self.contentView addSubview:_detailTextLabel];
|
||||
}
|
||||
|
||||
if (_titleText || _detailText) {
|
||||
_collapseButton = [UIButton new];
|
||||
_collapseButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_collapseButton setTintColor:[UIColor whiteColor]];
|
||||
[_collapseButton setBackgroundImage:collapseButtonImage forState:UIControlStateNormal];
|
||||
[_collapseButton addTarget:self
|
||||
action:@selector(collapseButtonPressed)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.contentView addSubview:_collapseButton];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
_startStopButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_detailTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[[_startStopButton.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:20.0] setActive:YES];
|
||||
[[_startStopButton.trailingAnchor constraintEqualToAnchor:_timerLabel.leadingAnchor constant:-15.0] setActive:YES];
|
||||
[[_startStopButton.bottomAnchor constraintEqualToAnchor:self.contentView.safeAreaLayoutGuide.bottomAnchor constant:-20.0] setActive:YES];
|
||||
[[_startStopButton.heightAnchor constraintEqualToConstant:50.0] setActive:YES];
|
||||
|
||||
[[_timerLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-20.0] setActive:YES];
|
||||
[[_timerLabel.centerYAnchor constraintEqualToAnchor:_startStopButton.centerYAnchor] setActive:YES];
|
||||
[[_timerLabel.widthAnchor constraintEqualToConstant:40.0] setActive:YES];
|
||||
|
||||
if (_titleLabel || _detailTextLabel) {
|
||||
|
||||
if (_detailTextLabel) {
|
||||
[[_detailTextLabel.leadingAnchor constraintEqualToAnchor:_startStopButton.leadingAnchor] setActive:YES];
|
||||
[[_detailTextLabel.trailingAnchor constraintEqualToAnchor:_timerLabel.trailingAnchor] setActive:YES];
|
||||
[[_detailTextLabel.bottomAnchor constraintEqualToAnchor:_startStopButton.topAnchor constant:-20.0] setActive:YES];
|
||||
}
|
||||
|
||||
if (_titleLabel) {
|
||||
[[_titleLabel.leadingAnchor constraintEqualToAnchor:_startStopButton.leadingAnchor] setActive:YES];
|
||||
[[_titleLabel.trailingAnchor constraintEqualToAnchor:_collapseButton.leadingAnchor constant: -10.0] setActive:YES];
|
||||
[[_titleLabel.bottomAnchor constraintEqualToAnchor:_detailTextLabel ? _detailTextLabel.topAnchor : _startStopButton.topAnchor constant: -15.0] setActive:YES];
|
||||
|
||||
[[_collapseButton.topAnchor constraintEqualToAnchor:_titleLabel.topAnchor] setActive:YES];
|
||||
|
||||
_blurViewTopConstraint = [self.contentView.topAnchor constraintEqualToAnchor:_titleLabel.topAnchor constant:-20.0];
|
||||
} else {
|
||||
[[_collapseButton.bottomAnchor constraintEqualToAnchor:_detailTextLabel.topAnchor constant:-15.0] setActive:YES];
|
||||
_blurViewTopConstraint = [self.contentView.topAnchor constraintEqualToAnchor:_collapseButton.topAnchor constant:-20.0];
|
||||
}
|
||||
|
||||
[[_collapseButton.trailingAnchor constraintEqualToAnchor:_timerLabel.trailingAnchor] setActive:YES];
|
||||
[[_collapseButton.heightAnchor constraintEqualToConstant:25.0] setActive:YES];
|
||||
[[_collapseButton.widthAnchor constraintEqualToConstant:25.0] setActive:YES];
|
||||
|
||||
[_blurViewTopConstraint setActive:YES];
|
||||
} else {
|
||||
[[self.contentView.topAnchor constraintEqualToAnchor:_startStopButton.topAnchor constant:-20.0] setActive:YES];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)setStartStopButtonState:(ORKStartStopButtonState)startStopButtonState
|
||||
{
|
||||
_startStopButtonState = startStopButtonState;
|
||||
|
||||
if (startStopButtonState == ORKStartStopButtonStateStartRecording)
|
||||
{
|
||||
[_startStopButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_START_TITLE", nil) forState:UIControlStateNormal];
|
||||
[_startStopButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
[_startStopButton setBackgroundColor:[UIColor systemBlueColor]];
|
||||
|
||||
[_timerLabel setText:ORKLocalizedString(@"FRONT_FACING_CAMERA_START_TIME", nil)];
|
||||
[_timerLabel setTextColor:[UIColor darkGrayColor]];
|
||||
}
|
||||
else
|
||||
{
|
||||
[_startStopButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_STOP_TITLE", nil) forState:UIControlStateNormal];
|
||||
[_startStopButton setTitleColor:[UIColor systemBlueColor] forState:UIControlStateNormal];
|
||||
[_startStopButton setBackgroundColor:[UIColor systemGrayColor]];
|
||||
|
||||
[_timerLabel setTextColor:[UIColor whiteColor]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)collapseButtonPressed {
|
||||
UIImage *collapseButtonImage;
|
||||
|
||||
if (_isTextCollapsed) {
|
||||
[_blurViewTopConstraint setActive:NO];
|
||||
_blurViewTopConstraint = [self.contentView.topAnchor constraintEqualToAnchor:_titleLabel.topAnchor constant:-20.0];
|
||||
[_blurViewTopConstraint setActive:YES];
|
||||
|
||||
[NSLayoutConstraint deactivateConstraints:_heightConstraints];
|
||||
_heightConstraints = nil;
|
||||
} else {
|
||||
[_blurViewTopConstraint setActive:NO];
|
||||
_blurViewTopConstraint = [self.contentView.topAnchor constraintEqualToAnchor:_collapseButton.topAnchor constant:-20.0];
|
||||
[_blurViewTopConstraint setActive:YES];
|
||||
|
||||
_heightConstraints = [NSMutableArray new];
|
||||
[_heightConstraints addObject:[_titleLabel.heightAnchor constraintEqualToConstant:0.0]];
|
||||
[_heightConstraints addObject:[_detailTextLabel.heightAnchor constraintEqualToConstant:0.0]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:_heightConstraints];
|
||||
}
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
collapseButtonImage = _isTextCollapsed ? [UIImage systemImageNamed:@"chevron.down"] : [UIImage systemImageNamed:@"chevron.up"];
|
||||
}
|
||||
|
||||
[_collapseButton setBackgroundImage:collapseButtonImage forState:UIControlStateNormal];
|
||||
_isTextCollapsed = !_isTextCollapsed;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKFrontFacingCameraStepContentView ()
|
||||
@property (nonatomic, copy, nullable) ORKFrontFacingCameraStepContentViewEventHandler viewEventhandler;
|
||||
@end
|
||||
|
||||
@implementation ORKFrontFacingCameraStepContentView {
|
||||
ORKStepHeaderView *_headerView;
|
||||
UIView *_cameraView;
|
||||
AVCaptureVideoPreviewLayer *_previewLayer;
|
||||
ORKBlurFooterView *_blurFooterView;
|
||||
|
||||
NSTimer *_timer;
|
||||
NSTimeInterval _maxRecordingTime;
|
||||
CGFloat _recordingTime;
|
||||
NSDateComponentsFormatter *_dateComponentsFormatter;
|
||||
|
||||
ORKFrontFacingCameraStepOptionsView *_optionsView;
|
||||
|
||||
NSString *_titleText;
|
||||
NSString *_bodyText;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTitle:(nullable NSString *)title text:(NSString *)text {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
|
||||
|
||||
if (self) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_titleText = title;
|
||||
_bodyText = text;
|
||||
|
||||
[self setUpSubviews];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setUpSubviews {
|
||||
_cameraView = [UIView new];
|
||||
_cameraView.alpha = 1.0;
|
||||
[self addSubview:_cameraView];
|
||||
|
||||
_blurFooterView = [[ORKBlurFooterView alloc] initWithTitleText:_titleText detailText:_bodyText];
|
||||
_blurFooterView.layer.cornerRadius = 10.0;
|
||||
_blurFooterView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
|
||||
_blurFooterView.clipsToBounds = YES;
|
||||
|
||||
[_blurFooterView.startStopButton addTarget:self
|
||||
action:@selector(startStopButtonPressed)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
[self addSubview:_blurFooterView];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
|
||||
if (_previewLayer && _previewLayer.frame.size.height == 0 && _cameraView.frame.size.height != 0) {
|
||||
_previewLayer.position = CGPointMake(_cameraView.frame.size.width / 2, _cameraView.frame.size.height / 2);
|
||||
_previewLayer.bounds = CGRectMake(0, 0, _cameraView.frame.size.width, _cameraView.frame.size.height);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
_cameraView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_blurFooterView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[[_cameraView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
|
||||
[[_cameraView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
|
||||
[[_cameraView.topAnchor constraintEqualToAnchor:self.topAnchor] setActive:YES];
|
||||
[[_cameraView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
|
||||
|
||||
[[_blurFooterView.leadingAnchor constraintEqualToAnchor:_cameraView.leadingAnchor] setActive:YES];
|
||||
[[_blurFooterView.trailingAnchor constraintEqualToAnchor:_cameraView.trailingAnchor] setActive:YES];
|
||||
[[_blurFooterView.bottomAnchor constraintEqualToAnchor:_cameraView.bottomAnchor] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)setViewEventHandler:(ORKFrontFacingCameraStepContentViewEventHandler)handler
|
||||
{
|
||||
self.viewEventhandler = [handler copy];
|
||||
}
|
||||
|
||||
- (void)invokeViewEventHandlerWithEvent:(ORKFrontFacingCameraStepContentViewEvent)event
|
||||
{
|
||||
if (self.viewEventhandler)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
self.viewEventhandler(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setPreviewLayerWithSession:(AVCaptureSession *)session {
|
||||
_previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
|
||||
_previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
|
||||
_previewLayer.needsDisplayOnBoundsChange = YES;
|
||||
_previewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortrait;
|
||||
|
||||
[_cameraView.layer addSublayer:_previewLayer];
|
||||
}
|
||||
|
||||
- (void)handleError:(NSError *)error
|
||||
{
|
||||
[_optionsView removeFromSuperview];
|
||||
[_cameraView removeFromSuperview];
|
||||
[_blurFooterView removeFromSuperview];
|
||||
[_previewLayer removeFromSuperlayer];
|
||||
|
||||
_optionsView = nil;
|
||||
_cameraView = nil;
|
||||
_blurFooterView = nil;
|
||||
_previewLayer = nil;
|
||||
|
||||
if (_headerView)
|
||||
{
|
||||
[_headerView removeFromSuperview];
|
||||
_headerView = nil;
|
||||
}
|
||||
|
||||
_headerView = [[ORKStepHeaderView alloc] init];
|
||||
_headerView.instructionLabel.text = error.localizedDescription;
|
||||
[_headerView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
[self addSubview:_headerView];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_headerView.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor],
|
||||
[_headerView.leftAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leftAnchor],
|
||||
[_headerView.rightAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.rightAnchor],
|
||||
]];
|
||||
|
||||
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventError];
|
||||
}
|
||||
|
||||
- (void)startStopButtonPressed
|
||||
{
|
||||
if (_blurFooterView.startStopButtonState == ORKStartStopButtonStateStartRecording)
|
||||
{
|
||||
[_blurFooterView setStartStopButtonState:ORKStartStopButtonStateStopRecording];
|
||||
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventStartRecording];
|
||||
}
|
||||
else
|
||||
{
|
||||
[_blurFooterView setStartStopButtonState:ORKStartStopButtonStateStartRecording];
|
||||
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventStopRecording];
|
||||
[_timer invalidate];
|
||||
_timer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startTimerWithMaximumRecordingLimit:(NSTimeInterval)maximumRecordingLimit
|
||||
{
|
||||
if (_timer) {
|
||||
[_timer invalidate];
|
||||
}
|
||||
|
||||
_maxRecordingTime = maximumRecordingLimit;
|
||||
_recordingTime = 0.0;
|
||||
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0
|
||||
target:self
|
||||
selector:@selector(updateRecordingTime)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
|
||||
- (void)updateRecordingTime {
|
||||
_recordingTime += _timer.timeInterval;
|
||||
|
||||
if (_recordingTime >= _maxRecordingTime) {
|
||||
[_timer invalidate];
|
||||
[_blurFooterView setStartStopButtonState:ORKStartStopButtonStateStartRecording];
|
||||
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventStopRecording];
|
||||
} else {
|
||||
_blurFooterView.timerLabel.text = [self formattedTimeFromSeconds:_recordingTime];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)formattedTimeFromSeconds:(CGFloat)seconds {
|
||||
if (!_dateComponentsFormatter) {
|
||||
_dateComponentsFormatter = [NSDateComponentsFormatter new];
|
||||
_dateComponentsFormatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
|
||||
_dateComponentsFormatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitNanosecond;
|
||||
}
|
||||
return [_dateComponentsFormatter stringFromTimeInterval:seconds];
|
||||
}
|
||||
|
||||
- (void)presentReviewOptionsAllowingReview:(BOOL)allowReview allowRetry:(BOOL)allowRetry
|
||||
{
|
||||
if (allowRetry || allowReview)
|
||||
{
|
||||
[self presentOptionsView];
|
||||
[_optionsView.reviewVideoButton setHidden:!allowReview];
|
||||
[_optionsView.deleteAndRetryVideoButton setHidden:!allowRetry];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)presentOptionsView
|
||||
{
|
||||
if (_optionsView)
|
||||
{
|
||||
[_optionsView removeFromSuperview];
|
||||
_optionsView = nil;
|
||||
}
|
||||
|
||||
_optionsView = [[ORKFrontFacingCameraStepOptionsView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];
|
||||
_optionsView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[_optionsView.reviewVideoButton addTarget:self
|
||||
action:@selector(optionsViewButtonPressed:)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
[_optionsView.deleteAndRetryVideoButton addTarget:self
|
||||
action:@selector(optionsViewButtonPressed:)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
[_optionsView.submitVideoButton addTarget:self
|
||||
action:@selector(optionsViewButtonPressed:)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
[self addSubview:_optionsView];
|
||||
[self setupOptionsViewConstraints];
|
||||
}
|
||||
|
||||
- (void)setupOptionsViewConstraints {
|
||||
[[_optionsView.topAnchor constraintEqualToAnchor:self.topAnchor] setActive:YES];
|
||||
[[_optionsView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
|
||||
[[_optionsView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
|
||||
[[_optionsView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)optionsViewButtonPressed:(UIButton *)button {
|
||||
if (button) {
|
||||
if (button.tag == 0) {
|
||||
//review video
|
||||
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventReviewRecording];
|
||||
} else if (button.tag == 1) {
|
||||
//delete and redo recording
|
||||
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventRetryRecording];
|
||||
[_optionsView removeFromSuperview];
|
||||
_optionsView = nil;
|
||||
} else if (button.tag == 2) {
|
||||
//submit video
|
||||
[self invokeViewEventHandlerWithEvent:ORKFrontFacingCameraStepContentViewEventSubmitRecording];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKFileResult.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKFrontFacingCameraStepResult : ORKFileResult
|
||||
|
||||
@property (nonatomic, assign) NSInteger retryCount;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+35
-32
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -28,37 +28,40 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKFrontFacingCameraStepResult.h"
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
import Foundation
|
||||
@implementation ORKFrontFacingCameraStepResult
|
||||
|
||||
@available(watchOSApplicationExtension 5.0, *)
|
||||
|
||||
class AssessmentManager {
|
||||
private var manager: CMMovementDisorderManager?
|
||||
init() {
|
||||
if CMMovementDisorderManager.isAvailable() {
|
||||
manager = CMMovementDisorderManager()
|
||||
|
||||
monitorForParkinsons()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func monitorForParkinsons() {
|
||||
manager?.monitorKinesias(forDuration: 7 * 24 * 3600)
|
||||
}
|
||||
|
||||
func queryNewAssessments() {
|
||||
let calendar = Calendar.current
|
||||
let toDate = Date()
|
||||
let fromDate:Date = calendar.date(byAdding: .day, value: -7, to: toDate)!
|
||||
|
||||
manager?.queryTremor(from: fromDate, to: toDate, withHandler: { (results, error) in
|
||||
|
||||
})
|
||||
|
||||
manager?.queryDyskineticSymptom(from: fromDate, to: toDate, withHandler: { (results, error) in
|
||||
|
||||
})
|
||||
}
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_INTEGER(aCoder, retryCount);
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_INTEGER(aDecoder, retryCount);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return isParentSame && (castObject.retryCount == self.retryCount);
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKFrontFacingCameraStepResult *result = [super copyWithZone:zone];
|
||||
result.retryCount = self.retryCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
|
||||
@interface ORKFrontFacingCameraStepViewController : ORKActiveStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <AVKit/AVKit.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <MediaPlayer/MediaPlayer.h>
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKFrontFacingCameraStep.h"
|
||||
#import "ORKFrontFacingCameraStepContentView.h"
|
||||
#import "ORKFrontFacingCameraStepResult.h"
|
||||
#import "ORKFrontFacingCameraStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
@interface ORKFrontFacingCameraStepViewController () <AVCaptureFileOutputRecordingDelegate, AVCaptureVideoDataOutputSampleBufferDelegate>
|
||||
|
||||
@property (nonatomic, strong) ORKFrontFacingCameraStepContentView *contentView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKFrontFacingCameraStepViewController {
|
||||
NSMutableArray *_results;
|
||||
|
||||
ORKFrontFacingCameraStep *_frontFacingCameraStep;
|
||||
|
||||
AVCaptureMovieFileOutput *_movieFileOutput;
|
||||
|
||||
NSURL *_tempOutputURL;
|
||||
NSURL *_savedFileURL;
|
||||
|
||||
NSString *_savedFileName;
|
||||
|
||||
AVCaptureDevice *_frontCameraCaptureDevice;
|
||||
AVCaptureSession *_captureSession;
|
||||
|
||||
NSInteger retryCount;
|
||||
}
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
self = [super initWithStep:step];
|
||||
|
||||
if (self) {
|
||||
retryCount = 0;
|
||||
_frontFacingCameraStep = (ORKFrontFacingCameraStep *)step;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
_results = [NSMutableArray new];
|
||||
|
||||
[self setupContentView];
|
||||
[self setupConstraints];
|
||||
[self startSession];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
[_contentView layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)handleError:(NSError *)error {
|
||||
// Shut down the session, if running
|
||||
if (_captureSession.isRunning) {
|
||||
[_captureSession stopRunning];
|
||||
}
|
||||
|
||||
// Reset the state to before the capture session was setup. Order here is important
|
||||
_captureSession = nil;
|
||||
_movieFileOutput = nil;
|
||||
_tempOutputURL = nil;
|
||||
_savedFileURL = nil;
|
||||
|
||||
// Handle error in the UI.
|
||||
[_contentView handleError:error];
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
[super stepDidFinish];
|
||||
|
||||
if (_tempOutputURL) {
|
||||
[self deleteTempVideoFile];
|
||||
}
|
||||
|
||||
[self goForward];
|
||||
}
|
||||
|
||||
- (void)setupContentView {
|
||||
_contentView = [[ORKFrontFacingCameraStepContentView alloc] initWithTitle:_frontFacingCameraStep.title text:_frontFacingCameraStep.text];
|
||||
_contentView.layer.cornerRadius = 10.0;
|
||||
_contentView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
|
||||
_contentView.clipsToBounds = YES;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[_contentView setViewEventHandler:^(ORKFrontFacingCameraStepContentViewEvent event) {
|
||||
[weakSelf handleContentViewEvent:event];
|
||||
}];
|
||||
|
||||
[self.view addSubview:_contentView];
|
||||
}
|
||||
|
||||
- (void)handleContentViewEvent:(ORKFrontFacingCameraStepContentViewEvent)event {
|
||||
|
||||
switch (event)
|
||||
{
|
||||
case ORKFrontFacingCameraStepContentViewEventStartRecording:
|
||||
[self startVideoRecording];
|
||||
break;
|
||||
|
||||
case ORKFrontFacingCameraStepContentViewEventStopRecording:
|
||||
[self stopVideoRecording];
|
||||
break;
|
||||
|
||||
case ORKFrontFacingCameraStepContentViewEventReviewRecording:
|
||||
{
|
||||
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:_tempOutputURL];
|
||||
AVPlayer *playVideo = [[AVPlayer alloc] initWithPlayerItem:playerItem];
|
||||
AVPlayerViewController *playerViewController = [[AVPlayerViewController alloc] init];
|
||||
playerViewController.player = playVideo;
|
||||
playerViewController.player.volume = 1.0;
|
||||
[self presentViewController:playerViewController animated:YES completion:nil];
|
||||
[playVideo play];
|
||||
break;
|
||||
}
|
||||
case ORKFrontFacingCameraStepContentViewEventRetryRecording:
|
||||
[self deleteTempVideoFile];
|
||||
retryCount++;
|
||||
break;
|
||||
|
||||
case ORKFrontFacingCameraStepContentViewEventSubmitRecording:
|
||||
{
|
||||
[self submitVideo];
|
||||
break;
|
||||
}
|
||||
case ORKFrontFacingCameraStepContentViewEventError:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
-(void)setupConstraints {
|
||||
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[[_contentView.topAnchor constraintEqualToAnchor:self.view.topAnchor] setActive:YES];
|
||||
[[_contentView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor] setActive:YES];
|
||||
[[_contentView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor] setActive:YES];
|
||||
[[_contentView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)startSession
|
||||
{
|
||||
_captureSession = [AVCaptureSession new];
|
||||
|
||||
_frontCameraCaptureDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
|
||||
|
||||
if (_frontCameraCaptureDevice)
|
||||
{
|
||||
NSError *error = nil;
|
||||
|
||||
AVCaptureDevice *captureAudioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
||||
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:_frontCameraCaptureDevice error:&error];
|
||||
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:captureAudioDevice error:&error];
|
||||
[AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVideoRecording options:0 error:&error];
|
||||
|
||||
if (error) {
|
||||
[self handleError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSFeatureUnsupportedError userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"CAPTURE_ERROR_CAMERA_NOT_FOUND", nil)}]];
|
||||
return;
|
||||
}
|
||||
|
||||
[_captureSession beginConfiguration];
|
||||
|
||||
if ([_captureSession canAddInput:deviceInput]) {
|
||||
[_captureSession addInput:deviceInput];
|
||||
}
|
||||
|
||||
if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
|
||||
[_captureSession setSessionPreset:AVCaptureSessionPreset640x480];
|
||||
}
|
||||
|
||||
if ([_captureSession canAddInput:audioInput]) {
|
||||
[_captureSession addInput:audioInput];
|
||||
}
|
||||
|
||||
_movieFileOutput = [AVCaptureMovieFileOutput new];
|
||||
if ([_captureSession canAddOutput:_movieFileOutput]) {
|
||||
[_captureSession addOutput:_movieFileOutput];
|
||||
AVCaptureConnection *captureConnection = [_movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
|
||||
if (captureConnection && [captureConnection isVideoStabilizationSupported]) {
|
||||
captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
|
||||
}
|
||||
}
|
||||
|
||||
AVCaptureVideoDataOutput *output = [AVCaptureVideoDataOutput new];
|
||||
|
||||
NSString* key = (NSString*)kCVPixelBufferPixelFormatTypeKey;
|
||||
NSNumber* value = [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA];
|
||||
NSDictionary* videoSettings = [NSDictionary dictionaryWithObject:value forKey:key];
|
||||
[output setVideoSettings:videoSettings];
|
||||
output.alwaysDiscardsLateVideoFrames = YES;
|
||||
|
||||
if ([_captureSession canAddOutput:output]) {
|
||||
[_captureSession addOutput:output];
|
||||
}
|
||||
|
||||
AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
|
||||
if ([connection isVideoOrientationSupported]) {
|
||||
connection.videoOrientation = AVCaptureVideoOrientationPortrait;
|
||||
}
|
||||
|
||||
if ([connection isVideoMirroringSupported]) {
|
||||
[connection setVideoMirrored:NO];
|
||||
}
|
||||
|
||||
[_captureSession commitConfiguration];
|
||||
|
||||
dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
|
||||
dispatch_queue_t recordingQueue = dispatch_queue_create("output.queue", qos);
|
||||
|
||||
[output setSampleBufferDelegate:self queue:recordingQueue];
|
||||
|
||||
[_contentView setPreviewLayerWithSession:_captureSession];
|
||||
|
||||
[_captureSession startRunning];
|
||||
}
|
||||
|
||||
[_contentView layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)startVideoRecording {
|
||||
if (![_movieFileOutput isRecording]) {
|
||||
|
||||
AVCaptureConnection *movieFileOutputConnection = [_movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
[movieFileOutputConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
|
||||
|
||||
NSArray<AVVideoCodecType> *availableVideoCodecTypes = _movieFileOutput.availableVideoCodecTypes;
|
||||
|
||||
if (availableVideoCodecTypes && [availableVideoCodecTypes containsObject:AVVideoCodecTypeHEVC]) {
|
||||
NSString* key = (NSString*)AVVideoCodecKey;
|
||||
NSString* value = (NSString*)AVVideoCodecTypeHEVC;
|
||||
NSDictionary* outputSettings = [NSDictionary dictionaryWithObject:value forKey:key];
|
||||
[_movieFileOutput setOutputSettings:outputSettings forConnection:movieFileOutputConnection];
|
||||
}
|
||||
|
||||
// Start recording to a temporary file.
|
||||
NSString *tempVideoFilePath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID new].UUIDString] stringByAppendingPathExtension:@"mov"];
|
||||
[_movieFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:tempVideoFilePath] recordingDelegate:self];
|
||||
}
|
||||
|
||||
[_contentView layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)stopVideoRecording {
|
||||
if (_movieFileOutput && [_movieFileOutput isRecording]) {
|
||||
[_movieFileOutput stopRecording];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)submitVideo {
|
||||
if ([self tempVideoFileExists])
|
||||
{
|
||||
//Save video to permanant file
|
||||
NSString *outputFileName = [NSUUID new].UUIDString;
|
||||
_savedFileName = [outputFileName stringByAppendingPathExtension:@"mov"];
|
||||
|
||||
NSURL *docURL = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject;
|
||||
docURL = [docURL URLByAppendingPathComponent:_savedFileName];
|
||||
|
||||
NSData *data = [NSData dataWithContentsOfURL:_tempOutputURL];
|
||||
BOOL wasDataSavedToURL = [data writeToURL:docURL atomically:YES];
|
||||
|
||||
if (wasDataSavedToURL)
|
||||
{
|
||||
//remove video saved to temp directory if it was saved successfully in the document directory
|
||||
_savedFileURL = docURL;
|
||||
[self deleteTempVideoFile];
|
||||
[self finish];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tempVideoFileExists {
|
||||
if (_tempOutputURL && [NSFileManager.defaultManager fileExistsAtPath:_tempOutputURL.relativePath]) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)deleteTempVideoFile {
|
||||
if ([self tempVideoFileExists]) {
|
||||
NSError *error;
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtPath:_tempOutputURL.relativePath error:&error];
|
||||
|
||||
if (!error) {
|
||||
_tempOutputURL = nil;
|
||||
} else {
|
||||
@throw [NSException exceptionWithName:NSGenericException reason:[NSString stringWithFormat:@"There was an error encountered while attempting to remove the saved video from the temp directory at path: %@", _tempOutputURL.path] userInfo:nil];
|
||||
}
|
||||
} else if (_tempOutputURL) {
|
||||
_tempOutputURL = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *stepResult = [super result];
|
||||
NSDate *now = stepResult.endDate;
|
||||
|
||||
NSMutableArray *results = [NSMutableArray arrayWithArray:stepResult.results];
|
||||
ORKFrontFacingCameraStepResult *frontFacingCameraResult = [[ORKFrontFacingCameraStepResult alloc] initWithIdentifier:self.step.identifier];
|
||||
frontFacingCameraResult.startDate = stepResult.startDate;
|
||||
frontFacingCameraResult.endDate = now;
|
||||
frontFacingCameraResult.contentType = @"video/quicktime";
|
||||
frontFacingCameraResult.fileURL = _savedFileURL;
|
||||
frontFacingCameraResult.fileName = _savedFileName;
|
||||
frontFacingCameraResult.retryCount = retryCount;
|
||||
|
||||
[results addObject:frontFacingCameraResult];
|
||||
stepResult.results = [results copy];
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
#pragma mark - AVCaptureFileOutputRecordingDelegate methods
|
||||
|
||||
- (void)captureOutput:(AVCaptureFileOutput *)output didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections {
|
||||
|
||||
[_contentView startTimerWithMaximumRecordingLimit:_frontFacingCameraStep.maximumRecordingLimit];
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
_tempOutputURL = outputFileURL;
|
||||
[_contentView presentReviewOptionsAllowingReview:_frontFacingCameraStep.allowsReview
|
||||
allowRetry:_frontFacingCameraStep.allowsRetry];
|
||||
|
||||
if (!_frontFacingCameraStep.allowsRetry && !_frontFacingCameraStep.allowsReview) {
|
||||
[self submitVideo];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -77,10 +77,10 @@
|
||||
[super start];
|
||||
|
||||
if (!_logger) {
|
||||
NSError *err = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&err];
|
||||
NSError *error = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&error];
|
||||
if (!_logger) {
|
||||
[self finishRecordingWithError:err];
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -99,18 +99,18 @@
|
||||
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:_healthClinicalType
|
||||
predicate:_healthFHIRResourceType ? [HKQuery predicateForClinicalRecordsWithFHIRResourceType:_healthFHIRResourceType] : nil limit:HKObjectQueryNoLimit
|
||||
sortDescriptors:nil
|
||||
resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
|
||||
NSUInteger resultCount = results.count;
|
||||
resultsHandler:^(HKSampleQuery * _Nonnull sampleQuery, NSArray<__kindof HKSample *> * _Nullable sampleResults, NSError * _Nullable error) {
|
||||
NSUInteger resultCount = sampleResults.count;
|
||||
if (resultCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
[results enumerateObjectsUsingBlock:^(HKClinicalRecord *clinicalRecord, NSUInteger idx, BOOL *stop) {
|
||||
[sampleResults enumerateObjectsUsingBlock:^(HKClinicalRecord *clinicalRecord, NSUInteger idx, BOOL *stop) {
|
||||
|
||||
NSError *error = nil;
|
||||
[_logger append:clinicalRecord.FHIRResource.data error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Warning(@"Failed to add health records object to the logger with error: %@", error);
|
||||
NSError *logError = nil;
|
||||
[_logger append:clinicalRecord.FHIRResource.data error:&logError];
|
||||
if (logError) {
|
||||
ORK_Log_Error("Failed to add health records object to the logger with error: %@", logError);
|
||||
return;
|
||||
}
|
||||
}];
|
||||
@@ -205,7 +205,7 @@
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, healthClinicalType, HKClinicalType);
|
||||
ORK_DECODE_OBJ(aDecoder, healthFHIRResourceType);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, healthFHIRResourceType, NSString);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
void (^handleResults)(NSArray <__kindof HKSample *> *, HKQueryAnchor *, NSUInteger, NSError *) = ^ (NSArray *results, HKQueryAnchor *newAnchor, NSUInteger newAnchorValue, NSError *error) {
|
||||
if (error) {
|
||||
// An error in the query's not the end of the world: we'll probably get another chance. Just log it.
|
||||
ORK_Log_Warning(@"Anchored query error: %@", error);
|
||||
ORK_Log_Error("Anchored query error: %@", error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,10 +194,10 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
[super start];
|
||||
|
||||
if (!_logger) {
|
||||
NSError *err = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&err];
|
||||
NSError *error = nil;
|
||||
_logger = [self makeJSONDataLoggerWithError:&error];
|
||||
if (!_logger) {
|
||||
[self finishRecordingWithError:err];
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,21 +141,6 @@ static const CGFloat ORKHolePegViewDiameter = 88.0f;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateLayoutMargins {
|
||||
CGFloat margin = ORKStandardHorizontalMarginForView(self);
|
||||
self.layoutMargins = (UIEdgeInsets){.left = margin * 2, .right = margin * 2};
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame {
|
||||
[super setFrame:frame];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds {
|
||||
[super setBounds:bounds];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if ([self.constraints count]) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
@@ -165,7 +150,6 @@ static const CGFloat ORKHolePegViewDiameter = 88.0f;
|
||||
NSMutableArray *constraintsArray = [NSMutableArray array];
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_progressView, _pegView, _holeView, _directionView);
|
||||
NSDictionary *metrics = @{@"diameter": @(ORKHolePegViewDiameter)};
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_progressView]-|"
|
||||
@@ -181,16 +165,6 @@ static const CGFloat ORKHolePegViewDiameter = 88.0f;
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_progressView]"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=0-[_pegView(diameter)]->=0-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:metrics views:views]];
|
||||
|
||||
[constraintsArray addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=0-[_holeView]->=0-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil views:views]];
|
||||
|
||||
[constraintsArray addObject:[NSLayoutConstraint constraintWithItem:self.pegView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKHolePegTestPlaceContentView.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
@@ -88,8 +89,7 @@
|
||||
self.holePegTestPlaceContentView.threshold = [self holePegTestPlaceStep].threshold;
|
||||
self.holePegTestPlaceContentView.delegate = self;
|
||||
self.activeStepView.activeCustomView = self.holePegTestPlaceContentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
self.activeStepView.scrollContainerShouldCollapseNavbar = NO;
|
||||
self.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
}
|
||||
|
||||
#pragma mark - step life cycle methods
|
||||
@@ -150,7 +150,7 @@
|
||||
[self start];
|
||||
}
|
||||
|
||||
[self.activeStepView updateTitle:nil
|
||||
[self.activeStepView updateTitle:self.step.title
|
||||
text:[[self holePegTestPlaceStep].movingDirection == ORKBodySagittalRight ? ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT_2", nil)]]];
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
[self saveSampleWithDistance:distance];
|
||||
|
||||
[holePegTestPlaceContentView setProgress:((CGFloat)self.successes / [self holePegTestPlaceStep].numberOfPegs) animated:YES];
|
||||
[self.activeStepView updateTitle:nil
|
||||
[self.activeStepView updateTitle:self.step.title
|
||||
text:[[self holePegTestPlaceStep].movingDirection == ORKBodySagittalRight ? ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)]]];
|
||||
|
||||
if (self.successes >= [self holePegTestPlaceStep].numberOfPegs) {
|
||||
@@ -172,7 +172,7 @@
|
||||
- (void)holePegTestPlaceDidFail:(ORKHolePegTestPlaceContentView *)holePegTestPlaceContentView {
|
||||
self.failures++;
|
||||
|
||||
[self.activeStepView updateTitle:nil
|
||||
[self.activeStepView updateTitle:self.step.title
|
||||
text:[[self holePegTestPlaceStep].movingDirection == ORKBodySagittalRight ? ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_PLACE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)]]];
|
||||
}
|
||||
|
||||
|
||||
@@ -134,21 +134,6 @@ static const CGFloat PegViewSeparatorWidth = 2.0f;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateLayoutMargins {
|
||||
CGFloat margin = ORKStandardHorizontalMarginForView(self);
|
||||
self.layoutMargins = (UIEdgeInsets){.left = margin * 2, .right = margin * 2};
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame {
|
||||
[super setFrame:frame];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds {
|
||||
[super setBounds:bounds];
|
||||
[self updateLayoutMargins];
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if ([self.constraints count]) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKHolePegTestRemoveContentView.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
@@ -86,8 +87,7 @@
|
||||
self.holePegTestRemoveContentView.threshold = [self holePegTestRemoveStep].threshold;
|
||||
self.holePegTestRemoveContentView.delegate = self;
|
||||
self.activeStepView.activeCustomView = self.holePegTestRemoveContentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
self.activeStepView.scrollContainerShouldCollapseNavbar = NO;
|
||||
self.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
|
||||
NSString *identifier = [[self holePegTestRemoveStep].identifier stringByReplacingOccurrencesOfString:@"remove" withString:@"place"];
|
||||
NSTimeInterval placeStepDuration = ((ORKHolePegTestResult *)[[self.taskViewController.result stepResultForStepIdentifier:identifier].results firstObject]).totalTime;
|
||||
@@ -150,7 +150,7 @@
|
||||
#pragma mark - hole peg test content view delegate
|
||||
|
||||
- (void)holePegTestRemoveDidProgress:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView {
|
||||
[self.activeStepView updateTitle:nil
|
||||
[self.activeStepView updateTitle:self.step.title
|
||||
text:[[self holePegTestRemoveStep].movingDirection == ORKBodySagittalLeft ? ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT_2", nil)]]];
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
[self saveSampleWithDistance:distance];
|
||||
|
||||
[holePegTestRemoveContentView setProgress:((CGFloat)self.successes / [self holePegTestRemoveStep].numberOfPegs) animated:YES];
|
||||
[self.activeStepView updateTitle:nil
|
||||
[self.activeStepView updateTitle:self.step.title
|
||||
text:[[self holePegTestRemoveStep].movingDirection == ORKBodySagittalLeft ? ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)]]];
|
||||
|
||||
if (self.successes >= [self holePegTestRemoveStep].numberOfPegs) {
|
||||
@@ -171,7 +171,7 @@
|
||||
- (void)holePegTestRemoveDidFail:(ORKHolePegTestRemoveContentView *)holePegTestRemoveContentView {
|
||||
self.failures++;
|
||||
|
||||
[self.activeStepView updateTitle:nil
|
||||
[self.activeStepView updateTitle:self.step.title
|
||||
text:[[self holePegTestRemoveStep].movingDirection == ORKBodySagittalLeft ? ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_RIGHT_HAND", nil) : ORKLocalizedString(@"HOLE_PEG_TEST_REMOVE_INSTRUCTION_LEFT_HAND", nil) stringByAppendingString:[@"\n" stringByAppendingString:ORKLocalizedString(@"HOLE_PEG_TEST_TEXT", nil)]]];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import ResearchKit.Private
|
||||
|
||||
public class ORKLandoltCResult: ORKResult {
|
||||
|
||||
public var outcome: Bool?
|
||||
public var letterAngle: Double?
|
||||
public var sliderAngle: Double?
|
||||
public var score: Int?
|
||||
|
||||
enum Keys: String {
|
||||
case outcome
|
||||
case letterAngle
|
||||
case sliderAngle
|
||||
case score
|
||||
}
|
||||
|
||||
public init(identifier: String, outcome: Bool, letterAngle: Double, sliderAngle: Double, score: Int) {
|
||||
super.init(identifier: identifier)
|
||||
|
||||
self.outcome = outcome
|
||||
self.letterAngle = letterAngle
|
||||
self.sliderAngle = sliderAngle
|
||||
self.score = score
|
||||
}
|
||||
|
||||
override public func encode(with aCoder: NSCoder) {
|
||||
super.encode(with: aCoder)
|
||||
|
||||
aCoder.encode(outcome, forKey: Keys.outcome.rawValue)
|
||||
aCoder.encode(letterAngle, forKey: Keys.letterAngle.rawValue)
|
||||
aCoder.encode(sliderAngle, forKey: Keys.sliderAngle.rawValue)
|
||||
aCoder.encode(score, forKey: Keys.score.rawValue)
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
outcome = aDecoder.decodeObject(forKey: Keys.outcome.rawValue) as? Bool ?? false
|
||||
letterAngle = aDecoder.decodeObject(forKey: Keys.letterAngle.rawValue) as? Double ?? 0.0
|
||||
sliderAngle = aDecoder.decodeObject(forKey: Keys.sliderAngle.rawValue) as? Double ?? 0.0
|
||||
score = aDecoder.decodeObject(forKey: Keys.score.rawValue) as? Int ?? 0
|
||||
}
|
||||
|
||||
override public func copy(with zone: NSZone? = nil) -> Any {
|
||||
let result = super.copy(with: zone) as! ORKLandoltCResult
|
||||
|
||||
result.outcome = outcome
|
||||
result.letterAngle = letterAngle
|
||||
result.sliderAngle = sliderAngle
|
||||
result.score = score
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override public func isEqual(_ object: Any?) -> Bool {
|
||||
let isParentSame = super.isEqual(object)
|
||||
|
||||
if let castObject = object as? ORKLandoltCResult {
|
||||
|
||||
return (isParentSame &&
|
||||
ORKEqualObjects(outcome as Any, castObject.outcome as Any) &&
|
||||
ORKEqualObjects(letterAngle as Any, castObject.letterAngle as Any) &&
|
||||
ORKEqualObjects(sliderAngle as Any, castObject.sliderAngle as Any) &&
|
||||
ORKEqualObjects(score as Any, castObject.score as Any))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override public func description(withNumberOfPaddingSpaces numberOfPaddingSpaces: UInt) -> String {
|
||||
let descriptionString = """
|
||||
\(descriptionPrefix(withNumberOfPaddingSpaces: numberOfPaddingSpaces));
|
||||
Outcome: \(String(describing: outcome)); LetterAngle: \(String(describing: letterAngle));
|
||||
SliderAngle: \(String(describing: sliderAngle)); Score: \(String(describing: score))
|
||||
"""
|
||||
return descriptionString
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@objc
|
||||
public enum VisionStepLeftOrRightEye: Int {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
@objc
|
||||
public enum VisionStepType: Int {
|
||||
case visualAcuity
|
||||
case contrastSensitivity
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ORKLandoltCStep: ORKActiveStep {
|
||||
|
||||
public var testType: VisionStepType?
|
||||
public var eyeToTest: VisionStepLeftOrRightEye?
|
||||
|
||||
enum Key: String {
|
||||
case testType
|
||||
case eyeToTest
|
||||
}
|
||||
|
||||
public override class func stepViewControllerClass() -> AnyClass {
|
||||
return ORKLandoltCStepViewController.self
|
||||
}
|
||||
|
||||
public class func supportsSecureCoding() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc
|
||||
public init(identifier: String, testType: VisionStepType, eyeToTest: VisionStepLeftOrRightEye) {
|
||||
super.init(identifier: identifier)
|
||||
self.testType = testType
|
||||
self.eyeToTest = eyeToTest
|
||||
}
|
||||
|
||||
public override var allowsBackNavigation: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let visionStep: ORKLandoltCStep = super.copy(with: zone) as! ORKLandoltCStep
|
||||
return visionStep
|
||||
}
|
||||
|
||||
public required init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
if let typeValue = aDecoder.decodeObject(forKey: "stepType") as? Int {
|
||||
testType = VisionStepType(rawValue: typeValue)
|
||||
}
|
||||
|
||||
if let eyeValue = aDecoder.decodeObject(forKey: "eyeToTest") as? Int {
|
||||
eyeToTest = VisionStepLeftOrRightEye(rawValue: eyeValue)
|
||||
}
|
||||
}
|
||||
|
||||
public override func encode(with aCoder: NSCoder) {
|
||||
super.encode(with: aCoder)
|
||||
aCoder.encode(testType, forKey: Key.testType.rawValue)
|
||||
aCoder.encode(eyeToTest, forKey: Key.eyeToTest.rawValue)
|
||||
}
|
||||
|
||||
public override func isEqual(_ object: Any?) -> Bool {
|
||||
if let object = object as? ORKLandoltCStep {
|
||||
return testType == object.testType && eyeToTest == object.eyeToTest
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import ResearchKit.Private
|
||||
|
||||
internal class ORKLandoltCStepContentView: UIView {
|
||||
|
||||
var eyeActivitySlider: EyeActivitySlider?
|
||||
private var testType: VisionStepType?
|
||||
|
||||
internal init(testType: VisionStepType) {
|
||||
super.init(frame: CGRect())
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.testType = testType
|
||||
setupSubviews()
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
internal required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
internal func setupSubviews() {
|
||||
guard let typeValue = testType else {
|
||||
return
|
||||
}
|
||||
|
||||
eyeActivitySlider = EyeActivitySlider(testType: typeValue)
|
||||
addSubview(eyeActivitySlider!)
|
||||
}
|
||||
|
||||
internal func setupConstraints() {
|
||||
eyeActivitySlider?.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
NSLayoutConstraint(item: eyeActivitySlider!,
|
||||
attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .width,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: eyeActivitySlider!,
|
||||
attribute: .height,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .width,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0)
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import ResearchKit.Private
|
||||
|
||||
public class ORKLandoltCStepViewController: ORKActiveStepViewController {
|
||||
|
||||
private var activityTimer = Timer()
|
||||
private var results = NSMutableArray()
|
||||
private var visionStepView: ORKLandoltCStepView
|
||||
private var eyeToTest: VisionStepLeftOrRightEye?
|
||||
private var testType: VisionStepType?
|
||||
|
||||
public override init(step: ORKStep?) {
|
||||
if let visionStep = step as? ORKLandoltCStep {
|
||||
eyeToTest = visionStep.eyeToTest
|
||||
testType = visionStep.testType
|
||||
}
|
||||
visionStepView = ORKLandoltCStepView(testType: testType)
|
||||
super.init(step: step)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override var result: ORKStepResult? {
|
||||
let stepResult = super.result
|
||||
stepResult?.results = results.copy() as? [ORKResult]
|
||||
|
||||
return stepResult!
|
||||
}
|
||||
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor.white
|
||||
activeStepView?.customContentView = visionStepView
|
||||
activeStepView?.removeCustomContentPadding()
|
||||
activeStepView?.customContentFillsAvailableSpace = true
|
||||
|
||||
// TODO: Localize
|
||||
visionStepView.currentEyeLabel.text = eyeToTest == .left ? "Left Eye" : "Right Eye"
|
||||
visionStepView.continueButton.addTarget(self, action: #selector(continueButtonWasPressed), for: .touchUpInside)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
override public func stepDidFinish() {
|
||||
super.stepDidFinish()
|
||||
goForward()
|
||||
}
|
||||
|
||||
private func startTimer() {
|
||||
activityTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(hideCircle), userInfo: nil, repeats: false)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func hideCircle() {
|
||||
activityTimer.invalidate()
|
||||
visionStepView.visionContentView?.eyeActivitySlider?.hideLetter()
|
||||
visionStepView.topInstructionLabel.isHidden = false
|
||||
}
|
||||
|
||||
@objc
|
||||
private func continueButtonWasPressed() {
|
||||
activityTimer.invalidate()
|
||||
visionStepView.topInstructionLabel.isHidden = true
|
||||
visionStepView.continueButton.isEnabled = false
|
||||
|
||||
if let resultData = visionStepView.visionContentView?.eyeActivitySlider?.fetchResultDataAndUpdateSlider() {
|
||||
let stepResult: ORKLandoltCResult = ORKLandoltCResult(identifier: step!.identifier,
|
||||
outcome: resultData.outcome,
|
||||
letterAngle: resultData.letterAngle,
|
||||
sliderAngle: resultData.sliderAngle,
|
||||
score: resultData.score)
|
||||
results.add(stepResult)
|
||||
|
||||
if resultData.incorrectAnswers == 2 || resultData.score == resultData.maxScore {
|
||||
stepDidFinish()
|
||||
} else {
|
||||
visionStepView.continueButton.isEnabled = true
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ORKLandoltCStepView: UIView {
|
||||
|
||||
var visionContentView: ORKLandoltCStepContentView?
|
||||
|
||||
let continueButtonCornerRadius: CGFloat = 12.0
|
||||
let eyeLabelTopPadding: CGFloat = 20.0
|
||||
let instructionLabelTopPadding: CGFloat = 15.0
|
||||
let visionContentTopPadding: CGFloat = 10.0
|
||||
|
||||
let continueButton = ORKRoundTappingButton()
|
||||
let currentEyeLabel = UILabel()
|
||||
let topInstructionLabel = UILabel()
|
||||
|
||||
init(testType: VisionStepType!) {
|
||||
super.init(frame: .zero)
|
||||
setupCurrentEyeLabel()
|
||||
setupTopInstructionLabel()
|
||||
setupVisionContentView(testType: testType)
|
||||
setupContinueButton()
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setupVisionContentView(testType: VisionStepType!) {
|
||||
if visionContentView == nil {
|
||||
visionContentView = ORKLandoltCStepContentView(testType: testType)
|
||||
}
|
||||
addSubview(visionContentView!)
|
||||
}
|
||||
|
||||
func setupCurrentEyeLabel() {
|
||||
currentEyeLabel.isHidden = true
|
||||
currentEyeLabel.textAlignment = .center
|
||||
currentEyeLabel.textColor = UIColor.black
|
||||
currentEyeLabel.numberOfLines = 0
|
||||
// TODO: set FontDescriptor
|
||||
currentEyeLabel.font = UIFont(name: "", size: 20.0)
|
||||
addSubview(currentEyeLabel)
|
||||
}
|
||||
|
||||
func setupTopInstructionLabel() {
|
||||
topInstructionLabel.textAlignment = .center
|
||||
topInstructionLabel.numberOfLines = 0
|
||||
topInstructionLabel.textColor = UIColor.black
|
||||
// TODO: Localize
|
||||
topInstructionLabel.text = "Move the dial to where you think the opening in the letter was."
|
||||
// TODO: set FontDescriptor
|
||||
topInstructionLabel.font = UIFont(name: "", size: 20.0)
|
||||
topInstructionLabel.isHidden = true
|
||||
addSubview(topInstructionLabel)
|
||||
}
|
||||
|
||||
func setupContinueButton() {
|
||||
// TODO: Localize
|
||||
continueButton.diameter = 60.0
|
||||
continueButton.setTitle("Next", for: UIControl.State.normal)
|
||||
continueButton.backgroundColor = tintColor
|
||||
continueButton.layer.cornerRadius = continueButtonCornerRadius
|
||||
addSubview(continueButton)
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
currentEyeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
topInstructionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
visionContentView?.translatesAutoresizingMaskIntoConstraints = false
|
||||
continueButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let constraints = [
|
||||
NSLayoutConstraint(item: currentEyeLabel,
|
||||
attribute: .top,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .top,
|
||||
multiplier: 1.0,
|
||||
constant: eyeLabelTopPadding),
|
||||
NSLayoutConstraint(item: currentEyeLabel,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: topInstructionLabel,
|
||||
attribute: .top,
|
||||
relatedBy: .equal,
|
||||
toItem: currentEyeLabel,
|
||||
attribute: .bottom,
|
||||
multiplier: 1.0,
|
||||
constant: instructionLabelTopPadding),
|
||||
NSLayoutConstraint(item: topInstructionLabel,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: topInstructionLabel,
|
||||
attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .width,
|
||||
multiplier: 0.8,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: visionContentView!,
|
||||
attribute: .top,
|
||||
relatedBy: .equal,
|
||||
toItem: topInstructionLabel,
|
||||
attribute: .bottom,
|
||||
multiplier: 1.0,
|
||||
constant: visionContentTopPadding),
|
||||
NSLayoutConstraint(item: visionContentView!,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: visionContentView!,
|
||||
attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .width,
|
||||
multiplier: 0.8,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: visionContentView!,
|
||||
attribute: .height,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .height,
|
||||
multiplier: 0.8,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: continueButton,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: continueButton,
|
||||
attribute: .bottom,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .bottom,
|
||||
multiplier: 1.0,
|
||||
constant: -20.0)
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,13 @@
|
||||
return @"location";
|
||||
}
|
||||
|
||||
|
||||
// Test Seam - unit tests don't support background updates or pausing.
|
||||
- (CLLocationManager *)createLocationManager {
|
||||
return [[CLLocationManager alloc] init];
|
||||
CLLocationManager *manager = [[CLLocationManager alloc] init];
|
||||
manager.pausesLocationUpdatesAutomatically = NO;
|
||||
manager.allowsBackgroundLocationUpdates = YES;
|
||||
return manager;
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
@@ -88,20 +93,19 @@
|
||||
}
|
||||
|
||||
self.locationManager = [self createLocationManager];
|
||||
if ([CLLocationManager authorizationStatus] <= kCLAuthorizationStatusDenied) {
|
||||
|
||||
CLAuthorizationStatus status = kCLAuthorizationStatusNotDetermined;
|
||||
|
||||
if (@available(iOS 14.0, *)) {
|
||||
status = self.locationManager.authorizationStatus;
|
||||
} else {
|
||||
status = [CLLocationManager authorizationStatus];
|
||||
}
|
||||
|
||||
if (status == kCLAuthorizationStatusRestricted || status == kCLAuthorizationStatusNotDetermined) {
|
||||
[self.locationManager requestWhenInUseAuthorization];
|
||||
}
|
||||
self.locationManager.pausesLocationUpdatesAutomatically = NO;
|
||||
self.locationManager.delegate = self;
|
||||
|
||||
if (!self.locationManager) {
|
||||
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
|
||||
code:NSFeatureUnsupportedError
|
||||
userInfo:@{@"recorder": self}];
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
self.uptime = [NSProcessInfo processInfo].systemUptime;
|
||||
[self.locationManager startUpdatingLocation];
|
||||
}
|
||||
@@ -129,7 +133,8 @@
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager
|
||||
didUpdateLocations:(NSArray *)locations {
|
||||
didUpdateLocations:(NSArray<CLLocation *> *)locations {
|
||||
|
||||
BOOL success = YES;
|
||||
NSParameterAssert(locations.count >= 0);
|
||||
NSError *error = nil;
|
||||
@@ -156,7 +161,15 @@
|
||||
}
|
||||
|
||||
- (BOOL)isRecording {
|
||||
return [CLLocationManager locationServicesEnabled] && (self.locationManager != nil) && ([CLLocationManager authorizationStatus] > kCLAuthorizationStatusDenied);
|
||||
CLAuthorizationStatus status = kCLAuthorizationStatusNotDetermined;
|
||||
|
||||
if (@available(iOS 14.0, *)) {
|
||||
status = self.locationManager.authorizationStatus;
|
||||
} else {
|
||||
status = [CLLocationManager authorizationStatus];
|
||||
}
|
||||
|
||||
return [CLLocationManager locationServicesEnabled] && (self.locationManager != nil) && (status > kCLAuthorizationStatusDenied);
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015, James Cox. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKNormalizedReactionTimeStimulusView.h"
|
||||
#import "ORKRoundTappingButton.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKNormalizedReactionTimeContentView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic) ORKRoundTappingButton *button;
|
||||
|
||||
- (void)setStimulusHidden:(BOOL)hidden;
|
||||
|
||||
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void (^)(void))completion;
|
||||
|
||||
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void (^)(void))completion;
|
||||
|
||||
- (void)resetAfterDelay:(NSTimeInterval)delay completion:(nullable void (^)(void))completion;
|
||||
|
||||
- (UIView *)getBackgroundView;
|
||||
|
||||
- (ORKNormalizedReactionTimeStimulusView *)getStimulusView;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015, James Cox. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKNormalizedReactionTimeContentView.h"
|
||||
|
||||
#import "ORKNavigationContainerView.h"
|
||||
#import "ORKNormalizedReactionTimeStimulusView.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
CGFloat NormalizeButtonSize = 100.0;
|
||||
CGFloat BackgroundViewSpaceMultiplier = 2.0;
|
||||
|
||||
@implementation ORKNormalizedReactionTimeContentView {
|
||||
ORKNormalizedReactionTimeStimulusView *_stimulusView;
|
||||
|
||||
UIView *_backgroundView;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self resizeConstraints];
|
||||
[self addStimulusView];
|
||||
[self addBackgroundView];
|
||||
[self addButton];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(void (^)(void))completion {
|
||||
[_stimulusView startSuccessAnimationWithDuration:duration completion:completion];
|
||||
}
|
||||
|
||||
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(void (^)(void))completion {
|
||||
[_stimulusView startFailureAnimationWithDuration:duration completion:completion];
|
||||
}
|
||||
|
||||
- (void)resetAfterDelay:(NSTimeInterval)delay completion:(nullable void (^)(void))completion {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
_stimulusView.hidden = YES;
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
-(void)resizeConstraints {
|
||||
ORKScreenType screenType = ORKGetVerticalScreenTypeForWindow([[[UIApplication sharedApplication] delegate] window]);
|
||||
if (screenType == ORKScreenTypeiPhone5 ) {
|
||||
NormalizeButtonSize = 70.0;
|
||||
BackgroundViewSpaceMultiplier = 1.75;
|
||||
}
|
||||
}
|
||||
|
||||
-(void)addButton {
|
||||
_button = [ORKRoundTappingButton new];
|
||||
_button.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_button setTitle: ORKLocalizedString(@"REACTION_TIME_TASK_NORM_BUTTON_TITLE", nil) forState:UIControlStateNormal];
|
||||
|
||||
[_button setDiameter:NormalizeButtonSize];
|
||||
|
||||
|
||||
[self addSubview:_button];
|
||||
|
||||
[NSLayoutConstraint activateConstraints: @[
|
||||
|
||||
[NSLayoutConstraint constraintWithItem:_button
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_button
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_backgroundView
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:1.0],
|
||||
|
||||
]];
|
||||
|
||||
}
|
||||
|
||||
- (void)addStimulusView {
|
||||
if (!_stimulusView) {
|
||||
_stimulusView = [ORKNormalizedReactionTimeStimulusView new];
|
||||
_stimulusView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_stimulusView.backgroundColor = self.tintColor;
|
||||
[self addSubview:_stimulusView];
|
||||
[self setUpStimulusViewConstraints];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addBackgroundView {
|
||||
if (!_backgroundView) {
|
||||
_backgroundView = [UIView new];
|
||||
}
|
||||
_backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_backgroundView.layer.borderWidth = 3.0;
|
||||
_backgroundView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:0.3];
|
||||
_backgroundView.layer.borderColor = [UIColor lightGrayColor].CGColor;
|
||||
[self insertSubview:_backgroundView belowSubview:_stimulusView];
|
||||
[self setupBackgroundViewConstraints];
|
||||
}
|
||||
|
||||
- (UIView *)getBackgroundView {
|
||||
return _backgroundView;
|
||||
}
|
||||
|
||||
- (ORKNormalizedReactionTimeStimulusView *)getStimulusView {
|
||||
return _stimulusView;
|
||||
}
|
||||
|
||||
- (void)setStimulusHidden:(BOOL)hidden {
|
||||
_stimulusView.hidden = hidden;
|
||||
}
|
||||
|
||||
- (void)setUpStimulusViewConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_stimulusView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_stimulusView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_stimulusView]-(>=0)-|"
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:nil
|
||||
views:NSDictionaryOfVariableBindings(_stimulusView)]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
- (void)setupBackgroundViewConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
[constraints addObjectsFromArray:@[
|
||||
[NSLayoutConstraint constraintWithItem:_backgroundView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_stimulusView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_backgroundView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_stimulusView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_backgroundView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_stimulusView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
multiplier:BackgroundViewSpaceMultiplier
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_backgroundView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_stimulusView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
multiplier:BackgroundViewSpaceMultiplier
|
||||
constant:0.0],
|
||||
|
||||
]];
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user