Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8473017ec | |||
| 2bc0033aa8 | |||
| a89059f5dc | |||
| 2cc8f9e7d5 | |||
| 29950b62e4 | |||
| 65de4b333c | |||
| 63fcf4918f | |||
| c9880b0139 | |||
| 13eabb7720 | |||
| 743b773ea3 | |||
| 249eee5dfb | |||
| 90c68d0d19 | |||
| d10a427911 | |||
| 05755a3213 | |||
| e18a633de1 | |||
| 0e68cdf744 | |||
| 7f119a8d0d | |||
| fde1e7e957 | |||
| 0ad96d505c | |||
| d4ff76fc25 | |||
| 85c1395361 | |||
| 1443e57c57 | |||
| 3b75f6213c | |||
| e9d5de64a5 | |||
| 19c61383f6 | |||
| cb6cc97fa4 | |||
| 0651bf0c2a | |||
| ae9b9e57cb |
@@ -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,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,7 +82,7 @@ The latest stable version of *ResearchKit framework* can be cloned with
|
||||
git clone -b stable https://github.com/ResearchKit/ResearchKit.git
|
||||
```
|
||||
|
||||
Or, for the latest changes, use the `master` branch:
|
||||
Or, for the latest changes, use the `main` branch:
|
||||
|
||||
```
|
||||
git clone https://github.com/ResearchKit/ResearchKit.git
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:ResearchKit.xcodeproj">
|
||||
</FileRef>
|
||||
@@ -13,7 +10,4 @@
|
||||
<FileRef
|
||||
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:samples/ORKSample/ORKSample.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
+1
-1
@@ -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 = "1250"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "NO">
|
||||
@@ -67,6 +67,8 @@
|
||||
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 = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1200"
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -167,6 +167,7 @@ internal class EyeActivitySlider: UIView {
|
||||
letterImageView.isHidden = true
|
||||
}
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
internal func fetchResultDataAndUpdateSlider() -> (outcome: Bool, letterAngle: Double, sliderAngle: Double, score: Int, incorrectAnswers: Int, maxScore: Int) {
|
||||
let outcome = getResult()
|
||||
let score = stepScores[currentStep]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#import "ORK3DModelStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORK3DModelStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
@@ -73,7 +74,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 +91,8 @@
|
||||
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
|
||||
+26
-17
@@ -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(ORKAccuracyStroopStep.colors.count)];
|
||||
self.isColorMatching = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_BOOL(aDecoder, isColorMatching);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, baseDisplayColor, UIColor);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_BOOL(aCoder, isColorMatching);
|
||||
ORK_ENCODE_OBJ(aCoder, baseDisplayColor);
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAccuracyStroopStep *step = [super copyWithZone:zone];
|
||||
step.isColorMatching = self.isColorMatching;
|
||||
step.baseDisplayColor = [self.baseDisplayColor copy];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
|
||||
return isParentSame
|
||||
&& self.isColorMatching == castObject.isColorMatching
|
||||
&& ORKEqualObjects(self.baseDisplayColor, castObject.baseDisplayColor);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return super.hash
|
||||
^ (self.isColorMatching ? 0xf : 0x0)
|
||||
^ (self.baseDisplayColor ? 0xf : 0x0);
|
||||
}
|
||||
|
||||
+ (NSArray<UIColor *> *)colors {
|
||||
return @[ UIColor.systemRedColor,
|
||||
UIColor.systemGreenColor,
|
||||
UIColor.systemBlueColor,
|
||||
UIColor.systemYellowColor,
|
||||
UIColor.systemOrangeColor ];
|
||||
}
|
||||
|
||||
- (UIColor *)actualDisplayColor {
|
||||
return self.isColorMatching ?
|
||||
self.baseDisplayColor :
|
||||
ORKAccuracyStroopStep.colors[arc4random_uniform(ORKAccuracyStroopStep.colors.count)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKStepViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKAccuracyStroopStepViewController : ORKStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAccuracyStroopStepViewController.h"
|
||||
#import "ORKAccuracyStroopStep.h"
|
||||
#import "ORKAccuracyStroopResult.h"
|
||||
|
||||
#import "ORKCollectionResult.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "UIColor+String.h"
|
||||
|
||||
@interface ORKAccuracyStroopStepViewController () <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic) NSMutableArray <UIView *> *circles;
|
||||
@property (nonatomic, strong) UILabel *colorLabel;
|
||||
@property (nonatomic) UIView *circlesView;
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *constraints;
|
||||
@property (nonatomic) double distanceToClosestCenter;
|
||||
@property (nonatomic) UIColor *selectedColor;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKAccuracyStroopStepViewController
|
||||
|
||||
- (ORKAccuracyStroopStep *)accuracyStroopStep {
|
||||
return (ORKAccuracyStroopStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
|
||||
if (self.step && [self isViewLoaded]) {
|
||||
[self setupColorLabel];
|
||||
[self setupCirclesView];
|
||||
[self setupConstraints];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self setupCircles];
|
||||
[self setupViewTap];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupViewTap {
|
||||
for (UIGestureRecognizer *recognizer in self.circlesView.gestureRecognizers) {
|
||||
[self.circlesView removeGestureRecognizer:recognizer];
|
||||
}
|
||||
|
||||
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||
tapGestureRecognizer.delegate = self;
|
||||
[self.circlesView addGestureRecognizer:tapGestureRecognizer];
|
||||
}
|
||||
|
||||
- (void)setupCirclesView {
|
||||
[self.circlesView removeFromSuperview];
|
||||
self.circlesView = nil;
|
||||
|
||||
self.circlesView = UIView.new;
|
||||
[self.view addSubview:self.circlesView];
|
||||
}
|
||||
|
||||
- (void)setupColorLabel {
|
||||
[self.colorLabel removeFromSuperview];
|
||||
self.colorLabel = nil;
|
||||
|
||||
self.colorLabel = UILabel.new;
|
||||
self.colorLabel.text = self.accuracyStroopStep.actualDisplayColor.textRepresentation;
|
||||
self.colorLabel.textColor = self.accuracyStroopStep.baseDisplayColor;
|
||||
self.colorLabel.font = [UIFont systemFontOfSize:35.0 weight:UIFontWeightMedium];
|
||||
[self.view addSubview:self.colorLabel];
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
if (self.constraints) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
}
|
||||
self.colorLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.circlesView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.constraints = nil;
|
||||
self.constraints = @[
|
||||
[NSLayoutConstraint constraintWithItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:10.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeLeading
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeLeading
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeTrailing
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeTrailing
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0]
|
||||
];
|
||||
[NSLayoutConstraint activateConstraints:self.constraints];
|
||||
}
|
||||
|
||||
- (void)setupCircles {
|
||||
for (UIView *circle in self.circles) {
|
||||
[circle removeFromSuperview];
|
||||
}
|
||||
|
||||
[self.circles removeAllObjects];
|
||||
self.circles = NSMutableArray.array;
|
||||
|
||||
// Constants to use for ball and grid
|
||||
int ballSize = 50;
|
||||
int padding = 10;
|
||||
int cellSize = ballSize + padding * 2;
|
||||
|
||||
// Calculating number of rows/columns in grid to layout color circles
|
||||
uint32_t numRows = (self.circlesView.bounds.size.height) / cellSize;
|
||||
uint32_t numColumns = (self.circlesView.bounds.size.width) / cellSize;
|
||||
|
||||
// Extra padding to ensure that the grid spans the whole screen width
|
||||
int extraHorizontalSpaceForCell = ((int)self.circlesView.bounds.size.width % cellSize) / numColumns;
|
||||
|
||||
// Matrix to keep track of cells that already have a circle --> avoid overlap in O(n)
|
||||
bool cellTakenMatrix[numRows][numColumns];
|
||||
for (uint32_t r = 0; r < numRows; r++) {
|
||||
for (uint32_t c = 0; c < numColumns; c++) {
|
||||
cellTakenMatrix[r][c] = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int colorIndex = 0; colorIndex < ORKAccuracyStroopStep.colors.count; colorIndex++) {
|
||||
// Obtain random location for color circle within bounds
|
||||
int randomR = (int)arc4random_uniform(numRows);
|
||||
int randomC = (int)arc4random_uniform(numColumns);
|
||||
|
||||
ORK_Log_Debug("Trying placement for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
|
||||
|
||||
// If cell is already taken, look at 8 spots around for a free spot
|
||||
if (cellTakenMatrix[randomR][randomC]) {
|
||||
ORK_Log_Debug("Position (r, c): (%d, %d) already taken", randomR, randomC);
|
||||
|
||||
// Loops through the 3x3 grid with randomR,randomC as the center
|
||||
bool shouldBreak = false;
|
||||
for (int r = randomR - 1; !shouldBreak && r <= randomR + 1; r++) {
|
||||
for (int c = randomC - 1; !shouldBreak && c <= randomC + 1; c++) {
|
||||
// If r/c are out of circleView's bounds, then don't consider
|
||||
if ((r < 0 || r >= numRows) || (c < 0 || c >= numColumns)) { continue; }
|
||||
|
||||
// If cell is not taken, then can assign to there and break out of for-loops
|
||||
if (!cellTakenMatrix[r][c]) {
|
||||
randomR = r;
|
||||
randomC = c;
|
||||
shouldBreak = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ORK_Log_Info("Final position for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
|
||||
|
||||
cellTakenMatrix[randomR][randomC] = true;
|
||||
|
||||
CGFloat circleX = (randomC * (cellSize + extraHorizontalSpaceForCell)) + padding + extraHorizontalSpaceForCell / 2;
|
||||
CGFloat circleY = (randomR * cellSize) + padding;
|
||||
CGRect frame = CGRectMake(circleX, circleY, ballSize, ballSize);
|
||||
UIView *newCircle = [[UIView alloc] initWithFrame:frame];
|
||||
newCircle.backgroundColor = ORKAccuracyStroopStep.colors[colorIndex];
|
||||
newCircle.clipsToBounds = YES;
|
||||
newCircle.layer.cornerRadius = ballSize / 2;
|
||||
newCircle.tag = colorIndex;
|
||||
[self.circles addObject:newCircle];
|
||||
[self.circlesView addSubview:newCircle];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
|
||||
CGPoint touchPoint = [recognizer locationInView:self.circlesView];
|
||||
double minDistance = INFINITY;
|
||||
|
||||
for (UIView *circle in self.circles) {
|
||||
double dx = (touchPoint.x - circle.center.x);
|
||||
double dy = (touchPoint.y - circle.center.y);
|
||||
double distance = sqrt(dx * dx + dy * dy);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
}
|
||||
|
||||
if (CGRectContainsPoint(circle.frame, touchPoint)) {
|
||||
self.selectedColor = ORKAccuracyStroopStep.colors[circle.tag];
|
||||
self.distanceToClosestCenter = distance;
|
||||
[super goForward];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.distanceToClosestCenter = minDistance;
|
||||
[super goForward];
|
||||
}
|
||||
|
||||
- (BOOL)hasPreviousStep {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *stepResult = [super result];
|
||||
|
||||
ORKAccuracyStroopResult *result = [[ORKAccuracyStroopResult alloc] initWithIdentifier:self.accuracyStroopStep.identifier];
|
||||
result.color = self.accuracyStroopStep.baseDisplayColor.textRepresentation;
|
||||
result.colorSelected = self.selectedColor.textRepresentation;
|
||||
result.distanceToClosestCenter = self.distanceToClosestCenter;
|
||||
result.startDate = stepResult.startDate;
|
||||
result.endDate = stepResult.endDate;
|
||||
result.timeTakenToSelect = [result.endDate timeIntervalSinceDate:result.startDate];
|
||||
|
||||
NSMutableArray *results = [[NSMutableArray alloc] init];
|
||||
if (stepResult.results) {
|
||||
results = [stepResult.results mutableCopy];
|
||||
}
|
||||
|
||||
[results addObject:result];
|
||||
|
||||
stepResult.results = [results copy];
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self stepDidChange];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -206,6 +206,15 @@ The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSArray<ORKRecorderConfiguration *> *recorderConfigurations;
|
||||
|
||||
/**
|
||||
A Boolean value that determines if a step is a practice step or not.
|
||||
|
||||
When the value of this property is `YES`, the ResearchKit framework sets the allowsBackNavigation property to 'YES'
|
||||
|
||||
The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL isPractice;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, spokenInstruction, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, finishedSpokenInstruction, NSString);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, recorderConfigurations, ORKRecorderConfiguration);
|
||||
ORK_DECODE_BOOL(aDecoder, isPractice);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -142,6 +143,7 @@
|
||||
ORK_ENCODE_OBJ(aCoder, spokenInstruction);
|
||||
ORK_ENCODE_OBJ(aCoder, finishedSpokenInstruction);
|
||||
ORK_ENCODE_OBJ(aCoder, recorderConfigurations);
|
||||
ORK_ENCODE_BOOL(aCoder, isPractice);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
@@ -162,7 +164,8 @@
|
||||
(self.shouldVibrateOnStart == castObject.shouldVibrateOnStart) &&
|
||||
(self.shouldVibrateOnFinish == castObject.shouldVibrateOnFinish) &&
|
||||
(self.shouldContinueOnFinish == castObject.shouldContinueOnFinish) &&
|
||||
(self.shouldUseNextAsSkipButton == castObject.shouldUseNextAsSkipButton));
|
||||
(self.shouldUseNextAsSkipButton == castObject.shouldUseNextAsSkipButton) &&
|
||||
(self.isPractice == castObject.isPractice));
|
||||
}
|
||||
|
||||
- (NSSet<HKObjectType *> *)requestedHealthKitTypesForReading {
|
||||
@@ -184,4 +187,8 @@
|
||||
return mask;
|
||||
}
|
||||
|
||||
- (BOOL)allowsBackNavigation {
|
||||
return self.isPractice;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,30 +30,26 @@
|
||||
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@import UIKit;
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
|
||||
static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
static mach_timebase_info_data_t sTimebaseInfo;
|
||||
if ( sTimebaseInfo.denom == 0 ) {
|
||||
(void) mach_timebase_info(&sTimebaseInfo);
|
||||
static mach_timebase_info_data_t sTimebaseInfo;
|
||||
if (sTimebaseInfo.denom == 0) {
|
||||
(void)mach_timebase_info(&sTimebaseInfo);
|
||||
}
|
||||
uint64_t elapsedNano = delta * sTimebaseInfo.numer / sTimebaseInfo.denom;
|
||||
return elapsedNano * 1.0 / NSEC_PER_SEC;
|
||||
}
|
||||
|
||||
|
||||
@implementation ORKActiveStepTimer {
|
||||
uint64_t _startTime;
|
||||
NSTimeInterval _preExistingRuntime;
|
||||
dispatch_queue_t _queue;
|
||||
dispatch_source_t _timer;
|
||||
UIBackgroundTaskIdentifier _backgroundTaskIdentifier;
|
||||
uint32_t _isRunning;
|
||||
}
|
||||
|
||||
@@ -68,7 +64,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
_interval = interval;
|
||||
_handler = [handler copy];
|
||||
_preExistingRuntime = runtime;
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
|
||||
_queue = dispatch_queue_create("active_step", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
@@ -128,7 +123,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
}
|
||||
|
||||
- (void)queue_event {
|
||||
[self queue_assertBackgroundTask];
|
||||
|
||||
NSTimeInterval runtime = [self queue_runtime];
|
||||
BOOL finished = (runtime >= _duration);
|
||||
@@ -138,13 +132,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
_handler(self, finished);
|
||||
dispatch_sync(_queue, ^{
|
||||
|
||||
// If the timer is still NULL here, we can safely release the background task.
|
||||
if (_timer == NULL) {
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,29 +143,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)queue_releaseBackgroundTask {
|
||||
if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
|
||||
return;
|
||||
}
|
||||
UIBackgroundTaskIdentifier identifier = _backgroundTaskIdentifier;
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[[UIApplication sharedApplication] endBackgroundTask:identifier];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)queue_assertBackgroundTask {
|
||||
if (_backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
|
||||
return;
|
||||
}
|
||||
_backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
||||
// This is guaranteed to be called synchronously on the main queue, switch to our queue to invalidate the identifier
|
||||
dispatch_sync(_queue, ^{
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)queue_resume {
|
||||
if (_timer != NULL) {
|
||||
// Already resumed
|
||||
@@ -190,11 +154,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We want to run in the background if we can, so voice can be played, etc.
|
||||
assert(_backgroundTaskIdentifier == UIBackgroundTaskInvalid);
|
||||
|
||||
[self queue_assertBackgroundTask];
|
||||
|
||||
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
|
||||
0, 0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
if (_timer == NULL) {
|
||||
@@ -228,16 +187,11 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
_preExistingRuntime += timeIntervalFromMachTime(now - _startTime);
|
||||
_startTime = 0;
|
||||
|
||||
if (!atFinish) {
|
||||
// If we are atFinish, the task will be released after the handler completes
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)queue_reset {
|
||||
[self queue_clearTimer];
|
||||
_preExistingRuntime = 0;
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKRecordButton;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, ORKAudioContentViewEvent) {
|
||||
ORKAudioContentViewEventStartRecording = 0,
|
||||
ORKAudioContentViewEventStopRecording
|
||||
};
|
||||
|
||||
typedef void (^ORKAudioStepContentViewEventHandler)(ORKAudioContentViewEvent);
|
||||
|
||||
@interface ORKAudioContentView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic, copy, nullable) UIColor *keyColor;
|
||||
@@ -48,6 +57,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, copy, nullable) NSArray *samples;
|
||||
|
||||
@property (nonatomic) BOOL useRecordButton;
|
||||
|
||||
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler;
|
||||
|
||||
// Samples should be in the range of (0, 1).
|
||||
- (void)addSample:(NSNumber *)sample;
|
||||
- (void)removeAllSamples;
|
||||
@@ -55,3 +68,4 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
#import "ORKAudioContentView.h"
|
||||
#import "ORKAudioGraphView.h"
|
||||
#import "ORKAudioMeteringView.h"
|
||||
|
||||
#import "ORKHeadlineLabel.h"
|
||||
#import "ORKLabel.h"
|
||||
@@ -38,6 +39,7 @@
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKRecordButton.h"
|
||||
|
||||
|
||||
// The central blue region.
|
||||
@@ -46,6 +48,8 @@ static const CGFloat GraphViewBlueZoneHeight = 170;
|
||||
// The two bands at top and bottom which are "loud" each have this height.
|
||||
static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
|
||||
static const CGFloat ORKAudioStepContentRecordButtonVerticalSpacing = 20.0;
|
||||
|
||||
@interface ORKAudioTimerLabel : ORKLabel
|
||||
|
||||
@end
|
||||
@@ -61,11 +65,12 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKAudioContentView ()
|
||||
@interface ORKAudioContentView () <ORKRecordButtonDelegate>
|
||||
|
||||
@property (nonatomic, strong) ORKHeadlineLabel *alertLabel;
|
||||
@property (nonatomic, strong) UILabel *timerLabel;
|
||||
@property (nonatomic, strong) ORKAudioGraphView *graphView;
|
||||
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
|
||||
@property (nonatomic, copy, nullable) ORKAudioStepContentViewEventHandler viewEventhandler;
|
||||
|
||||
@end
|
||||
|
||||
@@ -73,19 +78,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
@implementation ORKAudioContentView {
|
||||
NSMutableArray *_samples;
|
||||
UIColor *_keyColor;
|
||||
ORKRecordButton *_recordButton;
|
||||
BOOL _checkAudioLevel;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
|
||||
_checkAudioLevel = YES;
|
||||
_useRecordButton = NO;
|
||||
|
||||
self.alertLabel = [ORKHeadlineLabel new];
|
||||
_alertLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.timerLabel = [ORKAudioTimerLabel new];
|
||||
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_timerLabel.textAlignment = NSTextAlignmentRight;
|
||||
self.graphView = [ORKAudioGraphView new];
|
||||
self.graphView = [[ORKAudioMeteringView alloc] init];
|
||||
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
@@ -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
|
||||
+11
-14
@@ -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
|
||||
@@ -28,20 +28,15 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKAudioMeteringView.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKAudioGraphView : UIView
|
||||
|
||||
@property (nonatomic, strong) UIColor *keyColor;
|
||||
@property (nonatomic, strong) UIColor *alertColor;
|
||||
|
||||
@property (nonatomic, copy) NSArray *values;
|
||||
|
||||
@property (nonatomic) CGFloat alertThreshold;
|
||||
|
||||
@interface ORKAudioGraphView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -32,11 +32,21 @@
|
||||
#import "ORKAudioGraphView.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
static const CGFloat ValueLineWidth = 4.5;
|
||||
static const CGFloat ValueLineMargin = 1.5;
|
||||
static const CGFloat GraphHeight = 150.0;
|
||||
|
||||
@interface ORKAudioGraphView ()
|
||||
|
||||
/// ORKAudioMetering
|
||||
@property (nonatomic, copy, nullable) NSArray<NSNumber *> *samples;
|
||||
@property (nonatomic, assign) float alertThreshold;
|
||||
|
||||
/// ORKAudioMeteringView
|
||||
@property (nonatomic, strong) UIColor *meterColor;
|
||||
@property (nonatomic, strong, nullable) UIColor *alertColor;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKAudioGraphView
|
||||
|
||||
@@ -46,7 +56,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[self setUpConstraints];
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
_values = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
|
||||
_samples = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
@@ -65,26 +75,6 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[NSLayoutConstraint activateConstraints:@[heightConstraint]];
|
||||
}
|
||||
|
||||
- (void)setValues:(NSArray *)values {
|
||||
_values = [values copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setKeyColor:(UIColor *)keyColor {
|
||||
_keyColor = [keyColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor {
|
||||
_alertColor = [alertColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(CGFloat)alertThreshold {
|
||||
_alertThreshold = alertThreshold;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
CGRect bounds = self.bounds;
|
||||
|
||||
@@ -104,7 +94,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[centerLine addLineToPoint:(CGPoint){.x = maxX, .y = midY}];
|
||||
|
||||
CGContextSetLineWidth(context, 1.0 / scale);
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
CGFloat lengths[2] = {3, 3};
|
||||
CGContextSetLineDash(context, 0, lengths, 2);
|
||||
|
||||
@@ -125,7 +115,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
path1.lineWidth = ValueLineWidth;
|
||||
UIBezierPath *path2 = [path1 copy];
|
||||
|
||||
for (NSNumber *value in [_values reverseObjectEnumerator]) {
|
||||
for (NSNumber *value in [_samples reverseObjectEnumerator]) {
|
||||
CGFloat floatValue = value.doubleValue;
|
||||
|
||||
UIBezierPath *path = nil;
|
||||
@@ -134,7 +124,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[_alertColor setStroke];
|
||||
} else {
|
||||
path = path2;
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
}
|
||||
[path moveToPoint:(CGPoint){.x = x, .y = midY - floatValue*halfHeight}];
|
||||
[path addLineToPoint:(CGPoint){.x = x, .y = midY + floatValue*halfHeight}];
|
||||
@@ -150,11 +140,41 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[_alertColor setStroke];
|
||||
[path1 stroke];
|
||||
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
[path2 stroke];
|
||||
|
||||
}
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMetering
|
||||
|
||||
- (void)setSamples:(NSArray<NSNumber *> *)samples
|
||||
{
|
||||
_samples = [samples copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(float)threshold
|
||||
{
|
||||
_alertThreshold = threshold;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
#pragma mark = ORKAudioMeteringView
|
||||
|
||||
- (void)setMeterColor:(UIColor *)meterColor
|
||||
{
|
||||
_meterColor = [meterColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor
|
||||
{
|
||||
_alertColor = [alertColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@@ -75,3 +75,4 @@ ORK_CLASS_AVAILABLE
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -216,3 +216,4 @@ Float32 const VolumeClamp = 60.0;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
+14
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -29,26 +29,30 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@import UIKit;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKConsentSection;
|
||||
extern NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit);
|
||||
|
||||
@interface ORKConsentSceneViewController : UIViewController
|
||||
@protocol ORKAudioMetering <NSObject>
|
||||
|
||||
- (instancetype)initWithSection:(ORKConsentSection *)section;
|
||||
- (void)setSamples:(nullable NSArray<NSNumber *> *)samples;
|
||||
|
||||
@property (nonatomic, readonly, nullable) ORKConsentSection *section;
|
||||
- (void)setAlertThreshold:(float)threshold;
|
||||
|
||||
@property (nonatomic, strong, nullable) UIBarButtonItem *continueButtonItem;
|
||||
@end
|
||||
|
||||
@property (nonatomic, strong, nullable) UIBarButtonItem *cancelButtonItem;
|
||||
@protocol ORKAudioMeteringDisplay
|
||||
|
||||
@property (nonatomic, strong, nullable) NSString *learnMoreButtonTitle;
|
||||
- (void)setMeterColor:(nonnull UIColor *)meterColor;
|
||||
|
||||
@property (nonatomic, assign) BOOL imageHidden;
|
||||
- (void)setAlertColor:(nonnull UIColor *)alertColor;
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKAudioMeteringView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAudioMeteringView.h"
|
||||
|
||||
|
||||
#import "ORKAudioGraphView.h"
|
||||
|
||||
NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit) {
|
||||
|
||||
if (samples.count > limit) {
|
||||
|
||||
return [samples subarrayWithRange:(NSRange){samples.count - limit, samples.count - 1}];
|
||||
}
|
||||
|
||||
return [samples copy];
|
||||
}
|
||||
|
||||
@interface ORKAudioMeteringView ()
|
||||
@property (nonatomic, strong) UIView<ORKAudioMetering, ORKAudioMeteringDisplay> *meteringView;
|
||||
@end
|
||||
|
||||
@implementation ORKAudioMeteringView
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)configureMeteringView
|
||||
{
|
||||
if (!_meteringView) {
|
||||
[self setMeteringView:[[ORKAudioGraphView alloc] init]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
[_meteringView setFrame:[self bounds]];
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden
|
||||
{
|
||||
[super setHidden:hidden];
|
||||
[_meteringView setHidden:hidden];
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
if ([self superview] == nil)
|
||||
{
|
||||
[_meteringView removeFromSuperview];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self addSubview:_meteringView];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMetering
|
||||
|
||||
- (void)setSamples:(NSArray<NSNumber *> *)samples
|
||||
{
|
||||
[_meteringView setSamples:samples];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(float)threshold
|
||||
{
|
||||
[_meteringView setAlertThreshold:threshold];
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMeteringDisplay
|
||||
|
||||
- (void)setMeterColor:(UIColor *)meterColor
|
||||
{
|
||||
[_meteringView setMeterColor:meterColor];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor
|
||||
{
|
||||
[_meteringView setAlertColor:alertColor];
|
||||
}
|
||||
|
||||
#pragma mark - UIAccessibility
|
||||
|
||||
- (BOOL)isAccessibilityElement {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -38,6 +38,18 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAudioStep : ORKActiveStep
|
||||
|
||||
/**
|
||||
A Boolean value that determines if audio recording will start and stop
|
||||
automatcially or be controlled via a ORKRecordButton
|
||||
|
||||
When set to YES the user will be able to start and stop the audio recording
|
||||
by the ORKRecordButton
|
||||
|
||||
The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic) BOOL useRecordButton;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -49,16 +49,27 @@
|
||||
if (self) {
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
self.shouldStartTimerAutomatically = YES;
|
||||
self.useRecordButton = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setUseRecordButton:(BOOL)useRecordButton {
|
||||
_useRecordButton = useRecordButton;
|
||||
|
||||
[self setShouldStartTimerAutomatically:!_useRecordButton];
|
||||
|
||||
if (_useRecordButton) {
|
||||
self.stepDuration = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
|
||||
NSTimeInterval const ORKAudioTaskMinimumDuration = 5.0;
|
||||
|
||||
if ( self.stepDuration < ORKAudioTaskMinimumDuration) {
|
||||
if ( self.stepDuration < ORKAudioTaskMinimumDuration && !self.useRecordButton) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKAudioTaskMinimumDuration)] userInfo:nil];
|
||||
}
|
||||
}
|
||||
@@ -67,4 +78,35 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAudioStep *step = [super copyWithZone:zone];
|
||||
step.useRecordButton = self.useRecordButton;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_BOOL(aDecoder, useRecordButton);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_BOOL(aCoder, useRecordButton);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame && self.useRecordButton == castObject.useRecordButton);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
@@ -44,3 +45,4 @@ ORK_CLASS_AVAILABLE
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
ORKAudioContentView *_audioContentView;
|
||||
ORKAudioRecorder *_audioRecorder;
|
||||
ORKActiveStepTimer *_timer;
|
||||
NSTimer *_intervalTimer;
|
||||
NSError *_audioRecorderError;
|
||||
}
|
||||
|
||||
@@ -83,6 +84,12 @@
|
||||
// Do any additional setup after loading the view.
|
||||
_audioContentView = [ORKAudioContentView new];
|
||||
_audioContentView.timeLeft = self.audioStep.stepDuration;
|
||||
_audioContentView.useRecordButton = self.audioStep.useRecordButton && self.audioStep.stepDuration == 0;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[_audioContentView setViewEventHandler:^(ORKAudioContentViewEvent event) {
|
||||
[weakSelf handleContentViewEvent:event];
|
||||
}];
|
||||
|
||||
if (self.alertThreshold > 0) {
|
||||
_audioContentView.alertThreshold = self.alertThreshold;
|
||||
@@ -91,6 +98,19 @@
|
||||
self.activeStepView.activeCustomView = _audioContentView;
|
||||
}
|
||||
|
||||
- (void)handleContentViewEvent:(ORKAudioContentViewEvent)event {
|
||||
|
||||
switch (event) {
|
||||
case ORKAudioContentViewEventStartRecording:
|
||||
[self start];
|
||||
break;
|
||||
|
||||
case ORKAudioContentViewEventStopRecording:
|
||||
[self finish];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)audioRecorderDidChange {
|
||||
_audioRecorder.audioRecorder.meteringEnabled = YES;
|
||||
[self setAvAudioRecorder:_audioRecorder.audioRecorder];
|
||||
@@ -116,42 +136,72 @@
|
||||
if (_audioRecorderError) {
|
||||
return;
|
||||
}
|
||||
|
||||
[_avAudioRecorder updateMeters];
|
||||
float value = [_avAudioRecorder averagePowerForChannel:0];
|
||||
// Assume value is in range roughly -60dB to 0dB
|
||||
float clampedValue = MAX(value / 60.0, -1) + 1;
|
||||
[_audioContentView addSample:@(clampedValue)];
|
||||
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startNewTimerIfNeeded {
|
||||
if (!_timer) {
|
||||
NSTimeInterval duration = self.audioStep.stepDuration;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf doSample];
|
||||
if (finished) {
|
||||
[strongSelf finish];
|
||||
}
|
||||
}];
|
||||
[_timer resume];
|
||||
if (self.audioStep.useRecordButton) {
|
||||
|
||||
if (!_intervalTimer) {
|
||||
|
||||
_intervalTimer = [NSTimer scheduledTimerWithTimeInterval: 20 / 100
|
||||
target:self selector:@selector(doSample)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
} else {
|
||||
|
||||
if (!_timer) {
|
||||
NSTimeInterval duration = self.audioStep.stepDuration;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf doSample];
|
||||
if (finished) {
|
||||
[strongSelf finish];
|
||||
}
|
||||
}];
|
||||
[_timer resume];
|
||||
}
|
||||
}
|
||||
|
||||
_audioContentView.finished = NO;
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
[self audioRecorderDidChange];
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
[self startNewTimerIfNeeded];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
|
||||
[self startNewTimerIfNeeded];
|
||||
}
|
||||
|
||||
- (void)suspend {
|
||||
[super suspend];
|
||||
[_timer pause];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer pause];
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
|
||||
if (_avAudioRecorder) {
|
||||
[_audioContentView addSample:@(0)];
|
||||
}
|
||||
@@ -160,8 +210,12 @@
|
||||
- (void)resume {
|
||||
[super resume];
|
||||
[self audioRecorderDidChange];
|
||||
|
||||
[self startNewTimerIfNeeded];
|
||||
[_timer resume];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer resume];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finish {
|
||||
@@ -169,8 +223,14 @@
|
||||
return;
|
||||
}
|
||||
[super finish];
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
@@ -189,3 +249,4 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import Foundation;
|
||||
@import AVFoundation;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol ORKAudioStreamingDelegate <ORKRecorderDelegate>
|
||||
|
||||
- (void)audioAvailable:(AVAudioPCMBuffer *)buffer;
|
||||
|
||||
@end
|
||||
|
||||
@class ORKStep;
|
||||
|
||||
@interface ORKAudioStreamer : ORKRecorder
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier step:(nullable ORKStep *)step NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property (nonatomic, strong, readonly, nullable) AVAudioEngine *audioEngine;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAudioStreamer.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKStep.h"
|
||||
|
||||
#pragma mark - ORKAudioStreamerConfiguration
|
||||
|
||||
@implementation ORKAudioStreamerConfiguration
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (ORKRecorder *)recorderForStep:(ORKStep *)step outputDirectory:(NSURL *)outputDirectory {
|
||||
|
||||
ORKAudioStreamer *obj = [[ORKAudioStreamer alloc] initWithIdentifier:self.identifier step:step];
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
return [super isEqual:object];
|
||||
}
|
||||
|
||||
- (ORKPermissionMask)requestedPermissionMask {
|
||||
return ORKPermissionAudioRecording;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - ORKAudioStreamer
|
||||
|
||||
@implementation ORKAudioStreamer
|
||||
{
|
||||
NSString *_savedSessionCategory;
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier step:(ORKStep *)step
|
||||
{
|
||||
self = [super initWithIdentifier:identifier step:step outputDirectory:nil];
|
||||
if (self)
|
||||
{
|
||||
self.continuesInBackground = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)restoreSavedAudioSessionCategory
|
||||
{
|
||||
if (_savedSessionCategory)
|
||||
{
|
||||
NSError *error;
|
||||
if (![[AVAudioSession sharedInstance] setCategory:_savedSessionCategory error:&error])
|
||||
{
|
||||
ORK_Log_Error("Failed to restore the audio session category: %@", [error localizedDescription]);
|
||||
}
|
||||
_savedSessionCategory = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isRecording
|
||||
{
|
||||
return [_audioEngine isRunning];
|
||||
}
|
||||
|
||||
- (NSString *)recorderType
|
||||
{
|
||||
return @"audioStreaming";
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
if (!_audioEngine)
|
||||
{
|
||||
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
|
||||
_savedSessionCategory = audioSession.category;
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success =
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error] &&
|
||||
[[AVAudioSession sharedInstance] setMode:AVAudioSessionModeMeasurement error:&error] &&
|
||||
[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
|
||||
|
||||
if (!success && error)
|
||||
{
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
ORK_Log_Debug("Create audioEngine recorder %p", self);
|
||||
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
AVAudioInputNode *inputnode = _audioEngine.inputNode;
|
||||
AVAudioFormat *recordingFormat = [inputnode inputFormatForBus:0];
|
||||
|
||||
[inputnode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when)
|
||||
{
|
||||
id<ORKAudioStreamingDelegate> delegate = (id<ORKAudioStreamingDelegate>)self.delegate;
|
||||
|
||||
if (delegate && [delegate respondsToSelector:@selector(audioAvailable:)]) {
|
||||
[delegate audioAvailable:buffer];
|
||||
}
|
||||
}];
|
||||
|
||||
[_audioEngine prepare];
|
||||
|
||||
[_audioEngine startAndReturnError:&error];
|
||||
|
||||
if (error != nil)
|
||||
{
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[super start];
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
if (!_audioEngine)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
[self doStopRecording];
|
||||
|
||||
[super stop];
|
||||
}
|
||||
|
||||
- (void)doStopRecording
|
||||
{
|
||||
if (self.isRecording)
|
||||
{
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
_audioEngine = nil;
|
||||
|
||||
[self restoreSavedAudioSessionCategory];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finishRecordingWithError:(NSError *)error
|
||||
{
|
||||
[self doStopRecording];
|
||||
|
||||
[super finishRecordingWithError:error];
|
||||
}
|
||||
|
||||
- (void)reset
|
||||
{
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
_audioEngine = nil;
|
||||
[super reset];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
ORK_Log_Debug("Remove audiorecorder %p", self);
|
||||
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
|
||||
_audioEngine = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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,9 @@
|
||||
#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 +79,7 @@
|
||||
AVAudioSessionCategoryOptions _savedSessionCategoryOptions;
|
||||
UINotificationFeedbackGenerator *_notificationFeedbackGenerator;
|
||||
dispatch_semaphore_t _voiceOverAnnouncementSemaphore;
|
||||
NSTimer *_timeoutTimer;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) ORKEnvironmentSPLMeterContentView *environmentSPLMeterContentView;
|
||||
@@ -95,6 +101,8 @@
|
||||
_requiredContiguousSamples = 1;
|
||||
_sensitivityOffset = -23.3;
|
||||
_recordedSamples = [NSMutableArray new];
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -109,19 +117,9 @@
|
||||
_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];
|
||||
}
|
||||
|
||||
@@ -133,6 +131,7 @@
|
||||
}
|
||||
|
||||
- (void)setNavigationFooterView {
|
||||
|
||||
self.activeStepView.navigationFooterView.continueButtonItem = self.continueButtonItem;
|
||||
self.activeStepView.navigationFooterView.continueEnabled = NO;
|
||||
[self.activeStepView.navigationFooterView updateContinueAndSkipEnabled];
|
||||
@@ -151,7 +150,15 @@
|
||||
_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 {
|
||||
@@ -160,6 +167,8 @@
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
[self resetAudioSession];
|
||||
|
||||
}
|
||||
|
||||
- (NSString *)deviceType {
|
||||
@@ -194,45 +203,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,6 +373,17 @@
|
||||
});
|
||||
[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);
|
||||
});
|
||||
@@ -382,8 +446,8 @@
|
||||
}
|
||||
|
||||
- (void)reachedOptimumNoiseLevel {
|
||||
[self resetAudioSession];
|
||||
[_audioEngine stop];
|
||||
[self resetAudioSession];
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
@@ -417,19 +481,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
|
||||
|
||||
@@ -75,6 +75,9 @@ ORK_CLASS_AVAILABLE
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSURL *fileURL;
|
||||
|
||||
|
||||
@property (nonatomic, copy, nullable) NSString *fileName;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_URL(aCoder, fileURL);
|
||||
ORK_ENCODE_OBJ(aCoder, contentType);
|
||||
ORK_ENCODE_OBJ(aCoder, fileName);
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
@@ -52,6 +53,7 @@
|
||||
if (self) {
|
||||
ORK_DECODE_URL(aDecoder, fileURL);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, contentType, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, fileName, NSString);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -66,7 +68,8 @@
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualFileURLs(self.fileURL, castObject.fileURL) &&
|
||||
ORKEqualObjects(self.contentType, castObject.contentType));
|
||||
ORKEqualObjects(self.contentType, castObject.contentType) &&
|
||||
ORKEqualObjects(self.fileName, castObject.fileName));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
@@ -77,6 +80,7 @@
|
||||
ORKFileResult *result = [super copyWithZone:zone];
|
||||
result.fileURL = [self.fileURL copy];
|
||||
result.contentType = [self.contentType copy];
|
||||
result.fileName = [self.fileName copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -34,19 +34,38 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Displays a countdown ring and a timer.
|
||||
//
|
||||
// ------------------------------
|
||||
// | |
|
||||
// | Title Label |
|
||||
// | |
|
||||
// | subtitle label |
|
||||
// | |
|
||||
// | __________ |
|
||||
// | / \ |
|
||||
// | | 2:30 | |
|
||||
// | \ ________ / |
|
||||
// | |
|
||||
// |______________________________|
|
||||
@interface ORKFitnessContentView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic, assign, getter=isFinished) BOOL finished;
|
||||
|
||||
@property (nonatomic) BOOL hasHeartRate;
|
||||
@property (nonatomic) BOOL hasDistance;
|
||||
/// The total amount of time the active task is supposed to be performed for.
|
||||
/// For the six minute walk test, this will typically be 360 seconds.
|
||||
@property (nonatomic) NSTimeInterval duration;
|
||||
|
||||
@property (nonatomic, copy, nullable) NSString *heartRate;
|
||||
@property (nonatomic) double distanceInMeters;
|
||||
/// The amount of time that still remain.
|
||||
@property (nonatomic) NSTimeInterval timeLeft;
|
||||
|
||||
@property (nonatomic, strong, nullable) UIImage *image;
|
||||
/// Whether or not the text label is hidden.
|
||||
@property (nonatomic) BOOL labelHidden;
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval timeLeft;
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithDuration:(NSTimeInterval)duration;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -31,28 +31,8 @@
|
||||
|
||||
#import "ORKFitnessContentView.h"
|
||||
|
||||
#import "ORKActiveStepQuantityView.h"
|
||||
#import "ORKTintedImageView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
@import CoreMotion;
|
||||
@import HealthKit;
|
||||
|
||||
|
||||
// #define LAYOUT_TEST 1
|
||||
// #define LAYOUT_DEBUG 1
|
||||
|
||||
@interface ORKFitnessContentView () {
|
||||
ORKQuantityLabel *_timerLabel;
|
||||
ORKQuantityPairView *_quantityPairView;
|
||||
UIView *_imageSpacer1;
|
||||
UIView *_imageSpacer2;
|
||||
ORKTintedImageView *_imageView;
|
||||
NSLengthFormatter *_lengthFormatter;
|
||||
NSLayoutConstraint *_imageRatioConstraint;
|
||||
NSLayoutConstraint *_topConstraint;
|
||||
UILabel *_timerLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -60,272 +40,72 @@
|
||||
|
||||
@implementation ORKFitnessContentView
|
||||
|
||||
- (ORKActiveStepQuantityView *)distanceView {
|
||||
return _quantityPairView.leftView;
|
||||
}
|
||||
|
||||
- (ORKActiveStepQuantityView *)heartRateView {
|
||||
return _quantityPairView.rightView;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
- (instancetype)initWithDuration:(NSTimeInterval)duration {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_timerLabel = [ORKQuantityLabel new];
|
||||
_quantityPairView = [ORKQuantityPairView new];
|
||||
_imageSpacer1 = [UIView new];
|
||||
_imageSpacer1.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_imageSpacer2 = [UIView new];
|
||||
_imageSpacer2.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_imageSpacer1];
|
||||
[self addSubview:_imageSpacer2];
|
||||
[self heartRateView].image = [UIImage imageNamed:@"heart-fitness" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
|
||||
[self updateLengthFormatter];
|
||||
_imageView = [ORKTintedImageView new];
|
||||
_imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_imageView.shouldApplyTint = YES;
|
||||
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_quantityPairView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self updateKeylineVisible];
|
||||
|
||||
_timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently;
|
||||
_imageView.isAccessibilityElement = NO;
|
||||
|
||||
self.hasHeartRate = _hasHeartRate;
|
||||
self.hasDistance = _hasDistance;
|
||||
|
||||
#if LAYOUT_TEST
|
||||
self.timeLeft = 60 * 5;
|
||||
self.hasHeartRate = YES;
|
||||
self.hasDistance = YES;
|
||||
self.distanceInMeters = 100;
|
||||
self.heartRate = @"22";
|
||||
#endif
|
||||
#if LAYOUT_DEBUG
|
||||
self.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
|
||||
_quantityPairView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.2];
|
||||
#endif
|
||||
|
||||
[self setDistanceInMeters:0];
|
||||
[self heartRateView].title = ORKLocalizedString(@"FITNESS_HEARTRATE_TITLE", nil);
|
||||
_duration = duration;
|
||||
_timeLeft = duration;
|
||||
|
||||
_timerLabel = [[UILabel alloc] init];
|
||||
_timerLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_timerLabel.font = [self labelFont];
|
||||
_timerLabel.adjustsFontForContentSizeCategory = YES;
|
||||
_timerLabel.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
|
||||
|
||||
[self addSubview:_quantityPairView];
|
||||
[self addSubview:_imageView];
|
||||
[self addSubview:_timerLabel];
|
||||
[self setUpConstraints];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
|
||||
|
||||
[self tintColorDidChange];
|
||||
[self updateTimerLabel];
|
||||
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
- (void)tintColorDidChange {
|
||||
[self setNeedsDisplay];
|
||||
_timerLabel.textColor = self.tintColor;
|
||||
}
|
||||
|
||||
- (void)updateLengthFormatter {
|
||||
_lengthFormatter = [NSLengthFormatter new];
|
||||
_lengthFormatter.numberFormatter.maximumFractionDigits = 1;
|
||||
_lengthFormatter.numberFormatter.maximumSignificantDigits = 3;
|
||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
||||
_timerLabel.font = [self labelFont];
|
||||
}
|
||||
|
||||
- (void)localeDidChange:(NSNotification *)notification {
|
||||
[self updateLengthFormatter];
|
||||
[self setDistanceInMeters:_distanceInMeters];
|
||||
}
|
||||
|
||||
- (void)willMoveToWindow:(UIWindow *)newWindow {
|
||||
[super willMoveToWindow:newWindow];
|
||||
[self updateConstraintConstantsForWindow:newWindow];
|
||||
}
|
||||
|
||||
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
|
||||
const CGFloat CaptionBaselineToTimerTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToFitnessTimerTop, window);
|
||||
const CGFloat CaptionBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
|
||||
_topConstraint.constant = (CaptionBaselineToTimerTop - CaptionBaselineToStepViewTop);
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_timerLabel, _imageView, _quantityPairView, _imageSpacer1, _imageSpacer2);
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_timerLabel][_imageSpacer1(>=0)][_imageView]"
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
_topConstraint = [NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
[constraints addObject:_topConstraint];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self attribute:NSLayoutAttributeWidth
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self attribute:NSLayoutAttributeWidth
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_imageView][_imageSpacer2(>=0)][_quantityPairView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer2
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_imageSpacer2
|
||||
attribute:NSLayoutAttributeHeight
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
NSLayoutConstraint *imageSpacerHeightConstraint = [NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant: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
|
||||
|
||||
@@ -36,17 +36,11 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Fitness step.
|
||||
|
||||
Displays usual header, a counting-up timer, read outs for distance and/or
|
||||
heart rate if corresponding recorders are attached.
|
||||
|
||||
Also displays an image during the task.
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKFitnessStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, copy) NSDictionary *userInfo;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
|
||||
@@ -43,6 +44,7 @@
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.userInfo = [[NSDictionary alloc] init];
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
}
|
||||
return self;
|
||||
@@ -58,13 +60,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKFitnessStep *step = [super copyWithZone:zone];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)startsFinished {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(coder, userInfo, NSDictionary);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
[super encodeWithCoder:coder];
|
||||
ORK_ENCODE_OBJ(coder, userInfo);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKFitnessStep *step = [super copyWithZone:zone];
|
||||
step.userInfo = [self.userInfo copy];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
if ([self class] != [other class]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__typeof(self) castObject = other;
|
||||
return ORKEqualObjects(self.userInfo, castObject.userInfo);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [super hash] ^ self.userInfo.hash;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,27 +30,20 @@
|
||||
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKFitnessContentView.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKActiveStepTimer.h"
|
||||
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKHealthQuantityTypeRecorder.h"
|
||||
#import "ORKPedometerRecorder.h"
|
||||
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
|
||||
@interface ORKFitnessStepViewController () <ORKHealthQuantityTypeRecorderDelegate, ORKPedometerRecorderDelegate> {
|
||||
NSInteger _intendedSteps;
|
||||
@interface ORKFitnessStepViewController () {
|
||||
ORKFitnessContentView *_contentView;
|
||||
NSNumberFormatter *_hrFormatter;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -70,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];
|
||||
|
||||
@@ -349,6 +349,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,65 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import <ResearchKit/ORKResult.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKFileResult;
|
||||
|
||||
/**
|
||||
The `ORKReactionTimeResult` class represents the result of a single successful attempt within an ORKReactionTimeStep.
|
||||
|
||||
The `timestamp` property is equal to the value of systemUptime (in NSProcessInfo) when the stimulus occurred.
|
||||
Each entry of motion data in this file contains a time interval which may be directly compared to timestamp in order to determine the elapsed time since the stimulus.
|
||||
|
||||
The fileResult property references the motion data recorded from the beginning of the attempt until the threshold acceleration was reached.
|
||||
Using the time taken to reach the threshold acceleration as the reaction time of a participant will yield a rather crude measurement. Rather, you should devise your own method using the data recorded to obtain an accurate approximation of the true reaction time.
|
||||
|
||||
A reaction time result is typically generated by the framework as the task proceeds. When the task
|
||||
completes, it may be appropriate to serialize the sample for transmission to a server
|
||||
or to immediately perform analysis on it.
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKNormalizedReactionTimeResult: ORKResult
|
||||
|
||||
@property (nonatomic, copy) NSDate * timerStartDate;
|
||||
@property (nonatomic, copy) NSDate * timerEndDate;
|
||||
@property (nonatomic, copy, nullable) NSDate * stimulusStartDate;
|
||||
@property (nonatomic, copy, nullable) NSDate * reactionDate;
|
||||
@property (nonatomic) NSNumber *currentInterval;
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKNormalizedReactionTimeResult.h"
|
||||
|
||||
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKNormalizedReactionTimeResult
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, timerStartDate);
|
||||
ORK_ENCODE_OBJ(aCoder, timerEndDate);
|
||||
ORK_ENCODE_OBJ(aCoder, stimulusStartDate);
|
||||
ORK_ENCODE_OBJ(aCoder, reactionDate);
|
||||
ORK_ENCODE_OBJ(aCoder, currentInterval);
|
||||
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, timerStartDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, timerEndDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, stimulusStartDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, reactionDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, currentInterval, NSNumber);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualObjects(self.timerStartDate, castObject.timerStartDate) &&
|
||||
ORKEqualObjects(self.timerEndDate, castObject.timerEndDate) &&
|
||||
ORKEqualObjects(self.stimulusStartDate, castObject.stimulusStartDate) &&
|
||||
ORKEqualObjects(self.reactionDate, castObject.reactionDate) &&
|
||||
ORKEqualObjects(self.currentInterval, castObject.currentInterval)) ;
|
||||
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return super.hash ^ _timerStartDate.hash ^ _timerEndDate.hash ^ _stimulusStartDate.hash ^ _reactionDate.hash;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKNormalizedReactionTimeResult *result = [super copyWithZone:zone];
|
||||
result.timerStartDate = [self.timerStartDate copy];
|
||||
result.timerEndDate = [self.timerEndDate copy];
|
||||
result.stimulusStartDate = [self.stimulusStartDate copy];
|
||||
result.reactionDate = [self.reactionDate copy];
|
||||
result.currentInterval = [self.currentInterval copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015, James Cox. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import Foundation;
|
||||
@import AudioToolbox;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKNormalizedReactionTimeStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval maximumStimulusInterval;
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval minimumStimulusInterval;
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval timeout;
|
||||
|
||||
@property (nonatomic, assign) NSInteger numberOfAttempts;
|
||||
|
||||
@property (nonatomic, assign) double thresholdAcceleration;
|
||||
|
||||
@property (nonatomic, assign) SystemSoundID successSound;
|
||||
|
||||
@property (nonatomic, assign) SystemSoundID timeoutSound;
|
||||
|
||||
@property (nonatomic, assign) SystemSoundID failureSound;
|
||||
|
||||
@property (nonatomic) NSNumber *currentInterval;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015, James Cox. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKNormalizedReactionTimeStep.h"
|
||||
|
||||
#import "ORKNormalizedReactionTimeViewController.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKNormalizedReactionTimeStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORKNormalizedReactionTimeViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
self.shouldContinueOnFinish = YES;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKNormalizedReactionTimeStep *step = [super copyWithZone:zone];
|
||||
step.maximumStimulusInterval = self.maximumStimulusInterval;
|
||||
step.minimumStimulusInterval = self.minimumStimulusInterval;
|
||||
step.thresholdAcceleration = self.thresholdAcceleration;
|
||||
step.timeout = self.timeout;
|
||||
step.numberOfAttempts = self.numberOfAttempts;
|
||||
step.successSound = self.successSound;
|
||||
step.timeoutSound = self.timeoutSound;
|
||||
step.failureSound = self.failureSound;
|
||||
self.currentInterval = self.currentInterval;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
|
||||
if (self.minimumStimulusInterval <= 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"minimumStimulusInterval must be greater than zero"
|
||||
userInfo:nil];
|
||||
}
|
||||
if (self.maximumStimulusInterval < self.minimumStimulusInterval) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"maximumStimulusInterval cannot be less than minimumStimulusInterval"
|
||||
userInfo:nil];
|
||||
}
|
||||
if (self.thresholdAcceleration <= 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"thresholdAcceleration must be greater than zero"
|
||||
userInfo:nil];
|
||||
}
|
||||
if (self.timeout <= 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"timeout must be greater than zero"
|
||||
userInfo:nil];
|
||||
}
|
||||
if (self.numberOfAttempts <= 0) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
||||
reason:@"numberOfAttempts must be greater than zero"
|
||||
userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_DOUBLE(aDecoder, maximumStimulusInterval);
|
||||
ORK_DECODE_DOUBLE(aDecoder, minimumStimulusInterval);
|
||||
ORK_DECODE_DOUBLE(aDecoder, thresholdAcceleration);
|
||||
ORK_DECODE_DOUBLE(aDecoder, timeout);
|
||||
ORK_DECODE_UINT32(aDecoder, successSound);
|
||||
ORK_DECODE_UINT32(aDecoder, timeoutSound);
|
||||
ORK_DECODE_UINT32(aDecoder, failureSound);
|
||||
ORK_DECODE_INTEGER(aDecoder, numberOfAttempts);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, currentInterval, NSNumber);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_DOUBLE(aCoder, maximumStimulusInterval);
|
||||
ORK_ENCODE_DOUBLE(aCoder, minimumStimulusInterval);
|
||||
ORK_ENCODE_DOUBLE(aCoder, thresholdAcceleration);
|
||||
ORK_ENCODE_DOUBLE(aCoder, timeout);
|
||||
ORK_ENCODE_UINT32(aCoder, successSound);
|
||||
ORK_ENCODE_UINT32(aCoder, timeoutSound);
|
||||
ORK_ENCODE_UINT32(aCoder, failureSound);
|
||||
ORK_ENCODE_INTEGER(aCoder, numberOfAttempts);
|
||||
ORK_ENCODE_OBJ(aCoder, currentInterval);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
(self.maximumStimulusInterval == castObject.maximumStimulusInterval) &&
|
||||
(self.minimumStimulusInterval == castObject.minimumStimulusInterval) &&
|
||||
(self.thresholdAcceleration == castObject.thresholdAcceleration) &&
|
||||
(self.timeout == castObject.timeout) &&
|
||||
(self.successSound == castObject.successSound) &&
|
||||
(self.timeoutSound == castObject.timeoutSound) &&
|
||||
(self.failureSound == castObject.failureSound) &&
|
||||
(self.numberOfAttempts == castObject.numberOfAttempts) &&
|
||||
(self.currentInterval == castObject.currentInterval)
|
||||
);}
|
||||
|
||||
- (BOOL)allowsBackNavigation {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)startsFinished {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015, James Cox. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKNormalizedReactionTimeStimulusView : UIView
|
||||
|
||||
- (void)reset;
|
||||
|
||||
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void(^)(void))completion;
|
||||
|
||||
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(nullable void(^)(void))completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015, James Cox. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKNormalizedReactionTimeStimulusView.h"
|
||||
|
||||
|
||||
@implementation ORKNormalizedReactionTimeStimulusView {
|
||||
CAShapeLayer *_tickLayer;
|
||||
CAShapeLayer *_crossLayer;
|
||||
}
|
||||
|
||||
static const CGFloat RoundReactionTimeViewDiameter = 122;
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.layer.cornerRadius = RoundReactionTimeViewDiameter * 0.5;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize {
|
||||
return CGSizeMake(RoundReactionTimeViewDiameter, RoundReactionTimeViewDiameter);
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
[_tickLayer removeFromSuperlayer];
|
||||
[_crossLayer removeFromSuperlayer];
|
||||
_tickLayer = nil;
|
||||
_crossLayer = nil;
|
||||
self.layer.backgroundColor = self.tintColor.CGColor;
|
||||
}
|
||||
|
||||
- (void)startSuccessAnimationWithDuration:(NSTimeInterval)duration completion:(void(^)(void))completion {
|
||||
if (self.hidden) {
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
[self addTickLayer];
|
||||
[CATransaction begin];
|
||||
[CATransaction setCompletionBlock:completion];
|
||||
CAMediaTimingFunction *timing = [[CAMediaTimingFunction alloc] initWithControlPoints:0.180739998817444 :0 :0.577960014343262 :0.918200016021729];
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
|
||||
[animation setTimingFunction:timing];
|
||||
animation.removedOnCompletion = NO;
|
||||
[animation setFillMode:kCAFillModeForwards];
|
||||
animation.fromValue = @(0);
|
||||
animation.toValue = @(1);
|
||||
animation.duration = duration;
|
||||
[_tickLayer addAnimation:animation forKey:@"strokeEnd"];
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (void)startFailureAnimationWithDuration:(NSTimeInterval)duration completion:(void(^)(void))completion {
|
||||
self.hidden = NO;
|
||||
|
||||
self.layer.backgroundColor = [UIColor clearColor].CGColor;
|
||||
[self addCrossLayer];
|
||||
[CATransaction begin];
|
||||
[CATransaction setCompletionBlock:completion];
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
|
||||
[animation setFillMode:kCAFillModeForwards];
|
||||
animation.fromValue = @([(CAShapeLayer *)[_crossLayer presentationLayer] strokeEnd]);
|
||||
animation.toValue = @(1);
|
||||
animation.duration = duration;
|
||||
_crossLayer.strokeEnd = 1;
|
||||
[_crossLayer addAnimation:animation forKey:@"strokeEnd"];
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden {
|
||||
[self reset];
|
||||
[super setHidden:hidden];
|
||||
}
|
||||
|
||||
- (void)addCrossLayer {
|
||||
_crossLayer = [self lineDrawingLayer];
|
||||
_crossLayer.strokeColor = [UIColor redColor].CGColor;
|
||||
_crossLayer.path = [self crossPath];
|
||||
[self.layer addSublayer:_crossLayer];
|
||||
}
|
||||
|
||||
- (void)addTickLayer {
|
||||
_tickLayer = [self lineDrawingLayer];
|
||||
_tickLayer.strokeColor = [UIColor whiteColor].CGColor;
|
||||
_tickLayer.path = [self tickPath];
|
||||
[self.layer addSublayer:_tickLayer];
|
||||
}
|
||||
|
||||
- (CGPathRef)concealPath:(CGFloat)radius {
|
||||
return [[UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
|
||||
radius:radius / 2
|
||||
startAngle:M_PI + M_PI_2
|
||||
endAngle:-M_PI_2
|
||||
clockwise:NO] CGPath];
|
||||
}
|
||||
|
||||
- (CGPathRef)tickPath {
|
||||
UIBezierPath *path = [self linePath];
|
||||
[path moveToPoint:(CGPoint){37,65}];
|
||||
[path addLineToPoint:(CGPoint){50,78}];
|
||||
[path addLineToPoint:(CGPoint){87,42}];
|
||||
return path.CGPath;
|
||||
}
|
||||
|
||||
- (CGPathRef)crossPath {
|
||||
UIBezierPath *path = [self linePath];
|
||||
[path moveToPoint:(CGPoint){45,78}];
|
||||
[path addLineToPoint:(CGPoint){82,42}];
|
||||
[path moveToPoint:(CGPoint){45,42}];
|
||||
[path addLineToPoint:(CGPoint){82,78}];
|
||||
return path.CGPath;
|
||||
}
|
||||
|
||||
- (UIBezierPath *)linePath {
|
||||
UIBezierPath *path = [UIBezierPath new];
|
||||
path.lineCapStyle = kCGLineCapRound;
|
||||
path.lineWidth = 5;
|
||||
return path;
|
||||
}
|
||||
|
||||
- (CAShapeLayer *)lineDrawingLayer {
|
||||
CAShapeLayer *shapeLayer = [CAShapeLayer new];
|
||||
shapeLayer.strokeEnd = 0;
|
||||
shapeLayer.lineWidth = 5;
|
||||
shapeLayer.lineCap = kCALineCapRound;
|
||||
shapeLayer.lineJoin = kCALineJoinRound;
|
||||
shapeLayer.frame = self.layer.bounds;
|
||||
shapeLayer.backgroundColor = [UIColor clearColor].CGColor;
|
||||
shapeLayer.fillColor = nil;
|
||||
return shapeLayer;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015, James Cox. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKDefines.h"
|
||||
#import "ORKActiveStepViewController.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKNormalizedReactionTimeViewController : ORKActiveStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
Copyright (c) 2019, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2015, James Cox. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKNormalizedReactionTimeViewController.h"
|
||||
|
||||
#import "ORKBorderedButton.h"
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKNormalizedReactionTimeContentView.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKVerticalContainerView_Internal.h"
|
||||
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKNormalizedReactionTimeResult.h"
|
||||
#import "ORKNormalizedReactionTimeStep.h"
|
||||
#import "ORKResult.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import <AudioToolbox/AudioServices.h>
|
||||
|
||||
|
||||
@implementation ORKNormalizedReactionTimeViewController {
|
||||
ORKNormalizedReactionTimeContentView *_reactionTimeContentView;
|
||||
NSMutableArray *_results;
|
||||
NSTimer *_stimulusTimer;
|
||||
NSTimer *_timeoutTimer;
|
||||
NSTimeInterval _stimulusTimestamp;
|
||||
BOOL _validResult;
|
||||
BOOL _timedOut;
|
||||
BOOL _shouldIndicateFailure;
|
||||
|
||||
UIView *_backgroundView;
|
||||
ORKNormalizedReactionTimeStimulusView *_stimulusView;
|
||||
|
||||
NSDate *_timerStartDate;
|
||||
NSDate *_stimulusStartDate;
|
||||
NSDate *_reactionDate;
|
||||
}
|
||||
|
||||
static const NSTimeInterval OutcomeAnimationDuration = 0.3;
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
[self configureTitle];
|
||||
_results = [NSMutableArray new];
|
||||
_reactionTimeContentView = [ORKNormalizedReactionTimeContentView new];
|
||||
[_reactionTimeContentView.button addTarget:self action:@selector(startStimulusTimer) forControlEvents:UIControlEventTouchDown];
|
||||
[_reactionTimeContentView.button addTarget:self action:@selector(startReactionTimer) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
self.activeStepView.activeCustomView = _reactionTimeContentView;
|
||||
|
||||
_backgroundView = [_reactionTimeContentView getBackgroundView];
|
||||
_stimulusView = [_reactionTimeContentView getStimulusView];
|
||||
|
||||
[_backgroundView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected)]];
|
||||
[_stimulusView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected)]];
|
||||
|
||||
[_reactionTimeContentView setStimulusHidden:YES];
|
||||
}
|
||||
|
||||
|
||||
-(void)startReactionTimer {
|
||||
if (_stimulusView.hidden) {
|
||||
_validResult = NO;
|
||||
_timedOut = YES;
|
||||
[self addReactionTimeResult];
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// Device motion recorder won't work, so manually trigger didfinish
|
||||
[self attemptDidFinish];
|
||||
#endif
|
||||
} else {
|
||||
_timerStartDate = [NSDate date];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tapDetected {
|
||||
if ([_stimulusTimer isValid] || [_timeoutTimer isValid]) {
|
||||
_reactionDate = [NSDate date];
|
||||
[self addReactionTimeResult];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
[self start];
|
||||
_shouldIndicateFailure = YES;
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
_shouldIndicateFailure = NO;
|
||||
}
|
||||
|
||||
#pragma mark - ORKActiveStepViewController
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *stepResult = [super result];
|
||||
stepResult.results = [self.addedResults arrayByAddingObjectsFromArray:_results] ? : _results;
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
[super applicationWillResignActive:notification];
|
||||
_validResult = NO;
|
||||
[_stimulusTimer invalidate];
|
||||
[_timeoutTimer invalidate];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||
[super applicationDidBecomeActive:notification];
|
||||
[self resetAfterDelay:0];
|
||||
}
|
||||
|
||||
#pragma mark - ORKRecorderDelegate
|
||||
|
||||
- (void)addReactionTimeResult {
|
||||
ORKNormalizedReactionTimeResult *reactionTimeResult = [[ORKNormalizedReactionTimeResult alloc] initWithIdentifier:self.step.identifier];
|
||||
reactionTimeResult.timerStartDate = _timerStartDate;
|
||||
reactionTimeResult.timerEndDate = [NSDate date];
|
||||
reactionTimeResult.reactionDate = _reactionDate;
|
||||
reactionTimeResult.stimulusStartDate = _stimulusStartDate;
|
||||
reactionTimeResult.currentInterval = [self reactionTimeStep].currentInterval;
|
||||
[_results addObject:reactionTimeResult];
|
||||
_timerStartDate = nil;
|
||||
_reactionDate = nil;
|
||||
_stimulusStartDate = nil;
|
||||
|
||||
[self attemptDidFinish];
|
||||
}
|
||||
|
||||
#pragma mark - ORKReactionTimeStepViewController
|
||||
|
||||
- (ORKNormalizedReactionTimeStep *)reactionTimeStep {
|
||||
return (ORKNormalizedReactionTimeStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)configureTitle {
|
||||
NSString *format = ORKLocalizedString(@"REACTION_TIME_TASK_ATTEMPTS_FORMAT", nil);
|
||||
NSString *text = [[NSString stringWithFormat: @"%@\n",ORKLocalizedString(@"REACTION_TIME_NORMALIZED_TASK_ACTIVE_STEP_TITLE", nil)] stringByAppendingString: [NSString stringWithFormat:format, ORKLocalizedStringFromNumber(@(_results.count + 1)), ORKLocalizedStringFromNumber(@([self reactionTimeStep].numberOfAttempts))]];
|
||||
[self.activeStepView updateTitle:nil text:text];
|
||||
}
|
||||
|
||||
- (void)attemptDidFinish {
|
||||
void (^completion)(void) = ^{
|
||||
if (_results.count == [self reactionTimeStep].numberOfAttempts) {
|
||||
[self finish];
|
||||
} else {
|
||||
[self resetAfterDelay:2];
|
||||
}
|
||||
};
|
||||
if (_validResult) {
|
||||
[self indicateSuccess:completion];
|
||||
} else {
|
||||
[self indicateFailure:completion];
|
||||
}
|
||||
_validResult = NO;
|
||||
_timedOut = NO;
|
||||
[_stimulusTimer invalidate];
|
||||
[_timeoutTimer invalidate];
|
||||
}
|
||||
|
||||
- (void)indicateSuccess:(void(^)(void))completion {
|
||||
[_reactionTimeContentView startSuccessAnimationWithDuration:OutcomeAnimationDuration completion:completion];
|
||||
AudioServicesPlaySystemSound([self reactionTimeStep].successSound);
|
||||
}
|
||||
|
||||
- (void)indicateFailure:(void(^)(void))completion {
|
||||
if (!_shouldIndicateFailure) {
|
||||
return;
|
||||
}
|
||||
[_reactionTimeContentView startFailureAnimationWithDuration:OutcomeAnimationDuration completion:completion];
|
||||
SystemSoundID sound = _timedOut ? [self reactionTimeStep].timeoutSound : [self reactionTimeStep].failureSound;
|
||||
AudioServicesPlayAlertSound(sound);
|
||||
}
|
||||
|
||||
- (void)resetAfterDelay:(NSTimeInterval)delay {
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
[_reactionTimeContentView resetAfterDelay:delay completion:^{
|
||||
[weakSelf configureTitle];
|
||||
[weakSelf start];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)startStimulusTimer {
|
||||
_stimulusTimer = [NSTimer scheduledTimerWithTimeInterval:[self stimulusInterval] target:self selector:@selector(stimulusTimerDidFire) userInfo:nil repeats:NO];
|
||||
}
|
||||
|
||||
- (void)stimulusTimerDidFire {
|
||||
_stimulusStartDate = [NSDate date];
|
||||
|
||||
_stimulusTimestamp = [NSProcessInfo processInfo].systemUptime;
|
||||
[_reactionTimeContentView setStimulusHidden:NO];
|
||||
_validResult = YES;
|
||||
[self startTimeoutTimer];
|
||||
}
|
||||
|
||||
- (void)startTimeoutTimer {
|
||||
NSTimeInterval timeout = [self reactionTimeStep].timeout;
|
||||
if (timeout > 0) {
|
||||
_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:@selector(timeoutTimerDidFire) userInfo:nil repeats:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)timeoutTimerDidFire {
|
||||
_validResult = NO;
|
||||
_timedOut = YES;
|
||||
[self addReactionTimeResult];
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// Device motion recorder won't work, so manually trigger didfinish
|
||||
[self attemptDidFinish];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSTimeInterval)stimulusInterval {
|
||||
ORKNormalizedReactionTimeStep *step = [self reactionTimeStep];
|
||||
NSNumber* interval = [self getRandomInterval];
|
||||
step.currentInterval = interval;
|
||||
return [interval doubleValue];
|
||||
}
|
||||
|
||||
|
||||
- (NSNumber*) getRandomInterval {
|
||||
NSArray* values = @[@2,@4,@6];
|
||||
|
||||
int randIndex = arc4random() % [values count];
|
||||
return (NSNumber*)values[randIndex];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@end
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+54
-50
@@ -30,7 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKSpeechInNoiseContentView.h"
|
||||
#import "ORKAudioGraphView.h"
|
||||
#import "ORKAudioMeteringView.h"
|
||||
|
||||
#import "ORKHeadlineLabel.h"
|
||||
#import "ORKSubheadlineLabel.h"
|
||||
@@ -41,10 +41,16 @@
|
||||
#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 +77,11 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect
|
||||
{
|
||||
[self setUpConstraints];
|
||||
}
|
||||
|
||||
- (void)setupTextLabel {
|
||||
_textLabel = [ORKSubheadlineLabel new];
|
||||
_textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
@@ -81,9 +92,9 @@
|
||||
}
|
||||
|
||||
- (void)setupGraphView {
|
||||
self.graphView = [ORKAudioGraphView new];
|
||||
self.graphView = [[ORKAudioMeteringView alloc] init];
|
||||
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[_graphView setMeterColor:[UIColor lightGrayColor]];
|
||||
[self addSubview:_graphView];
|
||||
}
|
||||
|
||||
@@ -121,72 +132,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
|
||||
+36
-45
@@ -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
|
||||
+10
@@ -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.
|
||||
*/
|
||||
+13
-3
@@ -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)
|
||||
+102
-19
@@ -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
|
||||
BIN
Binary file not shown.
@@ -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,84 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (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;
|
||||
|
||||
}
|
||||
|
||||
- (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 +243,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 +430,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 +455,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
Reference in New Issue
Block a user