Compare commits
8 Commits
2.2.12-dev
...
3.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| c71d1a56dd | |||
| 6c24a7e922 | |||
| b14e5cfcb0 | |||
| fee76c2d93 | |||
| a07041901b | |||
| c370011616 | |||
| 1b9daf60ee | |||
| e71d71cf9c |
@@ -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 the [*ResearchKit* Forum](https://forums.developer.apple.com/community/researchkit) 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 the [*ResearchKit* Forum](https://forums.developer.apple.com/community/researchkit).
|
||||
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:
|
||||
|
||||
@@ -6,293 +6,170 @@ ResearchKit Framework
|
||||
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](docs/)
|
||||
* [Best Practices](../../wiki/best-practices)
|
||||
* [Contributing to ResearchKit](CONTRIBUTING.md)
|
||||
* [Installing](#installation)
|
||||
* [ORKCatalog App](#orkcatalog-app)
|
||||
* [Surveys](#surveys)
|
||||
* [Consent](#consent)
|
||||
* [Active Tasks](#active-tasks)
|
||||
* [Getting Help](#getting-help)
|
||||
* [License](#license)
|
||||
|
||||
# Requirements <a name="requirements"></a>
|
||||
|
||||
The *ResearchKit framework* codebase supports iOS and requires Xcode 12.0 or newer. The *ResearchKit framework* has a Base SDK version of 13.0.
|
||||
|
||||
# Documentation <a name="documentation"></a>
|
||||
|
||||
<img width="1000" alt="ebedded-framework" src="https://github.com/ResearchKit/ResearchKit/assets/29615893/19d6edd3-3d95-4416-9ac4-24ccb35e09c2">
|
||||
|
||||
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)
|
||||
* [ResearchKit BSD License](#license)
|
||||
* [WWDC: ResearchKit and CareKit Reimagined](https://developer.apple.com/videos/play/wwdc2019/217/)
|
||||
|
||||
Getting More Information
|
||||
========================
|
||||
|
||||
* Join the [*ResearchKit* Forum](https://developer.apple.com/forums/tags/researchkit) for discussing uses of the *ResearchKit framework and* related projects.
|
||||
### Install as an embedded framework <a name="installation"></a>
|
||||
|
||||
Use Cases
|
||||
===========
|
||||
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.
|
||||
|
||||
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/7479f313-ecc7-4d94-8c64-c58ae7362a4d">
|
||||
|
||||
Surveys
|
||||
-------
|
||||
### 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](docs/Survey/)* 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](docs/InformedConsent/)* 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](docs/ActiveTasks/)* 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 `main` branch:
|
||||
|
||||
```
|
||||
git clone https://github.com/ResearchKit/ResearchKit.git
|
||||
```
|
||||
|
||||
CocoaPods Installation
|
||||
------------
|
||||
For latest stable release
|
||||
|
||||
```
|
||||
pod 'ResearchKit'
|
||||
```
|
||||
|
||||
For early development releases
|
||||
|
||||
```
|
||||
pod 'ResearchKit', :git => 'https://github.com/ResearchKit/ResearchKit.git', :branch => 'main'
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<FileRef
|
||||
location = "group:ResearchKit.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Testing/ORKTest/ORKTest.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
|
||||
</FileRef>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ResearchKit'
|
||||
s.version = '2.1.0'
|
||||
s.summary = 'ResearchKit is an open source software framework that makes it easy to create apps for medical research or for other research projects.'
|
||||
s.homepage = 'https://www.github.com/ResearchKit/ResearchKit'
|
||||
s.documentation_url = 'http://researchkit.github.io/docs/'
|
||||
s.license = { :type => 'BSD', :file => 'LICENSE' }
|
||||
s.author = { 'researchkit.org' => 'http://researchkit.org' }
|
||||
s.source = { :git => 'https://github.com/ResearchKit/ResearchKit.git', :tag => s.version.to_s }
|
||||
s.public_header_files = `./scripts/find_headers.rb --public`.split("\n")
|
||||
s.private_header_files = `./scripts/find_headers.rb --private`.split("\n")
|
||||
s.source_files = 'ResearchKit/**/*.{h,m,swift}'
|
||||
s.resources = 'ResearchKit/**/*.{fsh,vsh}', 'ResearchKit/Animations/**/*.m4v', 'ResearchKit/Artwork.xcassets', 'ResearchKit/Localized/*.lproj'
|
||||
s.platform = :ios, '11.0'
|
||||
s.requires_arc = true
|
||||
s.swift_version = '5'
|
||||
s.module_map = "ResearchKit/ResearchKit.modulemap"
|
||||
end
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1310"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "NO">
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -20,20 +20,6 @@
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
|
||||
BuildableName = "ResearchKitTests.xctest"
|
||||
BlueprintName = "ResearchKitTests"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1310"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -50,6 +50,11 @@
|
||||
BlueprintName = "ResearchKitTests"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "ORKDataCollectionTests">
|
||||
</Test>
|
||||
</SkippedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1310"
|
||||
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>
|
||||
@@ -40,15 +40,6 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B18FF3A41A9FE25700C0C3B0"
|
||||
BuildableName = "docs"
|
||||
BlueprintName = "docs"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -59,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>
|
||||
@@ -13,6 +13,9 @@
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"skippedTests" : [
|
||||
"ORKDataCollectionTests"
|
||||
],
|
||||
"target" : {
|
||||
"containerPath" : "container:ResearchKit.xcodeproj",
|
||||
"identifier" : "86CC8E991AC09332001CCD89",
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
public enum CircleSliderOption {
|
||||
case startAngle(Double)
|
||||
case barColor(UIColor)
|
||||
case trackingColor(UIColor)
|
||||
case thumbColor(UIColor)
|
||||
case thumbImage(UIImage)
|
||||
case barWidth(CGFloat)
|
||||
case thumbWidth(CGFloat)
|
||||
case maxValue(Float)
|
||||
case minValue(Float)
|
||||
case sliderEnabled(Bool)
|
||||
case viewInset(CGFloat)
|
||||
case minMaxSwitchTreshold(Float)
|
||||
}
|
||||
|
||||
open class CircleSlider: UISlider {
|
||||
|
||||
private let minThumbTouchAreaWidth: CGFloat = 44
|
||||
private var latestDegree: Double = 0
|
||||
private var startValue: Float = 0
|
||||
open var sliderValue: Float {
|
||||
get {
|
||||
return startValue
|
||||
}
|
||||
set {
|
||||
var value = newValue
|
||||
let significantChange = (maxValue - minValue) * (1.0 - minMaxSwitchTreshold)
|
||||
let isSignificantChangeOccured = abs(newValue - startValue) > significantChange
|
||||
|
||||
if isSignificantChangeOccured {
|
||||
if startValue < newValue {
|
||||
value = minValue
|
||||
} else {
|
||||
value = maxValue
|
||||
}
|
||||
} else {
|
||||
value = newValue
|
||||
}
|
||||
|
||||
startValue = value
|
||||
sendActions(for: .valueChanged)
|
||||
var degree = Math.degreeFromValue(startAngle, value: sliderValue, maxValue: maxValue, minValue: minValue)
|
||||
|
||||
if startValue == maxValue {
|
||||
degree -= degree / (360 * 100)
|
||||
}
|
||||
|
||||
layout(degree)
|
||||
}
|
||||
}
|
||||
private var trackLayer: TrackLayer! {
|
||||
didSet {
|
||||
layer.addSublayer(trackLayer)
|
||||
}
|
||||
}
|
||||
private var thumbView: UIView! {
|
||||
didSet {
|
||||
if sliderEnabled {
|
||||
thumbView.backgroundColor = thumbColor
|
||||
thumbView.center = thumbCenter(startAngle)
|
||||
thumbView.layer.cornerRadius = thumbView!.bounds.size.width * 0.5
|
||||
addSubview(thumbView)
|
||||
if let thumbImage = thumbImage {
|
||||
let thumbImageView = UIImageView(frame: thumbView.bounds)
|
||||
thumbImageView.image = thumbImage
|
||||
thumbView.addSubview(thumbImageView)
|
||||
thumbView.backgroundColor = UIColor.clear
|
||||
}
|
||||
} else {
|
||||
thumbView.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var startAngle: Double = -90
|
||||
private var barColor = UIColor.lightGray
|
||||
private var trackingColor = UIColor.blue
|
||||
private var thumbColor = UIColor.black
|
||||
private var barWidth: CGFloat = 20
|
||||
private var maxValue: Float = 101
|
||||
private var minValue: Float = 0
|
||||
private var sliderEnabled = true
|
||||
private var viewInset: CGFloat = 20
|
||||
private var minMaxSwitchTreshold: Float = 0.0
|
||||
private var thumbImage: UIImage?
|
||||
private var _thumbWidth: CGFloat?
|
||||
private var thumbWidth: CGFloat {
|
||||
get {
|
||||
if let retValue = _thumbWidth {
|
||||
return retValue
|
||||
}
|
||||
|
||||
return (thumbImage?.size.height)!
|
||||
}
|
||||
set {
|
||||
_thumbWidth = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override open func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
backgroundColor = UIColor.clear
|
||||
}
|
||||
|
||||
public init(frame: CGRect, options: [CircleSliderOption]?) {
|
||||
super.init(frame: frame)
|
||||
if let options = options {
|
||||
build(options)
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapHandle(sender:)))
|
||||
tapGesture.numberOfTouchesRequired = 1
|
||||
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(tapHandle(sender:)))
|
||||
addGestureRecognizer(tapGesture)
|
||||
addGestureRecognizer(panGesture)
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override open func layoutSublayers(of layer: CALayer) {
|
||||
if trackLayer == nil {
|
||||
trackLayer = TrackLayer(bounds: bounds.insetBy(dx: viewInset, dy: viewInset), setting: createLayerSetting())
|
||||
}
|
||||
if thumbView == nil {
|
||||
if let image = thumbImage {
|
||||
thumbView = UIView(frame: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
|
||||
} else {
|
||||
thumbView = UIView(frame: CGRect(x: 0, y: 0, width: thumbWidth, height: thumbWidth))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !sliderEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
var bounds = self.bounds
|
||||
bounds = bounds.insetBy(dx: 100.0, dy: 100.0)
|
||||
return bounds.contains(point)
|
||||
}
|
||||
|
||||
override open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
let degree = Math.pointPairToBearingDegrees(center, endPoint: touch.location(in: self))
|
||||
latestDegree = degree
|
||||
layout(degree)
|
||||
let value = Float(Math.adjustValue(startAngle, degree: degree, maxValue: maxValue, minValue: minValue))
|
||||
thumbView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(degree)))
|
||||
sliderValue = value
|
||||
return true
|
||||
}
|
||||
|
||||
@objc
|
||||
func tapHandle(sender: UIGestureRecognizer) {
|
||||
if isUserInteractionEnabled {
|
||||
let degree = Math.pointPairToBearingDegrees(center, endPoint: sender.location(in: self))
|
||||
latestDegree = degree
|
||||
layout(degree)
|
||||
let value = Float(Math.adjustValue(startAngle, degree: degree, maxValue: maxValue, minValue: minValue))
|
||||
thumbView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(degree)))
|
||||
sliderValue = value
|
||||
}
|
||||
}
|
||||
|
||||
open func changeOptions(_ options: [CircleSliderOption]) {
|
||||
build(options)
|
||||
redraw()
|
||||
}
|
||||
|
||||
private func redraw() {
|
||||
|
||||
if trackLayer != nil {
|
||||
trackLayer.removeFromSuperlayer()
|
||||
}
|
||||
trackLayer = TrackLayer(bounds: bounds.insetBy(dx: viewInset, dy: viewInset), setting: createLayerSetting())
|
||||
if thumbView != nil {
|
||||
thumbView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if let image = thumbImage {
|
||||
thumbView = UIView(frame: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
|
||||
} else {
|
||||
thumbView = UIView(frame: CGRect(x: 0, y: 0, width: thumbWidth, height: thumbWidth))
|
||||
}
|
||||
|
||||
self.layout(self.latestDegree)
|
||||
}
|
||||
|
||||
func build(_ options: [CircleSliderOption]) {
|
||||
for option in options {
|
||||
switch option {
|
||||
case let .startAngle(value):
|
||||
startAngle = value
|
||||
latestDegree = startAngle
|
||||
case let .barColor(value):
|
||||
barColor = value
|
||||
case let .trackingColor(value):
|
||||
trackingColor = value
|
||||
case let .thumbColor(value):
|
||||
thumbColor = value
|
||||
case let .barWidth(value):
|
||||
barWidth = value
|
||||
case let .thumbWidth(value):
|
||||
thumbWidth = value
|
||||
case let .maxValue(value):
|
||||
maxValue = value
|
||||
maxValue += 1
|
||||
case let .minValue(value):
|
||||
minValue = value
|
||||
startValue = minValue
|
||||
case let .sliderEnabled(value):
|
||||
sliderEnabled = value
|
||||
case let .viewInset(value):
|
||||
viewInset = value
|
||||
case let .minMaxSwitchTreshold(value):
|
||||
minMaxSwitchTreshold = value
|
||||
case let .thumbImage(value):
|
||||
thumbImage = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func layout(_ degree: Double) {
|
||||
if let trackLayer = trackLayer, let thumbView = self.thumbView {
|
||||
trackLayer.degree = degree
|
||||
thumbView.center = thumbCenter(degree)
|
||||
thumbView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(degree)))
|
||||
trackLayer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
private func createLayerSetting() -> TrackLayer.Setting {
|
||||
var setting = TrackLayer.Setting()
|
||||
setting.startAngle = startAngle
|
||||
setting.barColor = barColor
|
||||
setting.trackingColor = trackingColor
|
||||
setting.barWidth = barWidth
|
||||
return setting
|
||||
}
|
||||
|
||||
private func thumbCenter(_ degree: Double) -> CGPoint {
|
||||
let radius = (bounds.insetBy(dx: viewInset, dy: viewInset).width * 0.5) - (barWidth * 0.5) + 5
|
||||
return Math.pointFromAngle(frame, angle: degree, radius: Double(radius))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
public enum DeviceType: String {
|
||||
|
||||
case iPhone5 = "iPhone5"
|
||||
case iPhone5C = "iPhone5C"
|
||||
case iPhone5S = "iPhone5S"
|
||||
case iPhone6Plus = "iPhone6Plus"
|
||||
case iPhone6 = "iPhone6"
|
||||
case iPhone6S = "iPhone6S"
|
||||
case iPhone6SPlus = "iPhone6SPlus"
|
||||
case iPhone7 = "iPhone7"
|
||||
case iPhone7Plus = "iPhone7Plus"
|
||||
case iPhoneSE = "iPhoneSE"
|
||||
|
||||
case IPodTouch5 = "iPod5,1"
|
||||
case IPodTouch6 = "iPod7,1"
|
||||
}
|
||||
|
||||
func parseDeviceType(_ identifier: String) -> DeviceType {
|
||||
|
||||
switch identifier {
|
||||
case "iPhone5,1", "iPhone5,2": return .iPhone5
|
||||
case "iPhone5,3", "iPhone5,4": return .iPhone5C
|
||||
case "iPhone6,1", "iPhone6,2": return .iPhone5S
|
||||
case "iPhone7,1": return .iPhone6Plus
|
||||
case "iPhone7,2": return .iPhone6
|
||||
case "iPhone8,2": return .iPhone6SPlus
|
||||
case "iPhone8,1": return .iPhone6S
|
||||
case "iPhone9,1", "iPhone9,3": return .iPhone7
|
||||
case "iPhone9,2", "iPhone9,4": return .iPhone7Plus
|
||||
case "iPhone8,4": return .iPhoneSE
|
||||
|
||||
case "iPod5,1": return .IPodTouch5
|
||||
case "iPod7,1": return .IPodTouch6
|
||||
|
||||
default:
|
||||
if UIDevice.iPhonePlus {
|
||||
return .iPhone7Plus
|
||||
} else {
|
||||
return .iPhone7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pixelPerInchIphonePlus: CGFloat = 401
|
||||
|
||||
var pixelPerInchIphone: CGFloat = 326
|
||||
|
||||
var inchPerMm: CGFloat = 25.4
|
||||
|
||||
var renderedPixels: CGFloat = 1.15
|
||||
|
||||
func parsePixelPerInch(deviceType: DeviceType) -> CGFloat {
|
||||
|
||||
switch deviceType {
|
||||
case .iPhone5, .iPhone5C, .iPhone5S, .iPhoneSE, .iPhone6, .iPhone6S, .iPhone7, .IPodTouch5, .IPodTouch6: return pixelPerInchIphone
|
||||
case .iPhone6Plus, .iPhone6SPlus, .iPhone7Plus: return pixelPerInchIphonePlus
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIDevice {
|
||||
|
||||
class var deviceType: DeviceType {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
|
||||
let machine = systemInfo.machine
|
||||
let mirror = Mirror(reflecting: machine)
|
||||
var identifier = ""
|
||||
|
||||
for child in mirror.children {
|
||||
if let value = child.value as? Int8, value != 0 {
|
||||
identifier.append(String(UnicodeScalar(UInt8(value))))
|
||||
}
|
||||
}
|
||||
|
||||
return parseDeviceType(identifier)
|
||||
}
|
||||
|
||||
class var pixelsPerMm: CGFloat {
|
||||
return parsePixelPerInch(deviceType: UIDevice.deviceType) / inchPerMm
|
||||
}
|
||||
|
||||
class var iPhonePlus: Bool {
|
||||
if UIDevice.current.userInterfaceIdiom != .phone {
|
||||
return false
|
||||
}
|
||||
|
||||
if UIScreen.main.scale > 2.9 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
internal class EyeActivitySlider: UIView {
|
||||
|
||||
private var testType: VisionStepType?
|
||||
|
||||
private var incorrectAnswers = 0
|
||||
private let contentGap: CGFloat = 20.0
|
||||
private let toleranceAngle = 22.5
|
||||
private let letterAngles = [0, 45, 90, 135, 180, 225, 270, 315]
|
||||
private var letterSize: CGFloat {
|
||||
var letterSize: CGFloat!
|
||||
|
||||
if self.testType == .visualAcuity {
|
||||
letterSize = letterMmSizes[currentStep] * UIDevice.pixelsPerMm / UIScreen.main.nativeScale
|
||||
} else {
|
||||
letterSize = 20 * UIDevice.pixelsPerMm / UIScreen.main.nativeScale
|
||||
}
|
||||
|
||||
return letterSize
|
||||
}
|
||||
|
||||
private var currentStep = 0
|
||||
private var letterMmSizes: [CGFloat] = [5.82, 4.65, 3.72, 2.91, 2.33, 1.86, 1.45, 1.16, 0.93, 0.73, 0.58, 0.47, 0.37]
|
||||
private var contrastLevels: [CGFloat] = [0.9, 0.92, 0.937, 0.95, 0.96, 0.968, 0.975, 0.98, 0.984, 0.9875, 0.99]
|
||||
private var stepScores: [Int] = [50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110]
|
||||
private var letterAngle = 0.0
|
||||
|
||||
private lazy var letterImageView: UIImageView = {
|
||||
let letterImage = UIImage(named: "iCNLandoltC",
|
||||
in: Bundle(for: type(of: self)),
|
||||
compatibleWith: nil)
|
||||
let imageView = UIImageView(image: letterImage!)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var circleImageView: UIImageView = {
|
||||
let circleImage = UIImage(named: "orangeGrayCircle",
|
||||
in: Bundle(for: type(of: self)),
|
||||
compatibleWith: nil)
|
||||
return UIImageView(image: circleImage!)
|
||||
}()
|
||||
|
||||
private var slider: CircleSlider?
|
||||
|
||||
internal init(testType: VisionStepType) {
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.testType = testType
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required internal init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
addSubview(letterImageView)
|
||||
|
||||
circleImageView.contentMode = .scaleAspectFit
|
||||
addSubview(circleImageView)
|
||||
|
||||
let thumbImage = UIImage(named: "iCNDialPointerWithShadow",
|
||||
in: Bundle(for: type(of: self)),
|
||||
compatibleWith: nil)
|
||||
|
||||
slider = CircleSlider(frame: bounds, options: [
|
||||
CircleSliderOption.barColor(UIColor.clear),
|
||||
CircleSliderOption.trackingColor(UIColor.clear),
|
||||
CircleSliderOption.startAngle(0),
|
||||
CircleSliderOption.maxValue(360),
|
||||
CircleSliderOption.minValue(0),
|
||||
CircleSliderOption.thumbImage(thumbImage!)
|
||||
])
|
||||
|
||||
addSubview(slider!)
|
||||
updateSliderAndLetter()
|
||||
}
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
letterAngle = Double(letterAngles[Int(arc4random_uniform(7))])
|
||||
letterImageView.transform = CGAffineTransform.identity
|
||||
letterImageView.frame = CGRect(origin: CGPoint(), size: CGSize(width: letterSize, height: letterSize))
|
||||
letterImageView.center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
|
||||
letterImageView.transform = CGAffineTransform(rotationAngle: CGFloat(Math.degreesToRadians(letterAngle)))
|
||||
letterImageView.alpha = getAlpha()
|
||||
slider?.frame = bounds
|
||||
|
||||
var frame = contentFrame()
|
||||
circleImageView.frame = frame
|
||||
|
||||
let labelMargin: CGFloat = 30.0
|
||||
frame.origin.x += labelMargin
|
||||
frame.origin.y += labelMargin
|
||||
frame.size.width -= labelMargin * 2
|
||||
frame.size.height -= labelMargin * 2
|
||||
}
|
||||
|
||||
private func updateSliderAndLetter() {
|
||||
guard incorrectAnswers < 2 else { return }
|
||||
|
||||
letterImageView.isHidden = false
|
||||
slider?.sliderValue = 0
|
||||
slider?.isUserInteractionEnabled = true
|
||||
slider?.isHidden = false
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
private func contentFrame() -> CGRect {
|
||||
let sideLength = min(bounds.size.width, bounds.size.height) - contentGap
|
||||
let contentFrame = CGRect(x: (bounds.size.width - sideLength) / 2, y: (bounds.size.height - sideLength) / 2, width: sideLength, height: sideLength)
|
||||
return contentFrame
|
||||
}
|
||||
|
||||
private func getAlpha() -> CGFloat {
|
||||
return testType == .visualAcuity ? 1.0 : (1 - contrastLevels[currentStep])
|
||||
}
|
||||
|
||||
private func getResult() -> Bool {
|
||||
let sliderValue = Double((slider?.sliderValue)!)
|
||||
let leftMargin = letterAngle - toleranceAngle
|
||||
let rightMargin = letterAngle + toleranceAngle
|
||||
let result = sliderValue > leftMargin && sliderValue < rightMargin
|
||||
|
||||
if result == false {
|
||||
incorrectAnswers += 1
|
||||
} else {
|
||||
currentStep += 1
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
internal func hideLetter() {
|
||||
letterImageView.isHidden = true
|
||||
}
|
||||
|
||||
internal func fetchResultDataAndUpdateSlider() -> (outcome: Bool, letterAngle: Double, sliderAngle: Double, score: Int, incorrectAnswers: Int, maxScore: Int) {
|
||||
let outcome = getResult()
|
||||
let score = stepScores[currentStep]
|
||||
let currentSliderValue = Double((slider?.sliderValue)!)
|
||||
let currentLetterAngle = letterAngle
|
||||
let maxScore = testType == .visualAcuity ? stepScores.last! : stepScores[contrastLevels.count - 1]
|
||||
updateSliderAndLetter()
|
||||
|
||||
return (outcome, currentLetterAngle, currentSliderValue, score, incorrectAnswers, maxScore)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
internal class Math {
|
||||
|
||||
internal class func degreesToRadians(_ angle: Double) -> Double {
|
||||
return angle / 180 * .pi
|
||||
}
|
||||
|
||||
internal class func pointFromAngle(_ frame: CGRect, angle: Double, radius: Double) -> CGPoint {
|
||||
let radian = degreesToRadians(angle)
|
||||
let xPoint = Double(frame.midX) + cos(radian) * radius
|
||||
let yPoint = Double(frame.midY) + sin(radian) * radius
|
||||
return CGPoint(x: xPoint, y: yPoint)
|
||||
}
|
||||
|
||||
internal class func pointPairToBearingDegrees(_ startPoint: CGPoint, endPoint: CGPoint) -> Double {
|
||||
let originPoint = CGPoint(x: endPoint.x - startPoint.x, y: endPoint.y - startPoint.y)
|
||||
let bearingRadians = atan2(Double(originPoint.y), Double(originPoint.x))
|
||||
var bearingDegrees = bearingRadians * (180.0 / .pi)
|
||||
bearingDegrees = (bearingDegrees > 0.0 ? bearingDegrees : (360.0 + bearingDegrees))
|
||||
return bearingDegrees
|
||||
}
|
||||
|
||||
internal class func adjustValue(_ startAngle: Double, degree: Double, maxValue: Float, minValue: Float) -> Double {
|
||||
let ratio = Double((maxValue - minValue) / 360)
|
||||
let ratioStart = ratio * startAngle
|
||||
let ratioDegree = ratio * degree
|
||||
let adjustValue: Double
|
||||
if startAngle < 0 {
|
||||
adjustValue = (360 + startAngle) > degree ? (ratioDegree - ratioStart) : (ratioDegree - ratioStart) - (360 * ratio)
|
||||
} else {
|
||||
adjustValue = (360 - (360 - startAngle)) < degree ? (ratioDegree - ratioStart) : (ratioDegree - ratioStart) + (360 * ratio)
|
||||
}
|
||||
return adjustValue + (Double(minValue))
|
||||
}
|
||||
|
||||
internal class func adjustDegree(_ startAngle: Double, degree: Double) -> Double {
|
||||
return (360 + startAngle) > degree ? degree : -(360 - degree)
|
||||
}
|
||||
|
||||
internal class func degreeFromValue(_ startAngle: Double, value: Float, maxValue: Float, minValue: Float) -> Double {
|
||||
let ratio = Double((maxValue - minValue) / 360)
|
||||
let angle = Double(value) / ratio
|
||||
return angle + startAngle - (Double(minValue) / ratio)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import ResearchKit.Private
|
||||
|
||||
public class ORKLandoltCResult: ORKResult {
|
||||
|
||||
public var outcome: Bool?
|
||||
public var letterAngle: Double?
|
||||
public var sliderAngle: Double?
|
||||
public var score: Int?
|
||||
|
||||
enum Keys: String {
|
||||
case outcome
|
||||
case letterAngle
|
||||
case sliderAngle
|
||||
case score
|
||||
}
|
||||
|
||||
public init(identifier: String, outcome: Bool, letterAngle: Double, sliderAngle: Double, score: Int) {
|
||||
super.init(identifier: identifier)
|
||||
|
||||
self.outcome = outcome
|
||||
self.letterAngle = letterAngle
|
||||
self.sliderAngle = sliderAngle
|
||||
self.score = score
|
||||
}
|
||||
|
||||
override public func encode(with aCoder: NSCoder) {
|
||||
super.encode(with: aCoder)
|
||||
|
||||
aCoder.encode(outcome, forKey: Keys.outcome.rawValue)
|
||||
aCoder.encode(letterAngle, forKey: Keys.letterAngle.rawValue)
|
||||
aCoder.encode(sliderAngle, forKey: Keys.sliderAngle.rawValue)
|
||||
aCoder.encode(score, forKey: Keys.score.rawValue)
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
outcome = aDecoder.decodeObject(forKey: Keys.outcome.rawValue) as? Bool ?? false
|
||||
letterAngle = aDecoder.decodeObject(forKey: Keys.letterAngle.rawValue) as? Double ?? 0.0
|
||||
sliderAngle = aDecoder.decodeObject(forKey: Keys.sliderAngle.rawValue) as? Double ?? 0.0
|
||||
score = aDecoder.decodeObject(forKey: Keys.score.rawValue) as? Int ?? 0
|
||||
}
|
||||
|
||||
override public func copy(with zone: NSZone? = nil) -> Any {
|
||||
let result = super.copy(with: zone) as! ORKLandoltCResult
|
||||
|
||||
result.outcome = outcome
|
||||
result.letterAngle = letterAngle
|
||||
result.sliderAngle = sliderAngle
|
||||
result.score = score
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override public func isEqual(_ object: Any?) -> Bool {
|
||||
let isParentSame = super.isEqual(object)
|
||||
|
||||
if let castObject = object as? ORKLandoltCResult {
|
||||
|
||||
return (isParentSame &&
|
||||
ORKEqualObjects(outcome as Any, castObject.outcome as Any) &&
|
||||
ORKEqualObjects(letterAngle as Any, castObject.letterAngle as Any) &&
|
||||
ORKEqualObjects(sliderAngle as Any, castObject.sliderAngle as Any) &&
|
||||
ORKEqualObjects(score as Any, castObject.score as Any))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override public func description(withNumberOfPaddingSpaces numberOfPaddingSpaces: UInt) -> String {
|
||||
let descriptionString = """
|
||||
\(descriptionPrefix(withNumberOfPaddingSpaces: numberOfPaddingSpaces));
|
||||
Outcome: \(String(describing: outcome)); LetterAngle: \(String(describing: letterAngle));
|
||||
SliderAngle: \(String(describing: sliderAngle)); Score: \(String(describing: score))
|
||||
"""
|
||||
return descriptionString
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@objc
|
||||
public enum VisionStepLeftOrRightEye: Int {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
@objc
|
||||
public enum VisionStepType: Int {
|
||||
case visualAcuity
|
||||
case contrastSensitivity
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ORKLandoltCStep: ORKActiveStep {
|
||||
|
||||
public var testType: VisionStepType?
|
||||
public var eyeToTest: VisionStepLeftOrRightEye?
|
||||
|
||||
enum Key: String {
|
||||
case testType
|
||||
case eyeToTest
|
||||
}
|
||||
|
||||
public override class func stepViewControllerClass() -> AnyClass {
|
||||
return ORKLandoltCStepViewController.self
|
||||
}
|
||||
|
||||
public class func supportsSecureCoding() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc
|
||||
public init(identifier: String, testType: VisionStepType, eyeToTest: VisionStepLeftOrRightEye) {
|
||||
super.init(identifier: identifier)
|
||||
self.testType = testType
|
||||
self.eyeToTest = eyeToTest
|
||||
}
|
||||
|
||||
public override var allowsBackNavigation: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let visionStep: ORKLandoltCStep = super.copy(with: zone) as! ORKLandoltCStep
|
||||
return visionStep
|
||||
}
|
||||
|
||||
public required init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
if let typeValue = aDecoder.decodeObject(forKey: "stepType") as? Int {
|
||||
testType = VisionStepType(rawValue: typeValue)
|
||||
}
|
||||
|
||||
if let eyeValue = aDecoder.decodeObject(forKey: "eyeToTest") as? Int {
|
||||
eyeToTest = VisionStepLeftOrRightEye(rawValue: eyeValue)
|
||||
}
|
||||
}
|
||||
|
||||
public override func encode(with aCoder: NSCoder) {
|
||||
super.encode(with: aCoder)
|
||||
aCoder.encode(testType, forKey: Key.testType.rawValue)
|
||||
aCoder.encode(eyeToTest, forKey: Key.eyeToTest.rawValue)
|
||||
}
|
||||
|
||||
public override func isEqual(_ object: Any?) -> Bool {
|
||||
if let object = object as? ORKLandoltCStep {
|
||||
return testType == object.testType && eyeToTest == object.eyeToTest
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import ResearchKit.Private
|
||||
|
||||
internal class ORKLandoltCStepContentView: UIView {
|
||||
|
||||
var eyeActivitySlider: EyeActivitySlider?
|
||||
private var testType: VisionStepType?
|
||||
|
||||
internal init(testType: VisionStepType) {
|
||||
super.init(frame: CGRect())
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.testType = testType
|
||||
setupSubviews()
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
internal required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
internal func setupSubviews() {
|
||||
guard let typeValue = testType else {
|
||||
return
|
||||
}
|
||||
|
||||
eyeActivitySlider = EyeActivitySlider(testType: typeValue)
|
||||
addSubview(eyeActivitySlider!)
|
||||
}
|
||||
|
||||
internal func setupConstraints() {
|
||||
eyeActivitySlider?.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
NSLayoutConstraint(item: eyeActivitySlider!,
|
||||
attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .width,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: eyeActivitySlider!,
|
||||
attribute: .height,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .width,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0)
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import ResearchKit.Private
|
||||
|
||||
public class ORKLandoltCStepViewController: ORKActiveStepViewController {
|
||||
|
||||
private var activityTimer = Timer()
|
||||
private var results = NSMutableArray()
|
||||
private var visionStepView: ORKLandoltCStepView
|
||||
private var eyeToTest: VisionStepLeftOrRightEye?
|
||||
private var testType: VisionStepType?
|
||||
|
||||
public override init(step: ORKStep?) {
|
||||
if let visionStep = step as? ORKLandoltCStep {
|
||||
eyeToTest = visionStep.eyeToTest
|
||||
testType = visionStep.testType
|
||||
}
|
||||
visionStepView = ORKLandoltCStepView(testType: testType)
|
||||
super.init(step: step)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override var result: ORKStepResult? {
|
||||
let stepResult = super.result
|
||||
stepResult?.results = results.copy() as? [ORKResult]
|
||||
|
||||
return stepResult!
|
||||
}
|
||||
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor.white
|
||||
activeStepView?.customContentView = visionStepView
|
||||
activeStepView?.removeCustomContentPadding()
|
||||
activeStepView?.customContentFillsAvailableSpace = true
|
||||
|
||||
// TODO: Localize
|
||||
visionStepView.currentEyeLabel.text = eyeToTest == .left ? "Left Eye" : "Right Eye"
|
||||
visionStepView.continueButton.addTarget(self, action: #selector(continueButtonWasPressed), for: .touchUpInside)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
override public func stepDidFinish() {
|
||||
super.stepDidFinish()
|
||||
goForward()
|
||||
}
|
||||
|
||||
private func startTimer() {
|
||||
activityTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(hideCircle), userInfo: nil, repeats: false)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func hideCircle() {
|
||||
activityTimer.invalidate()
|
||||
visionStepView.visionContentView?.eyeActivitySlider?.hideLetter()
|
||||
visionStepView.topInstructionLabel.isHidden = false
|
||||
}
|
||||
|
||||
@objc
|
||||
private func continueButtonWasPressed() {
|
||||
activityTimer.invalidate()
|
||||
visionStepView.topInstructionLabel.isHidden = true
|
||||
visionStepView.continueButton.isEnabled = false
|
||||
|
||||
if let resultData = visionStepView.visionContentView?.eyeActivitySlider?.fetchResultDataAndUpdateSlider() {
|
||||
let stepResult: ORKLandoltCResult = ORKLandoltCResult(identifier: step!.identifier,
|
||||
outcome: resultData.outcome,
|
||||
letterAngle: resultData.letterAngle,
|
||||
sliderAngle: resultData.sliderAngle,
|
||||
score: resultData.score)
|
||||
results.add(stepResult)
|
||||
|
||||
if resultData.incorrectAnswers == 2 || resultData.score == resultData.maxScore {
|
||||
stepDidFinish()
|
||||
} else {
|
||||
visionStepView.continueButton.isEnabled = true
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ORKLandoltCStepView: UIView {
|
||||
|
||||
var visionContentView: ORKLandoltCStepContentView?
|
||||
|
||||
let continueButtonCornerRadius: CGFloat = 12.0
|
||||
let eyeLabelTopPadding: CGFloat = 20.0
|
||||
let instructionLabelTopPadding: CGFloat = 15.0
|
||||
let visionContentTopPadding: CGFloat = 10.0
|
||||
|
||||
let continueButton = ORKRoundTappingButton()
|
||||
let currentEyeLabel = UILabel()
|
||||
let topInstructionLabel = UILabel()
|
||||
|
||||
init(testType: VisionStepType!) {
|
||||
super.init(frame: .zero)
|
||||
setupCurrentEyeLabel()
|
||||
setupTopInstructionLabel()
|
||||
setupVisionContentView(testType: testType)
|
||||
setupContinueButton()
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setupVisionContentView(testType: VisionStepType!) {
|
||||
if visionContentView == nil {
|
||||
visionContentView = ORKLandoltCStepContentView(testType: testType)
|
||||
}
|
||||
addSubview(visionContentView!)
|
||||
}
|
||||
|
||||
func setupCurrentEyeLabel() {
|
||||
currentEyeLabel.isHidden = true
|
||||
currentEyeLabel.textAlignment = .center
|
||||
currentEyeLabel.textColor = UIColor.black
|
||||
currentEyeLabel.numberOfLines = 0
|
||||
// TODO: set FontDescriptor
|
||||
currentEyeLabel.font = UIFont(name: "", size: 20.0)
|
||||
addSubview(currentEyeLabel)
|
||||
}
|
||||
|
||||
func setupTopInstructionLabel() {
|
||||
topInstructionLabel.textAlignment = .center
|
||||
topInstructionLabel.numberOfLines = 0
|
||||
topInstructionLabel.textColor = UIColor.black
|
||||
// TODO: Localize
|
||||
topInstructionLabel.text = "Move the dial to where you think the opening in the letter was."
|
||||
// TODO: set FontDescriptor
|
||||
topInstructionLabel.font = UIFont(name: "", size: 20.0)
|
||||
topInstructionLabel.isHidden = true
|
||||
addSubview(topInstructionLabel)
|
||||
}
|
||||
|
||||
func setupContinueButton() {
|
||||
// TODO: Localize
|
||||
continueButton.diameter = 60.0
|
||||
continueButton.setTitle("Next", for: UIControl.State.normal)
|
||||
continueButton.backgroundColor = tintColor
|
||||
continueButton.layer.cornerRadius = continueButtonCornerRadius
|
||||
addSubview(continueButton)
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
currentEyeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
topInstructionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
visionContentView?.translatesAutoresizingMaskIntoConstraints = false
|
||||
continueButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let constraints = [
|
||||
NSLayoutConstraint(item: currentEyeLabel,
|
||||
attribute: .top,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .top,
|
||||
multiplier: 1.0,
|
||||
constant: eyeLabelTopPadding),
|
||||
NSLayoutConstraint(item: currentEyeLabel,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: topInstructionLabel,
|
||||
attribute: .top,
|
||||
relatedBy: .equal,
|
||||
toItem: currentEyeLabel,
|
||||
attribute: .bottom,
|
||||
multiplier: 1.0,
|
||||
constant: instructionLabelTopPadding),
|
||||
NSLayoutConstraint(item: topInstructionLabel,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: topInstructionLabel,
|
||||
attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .width,
|
||||
multiplier: 0.8,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: visionContentView!,
|
||||
attribute: .top,
|
||||
relatedBy: .equal,
|
||||
toItem: topInstructionLabel,
|
||||
attribute: .bottom,
|
||||
multiplier: 1.0,
|
||||
constant: visionContentTopPadding),
|
||||
NSLayoutConstraint(item: visionContentView!,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: visionContentView!,
|
||||
attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .width,
|
||||
multiplier: 0.8,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: visionContentView!,
|
||||
attribute: .height,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .height,
|
||||
multiplier: 0.8,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: continueButton,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0),
|
||||
NSLayoutConstraint(item: continueButton,
|
||||
attribute: .bottom,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .bottom,
|
||||
multiplier: 1.0,
|
||||
constant: -20.0)
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2017, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKStroopContentView.h"
|
||||
#import "ORKUnitLabel.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
|
||||
|
||||
CGFloat minimumButtonHeight = 60;
|
||||
UILayoutConstraintAxis alignment = UILayoutConstraintAxisHorizontal;
|
||||
CGFloat labelHeight = 250.0;
|
||||
CGFloat labelWidth = 250.0;
|
||||
static const CGFloat buttonStackViewSpacing = 20.0;
|
||||
|
||||
|
||||
@implementation ORKStroopContentView {
|
||||
UILabel *_colorLabel;
|
||||
UIStackView *_buttonStackView;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_colorLabel = [UILabel new];
|
||||
_colorLabel.numberOfLines = 1;
|
||||
_colorLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_colorLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[_colorLabel setFont:[UIFont systemFontOfSize:60]];
|
||||
[_colorLabel setAdjustsFontSizeToFitWidth:YES];
|
||||
|
||||
ORKScreenType screenType = ORKGetVerticalScreenTypeForWindow([[[UIApplication sharedApplication] delegate] window]);
|
||||
|
||||
if (screenType == ORKScreenTypeiPhone5 ) {
|
||||
labelWidth = 200.0;
|
||||
labelHeight = 200.0;
|
||||
} else {
|
||||
labelWidth = 250.0;
|
||||
labelHeight = 250.0;
|
||||
}
|
||||
|
||||
[self setupDefaultButtons];
|
||||
|
||||
[self addSubview:_colorLabel];
|
||||
|
||||
[self setUpConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
-(void)setupButtons {
|
||||
_RButton = [[ORKBorderedButton alloc] init];
|
||||
[_RButton setNormalTintColor:[UIColor blackColor]];
|
||||
|
||||
_RButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_RButton setTitle:ORKLocalizedString(@"STROOP_COLOR_RED_INITIAL", nil) forState:UIControlStateNormal];
|
||||
|
||||
_GButton = [[ORKBorderedButton alloc] init];
|
||||
[_GButton setNormalTintColor:[UIColor blackColor]];
|
||||
|
||||
_GButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_GButton setTitle:ORKLocalizedString(@"STROOP_COLOR_GREEN_INITIAL", nil) forState:UIControlStateNormal];
|
||||
|
||||
_BButton = [[ORKBorderedButton alloc] init];
|
||||
[_BButton setNormalTintColor:[UIColor blackColor]];
|
||||
|
||||
_BButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_BButton setTitle:ORKLocalizedString(@"STROOP_COLOR_BLUE_INITIAL", nil) forState:UIControlStateNormal];
|
||||
|
||||
_YButton = [[ORKBorderedButton alloc] init];
|
||||
[_YButton setNormalTintColor:[UIColor blackColor]];
|
||||
|
||||
_YButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_YButton setTitle:ORKLocalizedString(@"STROOP_COLOR_YELLOW_INITIAL", nil) forState:UIControlStateNormal];
|
||||
|
||||
}
|
||||
|
||||
-(void)setupDefaultButtons {
|
||||
[self setupButtons];
|
||||
if (!_buttonStackView) {
|
||||
_buttonStackView = [[UIStackView alloc] initWithArrangedSubviews:@[_RButton, _GButton, _BButton, _YButton]];
|
||||
alignment = UILayoutConstraintAxisHorizontal;
|
||||
}
|
||||
|
||||
minimumButtonHeight = 60;
|
||||
|
||||
_buttonStackView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_buttonStackView.spacing = buttonStackViewSpacing;
|
||||
_buttonStackView.axis = alignment;
|
||||
[self addSubview:_buttonStackView];
|
||||
|
||||
}
|
||||
|
||||
-(void)setupGridButtons {
|
||||
if (_useGridLayoutForButtons) {
|
||||
[self setupButtons];
|
||||
|
||||
[_buttonStackView removeFromSuperview];
|
||||
UIStackView* stack1 = [[UIStackView alloc] initWithArrangedSubviews:@[_RButton, _GButton]];
|
||||
UIStackView* stack2 = [[UIStackView alloc] initWithArrangedSubviews:@[_BButton, _YButton]];
|
||||
|
||||
stack1.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
stack1.spacing = buttonStackViewSpacing;
|
||||
stack1.axis = UILayoutConstraintAxisHorizontal;
|
||||
|
||||
stack2.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
stack2.spacing = buttonStackViewSpacing;
|
||||
stack2.axis = UILayoutConstraintAxisHorizontal;
|
||||
|
||||
_buttonStackView = [[UIStackView alloc] initWithArrangedSubviews:@[stack1,stack2]];
|
||||
|
||||
_buttonStackView.axis = UILayoutConstraintAxisVertical;
|
||||
|
||||
ORKScreenType screenType = ORKGetVerticalScreenTypeForWindow([[[UIApplication sharedApplication] delegate] window]);
|
||||
|
||||
if (screenType == ORKScreenTypeiPhone6) {
|
||||
minimumButtonHeight = 150.0;
|
||||
} else if (screenType == ORKScreenTypeiPhone5 ) {
|
||||
minimumButtonHeight = 100.0;
|
||||
} else {
|
||||
minimumButtonHeight = 200;
|
||||
}
|
||||
|
||||
alignment = UILayoutConstraintAxisVertical;
|
||||
_buttonStackView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_buttonStackView.spacing = buttonStackViewSpacing;
|
||||
_buttonStackView.axis = alignment;
|
||||
|
||||
[self addSubview:_buttonStackView];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setUseGridLayoutForButtons:(bool)useGridLayoutForButtons{
|
||||
_useGridLayoutForButtons = useGridLayoutForButtons;
|
||||
[self setupGridButtons];
|
||||
|
||||
}
|
||||
|
||||
-(void)setUseTextForStimuli:(bool)useTextForStimuli{
|
||||
_useTextForStimuli = useTextForStimuli;
|
||||
if (!_useTextForStimuli) {
|
||||
[_colorLabel setFont:[UIFont boldSystemFontOfSize:60]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setColorLabelText:(NSString *)colorLabelText {
|
||||
[_colorLabel setText:colorLabelText];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setColorLabelColor:(UIColor *)colorLabelColor {
|
||||
[_colorLabel setTextColor:colorLabelColor];
|
||||
if (!_useTextForStimuli) {
|
||||
[_colorLabel setBackgroundColor:colorLabelColor];
|
||||
}
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (NSString *)colorLabelText {
|
||||
return _colorLabel.text;
|
||||
}
|
||||
|
||||
- (UIColor *)colorLabelColor {
|
||||
return _colorLabel.textColor;
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
|
||||
NSMutableArray *constraints = [[NSMutableArray alloc] init];
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_colorLabel, _buttonStackView);
|
||||
|
||||
int bottomStackViewSpace = _useGridLayoutForButtons ? 90 : 30;
|
||||
|
||||
NSString * constraintString = [NSString stringWithFormat: @"V:|-(==30)-[_colorLabel]-(>=10)-[_buttonStackView]-(==%d)-|", bottomStackViewSpace];
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:constraintString
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:nil
|
||||
views:views]];
|
||||
NSArray *baseLayouts = @[[NSLayoutConstraint constraintWithItem:_buttonStackView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:minimumButtonHeight],
|
||||
[NSLayoutConstraint constraintWithItem:_buttonStackView
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObjectsFromArray:baseLayouts];
|
||||
|
||||
if (!_useTextForStimuli) {
|
||||
|
||||
[constraints addObjectsFromArray: @[[NSLayoutConstraint constraintWithItem:_colorLabel
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:labelWidth],
|
||||
[NSLayoutConstraint constraintWithItem:_colorLabel
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant: labelHeight],
|
||||
[NSLayoutConstraint constraintWithItem:_buttonStackView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:minimumButtonHeight]]];
|
||||
}
|
||||
|
||||
for (ORKBorderedButton *button in @[_RButton, _GButton, _BButton, _YButton]) {
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:button
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:button
|
||||
attribute:NSLayoutAttributeHeight
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
}
|
||||
|
||||
[self addConstraints:constraints];
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
internal class ORKSwiftStroopContentView: ORKActiveStepCustomView {
|
||||
|
||||
public var colorLabelText: String?
|
||||
public var colorLabelColor: UIColor?
|
||||
|
||||
public let redButton: ORKBorderedButton = {
|
||||
let button = ORKBorderedButton()
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setTitle(ORKSwiftLocalizedString("STROOP_COLOR_RED_INITIAL", ""), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
public let greenButton: ORKBorderedButton = {
|
||||
let button = ORKBorderedButton()
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setTitle(ORKSwiftLocalizedString("STROOP_COLOR_GREEN_INITIAL", ""), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
public let blueButton: ORKBorderedButton = {
|
||||
let button = ORKBorderedButton()
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setTitle(ORKSwiftLocalizedString("STROOP_COLOR_BLUE_INITIAL", ""), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
public let yellowButton: ORKBorderedButton = {
|
||||
let button = ORKBorderedButton()
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setTitle(ORKSwiftLocalizedString("STROOP_COLOR_YELLOW_INITIAL", ""), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
private let colorLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let colorLabelFontSize: CGFloat = 60.0
|
||||
label.numberOfLines = 1
|
||||
label.text = " "
|
||||
label.textAlignment = .center
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.font = UIFont.systemFont(ofSize: colorLabelFontSize)
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
return label
|
||||
}()
|
||||
|
||||
private let buttonStackView: UIStackView
|
||||
|
||||
private let minimumButtonHeight: CGFloat = 60.0
|
||||
private let buttonStackViewSpacing: CGFloat = 20.0
|
||||
|
||||
private override init(frame: CGRect) {
|
||||
buttonStackView = UIStackView(arrangedSubviews: [redButton, greenButton, blueButton, yellowButton])
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
internal required init?(coder aDecoder: NSCoder) {
|
||||
buttonStackView = UIStackView(arrangedSubviews: [redButton, greenButton, blueButton, yellowButton])
|
||||
super.init(coder: aDecoder)
|
||||
setup()
|
||||
}
|
||||
|
||||
internal func setup() {
|
||||
self.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonStackView.spacing = buttonStackViewSpacing
|
||||
buttonStackView.axis = .horizontal
|
||||
self.addSubview(colorLabel)
|
||||
self.addSubview(buttonStackView)
|
||||
setUpConstraints()
|
||||
}
|
||||
|
||||
internal func setColorLabelText(colorLabelText text: String) {
|
||||
colorLabelText = text
|
||||
colorLabel.text = text
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
internal func setColorLabelColor(colorLabelColor color: UIColor) {
|
||||
colorLabelColor = color
|
||||
colorLabel.textColor = color
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
internal func getColorLabelText() -> String {
|
||||
return colorLabel.text!
|
||||
}
|
||||
|
||||
internal func getColorLabelColor() -> UIColor {
|
||||
return colorLabel.textColor
|
||||
}
|
||||
|
||||
internal func setUpConstraints() {
|
||||
|
||||
var constraints = [NSLayoutConstraint]()
|
||||
var views = [String: Any]()
|
||||
views["colorLabel"] = colorLabel
|
||||
views["buttonStackView"] = buttonStackView
|
||||
|
||||
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-(==30)-[colorLabel]-(>=10)-[buttonStackView]-(==30)-|",
|
||||
options: .alignAllCenterX,
|
||||
metrics: nil,
|
||||
views: views)
|
||||
|
||||
constraints += [
|
||||
NSLayoutConstraint(item: buttonStackView as Any,
|
||||
attribute: .height,
|
||||
relatedBy: .equal,
|
||||
toItem: nil,
|
||||
attribute: .notAnAttribute,
|
||||
multiplier: 1.0,
|
||||
constant: minimumButtonHeight),
|
||||
NSLayoutConstraint(item: buttonStackView as Any,
|
||||
attribute: .centerX,
|
||||
relatedBy: .equal,
|
||||
toItem: self,
|
||||
attribute: .centerX,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0)
|
||||
]
|
||||
|
||||
for button in [redButton, greenButton, blueButton, yellowButton] {
|
||||
constraints += [
|
||||
NSLayoutConstraint(item: button,
|
||||
attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: button,
|
||||
attribute: .height,
|
||||
multiplier: 1.0,
|
||||
constant: 0.0)
|
||||
]
|
||||
}
|
||||
|
||||
self.addConstraints(constraints)
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
import ResearchKit.Private
|
||||
|
||||
public class ORKSwiftStroopResult: ORKResult {
|
||||
|
||||
public var startTime: TimeInterval?
|
||||
public var endTime: TimeInterval?
|
||||
public var color: String?
|
||||
public var text: String?
|
||||
public var colorSelected: String?
|
||||
|
||||
enum Keys: String {
|
||||
case startTime
|
||||
case endTime
|
||||
case color
|
||||
case text
|
||||
case colorSelected
|
||||
}
|
||||
|
||||
public override init(identifier: String) {
|
||||
super.init(identifier: identifier)
|
||||
}
|
||||
|
||||
public override func encode(with aCoder: NSCoder) {
|
||||
super.encode(with: aCoder)
|
||||
aCoder.encode(startTime, forKey: Keys.startTime.rawValue)
|
||||
aCoder.encode(endTime, forKey: Keys.endTime.rawValue)
|
||||
aCoder.encode(color, forKey: Keys.color.rawValue)
|
||||
aCoder.encode(text, forKey: Keys.text.rawValue)
|
||||
aCoder.encode(colorSelected, forKey: Keys.colorSelected.rawValue)
|
||||
}
|
||||
|
||||
public required init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
startTime = aDecoder.decodeObject(forKey: Keys.startTime.rawValue) as? Double
|
||||
endTime = aDecoder.decodeObject(forKey: Keys.endTime.rawValue) as? Double
|
||||
color = aDecoder.decodeObject(forKey: Keys.color.rawValue) as? String
|
||||
text = aDecoder.decodeObject(forKey: Keys.text.rawValue) as? String
|
||||
colorSelected = aDecoder.decodeObject(forKey: Keys.colorSelected.rawValue) as? String
|
||||
}
|
||||
|
||||
public class func supportsSecureCoding() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override public func isEqual(_ object: Any?) -> Bool {
|
||||
let isParentSame = super.isEqual(object)
|
||||
|
||||
if let castObject = object as? ORKSwiftStroopResult {
|
||||
return (isParentSame &&
|
||||
(startTime == castObject.startTime) &&
|
||||
(endTime == castObject.endTime) &&
|
||||
(color == castObject.color) &&
|
||||
(text == castObject.text) &&
|
||||
(colorSelected == castObject.colorSelected))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override public func copy(with zone: NSZone? = nil) -> Any {
|
||||
if let result = super.copy(with: zone) as? ORKSwiftStroopResult {
|
||||
result.startTime = startTime
|
||||
result.endTime = endTime
|
||||
result.color = color
|
||||
result.text = text
|
||||
result.colorSelected = colorSelected
|
||||
return result
|
||||
} else {
|
||||
return super.copy(with: zone)
|
||||
}
|
||||
}
|
||||
|
||||
public override func description(withNumberOfPaddingSpaces numberOfPaddingSpaces: UInt) -> String {
|
||||
return "\(descriptionPrefix(withNumberOfPaddingSpaces: numberOfPaddingSpaces)); color: \(color ?? "") text: \(text ?? "") colorSelected: \(colorSelected ?? "") \(descriptionSuffix())"
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
public class ORKSwiftStroopStep: ORKActiveStep {
|
||||
|
||||
public var numberOfAttempts = 0
|
||||
private let minimumAttempts = 10
|
||||
|
||||
enum Key: String {
|
||||
case numberOfAttempts
|
||||
}
|
||||
|
||||
public override class func stepViewControllerClass() -> AnyClass {
|
||||
return ORKSwiftStroopStepViewController.self
|
||||
}
|
||||
|
||||
public class func supportsSecureCoding() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override init(identifier: String) {
|
||||
super.init(identifier: identifier)
|
||||
|
||||
shouldVibrateOnStart = true
|
||||
shouldShowDefaultTimer = false
|
||||
shouldContinueOnFinish = true
|
||||
stepDuration = TimeInterval(NSIntegerMax)
|
||||
}
|
||||
|
||||
public override func validateParameters() {
|
||||
super.validateParameters()
|
||||
assert(numberOfAttempts >= minimumAttempts, "number of attempts should be greater or equal to \(minimumAttempts)")
|
||||
}
|
||||
|
||||
public override func startsFinished() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public override var allowsBackNavigation: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let stroopStep = super.copy(with: zone)
|
||||
return stroopStep
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
numberOfAttempts = aDecoder.decodeInteger(forKey: Key.numberOfAttempts.rawValue)
|
||||
}
|
||||
|
||||
public override func encode(with aCoder: NSCoder) {
|
||||
super.encode(with: aCoder)
|
||||
aCoder.encode(numberOfAttempts, forKey: Key.numberOfAttempts.rawValue)
|
||||
}
|
||||
|
||||
public override func isEqual(_ object: Any?) -> Bool {
|
||||
if let object = object as? ORKSwiftStroopStep {
|
||||
return numberOfAttempts == object.numberOfAttempts
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
import ResearchKit.Private
|
||||
|
||||
public class ORKSwiftStroopStepViewController: ORKActiveStepViewController {
|
||||
|
||||
private let stroopContentView = ORKSwiftStroopContentView()
|
||||
private var colors = [String: UIColor]()
|
||||
private var differentColorLabels = [String: [UIColor]]()
|
||||
private var questionNumber = 0
|
||||
|
||||
private let red = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
|
||||
private let green = UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)
|
||||
private let blue = UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0)
|
||||
private let yellow = UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0)
|
||||
|
||||
private let redString = ORKSwiftLocalizedString("STROOP_COLOR_RED", "")
|
||||
private let greenString = ORKSwiftLocalizedString("STROOP_COLOR_GREEN", "")
|
||||
private let blueString = ORKSwiftLocalizedString("STROOP_COLOR_BLUE", "")
|
||||
private let yellowString = ORKSwiftLocalizedString("STROOP_COLOR_YELLOW", "")
|
||||
|
||||
private var nextQuestionTimer: Timer?
|
||||
private var results: NSMutableArray?
|
||||
private var startTime: TimeInterval?
|
||||
private var endTime: TimeInterval?
|
||||
|
||||
public override init(step: ORKStep?) {
|
||||
super.init(step: step)
|
||||
suspendIfInactive = true
|
||||
}
|
||||
|
||||
internal required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func stroopStep() -> ORKSwiftStroopStep {
|
||||
return step as! ORKSwiftStroopStep
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
results = NSMutableArray()
|
||||
|
||||
colors[redString] = red
|
||||
colors[blueString] = blue
|
||||
colors[yellowString] = yellow
|
||||
colors[greenString] = green
|
||||
|
||||
differentColorLabels[redString] = [blue, green, yellow]
|
||||
differentColorLabels[blueString] = [red, green, yellow]
|
||||
differentColorLabels[yellowString] = [red, blue, green]
|
||||
differentColorLabels[greenString] = [red, blue, yellow]
|
||||
|
||||
activeStepView?.activeCustomView = stroopContentView
|
||||
activeStepView?.customContentFillsAvailableSpace = true
|
||||
|
||||
stroopContentView.redButton.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
|
||||
stroopContentView.greenButton.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
|
||||
stroopContentView.blueButton.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
|
||||
stroopContentView.yellowButton.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func buttonPressed(sender: Any) {
|
||||
|
||||
if stroopContentView.colorLabelText != " " {
|
||||
setButtonDisabled()
|
||||
if let button = sender as? ORKBorderedButton {
|
||||
|
||||
if button == stroopContentView.redButton {
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first
|
||||
as? String ?? "", withText: stroopContentView.colorLabelText!,
|
||||
withColorSelected: redString)
|
||||
} else if button == stroopContentView.greenButton {
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first
|
||||
as? String ?? "", withText: stroopContentView.colorLabelText!,
|
||||
withColorSelected: greenString)
|
||||
} else if button == stroopContentView.blueButton {
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first
|
||||
as? String ?? "", withText: stroopContentView.colorLabelText!,
|
||||
withColorSelected: blueString)
|
||||
} else if button == stroopContentView.yellowButton {
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first
|
||||
as? String ?? "", withText: stroopContentView.colorLabelText!,
|
||||
withColorSelected: yellowString)
|
||||
}
|
||||
|
||||
nextQuestionTimer = Timer.scheduledTimer(timeInterval: 0.5,
|
||||
target: self,
|
||||
selector: #selector(startNextQuestionOrFinish),
|
||||
userInfo: nil,
|
||||
repeats: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
start()
|
||||
}
|
||||
|
||||
public override func stepDidFinish() {
|
||||
super.stepDidFinish()
|
||||
stroopContentView.finishStep(self)
|
||||
goForward()
|
||||
}
|
||||
|
||||
public override var result: ORKStepResult? {
|
||||
let stepResult = super.result
|
||||
if results != nil {
|
||||
stepResult?.results = results?.copy() as? [ORKResult]
|
||||
}
|
||||
return stepResult!
|
||||
}
|
||||
|
||||
public override func start() {
|
||||
super.start()
|
||||
startQuestion()
|
||||
}
|
||||
|
||||
private func createResult(color: String, withText text: String, withColorSelected colorSelected: String) {
|
||||
let stroopResult = ORKSwiftStroopResult(identifier: (step!.identifier))
|
||||
stroopResult.startTime = startTime
|
||||
stroopResult.endTime = ProcessInfo.processInfo.systemUptime
|
||||
stroopResult.color = color
|
||||
stroopResult.text = text
|
||||
stroopResult.colorSelected = colorSelected
|
||||
results?.add(stroopResult)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func startNextQuestionOrFinish() {
|
||||
if nextQuestionTimer != nil {
|
||||
nextQuestionTimer?.invalidate()
|
||||
nextQuestionTimer = nil
|
||||
}
|
||||
questionNumber += 1
|
||||
if questionNumber == stroopStep().numberOfAttempts {
|
||||
finish()
|
||||
} else {
|
||||
startQuestion()
|
||||
}
|
||||
}
|
||||
|
||||
private func startQuestion() {
|
||||
let pattern: Int = Int(arc4random()) % 2
|
||||
if pattern == 0 {
|
||||
let index: Int = Int(arc4random()) % differentColorLabels.keys.count
|
||||
let text = Array(differentColorLabels.keys)[index]
|
||||
stroopContentView.setColorLabelText(colorLabelText: text)
|
||||
let color = colors[text]!
|
||||
stroopContentView.colorLabelColor = color
|
||||
stroopContentView.setColorLabelColor(colorLabelColor: color)
|
||||
} else {
|
||||
let index: Int = Int(arc4random()) % differentColorLabels.keys.count
|
||||
let text = Array(differentColorLabels.keys)[index]
|
||||
stroopContentView.setColorLabelText(colorLabelText: text)
|
||||
let colorArray = differentColorLabels[text]!
|
||||
let randomColorIndex = Int(arc4random()) % colorArray.count
|
||||
let color = colorArray[randomColorIndex]
|
||||
stroopContentView.setColorLabelColor(colorLabelColor: color)
|
||||
}
|
||||
|
||||
setButtonsEnabled()
|
||||
startTime = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
|
||||
private func setButtonDisabled() {
|
||||
|
||||
stroopContentView.redButton.isEnabled = false
|
||||
stroopContentView.greenButton.isEnabled = false
|
||||
stroopContentView.blueButton.isEnabled = false
|
||||
stroopContentView.yellowButton.isEnabled = false
|
||||
}
|
||||
|
||||
private func setButtonsEnabled() {
|
||||
|
||||
stroopContentView.redButton.isEnabled = true
|
||||
stroopContentView.greenButton.isEnabled = true
|
||||
stroopContentView.blueButton.isEnabled = true
|
||||
stroopContentView.yellowButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2019, Novartis.
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
internal class TrackLayer: CAShapeLayer {
|
||||
|
||||
struct Setting {
|
||||
var startAngle = Double()
|
||||
var barWidth = CGFloat()
|
||||
var barColor = UIColor()
|
||||
var trackingColor = UIColor()
|
||||
}
|
||||
|
||||
internal var setting = Setting()
|
||||
internal var degree: Double = 0
|
||||
internal var hollowRadius: CGFloat {
|
||||
return (bounds.width * 0.5) - setting.barWidth
|
||||
}
|
||||
internal var currentCenter: CGPoint {
|
||||
return CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
}
|
||||
internal var hollowRect: CGRect {
|
||||
return CGRect(
|
||||
x: currentCenter.x - hollowRadius,
|
||||
y: currentCenter.y - hollowRadius,
|
||||
width: hollowRadius * 2.0,
|
||||
height: hollowRadius * 2.0)
|
||||
}
|
||||
internal init(bounds: CGRect, setting: Setting) {
|
||||
super.init()
|
||||
self.bounds = bounds
|
||||
self.setting = setting
|
||||
cornerRadius = bounds.size.width * 0.5
|
||||
masksToBounds = true
|
||||
position = currentCenter
|
||||
backgroundColor = setting.barColor.cgColor
|
||||
mask()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override internal func draw(in ctx: CGContext) {
|
||||
drawTrack(ctx: ctx)
|
||||
}
|
||||
|
||||
private func mask() {
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.bounds = bounds
|
||||
let ovalRect = hollowRect
|
||||
let path = UIBezierPath(ovalIn: ovalRect)
|
||||
path.append(UIBezierPath(rect: maskLayer.bounds))
|
||||
maskLayer.path = path.cgPath
|
||||
maskLayer.position = currentCenter
|
||||
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
|
||||
mask = maskLayer
|
||||
}
|
||||
|
||||
private func drawTrack(ctx: CGContext) {
|
||||
let adjustDegree = Math.adjustDegree(setting.startAngle, degree: degree)
|
||||
let centerX = currentCenter.x
|
||||
let centerY = currentCenter.y
|
||||
let radius = min(centerX, centerY)
|
||||
ctx.setFillColor(setting.trackingColor.cgColor)
|
||||
ctx.beginPath()
|
||||
ctx.move(to: CGPoint(x: centerX, y: centerY))
|
||||
ctx.addArc(center: CGPoint(x: centerX, y: centerY),
|
||||
radius: radius,
|
||||
startAngle: CGFloat(Math.degreesToRadians(setting.startAngle)),
|
||||
endAngle: CGFloat(Math.degreesToRadians(adjustDegree)),
|
||||
clockwise: false)
|
||||
|
||||
ctx.closePath()
|
||||
ctx.fillPath()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ContrastExam.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ContrastExam@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ContrastExam@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "VisualExam.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "VisualExam@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "VisualExam@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityHorizontalScroll.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityHorizontalScroll@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityHorizontalScroll@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityInstruction.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityInstruction@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityInstruction@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityPinch.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityPinch@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityPinch@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityRotation.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityRotation@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityRotation@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe1.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe1@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe1@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe2@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe2@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe3.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe3@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe3@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe4.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe4@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilitySwipe4@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityTap.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityTap@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityTap@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityVerticalScroll.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityVerticalScroll@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "touchAbilityVerticalScroll@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "iCNDialPointerWithShadow.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
%PDF-1.5
%âãÏÓ
|
||||
30 0 obj
<</Metadata 33 0 R/OCProperties<</D<</ON[34 0 R 58 0 R]/Order 59 0 R/RBGroups[]>>/OCGs[34 0 R 58 0 R]>>/Pages 3 0 R/Type/Catalog>>
endobj
33 0 obj
<</Length 7573/Subtype/XML/Type/Metadata>>stream
|
||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c132 79.159284, 2016/04/19-13:13:40 ">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
|
||||
xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
|
||||
xmlns:pdf="http://ns.adobe.com/pdf/1.3/"
|
||||
xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
|
||||
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
|
||||
xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/">
|
||||
<xmp:CreateDate>2016-11-10T19:24:21Z</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2016-11-10T11:31:44-08:00</xmp:ModifyDate>
|
||||
<xmp:MetadataDate>2016-11-10T11:31:44-08:00</xmp:MetadataDate>
|
||||
<xmp:Thumbnails>
|
||||
<rdf:Alt>
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<xmpGImg:width>144</xmpGImg:width>
|
||||
<xmpGImg:height>256</xmpGImg:height>
|
||||
<xmpGImg:format>JPEG</xmpGImg:format>
|
||||
<xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAACQAwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXV
GKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVRvLh7e2kmSJp3RSVhSgZj4AsQMjMkCwLZRAJ
omnk3mT8wfNdy7W0cZ0mL+RKiYjbrIwB7fsgZzOr7Szk8NeH9/z/AFU9LpOzcNcV+J93y/WxGC4v
4LtbyKZ0ulbmJgx5cvGuayOQg8QO7tJYwY8JGz6F024ludOtbiZQk0sSPIgBADMoJFDv1zt8UjKA
J5kPD5YiMiByBRGWMHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FUv1LQdL1KPhdQK/gSBUZDJij
MVIWGePJKBuJopBa/lnoMF6LluUsanksDGqVHj4/TmDHsrAJcVfDo50u1M5jw38erLwAAAOgzYuu
dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd
irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi
rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir
sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs
VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV
dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd
irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi
rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir
sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs
VdirsVdirsVdirsVdirsVdirsVdirsVdirsVf//Z</xmpGImg:image>
|
||||
</rdf:li>
|
||||
</rdf:Alt>
|
||||
</xmp:Thumbnails>
|
||||
<pdf:Producer>Mac OS X 10.12.1 Quartz PDFContext</pdf:Producer>
|
||||
<xmpTPg:NPages>1</xmpTPg:NPages>
|
||||
<xmpTPg:HasVisibleTransparency>True</xmpTPg:HasVisibleTransparency>
|
||||
<xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
|
||||
<xmpTPg:MaxPageSize rdf:parseType="Resource">
|
||||
<stDim:w>49.000000</stDim:w>
|
||||
<stDim:h>35.000000</stDim:h>
|
||||
<stDim:unit>Points</stDim:unit>
|
||||
</xmpTPg:MaxPageSize>
|
||||
<xmpTPg:PlateNames>
|
||||
<rdf:Seq>
|
||||
<rdf:li>Cyan</rdf:li>
|
||||
<rdf:li>Magenta</rdf:li>
|
||||
<rdf:li>Yellow</rdf:li>
|
||||
<rdf:li>Black</rdf:li>
|
||||
</rdf:Seq>
|
||||
</xmpTPg:PlateNames>
|
||||
<xmpTPg:SwatchGroups>
|
||||
<rdf:Seq>
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<xmpG:groupName>Default Swatch Group</xmpG:groupName>
|
||||
<xmpG:groupType>0</xmpG:groupType>
|
||||
</rdf:li>
|
||||
</rdf:Seq>
|
||||
</xmpTPg:SwatchGroups>
|
||||
<dc:format>application/pdf</dc:format>
|
||||
<xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
|
||||
<xmpMM:DocumentID>uuid:2efd0779-15bc-7d48-9832-af59b6fbb134</xmpMM:DocumentID>
|
||||
<xmpMM:InstanceID>uuid:95f52edf-f897-4942-acec-80c9294e7f9d</xmpMM:InstanceID>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<?xpacket end="w"?>
endstream
endobj
3 0 obj
<</Count 1/Kids[2 0 R]/MediaBox[0 0 49 32]/Type/Pages>>
endobj
2 0 obj
<</ArtBox[0.0 0.0 49.0 35.0]/BleedBox[0.0 0.0 49.0 35.0]/Contents 60 0 R/Group 61 0 R/LastModified(D:20161110113144-08'00')/MediaBox[0.0 0.0 49.0 35.0]/Parent 3 0 R/PieceInfo<</Illustrator 62 0 R>>/Resources<</ColorSpace<</CS0 63 0 R>>/ExtGState<</GS0 64 0 R/GS1 65 0 R>>/ProcSet[/PDF/ImageC/ImageI]/Properties<</MC0 58 0 R>>/XObject<</Fm0 66 0 R/Im0 67 0 R>>>>/Thumb 68 0 R/TrimBox[0.0 0.0 49.0 35.0]/Type/Page>>
endobj
60 0 obj
<</Filter/FlateDecode/Length 444>>stream
|
||||
H‰ì”ÏŽ1Æïy
|
||||
¿ÀxâØ‰“+]X!±BË"ñ | ||||