Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8473017ec | |||
| 2bc0033aa8 | |||
| a89059f5dc | |||
| 2cc8f9e7d5 |
@@ -1,5 +0,0 @@
|
||||
language: objective-c
|
||||
osx_image: xcode12
|
||||
xcode_project: ResearchKit.xcodeproj
|
||||
xcode_scheme: ResearchKit
|
||||
xcode_destination: platform=iOS Simulator,OS=14.0,name=iPhone 11 Pro Max
|
||||
@@ -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
|
||||
|
||||
+59
-46
@@ -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,67 +28,80 @@
|
||||
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 "ORKAccuracyStroopStep.h"
|
||||
#import "ORKAccuracyStroopStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
|
||||
@implementation ORKVisualConsentStep
|
||||
@implementation ORKAccuracyStroopStep
|
||||
|
||||
+ (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 {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, consentDocument);
|
||||
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 &&
|
||||
ORKEqualObjects(self.consentDocument, castObject.consentDocument));
|
||||
|
||||
return isParentSame
|
||||
&& self.isColorMatching == castObject.isColorMatching
|
||||
&& ORKEqualObjects(self.baseDisplayColor, castObject.baseDisplayColor);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return super.hash ^ self.consentDocument.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
|
||||
#pragma clang diagnostic pop
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
@@ -100,17 +105,7 @@
|
||||
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];
|
||||
}
|
||||
@@ -138,7 +133,8 @@
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager
|
||||
didUpdateLocations:(NSArray *)locations {
|
||||
didUpdateLocations:(NSArray<CLLocation *> *)locations {
|
||||
|
||||
BOOL success = YES;
|
||||
NSParameterAssert(locations.count >= 0);
|
||||
NSError *error = nil;
|
||||
|
||||
@@ -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
|
||||
|
||||
+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
|
||||
+51
-31
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -28,36 +28,56 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKSpeechInNoiseResult.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKResult_Private.h"
|
||||
|
||||
import Foundation
|
||||
@implementation ORKSpeechInNoiseResult
|
||||
|
||||
@available(watchOSApplicationExtension 5.0, *)
|
||||
class AssessmentManager {
|
||||
private var manager: CMMovementDisorderManager?
|
||||
init() {
|
||||
if CMMovementDisorderManager.isAvailable() {
|
||||
manager = CMMovementDisorderManager()
|
||||
|
||||
monitorForParkinsons()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func monitorForParkinsons() {
|
||||
manager?.monitorKinesias(forDuration: 7 * 24 * 3600)
|
||||
}
|
||||
|
||||
func queryNewAssessments() {
|
||||
let calendar = Calendar.current
|
||||
let toDate = Date()
|
||||
let fromDate: Date = calendar.date(byAdding: .day, value: -7, to: toDate)!
|
||||
|
||||
manager?.queryTremor(from: fromDate, to: toDate, withHandler: { (_/*results*/, _/*error*/) in
|
||||
|
||||
})
|
||||
|
||||
manager?.queryDyskineticSymptom(from: fromDate, to: toDate, withHandler: { (_/*results*/, _/*error*/) in
|
||||
|
||||
})
|
||||
}
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder
|
||||
{
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, filename);
|
||||
ORK_ENCODE_OBJ(aCoder, targetSentence);
|
||||
}
|
||||
|
||||
- (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 isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualObjects(self.targetSentence, castObject.targetSentence) &&
|
||||
ORKEqualObjects(self.filename, castObject.filename));
|
||||
}
|
||||
|
||||
- (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]];
|
||||
});
|
||||
|
||||
@@ -28,22 +28,28 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import AVFoundation;
|
||||
@import Speech;
|
||||
|
||||
@protocol ORKSpeechRecognitionDelegate
|
||||
@class ORKSpeechRecognizer;
|
||||
|
||||
@protocol ORKSpeechRecognitionDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
/**
|
||||
Tells the delegate when the recognition of requested utterance is finished.
|
||||
*/
|
||||
- (void)didFinishRecognitionWithError:(NSError *)error;
|
||||
- (void)didFinishRecognitionWithError:(null_unspecified NSError *)error;
|
||||
|
||||
/**
|
||||
Tells the delegate that a hypothesized transcription is available.
|
||||
*/
|
||||
- (void)didHypothesizeTranscription:(SFTranscription *)transcription;
|
||||
- (void)didHypothesizeTranscription:(null_unspecified SFTranscription *)transcription;
|
||||
|
||||
/**
|
||||
Tells the delegate the recognizer finished recognition, and passes back the full result.
|
||||
*/
|
||||
- (void)didFinishRecognition:(nonnull SFSpeechRecognitionResult *)recognitionResult;
|
||||
|
||||
/**
|
||||
Tells the delegate when the availability of the speech recognizer has changed
|
||||
@@ -57,11 +63,27 @@
|
||||
*/
|
||||
@interface ORKSpeechRecognizer: NSObject
|
||||
|
||||
/**
|
||||
Queries the client application for speech recognition authorization status.
|
||||
|
||||
@return The current authorization status of the client application.
|
||||
|
||||
*/
|
||||
+ (SFSpeechRecognizerAuthorizationStatus)authorizationStatus;
|
||||
|
||||
/**
|
||||
Asks the user to grant your app permission to perform speech recognition.
|
||||
*/
|
||||
+ (void)requestAuthorization;
|
||||
|
||||
/**
|
||||
Asks the user to grant your app permission to perform speech recognition.
|
||||
|
||||
@param handler A block to execute after the authorization attempt finishes. Not guaranteed to run on the apps main dispatch queue.
|
||||
|
||||
*/
|
||||
+ (void)requestAuthorization:(void (^ _Nonnull)(SFSpeechRecognizerAuthorizationStatus authorizationStatus))handler;
|
||||
|
||||
/**
|
||||
Starts speech recognition for the specified locale
|
||||
|
||||
@@ -71,14 +93,14 @@
|
||||
@param handler A handler to report errors
|
||||
|
||||
*/
|
||||
- (void)startRecognitionWithLocale:(NSLocale *)locale reportPartialResults:(BOOL)reportPartialResults responseDelegate:(id<ORKSpeechRecognitionDelegate>)delegate errorHandler:(void (^)(NSError *error))handler;
|
||||
- (void)startRecognitionWithLocale:(null_unspecified NSLocale *)locale reportPartialResults:(BOOL)reportPartialResults responseDelegate:(null_unspecified id<ORKSpeechRecognitionDelegate>)delegate errorHandler:(void (^ _Null_unspecified)(NSError * _Null_unspecified error))handler;
|
||||
|
||||
/**
|
||||
Appends audio to the end of the recognition request.
|
||||
|
||||
@param audioBuffer A buffer of audio
|
||||
*/
|
||||
- (void)addAudio:(AVAudioPCMBuffer *)audioBuffer;
|
||||
- (void)addAudio:(null_unspecified AVAudioPCMBuffer *)audioBuffer;
|
||||
|
||||
/**
|
||||
Indicates that the audio source is finished and no more audio will be appended to the
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSpeechRecognitionError.h"
|
||||
|
||||
|
||||
@interface ORKSpeechRecognizer() <SFSpeechRecognitionTaskDelegate, SFSpeechRecognizerDelegate>
|
||||
@property(nonatomic, weak) id<ORKSpeechRecognitionDelegate> responseDelegate;
|
||||
@end
|
||||
@@ -54,6 +53,11 @@
|
||||
dispatch_queue_t _responseQueue;
|
||||
}
|
||||
|
||||
+ (SFSpeechRecognizerAuthorizationStatus)authorizationStatus
|
||||
{
|
||||
return [SFSpeechRecognizer authorizationStatus];
|
||||
}
|
||||
|
||||
+ (void)requestAuthorization {
|
||||
|
||||
[SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) {
|
||||
@@ -68,6 +72,11 @@
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)requestAuthorization:(void (^ _Nonnull)(SFSpeechRecognizerAuthorizationStatus authorizationStatus))handler
|
||||
{
|
||||
[SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) { handler(status); }];
|
||||
}
|
||||
|
||||
- (void)startRecognitionWithLocale:(NSLocale *)locale reportPartialResults:(BOOL)reportPartialResults responseDelegate:(id<ORKSpeechRecognitionDelegate>)delegate errorHandler:(void (^)(NSError *error))handler
|
||||
{
|
||||
|
||||
@@ -125,7 +134,9 @@
|
||||
- (void)speechRecognizer:(SFSpeechRecognizer *)speechRecognizer availabilityDidChange:(BOOL)available {
|
||||
dispatch_async(_responseQueue, ^{
|
||||
ORK_Log_Debug("Availability did change = %d", available);
|
||||
[_responseDelegate availabilityDidChange:available];
|
||||
if (_responseDelegate && [_responseDelegate respondsToSelector:@selector(availabilityDidChange:)]) {
|
||||
[_responseDelegate availabilityDidChange:available];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,23 +145,33 @@
|
||||
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishRecognition:(SFSpeechRecognitionResult *)recognitionResult {
|
||||
dispatch_async(_responseQueue, ^{
|
||||
ORK_Log_Debug("did produce final result %@", [[recognitionResult bestTranscription] formattedString]);
|
||||
[_responseDelegate didHypothesizeTranscription:[recognitionResult bestTranscription]];
|
||||
|
||||
if (@available(iOS 14.5, *)) {
|
||||
if (_responseDelegate && [_responseDelegate respondsToSelector:@selector(didFinishRecognition:)]) {
|
||||
[_responseDelegate didFinishRecognition:recognitionResult];
|
||||
} else if (_responseDelegate && [_responseDelegate respondsToSelector:@selector(didHypothesizeTranscription:)]) {
|
||||
[_responseDelegate didHypothesizeTranscription:recognitionResult.bestTranscription];
|
||||
}
|
||||
} else if (_responseDelegate && [_responseDelegate respondsToSelector:@selector(didHypothesizeTranscription:)]) {
|
||||
[_responseDelegate didHypothesizeTranscription:recognitionResult.bestTranscription];
|
||||
}
|
||||
});
|
||||
}
|
||||
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didHypothesizeTranscription:(SFTranscription *)transcription {
|
||||
dispatch_async(_responseQueue, ^{
|
||||
// Produces transcription if shouldReportPartialResults is true
|
||||
ORK_Log_Debug("did produce partial results %@", [transcription formattedString]);
|
||||
[_responseDelegate didHypothesizeTranscription:transcription];
|
||||
|
||||
if (_responseDelegate && [_responseDelegate respondsToSelector:@selector(didHypothesizeTranscription:)]) {
|
||||
[_responseDelegate didHypothesizeTranscription:transcription];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishSuccessfully:(BOOL)successfully {
|
||||
dispatch_async(_responseQueue, ^{
|
||||
if (!successfully) {
|
||||
[_responseDelegate didFinishRecognitionWithError:task.error];
|
||||
} else {
|
||||
[_responseDelegate didFinishRecognitionWithError:nil];
|
||||
if (_responseDelegate && [_responseDelegate respondsToSelector:@selector(didFinishRecognitionWithError:)]) {
|
||||
[_responseDelegate didFinishRecognitionWithError:successfully ? nil : task.error];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -158,7 +179,10 @@
|
||||
- (void)speechRecognitionTaskWasCancelled:(SFSpeechRecognitionTask *)task {
|
||||
dispatch_async(_responseQueue, ^{
|
||||
ORK_Log_Debug("Request cancelled");
|
||||
[_responseDelegate didFinishRecognitionWithError:nil];
|
||||
|
||||
if (_responseDelegate && [_responseDelegate respondsToSelector:@selector(didFinishRecognitionWithError:)]) {
|
||||
[_responseDelegate didFinishRecognitionWithError:nil];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -239,7 +239,6 @@
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation ORKStreamingAudioRecorderConfiguration
|
||||
|
||||
#pragma clang diagnostic push
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
result -> _color = [self.color copy];
|
||||
result -> _text = [self.text copy];
|
||||
result -> _colorSelected = [self.colorSelected copy];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
NSString *_yellowString;
|
||||
NSTimer *_nextQuestionTimer;
|
||||
|
||||
NSTimer *_timeoutTimer;
|
||||
|
||||
NSMutableArray *_results;
|
||||
NSTimeInterval _startTime;
|
||||
NSTimeInterval _endTime;
|
||||
@@ -114,21 +112,6 @@
|
||||
|
||||
self.questionNumber = 0;
|
||||
_stroopContentView = [ORKStroopContentView new];
|
||||
[_stroopContentView setUseTextForStimuli: [self stroopStep].useTextForStimuli];
|
||||
[_stroopContentView setUseGridLayoutForButtons: [self stroopStep].useGridLayoutForButtons];
|
||||
|
||||
if ([self stroopStep].useGridLayoutForButtons) {
|
||||
[_navigationFooterView setHidden:true];
|
||||
|
||||
_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:30
|
||||
target:self
|
||||
selector:@selector(timeOut)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
|
||||
|
||||
|
||||
self.activeStepView.activeCustomView = _stroopContentView;
|
||||
|
||||
[self.stroopContentView.RButton addTarget:self
|
||||
@@ -166,37 +149,9 @@
|
||||
selector:@selector(startNextQuestionOrFinish)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
|
||||
if ([self stroopStep].useGridLayoutForButtons) {
|
||||
|
||||
[_timeoutTimer invalidate];
|
||||
_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:30
|
||||
target:self
|
||||
selector:@selector(timeOut)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)timeOut {
|
||||
|
||||
UIAlertController* controller = [UIAlertController alertControllerWithTitle: ORKLocalizedString(@"TIME_OUT_TILE", nil)
|
||||
message: ORKLocalizedString(@"TIME_OUT_BODY", nil) preferredStyle:UIAlertControllerStyleAlert];
|
||||
[controller addAction:[UIAlertAction actionWithTitle: ORKLocalizedString(@"TIME_OUT_RESTART_ACTION", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
||||
[[self taskViewController] flipToFirstPage];
|
||||
}]];
|
||||
|
||||
[controller addAction:[UIAlertAction actionWithTitle: ORKLocalizedString(@"TIME_OUT_END_ACTION", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
|
||||
ORKStrongTypeOf(self.taskViewController.delegate) strongDelegate = self.taskViewController.delegate;
|
||||
if ([strongDelegate respondsToSelector:@selector(taskViewController:didFinishWithReason:error:)]) {
|
||||
[strongDelegate taskViewController:self.taskViewController didFinishWithReason:ORKTaskViewControllerFinishReasonDiscarded error:nil];
|
||||
}
|
||||
}]];
|
||||
|
||||
[self presentViewController:controller animated:true completion:nil];
|
||||
}
|
||||
|
||||
- (void)startNextQuestionTimer {
|
||||
_nextQuestionTimer = [NSTimer scheduledTimerWithTimeInterval:0.3
|
||||
target:self
|
||||
|
||||
@@ -60,7 +60,7 @@ public class ORKSwiftStroopResult: ORKResult {
|
||||
aCoder.encode(colorSelected, forKey: Keys.colorSelected.rawValue)
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
public required init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
startTime = aDecoder.decodeObject(forKey: Keys.startTime.rawValue) as? Double
|
||||
endTime = aDecoder.decodeObject(forKey: Keys.endTime.rawValue) as? Double
|
||||
|
||||
@@ -98,13 +98,21 @@ public class ORKSwiftStroopStepViewController: ORKActiveStepViewController {
|
||||
if let button = sender as? ORKBorderedButton {
|
||||
|
||||
if button == stroopContentView.redButton {
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first as? String ?? "", withText: stroopContentView.colorLabelText!, withColorSelected: redString)
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first
|
||||
as? String ?? "", withText: stroopContentView.colorLabelText!,
|
||||
withColorSelected: redString)
|
||||
} else if button == stroopContentView.greenButton {
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first as? String ?? "", withText: stroopContentView.colorLabelText!, withColorSelected: greenString)
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first
|
||||
as? String ?? "", withText: stroopContentView.colorLabelText!,
|
||||
withColorSelected: greenString)
|
||||
} else if button == stroopContentView.blueButton {
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first as? String ?? "", withText: stroopContentView.colorLabelText!, withColorSelected: blueString)
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first
|
||||
as? String ?? "", withText: stroopContentView.colorLabelText!,
|
||||
withColorSelected: blueString)
|
||||
} else if button == stroopContentView.yellowButton {
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first as? String ?? "", withText: stroopContentView.colorLabelText!, withColorSelected: yellowString)
|
||||
createResult(color: (colors as NSDictionary).allKeys(for: stroopContentView.colorLabelColor!).first
|
||||
as? String ?? "", withText: stroopContentView.colorLabelText!,
|
||||
withColorSelected: yellowString)
|
||||
}
|
||||
|
||||
nextQuestionTimer = Timer.scheduledTimer(timeInterval: 0.5,
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
- (id)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ(aDecoder, outputVolume);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, outputVolume, NSNumber);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, samples, ORKToneAudiometrySample);
|
||||
}
|
||||
return self;
|
||||
|
||||
@@ -65,10 +65,10 @@
|
||||
if (self) {
|
||||
ORK_DECODE_DOUBLE(aDecoder, timestamp);
|
||||
ORK_DECODE_ENUM(aDecoder, state);
|
||||
ORK_DECODE_OBJ(aDecoder, allowedTouchTypes);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, allowedTouchTypes, NSNumber);
|
||||
ORK_DECODE_CGPOINT(aDecoder, locationInWindow);
|
||||
ORK_DECODE_INTEGER(aDecoder, numberOfTouches);
|
||||
ORK_DECODE_OBJ(aDecoder, locationInWindowOfTouchAtIndex);
|
||||
ORK_DECODE_OBJ_MUTABLE_DICTIONARY(aDecoder, locationInWindowOfTouchAtIndex, NSNumber, NSValue);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKTouchAbilityTrial.h"
|
||||
#import "ORKTouchAbilityTapTrial.h"
|
||||
|
||||
@implementation ORKTouchAbilityLongPressResult
|
||||
|
||||
@@ -47,7 +48,7 @@
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
if (self = [super initWithCoder:aDecoder]) {
|
||||
ORK_DECODE_OBJ(aDecoder, trials);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, trials, ORKTouchAbilityTapTrial);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKTouchAbilityTrial.h"
|
||||
#import "ORKTouchAbilityTapTrial.h"
|
||||
|
||||
@implementation ORKTouchAbilityTapResult
|
||||
|
||||
@@ -47,7 +48,7 @@
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
if (self = [super initWithCoder:aDecoder]) {
|
||||
ORK_DECODE_OBJ(aDecoder, trials);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, trials, ORKTouchAbilityTapTrial);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
self.azimuthUnitVectorInWindow = [aDecoder decodeCGVectorForKey:@ORK_STRINGIFY(azimuthUnitVectorInWindow)];
|
||||
|
||||
ORK_DECODE_DOUBLE(aDecoder, altitudeAngle);
|
||||
ORK_DECODE_OBJ(aDecoder, estimationUpdateIndex);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, estimationUpdateIndex, NSNumber);
|
||||
ORK_DECODE_INTEGER(aDecoder, estimatedProperties);
|
||||
ORK_DECODE_INTEGER(aDecoder, estimatedPropertiesExpectingUpdates);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ(aDecoder, touches);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, touches, ORKTouchAbilityTouch);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
#import "ORKTouchAbilityTrial_Internal.h"
|
||||
#import "ORKTouchAbilityTrack.h"
|
||||
#import "ORKTouchAbilityGestureRecoginzerEvent.h"
|
||||
#import "ORKTouchAbilityTapTrial.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
|
||||
@implementation ORKTouchAbilityTrial
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
@@ -53,10 +53,10 @@
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ(aDecoder, startDate);
|
||||
ORK_DECODE_OBJ(aDecoder, endDate);
|
||||
ORK_DECODE_OBJ(aDecoder, tracks);
|
||||
ORK_DECODE_OBJ(aDecoder, gestureRecognizerEvents);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, startDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, endDate, NSDate);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, tracks, ORKTouchAbilityTapTrial);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, gestureRecognizerEvents, ORKTouchAbilityGestureRecoginzerEvent);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ(aDecoder, trailType);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, trailType, NSString);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -194,13 +194,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<ORKResult *> *)provideResults {
|
||||
- (NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier {
|
||||
SCNNode *currentSelectedNode = [_usdzModelManagerScene currentSelectedNode];
|
||||
|
||||
ORKUSDZModelManagerResult *result = [ORKUSDZModelManagerResult new];
|
||||
ORKUSDZModelManagerResult *result = [[ORKUSDZModelManagerResult alloc] initWithIdentifier:identifier];
|
||||
result.identifierOfObjectSelectedAtClose = currentSelectedNode.name;
|
||||
result.identifiersOfSelectedObjects = [_usdzModelManagerScene selectedNodeIdentifierHistory];
|
||||
|
||||
return @[result];
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user