Compare commits
420 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c71d1a56dd | |||
| 6c24a7e922 | |||
| b14e5cfcb0 | |||
| fee76c2d93 | |||
| a07041901b | |||
| c370011616 | |||
| b8f155974c | |||
| 2fb22a6256 | |||
| bd60d945bb | |||
| 8f58410af9 | |||
| fc67cc944c | |||
| c8473017ec | |||
| 1b9daf60ee | |||
| 2bc0033aa8 | |||
| e71d71cf9c | |||
| 64a068596b | |||
| c10693930d | |||
| c0f309edf3 | |||
| b1cc631748 | |||
| 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 |
@@ -0,0 +1,26 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main', 'stable' ]
|
||||
pull_request:
|
||||
branches: [ 'main', 'stable' ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-14
|
||||
strategy:
|
||||
matrix:
|
||||
destination: ['platform=iOS\ Simulator,OS=17.4,name=iPhone\ 15\ Pro']
|
||||
scheme: ['ResearchKit']
|
||||
name: ${{ matrix.scheme }} Unit Tests
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set Xcode Version
|
||||
run: sudo xcode-select -s /Applications/Xcode_15.3.app
|
||||
- name: Test
|
||||
run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -verbose -workspace RKWorkspace.xcworkspace -scheme ${{ matrix.scheme }} -destination ${{ matrix.destination }} build test | xcpretty
|
||||
@@ -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://developer.apple.com/forums/tags/researchkit) or [contact us](mailto:researchkit@apple.com) for guidance.
|
||||
|
||||
|
||||
Contributing software
|
||||
@@ -16,8 +16,7 @@ This page assumes you already know how to check out and build the
|
||||
code. Contributions to the ResearchKit framework are expected to comply with the
|
||||
[ResearchKit Contribution Terms and License Policy](#contribution); please familiarize yourself
|
||||
with this policy prior to submitting a pull request. For any contribution, ensure that you own
|
||||
the rights or have permission from the copyright holder. (e.g. code, images, surveys, videos
|
||||
and other content you may include)
|
||||
the rights or have permission from the copyright holder (e.g. code, images, surveys, videos and other content you may include).
|
||||
|
||||
To contribute to ResearchKit:
|
||||
|
||||
@@ -27,7 +26,7 @@ To contribute to ResearchKit:
|
||||
4. [Run the tests.](#test)
|
||||
5. [Submit a pull request.](#request)
|
||||
6. Make any changes requested by the reviewer, and update your pull request as needed.
|
||||
7. Once accepted, your pull request will be merged into master.
|
||||
7. Once accepted, your pull request will be merged into main.
|
||||
|
||||
Choosing an issue to work on<a name="create"></a>
|
||||
----------------------------
|
||||
@@ -36,13 +35,12 @@ To find an issue to work on, either pick something that you need for
|
||||
your app, or select one of the issues from our [issue list](../../issues). Or,
|
||||
consider one of the areas where we'd like to extend ResearchKit:
|
||||
|
||||
* Faster 'get started' to a useful app
|
||||
* More active tasks
|
||||
* Data analysis for active tasks
|
||||
* More consent sections
|
||||
* Back end integrations
|
||||
* Improving the APIs needed to get started with a ResearchKit project
|
||||
* New Active Tasks
|
||||
* Data analysis for Active Tasks
|
||||
* Backend 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://developer.apple.com/forums/tags/researchkit).
|
||||
|
||||
|
||||
Creating a personal fork<a name="fork"></a>
|
||||
@@ -56,8 +54,7 @@ Develop your changes in your fork<a name="develop"></a>
|
||||
---------------------------------
|
||||
|
||||
Develop your changes using your normal development process. If you
|
||||
already have code from an existing project, you may need to adjust its
|
||||
style to more closely match the [ResearchKit framework coding style](./docs-standalone/coding-style-guide.md).
|
||||
already have code from an existing project, you may need to adjust its style to more closely match the [ResearchKit framework coding style](./docs-standalone/coding-style-guide.md).
|
||||
|
||||
New components may need to expose new Public or Private
|
||||
headers. Public headers are for APIs that are likely to be a stable
|
||||
@@ -78,7 +75,7 @@ code to other existing demo apps to exercise your feature.
|
||||
When adding UI driven components, make sure that they are accessible.
|
||||
Follow the steps outlined in the [Best Practices](../../wiki/best-practices)
|
||||
section under Accessibility. Before submitting the pull request, you should
|
||||
audit your components with Voice Over (or other relevant assistive technologies)
|
||||
audit your components with VoiceOver (or other relevant assistive technologies)
|
||||
enabled.
|
||||
|
||||
Keep changes that fix different issues separate. For bug fixes,
|
||||
@@ -99,11 +96,11 @@ verify that test apps run on both device and simulator.
|
||||
|
||||
Where your code affects UI presentation, also test:
|
||||
|
||||
* Multiple device form factors (for instance, iPhone 4S, iPhone 5, iPhone 6, iPhone 6 Plus).
|
||||
* Multiple device form factors (for instance, iPhone SE, iPhone 14, iPhone 15 Pro, iPhone 15 Pro Max).
|
||||
* Dynamic text, especially at the "Large" setting.
|
||||
* Rotation between portrait and landscape, where appropriate.
|
||||
|
||||
You can use the apps in the `Testing` and `samples` directories to
|
||||
You can use the `ORKCatalog` app in the `samples` directory to
|
||||
test your changes.
|
||||
|
||||
Submit a pull request<a name="request"></a>
|
||||
@@ -120,7 +117,7 @@ After acceptance<a name="after"></a>
|
||||
----------------
|
||||
|
||||
Once your pull request has been accepted, your changes will be merged
|
||||
to master. You are still responsible for your change after it is
|
||||
to main. You are still responsible for your change after it is
|
||||
accepted. Stay in contact, in case bugs are detected that may require
|
||||
your attention.
|
||||
|
||||
@@ -134,7 +131,7 @@ documentation, or other issues during this process.
|
||||
Release process
|
||||
-----------------
|
||||
|
||||
The `master` branch is used for work in progress. On `master`:
|
||||
The `main` branch is used for work in progress. On `main`:
|
||||
|
||||
* All test apps should build and run error free.
|
||||
* Unit tests should all pass.
|
||||
@@ -142,9 +139,9 @@ The `master` branch is used for work in progress. On `master`:
|
||||
base language).
|
||||
|
||||
The project will make periodic releases. When preparing a stable release, we
|
||||
will branch from `master` to a convergence branch. During this process,
|
||||
will branch from `main` to a convergence branch. During this process,
|
||||
changes will be made first to the convergence branch, and then merged into
|
||||
`master`. On the convergence branch, changes will be made only to:
|
||||
`main`. On the convergence branch, changes will be made only to:
|
||||
|
||||
* Fix high priority issues.
|
||||
* Update documentation.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015-2024, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
@@ -1,283 +1,175 @@
|
||||
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.
|
||||
|
||||
# Table of Contents
|
||||
|
||||
* [Requirements](#requirements)
|
||||
* [Documentation](#documentation)
|
||||
* [Getting Started](#gettingstarted)
|
||||
* Documentation:
|
||||
* [Programming Guide](http://researchkit.org/docs/docs/Overview/GuideOverview.html)
|
||||
* [Framework Reference](http://researchkit.org/docs/index.html)
|
||||
* [Best Practices](../../wiki/best-practices)
|
||||
* [Contributing to ResearchKit](CONTRIBUTING.md)
|
||||
* [Website](http://researchkit.org) and [Blog](http://researchkit.org/blog.html)
|
||||
* [ResearchKit BSD License](#license)
|
||||
* [Installing](#installation)
|
||||
* [ORKCatalog App](#orkcatalog-app)
|
||||
* [Surveys](#surveys)
|
||||
* [Consent](#consent)
|
||||
* [Active Tasks](#active-tasks)
|
||||
* [Getting Help](#getting-help)
|
||||
* [License](#license)
|
||||
|
||||
Getting More Information
|
||||
========================
|
||||
# Requirements <a name="requirements"></a>
|
||||
|
||||
* Join the [*ResearchKit* Forum](https://forums.developer.apple.com/community/researchkit) for discussing uses of the *ResearchKit framework and* related projects.
|
||||
The *ResearchKit framework* codebase supports iOS and requires Xcode 12.0 or newer. The *ResearchKit framework* has a Base SDK version of 13.0.
|
||||
|
||||
Use Cases
|
||||
===========
|
||||
# Documentation <a name="documentation"></a>
|
||||
|
||||
A task in the *ResearchKit framework* contains a set of steps to present to a user. Everything,
|
||||
whether it’s a *survey*, the *consent process*, or *active tasks*, is represented as a task that can
|
||||
be presented with a task view controller.
|
||||
<img width="1000" alt="ebedded-framework" src="https://github.com/ResearchKit/ResearchKit/assets/29615893/19d6edd3-3d95-4416-9ac4-24ccb35e09c2">
|
||||
|
||||
Surveys
|
||||
-------
|
||||
View the *ResearchKit framework* documentation by setting ResearchKit as your target in Xcode and selecting 'Build Documentation' in the Product menu dropdown.
|
||||
|
||||
|
||||
# Getting Started <a name="gettingstarted"></a>
|
||||
|
||||
* [Website](https://www.researchandcare.org)
|
||||
* [WWDC: ResearchKit and CareKit Reimagined](https://developer.apple.com/videos/play/wwdc2019/217/)
|
||||
|
||||
|
||||
### Install as an embedded framework <a name="installation"></a>
|
||||
|
||||
Download the project source code and drag in ResearchKit.xcodeproj. Then, embed *ResearchKit* framework in your app by adding it to the "Frameworks, Libraries, and Embedded Content" section for your target as shown in the figure below.
|
||||
|
||||
<img width="1000" alt="ebedded-framework" src="https://github.com/ResearchKit/ResearchKit/assets/29615893/7479f313-ecc7-4d94-8c64-c58ae7362a4d">
|
||||
|
||||
### ORKCatalog App <a name="orkcatalog-app"></a>
|
||||
|
||||
The included catalog app demonstrates the different modules that are available in *ResearchKit*. Find the
|
||||
project in ResearchKit's [`samples`](samples) directory.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
# Surveys <a name="surveys"></a>
|
||||
|
||||
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
|
||||
information.
|
||||
modally on an *iPhone* or *iPad*. The example below shows the process to present a height question for a participant to answer.
|
||||
|
||||
```swift
|
||||
import ResearchKit
|
||||
import ResearchKitUI
|
||||
|
||||
let sectionHeaderFormItem = ORKFormItem(sectionTitle: "Your question here.")
|
||||
|
||||
Consent
|
||||
----------------
|
||||
let heightQuestionFormItem = ORKFormItem(identifier: "heightQuestionFormItem1", text: nil, answerFormat: ORKAnswerFormat.heightAnswerFormat())
|
||||
heightQuestionFormItem.placeholder = "Tap here"
|
||||
|
||||
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
|
||||
more information.
|
||||
let formStep = ORKFormStep(identifier: "HeightQuestionIdentifier", title: "Height", text: "Local system")
|
||||
formStep.formItems = [sectionHeaderFormItem, heightQuestionFormItem]
|
||||
|
||||
return formStep
|
||||
```
|
||||
|
||||
Active Tasks
|
||||
------------
|
||||
The height question is presented in the figure below.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
# Consent <a name="consent"></a>
|
||||
|
||||
The *ResearchKit framework* provides classes that you can customize to explain the
|
||||
details of your research study and obtain a signature if needed. Use *ResearchKit's* provided classes to quickly welcome, and inform your participants of what the study entails.
|
||||
|
||||
```swift
|
||||
import ResearchKit
|
||||
import ResearchKitUI
|
||||
|
||||
// Welcome page.
|
||||
let welcomeStep = ORKInstructionStep(identifier: String(describing: Identifier.consentWelcomeInstructionStep))
|
||||
welcomeStep.iconImage = UIImage(systemName: "hand.wave")
|
||||
welcomeStep.title = "Welcome!"
|
||||
welcomeStep.detailText = "Thank you for joining our study. Tap Next to learn more before signing up."
|
||||
|
||||
// Before You Join page.
|
||||
let beforeYouJoinStep = ORKInstructionStep(identifier: String(describing: Identifier.informedConsentInstructionStep))
|
||||
beforeYouJoinStep.iconImage = UIImage(systemName: "doc.text.magnifyingglass")
|
||||
beforeYouJoinStep.title = "Before You Join"
|
||||
|
||||
let sharingHealthDataBodyItem = ORKBodyItem(text: "The study will ask you to share some of your Health data.",
|
||||
detailText: nil,
|
||||
image: UIImage(systemName: "heart.fill"),
|
||||
learnMoreItem: nil,
|
||||
bodyItemStyle: .image)
|
||||
|
||||
let completingTasksBodyItem = ORKBodyItem(text: "You will be asked to complete various tasks over the duration of the study.",
|
||||
detailText: nil,
|
||||
image: UIImage(systemName: "checkmark.circle.fill"),
|
||||
learnMoreItem: nil,
|
||||
bodyItemStyle: .image)
|
||||
|
||||
let signatureBodyItem = ORKBodyItem(text: "Before joining, we will ask you to sign an informed consent document.",
|
||||
detailText: nil,
|
||||
image: UIImage(systemName: "signature"),
|
||||
learnMoreItem: nil,
|
||||
bodyItemStyle: .image)
|
||||
|
||||
let secureDataBodyItem = ORKBodyItem(text: "Your data is kept private and secure.",
|
||||
detailText: nil,
|
||||
image: UIImage(systemName: "lock.fill"),
|
||||
learnMoreItem: nil,
|
||||
bodyItemStyle: .image)
|
||||
|
||||
beforeYouJoinStep.bodyItems = [
|
||||
sharingHealthDataBodyItem,
|
||||
completingTasksBodyItem,
|
||||
signatureBodyItem,
|
||||
secureDataBodyItem
|
||||
]
|
||||
```
|
||||
The consent steps are presented in the figure below.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
Vist the `Obtaining Consent`article in ResearchKit's Documentation for
|
||||
more examples that include signature collection and PDF file storage.
|
||||
|
||||
# Active Tasks <a name="active-tasks"></a>
|
||||
|
||||
Some studies may need data beyond survey questions or the passive data collection capabilities
|
||||
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
|
||||
information.
|
||||
while *iPhone* sensors actively collect data.
|
||||
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
|
||||
------------
|
||||
*ResearchKit* includes a *Charts module*. It features three chart types: a *pie chart* (`ORKPieChartView`), a *line graph chart* (`ORKLineGraphChartView`), and a *discrete graph chart* (`ORKDiscreteGraphChartView`).
|
||||
|
||||
The views in the *Charts module* can be used independently of the rest of *ResearchKit*. They don't automatically connect with any other part of *ResearchKit*: the developer has to supply the data to be displayed through the views' `dataSources`, which allows for maximum flexibility.
|
||||
|
||||
|
||||
Getting Started<a name="gettingstarted"></a>
|
||||
===============
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
The primary *ResearchKit framework* codebase supports *iOS* and requires *Xcode 8.0* or newer. The
|
||||
*ResearchKit framework* has a *Base SDK* version of *8.0*, meaning that apps using the *ResearchKit
|
||||
framework* can run on devices with *iOS 8.0* or newer.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
git clone https://github.com/ResearchKit/ResearchKit.git
|
||||
```
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
Build the *ResearchKit framework* by opening `ResearchKit.xcodeproj` and running the `ResearchKit`
|
||||
framework target. Optionally, run the unit tests too.
|
||||
|
||||
|
||||
Adding the ResearchKit framework to your App
|
||||
------------------------------
|
||||
|
||||
This walk-through shows how to embed the *ResearchKit framework* in your app as a dynamic framework,
|
||||
and present a simple task view controller.
|
||||
|
||||
### 1. Add the ResearchKit framework to Your Project
|
||||
|
||||
To get started, drag `ResearchKit.xcodeproj` from your checkout into your *iOS* app project
|
||||
in *Xcode*:
|
||||
|
||||
<center>
|
||||
<figure>
|
||||
<img src="../../wiki/AddingResearchKitXcode.png" alt="Adding the ResearchKit framework to your
|
||||
project" align="middle"/>
|
||||
</figure>
|
||||
</center>
|
||||
|
||||
Then, embed the *ResearchKit framework* as a dynamic framework in your app, by adding it to the
|
||||
*Embedded Binaries* section of the *General* pane for your target as shown in the figure below.
|
||||
|
||||
<center>
|
||||
<figure>
|
||||
<img src="../../wiki/AddedBinaries.png" width="100%" alt="Adding the ResearchKit framework to
|
||||
Embedded Binaries" align="middle"/>
|
||||
<figcaption><center>Adding the ResearchKit framework to Embedded Binaries</center></figcaption>
|
||||
</figure>
|
||||
</center>
|
||||
|
||||
Note: You can also import *ResearchKit* into your project using a
|
||||
[dependency manager](./docs-standalone/dependency-management.md) such as *CocoaPods* or *Carthage*.
|
||||
|
||||
### 2. Create a Step
|
||||
|
||||
In this walk-through, we will use the *ResearchKit framework* to modally present a simple
|
||||
single-step task showing a single instruction.
|
||||
|
||||
Create a step for your task by adding some code, perhaps in `viewDidAppear:` of an existing view
|
||||
controller. To keep things simple, we'll use an instruction step (`ORKInstructionStep`) and name
|
||||
the step `myStep`.
|
||||
|
||||
*Objective-C*
|
||||
|
||||
```objc
|
||||
ORKInstructionStep *myStep =
|
||||
[[ORKInstructionStep alloc] initWithIdentifier:@"intro"];
|
||||
myStep.title = @"Welcome to ResearchKit";
|
||||
```
|
||||
|
||||
*Swift*
|
||||
Use predefined tasks provided by *ResearchKit* to guide your participants through specific actions.
|
||||
|
||||
```swift
|
||||
let myStep = ORKInstructionStep(identifier: "intro")
|
||||
myStep.title = "Welcome to ResearchKit"
|
||||
```
|
||||
import ResearchKit
|
||||
import ResearchKitUI
|
||||
import ResearchKitActiveTask
|
||||
|
||||
### 3. Create a Task
|
||||
|
||||
Use the ordered task class (`ORKOrderedTask`) to create a task that contains `myStep`. An ordered
|
||||
task is just a task where the order and selection of later steps does not depend on the results of
|
||||
earlier ones. Name your task `task` and initialize it with `myStep`.
|
||||
|
||||
*Objective-C*
|
||||
|
||||
```objc
|
||||
ORKOrderedTask *task =
|
||||
[[ORKOrderedTask alloc] initWithIdentifier:@"task" steps:@[myStep]];
|
||||
```
|
||||
|
||||
*Swift*
|
||||
|
||||
```swift
|
||||
let task = ORKOrderedTask(identifier: "task", steps: [myStep])
|
||||
```
|
||||
|
||||
### 4. Present the Task
|
||||
|
||||
Create a task view controller (`ORKTaskViewController`) and initialize it with your `task`. A task
|
||||
view controller manages a task and collects the results of each step. In this case, your task view
|
||||
controller simply displays your instruction step.
|
||||
|
||||
*Objective-C*
|
||||
|
||||
```objc
|
||||
ORKTaskViewController *taskViewController =
|
||||
[[ORKTaskViewController alloc] initWithTask:task taskRunUUID:nil];
|
||||
taskViewController.delegate = self;
|
||||
[self presentViewController:taskViewController animated:YES completion:nil];
|
||||
```
|
||||
|
||||
*Swift*
|
||||
|
||||
```swift
|
||||
let taskViewController = ORKTaskViewController(task: task, taskRun: nil)
|
||||
let orderedTask = ORKOrderedTask.dBHLToneAudiometryTask(withIdentifier: "dBHLToneAudiometryTaskIdentifier",
|
||||
intendedUseDescription: nil, options: [])
|
||||
|
||||
let taskViewController = ORKTaskViewController(task: orderedTask, taskRun: nil)
|
||||
taskViewController.delegate = self
|
||||
present(taskViewController, animated: true, completion: nil)
|
||||
|
||||
present(taskViewController, animated: true)
|
||||
```
|
||||
The dBHL Tone Audiometry task is presented in the figure below.
|
||||
|
||||
The above snippet assumes that your class implements the `ORKTaskViewControllerDelegate` protocol.
|
||||
This has just one required method, which you must implement in order to handle the completion of
|
||||
the task:
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
*Objective-C*
|
||||
# Getting Help <a name="getting-help"></a>
|
||||
|
||||
```objc
|
||||
- (void)taskViewController:(ORKTaskViewController *)taskViewController
|
||||
didFinishWithReason:(ORKTaskViewControllerFinishReason)reason
|
||||
error:(NSError *)error {
|
||||
GitHub is our primary forum for ResearchKit. Feel free to open up issues about questions, problems, or ideas.
|
||||
|
||||
ORKTaskResult *taskResult = [taskViewController result];
|
||||
// You could do something with the result here.
|
||||
# License <a name="license"></a>
|
||||
|
||||
// Then, dismiss the task view controller.
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
```
|
||||
|
||||
*Swift*
|
||||
|
||||
```swift
|
||||
func taskViewController(_ taskViewController: ORKTaskViewController,
|
||||
didFinishWith reason: ORKTaskViewControllerFinishReason,
|
||||
error: Error?) {
|
||||
let taskResult = taskViewController.result
|
||||
// You could do something with the result here.
|
||||
|
||||
// Then, dismiss the task view controller.
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
If you now run your app, you should see your first *ResearchKit framework* instruction step:
|
||||
|
||||
<center>
|
||||
<figure>
|
||||
<img src="../../wiki/HelloWorld.png" width="50%" alt="HelloWorld example screenshot" align="middle"/>
|
||||
</figure>
|
||||
</center>
|
||||
|
||||
|
||||
|
||||
What else can the ResearchKit framework do?
|
||||
-----------------------------
|
||||
|
||||
The *ResearchKit* [`ORKCatalog`](samples/ORKCatalog) sample app is a good place to start. Find the
|
||||
project in ResearchKit's [`samples`](samples) directory. This project includes a list of all the
|
||||
types of steps supported by the *ResearchKit framework* in the first tab, and displays a browser for the
|
||||
results of the last completed task in the second tab. The third tab shows some examples from the *Charts module*.
|
||||
|
||||
|
||||
|
||||
License<a name="license"></a>
|
||||
=======
|
||||
|
||||
The source in the *ResearchKit* repository is made available under the following license unless
|
||||
another license is explicitly identified:
|
||||
|
||||
```
|
||||
Copyright (c) 2015 - 2018, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
This project is made available under the terms of a BSD license. See the [LICENSE](LICENSE) file.
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
# ResearchKit Release Notes
|
||||
|
||||
## ResearchKit 3.0.1 Release Notes
|
||||
In addition to general stability and performance improvements, ResearchKit 3.0.1 includes the following updates:
|
||||
|
||||
- **ORKFormStep**
|
||||
The `ORKFormStep` has an additional property named `autoScrollEnabled` that allows developers to disable autoscrolling.
|
||||
|
||||
- **ORKBodyItem**
|
||||
The `ORKBodyItem` has an additional property named `alignImageToTop` that will align a body item's image and text to the top of each other.
|
||||
|
||||
- **ORK3DModelStep**
|
||||
An example of the `ORK3DModelStep` has been added to the ORKCatalog app.
|
||||
|
||||
|
||||
## ResearchKit 3.0 Release Notes
|
||||
|
||||
*ResearchKit 3.0* is a beta release
|
||||
|
||||
In addition to general stability and performance improvements, ResearchKit 3.0 includes the following updates.
|
||||
|
||||
### Framework Updates
|
||||
|
||||
In order to modularize ResearchKit we have separated its functionality into the following modules.
|
||||
|
||||
- **ResearchKit**
|
||||
contains the core classes and objects needed to run ResearchKit in any environment.
|
||||
|
||||
- **ResearchKitUI**
|
||||
contains the UI classes and objects needed to present ResearchKit views in an IOS environment.
|
||||
|
||||
- **ResearchKitActiveTask**
|
||||
contains the classes and objects needed to present Active Tasks in an IOS enviroment. These tasks usually require access to device sensors.
|
||||
|
||||
### Future Deprecations
|
||||
The following APIs will be labeled deprecated spring of 2025.
|
||||
|
||||
- **Consent**
|
||||
Our `ORKConsent` APIs will be deprecated in favor of using existing functionality (e.g `ORKInstructionStep` and `ORKWebViewStep`) to achieve informed consent. Please visit the `ORKCatalog` app to view a recommended example.
|
||||
|
||||
- **Question Step**
|
||||
The `ORKQuestionStep` will be deprecated in favor of using the `ORKFormStep`. Certain answer formats will present slightly different UI when used with a `ORKFormStep`. Updates will be made to ensure backwards compatibility before the question step is removed.
|
||||
|
||||
## ResearchKit 2.0 Release Notes
|
||||
|
||||
*ResearchKit 2.0* supports *iOS* and requires *Xcode 9.0* or newer.
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
<?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>
|
||||
<FileRef
|
||||
location = "group:Testing/ORKTest/ORKTest.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:samples/ORKSample/ORKSample.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ResearchKit'
|
||||
s.version = '2.0.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.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
|
||||
end
|
||||
@@ -2,7 +2,7 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.healthkit</key>
|
||||
<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,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1000"
|
||||
version = "1.3">
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -26,35 +26,21 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
|
||||
BuildableName = "ResearchKitTests.xctest"
|
||||
BlueprintName = "ResearchKitTests"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B183A4731A8535D100C76870"
|
||||
BuildableName = "ResearchKit.framework"
|
||||
BlueprintName = "ResearchKit"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:ResearchKit.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
</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 +56,6 @@
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
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>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "ORKDataCollectionTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</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 = "1340"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -14,9 +14,9 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B18FF3A41A9FE25700C0C3B0"
|
||||
BuildableName = "docs"
|
||||
BlueprintName = "docs"
|
||||
BlueprintIdentifier = "CA1C7A40288B0C68004DAB3A"
|
||||
BuildableName = "ResearchKitUI.framework"
|
||||
BlueprintName = "ResearchKitUI (iOS)"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
@@ -29,8 +29,6 @@
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -42,17 +40,6 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B18FF3A41A9FE25700C0C3B0"
|
||||
BuildableName = "docs"
|
||||
BlueprintName = "docs"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -63,9 +50,9 @@
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B18FF3A41A9FE25700C0C3B0"
|
||||
BuildableName = "docs"
|
||||
BlueprintName = "docs"
|
||||
BlueprintIdentifier = "CA1C7A40288B0C68004DAB3A"
|
||||
BuildableName = "ResearchKitUI.framework"
|
||||
BlueprintName = "ResearchKitUI (iOS)"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "A9C8689E-BBEE-4643-8C5D-DBA6A5AF01B2",
|
||||
"name" : "Configuration 1",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"skippedTests" : [
|
||||
"ORKDataCollectionTests"
|
||||
],
|
||||
"target" : {
|
||||
"containerPath" : "container:ResearchKit.xcodeproj",
|
||||
"identifier" : "86CC8E991AC09332001CCD89",
|
||||
"name" : "ResearchKitTests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKEnvironmentSPLMeterContentView.h"
|
||||
|
||||
#import "ORKRoundTappingButton.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKRingView.h"
|
||||
#import "ORKProgressView.h"
|
||||
|
||||
static const CGFloat DBLabelFontSize = 35.0;
|
||||
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterContentView {
|
||||
NSLayoutConstraint *_topToProgressViewConstraint;
|
||||
UIStackView *stackView;
|
||||
UIStackView *miniStackView;
|
||||
UILabel *_dBValueLabel;
|
||||
UILabel *_unitLabel;
|
||||
UILabel *_thresholdLabel;
|
||||
CGFloat preValue;
|
||||
CGFloat currentValue;
|
||||
CAShapeLayer *circle;
|
||||
ORKProgressView *_loadingView;
|
||||
UIProgressView *_progressView;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
preValue = -M_PI_2;
|
||||
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];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) setupDBValueLabel {
|
||||
if (!_dBValueLabel) {
|
||||
_dBValueLabel = [UILabel 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 {
|
||||
|
||||
[_progressView setProgress:progress animated:animated];
|
||||
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
|
||||
[_progressView setAlpha:(progress == 0) ? 0 : 1];
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
- (void)setProgressCircle:(CGFloat)progress {
|
||||
[_ringView setValue:progress WithColor:progress < 1.0 ? [[UIColor greenColor] colorWithAlphaComponent:0.5] : [[UIColor redColor] colorWithAlphaComponent:0.5]];
|
||||
}
|
||||
|
||||
- (void)setThreshold:(double)threshold {
|
||||
if (_thresholdLabel) {
|
||||
[_thresholdLabel setText:[NSString stringWithFormat:ORKLocalizedString(@"ENVIRONMENTSPL_THRESHOLD", nil), @(threshold)]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setDBText:(NSString *)text {
|
||||
if (_loadingView) {
|
||||
[_loadingView setHidden:YES];
|
||||
[_loadingView removeFromSuperview];
|
||||
_loadingView = nil;
|
||||
|
||||
}
|
||||
if (_dBValueLabel) {
|
||||
[_dBValueLabel setText:[NSString stringWithFormat:@"%@", text]];
|
||||
[_unitLabel setHidden:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finishStep:(ORKActiveStepViewController *)viewController {
|
||||
[super finishStep:viewController];
|
||||
}
|
||||
|
||||
- (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)setUpConstraints {
|
||||
|
||||
NSArray *constraints = @[
|
||||
|
||||
[NSLayoutConstraint constraintWithItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0 constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0
|
||||
constant:-80.0],
|
||||
[NSLayoutConstraint constraintWithItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterY
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_unitLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_unitLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:10.0],
|
||||
[NSLayoutConstraint constraintWithItem:_thresholdLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_thresholdLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:-20.0],
|
||||
[NSLayoutConstraint constraintWithItem:_loadingView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:_loadingView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_dBValueLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:5.0],
|
||||
[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_ringView
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:80.0],
|
||||
[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeLeft
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeLeft
|
||||
multiplier:1.0
|
||||
constant:5.0],
|
||||
[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeRight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeRight
|
||||
multiplier:1.0
|
||||
constant:-5.0],
|
||||
|
||||
];
|
||||
|
||||
[self addConstraints:constraints];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -1,344 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKEnvironmentSPLMeterStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKRoundTappingButton.h"
|
||||
#import "ORKEnvironmentSPLMeterContentView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKEnvironmentSPLMeterResult.h"
|
||||
#import "ORKEnvironmentSPLMeterStep.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
@interface ORKEnvironmentSPLMeterStepViewController () {
|
||||
AVAudioEngine *_audioEngine;
|
||||
AVAudioInputNode *_inputNode;
|
||||
AVAudioUnitEQ *_eqUnit;
|
||||
AVAudioFrameCount _bufferSize;
|
||||
uint32_t _sampleRate;
|
||||
AVAudioFormat *_inputNodeOutputFormat;
|
||||
int _countToFetch;
|
||||
NSMutableArray *_rmsBuffer;
|
||||
dispatch_semaphore_t _semaphoreRms;
|
||||
float _rmsData;
|
||||
float _spl;
|
||||
double _samplingInterval;
|
||||
double _thresholdValue;
|
||||
double _sensitivityOffset;
|
||||
NSInteger _requiredContiguousSamples;
|
||||
int _counter;
|
||||
NSMutableArray *_recordedSamples;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) ORKEnvironmentSPLMeterContentView *environmentSPLMeterContentView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterStepViewController
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
self = [super initWithStep:step];
|
||||
|
||||
if (self) {
|
||||
_rmsBuffer = [NSMutableArray new];
|
||||
_semaphoreRms = dispatch_semaphore_create(1);
|
||||
_rmsData = 0.0;
|
||||
_spl = 0.0;
|
||||
_counter = 0;
|
||||
_samplingInterval = 1.0;
|
||||
_requiredContiguousSamples = 1;
|
||||
_sensitivityOffset = -23.3;
|
||||
_recordedSamples = [NSMutableArray new];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initializeInternalButtonItems {
|
||||
[super initializeInternalButtonItems];
|
||||
|
||||
// Don't show next button
|
||||
self.internalContinueButtonItem = nil;
|
||||
self.internalDoneButtonItem = nil;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
|
||||
[super viewDidLoad];
|
||||
_sensitivityOffset = [self sensitivityOffsetForDevice];
|
||||
_environmentSPLMeterContentView = [ORKEnvironmentSPLMeterContentView new];
|
||||
[_environmentSPLMeterContentView setProgress:0.01 animated:YES];
|
||||
self.activeStepView.activeCustomView = _environmentSPLMeterContentView;
|
||||
[self requestMicrophoneAuthorization];
|
||||
[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];
|
||||
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
[self start];
|
||||
|
||||
_samplingInterval = [self environmentSPLMeterStep].samplingInterval;
|
||||
_requiredContiguousSamples = [self environmentSPLMeterStep].requiredContiguousSamples;
|
||||
_thresholdValue = [self environmentSPLMeterStep].thresholdValue;
|
||||
[_environmentSPLMeterContentView setThreshold:_thresholdValue];
|
||||
[self splWorkBlock];
|
||||
|
||||
}
|
||||
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
}
|
||||
|
||||
- (NSString *)deviceType {
|
||||
return [[UIDevice currentDevice] model];
|
||||
}
|
||||
|
||||
- (double)sensitivityOffsetForDevice {
|
||||
NSDictionary *lookupTable = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForResource:@"splMeter_sensitivity_offset" ofType:@"plist"]];
|
||||
NSString *deviceTypeString = [self deviceType];
|
||||
double sensitivity = [[lookupTable valueForKey:deviceTypeString] doubleValue];
|
||||
return ( sensitivity ? : _sensitivityOffset);
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *sResult = [super result];
|
||||
// "Now" is the end time of the result, which is either actually now,
|
||||
// or the last time we were in the responder chain.
|
||||
NSDate *now = sResult.endDate;
|
||||
|
||||
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
|
||||
|
||||
ORKEnvironmentSPLMeterResult *splResult = [[ORKEnvironmentSPLMeterResult alloc] initWithIdentifier:self.step.identifier];
|
||||
splResult.startDate = sResult.startDate;
|
||||
splResult.endDate = now;
|
||||
splResult.sensitivityOffset = _sensitivityOffset;
|
||||
splResult.recordedSPLMeterSamples = [_recordedSamples copy];
|
||||
|
||||
[results addObject:splResult];
|
||||
|
||||
sResult.results = [results copy];
|
||||
|
||||
return sResult;
|
||||
}
|
||||
|
||||
- (void)requestMicrophoneAuthorization {
|
||||
[[AVAudioSession sharedInstance] recordPermission];
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configureEQ {
|
||||
_eqUnit.globalGain = 0;
|
||||
|
||||
// A-weighting EQ
|
||||
AVAudioUnitEQFilterParameters *eqCoefficient = _eqUnit.bands[0];
|
||||
eqCoefficient.filterType = AVAudioUnitEQFilterTypeHighPass;
|
||||
eqCoefficient.frequency = 290;
|
||||
eqCoefficient.bypass = NO;
|
||||
|
||||
eqCoefficient = _eqUnit.bands[1];
|
||||
eqCoefficient.filterType = AVAudioUnitEQFilterTypeParametric;
|
||||
eqCoefficient.frequency = 243;
|
||||
eqCoefficient.bandwidth = 1.3882;
|
||||
eqCoefficient.gain = -4.5;
|
||||
eqCoefficient.bypass = NO;
|
||||
|
||||
eqCoefficient = _eqUnit.bands[2];
|
||||
eqCoefficient.filterType = AVAudioUnitEQFilterTypeParametric;
|
||||
eqCoefficient.frequency = 450;
|
||||
eqCoefficient.bandwidth = 0.94428;
|
||||
eqCoefficient.gain = -1.5;
|
||||
eqCoefficient.bypass = NO;
|
||||
|
||||
eqCoefficient = _eqUnit.bands[3];
|
||||
eqCoefficient.filterType = AVAudioUnitEQFilterTypeParametric;
|
||||
eqCoefficient.frequency = 2650;
|
||||
eqCoefficient.bandwidth = 2.4924;
|
||||
eqCoefficient.gain = 1.25;
|
||||
eqCoefficient.bypass = NO;
|
||||
|
||||
eqCoefficient = _eqUnit.bands[4];
|
||||
eqCoefficient.filterType = AVAudioUnitEQFilterTypeParametric;
|
||||
eqCoefficient.frequency = 10000;
|
||||
eqCoefficient.bandwidth = 1.0246;
|
||||
eqCoefficient.gain = -1.5;
|
||||
eqCoefficient.bypass = NO;
|
||||
|
||||
eqCoefficient = _eqUnit.bands[5];
|
||||
eqCoefficient.filterType = AVAudioUnitEQFilterTypeLowPass;
|
||||
eqCoefficient.frequency = 11800;
|
||||
eqCoefficient.bypass = NO;
|
||||
}
|
||||
|
||||
|
||||
- (void)splWorkBlock {
|
||||
if (!_audioEngine.isRunning && ![[AVAudioSession sharedInstance] isOtherAudioPlaying]) {
|
||||
[_eqUnit installTapOnBus:0
|
||||
bufferSize:_bufferSize
|
||||
format:_inputNodeOutputFormat
|
||||
block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
|
||||
if ([AVAudioSession sharedInstance].recordPermission == AVAudioSessionRecordPermissionGranted) {
|
||||
if (buffer.frameLength != _bufferSize) {
|
||||
_bufferSize = buffer.frameLength;
|
||||
}
|
||||
int sampleCount = _samplingInterval * _countToFetch;
|
||||
float rms = 0.0;
|
||||
for (int i = 0; i < buffer.frameLength; i++) {
|
||||
float value = [@(buffer.floatChannelData[0][i]) floatValue];
|
||||
rms += value * value;
|
||||
}
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[_rmsBuffer addObject:@(rms)];
|
||||
|
||||
// perform averaging based on capture interval
|
||||
if (_rmsBuffer.count >= sampleCount + 1) {
|
||||
float rmsSum = 0.0;
|
||||
int i = sampleCount;
|
||||
NSUInteger j = _rmsBuffer.count - 1;
|
||||
while (i>0) {
|
||||
rmsSum += [_rmsBuffer[j] floatValue];
|
||||
i --;
|
||||
j --;
|
||||
}
|
||||
_rmsData = rmsSum/_samplingInterval;
|
||||
float calValue = _sensitivityOffset;
|
||||
_spl = (20 * log10f(sqrtf(_rmsData/(float)_sampleRate))) - calValue + 94;
|
||||
[_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];
|
||||
}
|
||||
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]) {
|
||||
NSError *error = nil;
|
||||
[_audioEngine startAndReturnError:&error];
|
||||
} else {
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) evaluateThreshold: (float)spl {
|
||||
if (spl < _thresholdValue) {
|
||||
_counter += 1;
|
||||
if (_counter >= _requiredContiguousSamples) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self finish];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_counter = 0;
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.environmentSPLMeterContentView setProgress:((float)_counter/_requiredContiguousSamples) + 0.01 animated:YES];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) resetAudioSession {
|
||||
NSError *error = nil;
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionMixWithOthers error:&error];
|
||||
if (error) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
[super stepDidFinish];
|
||||
|
||||
[self.environmentSPLMeterContentView finishStep:self];
|
||||
[self resetAudioSession];
|
||||
[self goForward];
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
}
|
||||
|
||||
- (ORKEnvironmentSPLMeterStep *)environmentSPLMeterStep {
|
||||
return (ORKEnvironmentSPLMeterStep *)self.step;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "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;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKFitnessContentView
|
||||
|
||||
- (ORKActiveStepQuantityView *)distanceView {
|
||||
return _quantityPairView.leftView;
|
||||
}
|
||||
|
||||
- (ORKActiveStepQuantityView *)heartRateView {
|
||||
return _quantityPairView.rightView;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
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);
|
||||
|
||||
[self addSubview:_quantityPairView];
|
||||
[self addSubview:_imageView];
|
||||
[self addSubview:_timerLabel];
|
||||
[self setUpConstraints];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
|
||||
|
||||
[self tintColorDidChange];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)updateLengthFormatter {
|
||||
_lengthFormatter = [NSLengthFormatter new];
|
||||
_lengthFormatter.numberFormatter.maximumFractionDigits = 1;
|
||||
_lengthFormatter.numberFormatter.maximumSignificantDigits = 3;
|
||||
}
|
||||
|
||||
- (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)setTimeLeft:(NSTimeInterval)timeLeft {
|
||||
_timeLeft = timeLeft;
|
||||
[self updateTimerLabel];
|
||||
}
|
||||
|
||||
- (void)updateTimerLabel {
|
||||
static NSDateComponentsFormatter *formatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
formatter = [NSDateComponentsFormatter new];
|
||||
formatter.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
|
||||
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
|
||||
formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
|
||||
});
|
||||
|
||||
NSString *labelString = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
_timerLabel.text = labelString;
|
||||
_timerLabel.hidden = (labelString == nil);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKFitnessContentView.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKHealthQuantityTypeRecorder.h"
|
||||
#import "ORKPedometerRecorder.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@interface ORKFitnessStepViewController () <ORKHealthQuantityTypeRecorderDelegate, ORKPedometerRecorderDelegate> {
|
||||
NSInteger _intendedSteps;
|
||||
ORKFitnessContentView *_contentView;
|
||||
NSNumberFormatter *_hrFormatter;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKFitnessStepViewController
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
self = [super initWithStep:step];
|
||||
if (self) {
|
||||
self.suspendIfInactive = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (ORKFitnessStep *)fitnessStep {
|
||||
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;
|
||||
self.activeStepView.activeCustomView = _contentView;
|
||||
self.activeStepView.stepViewFillsAvailableSpace = YES;
|
||||
}
|
||||
|
||||
- (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)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)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
|
||||
_contentView.timeLeft = finished ? 0 : (timer.duration - timer.runtime);
|
||||
[super countDownTimerFired:timer finished:finished];
|
||||
}
|
||||
|
||||
#pragma mark - ORKHealthQuantityTypeRecorderDelegate
|
||||
|
||||
- (void)healthQuantityTypeRecorderDidUpdate:(ORKHealthQuantityTypeRecorder *)healthQuantityTypeRecorder {
|
||||
if ([[healthQuantityTypeRecorder.quantityType identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
|
||||
[self updateHeartRateWithQuantity:healthQuantityTypeRecorder.lastSample unit:healthQuantityTypeRecorder.unit];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - ORKPedometerRecorderDelegate
|
||||
|
||||
- (void)pedometerRecorderDidUpdate:(ORKPedometerRecorder *)pedometerRecorder {
|
||||
double distanceInMeters = pedometerRecorder.totalDistance;
|
||||
[self updateDistance:distanceInMeters];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,257 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKSpeechRecognitionContentView.h"
|
||||
#import "ORKAudioGraphView.h"
|
||||
|
||||
#import "ORKHeadlineLabel.h"
|
||||
#import "ORKSubheadlineLabel.h"
|
||||
#import "ORKLabel.h"
|
||||
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
|
||||
@interface ORKSpeechRecognitionContentView () <UITextFieldDelegate>
|
||||
|
||||
@property (nonatomic, strong) ORKHeadlineLabel *alertLabel;
|
||||
@property (nonatomic, strong) ORKAudioGraphView *graphView;
|
||||
@property (nonatomic, strong) ORKSubheadlineLabel *transcriptLabel;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKSpeechRecognitionContentView {
|
||||
NSMutableArray *_samples;
|
||||
UIColor *_keyColor;
|
||||
UIImageView *_imageView;
|
||||
UILabel *_textLabel;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[self setupTranscriptLabel];
|
||||
[self setupGraphView];
|
||||
[self setupRecordButton];
|
||||
[self setupImageView];
|
||||
[self setupTextLabel];
|
||||
[self updateGraphSamples];
|
||||
[self applyKeyColor];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupImageView {
|
||||
_imageView = [UIImageView new];
|
||||
_imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_imageView];
|
||||
}
|
||||
|
||||
- (void)setupTextLabel {
|
||||
_textLabel = [UILabel new];
|
||||
_textLabel.font = [[UIFontMetrics metricsForTextStyle:UIFontTextStyleTitle2] scaledFontForFont:[UIFont systemFontOfSize:25.0 weight:UIFontWeightMedium]];
|
||||
_textLabel.textColor = [self tintColor];
|
||||
_textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_textLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_textLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
_textLabel.numberOfLines = 0;
|
||||
_textLabel.adjustsFontForContentSizeCategory = YES;
|
||||
[self addSubview:_textLabel];
|
||||
}
|
||||
|
||||
- (void)setupGraphView {
|
||||
self.graphView = [ORKAudioGraphView new];
|
||||
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_graphView.isAccessibilityElement = YES;
|
||||
_graphView.accessibilityLabel = ORKLocalizedString(@"AX_SPEECH_RECOGNITION_WAVEFORM", nil);
|
||||
_graphView.accessibilityTraits = UIAccessibilityTraitImage;
|
||||
|
||||
[self addSubview:_graphView];
|
||||
}
|
||||
|
||||
- (void)setupTranscriptLabel {
|
||||
_transcriptLabel = [ORKSubheadlineLabel new];
|
||||
_transcriptLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_transcriptLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_transcriptLabel.text = ORKLocalizedString(@"SPEECH_RECOGNITION_TRANSCRIPTION_LABEL", nil);
|
||||
_transcriptLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
_transcriptLabel.numberOfLines = 0;
|
||||
|
||||
[self addSubview:_transcriptLabel];
|
||||
}
|
||||
|
||||
- (void)setupRecordButton {
|
||||
self.recordButton = [[ORKBorderedButton alloc] init];
|
||||
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self.recordButton setTitle:ORKLocalizedString(@"SPEECH_RECOGNITION_START_RECORD_LABEL", nil)
|
||||
forState:UIControlStateNormal];
|
||||
self.recordButton.enabled = YES;
|
||||
self.recordButton.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitStartsMediaSession;
|
||||
self.recordButton.accessibilityHint = ORKLocalizedString(@"AX_SPEECH_RECOGNITION_START_RECORDING_HINT", nil);
|
||||
[self addSubview:_recordButton];
|
||||
}
|
||||
|
||||
- (void)setSpeechRecognitionText:(NSString *)speechRecognitionText {
|
||||
_speechRecognitionText = speechRecognitionText;
|
||||
[_textLabel setText:speechRecognitionText];
|
||||
}
|
||||
|
||||
- (void)setSpeechRecognitionImage:(UIImage *)speechRecognitionImage {
|
||||
_speechRecognitionImage = speechRecognitionImage;
|
||||
[_imageView setImage:speechRecognitionImage];
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[self applyKeyColor];
|
||||
}
|
||||
|
||||
- (void)setFinished:(BOOL)finished {
|
||||
_finished = finished;
|
||||
}
|
||||
|
||||
- (void)applyKeyColor {
|
||||
UIColor *keyColor = [self keyColor];
|
||||
_graphView.keyColor = keyColor;
|
||||
}
|
||||
|
||||
- (UIColor *)keyColor {
|
||||
return _keyColor ? : [self tintColor];
|
||||
}
|
||||
|
||||
- (void)setKeyColor:(UIColor *)keyColor {
|
||||
_keyColor = keyColor;
|
||||
[self applyKeyColor];
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_imageView, _textLabel, _transcriptLabel, _graphView, _recordButton);
|
||||
const CGFloat graphHeight = 150;
|
||||
|
||||
// In case the text on the button is large, ensure that the button can grow larger than the default height if needed
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_imageView]-[_textLabel]-(5)-[_graphView(graphHeight)]-[_transcriptLabel]-buttonGap-[_recordButton(50@250)]-topBottomMargin-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:@{
|
||||
@"graphHeight": @(graphHeight),
|
||||
@"topBottomMargin" : @(5),
|
||||
@"buttonGap" : @(20)
|
||||
}
|
||||
views:views]];
|
||||
|
||||
|
||||
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
|
||||
const CGFloat twiceSideMargin = sideMargin * 2;
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_imageView]-|"
|
||||
options:0
|
||||
metrics: nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_textLabel]-|"
|
||||
options:0
|
||||
metrics: nil
|
||||
views:views]];
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
|
||||
options:0
|
||||
metrics: @{@"sideMargin": @(sideMargin)}
|
||||
views:views]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_transcriptLabel]-|"
|
||||
options:0
|
||||
metrics: @{@"sideMargin": @(sideMargin)}
|
||||
views:views]];
|
||||
|
||||
// In case the text on the button is large, ensure that the button can grow larger than the default width if needed
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-twiceSideMargin@250-[_recordButton(200@250)]-twiceSideMargin@250-|"
|
||||
options:0
|
||||
metrics: @{@"twiceSideMargin": @(twiceSideMargin)}
|
||||
views:views]];
|
||||
[constraints addObject:[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor]];
|
||||
[constraints addObject:[_recordButton.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.layoutMarginsGuide.leadingAnchor]];
|
||||
[constraints addObject:[_recordButton.trailingAnchor constraintLessThanOrEqualToAnchor:self.layoutMarginsGuide.trailingAnchor]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
- (void)setShouldHideTranscript:(BOOL)shouldHideTranscript {
|
||||
_shouldHideTranscript = shouldHideTranscript;
|
||||
if (shouldHideTranscript) {
|
||||
_transcriptLabel.text = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateGraphSamples {
|
||||
_graphView.values = _samples;
|
||||
}
|
||||
|
||||
- (void)addSample:(NSNumber *)sample {
|
||||
NSAssert(sample != nil, @"Sample should be non-nil");
|
||||
if (!_samples) {
|
||||
_samples = [NSMutableArray array];
|
||||
}
|
||||
[_samples addObject:sample];
|
||||
// Try to keep around 250 samples
|
||||
if (_samples.count > 500) {
|
||||
_samples = [[_samples subarrayWithRange:(NSRange){250, _samples.count - 250}] mutableCopy];
|
||||
}
|
||||
[self updateGraphSamples];
|
||||
}
|
||||
|
||||
- (void)updateRecognitionText:(NSString *)recognitionText {
|
||||
if (!_shouldHideTranscript) {
|
||||
_transcriptLabel.text = recognitionText;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addRecognitionError:(NSString *)errorMsg {
|
||||
_transcriptLabel.textColor = [UIColor ork_redColor];
|
||||
_transcriptLabel.text = errorMsg;
|
||||
}
|
||||
|
||||
- (void)removeAllSamples {
|
||||
_samples = nil;
|
||||
[self updateGraphSamples];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,283 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING 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;
|
||||
@import Accelerate;
|
||||
|
||||
#import "ORKSpeechRecognitionStepViewController.h"
|
||||
|
||||
#import "ORKQuestionStep.h"
|
||||
#import "ORKAnswerFormat.h"
|
||||
#import "ORKTask.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
|
||||
#import "ORKSpeechRecognitionContentView.h"
|
||||
#import "ORKStreamingAudioRecorder.h"
|
||||
#import "ORKSpeechRecognizer.h"
|
||||
#import "ORKSpeechRecognitionStep.h"
|
||||
#import "ORKSpeechRecognitionError.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
#import "ORKSpeechRecognitionResult.h"
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKTaskViewController.h"
|
||||
|
||||
#import "ORKOrderedTask.h"
|
||||
|
||||
|
||||
@interface ORKSpeechRecognitionStepViewController () <ORKStreamingAudioResultDelegate, ORKSpeechRecognitionDelegate, UITextFieldDelegate>
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKSpeechRecognitionStepViewController {
|
||||
ORKSpeechRecognitionContentView *_speechRecognitionContentView;
|
||||
ORKStreamingAudioRecorder *_audioRecorder;
|
||||
ORKSpeechRecognizer *_speechRecognizer;
|
||||
|
||||
dispatch_queue_t _speechRecognitionQueue;
|
||||
ORKSpeechRecognitonResult *_localResult;
|
||||
BOOL _errorState;
|
||||
float _peakPower;
|
||||
}
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
self = [super initWithStep:step];
|
||||
if (self) {
|
||||
self.suspendIfInactive = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
ORKSpeechRecognitionStep *step = (ORKSpeechRecognitionStep *) self.step;
|
||||
_speechRecognitionContentView = [ORKSpeechRecognitionContentView new];
|
||||
_speechRecognitionContentView.shouldHideTranscript = step.shouldHideTranscript;
|
||||
self.activeStepView.activeCustomView = _speechRecognitionContentView;
|
||||
_speechRecognitionContentView.speechRecognitionImage = step.speechRecognitionImage;
|
||||
_speechRecognitionContentView.speechRecognitionText = step.speechRecognitionText;
|
||||
|
||||
[_speechRecognitionContentView.recordButton addTarget:self
|
||||
action:@selector(recordButtonPressed:)
|
||||
forControlEvents:UIControlEventTouchDown];
|
||||
|
||||
_errorState = NO;
|
||||
|
||||
[ORKSpeechRecognizer requestAuthorization];
|
||||
|
||||
_localResult = [[ORKSpeechRecognitonResult alloc] initWithIdentifier:self.step.identifier];
|
||||
_speechRecognitionQueue = dispatch_queue_create("SpeechRecognitionQueue", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
|
||||
- (void)initializeRecognizer {
|
||||
_speechRecognizer = [[ORKSpeechRecognizer alloc] init];
|
||||
|
||||
if (_speechRecognizer) {
|
||||
[_speechRecognizer startRecognitionWithLocale:[NSLocale localeWithLocaleIdentifier:((ORKSpeechRecognitionStep *)self.step).speechRecognizerLocale] reportPartialResults:YES responseDelegate:self errorHandler:^(NSError *error) {
|
||||
if (error) {
|
||||
[self stopWithError:error];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)recordButtonPressed:(id)sender {
|
||||
if (sender == _speechRecognitionContentView.recordButton) {
|
||||
if ([_speechRecognitionContentView.recordButton.titleLabel.text
|
||||
isEqualToString:ORKLocalizedString(@"SPEECH_RECOGNITION_STOP_RECORD_LABEL", nil)]) {
|
||||
[self stopWithError:nil];
|
||||
} else {
|
||||
|
||||
[self initializeRecognizer];
|
||||
|
||||
[self start];
|
||||
[_speechRecognitionContentView.recordButton setTitle:ORKLocalizedString(@"SPEECH_RECOGNITION_STOP_RECORD_LABEL", nil)
|
||||
forState:UIControlStateNormal];
|
||||
_speechRecognitionContentView.recordButton.enabled = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)recordersDidChange {
|
||||
ORKStreamingAudioRecorder *audioRecorder = nil;
|
||||
for (ORKRecorder *recorder in self.recorders) {
|
||||
if ([recorder isKindOfClass:[ORKStreamingAudioRecorder class]]) {
|
||||
audioRecorder = (ORKStreamingAudioRecorder *)recorder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_audioRecorder = audioRecorder;
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *sResult = [super result];
|
||||
if (_speechRecognitionQueue) {
|
||||
dispatch_sync(_speechRecognitionQueue, ^{
|
||||
if (_localResult != nil) {
|
||||
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
|
||||
[results addObject:_localResult];
|
||||
sResult.results = [results copy];
|
||||
}
|
||||
});
|
||||
}
|
||||
return sResult;
|
||||
}
|
||||
|
||||
- (void)stopWithError:(NSError *)error {
|
||||
if (_speechRecognizer) {
|
||||
[_speechRecognizer endAudio];
|
||||
}
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error(@"Speech recognition failed with error message: \"%@\"", error.localizedDescription);
|
||||
[_speechRecognitionContentView addRecognitionError:error.localizedDescription];
|
||||
_speechRecognitionContentView.recordButton.enabled = NO;
|
||||
_errorState = YES;
|
||||
}
|
||||
[self stopRecorders];
|
||||
}
|
||||
|
||||
- (void)resume {
|
||||
// Background processing is not supported
|
||||
}
|
||||
|
||||
- (void)goForward {
|
||||
if ([self hasNextStep]) {
|
||||
ORKQuestionStep *nextStep = [self nextStep];
|
||||
if (nextStep) {
|
||||
[((ORKTextAnswerFormat *)nextStep.answerFormat) setDefaultTextAnswer: [_localResult.transcription formattedString]];
|
||||
}
|
||||
}
|
||||
[super goForward];
|
||||
}
|
||||
|
||||
- (nullable ORKQuestionStep *)nextStep {
|
||||
ORKOrderedTask *task = (ORKOrderedTask *)[self.taskViewController task];
|
||||
NSUInteger nextStepIndex = [task indexOfStep:[self step]] + 1;
|
||||
ORKStep *nextStep = [task steps][nextStepIndex];
|
||||
|
||||
if ([nextStep isKindOfClass:[ORKQuestionStep class]]) {
|
||||
return (ORKQuestionStep *)nextStep;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
_speechRecognitionContentView.finished = YES;
|
||||
}
|
||||
|
||||
- (void)recorder:(ORKRecorder *)recorder didFailWithError:(NSError *)error {
|
||||
[super recorder:recorder didFailWithError:error];
|
||||
[self stopWithError:error];
|
||||
}
|
||||
|
||||
// Methods running on a different thread
|
||||
|
||||
#pragma mark - ORKStreamingAudioResultDelegate
|
||||
- (void)audioAvailable:(AVAudioPCMBuffer *)buffer {
|
||||
if (_errorState) {
|
||||
return;
|
||||
}
|
||||
[_speechRecognizer addAudio:buffer];
|
||||
|
||||
// audio metering display
|
||||
float * const *channelData = [buffer floatChannelData];
|
||||
if (channelData[0]) {
|
||||
float avgValue = 0;
|
||||
unsigned long nFrames = [buffer frameLength];
|
||||
vDSP_maxmgv(channelData[0], 1 , &avgValue, nFrames);
|
||||
float lvlLowPassTrig = 0.3;
|
||||
_peakPower = lvlLowPassTrig * ((avgValue == 0)? -100 : 20* log10(avgValue)) + (1 - lvlLowPassTrig) * _peakPower;
|
||||
float clampedValue = MAX(_peakPower / 60.0, -1) + 1;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_speechRecognitionContentView addSample:@(clampedValue)];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - ORKSpeechRecognitionDelegate
|
||||
|
||||
- (void)didFinishRecognitionWithError:(NSError *)error {
|
||||
if (_errorState) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error) {
|
||||
ORK_Log_Error(@"Speech framework failed with error code: %ld, and error description: %@", (long)error.code, error.localizedDescription);
|
||||
NSError *recognitionError = [NSError errorWithDomain:ORKErrorDomain
|
||||
code:ORKSpeechRecognitionErrorRecognitionFailed
|
||||
userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"SPEECH_RECOGNITION_FAILED", nil)}];
|
||||
[self stopWithError:recognitionError];
|
||||
} else {
|
||||
[self stopWithError:nil];
|
||||
[self finish];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didHypothesizeTranscription:(SFTranscription *)transcription {
|
||||
if (_errorState) {
|
||||
return;
|
||||
}
|
||||
dispatch_sync(_speechRecognitionQueue, ^{
|
||||
_localResult.transcription = transcription;
|
||||
});
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_speechRecognitionContentView updateRecognitionText:[transcription formattedString]];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)availabilityDidChange:(BOOL)available {
|
||||
if (!available) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self stopWithError:[NSError errorWithDomain:ORKErrorDomain
|
||||
code:ORKSpeechRecognitionErrorLanguageNotAvailable
|
||||
userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"Speech recognizer not available", nil)}]];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)recordersWillStart {
|
||||
NSLog(@"Recorder is starting");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKToneAudiometryContentView.h"
|
||||
|
||||
#import "ORKRoundTappingButton.h"
|
||||
#import "ORKUnitLabel.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
@interface ORKToneAudiometryContentView ()
|
||||
|
||||
@property (nonatomic, strong) ORKUnitLabel *captionLabel;
|
||||
@property (nonatomic, strong) UIProgressView *progressView;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKToneAudiometryContentView {
|
||||
NSLayoutConstraint *_topToProgressViewConstraint;
|
||||
NSLayoutConstraint *_topToCaptionLabelConstraint;
|
||||
NSLayoutConstraint *_leftButtonToBottomConstraint;
|
||||
NSLayoutConstraint *_rightButtonToBottomConstraint;
|
||||
UILabel *_leftLabel;
|
||||
UILabel *_rightLabel;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
|
||||
_captionLabel = [ORKUnitLabel new];
|
||||
_captionLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_captionLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
_progressView = [UIProgressView new];
|
||||
_progressView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_progressView.progressTintColor = [self tintColor];
|
||||
[_progressView setAlpha:0];
|
||||
|
||||
_leftButton = [[ORKRoundTappingButton alloc] init];
|
||||
_leftButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_leftButton setTitle:ORKLocalizedString(@"TAP_BUTTON_TITLE", nil) forState:UIControlStateNormal];
|
||||
|
||||
_rightButton = [[ORKRoundTappingButton alloc] init];
|
||||
_rightButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_rightButton setTitle:ORKLocalizedString(@"TAP_BUTTON_TITLE", nil) forState: UIControlStateNormal];
|
||||
|
||||
_leftLabel = [ORKUnitLabel new];
|
||||
_rightLabel = [ORKUnitLabel new];
|
||||
|
||||
_leftLabel.text = ORKLocalizedString(@"TONE_AUDIOMETRY_LABEL_LEFT_EAR", nil);
|
||||
_rightLabel.text = ORKLocalizedString(@"TONE_AUDIOMETRY_LABEL_RIGHT_EAR", nil);
|
||||
|
||||
_leftLabel.textColor = [UIColor lightGrayColor];
|
||||
_rightLabel.textColor = [UIColor lightGrayColor];
|
||||
|
||||
_leftLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_rightLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
_leftButton.accessibilityLabel = ORKLocalizedString(@"AX_TONE_AUDIOMETRY_BUTTON_LEFT_EAR_LABEL", nil);
|
||||
_leftButton.accessibilityHint = ORKLocalizedString(@"AX_TONE_AUDIOMETRY_BUTTON_LEFT_EAR_HINT", nil);
|
||||
_rightButton.accessibilityLabel = ORKLocalizedString(@"AX_TONE_AUDIOMETRY_BUTTON_RIGHT_EAR_LABEL", nil);
|
||||
_rightButton.accessibilityHint = ORKLocalizedString(@"AX_TONE_AUDIOMETRY_BUTTON_RIGHT_EAR_HINT", nil);
|
||||
// The labels will be included in the button accessibility label, so we should not expose them additionally here.
|
||||
_leftLabel.isAccessibilityElement = NO;
|
||||
_rightLabel.isAccessibilityElement = NO;
|
||||
|
||||
[self addSubview:_captionLabel];
|
||||
[self addSubview:_progressView];
|
||||
[self addSubview:_leftButton];
|
||||
[self addSubview:_rightButton];
|
||||
[self addSubview:_leftLabel];
|
||||
[self addSubview:_rightLabel];
|
||||
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
_captionLabel.text = nil;
|
||||
[_captionLabel setHidden:YES];
|
||||
|
||||
[self setUpConstraints];
|
||||
[self updateConstraintConstantsForWindow:self.window];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)willMoveToWindow:(UIWindow *)newWindow {
|
||||
[super willMoveToWindow:newWindow];
|
||||
[self updateConstraintConstantsForWindow:newWindow];
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
self.progressView.progressTintColor = [self tintColor];
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress
|
||||
caption:(NSString *)caption
|
||||
animated:(BOOL)animated {
|
||||
self.captionLabel.text = caption;
|
||||
|
||||
[self.progressView setProgress:progress animated:animated];
|
||||
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
|
||||
[self.progressView setAlpha:(progress == 0) ? 0 : 1];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)finishStep:(ORKActiveStepViewController *)viewController {
|
||||
[super finishStep:viewController];
|
||||
self.leftButton.enabled = NO;
|
||||
self.rightButton.enabled = NO;
|
||||
}
|
||||
|
||||
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
|
||||
const CGFloat HeaderBaselineToCaptionTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToTappingLabelTop, window);
|
||||
const CGFloat AssumedHeaderBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
|
||||
static const CGFloat buttonBottomToBottom = 36.0;
|
||||
|
||||
_topToProgressViewConstraint.constant = (HeaderBaselineToCaptionTop / 3) - AssumedHeaderBaselineToStepViewTop;
|
||||
_topToCaptionLabelConstraint.constant = HeaderBaselineToCaptionTop - AssumedHeaderBaselineToStepViewTop;
|
||||
_leftButtonToBottomConstraint.constant = buttonBottomToBottom;
|
||||
_rightButtonToBottomConstraint.constant = buttonBottomToBottom;
|
||||
|
||||
}
|
||||
|
||||
- (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)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_progressView, _captionLabel, _leftButton, _rightButton, _leftLabel, _rightLabel);
|
||||
|
||||
|
||||
_topToProgressViewConstraint = [NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0]; // constant will be set in updateConstraintConstantsForWindow:
|
||||
[constraints addObject:_topToProgressViewConstraint];
|
||||
|
||||
_topToCaptionLabelConstraint = [NSLayoutConstraint constraintWithItem:_captionLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0]; // constant will be set in updateConstraintConstantsForWindow:
|
||||
[constraints addObject:_topToCaptionLabelConstraint];
|
||||
|
||||
_leftButtonToBottomConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_leftButton
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0]; // constant will be set in updateConstraintConstantsForWindow:
|
||||
|
||||
[constraints addObject:_leftButtonToBottomConstraint];
|
||||
|
||||
_rightButtonToBottomConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_rightButton
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
|
||||
[constraints addObject:_rightButtonToBottomConstraint];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_captionLabel
|
||||
attribute:NSLayoutAttributeLeft
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_leftButton
|
||||
attribute:NSLayoutAttributeLeft
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_captionLabel
|
||||
attribute:NSLayoutAttributeRight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_rightButton
|
||||
attribute:NSLayoutAttributeRight
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_leftLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_leftButton
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_rightLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_rightButton
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_captionLabel]-(>=10)-[_leftButton]"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_captionLabel]-(>=10)-[_rightButton]"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_leftButton]-(>=10)-[_leftLabel]"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_rightButton]-(>=10)-[_rightLabel]"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_progressView]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
NSLayoutConstraint *progressWidthConstraint = [NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
progressWidthConstraint.priority = UILayoutPriorityRequired - 1;
|
||||
[constraints addObject:progressWidthConstraint];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_captionLabel]-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[self addConstraints:constraints];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
[self updateConstraintConstantsForWindow:self.window];
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2015, Shazino SAS. All rights reserved.
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKdBHLToneAudiometryContentView.h"
|
||||
|
||||
#import "ORKRoundTappingButton.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
@implementation ORKdBHLToneAudiometryContentView {
|
||||
NSLayoutConstraint *_topToProgressViewConstraint;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
|
||||
_progressView = [UIProgressView new];
|
||||
_progressView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_progressView.progressTintColor = [self tintColor];
|
||||
[_progressView setAlpha:0];
|
||||
[self addSubview:_progressView];
|
||||
_tapButton = [[ORKRoundTappingButton alloc] init];
|
||||
[_tapButton setDiameter:200];
|
||||
_tapButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_tapButton setTitle:ORKLocalizedString(@"TAP_BUTTON_TITLE", nil) forState:UIControlStateNormal];
|
||||
_tapButton.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAllowsDirectInteraction;
|
||||
|
||||
[self addSubview:_tapButton];
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[self setUpConstraints];
|
||||
[self updateConstraintConstantsForWindow:self.window];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)willMoveToWindow:(UIWindow *)newWindow {
|
||||
[super willMoveToWindow:newWindow];
|
||||
[self updateConstraintConstantsForWindow:newWindow];
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
if (self.window != nil && UIAccessibilityIsVoiceOverRunning()) {
|
||||
// Ensure that VoiceOver is aware of the direct touch area so that the first tap gets registered
|
||||
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, _tapButton);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
self.progressView.progressTintColor = [self tintColor];
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress
|
||||
animated:(BOOL)animated {
|
||||
[self.progressView setProgress:progress animated:animated];
|
||||
[UIView animateWithDuration:animated ? 0.2 : 0 animations:^{
|
||||
[self.progressView setAlpha:(progress == 0) ? 0 : 1];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)finishStep:(ORKActiveStepViewController *)viewController {
|
||||
[super finishStep:viewController];
|
||||
self.tapButton.enabled = NO;
|
||||
}
|
||||
|
||||
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
|
||||
const CGFloat HeaderBaselineToCaptionTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToTappingLabelTop, window);
|
||||
const CGFloat AssumedHeaderBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
|
||||
|
||||
_topToProgressViewConstraint.constant = (HeaderBaselineToCaptionTop / 3) - AssumedHeaderBaselineToStepViewTop;
|
||||
|
||||
}
|
||||
|
||||
- (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)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_progressView, _tapButton);
|
||||
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(100)-[_tapButton]-(100)-|"
|
||||
options:NSLayoutFormatDirectionLeadingToTrailing
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_progressView]|"
|
||||
options:NSLayoutFormatDirectionLeftToRight
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[_tapButton]-(50)-|"
|
||||
options:NSLayoutFormatDirectionLeftToRight
|
||||
metrics:nil
|
||||
views:views]];
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_progressView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
[self addConstraints:constraints];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,384 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKdBHLToneAudiometryStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKdBHLToneAudiometryAudioGenerator.h"
|
||||
#import "ORKRoundTappingButton.h"
|
||||
#import "ORKdBHLToneAudiometryContentView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKdBHLToneAudiometryResult.h"
|
||||
#import "ORKdBHLToneAudiometryStep.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@interface ORKdBHLToneAudiometryTransitions: NSObject
|
||||
|
||||
@property (nonatomic, assign) float userInitiated;
|
||||
@property (nonatomic, assign) float totalTransitions;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKdBHLToneAudiometryTransitions
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_userInitiated = 1;
|
||||
_totalTransitions = 1;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKdBHLToneAudiometryStepViewController () <ORKdBHLToneAudiometryAudioGeneratorDelegate> {
|
||||
double _prevFreq;
|
||||
double _currentdBHL;
|
||||
double _dBHLStepUpSize;
|
||||
double _dBHLStepDownSize;
|
||||
int _currentTestIndex;
|
||||
int _indexOfFreqLoopList;
|
||||
int _numberOfTransitionsPerFreq;
|
||||
NSInteger _maxNumberOfTransitionsPerFreq;
|
||||
BOOL _initialDescent;
|
||||
BOOL _ackOnce;
|
||||
ORKdBHLToneAudiometryAudioGenerator *_audioGenerator;
|
||||
NSArray *_freqLoopList;
|
||||
NSMutableArray *_arrayOfResultSamples;
|
||||
NSMutableArray *_arrayOfResultUnits;
|
||||
NSMutableDictionary *_transitionsDictionary;
|
||||
UIImpactFeedbackGenerator *_hapticFeedback;
|
||||
ORKdBHLToneAudiometryFrequencySample *_resultSample;
|
||||
ORKdBHLToneAudiometryUnit *_resultUnit;
|
||||
ORKAudioChannel _audioChannel;
|
||||
dispatch_block_t _preStimulusDelayWorkBlock;
|
||||
dispatch_block_t _pulseDurationWorkBlock;
|
||||
dispatch_block_t _postStimulusDelayWorkBlock;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) ORKdBHLToneAudiometryContentView *dBHLToneAudiometryContentView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKdBHLToneAudiometryStepViewController
|
||||
|
||||
- (instancetype)initWithStep:(ORKStep *)step {
|
||||
self = [super initWithStep:step];
|
||||
|
||||
if (self) {
|
||||
self.suspendIfInactive = YES;
|
||||
_indexOfFreqLoopList = 0;
|
||||
_initialDescent = YES;
|
||||
_ackOnce = NO;
|
||||
_prevFreq = 0;
|
||||
_currentTestIndex = 0;
|
||||
_transitionsDictionary = [NSMutableDictionary dictionary];
|
||||
_arrayOfResultSamples = [NSMutableArray array];
|
||||
_arrayOfResultUnits = [NSMutableArray array];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initializeInternalButtonItems {
|
||||
[super initializeInternalButtonItems];
|
||||
|
||||
// Don't show next button
|
||||
self.internalContinueButtonItem = nil;
|
||||
self.internalDoneButtonItem = nil;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
_maxNumberOfTransitionsPerFreq = [self dBHLToneAudiometryStep].maxNumberOfTransitionsPerFrequency;
|
||||
_freqLoopList = [self dBHLToneAudiometryStep].frequencyList;
|
||||
_currentdBHL = [self dBHLToneAudiometryStep].initialdBHLValue;
|
||||
_dBHLStepDownSize = [self dBHLToneAudiometryStep].dBHLStepDownSize;
|
||||
_dBHLStepUpSize = [self dBHLToneAudiometryStep].dBHLStepUpSize;
|
||||
|
||||
self.dBHLToneAudiometryContentView = [[ORKdBHLToneAudiometryContentView alloc] init];
|
||||
self.activeStepView.activeCustomView = self.dBHLToneAudiometryContentView;
|
||||
|
||||
[self.dBHLToneAudiometryContentView.tapButton addTarget:self action:@selector(tapButtonPressed) forControlEvents:UIControlEventTouchDown];
|
||||
|
||||
_audioChannel = [self dBHLToneAudiometryStep].earPreference;
|
||||
_audioGenerator = [[ORKdBHLToneAudiometryAudioGenerator alloc] initForHeadphones:[self dBHLToneAudiometryStep].headphoneType];
|
||||
_audioGenerator.delegate = self;
|
||||
_hapticFeedback = [[UIImpactFeedbackGenerator alloc] initWithStyle: UIImpactFeedbackStyleHeavy];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
[self start];
|
||||
[self addAnimationToButton:self.dBHLToneAudiometryContentView.tapButton];
|
||||
}
|
||||
|
||||
- (void)addAnimationToButton:(UIButton *)button {
|
||||
CABasicAnimation *pulseAnimation;
|
||||
|
||||
pulseAnimation=[CABasicAnimation animationWithKeyPath:@"transform.scale.xy"];
|
||||
pulseAnimation.duration=1.0;
|
||||
pulseAnimation.repeatCount=HUGE_VALF;
|
||||
pulseAnimation.autoreverses=YES;
|
||||
pulseAnimation.fromValue=[NSNumber numberWithFloat:1.0];
|
||||
pulseAnimation.toValue=[NSNumber numberWithFloat:0.85];
|
||||
[button.layer addAnimation:pulseAnimation forKey:@"pulse"];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
if (_preStimulusDelayWorkBlock) {
|
||||
dispatch_block_cancel(_preStimulusDelayWorkBlock);
|
||||
dispatch_block_cancel(_pulseDurationWorkBlock);
|
||||
dispatch_block_cancel(_postStimulusDelayWorkBlock);
|
||||
}
|
||||
[_audioGenerator stop];
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *sResult = [super result];
|
||||
// "Now" is the end time of the result, which is either actually now,
|
||||
// or the last time we were in the responder chain.
|
||||
NSDate *now = sResult.endDate;
|
||||
|
||||
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
|
||||
|
||||
ORKdBHLToneAudiometryResult *toneResult = [[ORKdBHLToneAudiometryResult alloc] initWithIdentifier:self.step.identifier];
|
||||
toneResult.startDate = sResult.startDate;
|
||||
toneResult.endDate = now;
|
||||
toneResult.samples = [_arrayOfResultSamples copy];
|
||||
toneResult.outputVolume = [AVAudioSession sharedInstance].outputVolume;
|
||||
toneResult.headphoneType = self.dBHLToneAudiometryStep.headphoneType;
|
||||
toneResult.tonePlaybackDuration = [self dBHLToneAudiometryStep].toneDuration;
|
||||
toneResult.postStimulusDelay = [self dBHLToneAudiometryStep].postStimulusDelay;
|
||||
[results addObject:toneResult];
|
||||
|
||||
sResult.results = [results copy];
|
||||
|
||||
return sResult;
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
[super stepDidFinish];
|
||||
if (_preStimulusDelayWorkBlock) {
|
||||
dispatch_block_cancel(_preStimulusDelayWorkBlock);
|
||||
dispatch_block_cancel(_pulseDurationWorkBlock);
|
||||
dispatch_block_cancel(_postStimulusDelayWorkBlock);
|
||||
}
|
||||
[self.dBHLToneAudiometryContentView finishStep:self];
|
||||
[self goForward];
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
[self estimatedBHLAndPlayToneWithFrequency:_freqLoopList[_indexOfFreqLoopList]];
|
||||
}
|
||||
|
||||
- (void)estimatedBHLAndPlayToneWithFrequency: (NSNumber *)freq {
|
||||
[_audioGenerator stop];
|
||||
if (_preStimulusDelayWorkBlock) {
|
||||
dispatch_block_cancel(_preStimulusDelayWorkBlock);
|
||||
dispatch_block_cancel(_pulseDurationWorkBlock);
|
||||
dispatch_block_cancel(_postStimulusDelayWorkBlock);
|
||||
}
|
||||
|
||||
if (_prevFreq != [freq doubleValue]) {
|
||||
CGFloat progress = 0.001 + (CGFloat)_indexOfFreqLoopList / _freqLoopList.count;
|
||||
[self.dBHLToneAudiometryContentView setProgress:progress
|
||||
animated:YES];
|
||||
|
||||
_numberOfTransitionsPerFreq = 0;
|
||||
_currentdBHL = [self dBHLToneAudiometryStep].initialdBHLValue;
|
||||
_initialDescent = YES;
|
||||
_ackOnce = NO;
|
||||
_transitionsDictionary = nil;
|
||||
_transitionsDictionary = [NSMutableDictionary dictionary];
|
||||
if (_resultSample) {
|
||||
_resultSample.units = [_arrayOfResultUnits copy];
|
||||
}
|
||||
_arrayOfResultUnits = [NSMutableArray array];
|
||||
_prevFreq = [freq doubleValue];
|
||||
_resultSample = [ORKdBHLToneAudiometryFrequencySample new];
|
||||
_resultSample.channel = _audioChannel;
|
||||
_resultSample.frequency = [freq doubleValue];
|
||||
_resultSample.calculatedThreshold = NAN;
|
||||
[_arrayOfResultSamples addObject:_resultSample];
|
||||
} else {
|
||||
_numberOfTransitionsPerFreq += 1;
|
||||
if (_numberOfTransitionsPerFreq >= _maxNumberOfTransitionsPerFreq) {
|
||||
_indexOfFreqLoopList += 1;
|
||||
if (_indexOfFreqLoopList >= _freqLoopList.count) {
|
||||
_resultSample.units = [_arrayOfResultUnits copy];
|
||||
[self finish];
|
||||
return;
|
||||
} else {
|
||||
[self estimatedBHLAndPlayToneWithFrequency:_freqLoopList[_indexOfFreqLoopList]];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_resultUnit = [ORKdBHLToneAudiometryUnit new];
|
||||
_resultUnit.dBHLValue = _currentdBHL;
|
||||
_resultUnit.startOfUnitTimeStamp = self.runtime;
|
||||
[_arrayOfResultUnits addObject:_resultUnit];
|
||||
|
||||
ORKdBHLToneAudiometryTransitions *currentTransition = [_transitionsDictionary objectForKey:[NSNumber numberWithFloat:_currentdBHL]];
|
||||
if (!_initialDescent) {
|
||||
if (currentTransition) {
|
||||
currentTransition.userInitiated += 1;
|
||||
currentTransition.totalTransitions += 1;
|
||||
} else {
|
||||
currentTransition = [[ORKdBHLToneAudiometryTransitions alloc] init];
|
||||
[_transitionsDictionary setObject:currentTransition forKey:[NSNumber numberWithFloat:_currentdBHL]];
|
||||
}
|
||||
}
|
||||
const NSTimeInterval toneDuration = [self dBHLToneAudiometryStep].toneDuration;
|
||||
const NSTimeInterval postStimulusDelay = [self dBHLToneAudiometryStep].postStimulusDelay;
|
||||
|
||||
double delay1 = arc4random_uniform([self dBHLToneAudiometryStep].maxRandomPreStimulusDelay - 1);
|
||||
double delay2 = (double)arc4random_uniform(10)/10;
|
||||
double preStimulusDelay = delay1 + delay2 + 1;
|
||||
_resultUnit.preStimulusDelay = preStimulusDelay;
|
||||
|
||||
_preStimulusDelayWorkBlock = dispatch_block_create(0, ^{
|
||||
[_audioGenerator playSoundAtFrequency:[freq floatValue] onChannel:_audioChannel dBHL:_currentdBHL];
|
||||
});
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(preStimulusDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), _preStimulusDelayWorkBlock);
|
||||
|
||||
_pulseDurationWorkBlock = dispatch_block_create(0, ^{
|
||||
[_audioGenerator stop];
|
||||
});
|
||||
// adding 0.2 seconds to account for the fadeInDuration which is being set in ORKdBHLToneAudiometryAudioGenerator
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((preStimulusDelay + toneDuration + 0.2) * NSEC_PER_SEC)), dispatch_get_main_queue(), _pulseDurationWorkBlock);
|
||||
|
||||
ORKWeakTypeOf(self)weakSelf = self;
|
||||
_postStimulusDelayWorkBlock = dispatch_block_create(0, ^{
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
NSUInteger storedTestIndex = _currentTestIndex;
|
||||
if (_currentTestIndex == storedTestIndex) {
|
||||
if (_initialDescent && _ackOnce) {
|
||||
_initialDescent = NO;
|
||||
ORKdBHLToneAudiometryTransitions *newTransition = [[ORKdBHLToneAudiometryTransitions alloc] init];
|
||||
newTransition.userInitiated -= 1;
|
||||
[_transitionsDictionary setObject:newTransition forKey:[NSNumber numberWithFloat:_currentdBHL]];
|
||||
}
|
||||
|
||||
_currentdBHL = _currentdBHL + _dBHLStepUpSize;
|
||||
|
||||
if (currentTransition) {
|
||||
currentTransition.userInitiated -= 1;
|
||||
}
|
||||
_resultUnit.timeoutTimeStamp = self.runtime;
|
||||
_currentTestIndex += 1;
|
||||
[strongSelf estimatedBHLAndPlayToneWithFrequency:_freqLoopList[_indexOfFreqLoopList]];
|
||||
return;
|
||||
}
|
||||
});
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((preStimulusDelay + toneDuration + postStimulusDelay) * NSEC_PER_SEC)), dispatch_get_main_queue(), _postStimulusDelayWorkBlock);
|
||||
|
||||
}
|
||||
|
||||
- (void)tapButtonPressed {
|
||||
_ackOnce = YES;
|
||||
[_hapticFeedback impactOccurred];
|
||||
_currentTestIndex += 1;
|
||||
_resultUnit.userTapTimeStamp = self.runtime;
|
||||
[_audioGenerator stop];
|
||||
if (_preStimulusDelayWorkBlock) {
|
||||
dispatch_block_cancel(_preStimulusDelayWorkBlock);
|
||||
dispatch_block_cancel(_pulseDurationWorkBlock);
|
||||
dispatch_block_cancel(_postStimulusDelayWorkBlock);
|
||||
}
|
||||
if ([self validateResultFordBHL:_currentdBHL]) {
|
||||
_resultSample.calculatedThreshold = _currentdBHL;
|
||||
_indexOfFreqLoopList += 1;
|
||||
if (_indexOfFreqLoopList >= _freqLoopList.count) {
|
||||
_resultSample.units = [_arrayOfResultUnits copy];
|
||||
[self finish];
|
||||
return;
|
||||
} else {
|
||||
_currentdBHL = [self dBHLToneAudiometryStep].initialdBHLValue;
|
||||
[self estimatedBHLAndPlayToneWithFrequency:_freqLoopList[_indexOfFreqLoopList]];
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_currentdBHL = _currentdBHL - _dBHLStepDownSize;
|
||||
[self estimatedBHLAndPlayToneWithFrequency:_freqLoopList[_indexOfFreqLoopList]];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)validateResultFordBHL:(float)dBHL {
|
||||
NSNumber *currentKey = [NSNumber numberWithFloat:_currentdBHL];
|
||||
ORKdBHLToneAudiometryTransitions *currentTransitionObject = [_transitionsDictionary objectForKey:currentKey];
|
||||
if ((currentTransitionObject.userInitiated/currentTransitionObject.totalTransitions >= 0.5) && currentTransitionObject.totalTransitions >= 2) {
|
||||
ORKdBHLToneAudiometryTransitions *previousTransitionObject = [_transitionsDictionary objectForKey:[NSNumber numberWithFloat:(dBHL - _dBHLStepUpSize)]];
|
||||
if ((previousTransitionObject.userInitiated/previousTransitionObject.totalTransitions <= 0.5) && (previousTransitionObject.totalTransitions >= 2)) {
|
||||
if (currentTransitionObject.totalTransitions == 2) {
|
||||
if (currentTransitionObject.userInitiated/currentTransitionObject.totalTransitions == 1.0) {
|
||||
_resultSample.calculatedThreshold = dBHL;
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
} else {
|
||||
_resultSample.calculatedThreshold = dBHL;
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)toneWillStartClipping {
|
||||
_indexOfFreqLoopList += 1;
|
||||
if (_indexOfFreqLoopList >= _freqLoopList.count) {
|
||||
_resultSample.units = [_arrayOfResultUnits copy];
|
||||
[self finish];
|
||||
} else {
|
||||
[self estimatedBHLAndPlayToneWithFrequency:_freqLoopList[_indexOfFreqLoopList]];
|
||||
}
|
||||
}
|
||||
|
||||
- (ORKdBHLToneAudiometryStep *)dBHLToneAudiometryStep {
|
||||
return (ORKdBHLToneAudiometryStep *)self.step;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Face.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Face@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Face@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "guideAreaCorners.pdf",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "airpods@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "airpods_3rd_gen@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods_3rd_gen@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods_3rd_gen@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 998 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "airpods_max@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods_max@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods_max@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "airpods_pro@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods_pro@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "airpods_pro@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,27 +1,18 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"filename" : "amslerGrid@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"filename" : "amslerGrid@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"filename" : "amslerGrid@2x~ipad.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 23 KiB |
@@ -2,15 +2,16 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "audiometry.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "audiometry@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "audiometry@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "earpods@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "earpods@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "earpods@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 998 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Tone-Audiometry-Headphones.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Tone-Audiometry-Headphones@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Tone-Audiometry-Headphones@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,42 +1,18 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"filename" : "heart-fitness@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"filename" : "heart-fitness@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"filename" : "heart-fitness@2x~ipad.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 604 B |
|
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 17 KiB |
@@ -1,42 +1,18 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"filename" : "heartbeat@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"filename" : "heartbeat@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"filename" : "heartbeat@2x~ipad.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,42 +1,18 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"filename" : "holepegtest1@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"filename" : "holepegtest1@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"filename" : "holepegtest1@2x~ipad.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 122 KiB |