Compare commits

...

37 Commits

Author SHA1 Message Date
Pariece McKinney b8f155974c Public release/2.2.12 (#1555)
- Fixed `ORKHealthKitQuantityTypeAnswerFormat`issues
- Improved `ORKReviewStep` initialization
- Introduced a new property called `shouldAutomaticallyAdjustImageTintColor` on `ORKStep` to automatically adopt dark mode version of an image
- Added `ORKSESQuestionResult` to Table View Providers
- ORKCatalog Improvements
- ORKImageSelectionView improvements
- Bumped version to 2.2.12
2023-11-01 18:18:20 -04:00
Pariece McKinney 2fb22a6256 Public release/2.2.10 (#1548)
Fix ORKNavigationContainerView so that it does not call didMoveToWindow from tintColorDidChange
Bumped version to 2.2.10
2023-05-10 17:40:25 -04:00
ronzilla-apple bd60d945bb Public release/2.2.9 (#1541)
* Address issue where there was no option to "Save Results" if task is cancelled (Issue #1536/1540)
* Address an issue where ORKTaskViewController setting tintColor on stepViewController views broke ORKStepViewController's addResult:

* Fixed order issue of tone audiometry steps
* Include missing ORKSpeechRecognition header export (Issue #1534)
* Switch to contentEdgeInsets API in ORKContinueButton to fix an issue where button title wouldn't always fit onscreen (on iOS 16 during long press) (Issue #1533)
* Remove min/max values for ORKCatalog "Numeric with Display Unit"

* xcconfig file updates for version and driving ORKCatalog bundleID
* Addressing compiler warnings
* Added unit tests for init'ing taskViewController with restoration data
* Adding task restoration feature to ORKCatalog's taskViewController presentation
2023-02-13 11:03:48 -08:00
Pariece McKinney 8f58410af9 Release 2.2.8
-Bug fixes and enhancements
-New helper method (ORKViewTintColor) for setting tintColor
-New “displayUnit” property added to ORKAnswerFormat
-Updates in ORKNormalizedReactionTimeResult’s secure coding (breaks compatibility with previous archives).
2022-12-20 11:14:59 -08:00
Louie fc67cc944c Merge pull request #1519 from cbaker6/fixTintColor
Propagate tintColor if window not available
2022-11-29 20:22:37 -08:00
Louie c8473017ec Merge pull request #1531 from ResearchKit/cocoapod_fix
remove deprecated ORKConsent files
2022-11-29 13:21:51 -08:00
Louis Chatta 2bc0033aa8 remove deprecated ORKConsent files 2022-11-29 11:51:42 -08:00
Corey Baker 64a068596b make suggested changes 2022-11-28 15:25:59 -05:00
Corey Baker c10693930d Fix tintColor for ORKWebViewStepViewController buttons 2022-11-15 20:48:25 -05:00
Corey Baker c0f309edf3 only check for tintColor once in viewControllerForStep 2022-11-15 09:16:44 -05:00
Corey Baker b1cc631748 propagate tintColor if window not available 2022-11-14 14:46:02 -05:00
Pariece McKinney a89059f5dc 2.1.1 Release
-Bug fixes
-New ORKAccuracyStroopStep
-AudioStep now allows for audio recording 
-New Styles for ORKDontKnowButton
-General layout and UI improvements
2022-10-26 16:07:45 -07:00
Will 2cc8f9e7d5 Prevent isPasscodeStoredInKeychain crash, correct warnings (#1505)
* fix: prevent isPasscodeStoredInKeychain from causing a crash

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

* New visual style for request permission step

* Add a new notifications permission type

* Add a new motion activity permission type

* Update ORKRequestPermissionButton.m

* Forward declare button

* Update ResearchKit/Common/ORKPermissionType.m

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

* Update ResearchKit/Common/ORKPermissionType.h

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

* Update ResearchKit/Common/ORKMotionActivityPermissionType.h

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

* Update ResearchKit/Common/ORKPermissionType.h

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

* Update ResearchKit/Common/ORKPermissionType.h

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

* Add missing import

* Add missing import

Co-authored-by: joeylabarck-apple <81833193+joeylabarck-apple@users.noreply.github.com>
2021-04-16 17:02:45 -07:00
Steve Moser cb6cc97fa4 Fix broken Apple Forums link 2021-03-01 08:26:36 -05:00
srinathtm-apple 0651bf0c2a Use wheel date picker style (#1447)
* Use wheel date picker style
2021-02-11 19:16:32 -08:00
akshay-yadav ae9b9e57cb updates 2019-10-24 14:19:09 -07:00
737 changed files with 21345 additions and 16506 deletions
-5
View File
@@ -1,5 +0,0 @@
language: objective-c
osx_image: xcode11
xcode_project: ResearchKit.xcodeproj
xcode_scheme: ResearchKit
xcode_destination: platform=iOS Simulator,OS=13.0,name=iPhone 11 Pro Max
+16 -2
View File
@@ -16,7 +16,7 @@ for medical research or for other research projects.
Getting More Information
========================
* Join the [*ResearchKit* Forum](https://forums.developer.apple.com/community/researchkit) for discussing uses of the *ResearchKit framework and* related projects.
* Join the [*ResearchKit* Forum](https://developer.apple.com/forums/tags/researchkit) for discussing uses of the *ResearchKit framework and* related projects.
Use Cases
===========
@@ -82,12 +82,26 @@ The latest stable version of *ResearchKit framework* can be cloned with
git clone -b stable https://github.com/ResearchKit/ResearchKit.git
```
Or, for the latest changes, use the `master` branch:
Or, for the latest changes, use the `main` branch:
```
git clone https://github.com/ResearchKit/ResearchKit.git
```
CocoaPods Installation
------------
For latest stable release
```
pod 'ResearchKit'
```
For early development releases
```
pod 'ResearchKit', :git => 'https://github.com/ResearchKit/ResearchKit.git', :branch => 'main'
```
Building
--------
-6
View File
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj">
</FileRef>
<FileRef
location = "group:ResearchKit.xcodeproj">
</FileRef>
@@ -13,7 +10,4 @@
<FileRef
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
</FileRef>
<FileRef
location = "group:samples/ORKSample/ORKSample.xcodeproj">
</FileRef>
</Workspace>
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'ResearchKit'
s.version = '2.1.0-beta'
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/'
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
version = "1.3">
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1310"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "NO">
@@ -40,33 +40,21 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B183A4731A8535D100C76870"
BuildableName = "ResearchKit.framework"
BlueprintName = "ResearchKit"
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
BuildableName = "ResearchKitTests.xctest"
BlueprintName = "ResearchKitTests"
ReferencedContainer = "container:ResearchKit.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<TestPlans>
<TestPlanReference
reference = "container:ResearchKit.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
enableUBSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1310"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1310"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
+24
View File
@@ -0,0 +1,24 @@
{
"configurations" : [
{
"id" : "A9C8689E-BBEE-4643-8C5D-DBA6A5AF01B2",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:ResearchKit.xcodeproj",
"identifier" : "86CC8E991AC09332001CCD89",
"name" : "ResearchKitTests"
}
}
],
"version" : 1
}
+1 -1
View File
@@ -57,7 +57,7 @@ This method signifies that the step is about to end so any necessary clean up be
You can also optionally pass back an array of ORKResults.
*/
- (nullable NSArray<ORKResult *> *)provideResults;
- (nullable NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier;
@end
+2 -2
View File
@@ -42,7 +42,7 @@ NSNotificationName const ORK3DModelEndStepNotification = @"ORK3DModelEndStepNoti
self = [super init];
if (self) {
_allowsSelection = YES;
_allowsSelection = NO;
_highlightColor = [UIColor yellowColor];
_identifiersOfObjectsToHighlight = nil;
}
@@ -117,7 +117,7 @@ NSNotificationName const ORK3DModelEndStepNotification = @"ORK3DModelEndStepNoti
[NSException raise:@"stepWillEnd not overwitten" format:@"Subclasses must overwrite the stepWillEnd function"];
}
- (NSArray<ORKResult *> *)provideResults {
- (NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier {
[NSException raise:@"provideResults not overwitten" format:@"Subclasses must overwrite the provideResults function"];
return nil;
}
+2 -3
View File
@@ -73,7 +73,7 @@
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self ) {
ORK_DECODE_OBJ(aDecoder, modelManager);
ORK_DECODE_OBJ_CLASS(aDecoder, modelManager, ORK3DModelManager);
}
return self;
}
@@ -90,8 +90,7 @@
return (isParentSame && ORKEqualObjects(self.modelManager, castObject.modelManager));
}
- (NSUInteger)hash
{
- (NSUInteger)hash {
return [super hash] ^ [_modelManager hash];
}
@@ -45,7 +45,7 @@
@implementation ORK3DModelStepViewController {
ORK3DModelManager *_modelManager;
ORK3DModelStepContentView *_stepContentview;
ORK3DModelStepContentView *_stepContentView;
ORK3DModelStep *_step;
}
@@ -62,13 +62,13 @@
- (void)viewDidLoad {
[super viewDidLoad];
_stepContentview = [ORK3DModelStepContentView new];
_stepContentview.translatesAutoresizingMaskIntoConstraints = NO;
self.activeStepView.activeCustomView = _stepContentview;
_stepContentView = [ORK3DModelStepContentView new];
_stepContentView.translatesAutoresizingMaskIntoConstraints = NO;
self.activeStepView.activeCustomView = _stepContentView;
self.activeStepView.customContentFillsAvailableSpace = NO;
self.activeStepView.navigationFooterView.neverHasContinueButton = NO;
[[_stepContentview.bottomAnchor constraintEqualToAnchor:self.activeStepView.navigationFooterView.topAnchor] setActive:YES];
[[_stepContentView.bottomAnchor constraintEqualToAnchor:self.activeStepView.navigationFooterView.topAnchor] setActive:YES];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(disableContinueButton:)
@@ -97,7 +97,7 @@
- (void)activate3DModelManager {
_modelManager = _step.modelManager;
[_modelManager addContentToView:_stepContentview];
[_modelManager addContentToView:_stepContentView];
}
- (void)stepDidFinish {
@@ -118,7 +118,7 @@
ORKStepResult *stepResult = [super result];
if (_modelManager) {
NSArray<ORKResult *> *managerResults = [_modelManager provideResults];
NSArray<ORKResult *> *managerResults = [_modelManager provideResultsWithIdentifier:self.step.identifier];
if (managerResults) {
stepResult.results = [managerResults copy];
}
@@ -0,0 +1,56 @@
/*
Copyright (c) 2021, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <ResearchKit/ORKStroopResult.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKAccuracyStroopResult : ORKStroopResult
/**
A value that indicates whether the user selected the correct color (i.e. the base display color).
*/
@property (nonatomic, readonly) BOOL didSelectCorrectColor;
/**
A value that indicates how long it took for the user to make a selection.
*/
@property (nonatomic, assign) NSTimeInterval timeTakenToSelect;
/**
A value that indicates how far away (in pixels) that the user selected away from the center
of the correct circle.
*/
@property (nonatomic) double distanceToClosestCenter;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,99 @@
/*
Copyright (c) 2021, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKAccuracyStroopResult.h"
#import "ORKResult_Private.h"
#import "ORKHelpers_Internal.h"
@interface ORKAccuracyStroopResult ()
@property (readwrite) BOOL didSelectCorrectColor;
@end
@implementation ORKAccuracyStroopResult
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
ORK_DECODE_DOUBLE(coder, distanceToClosestCenter);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
ORK_ENCODE_BOOL(coder, didSelectCorrectColor);
ORK_ENCODE_DOUBLE(coder, timeTakenToSelect);
ORK_ENCODE_DOUBLE(coder, distanceToClosestCenter);
}
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAccuracyStroopResult *result = [super copyWithZone:zone];
result.distanceToClosestCenter = self.distanceToClosestCenter;
return result;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
self.distanceToClosestCenter == castObject.distanceToClosestCenter);
}
- (NSUInteger)hash {
return [super hash] ^ @(self.didSelectCorrectColor).hash ^ @(self.timeTakenToSelect).hash ^ @(self.distanceToClosestCenter).hash;
}
#pragma mark - ResearchKit
- (BOOL)didSelectCorrectColor {
_didSelectCorrectColor = [self.color isEqualToString:self.colorSelected];
return _didSelectCorrectColor;
}
- (NSString *)descriptionWithNumberOfPaddingSpaces:(NSUInteger)numberOfPaddingSpaces {
return [NSString stringWithFormat:@"%@; didSelectCorrectColor: %i; timeTakenToSelect: %.3f; distanceToClosestCenter: %.0f %@",
[self descriptionPrefixWithNumberOfPaddingSpaces:numberOfPaddingSpaces],
self.didSelectCorrectColor,
self.timeTakenToSelect,
self.distanceToClosestCenter,
self.descriptionSuffix];
}
@end
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2021, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -28,31 +28,40 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import UIKit;
#import <ResearchKit/ORKActiveStep.h>
NS_ASSUME_NONNULL_BEGIN
@class ORKVisualConsentStepViewController;
@class ORKVisualConsentTransitionAnimator;
@protocol ORKVisualConsentTransitionAnimatorDelegate;
ORK_CLASS_AVAILABLE
@interface ORKAccuracyStroopStep : ORKActiveStep
typedef void (^ORKVisualConsentAnimationCompletionHandler)(ORKVisualConsentTransitionAnimator *animator, UIPageViewControllerNavigationDirection direction);
/**
The color of the label.
@interface ORKVisualConsentTransitionAnimator : NSObject
The base display color is the color that the user must tap on to be correct. The text of
the label may match the base display color depending on the `isColorMatching` property.
*/
@property (nonatomic) UIColor *baseDisplayColor;
- (instancetype)initWithVisualConsentStepViewController:(ORKVisualConsentStepViewController *)stepViewController
movieURL:(NSURL *)movieURL;
/**
Whether the text and base display color are matching.
@property (nonatomic, readonly, copy) NSURL *movieURL;
If this value is true, the text of the label will spell out the same color as the base display
color, making the task easier for the user. If this value is false, the label color and label text
will represent different colors, which adds complexity to the puzzle task.
*/
@property (nonatomic) BOOL isColorMatching;
- (void)animateTransitionWithDirection:(UIPageViewControllerNavigationDirection)direction
loadHandler:(nullable ORKVisualConsentAnimationCompletionHandler)loadHandler
completionHandler:(nullable ORKVisualConsentAnimationCompletionHandler)handler;
/**
The text of the label. (read-only)
// Call to invalidate display link and remove any observations.
- (void)finish;
The value of this property is generated based on the `baseDisplayColor` and `isColorMatching`
properties. If `isColorMatching` is false, the actual display color will be randomly generated
to be a color that is not the base display color.
*/
@property (nonatomic, readonly) UIColor *actualDisplayColor;
+ (NSArray <UIColor *> *)colors;
@end
@@ -0,0 +1,107 @@
/*
Copyright (c) 2021, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKAccuracyStroopStep.h"
#import "ORKAccuracyStroopStepViewController.h"
#import "ORKHelpers_Internal.h"
@implementation ORKAccuracyStroopStep
+ (Class)stepViewControllerClass {
return ORKAccuracyStroopStepViewController.class;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super initWithIdentifier:identifier];
if (self) {
self.baseDisplayColor = ORKAccuracyStroopStep.colors[arc4random_uniform(((uint32_t)ORKAccuracyStroopStep.colors.count))];
self.isColorMatching = YES;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_BOOL(aDecoder, isColorMatching);
ORK_DECODE_OBJ_CLASS(aDecoder, baseDisplayColor, UIColor);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_BOOL(aCoder, isColorMatching);
ORK_ENCODE_OBJ(aCoder, baseDisplayColor);
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAccuracyStroopStep *step = [super copyWithZone:zone];
step.isColorMatching = self.isColorMatching;
step.baseDisplayColor = [self.baseDisplayColor copy];
return step;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return isParentSame
&& self.isColorMatching == castObject.isColorMatching
&& ORKEqualObjects(self.baseDisplayColor, castObject.baseDisplayColor);
}
- (NSUInteger)hash {
return super.hash
^ (self.isColorMatching ? 0xf : 0x0)
^ (self.baseDisplayColor ? 0xf : 0x0);
}
+ (NSArray<UIColor *> *)colors {
return @[ UIColor.systemRedColor,
UIColor.systemGreenColor,
UIColor.systemBlueColor,
UIColor.systemYellowColor,
UIColor.systemOrangeColor ];
}
- (UIColor *)actualDisplayColor {
return self.isColorMatching ?
self.baseDisplayColor :
ORKAccuracyStroopStep.colors[arc4random_uniform(((uint32_t) ORKAccuracyStroopStep.colors.count))];
}
@end
@@ -0,0 +1,41 @@
/*
Copyright (c) 2021, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import Foundation;
#import <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKStepViewController.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKAccuracyStroopStepViewController : ORKStepViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,283 @@
/*
Copyright (c) 2021, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKAccuracyStroopStepViewController.h"
#import "ORKAccuracyStroopStep.h"
#import "ORKAccuracyStroopResult.h"
#import "ORKCollectionResult.h"
#import "ORKHelpers_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "UIColor+String.h"
@interface ORKAccuracyStroopStepViewController () <UIGestureRecognizerDelegate>
@property (nonatomic) NSMutableArray <UIView *> *circles;
@property (nonatomic, strong) UILabel *colorLabel;
@property (nonatomic) UIView *circlesView;
@property (nonatomic) NSArray<NSLayoutConstraint *> *constraints;
@property (nonatomic) double distanceToClosestCenter;
@property (nonatomic) UIColor *selectedColor;
@end
@implementation ORKAccuracyStroopStepViewController
- (ORKAccuracyStroopStep *)accuracyStroopStep {
return (ORKAccuracyStroopStep *)self.step;
}
- (void)stepDidChange {
[super stepDidChange];
if (self.step && [self isViewLoaded]) {
[self setupColorLabel];
[self setupCirclesView];
[self setupConstraints];
dispatch_async(dispatch_get_main_queue(), ^{
[self setupCircles];
[self setupViewTap];
});
}
}
- (void)setupViewTap {
for (UIGestureRecognizer *recognizer in self.circlesView.gestureRecognizers) {
[self.circlesView removeGestureRecognizer:recognizer];
}
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tapGestureRecognizer.delegate = self;
[self.circlesView addGestureRecognizer:tapGestureRecognizer];
}
- (void)setupCirclesView {
[self.circlesView removeFromSuperview];
self.circlesView = nil;
self.circlesView = UIView.new;
[self.view addSubview:self.circlesView];
}
- (void)setupColorLabel {
[self.colorLabel removeFromSuperview];
self.colorLabel = nil;
self.colorLabel = UILabel.new;
self.colorLabel.text = self.accuracyStroopStep.actualDisplayColor.textRepresentation;
self.colorLabel.textColor = self.accuracyStroopStep.baseDisplayColor;
self.colorLabel.font = [UIFont systemFontOfSize:35.0 weight:UIFontWeightMedium];
[self.view addSubview:self.colorLabel];
}
- (void)setupConstraints {
if (self.constraints) {
[NSLayoutConstraint deactivateConstraints:self.constraints];
}
self.colorLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.circlesView.translatesAutoresizingMaskIntoConstraints = NO;
self.constraints = nil;
self.constraints = @[
[NSLayoutConstraint constraintWithItem:self.colorLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:10.0],
[NSLayoutConstraint constraintWithItem:self.colorLabel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:self.circlesView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.colorLabel
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:self.circlesView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:self.circlesView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:self.circlesView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0]
];
[NSLayoutConstraint activateConstraints:self.constraints];
}
- (void)setupCircles {
for (UIView *circle in self.circles) {
[circle removeFromSuperview];
}
[self.circles removeAllObjects];
self.circles = NSMutableArray.array;
// Constants to use for ball and grid
int ballSize = 50;
int padding = 10;
int cellSize = ballSize + padding * 2;
// Calculating number of rows/columns in grid to layout color circles
uint32_t numRows = (self.circlesView.bounds.size.height) / cellSize;
uint32_t numColumns = (self.circlesView.bounds.size.width) / cellSize;
// Extra padding to ensure that the grid spans the whole screen width
int extraHorizontalSpaceForCell = ((int)self.circlesView.bounds.size.width % cellSize) / numColumns;
// Matrix to keep track of cells that already have a circle --> avoid overlap in O(n)
bool cellTakenMatrix[numRows][numColumns];
for (uint32_t r = 0; r < numRows; r++) {
for (uint32_t c = 0; c < numColumns; c++) {
cellTakenMatrix[r][c] = false;
}
}
for (int colorIndex = 0; colorIndex < ORKAccuracyStroopStep.colors.count; colorIndex++) {
// Obtain random location for color circle within bounds
int randomR = (int)arc4random_uniform(numRows);
int randomC = (int)arc4random_uniform(numColumns);
ORK_Log_Debug("Trying placement for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
// If cell is already taken, look at 8 spots around for a free spot
if (cellTakenMatrix[randomR][randomC]) {
ORK_Log_Debug("Position (r, c): (%d, %d) already taken", randomR, randomC);
// Loops through the 3x3 grid with randomR,randomC as the center
bool shouldBreak = false;
for (int r = randomR - 1; !shouldBreak && r <= randomR + 1; r++) {
for (int c = randomC - 1; !shouldBreak && c <= randomC + 1; c++) {
// If r/c are out of circleView's bounds, then don't consider
if ((r < 0 || r >= numRows) || (c < 0 || c >= numColumns)) { continue; }
// If cell is not taken, then can assign to there and break out of for-loops
if (!cellTakenMatrix[r][c]) {
randomR = r;
randomC = c;
shouldBreak = true;
}
}
}
}
ORK_Log_Info("Final position for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
cellTakenMatrix[randomR][randomC] = true;
CGFloat circleX = (randomC * (cellSize + extraHorizontalSpaceForCell)) + padding + extraHorizontalSpaceForCell / 2;
CGFloat circleY = (randomR * cellSize) + padding;
CGRect frame = CGRectMake(circleX, circleY, ballSize, ballSize);
UIView *newCircle = [[UIView alloc] initWithFrame:frame];
newCircle.backgroundColor = ORKAccuracyStroopStep.colors[colorIndex];
newCircle.clipsToBounds = YES;
newCircle.layer.cornerRadius = ballSize / 2;
newCircle.tag = colorIndex;
[self.circles addObject:newCircle];
[self.circlesView addSubview:newCircle];
}
}
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
CGPoint touchPoint = [recognizer locationInView:self.circlesView];
double minDistance = INFINITY;
for (UIView *circle in self.circles) {
double dx = (touchPoint.x - circle.center.x);
double dy = (touchPoint.y - circle.center.y);
double distance = sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
minDistance = distance;
}
if (CGRectContainsPoint(circle.frame, touchPoint)) {
self.selectedColor = ORKAccuracyStroopStep.colors[circle.tag];
self.distanceToClosestCenter = distance;
[super goForward];
return;
}
}
self.distanceToClosestCenter = minDistance;
[super goForward];
}
- (BOOL)hasPreviousStep {
return NO;
}
- (ORKStepResult *)result {
ORKStepResult *stepResult = [super result];
ORKAccuracyStroopResult *result = [[ORKAccuracyStroopResult alloc] initWithIdentifier:self.accuracyStroopStep.identifier];
result.color = self.accuracyStroopStep.baseDisplayColor.textRepresentation;
result.colorSelected = self.selectedColor.textRepresentation;
result.distanceToClosestCenter = self.distanceToClosestCenter;
result.startDate = stepResult.startDate;
result.endDate = stepResult.endDate;
result.timeTakenToSelect = [result.endDate timeIntervalSinceDate:result.startDate];
NSMutableArray *results = [[NSMutableArray alloc] init];
if (stepResult.results) {
results = [stepResult.results mutableCopy];
}
[results addObject:result];
stepResult.results = [results copy];
return stepResult;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self stepDidChange];
}
@end
+3 -49
View File
@@ -30,30 +30,26 @@
#import "ORKActiveStepTimer.h"
#import "ORKHelpers_Internal.h"
@import UIKit;
#include <mach/mach.h>
#include <mach/mach_time.h>
static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
static mach_timebase_info_data_t sTimebaseInfo;
if ( sTimebaseInfo.denom == 0 ) {
(void) mach_timebase_info(&sTimebaseInfo);
static mach_timebase_info_data_t sTimebaseInfo;
if (sTimebaseInfo.denom == 0) {
(void)mach_timebase_info(&sTimebaseInfo);
}
uint64_t elapsedNano = delta * sTimebaseInfo.numer / sTimebaseInfo.denom;
return elapsedNano * 1.0 / NSEC_PER_SEC;
}
@implementation ORKActiveStepTimer {
uint64_t _startTime;
NSTimeInterval _preExistingRuntime;
dispatch_queue_t _queue;
dispatch_source_t _timer;
UIBackgroundTaskIdentifier _backgroundTaskIdentifier;
uint32_t _isRunning;
}
@@ -68,7 +64,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
_interval = interval;
_handler = [handler copy];
_preExistingRuntime = runtime;
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
_queue = dispatch_queue_create("active_step", DISPATCH_QUEUE_SERIAL);
@@ -128,7 +123,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
}
- (void)queue_event {
[self queue_assertBackgroundTask];
NSTimeInterval runtime = [self queue_runtime];
BOOL finished = (runtime >= _duration);
@@ -138,13 +132,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
dispatch_async(dispatch_get_main_queue(), ^{
_handler(self, finished);
dispatch_sync(_queue, ^{
// If the timer is still NULL here, we can safely release the background task.
if (_timer == NULL) {
[self queue_releaseBackgroundTask];
}
});
});
}
@@ -156,29 +143,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
}
}
- (void)queue_releaseBackgroundTask {
if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
return;
}
UIBackgroundTaskIdentifier identifier = _backgroundTaskIdentifier;
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] endBackgroundTask:identifier];
});
}
- (void)queue_assertBackgroundTask {
if (_backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
return;
}
_backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
// This is guaranteed to be called synchronously on the main queue, switch to our queue to invalidate the identifier
dispatch_sync(_queue, ^{
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
});
}];
}
- (void)queue_resume {
if (_timer != NULL) {
// Already resumed
@@ -190,11 +154,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
return;
}
// We want to run in the background if we can, so voice can be played, etc.
assert(_backgroundTaskIdentifier == UIBackgroundTaskInvalid);
[self queue_assertBackgroundTask];
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
if (_timer == NULL) {
@@ -228,16 +187,11 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
_preExistingRuntime += timeIntervalFromMachTime(now - _startTime);
_startTime = 0;
if (!atFinish) {
// If we are atFinish, the task will be released after the handler completes
[self queue_releaseBackgroundTask];
}
}
- (void)queue_reset {
[self queue_clearTimer];
_preExistingRuntime = 0;
[self queue_releaseBackgroundTask];
}
@end
@@ -45,6 +45,9 @@
#import "ORKTaskViewController_Internal.h"
#import "ORKRecorder_Internal.h"
#import "ORKStepView_Private.h"
#import "ORKStepContentView.h"
#import "ORKActiveStep_Internal.h"
#import "ORKCollectionResult_Private.h"
#import "ORKResult.h"
@@ -125,6 +128,7 @@
if (_customView) {
_activeStepView.customContentView = _customView;
}
_activeStepView.stepContentView.shouldAutomaticallyAdjustImageTintColor = YES;
[self.view addSubview:_activeStepView];
}
@@ -295,8 +299,9 @@
outputDirectory:self.outputDirectory];
recorder.configuration = provider;
recorder.delegate = self;
[recorders addObject:recorder];
if (recorder) {
[recorders addObject:recorder];
}
}
self.recorders = recorders;
@@ -566,7 +571,7 @@ static NSString *const _ORKRecorderResultsRestoreKey = @"recorderResults";
[super decodeRestorableStateWithCoder:coder];
self.finished = [coder decodeBoolForKey:_ORKFinishedRestoreKey];
_recorderResults = [coder decodeObjectOfClass:[NSArray class] forKey:_ORKRecorderResultsRestoreKey];
_recorderResults = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSArray.self, ORKResult.self]] forKey:_ORKRecorderResultsRestoreKey];
}
@end
@@ -35,6 +35,15 @@
NS_ASSUME_NONNULL_BEGIN
@class ORKRecordButton;
typedef NS_ENUM(NSUInteger, ORKAudioContentViewEvent) {
ORKAudioContentViewEventStartRecording = 0,
ORKAudioContentViewEventStopRecording
};
typedef void (^ORKAudioStepContentViewEventHandler)(ORKAudioContentViewEvent);
@interface ORKAudioContentView : ORKActiveStepCustomView
@property (nonatomic, copy, nullable) UIColor *keyColor;
@@ -48,6 +57,10 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy, nullable) NSArray *samples;
@property (nonatomic) BOOL useRecordButton;
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler;
// Samples should be in the range of (0, 1).
- (void)addSample:(NSNumber *)sample;
- (void)removeAllSamples;
@@ -55,3 +68,4 @@ NS_ASSUME_NONNULL_BEGIN
@end
NS_ASSUME_NONNULL_END
+90 -16
View File
@@ -29,8 +29,9 @@
*/
#import "ORKAudioContentView.h"
#import "ORKAudioGraphView.h"
#import "ORKAudioMeteringView.h"
#import "ORKHeadlineLabel.h"
#import "ORKLabel.h"
@@ -38,6 +39,7 @@
#import "ORKAccessibility.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKRecordButton.h"
// The central blue region.
@@ -46,6 +48,8 @@ static const CGFloat GraphViewBlueZoneHeight = 170;
// The two bands at top and bottom which are "loud" each have this height.
static const CGFloat GraphViewRedZoneHeight = 25;
static const CGFloat ORKAudioStepContentRecordButtonVerticalSpacing = 20.0;
@interface ORKAudioTimerLabel : ORKLabel
@end
@@ -61,11 +65,12 @@ static const CGFloat GraphViewRedZoneHeight = 25;
@end
@interface ORKAudioContentView ()
@interface ORKAudioContentView () <ORKRecordButtonDelegate>
@property (nonatomic, strong) ORKHeadlineLabel *alertLabel;
@property (nonatomic, strong) UILabel *timerLabel;
@property (nonatomic, strong) ORKAudioGraphView *graphView;
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
@property (nonatomic, copy, nullable) ORKAudioStepContentViewEventHandler viewEventhandler;
@end
@@ -73,19 +78,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
@implementation ORKAudioContentView {
NSMutableArray *_samples;
UIColor *_keyColor;
ORKRecordButton *_recordButton;
BOOL _checkAudioLevel;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
_checkAudioLevel = YES;
_useRecordButton = NO;
self.alertLabel = [ORKHeadlineLabel new];
_alertLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.timerLabel = [ORKAudioTimerLabel new];
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
_timerLabel.textAlignment = NSTextAlignmentRight;
self.graphView = [ORKAudioGraphView new];
self.graphView = [[ORKAudioMeteringView alloc] init];
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
@@ -122,10 +131,22 @@ static const CGFloat GraphViewRedZoneHeight = 25;
[self updateAlertLabelHidden];
}
- (void)setUseRecordButton:(BOOL)useRecordButton {
_useRecordButton = useRecordButton;
if (_useRecordButton) {
_checkAudioLevel = NO;
[_timerLabel setHidden: YES];
[self setupRecordButton];
[self setUpConstraints];
}
}
- (void)applyKeyColor {
UIColor *keyColor = [self keyColor];
_timerLabel.textColor = keyColor;
_graphView.keyColor = keyColor;
_graphView.meterColor = keyColor;
}
- (UIColor *)keyColor {
@@ -143,6 +164,44 @@ static const CGFloat GraphViewRedZoneHeight = 25;
_graphView.alertColor = alertColor;
}
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler {
self.viewEventhandler = [handler copy];
}
- (void)invokeViewEventHandlerWithEvent:(ORKAudioContentViewEvent)event {
if (self.viewEventhandler) {
dispatch_async(dispatch_get_main_queue(), ^{
self.viewEventhandler(event);
});
}
}
- (void)buttonPressed:(ORKRecordButton *)recordButton {
switch (recordButton.buttonType) {
case ORKRecordButtonTypeRecord:
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStartRecording];
[_recordButton setButtonType:ORKRecordButtonTypeStop];
break;
default:
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStopRecording];
[_recordButton setButtonState:ORKRecordButtonStateDisabled];
break;
}
}
- (void)setupRecordButton {
if (!_recordButton) {
_recordButton = [[ORKRecordButton alloc] init];
_recordButton.delegate = self;
_recordButton.translatesAutoresizingMaskIntoConstraints = NO;
[_recordButton setButtonType:ORKRecordButtonTypeRecord];
[self addSubview:_recordButton];
}
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
@@ -161,12 +220,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
const CGFloat innerMargin = 2;
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
views:views]];
if (_useRecordButton) {
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin)}
views:views]];
[constraints addObject:[_recordButton.topAnchor constraintEqualToAnchor:_graphView.bottomAnchor constant:ORKAudioStepContentRecordButtonVerticalSpacing]];
[constraints addObject:[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor]];
} else {
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
views:views]];
}
[constraints addObject:[NSLayoutConstraint constraintWithItem:_graphView
attribute:NSLayoutAttributeHeight
@@ -206,18 +276,21 @@ static const CGFloat GraphViewRedZoneHeight = 25;
}
- (void)updateGraphSamples {
_graphView.values = _samples;
_graphView.samples = _samples;
[self updateAlertLabelHidden];
}
- (void)updateAlertLabelHidden {
NSNumber *sample = _samples.lastObject;
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
if (_alertLabel.hidden && show) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
if (_checkAudioLevel) {
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
if (_alertLabel.hidden && show) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
}
_alertLabel.hidden = !show;
}
_alertLabel.hidden = !show;
}
- (void)setSamples:(NSArray *)samples {
@@ -260,3 +333,4 @@ static const CGFloat GraphViewRedZoneHeight = 25;
}
@end
@@ -0,0 +1,64 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <ResearchKit/ResearchKit.h>
#import <ResearchKit/ORKFitnessStep.h>
NS_ASSUME_NONNULL_BEGIN
@class ORKBundleAsset;
ORK_CLASS_AVAILABLE
@interface ORKVocalCue : NSObject <NSSecureCoding, NSCopying>
@property (atomic) NSTimeInterval time;
@property (atomic, copy) NSString *spokenText;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithTime:(NSTimeInterval) time
spokenText:(NSString *) spokenText;
@end
ORK_CLASS_AVAILABLE
@interface ORKAudioFitnessStep : ORKFitnessStep
@property (nonatomic, copy) ORKBundleAsset *audioAsset;
@property (nonatomic, copy) NSArray<ORKVocalCue *> *vocalCues;
- (instancetype)initWithIdentifier:(NSString *) identifier
audioAsset:(ORKBundleAsset *) audioAsset
vocalCues:(nullable NSArray<ORKVocalCue *> *) vocalCues;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,146 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKAudioFitnessStep.h"
#import "ORKAudioFitnessStepViewController.h"
#import "ORKBundleAsset.h"
#import "ORKHelpers_Internal.h"
@implementation ORKVocalCue
- (instancetype)initWithTime:(NSTimeInterval)time
spokenText:(NSString *)spokenText {
self = [super init];
if (self) {
self.time = time;
self.spokenText = [spokenText copy];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
if (self) {
ORK_DECODE_DOUBLE(coder, time);
ORK_DECODE_OBJ_CLASS(coder, spokenText, NSString);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
ORK_ENCODE_DOUBLE(coder, time);
ORK_ENCODE_OBJ(coder, spokenText);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
return [[ORKVocalCue alloc] initWithTime:self.time spokenText:self.spokenText];
}
- (BOOL)isEqual:(id)other
{
if ([self class] != [other class]) {
return NO;
}
__typeof(self) castObject = other;
return (self.time == castObject.time &&
ORKEqualObjects(self.spokenText, castObject.spokenText));
}
@end
@implementation ORKAudioFitnessStep
- (Class)stepViewControllerClass {
return [ORKAudioFitnessStepViewController class];
}
- (instancetype)initWithIdentifier:(NSString *)identifier
audioAsset:(ORKBundleAsset *)audioAsset
vocalCues:(nullable NSArray<ORKVocalCue *> *)vocalCues {
self = [super initWithIdentifier:identifier];
if (self) {
self.stepDuration = 180;
self.shouldShowDefaultTimer = NO;
self.audioAsset = [audioAsset copy];
self.vocalCues = vocalCues == nil ? [NSArray new] : [vocalCues copy];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
ORK_DECODE_OBJ_CLASS(coder, audioAsset, ORKBundleAsset);
ORK_DECODE_OBJ_ARRAY(coder, vocalCues, ORKVocalCue);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
ORK_ENCODE_OBJ(coder, audioAsset);
ORK_ENCODE_OBJ(coder, vocalCues);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAudioFitnessStep *step = [super copyWithZone:zone];
step.audioAsset = [self.audioAsset copy];
step.vocalCues = [self.vocalCues copy];
return step;
}
- (BOOL)isEqual:(id)other
{
BOOL superIsEqual = [super isEqual:other];
__typeof(self) castObject = other;
return (superIsEqual &&
ORKEqualObjects(self.audioAsset, castObject.audioAsset) &&
ORKEqualObjects(self.vocalCues, castObject.vocalCues));
}
- (NSUInteger)hash
{
return super.hash ^ self.audioAsset.hash ^ self.vocalCues.hash;
}
@end
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Ricardo Sánchez-Sáez.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -28,25 +28,22 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKConsentSceneViewController.h"
#import "ORKStepContainerView.h"
#import <ResearchKit/ResearchKit.h>
#import <ResearchKit/ORKFitnessStepViewController.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKConsentSceneView : ORKStepContainerView
// Test Seam
@protocol ORKAudioPlayer
- (BOOL)prepareToPlay;
- (BOOL)play;
- (void)pause;
- (void)stop;
@end
@interface ORKAudioFitnessStepViewController : ORKFitnessStepViewController
@interface ORKConsentSceneViewController ()
@property (nonatomic, readonly) ORKConsentSceneView *sceneView;
@property (nonatomic, readonly) UIScrollView *scrollView;
- (void)scrollToTopAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion;
@property (nonatomic) id<ORKAudioPlayer> audioPlayer;
@end
@@ -0,0 +1,150 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKActiveStepTimer.h"
#import "ORKAudioFitnessStep.h"
#import "ORKAudioFitnessStepViewController.h"
#import "ORKVoiceEngine.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKHelpers_Internal.h"
#import <AVFoundation/AVFoundation.h>
@interface ORKAVAudioPlayer : AVAudioPlayer <ORKAudioPlayer>
@end
@implementation ORKAVAudioPlayer
@end
@interface ORKAudioFitnessStepViewController ()
@property (nonatomic) BOOL appHasAudioBackgroundMode;
@property (nonatomic) NSMutableSet<ORKVocalCue *> *playedCues;
@end
@implementation ORKAudioFitnessStepViewController
- (ORKAudioFitnessStep *)audioStep {
return (ORKAudioFitnessStep *)self.step;
}
- (NSMutableSet *)playedCues {
if (!_playedCues) {
_playedCues = [NSMutableSet new];
}
return _playedCues;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.audioPlayer prepareToPlay];
}
- (void)start {
[super start];
if (self.appHasAudioBackgroundMode) {
[self enableBackgroundAudioSession:YES];
}
[self.audioPlayer play];
}
- (void)suspend {
[super suspend];
[self.audioPlayer pause];
}
- (void)resume {
[super resume];
[self.audioPlayer play];
}
- (void)finish {
[super finish];
[self.audioPlayer stop];
if (self.appHasAudioBackgroundMode) {
[self enableBackgroundAudioSession:NO];
}
}
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
[super countDownTimerFired:timer finished:finished];
ORKVoiceEngine *voice = [ORKVoiceEngine sharedVoiceEngine];
NSTimeInterval timeRemaining = [timer duration] - [timer runtime];
for (ORKVocalCue *cue in [self audioStep].vocalCues) {
if (cue.time >= timeRemaining && ![self.playedCues containsObject:cue]) {
[self.playedCues addObject:cue];
[voice speakText: cue.spokenText];
}
}
}
- (BOOL)appHasAudioBackgroundMode {
NSArray<NSString *> *backgroundModes = (NSArray<NSString *> *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
BOOL hasBackgroundAudioMode = [backgroundModes containsObject:@"audio"];
return hasBackgroundAudioMode;
}
- (void)enableBackgroundAudioSession:(BOOL)enabled {
NSError *error;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
mode:AVAudioSessionModeDefault
routeSharingPolicy:AVAudioSessionRouteSharingPolicyLongFormAudio
options:0
error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessStepViewController failed to setup audio session: %@", error);
return;
}
[[AVAudioSession sharedInstance] setActive:enabled error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessViewController failed to start audio session: %@", error);
return;
}
}
- (id<ORKAudioPlayer>)audioPlayer {
if (!_audioPlayer) {
ORKAudioFitnessStep *step = [self audioStep];
NSError *error;
_audioPlayer = [[ORKAVAudioPlayer alloc] initWithContentsOfURL:step.audioAsset.url error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessStepViewController Failed to load audio file: %@", error.localizedFailureReason);
}
}
return _audioPlayer;
}
@end
+4 -9
View File
@@ -28,20 +28,15 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import UIKit;
#import <ResearchKit/ORKAudioMeteringView.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKAudioGraphView : UIView
@property (nonatomic, strong) UIColor *keyColor;
@property (nonatomic, strong) UIColor *alertColor;
@property (nonatomic, copy) NSArray *values;
@property (nonatomic) CGFloat alertThreshold;
@interface ORKAudioGraphView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
@end
NS_ASSUME_NONNULL_END
+46 -26
View File
@@ -32,11 +32,21 @@
#import "ORKAudioGraphView.h"
#import "ORKSkin.h"
static const CGFloat ValueLineWidth = 4.5;
static const CGFloat ValueLineMargin = 1.5;
static const CGFloat GraphHeight = 150.0;
@interface ORKAudioGraphView ()
/// ORKAudioMetering
@property (nonatomic, copy, nullable) NSArray<NSNumber *> *samples;
@property (nonatomic, assign) float alertThreshold;
/// ORKAudioMeteringView
@property (nonatomic, strong) UIColor *meterColor;
@property (nonatomic, strong, nullable) UIColor *alertColor;
@end
@implementation ORKAudioGraphView
@@ -46,7 +56,7 @@ static const CGFloat GraphHeight = 150.0;
[self setUpConstraints];
#if TARGET_IPHONE_SIMULATOR
_values = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
_samples = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
#endif
}
return self;
@@ -65,26 +75,6 @@ static const CGFloat GraphHeight = 150.0;
[NSLayoutConstraint activateConstraints:@[heightConstraint]];
}
- (void)setValues:(NSArray *)values {
_values = [values copy];
[self setNeedsDisplay];
}
- (void)setKeyColor:(UIColor *)keyColor {
_keyColor = [keyColor copy];
[self setNeedsDisplay];
}
- (void)setAlertColor:(UIColor *)alertColor {
_alertColor = [alertColor copy];
[self setNeedsDisplay];
}
- (void)setAlertThreshold:(CGFloat)alertThreshold {
_alertThreshold = alertThreshold;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
@@ -104,7 +94,7 @@ static const CGFloat GraphHeight = 150.0;
[centerLine addLineToPoint:(CGPoint){.x = maxX, .y = midY}];
CGContextSetLineWidth(context, 1.0 / scale);
[_keyColor setStroke];
[_meterColor setStroke];
CGFloat lengths[2] = {3, 3};
CGContextSetLineDash(context, 0, lengths, 2);
@@ -125,7 +115,7 @@ static const CGFloat GraphHeight = 150.0;
path1.lineWidth = ValueLineWidth;
UIBezierPath *path2 = [path1 copy];
for (NSNumber *value in [_values reverseObjectEnumerator]) {
for (NSNumber *value in [_samples reverseObjectEnumerator]) {
CGFloat floatValue = value.doubleValue;
UIBezierPath *path = nil;
@@ -134,7 +124,7 @@ static const CGFloat GraphHeight = 150.0;
[_alertColor setStroke];
} else {
path = path2;
[_keyColor setStroke];
[_meterColor setStroke];
}
[path moveToPoint:(CGPoint){.x = x, .y = midY - floatValue*halfHeight}];
[path addLineToPoint:(CGPoint){.x = x, .y = midY + floatValue*halfHeight}];
@@ -150,11 +140,41 @@ static const CGFloat GraphHeight = 150.0;
[_alertColor setStroke];
[path1 stroke];
[_keyColor setStroke];
[_meterColor setStroke];
[path2 stroke];
}
CGContextRestoreGState(context);
}
#pragma mark - ORKAudioMetering
- (void)setSamples:(NSArray<NSNumber *> *)samples
{
_samples = [samples copy];
[self setNeedsDisplay];
}
- (void)setAlertThreshold:(float)threshold
{
_alertThreshold = threshold;
[self setNeedsDisplay];
}
#pragma mark = ORKAudioMeteringView
- (void)setMeterColor:(UIColor *)meterColor
{
_meterColor = [meterColor copy];
[self setNeedsDisplay];
}
- (void)setAlertColor:(UIColor *)alertColor
{
_alertColor = [alertColor copy];
[self setNeedsDisplay];
}
@end
@@ -75,3 +75,4 @@ ORK_CLASS_AVAILABLE
@end
NS_ASSUME_NONNULL_END
@@ -94,7 +94,7 @@ Float32 const VolumeClamp = 60.0;
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, audioLevelStepIdentifier, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, destinationStepIdentifier, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, recordingSettings, NSDictionary);
ORK_DECODE_OBJ_PLIST(aDecoder, recordingSettings);
}
return self;
}
@@ -216,3 +216,4 @@ Float32 const VolumeClamp = 60.0;
@end
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -29,26 +29,30 @@
*/
@import UIKit;
@import UIKit;
NS_ASSUME_NONNULL_BEGIN
@class ORKConsentSection;
extern NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit);
@interface ORKConsentSceneViewController : UIViewController
@protocol ORKAudioMetering <NSObject>
- (instancetype)initWithSection:(ORKConsentSection *)section;
- (void)setSamples:(nullable NSArray<NSNumber *> *)samples;
@property (nonatomic, readonly, nullable) ORKConsentSection *section;
- (void)setAlertThreshold:(float)threshold;
@property (nonatomic, strong, nullable) UIBarButtonItem *continueButtonItem;
@end
@property (nonatomic, strong, nullable) UIBarButtonItem *cancelButtonItem;
@protocol ORKAudioMeteringDisplay
@property (nonatomic, strong, nullable) NSString *learnMoreButtonTitle;
- (void)setMeterColor:(nonnull UIColor *)meterColor;
@property (nonatomic, assign) BOOL imageHidden;
- (void)setAlertColor:(nonnull UIColor *)alertColor;
@end
@interface ORKAudioMeteringView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
@end
@@ -0,0 +1,146 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKAudioMeteringView.h"
#import "ORKAudioGraphView.h"
NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit) {
if (samples.count > limit) {
return [samples subarrayWithRange:(NSRange){samples.count - limit, samples.count - 1}];
}
return [samples copy];
}
@interface ORKAudioMeteringView ()
@property (nonatomic, strong) UIView<ORKAudioMetering, ORKAudioMeteringDisplay> *meteringView;
@end
@implementation ORKAudioMeteringView
- (instancetype)init
{
self = [super init];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (void)configureMeteringView
{
if (!_meteringView) {
[self setMeteringView:[[ORKAudioGraphView alloc] init]];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[_meteringView setFrame:[self bounds]];
}
- (void)setHidden:(BOOL)hidden
{
[super setHidden:hidden];
[_meteringView setHidden:hidden];
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if ([self superview] == nil)
{
[_meteringView removeFromSuperview];
}
else
{
[self addSubview:_meteringView];
}
}
#pragma mark - ORKAudioMetering
- (void)setSamples:(NSArray<NSNumber *> *)samples
{
[_meteringView setSamples:samples];
}
- (void)setAlertThreshold:(float)threshold
{
[_meteringView setAlertThreshold:threshold];
}
#pragma mark - ORKAudioMeteringDisplay
- (void)setMeterColor:(UIColor *)meterColor
{
[_meteringView setMeterColor:meterColor];
}
- (void)setAlertColor:(UIColor *)alertColor
{
[_meteringView setAlertColor:alertColor];
}
#pragma mark - UIAccessibility
- (BOOL)isAccessibilityElement {
return NO;
}
@end
+1 -1
View File
@@ -312,7 +312,7 @@
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, recorderSettings, NSDictionary);
ORK_DECODE_OBJ_PLIST(aDecoder, recorderSettings);
}
return self;
}
+12
View File
@@ -38,6 +38,18 @@ NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKAudioStep : ORKActiveStep
/**
A Boolean value that determines if audio recording will start and stop
automatcially or be controlled via a ORKRecordButton
When set to YES the user will be able to start and stop the audio recording
by the ORKRecordButton
The default value of this property is `NO`.
*/
@property (nonatomic) BOOL useRecordButton;
@end
NS_ASSUME_NONNULL_END
+43 -1
View File
@@ -49,16 +49,27 @@
if (self) {
self.shouldShowDefaultTimer = NO;
self.shouldStartTimerAutomatically = YES;
self.useRecordButton = NO;
}
return self;
}
- (void)setUseRecordButton:(BOOL)useRecordButton {
_useRecordButton = useRecordButton;
[self setShouldStartTimerAutomatically:!_useRecordButton];
if (_useRecordButton) {
self.stepDuration = 0;
}
}
- (void)validateParameters {
[super validateParameters];
NSTimeInterval const ORKAudioTaskMinimumDuration = 5.0;
if ( self.stepDuration < ORKAudioTaskMinimumDuration) {
if ( self.stepDuration < ORKAudioTaskMinimumDuration && !self.useRecordButton) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKAudioTaskMinimumDuration)] userInfo:nil];
}
}
@@ -67,4 +78,35 @@
return NO;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAudioStep *step = [super copyWithZone:zone];
step.useRecordButton = self.useRecordButton;
return step;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_BOOL(aDecoder, useRecordButton);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_BOOL(aCoder, useRecordButton);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame && self.useRecordButton == castObject.useRecordButton);
}
@end
@@ -29,6 +29,7 @@
*/
@import Foundation;
#import <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKActiveStepViewController.h>
@@ -44,3 +45,4 @@ ORK_CLASS_AVAILABLE
@end
NS_ASSUME_NONNULL_END
@@ -59,6 +59,7 @@
ORKAudioContentView *_audioContentView;
ORKAudioRecorder *_audioRecorder;
ORKActiveStepTimer *_timer;
NSTimer *_intervalTimer;
NSError *_audioRecorderError;
}
@@ -83,6 +84,12 @@
// Do any additional setup after loading the view.
_audioContentView = [ORKAudioContentView new];
_audioContentView.timeLeft = self.audioStep.stepDuration;
_audioContentView.useRecordButton = self.audioStep.useRecordButton && self.audioStep.stepDuration == 0;
__weak typeof(self) weakSelf = self;
[_audioContentView setViewEventHandler:^(ORKAudioContentViewEvent event) {
[weakSelf handleContentViewEvent:event];
}];
if (self.alertThreshold > 0) {
_audioContentView.alertThreshold = self.alertThreshold;
@@ -91,6 +98,19 @@
self.activeStepView.activeCustomView = _audioContentView;
}
- (void)handleContentViewEvent:(ORKAudioContentViewEvent)event {
switch (event) {
case ORKAudioContentViewEventStartRecording:
[self start];
break;
case ORKAudioContentViewEventStopRecording:
[self finish];
break;
}
}
- (void)audioRecorderDidChange {
_audioRecorder.audioRecorder.meteringEnabled = YES;
[self setAvAudioRecorder:_audioRecorder.audioRecorder];
@@ -116,42 +136,72 @@
if (_audioRecorderError) {
return;
}
[_avAudioRecorder updateMeters];
float value = [_avAudioRecorder averagePowerForChannel:0];
// Assume value is in range roughly -60dB to 0dB
float clampedValue = MAX(value / 60.0, -1) + 1;
[_audioContentView addSample:@(clampedValue)];
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
if (!self.audioStep.useRecordButton) {
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
}
}
- (void)startNewTimerIfNeeded {
if (!_timer) {
NSTimeInterval duration = self.audioStep.stepDuration;
ORKWeakTypeOf(self) weakSelf = self;
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
ORKStrongTypeOf(self) strongSelf = weakSelf;
[strongSelf doSample];
if (finished) {
[strongSelf finish];
}
}];
[_timer resume];
if (self.audioStep.useRecordButton) {
if (!_intervalTimer) {
_intervalTimer = [NSTimer scheduledTimerWithTimeInterval: 20 / 100
target:self selector:@selector(doSample)
userInfo:nil
repeats:YES];
}
} else {
if (!_timer) {
NSTimeInterval duration = self.audioStep.stepDuration;
ORKWeakTypeOf(self) weakSelf = self;
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
ORKStrongTypeOf(self) strongSelf = weakSelf;
[strongSelf doSample];
if (finished) {
[strongSelf finish];
}
}];
[_timer resume];
}
}
_audioContentView.finished = NO;
}
- (void)start {
[super start];
[self audioRecorderDidChange];
[_timer reset];
_timer = nil;
[self startNewTimerIfNeeded];
if (!self.audioStep.useRecordButton) {
[_timer reset];
_timer = nil;
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
[self startNewTimerIfNeeded];
}
- (void)suspend {
[super suspend];
[_timer pause];
if (!self.audioStep.useRecordButton) {
[_timer pause];
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
if (_avAudioRecorder) {
[_audioContentView addSample:@(0)];
}
@@ -160,8 +210,12 @@
- (void)resume {
[super resume];
[self audioRecorderDidChange];
[self startNewTimerIfNeeded];
[_timer resume];
if (!self.audioStep.useRecordButton) {
[_timer resume];
}
}
- (void)finish {
@@ -169,8 +223,14 @@
return;
}
[super finish];
[_timer reset];
_timer = nil;
if (!self.audioStep.useRecordButton) {
[_timer reset];
_timer = nil;
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
}
- (void)stepDidFinish {
@@ -189,3 +249,4 @@
}
@end
@@ -0,0 +1,53 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import Foundation;
@import AVFoundation;
#import <ResearchKit/ORKRecorder.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ORKAudioStreamingDelegate <ORKRecorderDelegate>
- (void)audioAvailable:(AVAudioPCMBuffer *)buffer;
@end
@class ORKStep;
@interface ORKAudioStreamer : ORKRecorder
- (instancetype)initWithIdentifier:(NSString *)identifier step:(nullable ORKStep *)step NS_DESIGNATED_INITIALIZER;
@property (nonatomic, strong, readonly, nullable) AVAudioEngine *audioEngine;
@end
NS_ASSUME_NONNULL_END
+219
View File
@@ -0,0 +1,219 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKAudioStreamer.h"
#import "ORKHelpers_Internal.h"
#import "ORKRecorder_Internal.h"
#import "ORKStep.h"
#pragma mark - ORKAudioStreamerConfiguration
@implementation ORKAudioStreamerConfiguration
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super initWithIdentifier:identifier];
return self;
}
- (ORKRecorder *)recorderForStep:(ORKStep *)step outputDirectory:(NSURL *)outputDirectory {
ORKAudioStreamer *obj = [[ORKAudioStreamer alloc] initWithIdentifier:self.identifier step:step];
return obj;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (BOOL)isEqual:(id)object {
return [super isEqual:object];
}
- (ORKPermissionMask)requestedPermissionMask {
return ORKPermissionAudioRecording;
}
@end
#pragma mark - ORKAudioStreamer
@implementation ORKAudioStreamer
{
NSString *_savedSessionCategory;
}
- (instancetype)initWithIdentifier:(NSString *)identifier step:(ORKStep *)step
{
self = [super initWithIdentifier:identifier step:step outputDirectory:nil];
if (self)
{
self.continuesInBackground = YES;
}
return self;
}
- (void)restoreSavedAudioSessionCategory
{
if (_savedSessionCategory)
{
NSError *error;
if (![[AVAudioSession sharedInstance] setCategory:_savedSessionCategory error:&error])
{
ORK_Log_Error("Failed to restore the audio session category: %@", [error localizedDescription]);
}
_savedSessionCategory = nil;
}
}
- (BOOL)isRecording
{
return [_audioEngine isRunning];
}
- (NSString *)recorderType
{
return @"audioStreaming";
}
- (void)start
{
if (!_audioEngine)
{
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
_savedSessionCategory = audioSession.category;
NSError *error = nil;
BOOL success =
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error] &&
[[AVAudioSession sharedInstance] setMode:AVAudioSessionModeMeasurement error:&error] &&
[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
if (!success && error)
{
[self finishRecordingWithError:error];
return;
}
ORK_Log_Debug("Create audioEngine recorder %p", self);
_audioEngine = [[AVAudioEngine alloc] init];
AVAudioInputNode *inputnode = _audioEngine.inputNode;
AVAudioFormat *recordingFormat = [inputnode inputFormatForBus:0];
[inputnode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when)
{
id<ORKAudioStreamingDelegate> delegate = (id<ORKAudioStreamingDelegate>)self.delegate;
if (delegate && [delegate respondsToSelector:@selector(audioAvailable:)]) {
[delegate audioAvailable:buffer];
}
}];
[_audioEngine prepare];
[_audioEngine startAndReturnError:&error];
if (error != nil)
{
[self finishRecordingWithError:error];
return;
}
}
[super start];
}
- (void)stop
{
if (!_audioEngine)
{
return;
}
[self doStopRecording];
[super stop];
}
- (void)doStopRecording
{
if (self.isRecording)
{
if ([_audioEngine isRunning])
{
[_audioEngine stop];
[[_audioEngine inputNode] removeTapOnBus:0];
}
_audioEngine = nil;
[self restoreSavedAudioSessionCategory];
}
}
- (void)finishRecordingWithError:(NSError *)error
{
[self doStopRecording];
[super finishRecordingWithError:error];
}
- (void)reset
{
if ([_audioEngine isRunning])
{
[_audioEngine stop];
[[_audioEngine inputNode] removeTapOnBus:0];
}
_audioEngine = nil;
[super reset];
}
- (void)dealloc
{
ORK_Log_Debug("Remove audiorecorder %p", self);
if ([_audioEngine isRunning])
{
[_audioEngine stop];
[[_audioEngine inputNode] removeTapOnBus:0];
}
_audioEngine = nil;
}
@end
@@ -242,6 +242,14 @@ static const CGFloat ProgressIndicatorOuterMargin = 1.0;
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @(_countDown).stringValue);
[_countdownView startAnimateWithDuration:[(ORKActiveStep *)self.step stepDuration]];
[UIApplication.sharedApplication setIdleTimerDisabled:YES];
}
- (void)viewWillDisappear:(BOOL)animated {
[UIApplication.sharedApplication setIdleTimerDisabled:NO];
[super viewWillDisappear:animated];
}
- (void)updateCountdownLabel {
@@ -0,0 +1,42 @@
/*
Copyright (c) 2021, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKEnvironmentSPLMeterBarView : UIView
- (void)setProgress:(CGFloat)progress;
- (void)stopAnimation;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,292 @@
/*
Copyright (c) 2021, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKEnvironmentSPLMeterBarView.h"
#import <QuartzCore/QuartzCore.h>
static const CGFloat ORKEnvironmentSPLMeterSquareSize = 8.0;
static const CGFloat ORKEnvironmentSPLMeterSquareDistance = 4.0;
static const int ORKEnvironmentSPLMeterNumberOfRows = 4;
@interface ORKEnvironmentSPLMeterColumnView : UIView {
int _numberOfRows;
CGFloat _squareSize;
CGFloat _cornerRadius;
NSArray<CAShapeLayer*> *_dots;
}
- (void)setColor:(UIColor *)color;
- (void)setOpacity:(CGFloat)opacity;
@end
@implementation ORKEnvironmentSPLMeterColumnView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_numberOfRows = ORKEnvironmentSPLMeterNumberOfRows;
_squareSize = ORKEnvironmentSPLMeterSquareSize;
_cornerRadius = ORKEnvironmentSPLMeterSquareDistance;
[self initRows];
}
return self;
}
- (void)initRows {
CGFloat halfSquareSize = _squareSize * 0.5;
CGFloat spacing = _squareSize + halfSquareSize;
NSMutableArray<CAShapeLayer*> *dots = [[NSMutableArray alloc] init];
for (int i = 0; i < _numberOfRows; i++) {
CAShapeLayer *dot = [CAShapeLayer layer];
CGRect dotRect = CGRectMake(0,
spacing * i,
_squareSize, _squareSize);
[dot setPath:[UIBezierPath bezierPathWithRoundedRect:dotRect
cornerRadius:_cornerRadius].CGPath];
if (@available(iOS 13.0, *)) {
dot.fillColor = [UIColor systemGray6Color].CGColor;
}
[[self layer] addSublayer:dot];
[dots addObject:dot];
}
_dots = [dots copy];
}
- (void)setOpacity:(CGFloat)opacity {
for (NSInteger i = 0 ; i < _dots.count; i++) {
CAShapeLayer *dot = _dots[i];
dot.opacity = opacity;
}
}
- (void)setColor:(UIColor *)color {
[_dots makeObjectsPerformSelector:@selector(setFillColor:) withObject:(id)[color CGColor]];
}
@end
@interface ORKEnvironmentSPLMeterBarView () {
NSArray<ORKEnvironmentSPLMeterColumnView *> *_columnViews;
int _currentIndex;
int _targetIndex;
int _maximumNumberOfDots;
int _greenIndexLimit;
BOOL _didLayoutViews;
BOOL _isAnimating;
NSTimer *_animationTimer;
}
@end
@implementation ORKEnvironmentSPLMeterBarView
- (void)didMoveToSuperview {
[super didMoveToSuperview];
_didLayoutViews = NO;
_isAnimating = NO;
}
- (void)setupView {
CGFloat width = CGRectGetWidth(self.frame);
CGFloat dotSpacing = (ORKEnvironmentSPLMeterSquareSize + ORKEnvironmentSPLMeterSquareDistance);
_maximumNumberOfDots = (int) (floor(width/dotSpacing)) + 1;
NSMutableArray<ORKEnvironmentSPLMeterColumnView*> *columnViews = [[NSMutableArray alloc] init];
_greenIndexLimit = _maximumNumberOfDots * 0.66;
_currentIndex = 0;
_targetIndex = _greenIndexLimit;
for (int i = 1 ; i <= _maximumNumberOfDots; i++) {
CGRect columnRect = CGRectMake((i - 1) * dotSpacing,
0, ORKEnvironmentSPLMeterSquareSize, ORKEnvironmentSPLMeterSquareSize);
ORKEnvironmentSPLMeterColumnView *columnView = [[ORKEnvironmentSPLMeterColumnView alloc] initWithFrame:columnRect];
if (i <= _greenIndexLimit - 1) {
[columnView setColor:[UIColor systemGreenColor]];
} else {
[columnView setColor:[UIColor systemOrangeColor]];
}
[self addSubview:columnView];
[columnViews addObject:columnView];
}
_columnViews = [columnViews copy];
[self updateViewForIndex:_currentIndex];
[self animateColumns];
}
- (void)setProgress:(CGFloat)progress {
CGFloat resultProgress = progress;
if (progress == 20.0) {
return;
}
if(progress < 0) {
resultProgress = 0.0;
}
float inMin = 0.0;
float inMax = 1.0;
float outMin = 0.0;
float outMax = 0.66;
float normalizedIndexValue = outMin + (outMax - outMin) * (resultProgress - inMin) / (inMax - inMin);
if (resultProgress > 1.0) {
inMin = 1.0;
inMax = 1.5;
outMin = 0.66;
outMax = 1.0;
normalizedIndexValue = outMin + (outMax - outMin) * (resultProgress - inMin) / (inMax - inMin);
}
int newTargetIndex = (int) (floor(normalizedIndexValue * _maximumNumberOfDots) + 1);
if (newTargetIndex != _targetIndex) {
[self stopAnimation];
_targetIndex = newTargetIndex;
_currentIndex = _targetIndex + (-1 + arc4random_uniform(3));
[self updateViewForIndex:newTargetIndex];
} else if (!_isAnimating) {
int indexDistance = abs(_currentIndex - newTargetIndex);
for (int i = 0; i < indexDistance; i++) {
int newIndex;
if (newTargetIndex < _currentIndex) {
newIndex = _currentIndex - i;
} else {
newIndex = _currentIndex + i;
}
[self updateViewForIndex:newIndex];
}
[self animateColumns];
}
}
- (void)animateColumns {
[_animationTimer invalidate];
_isAnimating = YES;
_animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(timerTicked) userInfo:nil repeats:YES];
}
- (void)timerTicked {
if (_currentIndex > _targetIndex) {
_currentIndex = _currentIndex - 1;
} else if (_currentIndex < _targetIndex) {
_currentIndex = _currentIndex + 1;
} else {
_currentIndex = _currentIndex + (-1 + arc4random_uniform(3));
}
[self updateViewForIndex:_currentIndex];
}
- (void)updateViewForIndex:(int)index {
for (int i = 0 ; i < _maximumNumberOfDots; i++) {
ORKEnvironmentSPLMeterColumnView *columnView = _columnViews[i];
NSInteger distanceToIndex = i - index;
CGFloat opacityFactor = 0.1 * distanceToIndex;
UIColor *grayColor;
UIColor *greenColor;
UIColor *orangeColor;
if (@available(iOS 13.0, *)) {
greenColor = [UIColor systemGreenColor];
orangeColor = [UIColor systemOrangeColor];
} else {
grayColor = [UIColor grayColor];
greenColor = [UIColor greenColor];
orangeColor = [UIColor orangeColor];
}
if (i <= _greenIndexLimit) {
if (i < index) {
[columnView setColor:greenColor];
[columnView setOpacity:1.0];
} else {
if (distanceToIndex < 3){
[columnView setColor:greenColor];
[columnView setOpacity:0.5 - opacityFactor];
} else {
[columnView setColor:grayColor];
[columnView setOpacity:1.0];
}
}
} else {
if (i < index) {
[columnView setColor:orangeColor];
[columnView setOpacity:1.0];
} else {
if (distanceToIndex < 3){
[columnView setColor:orangeColor];
[columnView setOpacity:0.5 - opacityFactor];
} else {
[columnView setColor:grayColor];
[columnView setOpacity:1.0];
}
}
}
}
}
- (void)stopAnimation {
_isAnimating = NO;
[_animationTimer invalidate];
_animationTimer = nil;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!_didLayoutViews) {
_didLayoutViews = YES;
[self setupView];
}
}
- (void)dealloc {
[_animationTimer invalidate];
_animationTimer = nil;
}
@end
@@ -33,6 +33,7 @@
NS_ASSUME_NONNULL_BEGIN
@class ORKEnvironmentSPLMeterBarView;
@class ORKRingView;
@class ORKRoundTappingButton;
@class ORKNavigationContainerView;
@@ -50,12 +51,16 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, weak) id<ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> voiceOverDelegate;
- (ORKEnvironmentSPLMeterBarView *)barView;
- (ORKRingView *)ringView;
- (void)setProgress:(CGFloat)progress;
- (void)setProgressCircle:(CGFloat)progress;
- (void)setProgressBar:(CGFloat)progress;
- (void)reachedOptimumNoiseLevel;
@end
@@ -30,7 +30,7 @@
#import "ORKEnvironmentSPLMeterContentView.h"
#import "ORKEnvironmentSPLMeterBarView.h"
#import "ORKRoundTappingButton.h"
#import "ORKUnitLabel.h"
#import "ORKHelpers_Internal.h"
@@ -39,63 +39,25 @@
#import "ORKProgressView.h"
#import "ORKCompletionCheckmarkView.h"
static const CGFloat CircleIndicatorMaxDiameter = 150.0;
static const CGFloat RingViewTopPadding = 24.0;
static const CGFloat InstructionLabelTopPadding = 50.0;
static const CGFloat InstructionLabelBottomPadding = 10.0;
static CGFloat CircleIndicatorViewScaleFactorForProgress(CGFloat progress) {
CGFloat y1 = 0.5, x1 = 0.8, y2 = 1.4, x2 = 1.2;
if (progress < x1) // lower limit for diameter
{
return y1;
}
else if (progress > x2) // upper limit for diameter
{
return y2;
}
else // linear interpolation
{
return y1 + (y2 - y1)/(x2 - x1) * (progress - x1);
}
}
static CGFloat CircleIndicatorPulseVarianceForProgress(CGFloat progress) {
// Linear Interpolation
// kMin: Lower bound of interpolation. (Matches above)
// kMax: Higher bound of interpolation. (Matches above)
// min: Lower bound of variance.
// max: Higher bound of variance.
CGFloat min = 0.0075, max = 0.025;
CGFloat kMin = 0.8, kMax = 1.2;
if (progress < kMin)
{
return min;
}
else if (progress > kMax)
{
return max;
}
else
{
return min + (max - min)/(kMax - kMin) * (progress - kMin);
}
}
static const CGFloat RingViewPadding = 18.0;
static const CGFloat InstructionLabelPadding = 8.0;
static const CGFloat HalfCircleSize = 14.0;
static const CGFloat BarViewHeight = 50.0;
@interface ORKEnvironmentSPLMeterContentView ()
@property(nonatomic, strong) ORKRingView *ringView;
@property(nonatomic, strong) ORKEnvironmentSPLMeterBarView *barView;
@end
@implementation ORKEnvironmentSPLMeterContentView {
UIView *_circleIndicatorView;
UIView *_containerView;
UILabel *_DBInstructionLabel;
UIImage *_checkmarkImage;
UIImage *_xmarkImage;
UIImageView *_xmarkView;
CGFloat preValue;
CGFloat currentValue;
UIColor *_circleIndicatorNoiseColor;
}
- (instancetype)init {
@@ -103,133 +65,144 @@ static CGFloat CircleIndicatorPulseVarianceForProgress(CGFloat progress) {
if (self) {
preValue = -M_PI_2;
currentValue = 0.0;
_circleIndicatorNoiseColor = UIColor.systemOrangeColor;
self.translatesAutoresizingMaskIntoConstraints = NO;
[self setupRingView];
[self setupCircleIndicatorView];
[self setProgressCircle:0.0];
[self setupContainerView];
[self setupDBInstructionLabel];
[self setupRingView];
[self setupBarView];
[self setupXmarkView];
[self setProgressCircle:0.0];
}
return self;
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
_DBInstructionLabel.font = [self title3TextFont];
}
- (UIFont *)title3TextFont {
UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleTitle3];
UIFontDescriptor *fontDescriptor = [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
return [UIFont fontWithDescriptor:fontDescriptor size:[[descriptor objectForKey: UIFontDescriptorSizeAttribute] doubleValue]];
}
- (void)setupContainerView {
if (!_containerView) {
_containerView = [UIView new];
}
_containerView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_containerView];
[[_containerView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:-RingViewPadding] setActive:YES];
[[_containerView.topAnchor constraintGreaterThanOrEqualToAnchor:self.topAnchor] setActive:YES];
[[_containerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
[[_containerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
[[_containerView.topAnchor constraintLessThanOrEqualToAnchor:self.bottomAnchor] setActive:YES];
}
- (void)setupXmarkView {
if (!_xmarkView) {
if (@available(iOS 13.0, *)) {
UIImageConfiguration *configuration = [UIImageSymbolConfiguration configurationWithPointSize:HalfCircleSize weight:UIImageSymbolWeightBold scale:UIImageSymbolScaleDefault];
_xmarkImage = [[UIImage systemImageNamed:@"xmark" withConfiguration:configuration] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
_checkmarkImage = [[UIImage systemImageNamed:@"checkmark" withConfiguration:configuration] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
_xmarkView = [[UIImageView alloc] initWithImage: _xmarkImage];
_xmarkView.tintColor = UIColor.systemOrangeColor;
}
_xmarkView.translatesAutoresizingMaskIntoConstraints = NO;
[_containerView addSubview:_xmarkView];
[[_xmarkView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
[[_xmarkView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
_xmarkView.hidden = YES;
}
- (void)setupBarView {
if (!_barView) {
_barView = [[ORKEnvironmentSPLMeterBarView alloc] initWithFrame:CGRectZero];
}
_barView.translatesAutoresizingMaskIntoConstraints = NO;
[_containerView addSubview:_barView];
[[_barView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor] setActive:YES];
[[_barView.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor] setActive:YES];
[[_barView.heightAnchor constraintEqualToConstant:BarViewHeight] setActive:YES];
[[_barView.topAnchor constraintEqualToAnchor:_DBInstructionLabel.bottomAnchor constant:RingViewPadding] setActive:YES];
[[_barView.bottomAnchor constraintEqualToAnchor:_containerView.bottomAnchor constant:RingViewPadding] setActive:YES];
}
- (void)setupRingView {
if (!_ringView) {
_ringView = [ORKRingView new];
}
_ringView.animationDuration = 0.0;
_ringView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_ringView];
[[_ringView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
[[_ringView.topAnchor constraintEqualToAnchor:self.topAnchor constant:RingViewTopPadding] setActive:YES];
[_ringView setColor:UIColor.grayColor];
}
[_containerView addSubview:_ringView];
- (void)setupCircleIndicatorView {
if (!_circleIndicatorView) {
_circleIndicatorView = [UIView new];
[[_ringView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor] setActive:YES];
[[_ringView.centerYAnchor constraintEqualToAnchor:_DBInstructionLabel.centerYAnchor] setActive:YES];
[[_ringView.trailingAnchor constraintEqualToAnchor:_DBInstructionLabel.leadingAnchor constant:-InstructionLabelPadding] setActive:YES];
if (@available(iOS 13.0, *)) {
[_ringView setColor:UIColor.systemGray6Color];
}
_circleIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
[self insertSubview:_circleIndicatorView belowSubview:_ringView];
[[_circleIndicatorView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
[[_circleIndicatorView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
[[_circleIndicatorView.heightAnchor constraintEqualToConstant:CircleIndicatorMaxDiameter] setActive:YES];
[[_circleIndicatorView.widthAnchor constraintEqualToConstant:CircleIndicatorMaxDiameter] setActive:YES];
_circleIndicatorView.layer.cornerRadius = CircleIndicatorMaxDiameter * 0.5;
}
- (void)setupDBInstructionLabel {
if (!_DBInstructionLabel) {
_DBInstructionLabel = [ORKLabel new];
_DBInstructionLabel.numberOfLines = 0;
_DBInstructionLabel.textColor = UIColor.systemGrayColor;
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
_DBInstructionLabel.font = [self title3TextFont];
if (@available(iOS 13.0, *)) {
_DBInstructionLabel.textColor = UIColor.labelColor;
}
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil);
}
_DBInstructionLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_DBInstructionLabel];
[[_DBInstructionLabel.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
[[_DBInstructionLabel.topAnchor constraintEqualToAnchor:_circleIndicatorView.bottomAnchor constant:InstructionLabelTopPadding] setActive:YES];
[[_DBInstructionLabel.bottomAnchor constraintLessThanOrEqualToAnchor:self.bottomAnchor constant:-InstructionLabelBottomPadding] setActive:YES];
[_containerView addSubview:_DBInstructionLabel];
[[_DBInstructionLabel.topAnchor constraintEqualToAnchor:_containerView.topAnchor constant:InstructionLabelPadding] setActive:YES];
[[_DBInstructionLabel.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor constant:-InstructionLabelPadding] setActive:YES];
}
- (void)setProgressBar:(CGFloat)progress {
[_barView setProgress:progress];
}
- (void)setProgressCircle:(CGFloat)progress {
CGFloat circleDiameter = CircleIndicatorViewScaleFactorForProgress(progress);
CGFloat variance = CircleIndicatorPulseVarianceForProgress(progress);
[self startPulsingWithTranformScaleFactor:circleDiameter variance:variance];
if (progress >= ORKRingViewMaximumValue)
{
[_ringView setBackgroundLayerStrokeColor:[UIColor.whiteColor colorWithAlphaComponent:0.3] circleStrokeColor:UIColor.whiteColor withAnimationDuration:0.8];
}
else
{
if (progress >= ORKRingViewMaximumValue) {
} else {
[_ringView resetLayerColors];
}
[UIView animateWithDuration:0.8
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
_circleIndicatorView.transform = CGAffineTransformMakeScale(circleDiameter, circleDiameter);
_circleIndicatorView.backgroundColor = progress >= ORKRingViewMaximumValue ? _circleIndicatorNoiseColor : self.tintColor;
} completion:nil];
[self updateInstructionForValue:progress];
}
- (ORKRingView *)ringView
{
- (ORKRingView *)ringView {
return _ringView;
}
- (void)startPulsingWithTranformScaleFactor:(CGFloat)transformScaleFactor variance:(CGFloat)variance {
[self stopPulsing];
CAKeyframeAnimation *pulse = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.xy"];
pulse.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pulse.repeatCount = MAXFLOAT;
pulse.duration = 0.6;
pulse.values = @[
@(transformScaleFactor),
@(transformScaleFactor * (1 - variance)),
@(transformScaleFactor),
@(transformScaleFactor * (1 + variance)),
@(transformScaleFactor)
];
[_circleIndicatorView.layer addAnimation:pulse forKey:@"pulse"];
}
- (void)stopPulsing {
[_circleIndicatorView.layer removeAnimationForKey:@"pulse"];
}
- (void)setProgress:(CGFloat)progress {
CGFloat value = progress < ORKRingViewMinimumValue ? ORKRingViewMinimumValue : progress;
[_ringView setValue:value];
}
- (void)updateInstructionForValue:(CGFloat)progress
{
- (void)updateInstructionForValue:(CGFloat)progress {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *currentInstruction = [_DBInstructionLabel.text copy];
NSString *newInstruction = progress >= ORKRingViewMaximumValue ? ORKLocalizedString(@"ENVIRONMENTSPL_NOISE", nil) : ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
if (![newInstruction isEqualToString:currentInstruction])
{
BOOL isNoise = (progress >= ORKRingViewMaximumValue);
NSString *newInstruction = isNoise ? ORKLocalizedString(@"ENVIRONMENTSPL_NOISE", nil) : ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil);
_xmarkView.hidden = !isNoise;
if (![newInstruction isEqualToString:currentInstruction]) {
_DBInstructionLabel.text = newInstruction;
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)])
{
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)]) {
[self.voiceOverDelegate contentView:self shouldAnnounce:_DBInstructionLabel.text];
}
}
@@ -241,15 +214,19 @@ static CGFloat CircleIndicatorPulseVarianceForProgress(CGFloat progress) {
}
- (void)reachedOptimumNoiseLevel {
[self stopPulsing];
_ringView.hidden = YES;
_circleIndicatorView.hidden = YES;
ORKCompletionCheckmarkView *checkmarkView = [[ORKCompletionCheckmarkView alloc] initWithDimension:_ringView.bounds.size.width];
checkmarkView.translatesAutoresizingMaskIntoConstraints = NO;
[self insertSubview:checkmarkView aboveSubview:_ringView];
[[checkmarkView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
[[checkmarkView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
[checkmarkView setAnimationPoint:1 animated:YES];
_xmarkView.hidden = NO;
_xmarkView.image = _checkmarkImage;
_xmarkView.tintColor = UIColor.systemGreenColor;
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)]) {
[self.voiceOverDelegate contentView:self shouldAnnounce:_DBInstructionLabel.text];
}
[_ringView setBackgroundLayerStrokeColor:UIColor.systemGreenColor circleStrokeColor:UIColor.systemGreenColor withAnimationDuration:0.0];
[_barView stopAnimation];
}
@end
@@ -31,7 +31,7 @@
#import "ORKEnvironmentSPLMeterStep.h"
#import "ORKEnvironmentSPLMeterStepViewController.h"
#import "ORKRecorder_Private.h"
#import "ORKHelpers_Internal.h"
#define ORKEnvironmentSPLMeterTaskDefaultThresholdValue 35.0
@@ -58,6 +58,10 @@
self.requiredContiguousSamples = ORKEnvironmentSPLMeterTaskDefaultRequiredContiguousSamples;
self.stepDuration = CGFLOAT_MAX;
self.shouldShowDefaultTimer = NO;
// This is inserted here because it is required for any task that requires the SPL Meter step
ORKAudioStreamerConfiguration *config = [[ORKAudioStreamerConfiguration alloc] initWithIdentifier:[NSString stringWithFormat:@"%@_streamerConfiguration",self.identifier]];
self.recorderConfigurations = @[config];
}
- (void)validateParameters {
@@ -31,6 +31,7 @@
#import "ORKEnvironmentSPLMeterStepViewController.h"
#import "ORKActiveStepView.h"
#import "ORKStepView.h"
#import "ORKStepContainerView_Private.h"
@@ -40,6 +41,7 @@
#import "ORKActiveStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
#import "ORKCollectionResult_Private.h"
#import "ORKEnvironmentSPLMeterResult.h"
@@ -51,6 +53,8 @@
#import <AVFoundation/AVFoundation.h>
#include <sys/sysctl.h>
static const NSTimeInterval SPL_METER_PLAY_DELAY_VOICEOVER = 3.0;
@interface ORKEnvironmentSPLMeterStepViewController ()<ORKRingViewDelegate, ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> {
AVAudioEngine *_audioEngine;
AVAudioInputNode *_inputNode;
@@ -74,6 +78,7 @@
AVAudioSessionCategoryOptions _savedSessionCategoryOptions;
UINotificationFeedbackGenerator *_notificationFeedbackGenerator;
dispatch_semaphore_t _voiceOverAnnouncementSemaphore;
NSTimer *_timeoutTimer;
}
@property (nonatomic, strong) ORKEnvironmentSPLMeterContentView *environmentSPLMeterContentView;
@@ -95,6 +100,8 @@
_requiredContiguousSamples = 1;
_sensitivityOffset = -23.3;
_recordedSamples = [NSMutableArray new];
_audioEngine = [[AVAudioEngine alloc] init];
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
}
return self;
@@ -109,20 +116,12 @@
_environmentSPLMeterContentView.voiceOverDelegate = self;
_environmentSPLMeterContentView.ringView.delegate = self;
self.activeStepView.activeCustomView = _environmentSPLMeterContentView;
self.activeStepView.customContentFillsAvailableSpace = YES;
[self requestMicrophoneAuthorization];
[self requestRecordPermissionIfNeeded];
[self configureAudioSession];
_audioEngine = [[AVAudioEngine alloc] init];
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
_inputNode = [_audioEngine inputNode];
_inputNodeOutputFormat = [_inputNode inputFormatForBus:0];
_sampleRate = (uint32_t)_inputNodeOutputFormat.sampleRate;
_bufferSize = _sampleRate/10;
_countToFetch = _sampleRate/(int)_bufferSize;
[self configureEQ];
[_audioEngine attachNode:_eqUnit];
[_audioEngine connect:_inputNode to:_eqUnit format:_inputNodeOutputFormat];
[self setupFeedbackGenerator];
[self.taskViewController setNavigationBarColor:[self.view backgroundColor]];
}
- (void)saveAudioSession {
@@ -133,6 +132,7 @@
}
- (void)setNavigationFooterView {
self.activeStepView.navigationFooterView.continueButtonItem = self.continueButtonItem;
self.activeStepView.navigationFooterView.continueEnabled = NO;
[self.activeStepView.navigationFooterView updateContinueAndSkipEnabled];
@@ -143,6 +143,19 @@
_navigationFooterView.continueButtonItem = continueButtonItem;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (!_audioEngine.isRunning) {
[self saveAudioSession];
_sensitivityOffset = [self sensitivityOffsetForDevice];
[self requestRecordPermissionIfNeeded];
[self configureAudioSession];
[self setupFeedbackGenerator];
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
@@ -151,15 +164,21 @@
_samplingInterval = [self environmentSPLMeterStep].samplingInterval;
_requiredContiguousSamples = [self environmentSPLMeterStep].requiredContiguousSamples;
_thresholdValue = [self environmentSPLMeterStep].thresholdValue;
[self configureInputNode];
[self splWorkBlock];
if (UIAccessibilityIsVoiceOverRunning()) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SPL_METER_PLAY_DELAY_VOICEOVER * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil));
});
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self stopAudioEngine];
[self resetAudioSession];
[_eqUnit removeTapOnBus:0];
[_audioEngine stop];
[_rmsBuffer removeAllObjects];
}
- (NSString *)deviceType {
@@ -194,45 +213,89 @@
return sResult;
}
- (void)requestMicrophoneAuthorization {
[[AVAudioSession sharedInstance] recordPermission];
- (void)requestRecordPermissionIfNeeded
{
[self handleRecordPermission:[[AVAudioSession sharedInstance] recordPermission]];
}
- (void)handleRecordPermission:(AVAudioSessionRecordPermission)recordPermission
{
switch (recordPermission)
{
case AVAudioSessionRecordPermissionGranted:
break;
case AVAudioSessionRecordPermissionDenied:
{
ORK_Log_Error("User has denied record permission for a step which requires microphone access.");
break;
}
case AVAudioSessionRecordPermissionUndetermined:
{
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
[self handleRecordPermission:granted ? AVAudioSessionRecordPermissionGranted : AVAudioSessionRecordPermissionDenied];
}];
break;
}
}
}
- (void)configureAudioSession {
NSError *error = nil;
AVAudioSession * session = [AVAudioSession sharedInstance];
// Stop any existing audio
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategorySoloAmbient error:&error];
[session setCategory:AVAudioSessionCategorySoloAmbient error:&error];
if (error) {
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
}
[[AVAudioSession sharedInstance] setActive:YES error:&error];
[session setActive:YES error:&error];
if (error) {
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
}
// Force input/output from iOS device
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
if (error) {
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
}
// Override Output (and Input) to use built-in mic and speaker.
// We need to make sure audio output is to the Headphones and Audio Input is uing the built-in mic.
// Although this forces both to the built-in mic AND Speaker, we need to also override the speaker.
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
if (error)
{
ORK_Log_Error("Setting AVAudioSessionPortOverrideSpeaker failed with error message: \"%@\"", error.localizedDescription);
// When setting the input like this, we do not need to set the input AND the output to the iPhone.
NSArray<AVAudioSessionPortDescription *> * inputs = [session availableInputs];
for (AVAudioSessionPortDescription* desc in inputs) {
if ([desc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
// go ahead and set our preferred input to the built-in mic
[session setPreferredInput:desc error:&error];
if (error) {
ORK_Log_Error("Setting AVAudioSession preferred input failed with error message: \"%@\"", error.localizedDescription);
}
}
}
[[AVAudioSession sharedInstance] setActive:YES error:&error];
[session setActive:YES error:&error];
if (error) {
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
}
}
- (void)configureInputNode {
_inputNode = [_audioEngine inputNode];
_inputNodeOutputFormat = [_inputNode inputFormatForBus:0];
_sampleRate = (uint32_t)_inputNodeOutputFormat.sampleRate;
_bufferSize = _sampleRate/10;
_countToFetch = _sampleRate > 0 ? _sampleRate/(int)_bufferSize : 0;
[self configureEQ];
[_audioEngine attachNode:_eqUnit];
[_audioEngine connect:_inputNode to:_eqUnit format:_inputNodeOutputFormat];
}
- (void)configureEQ {
_eqUnit.globalGain = 0;
@@ -320,12 +383,23 @@
});
[self evaluateThreshold:_spl];
[_rmsBuffer removeAllObjects];
} else {
if (rms > 0.0 && _sampleRate > 0.0) {
float spl = (20 * log10f(sqrtf(rms/(float)_sampleRate))) - _sensitivityOffset + 96;
dispatch_async(dispatch_get_main_queue(), ^{
[self.environmentSPLMeterContentView setProgressBar:(spl/_thresholdValue)];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self.environmentSPLMeterContentView setProgressBar:(_spl/_thresholdValue)];
});
}
}
dispatch_semaphore_signal(_semaphoreRms);
});
dispatch_semaphore_wait(_semaphoreRms, DISPATCH_TIME_FOREVER);
} else if ([AVAudioSession sharedInstance].recordPermission == AVAudioSessionRecordPermissionDenied) {
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[_eqUnit removeTapOnBus:0];
[_audioEngine stop];
[_rmsBuffer removeAllObjects];
@@ -336,9 +410,7 @@
NSError *error = nil;
[_audioEngine startAndReturnError:&error];
} else {
[_eqUnit removeTapOnBus:0];
[_audioEngine stop];
[_rmsBuffer removeAllObjects];
[self stopAudioEngine];
}
}
}
@@ -381,9 +453,19 @@
}
}
- (void)stopAudioEngine {
if ([_audioEngine isRunning]) {
dispatch_semaphore_signal(_semaphoreRms);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[_eqUnit removeTapOnBus:0];
[_audioEngine stop];
[_rmsBuffer removeAllObjects];
});
}
}
- (void)reachedOptimumNoiseLevel {
[self resetAudioSession];
[_audioEngine stop];
}
- (void)stepDidFinish {
@@ -403,6 +485,7 @@
#pragma mark - ORKRingViewDelegate
- (void)ringViewDidFinishFillAnimation {
[self reachedOptimumNoiseLevel];
[self.environmentSPLMeterContentView reachedOptimumNoiseLevel];
self.activeStepView.navigationFooterView.continueEnabled = YES;
}
@@ -417,19 +500,17 @@
- (void)sendHapticEvent:(UINotificationFeedbackType)eventType
{
[_notificationFeedbackGenerator notificationOccurred:eventType];
[_notificationFeedbackGenerator prepare];
dispatch_async(dispatch_get_main_queue(), ^{
[_notificationFeedbackGenerator notificationOccurred:eventType];
[_notificationFeedbackGenerator prepare];
});
}
#pragma mark - ORKEnvironmentSPLMeterContentViewVoiceOverDelegate
- (void)contentView:(ORKEnvironmentSPLMeterContentView *)contentView shouldAnnounce:(NSString *)inAnnouncement
{
if ([_audioEngine isRunning] == NO)
{
// Only make this announcement if the audio engine is not running.
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, inAnnouncement);
}
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, inAnnouncement);
}
@end
+3
View File
@@ -75,6 +75,9 @@ ORK_CLASS_AVAILABLE
*/
@property (nonatomic, copy, nullable) NSURL *fileURL;
@property (nonatomic, copy, nullable) NSString *fileName;
@end
NS_ASSUME_NONNULL_END
+5 -1
View File
@@ -45,6 +45,7 @@
[super encodeWithCoder:aCoder];
ORK_ENCODE_URL(aCoder, fileURL);
ORK_ENCODE_OBJ(aCoder, contentType);
ORK_ENCODE_OBJ(aCoder, fileName);
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
@@ -52,6 +53,7 @@
if (self) {
ORK_DECODE_URL(aDecoder, fileURL);
ORK_DECODE_OBJ_CLASS(aDecoder, contentType, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, fileName, NSString);
}
return self;
}
@@ -66,7 +68,8 @@
__typeof(self) castObject = object;
return (isParentSame &&
ORKEqualFileURLs(self.fileURL, castObject.fileURL) &&
ORKEqualObjects(self.contentType, castObject.contentType));
ORKEqualObjects(self.contentType, castObject.contentType) &&
ORKEqualObjects(self.fileName, castObject.fileName));
}
- (NSUInteger)hash {
@@ -77,6 +80,7 @@
ORKFileResult *result = [super copyWithZone:zone];
result.fileURL = [self.fileURL copy];
result.contentType = [self.contentType copy];
result.fileName = [self.fileName copy];
return result;
}
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -34,19 +34,38 @@
NS_ASSUME_NONNULL_BEGIN
// Displays a countdown ring and a timer.
//
// ------------------------------
// | |
// | Title Label |
// | |
// | subtitle label |
// | |
// | __________ |
// | / \ |
// | | 2:30 | |
// | \ ________ / |
// | |
// |______________________________|
@interface ORKFitnessContentView : ORKActiveStepCustomView
@property (nonatomic, assign, getter=isFinished) BOOL finished;
@property (nonatomic) BOOL hasHeartRate;
@property (nonatomic) BOOL hasDistance;
/// The total amount of time the active task is supposed to be performed for.
/// For the six minute walk test, this will typically be 360 seconds.
@property (nonatomic) NSTimeInterval duration;
@property (nonatomic, copy, nullable) NSString *heartRate;
@property (nonatomic) double distanceInMeters;
/// The amount of time that still remain.
@property (nonatomic) NSTimeInterval timeLeft;
@property (nonatomic, strong, nullable) UIImage *image;
/// Whether or not the text label is hidden.
@property (nonatomic) BOOL labelHidden;
@property (nonatomic, assign) NSTimeInterval timeLeft;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithDuration:(NSTimeInterval)duration;
@end
+86 -275
View File
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -31,28 +31,8 @@
#import "ORKFitnessContentView.h"
#import "ORKActiveStepQuantityView.h"
#import "ORKTintedImageView.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
@import CoreMotion;
@import HealthKit;
// #define LAYOUT_TEST 1
// #define LAYOUT_DEBUG 1
@interface ORKFitnessContentView () {
ORKQuantityLabel *_timerLabel;
ORKQuantityPairView *_quantityPairView;
UIView *_imageSpacer1;
UIView *_imageSpacer2;
ORKTintedImageView *_imageView;
NSLengthFormatter *_lengthFormatter;
NSLayoutConstraint *_imageRatioConstraint;
NSLayoutConstraint *_topConstraint;
UILabel *_timerLabel;
}
@end
@@ -60,272 +40,72 @@
@implementation ORKFitnessContentView
- (ORKActiveStepQuantityView *)distanceView {
return _quantityPairView.leftView;
}
- (ORKActiveStepQuantityView *)heartRateView {
return _quantityPairView.rightView;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
- (instancetype)initWithDuration:(NSTimeInterval)duration {
self = [super init];
if (self) {
_timerLabel = [ORKQuantityLabel new];
_quantityPairView = [ORKQuantityPairView new];
_imageSpacer1 = [UIView new];
_imageSpacer1.translatesAutoresizingMaskIntoConstraints = NO;
_imageSpacer2 = [UIView new];
_imageSpacer2.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_imageSpacer1];
[self addSubview:_imageSpacer2];
[self heartRateView].image = [UIImage imageNamed:@"heart-fitness" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
[self updateLengthFormatter];
_imageView = [ORKTintedImageView new];
_imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView.shouldApplyTint = YES;
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
_quantityPairView.translatesAutoresizingMaskIntoConstraints = NO;
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
[self updateKeylineVisible];
_timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently;
_imageView.isAccessibilityElement = NO;
self.hasHeartRate = _hasHeartRate;
self.hasDistance = _hasDistance;
#if LAYOUT_TEST
self.timeLeft = 60 * 5;
self.hasHeartRate = YES;
self.hasDistance = YES;
self.distanceInMeters = 100;
self.heartRate = @"22";
#endif
#if LAYOUT_DEBUG
self.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
_quantityPairView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.2];
#endif
[self setDistanceInMeters:0];
[self heartRateView].title = ORKLocalizedString(@"FITNESS_HEARTRATE_TITLE", nil);
_duration = duration;
_timeLeft = duration;
_timerLabel = [[UILabel alloc] init];
_timerLabel.textAlignment = NSTextAlignmentCenter;
_timerLabel.font = [self labelFont];
_timerLabel.adjustsFontForContentSizeCategory = YES;
_timerLabel.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
[self addSubview:_quantityPairView];
[self addSubview:_imageView];
[self addSubview:_timerLabel];
[self setUpConstraints];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
[self tintColorDidChange];
[self updateTimerLabel];
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
- (void)tintColorDidChange {
[self setNeedsDisplay];
_timerLabel.textColor = self.tintColor;
}
- (void)updateLengthFormatter {
_lengthFormatter = [NSLengthFormatter new];
_lengthFormatter.numberFormatter.maximumFractionDigits = 1;
_lengthFormatter.numberFormatter.maximumSignificantDigits = 3;
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
_timerLabel.font = [self labelFont];
}
- (void)localeDidChange:(NSNotification *)notification {
[self updateLengthFormatter];
[self setDistanceInMeters:_distanceInMeters];
}
- (void)willMoveToWindow:(UIWindow *)newWindow {
[super willMoveToWindow:newWindow];
[self updateConstraintConstantsForWindow:newWindow];
}
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
const CGFloat CaptionBaselineToTimerTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToFitnessTimerTop, window);
const CGFloat CaptionBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
_topConstraint.constant = (CaptionBaselineToTimerTop - CaptionBaselineToStepViewTop);
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
NSDictionary *views = NSDictionaryOfVariableBindings(_timerLabel, _imageView, _quantityPairView, _imageSpacer1, _imageSpacer2);
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_timerLabel][_imageSpacer1(>=0)][_imageView]"
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:views]];
_topConstraint = [NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0];
[constraints addObject:_topConstraint];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_imageView][_imageSpacer2(>=0)][_quantityPairView]|"
options:(NSLayoutFormatOptions)0
metrics:nil
views:views]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer2
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_imageSpacer2
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0.0]];
NSLayoutConstraint *imageSpacerHeightConstraint = [NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:CGFLOAT_MIN];
imageSpacerHeightConstraint.priority = UILayoutPriorityDefaultLow - 1;
[constraints addObject:imageSpacerHeightConstraint];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_quantityPairView]|"
options:(NSLayoutFormatOptions)0
metrics:nil
views:views]];
NSLayoutConstraint *maxWidthConstraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:ORKScreenMetricMaxDimension];
maxWidthConstraint.priority = UILayoutPriorityRequired - 1;
[constraints addObject:maxWidthConstraint];
[NSLayoutConstraint activateConstraints:constraints];
[self updateConstraintConstantsForWindow:self.window];
}
- (void)setImage:(UIImage *)image {
_image = image;
_imageView.image = image;
_imageRatioConstraint.active = NO;
CGSize size = image.size;
if (size.width > 0 && size.height > 0) {
_imageRatioConstraint = [NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_imageView
attribute:NSLayoutAttributeWidth
multiplier:size.height / size.width
constant:0.0];
_imageRatioConstraint.active = YES;
}
}
- (void)setHasDistance:(BOOL)hasDistance {
_hasDistance = hasDistance;
[self distanceView].enabled = _hasDistance;
[self updateKeylineVisible];
}
- (void)setHasHeartRate:(BOOL)hasHeartRate {
_hasHeartRate = hasHeartRate;
[self heartRateView].enabled = _hasHeartRate;
[self updateKeylineVisible];
}
- (void)setHeartRate:(NSString *)heartRate {
_heartRate = heartRate;
[self heartRateView].value = heartRate;
}
- (void)updateKeylineVisible {
[_quantityPairView setKeylineHidden:!(_hasDistance && _hasHeartRate)];
}
- (void)setDistanceInMeters:(double)distanceInMeters {
_distanceInMeters = distanceInMeters;
double displayDistance = _distanceInMeters;
NSString *distanceString = nil;
NSLengthFormatterUnit unit;
NSString *unitString = [_lengthFormatter unitStringFromMeters:displayDistance usedUnit:&unit];
switch (unit) {
case NSLengthFormatterUnitCentimeter:
case NSLengthFormatterUnitMillimeter:
unit = NSLengthFormatterUnitMeter;
// Force showing 0 meters if the distance is sufficiently short to be displayed in cm or mm
unitString = [_lengthFormatter unitStringFromValue:0 unit:NSLengthFormatterUnitMeter];
displayDistance = 0;
break;
default:
break;
}
// Use HealthKit to convert the unit, so we can use the number formatter directly.
HKUnit *hkUnit = [HKUnit unitFromLengthFormatterUnit:unit];
double conversionFactor = 1.0;
if ([hkUnit isNull] && (unit == NSLengthFormatterUnitYard)) {
hkUnit = [HKUnit footUnit];
conversionFactor = 1.0 / 3.0;
}
HKQuantity *quantity = [HKQuantity quantityWithUnit:[HKUnit meterUnit] doubleValue:displayDistance];
distanceString = [_lengthFormatter.numberFormatter stringFromNumber:@([quantity doubleValueForUnit:hkUnit]*conversionFactor)];
[self distanceView].title = [NSString localizedStringWithFormat:ORKLocalizedString(@"FITNESS_DISTANCE_TITLE_FORMAT", nil), unitString];
[self distanceView].value = distanceString;
- (void)setDuration:(NSTimeInterval)duration {
_duration = duration;
[self setNeedsDisplay];
}
- (void)setTimeLeft:(NSTimeInterval)timeLeft {
_timeLeft = timeLeft;
[self updateTimerLabel];
[self setNeedsDisplay];
}
- (BOOL)labelHidden {
return _timerLabel.isHidden;
}
- (void)setLabelHidden:(BOOL)labelHidden {
[_timerLabel setHidden:labelHidden];
}
- (UIFont*) labelFont {
UIFont* font = [UIFont preferredFontForTextStyle: UIFontTextStyleLargeTitle];
UIFontMetrics* metrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleLargeTitle];
if (@available(iOS 13, *)) {
UIFontDescriptor* round = [[font fontDescriptor] fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
UIFontDescriptor* weighted = [round fontDescriptorByAddingAttributes:@{
UIFontDescriptorTraitsAttribute: @{
UIFontWeightTrait: @1.5
}
}];
font = [UIFont fontWithDescriptor:weighted size:44];
}
UIFont* scaled = [metrics scaledFontForFont:font];
return scaled;
}
- (void)updateTimerLabel {
@@ -334,13 +114,44 @@
dispatch_once(&onceToken, ^{
formatter = [NSDateComponentsFormatter new];
formatter.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorDropLeading;
formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
});
NSString *labelString = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
_timerLabel.text = labelString;
_timerLabel.hidden = (labelString == nil);
_timerLabel.text = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
}
- (void)drawRect:(CGRect)rect {
// The ring should be be centered and fill 1/2 of the view's width
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat strokeWidth = 12;
CGFloat xCenter = self.bounds.size.width / 2;
CGFloat yCenter = self.bounds.size.height / 2;
CGFloat dimension = MIN(self.bounds.size.width, self.bounds.size.height);
CGFloat radius = 0.5 * (dimension * 0.5);
CGFloat percentFilled = _timeLeft / _duration;
CGFloat startAngle = -M_PI_2 - (percentFilled * 2 * M_PI);
CGFloat stopAngle = -M_PI_2;
bool clockwise = NO;
CGContextSetLineWidth(context, strokeWidth);
CGContextSetLineCap(context, kCGLineCapRound);
// Draw a circular track
if (@available(iOS 13.0, *)) {
[[UIColor systemGray5Color] setStroke];
} else {
[[UIColor lightGrayColor] setStroke];
}
CGContextAddArc(context, xCenter, yCenter, radius, 0, 2 * M_PI, clockwise ? 1 : 0);
CGContextStrokePath(context);
// Fill in the track based on progress
[self.tintColor setStroke];
CGContextAddArc(context, xCenter, yCenter, radius, startAngle, stopAngle, clockwise ? 1 : 0);
CGContextStrokePath(context);
}
@end
+2 -8
View File
@@ -36,17 +36,11 @@
NS_ASSUME_NONNULL_BEGIN
/**
Fitness step.
Displays usual header, a counting-up timer, read outs for distance and/or
heart rate if corresponding recorders are attached.
Also displays an image during the task.
*/
ORK_CLASS_AVAILABLE
@interface ORKFitnessStep : ORKActiveStep
@property (nonatomic, copy) NSDictionary *userInfo;
@end
NS_ASSUME_NONNULL_END
+42 -5
View File
@@ -30,6 +30,7 @@
#import "ORKFitnessStep.h"
#import "ORKHelpers_Internal.h"
#import "ORKFitnessStepViewController.h"
@@ -43,6 +44,7 @@
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super initWithIdentifier:identifier];
if (self) {
self.userInfo = [[NSDictionary alloc] init];
self.shouldShowDefaultTimer = NO;
}
return self;
@@ -58,13 +60,48 @@
}
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKFitnessStep *step = [super copyWithZone:zone];
return step;
}
- (BOOL)startsFinished {
return NO;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
ORK_DECODE_OBJ_PLIST(coder, userInfo);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
ORK_ENCODE_OBJ(coder, userInfo);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKFitnessStep *step = [super copyWithZone:zone];
step.userInfo = [self.userInfo copy];
return step;
}
- (BOOL)isEqual:(id)other
{
if ([self class] != [other class]) {
return NO;
}
__typeof(self) castObject = other;
return ORKEqualObjects(self.userInfo, castObject.userInfo);
}
- (NSUInteger)hash
{
return [super hash] ^ self.userInfo.hash;
}
@end
@@ -30,27 +30,20 @@
#import "ORKFitnessStepViewController.h"
#import "ORKActiveStepTimer.h"
#import "ORKActiveStepView.h"
#import "ORKFitnessContentView.h"
#import "ORKVerticalContainerView.h"
#import "ORKFitnessStep.h"
#import "ORKActiveStepView.h"
#import "ORKActiveStepTimer.h"
#import "ORKStepViewController_Internal.h"
#import "ORKHealthQuantityTypeRecorder.h"
#import "ORKPedometerRecorder.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKFitnessStep.h"
#import "ORKStep_Private.h"
#import "ORKHelpers_Internal.h"
#import "ORKStepContainerView_Private.h"
@interface ORKFitnessStepViewController () <ORKHealthQuantityTypeRecorderDelegate, ORKPedometerRecorderDelegate> {
NSInteger _intendedSteps;
@interface ORKFitnessStepViewController () {
ORKFitnessContentView *_contentView;
NSNumberFormatter *_hrFormatter;
}
@end
@@ -70,82 +63,61 @@
return (ORKFitnessStep *)self.step;
}
- (void)stepDidChange {
[super stepDidChange];
_hrFormatter = [[NSNumberFormatter alloc] init];
_hrFormatter.numberStyle = NSNumberFormatterNoStyle;
_contentView.timeLeft = self.fitnessStep.stepDuration;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_contentView = [ORKFitnessContentView new];
_contentView.timeLeft = self.fitnessStep.stepDuration;
_contentView = [[ORKFitnessContentView alloc] initWithDuration:self.fitnessStep.stepDuration];
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
self.activeStepView.activeCustomView = _contentView;
self.activeStepView.customContentFillsAvailableSpace = YES;
self.continueButtonTitle = ORKLocalizedString(@"BUTTON_SKIP_STEP", nil);
}
- (void)updateHeartRateWithQuantity:(HKQuantitySample *)quantity unit:(HKUnit *)unit {
if (quantity != nil) {
_contentView.hasHeartRate = YES;
}
if (quantity) {
_contentView.heartRate = [_hrFormatter stringFromNumber:@([quantity.quantity doubleValueForUnit:unit])];
} else {
_contentView.heartRate = @"--";
}
- (void)finish {
[super finish];
_contentView.labelHidden = YES;
self.continueButtonTitle = ORKLocalizedString(@"BUTTON_NEXT", nil);
}
- (void)updateDistance:(double)distanceInMeters {
_contentView.hasDistance = YES;
_contentView.distanceInMeters = distanceInMeters;
}
- (void)recordersDidChange {
[super recordersDidChange];
ORKPedometerRecorder *pedometerRecorder = nil;
ORKHealthQuantityTypeRecorder *heartRateRecorder = nil;
for (ORKRecorder *recorder in self.recorders) {
if ([recorder isKindOfClass:[ORKPedometerRecorder class]]) {
pedometerRecorder = (ORKPedometerRecorder *)recorder;
} else if ([recorder isKindOfClass:[ORKHealthQuantityTypeRecorder class]]) {
ORKHealthQuantityTypeRecorder *rec1 = (ORKHealthQuantityTypeRecorder *)recorder;
if ([[[rec1 quantityType] identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
heartRateRecorder = (ORKHealthQuantityTypeRecorder *)recorder;
}
}
}
if (heartRateRecorder == nil) {
_contentView.hasHeartRate = NO;
}
_contentView.heartRate = @"--";
_contentView.hasDistance = (pedometerRecorder != nil);
_contentView.distanceInMeters = 0;
- (void)stepDidChange {
[super stepDidChange];
_contentView.duration = self.fitnessStep.stepDuration;
_contentView.timeLeft = self.fitnessStep.stepDuration;
}
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
_contentView.timeLeft = finished ? 0 : (timer.duration - timer.runtime);
_contentView.duration = self.fitnessStep.stepDuration;
[super countDownTimerFired:timer finished:finished];
}
#pragma mark - ORKHealthQuantityTypeRecorderDelegate
- (void)goForward {
- (void)healthQuantityTypeRecorderDidUpdate:(ORKHealthQuantityTypeRecorder *)healthQuantityTypeRecorder {
if ([[healthQuantityTypeRecorder.quantityType identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
[self updateHeartRateWithQuantity:healthQuantityTypeRecorder.lastSample unit:healthQuantityTypeRecorder.unit];
if (self.finished) {
[super goForward];
return;
}
}
#pragma mark - ORKPedometerRecorderDelegate
UIAlertController *alert = [UIAlertController alertControllerWithTitle:ORKLocalizedString(@"FITNESS_STOP_TEST_CONFIRMATION", nil)
message:ORKLocalizedString(@"FITNESS_STOP_TEST_DETAIL", nil)
preferredStyle:UIAlertControllerStyleAlert];
- (void)pedometerRecorderDidUpdate:(ORKPedometerRecorder *)pedometerRecorder {
double distanceInMeters = pedometerRecorder.totalDistance;
[self updateDistance:distanceInMeters];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"FITNESS_RESUME_TEST", nil)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * _Nonnull action) {
[alert dismissViewControllerAnimated:YES completion:nil];
}]];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_SKIP_STEP", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[alert dismissViewControllerAnimated:YES completion:^{
[super goForward];
}];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
@end
@@ -109,7 +109,6 @@
_submitVideoButton.titleLabel.font = [UIFont systemFontOfSize:20.0];
[_submitVideoButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[_submitVideoButton setBackgroundColor:[UIColor systemBlueColor]];
[_submitVideoButton setTitleEdgeInsets:UIEdgeInsetsMake(5.0, 8.0, 5.0, 8.0)];
[_submitVideoButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_SUBMIT_VIDEO", nil) forState:UIControlStateNormal];
[self.contentView addSubview:_submitVideoButton];
}
@@ -194,9 +193,18 @@ typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
- (void)setupSubviews {
_startStopButton = [UIButton new];
if (@available(iOS 15.0, *)) {
UIButtonConfiguration *buttonConfiguration = [UIButtonConfiguration plainButtonConfiguration];
[buttonConfiguration setContentInsets:NSDirectionalEdgeInsetsMake(0, 6, 0, 6)];
[_startStopButton setConfiguration:buttonConfiguration];
} else {
_startStopButton.contentEdgeInsets = (UIEdgeInsets){.left = 6, .right = 6};
}
_startStopButton.layer.cornerRadius = 14.0;
_startStopButton.clipsToBounds = YES;
_startStopButton.contentEdgeInsets = (UIEdgeInsets){.left = 6, .right = 6};
UIFontDescriptor *descriptorOne = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
_startStopButton.titleLabel.font = [UIFont boldSystemFontOfSize:[[descriptorOne objectForKey: UIFontDescriptorSizeAttribute] doubleValue] + 1.0];
[self.contentView addSubview:_startStopButton];
@@ -299,7 +307,7 @@ typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
{
[_startStopButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_START_TITLE", nil) forState:UIControlStateNormal];
[_startStopButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[_startStopButton setBackgroundColor:[UIColor systemBlueColor]];
[_startStopButton setBackgroundColor:self.tintColor];
[_timerLabel setText:ORKLocalizedString(@"FRONT_FACING_CAMERA_START_TIME", nil)];
[_timerLabel setTextColor:[UIColor darkGrayColor]];
@@ -307,7 +315,7 @@ typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
else
{
[_startStopButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_STOP_TITLE", nil) forState:UIControlStateNormal];
[_startStopButton setTitleColor:[UIColor systemBlueColor] forState:UIControlStateNormal];
[_startStopButton setTitleColor:self.tintColor forState:UIControlStateNormal];
[_startStopButton setBackgroundColor:[UIColor systemGrayColor]];
[_timerLabel setTextColor:[UIColor whiteColor]];
@@ -344,6 +352,10 @@ typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
_isTextCollapsed = !_isTextCollapsed;
}
- (void)didMoveToWindow {
self.tintColor = ORKViewTintColor(self);
[self setStartStopButtonState:_startStopButtonState];
}
@end
@interface ORKFrontFacingCameraStepContentView ()
@@ -46,6 +46,7 @@
#import "ORKResult_Private.h"
#import "ORKStepContainerView_Private.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
@interface ORKFrontFacingCameraStepViewController () <AVCaptureFileOutputRecordingDelegate, AVCaptureVideoDataOutputSampleBufferDelegate>
@@ -89,6 +90,8 @@
[self setupContentView];
[self setupConstraints];
[self startSession];
[self.taskViewController setNavigationBarColor:[self.view backgroundColor]];
}
- (void)viewDidAppear:(BOOL)animated {
@@ -349,6 +352,7 @@
frontFacingCameraResult.endDate = now;
frontFacingCameraResult.contentType = @"video/quicktime";
frontFacingCameraResult.fileURL = _savedFileURL;
frontFacingCameraResult.fileName = _savedFileName;
frontFacingCameraResult.retryCount = retryCount;
[results addObject:frontFacingCameraResult];
@@ -205,7 +205,7 @@
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, healthClinicalType, HKClinicalType);
ORK_DECODE_OBJ(aDecoder, healthFHIRResourceType);
ORK_DECODE_OBJ_CLASS(aDecoder, healthFHIRResourceType, NSString);
}
return self;
}
@@ -63,7 +63,7 @@ public class ORKLandoltCResult: ORKResult {
aCoder.encode(score, forKey: Keys.score.rawValue)
}
required init?(coder aDecoder: NSCoder) {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
outcome = aDecoder.decodeObject(forKey: Keys.outcome.rawValue) as? Bool ?? false
@@ -99,7 +99,11 @@ public class ORKLandoltCResult: ORKResult {
}
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))"
let descriptionString = """
\(descriptionPrefix(withNumberOfPaddingSpaces: numberOfPaddingSpaces));
Outcome: \(String(describing: outcome)); LetterAngle: \(String(describing: letterAngle));
SliderAngle: \(String(describing: sliderAngle)); Score: \(String(describing: score))
"""
return descriptionString
}
}
+28 -15
View File
@@ -71,8 +71,13 @@
return @"location";
}
// Test Seam - unit tests don't support background updates or pausing.
- (CLLocationManager *)createLocationManager {
return [[CLLocationManager alloc] init];
CLLocationManager *manager = [[CLLocationManager alloc] init];
manager.pausesLocationUpdatesAutomatically = NO;
manager.allowsBackgroundLocationUpdates = YES;
return manager;
}
- (void)start {
@@ -88,20 +93,19 @@
}
self.locationManager = [self createLocationManager];
if ([CLLocationManager authorizationStatus] <= kCLAuthorizationStatusDenied) {
CLAuthorizationStatus status = kCLAuthorizationStatusNotDetermined;
if (@available(iOS 14.0, *)) {
status = self.locationManager.authorizationStatus;
} else {
status = [CLLocationManager authorizationStatus];
}
if (status == kCLAuthorizationStatusRestricted || status == kCLAuthorizationStatusNotDetermined) {
[self.locationManager requestWhenInUseAuthorization];
}
self.locationManager.pausesLocationUpdatesAutomatically = NO;
self.locationManager.delegate = self;
if (!self.locationManager) {
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSFeatureUnsupportedError
userInfo:@{@"recorder": self}];
[self finishRecordingWithError:error];
return;
}
self.uptime = [NSProcessInfo processInfo].systemUptime;
[self.locationManager startUpdatingLocation];
}
@@ -129,7 +133,8 @@
}
- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations {
didUpdateLocations:(NSArray<CLLocation *> *)locations {
BOOL success = YES;
NSParameterAssert(locations.count >= 0);
NSError *error = nil;
@@ -156,7 +161,15 @@
}
- (BOOL)isRecording {
return [CLLocationManager locationServicesEnabled] && (self.locationManager != nil) && ([CLLocationManager authorizationStatus] > kCLAuthorizationStatusDenied);
CLAuthorizationStatus status = kCLAuthorizationStatusNotDetermined;
if (@available(iOS 14.0, *)) {
status = self.locationManager.authorizationStatus;
} else {
status = [CLLocationManager authorizationStatus];
}
return [CLLocationManager locationServicesEnabled] && (self.locationManager != nil) && (status > kCLAuthorizationStatusDenied);
}
- (void)reset {
@@ -0,0 +1,58 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import UIKit;
#import "ORKCustomStepView_Internal.h"
#import "ORKNormalizedReactionTimeStimulusView.h"
#import "ORKRoundTappingButton.h"
NS_ASSUME_NONNULL_BEGIN
@interface ORKNormalizedReactionTimeContentView : ORKActiveStepCustomView
@property (nonatomic) ORKRoundTappingButton *button;
- (void)setStimulusHidden:(BOOL)hidden;
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void (^)(void))completion;
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void (^)(void))completion;
- (void)resetAfterDelay:(NSTimeInterval)delay completion:(nullable void (^)(void))completion;
- (UIView *)getBackgroundView;
- (ORKNormalizedReactionTimeStimulusView *)getStimulusView;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,213 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKNormalizedReactionTimeContentView.h"
#import "ORKNavigationContainerView.h"
#import "ORKNormalizedReactionTimeStimulusView.h"
#import "ORKSkin.h"
#import "ORKHelpers_Internal.h"
CGFloat NormalizeButtonSize = 100.0;
CGFloat BackgroundViewSpaceMultiplier = 2.0;
@implementation ORKNormalizedReactionTimeContentView {
ORKNormalizedReactionTimeStimulusView *_stimulusView;
UIView *_backgroundView;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.translatesAutoresizingMaskIntoConstraints = NO;
[self resizeConstraints];
[self addStimulusView];
[self addBackgroundView];
[self addButton];
}
return self;
}
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(void (^)(void))completion {
[_stimulusView startSuccessAnimationWithDuration:duration completion:completion];
}
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(void (^)(void))completion {
[_stimulusView startFailureAnimationWithDuration:duration completion:completion];
}
- (void)resetAfterDelay:(NSTimeInterval)delay completion:(nullable void (^)(void))completion {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
_stimulusView.hidden = YES;
if (completion) {
completion();
}
});
}
-(void)resizeConstraints {
ORKScreenType screenType = ORKGetVerticalScreenTypeForWindow([[[UIApplication sharedApplication] delegate] window]);
if (screenType == ORKScreenTypeiPhone5 ) {
NormalizeButtonSize = 70.0;
BackgroundViewSpaceMultiplier = 1.75;
}
}
-(void)addButton {
_button = [ORKRoundTappingButton new];
_button.translatesAutoresizingMaskIntoConstraints = NO;
[_button setTitle: ORKLocalizedString(@"REACTION_TIME_TASK_NORM_BUTTON_TITLE", nil) forState:UIControlStateNormal];
[_button setDiameter:NormalizeButtonSize];
[self addSubview:_button];
[NSLayoutConstraint activateConstraints: @[
[NSLayoutConstraint constraintWithItem:_button
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_button
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_backgroundView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:1.0],
]];
}
- (void)addStimulusView {
if (!_stimulusView) {
_stimulusView = [ORKNormalizedReactionTimeStimulusView new];
_stimulusView.translatesAutoresizingMaskIntoConstraints = NO;
_stimulusView.backgroundColor = self.tintColor;
[self addSubview:_stimulusView];
[self setUpStimulusViewConstraints];
}
}
- (void)addBackgroundView {
if (!_backgroundView) {
_backgroundView = [UIView new];
}
_backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
_backgroundView.layer.borderWidth = 3.0;
_backgroundView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:0.3];
_backgroundView.layer.borderColor = [UIColor lightGrayColor].CGColor;
[self insertSubview:_backgroundView belowSubview:_stimulusView];
[self setupBackgroundViewConstraints];
}
- (UIView *)getBackgroundView {
return _backgroundView;
}
- (ORKNormalizedReactionTimeStimulusView *)getStimulusView {
return _stimulusView;
}
- (void)setStimulusHidden:(BOOL)hidden {
_stimulusView.hidden = hidden;
}
- (void)setUpStimulusViewConstraints {
NSMutableArray *constraints = [NSMutableArray array];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_stimulusView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_stimulusView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0.0]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_stimulusView]-(>=0)-|"
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:NSDictionaryOfVariableBindings(_stimulusView)]];
[NSLayoutConstraint activateConstraints:constraints];
}
- (void)setupBackgroundViewConstraints {
NSMutableArray *constraints = [NSMutableArray array];
[constraints addObjectsFromArray:@[
[NSLayoutConstraint constraintWithItem:_backgroundView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:_stimulusView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_backgroundView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:_stimulusView
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_backgroundView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:_stimulusView
attribute:NSLayoutAttributeWidth
multiplier:BackgroundViewSpaceMultiplier
constant:0.0],
[NSLayoutConstraint constraintWithItem:_backgroundView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_stimulusView
attribute:NSLayoutAttributeHeight
multiplier:BackgroundViewSpaceMultiplier
constant:0.0],
]];
[NSLayoutConstraint activateConstraints:constraints];
}
@end
@@ -0,0 +1,63 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <ResearchKit/ORKResult.h>
NS_ASSUME_NONNULL_BEGIN
@class ORKFileResult;
/**
The `ORKReactionTimeResult` class represents the result of a single successful attempt within an ORKReactionTimeStep.
The `timestamp` property is equal to the value of systemUptime (in NSProcessInfo) when the stimulus occurred.
Each entry of motion data in this file contains a time interval which may be directly compared to timestamp in order to determine the elapsed time since the stimulus.
The fileResult property references the motion data recorded from the beginning of the attempt until the threshold acceleration was reached.
Using the time taken to reach the threshold acceleration as the reaction time of a participant will yield a rather crude measurement. Rather, you should devise your own method using the data recorded to obtain an accurate approximation of the true reaction time.
A reaction time result is typically generated by the framework as the task proceeds. When the task
completes, it may be appropriate to serialize the sample for transmission to a server
or to immediately perform analysis on it.
*/
ORK_CLASS_AVAILABLE
@interface ORKNormalizedReactionTimeResult: ORKResult
@property (nonatomic, copy) NSDate * timerStartDate;
@property (nonatomic, copy) NSDate * timerEndDate;
@property (nonatomic, copy, nullable) NSDate * stimulusStartDate;
@property (nonatomic, copy, nullable) NSDate * reactionDate;
@property (nonatomic) double currentInterval;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,92 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKNormalizedReactionTimeResult.h"
#import "ORKResult_Private.h"
#import "ORKHelpers_Internal.h"
@implementation ORKNormalizedReactionTimeResult
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_OBJ(aCoder, timerStartDate);
ORK_ENCODE_OBJ(aCoder, timerEndDate);
ORK_ENCODE_OBJ(aCoder, stimulusStartDate);
ORK_ENCODE_OBJ(aCoder, reactionDate);
ORK_ENCODE_INTEGER(aCoder, currentInterval);
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, timerStartDate, NSDate);
ORK_DECODE_OBJ_CLASS(aDecoder, timerEndDate, NSDate);
ORK_DECODE_OBJ_CLASS(aDecoder, stimulusStartDate, NSDate);
ORK_DECODE_OBJ_CLASS(aDecoder, reactionDate, NSDate);
ORK_DECODE_INTEGER(aDecoder, currentInterval);
}
return self;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
ORKEqualObjects(self.timerStartDate, castObject.timerStartDate) &&
ORKEqualObjects(self.timerEndDate, castObject.timerEndDate) &&
ORKEqualObjects(self.stimulusStartDate, castObject.stimulusStartDate) &&
ORKEqualObjects(self.reactionDate, castObject.reactionDate) &&
(self.currentInterval == castObject.currentInterval));
}
- (NSUInteger)hash {
return super.hash ^ _timerStartDate.hash ^ _timerEndDate.hash ^ _stimulusStartDate.hash ^ _reactionDate.hash;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKNormalizedReactionTimeResult *result = [super copyWithZone:zone];
result.timerStartDate = [self.timerStartDate copy];
result.timerEndDate = [self.timerEndDate copy];
result.stimulusStartDate = [self.stimulusStartDate copy];
result.reactionDate = [self.reactionDate copy];
result.currentInterval = self.currentInterval;
return result;
}
@end
@@ -0,0 +1,65 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import Foundation;
@import AudioToolbox;
#import <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKActiveStep.h>
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKNormalizedReactionTimeStep : ORKActiveStep
@property (nonatomic, assign) NSTimeInterval maximumStimulusInterval;
@property (nonatomic, assign) NSTimeInterval minimumStimulusInterval;
@property (nonatomic, assign) NSTimeInterval timeout;
@property (nonatomic, assign) NSInteger numberOfAttempts;
@property (nonatomic, assign) double thresholdAcceleration;
@property (nonatomic, assign) SystemSoundID successSound;
@property (nonatomic, assign) SystemSoundID timeoutSound;
@property (nonatomic, assign) SystemSoundID failureSound;
@property (nonatomic) double currentInterval;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,153 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKNormalizedReactionTimeStep.h"
#import "ORKNormalizedReactionTimeViewController.h"
#import "ORKHelpers_Internal.h"
@implementation ORKNormalizedReactionTimeStep
+ (Class)stepViewControllerClass {
return [ORKNormalizedReactionTimeViewController class];
}
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super initWithIdentifier:identifier];
self.shouldContinueOnFinish = YES;
return self;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKNormalizedReactionTimeStep *step = [super copyWithZone:zone];
step.maximumStimulusInterval = self.maximumStimulusInterval;
step.minimumStimulusInterval = self.minimumStimulusInterval;
step.thresholdAcceleration = self.thresholdAcceleration;
step.timeout = self.timeout;
step.numberOfAttempts = self.numberOfAttempts;
step.successSound = self.successSound;
step.timeoutSound = self.timeoutSound;
step.failureSound = self.failureSound;
self.currentInterval = self.currentInterval;
return step;
}
- (void)validateParameters {
[super validateParameters];
if (self.minimumStimulusInterval <= 0) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"minimumStimulusInterval must be greater than zero"
userInfo:nil];
}
if (self.maximumStimulusInterval < self.minimumStimulusInterval) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"maximumStimulusInterval cannot be less than minimumStimulusInterval"
userInfo:nil];
}
if (self.thresholdAcceleration <= 0) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"thresholdAcceleration must be greater than zero"
userInfo:nil];
}
if (self.timeout <= 0) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"timeout must be greater than zero"
userInfo:nil];
}
if (self.numberOfAttempts <= 0) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"numberOfAttempts must be greater than zero"
userInfo:nil];
}
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_DOUBLE(aDecoder, maximumStimulusInterval);
ORK_DECODE_DOUBLE(aDecoder, minimumStimulusInterval);
ORK_DECODE_DOUBLE(aDecoder, thresholdAcceleration);
ORK_DECODE_DOUBLE(aDecoder, timeout);
ORK_DECODE_UINT32(aDecoder, successSound);
ORK_DECODE_UINT32(aDecoder, timeoutSound);
ORK_DECODE_UINT32(aDecoder, failureSound);
ORK_DECODE_INTEGER(aDecoder, numberOfAttempts);
ORK_DECODE_INTEGER(aDecoder, currentInterval);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_DOUBLE(aCoder, maximumStimulusInterval);
ORK_ENCODE_DOUBLE(aCoder, minimumStimulusInterval);
ORK_ENCODE_DOUBLE(aCoder, thresholdAcceleration);
ORK_ENCODE_DOUBLE(aCoder, timeout);
ORK_ENCODE_UINT32(aCoder, successSound);
ORK_ENCODE_UINT32(aCoder, timeoutSound);
ORK_ENCODE_UINT32(aCoder, failureSound);
ORK_ENCODE_INTEGER(aCoder, numberOfAttempts);
ORK_ENCODE_INTEGER(aCoder, currentInterval);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
(self.maximumStimulusInterval == castObject.maximumStimulusInterval) &&
(self.minimumStimulusInterval == castObject.minimumStimulusInterval) &&
(self.thresholdAcceleration == castObject.thresholdAcceleration) &&
(self.timeout == castObject.timeout) &&
(self.successSound == castObject.successSound) &&
(self.timeoutSound == castObject.timeoutSound) &&
(self.failureSound == castObject.failureSound) &&
(self.numberOfAttempts == castObject.numberOfAttempts) &&
(self.currentInterval == castObject.currentInterval)
);}
- (BOOL)allowsBackNavigation {
return NO;
}
- (BOOL)startsFinished {
return NO;
}
@end
@@ -0,0 +1,49 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import UIKit;
#import "ORKCustomStepView_Internal.h"
NS_ASSUME_NONNULL_BEGIN
@interface ORKNormalizedReactionTimeStimulusView : UIView
- (void)reset;
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void(^)(void))completion;
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void(^)(void))completion;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,166 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKNormalizedReactionTimeStimulusView.h"
@implementation ORKNormalizedReactionTimeStimulusView {
CAShapeLayer *_tickLayer;
CAShapeLayer *_crossLayer;
}
static const CGFloat RoundReactionTimeViewDiameter = 122;
- (instancetype)init {
self = [super init];
if (self) {
self.layer.cornerRadius = RoundReactionTimeViewDiameter * 0.5;
}
return self;
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(RoundReactionTimeViewDiameter, RoundReactionTimeViewDiameter);
}
- (void)reset {
[_tickLayer removeFromSuperlayer];
[_crossLayer removeFromSuperlayer];
_tickLayer = nil;
_crossLayer = nil;
self.layer.backgroundColor = self.tintColor.CGColor;
}
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(void(^)(void))completion {
if (self.hidden) {
if (completion) {
completion();
}
return;
}
[self addTickLayer];
[CATransaction begin];
[CATransaction setCompletionBlock:completion];
CAMediaTimingFunction *timing = [[CAMediaTimingFunction alloc] initWithControlPoints:0.180739998817444 :0 :0.577960014343262 :0.918200016021729];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
[animation setTimingFunction:timing];
animation.removedOnCompletion = NO;
[animation setFillMode:kCAFillModeForwards];
animation.fromValue = @(0);
animation.toValue = @(1);
animation.duration = duration;
[_tickLayer addAnimation:animation forKey:@"strokeEnd"];
[CATransaction commit];
}
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(void(^)(void))completion {
self.hidden = NO;
self.layer.backgroundColor = [UIColor clearColor].CGColor;
[self addCrossLayer];
[CATransaction begin];
[CATransaction setCompletionBlock:completion];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
[animation setFillMode:kCAFillModeForwards];
animation.fromValue = @([(CAShapeLayer *)[_crossLayer presentationLayer] strokeEnd]);
animation.toValue = @(1);
animation.duration = duration;
_crossLayer.strokeEnd = 1;
[_crossLayer addAnimation:animation forKey:@"strokeEnd"];
[CATransaction commit];
}
- (void)setHidden:(BOOL)hidden {
[self reset];
[super setHidden:hidden];
}
- (void)addCrossLayer {
_crossLayer = [self lineDrawingLayer];
_crossLayer.strokeColor = [UIColor redColor].CGColor;
_crossLayer.path = [self crossPath];
[self.layer addSublayer:_crossLayer];
}
- (void)addTickLayer {
_tickLayer = [self lineDrawingLayer];
_tickLayer.strokeColor = [UIColor whiteColor].CGColor;
_tickLayer.path = [self tickPath];
[self.layer addSublayer:_tickLayer];
}
- (CGPathRef)concealPath:(CGFloat)radius {
return [[UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
radius:radius / 2
startAngle:M_PI + M_PI_2
endAngle:-M_PI_2
clockwise:NO] CGPath];
}
- (CGPathRef)tickPath {
UIBezierPath *path = [self linePath];
[path moveToPoint:(CGPoint){37,65}];
[path addLineToPoint:(CGPoint){50,78}];
[path addLineToPoint:(CGPoint){87,42}];
return path.CGPath;
}
- (CGPathRef)crossPath {
UIBezierPath *path = [self linePath];
[path moveToPoint:(CGPoint){45,78}];
[path addLineToPoint:(CGPoint){82,42}];
[path moveToPoint:(CGPoint){45,42}];
[path addLineToPoint:(CGPoint){82,78}];
return path.CGPath;
}
- (UIBezierPath *)linePath {
UIBezierPath *path = [UIBezierPath new];
path.lineCapStyle = kCGLineCapRound;
path.lineWidth = 5;
return path;
}
- (CAShapeLayer *)lineDrawingLayer {
CAShapeLayer *shapeLayer = [CAShapeLayer new];
shapeLayer.strokeEnd = 0;
shapeLayer.lineWidth = 5;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.frame = self.layer.bounds;
shapeLayer.backgroundColor = [UIColor clearColor].CGColor;
shapeLayer.fillColor = nil;
return shapeLayer;
}
@end
@@ -0,0 +1,45 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import UIKit;
#import "ORKDefines.h"
#import "ORKActiveStepViewController.h"
NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKNormalizedReactionTimeViewController : ORKActiveStepViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,272 @@
/*
Copyright (c) 2019, Apple Inc. All rights reserved.
Copyright (c) 2015, James Cox. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKNormalizedReactionTimeViewController.h"
#import "ORKBorderedButton.h"
#import "ORKActiveStepView.h"
#import "ORKNormalizedReactionTimeContentView.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "ORKVerticalContainerView_Internal.h"
#import "ORKCollectionResult_Private.h"
#import "ORKNormalizedReactionTimeResult.h"
#import "ORKNormalizedReactionTimeStep.h"
#import "ORKResult.h"
#import "ORKHelpers_Internal.h"
#import <AudioToolbox/AudioServices.h>
@implementation ORKNormalizedReactionTimeViewController {
ORKNormalizedReactionTimeContentView *_reactionTimeContentView;
NSMutableArray *_results;
NSTimer *_stimulusTimer;
NSTimer *_timeoutTimer;
NSTimeInterval _stimulusTimestamp;
BOOL _validResult;
BOOL _timedOut;
BOOL _shouldIndicateFailure;
UIView *_backgroundView;
ORKNormalizedReactionTimeStimulusView *_stimulusView;
NSDate *_timerStartDate;
NSDate *_stimulusStartDate;
NSDate *_reactionDate;
}
static const NSTimeInterval OutcomeAnimationDuration = 0.3;
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self configureTitle];
_results = [NSMutableArray new];
_reactionTimeContentView = [ORKNormalizedReactionTimeContentView new];
[_reactionTimeContentView.button addTarget:self action:@selector(startStimulusTimer) forControlEvents:UIControlEventTouchDown];
[_reactionTimeContentView.button addTarget:self action:@selector(startReactionTimer) forControlEvents:UIControlEventTouchUpInside];
self.activeStepView.activeCustomView = _reactionTimeContentView;
_backgroundView = [_reactionTimeContentView getBackgroundView];
_stimulusView = [_reactionTimeContentView getStimulusView];
[_backgroundView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected)]];
[_stimulusView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected)]];
[_reactionTimeContentView setStimulusHidden:YES];
}
-(void)startReactionTimer {
if (_stimulusView.hidden) {
_validResult = NO;
_timedOut = YES;
[self addReactionTimeResult];
#if TARGET_IPHONE_SIMULATOR
// Device motion recorder won't work, so manually trigger didfinish
[self attemptDidFinish];
#endif
} else {
_timerStartDate = [NSDate date];
}
}
- (void)tapDetected {
if ([_stimulusTimer isValid] || [_timeoutTimer isValid]) {
_reactionDate = [NSDate date];
[self addReactionTimeResult];
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self start];
_shouldIndicateFailure = YES;
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
_shouldIndicateFailure = NO;
}
#pragma mark - ORKActiveStepViewController
- (void)start {
[super start];
}
- (ORKStepResult *)result {
ORKStepResult *stepResult = [super result];
stepResult.results = [self.addedResults arrayByAddingObjectsFromArray:_results] ? : _results;
return stepResult;
}
- (void)applicationWillResignActive:(NSNotification *)notification {
[super applicationWillResignActive:notification];
_validResult = NO;
[_stimulusTimer invalidate];
[_timeoutTimer invalidate];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
[super applicationDidBecomeActive:notification];
[self resetAfterDelay:0];
}
#pragma mark - ORKRecorderDelegate
- (void)addReactionTimeResult {
ORKNormalizedReactionTimeResult *reactionTimeResult = [[ORKNormalizedReactionTimeResult alloc] initWithIdentifier:self.step.identifier];
reactionTimeResult.timerStartDate = _timerStartDate;
reactionTimeResult.timerEndDate = [NSDate date];
reactionTimeResult.reactionDate = _reactionDate;
reactionTimeResult.stimulusStartDate = _stimulusStartDate;
reactionTimeResult.currentInterval = [self reactionTimeStep].currentInterval;
[_results addObject:reactionTimeResult];
_timerStartDate = nil;
_reactionDate = nil;
_stimulusStartDate = nil;
[self attemptDidFinish];
}
#pragma mark - ORKReactionTimeStepViewController
- (ORKNormalizedReactionTimeStep *)reactionTimeStep {
return (ORKNormalizedReactionTimeStep *)self.step;
}
- (void)configureTitle {
NSString *format = ORKLocalizedString(@"REACTION_TIME_TASK_ATTEMPTS_FORMAT", nil);
NSString *text = [[NSString stringWithFormat: @"%@\n",ORKLocalizedString(@"REACTION_TIME_NORMALIZED_TASK_ACTIVE_STEP_TITLE", nil)] stringByAppendingString: [NSString stringWithFormat:format, ORKLocalizedStringFromNumber(@(_results.count + 1)), ORKLocalizedStringFromNumber(@([self reactionTimeStep].numberOfAttempts))]];
[self.activeStepView updateTitle:nil text:text];
}
- (void)attemptDidFinish {
void (^completion)(void) = ^{
if (_results.count == [self reactionTimeStep].numberOfAttempts) {
[self finish];
} else {
[self resetAfterDelay:2];
}
};
if (_validResult) {
[self indicateSuccess:completion];
} else {
[self indicateFailure:completion];
}
_validResult = NO;
_timedOut = NO;
[_stimulusTimer invalidate];
[_timeoutTimer invalidate];
}
- (void)indicateSuccess:(void(^)(void))completion {
[_reactionTimeContentView startSuccessAnimationWithDuration:OutcomeAnimationDuration completion:completion];
AudioServicesPlaySystemSound([self reactionTimeStep].successSound);
}
- (void)indicateFailure:(void(^)(void))completion {
if (!_shouldIndicateFailure) {
return;
}
[_reactionTimeContentView startFailureAnimationWithDuration:OutcomeAnimationDuration completion:completion];
SystemSoundID sound = _timedOut ? [self reactionTimeStep].timeoutSound : [self reactionTimeStep].failureSound;
AudioServicesPlayAlertSound(sound);
}
- (void)resetAfterDelay:(NSTimeInterval)delay {
ORKWeakTypeOf(self) weakSelf = self;
[_reactionTimeContentView resetAfterDelay:delay completion:^{
[weakSelf configureTitle];
[weakSelf start];
}];
}
- (void)startStimulusTimer {
_stimulusTimer = [NSTimer scheduledTimerWithTimeInterval:[self stimulusInterval] target:self selector:@selector(stimulusTimerDidFire) userInfo:nil repeats:NO];
}
- (void)stimulusTimerDidFire {
_stimulusStartDate = [NSDate date];
_stimulusTimestamp = [NSProcessInfo processInfo].systemUptime;
[_reactionTimeContentView setStimulusHidden:NO];
_validResult = YES;
[self startTimeoutTimer];
}
- (void)startTimeoutTimer {
NSTimeInterval timeout = [self reactionTimeStep].timeout;
if (timeout > 0) {
_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:@selector(timeoutTimerDidFire) userInfo:nil repeats:NO];
}
}
- (void)timeoutTimerDidFire {
_validResult = NO;
_timedOut = YES;
[self addReactionTimeResult];
#if TARGET_IPHONE_SIMULATOR
// Device motion recorder won't work, so manually trigger didfinish
[self attemptDidFinish];
#endif
}
- (NSTimeInterval)stimulusInterval {
ORKNormalizedReactionTimeStep *step = [self reactionTimeStep];
NSNumber* interval = [self getRandomInterval];
step.currentInterval = interval.doubleValue;
return [interval doubleValue];
}
- (NSNumber*) getRandomInterval {
NSArray* values = @[@2,@4,@6];
int randIndex = arc4random() % [values count];
return (NSNumber*)values[randIndex];
}
@end
@@ -30,7 +30,7 @@
#import "ORKPSATKeyboardView.h"
#import "ORKHelpers_Internal.h"
#import "ORKBorderedButton.h"
@@ -80,6 +80,7 @@ NSUInteger const ORKPSATMaximumAnswer = 17;
- (void)setEnabled:(BOOL)enabled {
for (ORKBorderedButton *answerButton in self.answerButtons) {
[answerButton setEnabled:enabled];
[answerButton setBackgroundColor:ORKViewTintColor(self)];
}
}
@@ -189,7 +189,7 @@
*/
- (double)getDeviceAngleInDegreesFromAttitude:(CMAttitude *)attitude {
if (!_orientation) {
_orientation = [UIApplication sharedApplication].statusBarOrientation;
_orientation = self.view.window.windowScene.interfaceOrientation;
}
double angle;
if (UIInterfaceOrientationIsLandscape(_orientation)) {
@@ -159,22 +159,24 @@ static const NSTimeInterval OutcomeAnimationDuration = 0.3;
}
- (void)attemptDidFinish {
void (^completion)(void) = ^{
if (_results.count == [self reactionTimeStep].numberOfAttempts) {
[self finish];
dispatch_async(dispatch_get_main_queue(), ^(void) {
void (^completion)(void) = ^{
if (_results.count == [self reactionTimeStep].numberOfAttempts) {
[self finish];
} else {
[self resetAfterDelay:2];
}
};
if (_validResult) {
[self indicateSuccess:completion];
} else {
[self resetAfterDelay:2];
[self indicateFailure:completion];
}
};
if (_validResult) {
[self indicateSuccess:completion];
} else {
[self indicateFailure:completion];
}
_validResult = NO;
_timedOut = NO;
[_stimulusTimer invalidate];
[_timeoutTimer invalidate];
_validResult = NO;
_timedOut = NO;
[_stimulusTimer invalidate];
[_timeoutTimer invalidate];
});
}
- (void)indicateSuccess:(void(^)(void))completion {
+1
View File
@@ -242,6 +242,7 @@
ORKFileResult *result = [[ORKFileResult alloc] initWithIdentifier:self.identifier];
result.contentType = [self mimeType];
result.fileURL = fileUrl;
result.fileName = [fileUrl lastPathComponent];
result.userInfo = self.userInfo;
result.startDate = self.startDate;
@@ -148,4 +148,14 @@ ORK_CLASS_AVAILABLE
@end
// A simple audio streaming configuration which does not save any audio. Only streams audio buffers.
ORK_CLASS_AVAILABLE
@interface ORKAudioStreamerConfiguration : ORKRecorderConfiguration
- (instancetype)initWithIdentifier:(NSString *)identifier NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END
@@ -260,7 +260,7 @@
- (void)setButtonItem:(ORKBorderedButton *)buttonItem {
_buttonItem = buttonItem;
if (buttonItem) {
buttonItem.contentEdgeInsets = (UIEdgeInsets){.top = 2, .bottom = 2, .left = 8, .right = 8};
[buttonItem updateContentInsets:NSDirectionalEdgeInsetsMake(2, 8, 2, 8)];
buttonItem.translatesAutoresizingMaskIntoConstraints = NO;
[_continueView addSubview:buttonItem];
[[NSLayoutConstraint constraintWithItem:_buttonItem
@@ -30,7 +30,7 @@
#import "ORKSpeechInNoiseContentView.h"
#import "ORKAudioGraphView.h"
#import "ORKAudioMeteringView.h"
#import "ORKHeadlineLabel.h"
#import "ORKSubheadlineLabel.h"
@@ -41,10 +41,15 @@
#import "ORKSkin.h"
#import "ORKPlaybackButton.h"
static CGFloat const ORKSpeechInNoiseContentFlamesViewHeightConstant = 150.0;
static CGFloat const ORKSpeechInNoiseContentFlamesViewVerticalSpacing = 44.0;
static CGFloat const ORKSpeechInNoiseContentViewVerticalMargin = 44;
@interface ORKSpeechInNoiseContentView () <UITextFieldDelegate>
@property (nonatomic, strong) ORKAudioGraphView *graphView;
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
@property (nonatomic, strong) ORKSubheadlineLabel *transcriptLabel;
@property (nonatomic, copy) NSArray<NSLayoutConstraint *> *constraints;
@end
@@ -71,6 +76,11 @@
return self;
}
- (void)drawRect:(CGRect)rect
{
[self setUpConstraints];
}
- (void)setupTextLabel {
_textLabel = [ORKSubheadlineLabel new];
_textLabel.textAlignment = NSTextAlignmentCenter;
@@ -81,9 +91,9 @@
}
- (void)setupGraphView {
self.graphView = [ORKAudioGraphView new];
self.graphView = [[ORKAudioMeteringView alloc] init];
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
[_graphView setMeterColor:[UIColor lightGrayColor]];
[self addSubview:_graphView];
}
@@ -121,72 +131,65 @@
[self applyAlertColor];
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
- (void)setUpConstraints
{
if (self.constraints.count > 0)
{
[NSLayoutConstraint deactivateConstraints:self.constraints];
}
NSDictionary *views = NSDictionaryOfVariableBindings(_textLabel, _graphView, _playButton);
const CGFloat graphHeight = 150;
NSLayoutConstraint *centeredYLayoutConstaint = [_graphView.centerYAnchor constraintLessThanOrEqualToAnchor:self.centerYAnchor constant:-ORKSpeechInNoiseContentFlamesViewVerticalSpacing];
centeredYLayoutConstaint.priority = UILayoutPriorityDefaultLow;
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_textLabel]-(5)-[_graphView(graphHeight)]-buttonGap-[_playButton]-(>=topBottomMargin)-|"
options:(NSLayoutFormatOptions)0
metrics:@{
@"graphHeight": @(graphHeight),
@"topBottomMargin" : @(5),
@"buttonGap" : @(20)
}
views:views]];
self.constraints = @[
[_graphView.topAnchor constraintGreaterThanOrEqualToAnchor:self.topAnchor],
centeredYLayoutConstaint,
[_graphView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_graphView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_graphView.heightAnchor constraintEqualToConstant:ORKSpeechInNoiseContentFlamesViewHeightConstant],
[_playButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_playButton.topAnchor constraintGreaterThanOrEqualToAnchor:_graphView.bottomAnchor constant:ORKSpeechInNoiseContentFlamesViewVerticalSpacing],
[_playButton.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-ORKSpeechInNoiseContentViewVerticalMargin]
];
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
const CGFloat twiceSideMargin = sideMargin * 2;
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_textLabel]-|"
options:0
metrics: nil
views:views]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
options:0
metrics: @{@"sideMargin": @(sideMargin)}
views:views]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-twiceSideMargin-[_playButton(>=200)]-twiceSideMargin-|"
options:0
metrics: @{@"twiceSideMargin": @(twiceSideMargin)}
views:views]];
[NSLayoutConstraint activateConstraints:constraints];
[NSLayoutConstraint activateConstraints:self.constraints];
}
- (void)updateGraphSamples {
_graphView.values = _samples;
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
[self setUpConstraints];
}
- (void)setGraphViewHidden:(BOOL)hidden {
- (void)updateGraphSamples
{
_graphView.samples = _samples;
}
- (void)setGraphViewHidden:(BOOL)hidden
{
[_graphView setHidden:hidden];
}
- (void)addSample:(NSNumber *)sample {
- (void)addSample:(NSNumber *)sample
{
NSAssert(sample != nil, @"Sample should be non-nil");
if (!_samples) {
_samples = [NSMutableArray array];
}
[_samples addObject:sample];
// Try to keep around 250 samples
if (_samples.count > 500) {
_samples = [[_samples subarrayWithRange:(NSRange){250, _samples.count - 250}] mutableCopy];
}
_samples = [ORKLastNSamples(_samples, 500) mutableCopy];
[self updateGraphSamples];
}
- (void)removeAllSamples {
- (void)removeAllSamples
{
_samples = nil;
[self updateGraphSamples];
}
@end
@@ -0,0 +1,52 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <ResearchKit/ORKResult.h>
NS_ASSUME_NONNULL_BEGIN
/**
The `ORKSpeechInNoiseResult` class represents the result of a single successful attempt of an ORKSpeechInNoiseStep.
A speech-in-noise result contains a single string representing the target sentence to be repeated in subsequent ORKSpeechRecognitionSteps.
*/
ORK_CLASS_AVAILABLE
@interface ORKSpeechInNoiseResult : ORKResult
@property (nonatomic, copy, nullable) NSString *filename;
@property (nonatomic, copy, nullable) NSString *targetSentence;
@end
NS_ASSUME_NONNULL_END
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -28,65 +28,56 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKVisualConsentStep.h"
#import "ORKVisualConsentStepViewController.h"
#import "ORKConsentDocument_Internal.h"
#import "ORKStep_Private.h"
#import "ORKSpeechInNoiseResult.h"
#import "ORKHelpers_Internal.h"
#import "ORKResult_Private.h"
@implementation ORKSpeechInNoiseResult
@implementation ORKVisualConsentStep
+ (Class)stepViewControllerClass {
return [ORKVisualConsentStepViewController class];
}
- (instancetype)initWithIdentifier:(NSString *)identifier document:(ORKConsentDocument *)consentDocument {
self = [super initWithIdentifier:identifier];
if (self) {
self.consentDocument = consentDocument;
self.showsProgress = NO;
}
return self;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKVisualConsentStep *step = [super copyWithZone:zone];
step.consentDocument = self.consentDocument;
return step;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, consentDocument, ORKConsentDocument);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[super encodeWithCoder:aCoder];
ORK_ENCODE_OBJ(aCoder, consentDocument);
ORK_ENCODE_OBJ(aCoder, filename);
ORK_ENCODE_OBJ(aCoder, targetSentence);
}
+ (BOOL)supportsSecureCoding {
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
ORK_DECODE_OBJ_CLASS(aDecoder, filename, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, targetSentence, NSString);
}
return self;
}
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (BOOL)isEqual:(id)object {
- (BOOL)isEqual:(id)object
{
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
ORKEqualObjects(self.consentDocument, castObject.consentDocument));
ORKEqualObjects(self.targetSentence, castObject.targetSentence) &&
ORKEqualObjects(self.filename, castObject.filename));
}
- (NSUInteger)hash {
return super.hash ^ self.consentDocument.hash;
- (instancetype)copyWithZone:(NSZone *)zone
{
ORKSpeechInNoiseResult *result = [super copyWithZone:zone];
result.targetSentence = [self.targetSentence copy];
result.filename = [self.filename copy];
return result;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"filename = %@;\r\ntargetSentence = %@;", self.filename, self.targetSentence];
}
@end
@@ -41,6 +41,16 @@ ORK_CLASS_AVAILABLE
*/
@interface ORKSpeechInNoiseStep : ORKActiveStep
/**
This property accepts the speech file Path.
*/
@property (nonatomic, copy, nullable) NSString *speechFilePath;
/**
This property acceopts the string representation of the speech to be played.
*/
@property (nonatomic, copy, nullable) NSString *targetSentence;
/**
This property accepts the speech file.
*/
@@ -54,6 +54,8 @@
- (void)commonInit {
_willAudioLoop = NO;
_speechFilePath = nil;
_targetSentence = nil;
_noiseFileNameWithExtension = @ORKSpeechInNoiseDefaultNoiseFileName;
_filterFileNameWithExtension = @ORKSpeechInNoiseDefaultFilterFileName;
_speechFileNameWithExtension = @ORKSpeechInNoiseDefaultSpeechFileName;
@@ -73,6 +75,8 @@
- (instancetype)copyWithZone:(NSZone *)zone {
ORKSpeechInNoiseStep *step = [super copyWithZone:zone];
step.speechFilePath = self.speechFilePath;
step.targetSentence = self.targetSentence;
step.speechFileNameWithExtension = self.speechFileNameWithExtension;
step.noiseFileNameWithExtension = self.noiseFileNameWithExtension;
step.filterFileNameWithExtension = self.filterFileNameWithExtension;
@@ -85,9 +89,11 @@
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ(aDecoder, speechFileNameWithExtension);
ORK_DECODE_OBJ(aDecoder, noiseFileNameWithExtension);
ORK_DECODE_OBJ(aDecoder, filterFileNameWithExtension);
ORK_DECODE_OBJ_CLASS(aDecoder, speechFilePath, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, targetSentence, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, speechFileNameWithExtension, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, noiseFileNameWithExtension, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, filterFileNameWithExtension, NSString);
ORK_DECODE_DOUBLE(aDecoder, gainAppliedToNoise);
ORK_DECODE_BOOL(aDecoder, willAudioLoop);
ORK_DECODE_BOOL(aDecoder, hideGraphView);
@@ -97,6 +103,8 @@
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_OBJ(aCoder, speechFilePath);
ORK_ENCODE_OBJ(aCoder, targetSentence);
ORK_ENCODE_OBJ(aCoder, speechFileNameWithExtension);
ORK_ENCODE_OBJ(aCoder, noiseFileNameWithExtension);
ORK_ENCODE_OBJ(aCoder, filterFileNameWithExtension);
@@ -114,6 +122,8 @@
__typeof(self) castObject = object;
return (isParentSame
&& ORKEqualObjects(self.speechFilePath, castObject.speechFilePath)
&& ORKEqualObjects(self.targetSentence, castObject.targetSentence)
&& ORKEqualObjects(self.speechFileNameWithExtension, castObject.speechFileNameWithExtension)
&& ORKEqualObjects(self.noiseFileNameWithExtension, castObject.noiseFileNameWithExtension)
&& ORKEqualObjects(self.filterFileNameWithExtension, castObject.filterFileNameWithExtension)
@@ -37,12 +37,17 @@
#import "ORKStepContainerView_Private.h"
#import "ORKSpeechInNoiseContentView.h"
#import "ORKSpeechInNoiseStep.h"
#import "ORKSpeechInNoiseResult.h"
#import "ORKCollectionResult_Private.h"
#import "ORKHelpers_Internal.h"
#import "ORKRoundTappingButton.h"
#import "ORKPlaybackButton.h"
#import "ORKSkin.h"
#import "ORKTaskViewController.h"
#import "ORKTaskViewController_Internal.h"
#import <AVFoundation/AVFoundation.h>
@import Accelerate;
@@ -74,9 +79,10 @@
_speechAudioBuffer = [[AVAudioPCMBuffer alloc] init];
_filterAudioBuffer = [[AVAudioPCMBuffer alloc] init];
_installedTap = NO;
self.speechInNoiseContentView = [[ORKSpeechInNoiseContentView alloc] init];
self.activeStepView.activeCustomView = self.speechInNoiseContentView;
self.activeStepView.customContentFillsAvailableSpace = YES;
self.activeStepView.customContentFillsAvailableSpace = NO;
_speechInNoiseContentView.alertColor = [UIColor blueColor];
[self.speechInNoiseContentView.playButton addTarget:self action:@selector(tapButtonPressed) forControlEvents:UIControlEventTouchDown];
[_speechInNoiseContentView setGraphViewHidden:[self speechInNoiseStep].hideGraphView];
@@ -102,7 +108,17 @@
}
- (void)setupBuffers {
[self loadFileName:[self speechInNoiseStep].speechFileNameWithExtension intoBuffer:&_speechAudioBuffer];
if ([[self speechInNoiseStep] speechFilePath] != nil)
{
NSURL *url = [NSURL fileURLWithPath:[self speechInNoiseStep].speechFilePath isDirectory:NO];
[self loadFileAtURL:url intoBuffer:&_speechAudioBuffer];
}
else
{
[self loadFileName:[self speechInNoiseStep].speechFileNameWithExtension intoBuffer:&_speechAudioBuffer];
}
[self loadFileName:[self speechInNoiseStep].noiseFileNameWithExtension intoBuffer:&_noiseAudioBuffer];
[self loadFileName:[self speechInNoiseStep].filterFileNameWithExtension intoBuffer:&_filterAudioBuffer];
@@ -124,6 +140,32 @@
}
}
- (void)loadFileAtURL:(NSURL *)url intoBuffer:(AVAudioPCMBuffer * __strong *)buffer {
AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:url error:nil];
AVAudioFormat *audioFileFormat = audioFile.processingFormat;
AVAudioFrameCount audioFileCapacity = (AVAudioFrameCount)audioFile.length;
if (*buffer == _filterAudioBuffer)
{
_speechToneCapacity = audioFileCapacity;
}
else if (*buffer == _noiseAudioBuffer)
{
_noiseToneCapacity = audioFileCapacity;
}
else
{
AVAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
CMTime audioDuration = asset.duration;
_toneDuration = CMTimeGetSeconds(audioDuration);
}
*buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFileFormat frameCapacity:audioFileCapacity];
[audioFile readIntoBuffer:*buffer error:nil];
}
- (void)loadFileName: (NSString *)file intoBuffer: (AVAudioPCMBuffer * __strong *)buffer {
NSArray *fileComponents = [file componentsSeparatedByString:@"."];
NSString *fileName = fileComponents[0];
@@ -131,27 +173,14 @@
NSURL *fileURL = [[NSBundle bundleForClass:[self class]] URLForResource:fileName withExtension:fileExtension];
AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:fileURL error:nil];
AVAudioFormat *audioFileFormat = audioFile.processingFormat;
AVAudioFrameCount audioFileCapacity = (AVAudioFrameCount)audioFile.length;
if (*buffer == _filterAudioBuffer) {
_speechToneCapacity = audioFileCapacity;
} else if (*buffer == _noiseAudioBuffer) {
_noiseToneCapacity = audioFileCapacity;
} else {
AVAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
CMTime audioDuration = asset.duration;
_toneDuration = CMTimeGetSeconds(audioDuration);
}
*buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFileFormat frameCapacity:audioFileCapacity];
[audioFile readIntoBuffer:*buffer error:nil];
[self loadFileAtURL:fileURL intoBuffer:buffer];
}
- (void)installTap {
AVAudioFormat *mainMixerFormat = [[_audioEngine mainMixerNode] outputFormatForBus:0];
[_mixerNode installTapOnBus:0 bufferSize:1024 format:mainMixerFormat block:^(AVAudioPCMBuffer * _Nonnull buffer5, AVAudioTime * _Nonnull when) {
[_mixerNode installTapOnBus:0 bufferSize:64 format:mainMixerFormat block:^(AVAudioPCMBuffer * _Nonnull buffer5, AVAudioTime * _Nonnull when) {
float * const *channelData = [buffer5 floatChannelData];
if (channelData[0]) {
float avgValue = 0;
@@ -176,11 +205,12 @@
[_mixerNode removeTapOnBus:0];
[self finish];
} else {
[self.navigationItem setHidesBackButton:YES animated:YES];
[self installTap];
[_playerNode play];
if ([self speechInNoiseStep].willAudioLoop) {
[_speechInNoiseContentView.playButton setTitle:ORKLocalizedString(@"SPEECH_IN_NOISE_STOP_AUDIO_LABEL", nil)
forState:UIControlStateNormal];
forState:UIControlStateNormal];
[_speechInNoiseContentView.playButton setTintColor:[UIColor ork_redColor]];
} else {
ORKWeakTypeOf(self) weakSelf = self;
@@ -199,4 +229,57 @@
return (ORKSpeechInNoiseStep *)self.step;
}
- (NSString *)filename
{
NSString *filename = nil;
BOOL (^validate)(NSString * _Nullable) = ^BOOL(NSString * _Nullable str) { return str && str.length > 0; };
NSString *path = [[self speechInNoiseStep] speechFilePath];
NSString *file = [path lastPathComponent];
if (validate(file))
{
filename = [file copy];
}
return filename;
}
- (ORKStepResult *)result
{
ORKStepResult *sResult = [super result];
ORKSpeechInNoiseStep *currentStep = (ORKSpeechInNoiseStep *)self.step;
if (currentStep && [currentStep isKindOfClass:[ORKSpeechInNoiseStep class]] && currentStep.targetSentence)
{
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
ORKSpeechInNoiseResult *speechInNoiseResult = [[ORKSpeechInNoiseResult alloc] initWithIdentifier:currentStep.identifier];
speechInNoiseResult.targetSentence = currentStep.targetSentence;
speechInNoiseResult.filename = [self filename];
[results addObject:speechInNoiseResult];
sResult.results = [results copy];
}
return sResult;
}
- (void)finish
{
[_speechInNoiseContentView removeAllSamples];
[super finish];
}
@end
@@ -34,10 +34,20 @@
NS_ASSUME_NONNULL_BEGIN
@class ORKBorderedButton;
@class ORKRecordButton;
@protocol ORKSpeechRecognitionContentViewDelegate <NSObject>
- (void)didPressRecordButton:(ORKRecordButton *)recordButton;
- (void)didPressUseKeyboardButton;
@end
@interface ORKSpeechRecognitionContentView : ORKActiveStepCustomView
@property (nonatomic, weak) id<ORKSpeechRecognitionContentViewDelegate> delegate;
@property (nonatomic, copy, nullable) UIColor *keyColor;
@property (nonatomic, assign) BOOL failed;
@@ -46,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy, nullable) NSArray *samples;
@property (nonatomic) ORKBorderedButton *recordButton;
@property (nonatomic) ORKRecordButton *recordButton;
@property (nonatomic, copy, nullable) UIImage *speechRecognitionImage;
@@ -60,7 +70,9 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateRecognitionText:(NSString *)recognitionText;
- (void)addRecognitionError:(NSString *)errorMsg;
- (void)addRecognitionError:(NSString * _Nullable)errorMsg;
- (void)updateButtonStates;
@end
@@ -29,8 +29,9 @@
*/
#import "ORKSpeechRecognitionContentView.h"
#import "ORKAudioGraphView.h"
#import "ORKAudioMeteringView.h"
#import "ORKHeadlineLabel.h"
#import "ORKSubheadlineLabel.h"
@@ -39,21 +40,27 @@
#import "ORKAccessibility.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKBorderedButton.h"
#import "ORKRecordButton.h"
@interface ORKSpeechRecognitionContentView () <UITextFieldDelegate>
static CGFloat const ORKSpeechRecognitionContentFlamesViewHeightConstant = 150.0;
static CGFloat const ORKSpeechRecognitionContentFlamesViewMaxOffset = 44.0;
static CGFloat const ORKSpeechRecognitionContentRecordButtonVerticalSpacing = 20.0;
static CGFloat const ORKSpeechRecognitionContentBottomLayoutMargin = 44.0;
@property (nonatomic, strong) ORKAudioGraphView *graphView;
@interface ORKSpeechRecognitionContentView () <UITextFieldDelegate, ORKRecordButtonDelegate>
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
@property (nonatomic, strong) ORKSubheadlineLabel *transcriptLabel;
@property (nonatomic, copy) NSArray<NSLayoutConstraint *> *constraints;
@end
@implementation ORKSpeechRecognitionContentView {
NSMutableArray *_samples;
UIColor *_keyColor;
UIImageView *_imageView;
UILabel *_textLabel;
UIButton *_useKeyboardButton;
}
- (instancetype)initWithFrame:(CGRect)frame {
@@ -61,12 +68,12 @@
if (self) {
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
self.translatesAutoresizingMaskIntoConstraints = NO;
[self setupTranscriptLabel];
[self setupGraphView];
[self setupRecordButton];
[self setupImageView];
[self setupTextLabel];
[self setupUseKeyboardButton];
[self updateGraphSamples];
[self applyKeyColor];
[self setUpConstraints];
@@ -74,15 +81,37 @@
return self;
}
- (void)drawRect:(CGRect)rect
{
[self setUpConstraints];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
[self setUpConstraints];
NSAttributedString *attributedTitle = [[NSAttributedString alloc]
initWithString:ORKLocalizedString(@"SPEECH_IN_NOISE_PREDEFINED_USE_KEYBOARD_INSTEAD", nil)
attributes:@{NSFontAttributeName:[self buttonTextFont],
NSForegroundColorAttributeName:self.tintColor}];
[_useKeyboardButton setAttributedTitle:attributedTitle forState:UIControlStateNormal];
}
- (void)setupImageView {
_imageView = [UIImageView new];
_imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
_imageView.backgroundColor = [UIColor redColor];
[self addSubview:_imageView];
}
- (void)setupTextLabel {
_textLabel = [UILabel new];
_textLabel.backgroundColor = [UIColor greenColor];
_textLabel.font = [[UIFontMetrics metricsForTextStyle:UIFontTextStyleTitle2] scaledFontForFont:[UIFont systemFontOfSize:25.0 weight:UIFontWeightMedium]];
_textLabel.textColor = [self tintColor];
_textLabel.textAlignment = NSTextAlignmentCenter;
@@ -94,7 +123,7 @@
}
- (void)setupGraphView {
self.graphView = [ORKAudioGraphView new];
self.graphView = [[ORKAudioMeteringView alloc] init];
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
_graphView.isAccessibilityElement = YES;
_graphView.accessibilityLabel = ORKLocalizedString(@"AX_SPEECH_RECOGNITION_WAVEFORM", nil);
@@ -114,15 +143,100 @@
[self addSubview:_transcriptLabel];
}
- (void)setupRecordButton {
self.recordButton = [[ORKBorderedButton alloc] init];
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.recordButton setTitle:ORKLocalizedString(@"SPEECH_RECOGNITION_START_RECORD_LABEL", nil)
forState:UIControlStateNormal];
self.recordButton.enabled = YES;
self.recordButton.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitStartsMediaSession;
self.recordButton.accessibilityHint = ORKLocalizedString(@"AX_SPEECH_RECOGNITION_START_RECORDING_HINT", nil);
[self addSubview:_recordButton];
- (void)setupRecordButton
{
if (!_recordButton)
{
self.recordButton = [[ORKRecordButton alloc] init];
self.recordButton.delegate = self;
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
self.recordButton.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitStartsMediaSession;
self.recordButton.accessibilityHint = ORKLocalizedString(@"AX_SPEECH_RECOGNITION_START_RECORDING_HINT", nil);
[self addSubview:_recordButton];
}
}
- (void)buttonPressed:(ORKRecordButton *)recordButton
{
if ([self.delegate conformsToProtocol:@protocol(ORKSpeechRecognitionContentViewDelegate)] &&
[self.delegate respondsToSelector:@selector(didPressRecordButton:)])
{
[self.delegate didPressRecordButton:recordButton];
}
switch ([recordButton buttonType])
{
case ORKRecordButtonTypeRecord:
[recordButton setButtonType:ORKRecordButtonTypeStop animated:YES];
[self setKeyboardButtonEnabled:NO];
break;
default:
[recordButton setButtonType:ORKRecordButtonTypeRecord animated:YES];
[self setKeyboardButtonEnabled:YES];
break;
}
}
- (void)updateButtonStates
{
switch ([_recordButton buttonType])
{
case ORKRecordButtonTypeRecord:
[self setKeyboardButtonEnabled:YES];
break;
default:
[self setKeyboardButtonEnabled:NO];
break;
}
}
- (void)setupUseKeyboardButton
{
_useKeyboardButton = [[UIButton alloc] init];
if (@available(iOS 13.0, *))
{
[_useKeyboardButton setImage:[UIImage systemImageNamed:@"keyboard" compatibleWithTraitCollection:self.traitCollection] forState:UIControlStateNormal];
}
_useKeyboardButton.adjustsImageWhenHighlighted = NO;
NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:ORKLocalizedString(@"SPEECH_IN_NOISE_PREDEFINED_USE_KEYBOARD_INSTEAD", nil)
attributes:@{NSFontAttributeName:[self buttonTextFont],
NSForegroundColorAttributeName:self.tintColor}];
[_useKeyboardButton setAttributedTitle:attributedTitle forState:UIControlStateNormal];
[_useKeyboardButton setTranslatesAutoresizingMaskIntoConstraints:NO];
_useKeyboardButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
_useKeyboardButton.titleLabel.textAlignment = NSTextAlignmentCenter;
CGFloat spacing = 8;
_useKeyboardButton.imageEdgeInsets = UIEdgeInsetsMake(0, -(spacing/2), 0, (spacing/2));
_useKeyboardButton.titleEdgeInsets = UIEdgeInsetsMake(0, (spacing/2), 0, -(spacing/2));
_useKeyboardButton.contentEdgeInsets = UIEdgeInsetsMake(0, -spacing, 0, -spacing);
[self addSubview:_useKeyboardButton];
[_useKeyboardButton addTarget:self action:@selector(useKeyboardButtonPressed) forControlEvents:UIControlEventTouchUpInside];
}
- (void)setKeyboardButtonEnabled:(BOOL)enabled
{
_useKeyboardButton.userInteractionEnabled = enabled;
_useKeyboardButton.alpha = enabled ? 1.0 : 0.25;
}
- (void)useKeyboardButtonPressed
{
if ([self.delegate conformsToProtocol:@protocol(ORKSpeechRecognitionContentViewDelegate)] &&
[self.delegate respondsToSelector:@selector(didPressUseKeyboardButton)])
{
[self.delegate didPressUseKeyboardButton];
}
}
- (UIFont *)buttonTextFont
{
CGFloat fontSize = [[UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleCallout] pointSize];
return [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold];
}
- (void)setSpeechRecognitionText:(NSString *)speechRecognitionText {
@@ -145,7 +259,7 @@
- (void)applyKeyColor {
UIColor *keyColor = [self keyColor];
_graphView.keyColor = keyColor;
[_graphView setMeterColor:keyColor];
}
- (UIColor *)keyColor {
@@ -157,60 +271,47 @@
[self applyKeyColor];
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
- (void)setUpConstraints
{
if (self.constraints.count > 0)
{
[NSLayoutConstraint deactivateConstraints:self.constraints];
}
NSLayoutConstraint *centeredGraphOnScreenLayoutConstraint = [_graphView.centerYAnchor constraintLessThanOrEqualToAnchor:self.centerYAnchor constant:-ORKSpeechRecognitionContentFlamesViewMaxOffset];
centeredGraphOnScreenLayoutConstraint.priority = UILayoutPriorityDefaultLow;
NSDictionary *views = NSDictionaryOfVariableBindings(_imageView, _textLabel, _transcriptLabel, _graphView, _recordButton);
const CGFloat graphHeight = 150;
// In case the text on the button is large, ensure that the button can grow larger than the default height if needed
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_imageView]-[_textLabel]-(5)-[_graphView(graphHeight)]-[_transcriptLabel]-buttonGap-[_recordButton(50@250)]-topBottomMargin-|"
options:(NSLayoutFormatOptions)0
metrics:@{
@"graphHeight": @(graphHeight),
@"topBottomMargin" : @(5),
@"buttonGap" : @(20)
}
views:views]];
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
const CGFloat twiceSideMargin = sideMargin * 2;
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_imageView]-|"
options:0
metrics: nil
views:views]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_textLabel]-|"
options:0
metrics: nil
views:views]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
options:0
metrics: @{@"sideMargin": @(sideMargin)}
views:views]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_transcriptLabel]-|"
options:0
metrics: @{@"sideMargin": @(sideMargin)}
views:views]];
// In case the text on the button is large, ensure that the button can grow larger than the default width if needed
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-twiceSideMargin@250-[_recordButton(200@250)]-twiceSideMargin@250-|"
options:0
metrics: @{@"twiceSideMargin": @(twiceSideMargin)}
views:views]];
[constraints addObject:[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor]];
[constraints addObject:[_recordButton.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.layoutMarginsGuide.leadingAnchor]];
[constraints addObject:[_recordButton.trailingAnchor constraintLessThanOrEqualToAnchor:self.layoutMarginsGuide.trailingAnchor]];
[NSLayoutConstraint activateConstraints:constraints];
self.constraints = @[
[_imageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_imageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_imageView.topAnchor constraintEqualToAnchor:self.topAnchor],
[_textLabel.topAnchor constraintEqualToAnchor:_imageView.bottomAnchor],
[_textLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_textLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_graphView.topAnchor constraintGreaterThanOrEqualToAnchor:_textLabel.bottomAnchor],
centeredGraphOnScreenLayoutConstraint,
[_graphView.heightAnchor constraintEqualToConstant:ORKSpeechRecognitionContentFlamesViewHeightConstant],
[_graphView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_graphView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_transcriptLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_transcriptLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_transcriptLabel.topAnchor constraintGreaterThanOrEqualToAnchor:_graphView.bottomAnchor],
[_transcriptLabel.bottomAnchor constraintEqualToAnchor:_recordButton.topAnchor constant:-ORKSpeechRecognitionContentRecordButtonVerticalSpacing],
[_recordButton.topAnchor constraintGreaterThanOrEqualToAnchor:_transcriptLabel.bottomAnchor constant:ORKSpeechRecognitionContentRecordButtonVerticalSpacing],
[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_useKeyboardButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_useKeyboardButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_useKeyboardButton.topAnchor constraintEqualToAnchor:_recordButton.bottomAnchor constant:ORKSpeechRecognitionContentRecordButtonVerticalSpacing],
[_useKeyboardButton.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-ORKSpeechRecognitionContentBottomLayoutMargin]
];
[NSLayoutConstraint activateConstraints:self.constraints];
}
- (void)setShouldHideTranscript:(BOOL)shouldHideTranscript {
@@ -221,19 +322,20 @@
}
- (void)updateGraphSamples {
_graphView.values = _samples;
_graphView.samples = _samples;
}
- (void)addSample:(NSNumber *)sample {
NSAssert(sample != nil, @"Sample should be non-nil");
if (!_samples) {
_samples = [NSMutableArray array];
}
[_samples addObject:sample];
// Try to keep around 250 samples
if (_samples.count > 500) {
_samples = [[_samples subarrayWithRange:(NSRange){250, _samples.count - 250}] mutableCopy];
}
_samples = [ORKLastNSamples(_samples, 500) mutableCopy];
[self updateGraphSamples];
}
@@ -243,7 +345,8 @@
}
}
- (void)addRecognitionError:(NSString *)errorMsg {
- (void)addRecognitionError:(NSString * _Nullable)errorMsg
{
_transcriptLabel.textColor = [UIColor ork_redColor];
_transcriptLabel.text = errorMsg;
}
@@ -254,3 +357,4 @@
}
@end
@@ -28,9 +28,11 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <Speech/SFTranscription.h>
#import <Speech/SFTranscriptionSegment.h>
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
#import <Speech/SFSpeechRecognitionMetadata.h>
#endif
#import <ResearchKit/ORKResult.h>
@@ -51,8 +53,10 @@ ORK_CLASS_AVAILABLE
*/
@property (nonatomic, copy, nullable) SFTranscription *transcription;
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
@property (nonatomic, copy, nullable) SFSpeechRecognitionMetadata *recognitionMetadata API_AVAILABLE(ios(14.5));
#endif
@end
NS_ASSUME_NONNULL_END
@@ -38,12 +38,22 @@
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_OBJ(aCoder, transcription);
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
ORK_ENCODE_OBJ(aCoder, recognitionMetadata);
}
#endif
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, transcription, SFTranscription);
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
ORK_DECODE_OBJ_CLASS(aDecoder, recognitionMetadata, SFSpeechRecognitionMetadata);
}
#endif
}
return self;
}
@@ -53,16 +63,24 @@
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
ORKEqualObjects(self.transcription, castObject.transcription));
BOOL isParentSame = [super isEqual:object] && ORKEqualObjects(self.transcription, castObject.transcription);
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
return isParentSame && ORKEqualObjects(self.recognitionMetadata, castObject.recognitionMetadata);
}
#endif
return isParentSame;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKSpeechRecognitionResult *result = [super copyWithZone:zone];
result.transcription = [self.transcription copy];
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
result.recognitionMetadata = [self.recognitionMetadata copy];
}
#endif
return result;
}
@@ -68,7 +68,7 @@
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ(aDecoder, speechRecognizerLocale);
ORK_DECODE_OBJ_CLASS(aDecoder, speechRecognizerLocale, NSString);
}
return self;
}
@@ -39,38 +39,43 @@
#import "ORKTask.h"
#import "ORKActiveStepView.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKBodyItem_Internal.h"
#import "ORKStepContainerView_Private.h"
#import "ORKSpeechRecognitionContentView.h"
#import "ORKStreamingAudioRecorder.h"
#import "ORKAudioStreamer.h"
#import "ORKSpeechRecognizer.h"
#import "ORKSpeechRecognitionStep.h"
#import "ORKSpeechRecognitionError.h"
#import "ORKHelpers_Internal.h"
#import "ORKBorderedButton.h"
#import "ORKRecordButton.h"
#import "ORKSpeechRecognitionResult.h"
#import "ORKResult_Private.h"
#import "ORKCollectionResult_Private.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
#import "ORKTaskViewController.h"
#import "ORKOrderedTask.h"
@interface ORKSpeechRecognitionStepViewController () <ORKStreamingAudioResultDelegate, ORKSpeechRecognitionDelegate, UITextFieldDelegate>
@interface ORKSpeechRecognitionStepViewController () <ORKStreamingAudioResultDelegate, ORKSpeechRecognitionDelegate, UITextFieldDelegate, ORKSpeechRecognitionContentViewDelegate>
@end
@implementation ORKSpeechRecognitionStepViewController {
ORKSpeechRecognitionContentView *_speechRecognitionContentView;
ORKStreamingAudioRecorder *_audioRecorder;
ORKAudioStreamer *_audioRecorder;
ORKSpeechRecognizer *_speechRecognizer;
dispatch_queue_t _speechRecognitionQueue;
ORKSpeechRecognitionResult *_localResult;
BOOL _errorState;
float _peakPower;
BOOL _allowUserToRecordInsteadOnNextStep;
}
- (instancetype)initWithStep:(ORKStep *)step {
@@ -83,26 +88,59 @@
- (void)viewDidLoad {
[super viewDidLoad];
[self setAllowUserToRecordInsteadOnNextStep:NO];
ORKSpeechRecognitionStep *step = (ORKSpeechRecognitionStep *) self.step;
_speechRecognitionContentView = [ORKSpeechRecognitionContentView new];
_speechRecognitionContentView.shouldHideTranscript = step.shouldHideTranscript;
self.activeStepView.customContentFillsAvailableSpace = YES;
self.activeStepView.activeCustomView = _speechRecognitionContentView;
_speechRecognitionContentView.speechRecognitionImage = step.speechRecognitionImage;
_speechRecognitionContentView.speechRecognitionText = step.speechRecognitionText;
[_speechRecognitionContentView.recordButton addTarget:self
action:@selector(recordButtonPressed:)
forControlEvents:UIControlEventTouchDown];
_speechRecognitionContentView.delegate = self;
_errorState = NO;
[ORKSpeechRecognizer requestAuthorization];
[self requestSpeechRecognizerAuthorizationIfNeeded];
_localResult = [[ORKSpeechRecognitionResult alloc] initWithIdentifier:self.step.identifier];
_speechRecognitionQueue = dispatch_queue_create("SpeechRecognitionQueue", DISPATCH_QUEUE_SERIAL);
}
- (void)requestSpeechRecognizerAuthorizationIfNeeded
{
[self handleSpeechRecognizerAuthorizationStatus:[ORKSpeechRecognizer authorizationStatus]];
}
- (void)handleSpeechRecognizerAuthorizationStatus:(SFSpeechRecognizerAuthorizationStatus)status
{
switch (status)
{
case SFSpeechRecognizerAuthorizationStatusAuthorized:
{
[_speechRecognitionContentView.recordButton setButtonState:ORKRecordButtonStateEnabled];
break;
}
case SFSpeechRecognizerAuthorizationStatusRestricted:
case SFSpeechRecognizerAuthorizationStatusDenied:
{
[_speechRecognitionContentView.recordButton setButtonState:ORKRecordButtonStateDisabled];
break;
}
case SFSpeechRecognizerAuthorizationStatusNotDetermined:
{
[ORKSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus authorizationStatus)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self handleSpeechRecognizerAuthorizationStatus:authorizationStatus == SFSpeechRecognizerAuthorizationStatusAuthorized ?
SFSpeechRecognizerAuthorizationStatusAuthorized:
SFSpeechRecognizerAuthorizationStatusDenied];
});
}];
break;
}
}
}
- (void)initializeRecognizer {
_speechRecognizer = [[ORKSpeechRecognizer alloc] init];
@@ -115,36 +153,83 @@
}
}
- (void)recordButtonPressed:(id)sender {
if (sender == _speechRecognitionContentView.recordButton) {
if ([_speechRecognitionContentView.recordButton.titleLabel.text
isEqualToString:ORKLocalizedString(@"SPEECH_RECOGNITION_STOP_RECORD_LABEL", nil)]) {
[self stopWithError:nil];
} else {
- (void)start
{
[super start];
// Remove any errors on the content view.
[_speechRecognitionContentView addRecognitionError:nil];
}
- (void)didPressRecordButton:(ORKRecordButton *)recordButton
{
switch ([recordButton buttonType])
{
case ORKRecordButtonTypeRecord:
[self initializeRecognizer];
[self start];
[_speechRecognitionContentView.recordButton setTitle:ORKLocalizedString(@"SPEECH_RECOGNITION_STOP_RECORD_LABEL", nil)
forState:UIControlStateNormal];
_speechRecognitionContentView.recordButton.enabled = YES;
}
break;
default:
[self stopWithError:nil];
break;
}
}
- (void)didPressUseKeyboardButton
{
[self setAllowUserToRecordInsteadOnNextStep:YES];
[self goForward];
}
- (void)setAllowUserToRecordInsteadOnNextStep:(BOOL)allowUserToRecordInsteadOnNextStep
{
_allowUserToRecordInsteadOnNextStep = (allowUserToRecordInsteadOnNextStep && [SFSpeechRecognizer authorizationStatus] != SFSpeechRecognizerAuthorizationStatusDenied);
}
- (CAShapeLayer *)recordingShapeLayer
{
CAShapeLayer *layer = [CAShapeLayer layer];
UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 30, 30)];
layer.path = circlePath.CGPath;
layer.strokeColor = UIColor.systemRedColor.CGColor;
return layer;
}
- (UIImage *)imageFromLayer:(CALayer *)layer
{
UIGraphicsBeginImageContextWithOptions(layer.frame.size, NO, 0);
[layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
- (UIFont *)buttonTextFont
{
CGFloat fontSize = [[UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleCallout] pointSize];
return [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold];
}
- (void)recordersDidChange {
ORKStreamingAudioRecorder *audioRecorder = nil;
ORKAudioStreamer *audioRecorder = nil;
for (ORKRecorder *recorder in self.recorders) {
if ([recorder isKindOfClass:[ORKStreamingAudioRecorder class]]) {
audioRecorder = (ORKStreamingAudioRecorder *)recorder;
if ([recorder isKindOfClass:[ORKAudioStreamer class]]) {
audioRecorder = (ORKAudioStreamer *)recorder;
break;
}
}
_audioRecorder = audioRecorder;
}
- (ORKStepResult *)result {
- (ORKStepResult *)result
{
ORKStepResult *sResult = [super result];
if (_speechRecognitionQueue) {
dispatch_sync(_speechRecognitionQueue, ^{
if (_localResult != nil) {
@@ -157,38 +242,130 @@
return sResult;
}
- (void)stopWithError:(NSError *)error {
if (_speechRecognizer) {
- (void)stopWithError:(NSError *)error
{
[_speechRecognitionContentView.recordButton setButtonType:ORKRecordButtonTypeRecord animated:YES];
[_speechRecognitionContentView updateButtonStates];
if (_speechRecognizer)
{
[_speechRecognizer endAudio];
}
if (error) {
if (error)
{
ORK_Log_Error("Speech recognition failed with error message: \"%@\"", error.localizedDescription);
if (error.code == ORKSpeechRecognitionErrorRecognitionFailed)
{
// Speech Recognition Failed, let the user try again.
[_speechRecognitionContentView addRecognitionError:ORKLocalizedString(@"SPEECH_RECOGNITION_FAILED_TRY_AGAIN", nil)];
return;
}
// Speech Recogntion Failed (Fatal)
// In this case, the user can't try again and they will need to cancel out of the task.
// Disable the Record button.
[_speechRecognitionContentView addRecognitionError:error.localizedDescription];
_speechRecognitionContentView.recordButton.enabled = NO;
_speechRecognitionContentView.recordButton.userInteractionEnabled = NO;
[_speechRecognitionContentView.recordButton setButtonState:ORKRecordButtonStateDisabled];
_errorState = YES;
}
[self stopRecorders];
}
- (void)suspend
{
[super suspend];
[_speechRecognitionContentView removeAllSamples];
[_speechRecognitionContentView.recordButton setButtonType:ORKRecordButtonTypeRecord animated:YES];
[_speechRecognitionContentView updateButtonStates];
}
- (void)resume {
// Background processing is not supported
}
- (void)goForward {
if ([self hasNextStep]) {
ORKQuestionStep *nextStep = [self nextStep];
if (nextStep) {
[((ORKTextAnswerFormat *)nextStep.answerFormat) setDefaultTextAnswer: [_localResult.transcription formattedString]];
}
}
- (void)goForward
{
[self setupNextStepForAllowingUserToRecordInstead:_allowUserToRecordInsteadOnNextStep];
[super goForward];
}
- (void)setupNextStepForAllowingUserToRecordInstead:(BOOL)allowUserToRecordInsteadOnNextStep
{
if ([[self nextStep] isKindOfClass:[ORKQuestionStep class]] && [[[self nextStep] answerFormat] isKindOfClass:[ORKTextAnswerFormat class]]) {
NSString *substitutedTextAnswer = [self substitutedStringWithString:[_localResult.transcription formattedString]];
[((ORKTextAnswerFormat *)self.nextStep.answerFormat) setDefaultTextAnswer:substitutedTextAnswer];
}
}
- (nullable NSString *)substitutedStringWithString:(nullable NSString *)string
{
if (!string)
{
return nil;
}
// Known substitutions
static NSDictionary *substitutions = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
substitutions = @{ @"for" : @"four",
@"to" : @"two",
@"too" : @"two",
@"10" : @"ten",
@"11" : @"eleven",
@"12" : @"twelve",
@"13" : @"thirteen",
@"14" : @"fourteen",
@"15" : @"fifteen",
@"16" : @"sixteen",
@"17" : @"seventeen",
@"read" : @"red"
};
});
NSArray *words = [string componentsSeparatedByString:@" "];
NSMutableArray *substitutedWords = [NSMutableArray new];
for (NSString *word in words)
{
[substitutedWords addObject:[substitutions objectForKey:word] ? : word];
}
NSString *substitutedString = [substitutedWords componentsJoinedByString:@" "];
// Test For Non-Whitespace/Newline Characters
NSCharacterSet *inverted = [[NSCharacterSet whitespaceAndNewlineCharacterSet] invertedSet];
NSRange range = [substitutedString rangeOfCharacterFromSet:inverted];
BOOL empty = (range.location == NSNotFound);
return empty ? nil : substitutedString;
}
- (nullable ORKQuestionStep *)nextStep {
ORKOrderedTask *task = (ORKOrderedTask *)[self.taskViewController task];
NSUInteger nextStepIndex = [task indexOfStep:[self step]] + 1;
ORKStep *nextStep = [task steps][nextStepIndex];
ORKStep *nextStep = nil;
if ([[task steps] count] > nextStepIndex) {
nextStep = [[task steps] objectAtIndex:nextStepIndex];
}
if ([nextStep isKindOfClass:[ORKQuestionStep class]]) {
return (ORKQuestionStep *)nextStep;
@@ -252,6 +429,24 @@
});
}
- (void)didFinishRecognition:(SFSpeechRecognitionResult *)recognitionResult {
if (_errorState) {
return;
}
dispatch_sync(_speechRecognitionQueue, ^{
_localResult.transcription = recognitionResult.bestTranscription;
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
_localResult.recognitionMetadata = recognitionResult.speechRecognitionMetadata;
}
#endif
});
dispatch_async(dispatch_get_main_queue(), ^{
[_speechRecognitionContentView updateRecognitionText:[recognitionResult.bestTranscription formattedString]];
});
}
- (void)didHypothesizeTranscription:(SFTranscription *)transcription {
if (_errorState) {
return;
@@ -259,7 +454,7 @@
dispatch_sync(_speechRecognitionQueue, ^{
_localResult.transcription = transcription;
});
dispatch_async(dispatch_get_main_queue(), ^{
[_speechRecognitionContentView updateRecognitionText:[transcription formattedString]];
});

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