Compare commits

...

4 Commits

Author SHA1 Message Date
Louie c8473017ec Merge pull request #1531 from ResearchKit/cocoapod_fix
remove deprecated ORKConsent files
2022-11-29 13:21:51 -08:00
Louis Chatta 2bc0033aa8 remove deprecated ORKConsent files 2022-11-29 11:51:42 -08:00
Pariece McKinney a89059f5dc 2.1.1 Release
-Bug fixes
-New ORKAccuracyStroopStep
-AudioStep now allows for audio recording 
-New Styles for ORKDontKnowButton
-General layout and UI improvements
2022-10-26 16:07:45 -07:00
Will 2cc8f9e7d5 Prevent isPasscodeStoredInKeychain crash, correct warnings (#1505)
* fix: prevent isPasscodeStoredInKeychain from causing a crash

* fix: prevent nskeyedunarchiver warnings, add missing classes
2022-06-16 10:55:09 -07:00
543 changed files with 13804 additions and 9593 deletions
-5
View File
@@ -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
-6
View File
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj">
</FileRef>
<FileRef
location = "group:ResearchKit.xcodeproj">
</FileRef>
@@ -13,7 +10,4 @@
<FileRef
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
</FileRef>
<FileRef
location = "group:samples/ORKSample/ORKSample.xcodeproj">
</FileRef>
</Workspace>
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'ResearchKit'
s.version = '2.1.0-beta'
s.version = '2.1.0'
s.summary = 'ResearchKit is an open source software framework that makes it easy to create apps for medical research or for other research projects.'
s.homepage = 'https://www.github.com/ResearchKit/ResearchKit'
s.documentation_url = 'http://researchkit.github.io/docs/'
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
version = "1.3">
LastUpgradeVersion = "1200"
LastUpgradeVersion = "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"
+24
View File
@@ -0,0 +1,24 @@
{
"configurations" : [
{
"id" : "A9C8689E-BBEE-4643-8C5D-DBA6A5AF01B2",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:ResearchKit.xcodeproj",
"identifier" : "86CC8E991AC09332001CCD89",
"name" : "ResearchKitTests"
}
}
],
"version" : 1
}
@@ -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]
+1 -1
View File
@@ -57,7 +57,7 @@ This method signifies that the step is about to end so any necessary clean up be
You can also optionally pass back an array of ORKResults.
*/
- (nullable NSArray<ORKResult *> *)provideResults;
- (nullable NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier;
@end
+2 -2
View File
@@ -42,7 +42,7 @@ NSNotificationName const ORK3DModelEndStepNotification = @"ORK3DModelEndStepNoti
self = [super init];
if (self) {
_allowsSelection = YES;
_allowsSelection = NO;
_highlightColor = [UIColor yellowColor];
_identifiersOfObjectsToHighlight = nil;
}
@@ -117,7 +117,7 @@ NSNotificationName const ORK3DModelEndStepNotification = @"ORK3DModelEndStepNoti
[NSException raise:@"stepWillEnd not overwitten" format:@"Subclasses must overwrite the stepWillEnd function"];
}
- (NSArray<ORKResult *> *)provideResults {
- (NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier {
[NSException raise:@"provideResults not overwitten" format:@"Subclasses must overwrite the provideResults function"];
return nil;
}
+4 -3
View File
@@ -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
@@ -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
@@ -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
+3 -49
View File
@@ -30,30 +30,26 @@
#import "ORKActiveStepTimer.h"
#import "ORKHelpers_Internal.h"
@import UIKit;
#include <mach/mach.h>
#include <mach/mach_time.h>
static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
static mach_timebase_info_data_t sTimebaseInfo;
if ( sTimebaseInfo.denom == 0 ) {
(void) mach_timebase_info(&sTimebaseInfo);
static mach_timebase_info_data_t sTimebaseInfo;
if (sTimebaseInfo.denom == 0) {
(void)mach_timebase_info(&sTimebaseInfo);
}
uint64_t elapsedNano = delta * sTimebaseInfo.numer / sTimebaseInfo.denom;
return elapsedNano * 1.0 / NSEC_PER_SEC;
}
@implementation ORKActiveStepTimer {
uint64_t _startTime;
NSTimeInterval _preExistingRuntime;
dispatch_queue_t _queue;
dispatch_source_t _timer;
UIBackgroundTaskIdentifier _backgroundTaskIdentifier;
uint32_t _isRunning;
}
@@ -68,7 +64,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
_interval = interval;
_handler = [handler copy];
_preExistingRuntime = runtime;
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
_queue = dispatch_queue_create("active_step", DISPATCH_QUEUE_SERIAL);
@@ -128,7 +123,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
}
- (void)queue_event {
[self queue_assertBackgroundTask];
NSTimeInterval runtime = [self queue_runtime];
BOOL finished = (runtime >= _duration);
@@ -138,13 +132,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
dispatch_async(dispatch_get_main_queue(), ^{
_handler(self, finished);
dispatch_sync(_queue, ^{
// If the timer is still NULL here, we can safely release the background task.
if (_timer == NULL) {
[self queue_releaseBackgroundTask];
}
});
});
}
@@ -156,29 +143,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
}
}
- (void)queue_releaseBackgroundTask {
if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
return;
}
UIBackgroundTaskIdentifier identifier = _backgroundTaskIdentifier;
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] endBackgroundTask:identifier];
});
}
- (void)queue_assertBackgroundTask {
if (_backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
return;
}
_backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
// This is guaranteed to be called synchronously on the main queue, switch to our queue to invalidate the identifier
dispatch_sync(_queue, ^{
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
});
}];
}
- (void)queue_resume {
if (_timer != NULL) {
// Already resumed
@@ -190,11 +154,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
return;
}
// We want to run in the background if we can, so voice can be played, etc.
assert(_backgroundTaskIdentifier == UIBackgroundTaskInvalid);
[self queue_assertBackgroundTask];
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
if (_timer == NULL) {
@@ -228,16 +187,11 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
_preExistingRuntime += timeIntervalFromMachTime(now - _startTime);
_startTime = 0;
if (!atFinish) {
// If we are atFinish, the task will be released after the handler completes
[self queue_releaseBackgroundTask];
}
}
- (void)queue_reset {
[self queue_clearTimer];
_preExistingRuntime = 0;
[self queue_releaseBackgroundTask];
}
@end
@@ -35,6 +35,15 @@
NS_ASSUME_NONNULL_BEGIN
@class ORKRecordButton;
typedef NS_ENUM(NSUInteger, ORKAudioContentViewEvent) {
ORKAudioContentViewEventStartRecording = 0,
ORKAudioContentViewEventStopRecording
};
typedef void (^ORKAudioStepContentViewEventHandler)(ORKAudioContentViewEvent);
@interface ORKAudioContentView : ORKActiveStepCustomView
@property (nonatomic, copy, nullable) UIColor *keyColor;
@@ -48,6 +57,10 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy, nullable) NSArray *samples;
@property (nonatomic) BOOL useRecordButton;
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler;
// Samples should be in the range of (0, 1).
- (void)addSample:(NSNumber *)sample;
- (void)removeAllSamples;
@@ -55,3 +68,4 @@ NS_ASSUME_NONNULL_BEGIN
@end
NS_ASSUME_NONNULL_END
+90 -16
View File
@@ -29,8 +29,9 @@
*/
#import "ORKAudioContentView.h"
#import "ORKAudioGraphView.h"
#import "ORKAudioMeteringView.h"
#import "ORKHeadlineLabel.h"
#import "ORKLabel.h"
@@ -38,6 +39,7 @@
#import "ORKAccessibility.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKRecordButton.h"
// The central blue region.
@@ -46,6 +48,8 @@ static const CGFloat GraphViewBlueZoneHeight = 170;
// The two bands at top and bottom which are "loud" each have this height.
static const CGFloat GraphViewRedZoneHeight = 25;
static const CGFloat ORKAudioStepContentRecordButtonVerticalSpacing = 20.0;
@interface ORKAudioTimerLabel : ORKLabel
@end
@@ -61,11 +65,12 @@ static const CGFloat GraphViewRedZoneHeight = 25;
@end
@interface ORKAudioContentView ()
@interface ORKAudioContentView () <ORKRecordButtonDelegate>
@property (nonatomic, strong) ORKHeadlineLabel *alertLabel;
@property (nonatomic, strong) UILabel *timerLabel;
@property (nonatomic, strong) ORKAudioGraphView *graphView;
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
@property (nonatomic, copy, nullable) ORKAudioStepContentViewEventHandler viewEventhandler;
@end
@@ -73,19 +78,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
@implementation ORKAudioContentView {
NSMutableArray *_samples;
UIColor *_keyColor;
ORKRecordButton *_recordButton;
BOOL _checkAudioLevel;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
_checkAudioLevel = YES;
_useRecordButton = NO;
self.alertLabel = [ORKHeadlineLabel new];
_alertLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.timerLabel = [ORKAudioTimerLabel new];
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
_timerLabel.textAlignment = NSTextAlignmentRight;
self.graphView = [ORKAudioGraphView new];
self.graphView = [[ORKAudioMeteringView alloc] init];
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
@@ -122,10 +131,22 @@ static const CGFloat GraphViewRedZoneHeight = 25;
[self updateAlertLabelHidden];
}
- (void)setUseRecordButton:(BOOL)useRecordButton {
_useRecordButton = useRecordButton;
if (_useRecordButton) {
_checkAudioLevel = NO;
[_timerLabel setHidden: YES];
[self setupRecordButton];
[self setUpConstraints];
}
}
- (void)applyKeyColor {
UIColor *keyColor = [self keyColor];
_timerLabel.textColor = keyColor;
_graphView.keyColor = keyColor;
_graphView.meterColor = keyColor;
}
- (UIColor *)keyColor {
@@ -143,6 +164,44 @@ static const CGFloat GraphViewRedZoneHeight = 25;
_graphView.alertColor = alertColor;
}
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler {
self.viewEventhandler = [handler copy];
}
- (void)invokeViewEventHandlerWithEvent:(ORKAudioContentViewEvent)event {
if (self.viewEventhandler) {
dispatch_async(dispatch_get_main_queue(), ^{
self.viewEventhandler(event);
});
}
}
- (void)buttonPressed:(ORKRecordButton *)recordButton {
switch (recordButton.buttonType) {
case ORKRecordButtonTypeRecord:
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStartRecording];
[_recordButton setButtonType:ORKRecordButtonTypeStop];
break;
default:
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStopRecording];
[_recordButton setButtonState:ORKRecordButtonStateDisabled];
break;
}
}
- (void)setupRecordButton {
if (!_recordButton) {
_recordButton = [[ORKRecordButton alloc] init];
_recordButton.delegate = self;
_recordButton.translatesAutoresizingMaskIntoConstraints = NO;
[_recordButton setButtonType:ORKRecordButtonTypeRecord];
[self addSubview:_recordButton];
}
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
@@ -161,12 +220,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
const CGFloat innerMargin = 2;
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
views:views]];
if (_useRecordButton) {
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin)}
views:views]];
[constraints addObject:[_recordButton.topAnchor constraintEqualToAnchor:_graphView.bottomAnchor constant:ORKAudioStepContentRecordButtonVerticalSpacing]];
[constraints addObject:[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor]];
} else {
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
options:NSLayoutFormatAlignAllCenterY
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
views:views]];
}
[constraints addObject:[NSLayoutConstraint constraintWithItem:_graphView
attribute:NSLayoutAttributeHeight
@@ -206,18 +276,21 @@ static const CGFloat GraphViewRedZoneHeight = 25;
}
- (void)updateGraphSamples {
_graphView.values = _samples;
_graphView.samples = _samples;
[self updateAlertLabelHidden];
}
- (void)updateAlertLabelHidden {
NSNumber *sample = _samples.lastObject;
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
if (_alertLabel.hidden && show) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
if (_checkAudioLevel) {
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
if (_alertLabel.hidden && show) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
}
_alertLabel.hidden = !show;
}
_alertLabel.hidden = !show;
}
- (void)setSamples:(NSArray *)samples {
@@ -260,3 +333,4 @@ static const CGFloat GraphViewRedZoneHeight = 25;
}
@end
@@ -0,0 +1,64 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <ResearchKit/ResearchKit.h>
#import <ResearchKit/ORKFitnessStep.h>
NS_ASSUME_NONNULL_BEGIN
@class ORKBundleAsset;
ORK_CLASS_AVAILABLE
@interface ORKVocalCue : NSObject <NSSecureCoding, NSCopying>
@property (atomic) NSTimeInterval time;
@property (atomic, copy) NSString *spokenText;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithTime:(NSTimeInterval) time
spokenText:(NSString *) spokenText;
@end
ORK_CLASS_AVAILABLE
@interface ORKAudioFitnessStep : ORKFitnessStep
@property (nonatomic, copy) ORKBundleAsset *audioAsset;
@property (nonatomic, copy) NSArray<ORKVocalCue *> *vocalCues;
- (instancetype)initWithIdentifier:(NSString *) identifier
audioAsset:(ORKBundleAsset *) audioAsset
vocalCues:(nullable NSArray<ORKVocalCue *> *) vocalCues;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,146 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKAudioFitnessStep.h"
#import "ORKAudioFitnessStepViewController.h"
#import "ORKBundleAsset.h"
#import "ORKHelpers_Internal.h"
@implementation ORKVocalCue
- (instancetype)initWithTime:(NSTimeInterval)time
spokenText:(NSString *)spokenText {
self = [super init];
if (self) {
self.time = time;
self.spokenText = [spokenText copy];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
if (self) {
ORK_DECODE_DOUBLE(coder, time);
ORK_DECODE_OBJ_CLASS(coder, spokenText, NSString);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
ORK_ENCODE_DOUBLE(coder, time);
ORK_ENCODE_OBJ(coder, spokenText);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
return [[ORKVocalCue alloc] initWithTime:self.time spokenText:self.spokenText];
}
- (BOOL)isEqual:(id)other
{
if ([self class] != [other class]) {
return NO;
}
__typeof(self) castObject = other;
return (self.time == castObject.time &&
ORKEqualObjects(self.spokenText, castObject.spokenText));
}
@end
@implementation ORKAudioFitnessStep
- (Class)stepViewControllerClass {
return [ORKAudioFitnessStepViewController class];
}
- (instancetype)initWithIdentifier:(NSString *)identifier
audioAsset:(ORKBundleAsset *)audioAsset
vocalCues:(nullable NSArray<ORKVocalCue *> *)vocalCues {
self = [super initWithIdentifier:identifier];
if (self) {
self.stepDuration = 180;
self.shouldShowDefaultTimer = NO;
self.audioAsset = [audioAsset copy];
self.vocalCues = vocalCues == nil ? [NSArray new] : [vocalCues copy];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
ORK_DECODE_OBJ_CLASS(coder, audioAsset, ORKBundleAsset);
ORK_DECODE_OBJ_ARRAY(coder, vocalCues, ORKVocalCue);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
ORK_ENCODE_OBJ(coder, audioAsset);
ORK_ENCODE_OBJ(coder, vocalCues);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAudioFitnessStep *step = [super copyWithZone:zone];
step.audioAsset = [self.audioAsset copy];
step.vocalCues = [self.vocalCues copy];
return step;
}
- (BOOL)isEqual:(id)other
{
BOOL superIsEqual = [super isEqual:other];
__typeof(self) castObject = other;
return (superIsEqual &&
ORKEqualObjects(self.audioAsset, castObject.audioAsset) &&
ORKEqualObjects(self.vocalCues, castObject.vocalCues));
}
- (NSUInteger)hash
{
return super.hash ^ self.audioAsset.hash ^ self.vocalCues.hash;
}
@end
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Ricardo Sánchez-Sáez.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -28,25 +28,22 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKConsentSceneViewController.h"
#import "ORKStepContainerView.h"
#import <ResearchKit/ResearchKit.h>
#import <ResearchKit/ORKFitnessStepViewController.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKConsentSceneView : ORKStepContainerView
// Test Seam
@protocol ORKAudioPlayer
- (BOOL)prepareToPlay;
- (BOOL)play;
- (void)pause;
- (void)stop;
@end
@interface ORKAudioFitnessStepViewController : ORKFitnessStepViewController
@interface ORKConsentSceneViewController ()
@property (nonatomic, readonly) ORKConsentSceneView *sceneView;
@property (nonatomic, readonly) UIScrollView *scrollView;
- (void)scrollToTopAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion;
@property (nonatomic) id<ORKAudioPlayer> audioPlayer;
@end
@@ -0,0 +1,150 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKActiveStepTimer.h"
#import "ORKAudioFitnessStep.h"
#import "ORKAudioFitnessStepViewController.h"
#import "ORKVoiceEngine.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKHelpers_Internal.h"
#import <AVFoundation/AVFoundation.h>
@interface ORKAVAudioPlayer : AVAudioPlayer <ORKAudioPlayer>
@end
@implementation ORKAVAudioPlayer
@end
@interface ORKAudioFitnessStepViewController ()
@property (nonatomic) BOOL appHasAudioBackgroundMode;
@property (nonatomic) NSMutableSet<ORKVocalCue *> *playedCues;
@end
@implementation ORKAudioFitnessStepViewController
- (ORKAudioFitnessStep *)audioStep {
return (ORKAudioFitnessStep *)self.step;
}
- (NSMutableSet *)playedCues {
if (!_playedCues) {
_playedCues = [NSMutableSet new];
}
return _playedCues;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.audioPlayer prepareToPlay];
}
- (void)start {
[super start];
if (self.appHasAudioBackgroundMode) {
[self enableBackgroundAudioSession:YES];
}
[self.audioPlayer play];
}
- (void)suspend {
[super suspend];
[self.audioPlayer pause];
}
- (void)resume {
[super resume];
[self.audioPlayer play];
}
- (void)finish {
[super finish];
[self.audioPlayer stop];
if (self.appHasAudioBackgroundMode) {
[self enableBackgroundAudioSession:NO];
}
}
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
[super countDownTimerFired:timer finished:finished];
ORKVoiceEngine *voice = [ORKVoiceEngine sharedVoiceEngine];
NSTimeInterval timeRemaining = [timer duration] - [timer runtime];
for (ORKVocalCue *cue in [self audioStep].vocalCues) {
if (cue.time >= timeRemaining && ![self.playedCues containsObject:cue]) {
[self.playedCues addObject:cue];
[voice speakText: cue.spokenText];
}
}
}
- (BOOL)appHasAudioBackgroundMode {
NSArray<NSString *> *backgroundModes = (NSArray<NSString *> *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
BOOL hasBackgroundAudioMode = [backgroundModes containsObject:@"audio"];
return hasBackgroundAudioMode;
}
- (void)enableBackgroundAudioSession:(BOOL)enabled {
NSError *error;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
mode:AVAudioSessionModeDefault
routeSharingPolicy:AVAudioSessionRouteSharingPolicyLongFormAudio
options:0
error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessStepViewController failed to setup audio session: %@", error);
return;
}
[[AVAudioSession sharedInstance] setActive:enabled error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessViewController failed to start audio session: %@", error);
return;
}
}
- (id<ORKAudioPlayer>)audioPlayer {
if (!_audioPlayer) {
ORKAudioFitnessStep *step = [self audioStep];
NSError *error;
_audioPlayer = [[ORKAVAudioPlayer alloc] initWithContentsOfURL:step.audioAsset.url error:&error];
if (error) {
ORK_Log_Error("ORKAudioFitnessStepViewController Failed to load audio file: %@", error.localizedFailureReason);
}
}
return _audioPlayer;
}
@end
+4 -9
View File
@@ -28,20 +28,15 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import UIKit;
#import <ResearchKit/ORKAudioMeteringView.h>
NS_ASSUME_NONNULL_BEGIN
@interface ORKAudioGraphView : UIView
@property (nonatomic, strong) UIColor *keyColor;
@property (nonatomic, strong) UIColor *alertColor;
@property (nonatomic, copy) NSArray *values;
@property (nonatomic) CGFloat alertThreshold;
@interface ORKAudioGraphView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
@end
NS_ASSUME_NONNULL_END
+46 -26
View File
@@ -32,11 +32,21 @@
#import "ORKAudioGraphView.h"
#import "ORKSkin.h"
static const CGFloat ValueLineWidth = 4.5;
static const CGFloat ValueLineMargin = 1.5;
static const CGFloat GraphHeight = 150.0;
@interface ORKAudioGraphView ()
/// ORKAudioMetering
@property (nonatomic, copy, nullable) NSArray<NSNumber *> *samples;
@property (nonatomic, assign) float alertThreshold;
/// ORKAudioMeteringView
@property (nonatomic, strong) UIColor *meterColor;
@property (nonatomic, strong, nullable) UIColor *alertColor;
@end
@implementation ORKAudioGraphView
@@ -46,7 +56,7 @@ static const CGFloat GraphHeight = 150.0;
[self setUpConstraints];
#if TARGET_IPHONE_SIMULATOR
_values = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
_samples = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
#endif
}
return self;
@@ -65,26 +75,6 @@ static const CGFloat GraphHeight = 150.0;
[NSLayoutConstraint activateConstraints:@[heightConstraint]];
}
- (void)setValues:(NSArray *)values {
_values = [values copy];
[self setNeedsDisplay];
}
- (void)setKeyColor:(UIColor *)keyColor {
_keyColor = [keyColor copy];
[self setNeedsDisplay];
}
- (void)setAlertColor:(UIColor *)alertColor {
_alertColor = [alertColor copy];
[self setNeedsDisplay];
}
- (void)setAlertThreshold:(CGFloat)alertThreshold {
_alertThreshold = alertThreshold;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
@@ -104,7 +94,7 @@ static const CGFloat GraphHeight = 150.0;
[centerLine addLineToPoint:(CGPoint){.x = maxX, .y = midY}];
CGContextSetLineWidth(context, 1.0 / scale);
[_keyColor setStroke];
[_meterColor setStroke];
CGFloat lengths[2] = {3, 3};
CGContextSetLineDash(context, 0, lengths, 2);
@@ -125,7 +115,7 @@ static const CGFloat GraphHeight = 150.0;
path1.lineWidth = ValueLineWidth;
UIBezierPath *path2 = [path1 copy];
for (NSNumber *value in [_values reverseObjectEnumerator]) {
for (NSNumber *value in [_samples reverseObjectEnumerator]) {
CGFloat floatValue = value.doubleValue;
UIBezierPath *path = nil;
@@ -134,7 +124,7 @@ static const CGFloat GraphHeight = 150.0;
[_alertColor setStroke];
} else {
path = path2;
[_keyColor setStroke];
[_meterColor setStroke];
}
[path moveToPoint:(CGPoint){.x = x, .y = midY - floatValue*halfHeight}];
[path addLineToPoint:(CGPoint){.x = x, .y = midY + floatValue*halfHeight}];
@@ -150,11 +140,41 @@ static const CGFloat GraphHeight = 150.0;
[_alertColor setStroke];
[path1 stroke];
[_keyColor setStroke];
[_meterColor setStroke];
[path2 stroke];
}
CGContextRestoreGState(context);
}
#pragma mark - ORKAudioMetering
- (void)setSamples:(NSArray<NSNumber *> *)samples
{
_samples = [samples copy];
[self setNeedsDisplay];
}
- (void)setAlertThreshold:(float)threshold
{
_alertThreshold = threshold;
[self setNeedsDisplay];
}
#pragma mark = ORKAudioMeteringView
- (void)setMeterColor:(UIColor *)meterColor
{
_meterColor = [meterColor copy];
[self setNeedsDisplay];
}
- (void)setAlertColor:(UIColor *)alertColor
{
_alertColor = [alertColor copy];
[self setNeedsDisplay];
}
@end
@@ -75,3 +75,4 @@ ORK_CLASS_AVAILABLE
@end
NS_ASSUME_NONNULL_END
@@ -216,3 +216,4 @@ Float32 const VolumeClamp = 60.0;
@end
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -29,26 +29,30 @@
*/
@import UIKit;
@import UIKit;
NS_ASSUME_NONNULL_BEGIN
@class ORKConsentSection;
extern NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit);
@interface ORKConsentSceneViewController : UIViewController
@protocol ORKAudioMetering <NSObject>
- (instancetype)initWithSection:(ORKConsentSection *)section;
- (void)setSamples:(nullable NSArray<NSNumber *> *)samples;
@property (nonatomic, readonly, nullable) ORKConsentSection *section;
- (void)setAlertThreshold:(float)threshold;
@property (nonatomic, strong, nullable) UIBarButtonItem *continueButtonItem;
@end
@property (nonatomic, strong, nullable) UIBarButtonItem *cancelButtonItem;
@protocol ORKAudioMeteringDisplay
@property (nonatomic, strong, nullable) NSString *learnMoreButtonTitle;
- (void)setMeterColor:(nonnull UIColor *)meterColor;
@property (nonatomic, assign) BOOL imageHidden;
- (void)setAlertColor:(nonnull UIColor *)alertColor;
@end
@interface ORKAudioMeteringView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
@end
@@ -0,0 +1,146 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKAudioMeteringView.h"
#import "ORKAudioGraphView.h"
NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit) {
if (samples.count > limit) {
return [samples subarrayWithRange:(NSRange){samples.count - limit, samples.count - 1}];
}
return [samples copy];
}
@interface ORKAudioMeteringView ()
@property (nonatomic, strong) UIView<ORKAudioMetering, ORKAudioMeteringDisplay> *meteringView;
@end
@implementation ORKAudioMeteringView
- (instancetype)init
{
self = [super init];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self)
{
[self configureMeteringView];
}
return self;
}
- (void)configureMeteringView
{
if (!_meteringView) {
[self setMeteringView:[[ORKAudioGraphView alloc] init]];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[_meteringView setFrame:[self bounds]];
}
- (void)setHidden:(BOOL)hidden
{
[super setHidden:hidden];
[_meteringView setHidden:hidden];
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if ([self superview] == nil)
{
[_meteringView removeFromSuperview];
}
else
{
[self addSubview:_meteringView];
}
}
#pragma mark - ORKAudioMetering
- (void)setSamples:(NSArray<NSNumber *> *)samples
{
[_meteringView setSamples:samples];
}
- (void)setAlertThreshold:(float)threshold
{
[_meteringView setAlertThreshold:threshold];
}
#pragma mark - ORKAudioMeteringDisplay
- (void)setMeterColor:(UIColor *)meterColor
{
[_meteringView setMeterColor:meterColor];
}
- (void)setAlertColor:(UIColor *)alertColor
{
[_meteringView setAlertColor:alertColor];
}
#pragma mark - UIAccessibility
- (BOOL)isAccessibilityElement {
return NO;
}
@end
+12
View File
@@ -38,6 +38,18 @@ NS_ASSUME_NONNULL_BEGIN
ORK_CLASS_AVAILABLE
@interface ORKAudioStep : ORKActiveStep
/**
A Boolean value that determines if audio recording will start and stop
automatcially or be controlled via a ORKRecordButton
When set to YES the user will be able to start and stop the audio recording
by the ORKRecordButton
The default value of this property is `NO`.
*/
@property (nonatomic) BOOL useRecordButton;
@end
NS_ASSUME_NONNULL_END
+43 -1
View File
@@ -49,16 +49,27 @@
if (self) {
self.shouldShowDefaultTimer = NO;
self.shouldStartTimerAutomatically = YES;
self.useRecordButton = NO;
}
return self;
}
- (void)setUseRecordButton:(BOOL)useRecordButton {
_useRecordButton = useRecordButton;
[self setShouldStartTimerAutomatically:!_useRecordButton];
if (_useRecordButton) {
self.stepDuration = 0;
}
}
- (void)validateParameters {
[super validateParameters];
NSTimeInterval const ORKAudioTaskMinimumDuration = 5.0;
if ( self.stepDuration < ORKAudioTaskMinimumDuration) {
if ( self.stepDuration < ORKAudioTaskMinimumDuration && !self.useRecordButton) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKAudioTaskMinimumDuration)] userInfo:nil];
}
}
@@ -67,4 +78,35 @@
return NO;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKAudioStep *step = [super copyWithZone:zone];
step.useRecordButton = self.useRecordButton;
return step;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_BOOL(aDecoder, useRecordButton);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_BOOL(aCoder, useRecordButton);
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame && self.useRecordButton == castObject.useRecordButton);
}
@end
@@ -29,6 +29,7 @@
*/
@import Foundation;
#import <ResearchKit/ORKDefines.h>
#import <ResearchKit/ORKActiveStepViewController.h>
@@ -44,3 +45,4 @@ ORK_CLASS_AVAILABLE
@end
NS_ASSUME_NONNULL_END
@@ -59,6 +59,7 @@
ORKAudioContentView *_audioContentView;
ORKAudioRecorder *_audioRecorder;
ORKActiveStepTimer *_timer;
NSTimer *_intervalTimer;
NSError *_audioRecorderError;
}
@@ -83,6 +84,12 @@
// Do any additional setup after loading the view.
_audioContentView = [ORKAudioContentView new];
_audioContentView.timeLeft = self.audioStep.stepDuration;
_audioContentView.useRecordButton = self.audioStep.useRecordButton && self.audioStep.stepDuration == 0;
__weak typeof(self) weakSelf = self;
[_audioContentView setViewEventHandler:^(ORKAudioContentViewEvent event) {
[weakSelf handleContentViewEvent:event];
}];
if (self.alertThreshold > 0) {
_audioContentView.alertThreshold = self.alertThreshold;
@@ -91,6 +98,19 @@
self.activeStepView.activeCustomView = _audioContentView;
}
- (void)handleContentViewEvent:(ORKAudioContentViewEvent)event {
switch (event) {
case ORKAudioContentViewEventStartRecording:
[self start];
break;
case ORKAudioContentViewEventStopRecording:
[self finish];
break;
}
}
- (void)audioRecorderDidChange {
_audioRecorder.audioRecorder.meteringEnabled = YES;
[self setAvAudioRecorder:_audioRecorder.audioRecorder];
@@ -116,42 +136,72 @@
if (_audioRecorderError) {
return;
}
[_avAudioRecorder updateMeters];
float value = [_avAudioRecorder averagePowerForChannel:0];
// Assume value is in range roughly -60dB to 0dB
float clampedValue = MAX(value / 60.0, -1) + 1;
[_audioContentView addSample:@(clampedValue)];
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
if (!self.audioStep.useRecordButton) {
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
}
}
- (void)startNewTimerIfNeeded {
if (!_timer) {
NSTimeInterval duration = self.audioStep.stepDuration;
ORKWeakTypeOf(self) weakSelf = self;
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
ORKStrongTypeOf(self) strongSelf = weakSelf;
[strongSelf doSample];
if (finished) {
[strongSelf finish];
}
}];
[_timer resume];
if (self.audioStep.useRecordButton) {
if (!_intervalTimer) {
_intervalTimer = [NSTimer scheduledTimerWithTimeInterval: 20 / 100
target:self selector:@selector(doSample)
userInfo:nil
repeats:YES];
}
} else {
if (!_timer) {
NSTimeInterval duration = self.audioStep.stepDuration;
ORKWeakTypeOf(self) weakSelf = self;
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
ORKStrongTypeOf(self) strongSelf = weakSelf;
[strongSelf doSample];
if (finished) {
[strongSelf finish];
}
}];
[_timer resume];
}
}
_audioContentView.finished = NO;
}
- (void)start {
[super start];
[self audioRecorderDidChange];
[_timer reset];
_timer = nil;
[self startNewTimerIfNeeded];
if (!self.audioStep.useRecordButton) {
[_timer reset];
_timer = nil;
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
[self startNewTimerIfNeeded];
}
- (void)suspend {
[super suspend];
[_timer pause];
if (!self.audioStep.useRecordButton) {
[_timer pause];
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
if (_avAudioRecorder) {
[_audioContentView addSample:@(0)];
}
@@ -160,8 +210,12 @@
- (void)resume {
[super resume];
[self audioRecorderDidChange];
[self startNewTimerIfNeeded];
[_timer resume];
if (!self.audioStep.useRecordButton) {
[_timer resume];
}
}
- (void)finish {
@@ -169,8 +223,14 @@
return;
}
[super finish];
[_timer reset];
_timer = nil;
if (!self.audioStep.useRecordButton) {
[_timer reset];
_timer = nil;
} else {
[_intervalTimer invalidate];
_intervalTimer = nil;
}
}
- (void)stepDidFinish {
@@ -189,3 +249,4 @@
}
@end
@@ -0,0 +1,53 @@
/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@import Foundation;
@import AVFoundation;
#import <ResearchKit/ORKRecorder.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ORKAudioStreamingDelegate <ORKRecorderDelegate>
- (void)audioAvailable:(AVAudioPCMBuffer *)buffer;
@end
@class ORKStep;
@interface ORKAudioStreamer : ORKRecorder
- (instancetype)initWithIdentifier:(NSString *)identifier step:(nullable ORKStep *)step NS_DESIGNATED_INITIALIZER;
@property (nonatomic, strong, readonly, nullable) AVAudioEngine *audioEngine;
@end
NS_ASSUME_NONNULL_END
+222
View File
@@ -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
+3
View File
@@ -75,6 +75,9 @@ ORK_CLASS_AVAILABLE
*/
@property (nonatomic, copy, nullable) NSURL *fileURL;
@property (nonatomic, copy, nullable) NSString *fileName;
@end
NS_ASSUME_NONNULL_END
+5 -1
View File
@@ -45,6 +45,7 @@
[super encodeWithCoder:aCoder];
ORK_ENCODE_URL(aCoder, fileURL);
ORK_ENCODE_OBJ(aCoder, contentType);
ORK_ENCODE_OBJ(aCoder, fileName);
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
@@ -52,6 +53,7 @@
if (self) {
ORK_DECODE_URL(aDecoder, fileURL);
ORK_DECODE_OBJ_CLASS(aDecoder, contentType, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, fileName, NSString);
}
return self;
}
@@ -66,7 +68,8 @@
__typeof(self) castObject = object;
return (isParentSame &&
ORKEqualFileURLs(self.fileURL, castObject.fileURL) &&
ORKEqualObjects(self.contentType, castObject.contentType));
ORKEqualObjects(self.contentType, castObject.contentType) &&
ORKEqualObjects(self.fileName, castObject.fileName));
}
- (NSUInteger)hash {
@@ -77,6 +80,7 @@
ORKFileResult *result = [super copyWithZone:zone];
result.fileURL = [self.fileURL copy];
result.contentType = [self.contentType copy];
result.fileName = [self.fileName copy];
return result;
}
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -34,19 +34,38 @@
NS_ASSUME_NONNULL_BEGIN
// Displays a countdown ring and a timer.
//
// ------------------------------
// | |
// | Title Label |
// | |
// | subtitle label |
// | |
// | __________ |
// | / \ |
// | | 2:30 | |
// | \ ________ / |
// | |
// |______________________________|
@interface ORKFitnessContentView : ORKActiveStepCustomView
@property (nonatomic, assign, getter=isFinished) BOOL finished;
@property (nonatomic) BOOL hasHeartRate;
@property (nonatomic) BOOL hasDistance;
/// The total amount of time the active task is supposed to be performed for.
/// For the six minute walk test, this will typically be 360 seconds.
@property (nonatomic) NSTimeInterval duration;
@property (nonatomic, copy, nullable) NSString *heartRate;
@property (nonatomic) double distanceInMeters;
/// The amount of time that still remain.
@property (nonatomic) NSTimeInterval timeLeft;
@property (nonatomic, strong, nullable) UIImage *image;
/// Whether or not the text label is hidden.
@property (nonatomic) BOOL labelHidden;
@property (nonatomic, assign) NSTimeInterval timeLeft;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithDuration:(NSTimeInterval)duration;
@end
+86 -275
View File
@@ -1,5 +1,5 @@
/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -31,28 +31,8 @@
#import "ORKFitnessContentView.h"
#import "ORKActiveStepQuantityView.h"
#import "ORKTintedImageView.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
@import CoreMotion;
@import HealthKit;
// #define LAYOUT_TEST 1
// #define LAYOUT_DEBUG 1
@interface ORKFitnessContentView () {
ORKQuantityLabel *_timerLabel;
ORKQuantityPairView *_quantityPairView;
UIView *_imageSpacer1;
UIView *_imageSpacer2;
ORKTintedImageView *_imageView;
NSLengthFormatter *_lengthFormatter;
NSLayoutConstraint *_imageRatioConstraint;
NSLayoutConstraint *_topConstraint;
UILabel *_timerLabel;
}
@end
@@ -60,272 +40,72 @@
@implementation ORKFitnessContentView
- (ORKActiveStepQuantityView *)distanceView {
return _quantityPairView.leftView;
}
- (ORKActiveStepQuantityView *)heartRateView {
return _quantityPairView.rightView;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
- (instancetype)initWithDuration:(NSTimeInterval)duration {
self = [super init];
if (self) {
_timerLabel = [ORKQuantityLabel new];
_quantityPairView = [ORKQuantityPairView new];
_imageSpacer1 = [UIView new];
_imageSpacer1.translatesAutoresizingMaskIntoConstraints = NO;
_imageSpacer2 = [UIView new];
_imageSpacer2.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_imageSpacer1];
[self addSubview:_imageSpacer2];
[self heartRateView].image = [UIImage imageNamed:@"heart-fitness" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
[self updateLengthFormatter];
_imageView = [ORKTintedImageView new];
_imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView.shouldApplyTint = YES;
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
_quantityPairView.translatesAutoresizingMaskIntoConstraints = NO;
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
[self updateKeylineVisible];
_timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently;
_imageView.isAccessibilityElement = NO;
self.hasHeartRate = _hasHeartRate;
self.hasDistance = _hasDistance;
#if LAYOUT_TEST
self.timeLeft = 60 * 5;
self.hasHeartRate = YES;
self.hasDistance = YES;
self.distanceInMeters = 100;
self.heartRate = @"22";
#endif
#if LAYOUT_DEBUG
self.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
_quantityPairView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.2];
#endif
[self setDistanceInMeters:0];
[self heartRateView].title = ORKLocalizedString(@"FITNESS_HEARTRATE_TITLE", nil);
_duration = duration;
_timeLeft = duration;
_timerLabel = [[UILabel alloc] init];
_timerLabel.textAlignment = NSTextAlignmentCenter;
_timerLabel.font = [self labelFont];
_timerLabel.adjustsFontForContentSizeCategory = YES;
_timerLabel.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
[self addSubview:_quantityPairView];
[self addSubview:_imageView];
[self addSubview:_timerLabel];
[self setUpConstraints];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
[self tintColorDidChange];
[self updateTimerLabel];
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
- (void)tintColorDidChange {
[self setNeedsDisplay];
_timerLabel.textColor = self.tintColor;
}
- (void)updateLengthFormatter {
_lengthFormatter = [NSLengthFormatter new];
_lengthFormatter.numberFormatter.maximumFractionDigits = 1;
_lengthFormatter.numberFormatter.maximumSignificantDigits = 3;
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
_timerLabel.font = [self labelFont];
}
- (void)localeDidChange:(NSNotification *)notification {
[self updateLengthFormatter];
[self setDistanceInMeters:_distanceInMeters];
}
- (void)willMoveToWindow:(UIWindow *)newWindow {
[super willMoveToWindow:newWindow];
[self updateConstraintConstantsForWindow:newWindow];
}
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
const CGFloat CaptionBaselineToTimerTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToFitnessTimerTop, window);
const CGFloat CaptionBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
_topConstraint.constant = (CaptionBaselineToTimerTop - CaptionBaselineToStepViewTop);
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
NSDictionary *views = NSDictionaryOfVariableBindings(_timerLabel, _imageView, _quantityPairView, _imageSpacer1, _imageSpacer2);
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_timerLabel][_imageSpacer1(>=0)][_imageView]"
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:views]];
_topConstraint = [NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0];
[constraints addObject:_topConstraint];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_imageView][_imageSpacer2(>=0)][_quantityPairView]|"
options:(NSLayoutFormatOptions)0
metrics:nil
views:views]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer2
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_imageSpacer2
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0.0]];
NSLayoutConstraint *imageSpacerHeightConstraint = [NSLayoutConstraint constraintWithItem:_imageSpacer1
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:CGFLOAT_MIN];
imageSpacerHeightConstraint.priority = UILayoutPriorityDefaultLow - 1;
[constraints addObject:imageSpacerHeightConstraint];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_quantityPairView]|"
options:(NSLayoutFormatOptions)0
metrics:nil
views:views]];
NSLayoutConstraint *maxWidthConstraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:ORKScreenMetricMaxDimension];
maxWidthConstraint.priority = UILayoutPriorityRequired - 1;
[constraints addObject:maxWidthConstraint];
[NSLayoutConstraint activateConstraints:constraints];
[self updateConstraintConstantsForWindow:self.window];
}
- (void)setImage:(UIImage *)image {
_image = image;
_imageView.image = image;
_imageRatioConstraint.active = NO;
CGSize size = image.size;
if (size.width > 0 && size.height > 0) {
_imageRatioConstraint = [NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:_imageView
attribute:NSLayoutAttributeWidth
multiplier:size.height / size.width
constant:0.0];
_imageRatioConstraint.active = YES;
}
}
- (void)setHasDistance:(BOOL)hasDistance {
_hasDistance = hasDistance;
[self distanceView].enabled = _hasDistance;
[self updateKeylineVisible];
}
- (void)setHasHeartRate:(BOOL)hasHeartRate {
_hasHeartRate = hasHeartRate;
[self heartRateView].enabled = _hasHeartRate;
[self updateKeylineVisible];
}
- (void)setHeartRate:(NSString *)heartRate {
_heartRate = heartRate;
[self heartRateView].value = heartRate;
}
- (void)updateKeylineVisible {
[_quantityPairView setKeylineHidden:!(_hasDistance && _hasHeartRate)];
}
- (void)setDistanceInMeters:(double)distanceInMeters {
_distanceInMeters = distanceInMeters;
double displayDistance = _distanceInMeters;
NSString *distanceString = nil;
NSLengthFormatterUnit unit;
NSString *unitString = [_lengthFormatter unitStringFromMeters:displayDistance usedUnit:&unit];
switch (unit) {
case NSLengthFormatterUnitCentimeter:
case NSLengthFormatterUnitMillimeter:
unit = NSLengthFormatterUnitMeter;
// Force showing 0 meters if the distance is sufficiently short to be displayed in cm or mm
unitString = [_lengthFormatter unitStringFromValue:0 unit:NSLengthFormatterUnitMeter];
displayDistance = 0;
break;
default:
break;
}
// Use HealthKit to convert the unit, so we can use the number formatter directly.
HKUnit *hkUnit = [HKUnit unitFromLengthFormatterUnit:unit];
double conversionFactor = 1.0;
if ([hkUnit isNull] && (unit == NSLengthFormatterUnitYard)) {
hkUnit = [HKUnit footUnit];
conversionFactor = 1.0 / 3.0;
}
HKQuantity *quantity = [HKQuantity quantityWithUnit:[HKUnit meterUnit] doubleValue:displayDistance];
distanceString = [_lengthFormatter.numberFormatter stringFromNumber:@([quantity doubleValueForUnit:hkUnit]*conversionFactor)];
[self distanceView].title = [NSString localizedStringWithFormat:ORKLocalizedString(@"FITNESS_DISTANCE_TITLE_FORMAT", nil), unitString];
[self distanceView].value = distanceString;
- (void)setDuration:(NSTimeInterval)duration {
_duration = duration;
[self setNeedsDisplay];
}
- (void)setTimeLeft:(NSTimeInterval)timeLeft {
_timeLeft = timeLeft;
[self updateTimerLabel];
[self setNeedsDisplay];
}
- (BOOL)labelHidden {
return _timerLabel.isHidden;
}
- (void)setLabelHidden:(BOOL)labelHidden {
[_timerLabel setHidden:labelHidden];
}
- (UIFont*) labelFont {
UIFont* font = [UIFont preferredFontForTextStyle: UIFontTextStyleLargeTitle];
UIFontMetrics* metrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleLargeTitle];
if (@available(iOS 13, *)) {
UIFontDescriptor* round = [[font fontDescriptor] fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
UIFontDescriptor* weighted = [round fontDescriptorByAddingAttributes:@{
UIFontDescriptorTraitsAttribute: @{
UIFontWeightTrait: @1.5
}
}];
font = [UIFont fontWithDescriptor:weighted size:44];
}
UIFont* scaled = [metrics scaledFontForFont:font];
return scaled;
}
- (void)updateTimerLabel {
@@ -334,13 +114,44 @@
dispatch_once(&onceToken, ^{
formatter = [NSDateComponentsFormatter new];
formatter.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorDropLeading;
formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
});
NSString *labelString = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
_timerLabel.text = labelString;
_timerLabel.hidden = (labelString == nil);
_timerLabel.text = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
}
- (void)drawRect:(CGRect)rect {
// The ring should be be centered and fill 1/2 of the view's width
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat strokeWidth = 12;
CGFloat xCenter = self.bounds.size.width / 2;
CGFloat yCenter = self.bounds.size.height / 2;
CGFloat dimension = MIN(self.bounds.size.width, self.bounds.size.height);
CGFloat radius = 0.5 * (dimension * 0.5);
CGFloat percentFilled = _timeLeft / _duration;
CGFloat startAngle = -M_PI_2 - (percentFilled * 2 * M_PI);
CGFloat stopAngle = -M_PI_2;
bool clockwise = NO;
CGContextSetLineWidth(context, strokeWidth);
CGContextSetLineCap(context, kCGLineCapRound);
// Draw a circular track
if (@available(iOS 13.0, *)) {
[[UIColor systemGray5Color] setStroke];
} else {
[[UIColor lightGrayColor] setStroke];
}
CGContextAddArc(context, xCenter, yCenter, radius, 0, 2 * M_PI, clockwise ? 1 : 0);
CGContextStrokePath(context);
// Fill in the track based on progress
[self.tintColor setStroke];
CGContextAddArc(context, xCenter, yCenter, radius, startAngle, stopAngle, clockwise ? 1 : 0);
CGContextStrokePath(context);
}
@end
+2 -8
View File
@@ -36,17 +36,11 @@
NS_ASSUME_NONNULL_BEGIN
/**
Fitness step.
Displays usual header, a counting-up timer, read outs for distance and/or
heart rate if corresponding recorders are attached.
Also displays an image during the task.
*/
ORK_CLASS_AVAILABLE
@interface ORKFitnessStep : ORKActiveStep
@property (nonatomic, copy) NSDictionary *userInfo;
@end
NS_ASSUME_NONNULL_END
+42 -5
View File
@@ -30,6 +30,7 @@
#import "ORKFitnessStep.h"
#import "ORKHelpers_Internal.h"
#import "ORKFitnessStepViewController.h"
@@ -43,6 +44,7 @@
- (instancetype)initWithIdentifier:(NSString *)identifier {
self = [super initWithIdentifier:identifier];
if (self) {
self.userInfo = [[NSDictionary alloc] init];
self.shouldShowDefaultTimer = NO;
}
return self;
@@ -58,13 +60,48 @@
}
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKFitnessStep *step = [super copyWithZone:zone];
return step;
}
- (BOOL)startsFinished {
return NO;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
ORK_DECODE_OBJ_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
}
}
+9 -13
View File
@@ -71,8 +71,13 @@
return @"location";
}
// Test Seam - unit tests don't support background updates or pausing.
- (CLLocationManager *)createLocationManager {
return [[CLLocationManager alloc] init];
CLLocationManager *manager = [[CLLocationManager alloc] init];
manager.pausesLocationUpdatesAutomatically = NO;
manager.allowsBackgroundLocationUpdates = YES;
return manager;
}
- (void)start {
@@ -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 {
+1
View File
@@ -242,6 +242,7 @@
ORKFileResult *result = [[ORKFileResult alloc] initWithIdentifier:self.identifier];
result.contentType = [self mimeType];
result.fileURL = fileUrl;
result.fileName = [fileUrl lastPathComponent];
result.userInfo = self.userInfo;
result.startDate = self.startDate;
@@ -148,4 +148,14 @@ ORK_CLASS_AVAILABLE
@end
// A simple audio streaming configuration which does not save any audio. Only streams audio buffers.
ORK_CLASS_AVAILABLE
@interface ORKAudioStreamerConfiguration : ORKRecorderConfiguration
- (instancetype)initWithIdentifier:(NSString *)identifier NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END
@@ -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
@@ -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
@@ -41,6 +41,16 @@ ORK_CLASS_AVAILABLE
*/
@interface ORKSpeechInNoiseStep : ORKActiveStep
/**
This property accepts the speech file Path.
*/
@property (nonatomic, copy, nullable) NSString *speechFilePath;
/**
This property acceopts the string representation of the speech to be played.
*/
@property (nonatomic, copy, nullable) NSString *targetSentence;
/**
This property accepts the speech file.
*/
@@ -54,6 +54,8 @@
- (void)commonInit {
_willAudioLoop = NO;
_speechFilePath = nil;
_targetSentence = nil;
_noiseFileNameWithExtension = @ORKSpeechInNoiseDefaultNoiseFileName;
_filterFileNameWithExtension = @ORKSpeechInNoiseDefaultFilterFileName;
_speechFileNameWithExtension = @ORKSpeechInNoiseDefaultSpeechFileName;
@@ -73,6 +75,8 @@
- (instancetype)copyWithZone:(NSZone *)zone {
ORKSpeechInNoiseStep *step = [super copyWithZone:zone];
step.speechFilePath = self.speechFilePath;
step.targetSentence = self.targetSentence;
step.speechFileNameWithExtension = self.speechFileNameWithExtension;
step.noiseFileNameWithExtension = self.noiseFileNameWithExtension;
step.filterFileNameWithExtension = self.filterFileNameWithExtension;
@@ -85,9 +89,11 @@
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ(aDecoder, speechFileNameWithExtension);
ORK_DECODE_OBJ(aDecoder, noiseFileNameWithExtension);
ORK_DECODE_OBJ(aDecoder, filterFileNameWithExtension);
ORK_DECODE_OBJ_CLASS(aDecoder, speechFilePath, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, targetSentence, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, speechFileNameWithExtension, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, noiseFileNameWithExtension, NSString);
ORK_DECODE_OBJ_CLASS(aDecoder, filterFileNameWithExtension, NSString);
ORK_DECODE_DOUBLE(aDecoder, gainAppliedToNoise);
ORK_DECODE_BOOL(aDecoder, willAudioLoop);
ORK_DECODE_BOOL(aDecoder, hideGraphView);
@@ -97,6 +103,8 @@
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_OBJ(aCoder, speechFilePath);
ORK_ENCODE_OBJ(aCoder, targetSentence);
ORK_ENCODE_OBJ(aCoder, speechFileNameWithExtension);
ORK_ENCODE_OBJ(aCoder, noiseFileNameWithExtension);
ORK_ENCODE_OBJ(aCoder, filterFileNameWithExtension);
@@ -114,6 +122,8 @@
__typeof(self) castObject = object;
return (isParentSame
&& ORKEqualObjects(self.speechFilePath, castObject.speechFilePath)
&& ORKEqualObjects(self.targetSentence, castObject.targetSentence)
&& ORKEqualObjects(self.speechFileNameWithExtension, castObject.speechFileNameWithExtension)
&& ORKEqualObjects(self.noiseFileNameWithExtension, castObject.noiseFileNameWithExtension)
&& ORKEqualObjects(self.filterFileNameWithExtension, castObject.filterFileNameWithExtension)
@@ -37,12 +37,17 @@
#import "ORKStepContainerView_Private.h"
#import "ORKSpeechInNoiseContentView.h"
#import "ORKSpeechInNoiseStep.h"
#import "ORKSpeechInNoiseResult.h"
#import "ORKCollectionResult_Private.h"
#import "ORKHelpers_Internal.h"
#import "ORKRoundTappingButton.h"
#import "ORKPlaybackButton.h"
#import "ORKSkin.h"
#import "ORKTaskViewController.h"
#import "ORKTaskViewController_Internal.h"
#import <AVFoundation/AVFoundation.h>
@import Accelerate;
@@ -74,9 +79,10 @@
_speechAudioBuffer = [[AVAudioPCMBuffer alloc] init];
_filterAudioBuffer = [[AVAudioPCMBuffer alloc] init];
_installedTap = NO;
self.speechInNoiseContentView = [[ORKSpeechInNoiseContentView alloc] init];
self.activeStepView.activeCustomView = self.speechInNoiseContentView;
self.activeStepView.customContentFillsAvailableSpace = YES;
self.activeStepView.customContentFillsAvailableSpace = NO;
_speechInNoiseContentView.alertColor = [UIColor blueColor];
[self.speechInNoiseContentView.playButton addTarget:self action:@selector(tapButtonPressed) forControlEvents:UIControlEventTouchDown];
[_speechInNoiseContentView setGraphViewHidden:[self speechInNoiseStep].hideGraphView];
@@ -102,7 +108,17 @@
}
- (void)setupBuffers {
[self loadFileName:[self speechInNoiseStep].speechFileNameWithExtension intoBuffer:&_speechAudioBuffer];
if ([[self speechInNoiseStep] speechFilePath] != nil)
{
NSURL *url = [NSURL fileURLWithPath:[self speechInNoiseStep].speechFilePath isDirectory:NO];
[self loadFileAtURL:url intoBuffer:&_speechAudioBuffer];
}
else
{
[self loadFileName:[self speechInNoiseStep].speechFileNameWithExtension intoBuffer:&_speechAudioBuffer];
}
[self loadFileName:[self speechInNoiseStep].noiseFileNameWithExtension intoBuffer:&_noiseAudioBuffer];
[self loadFileName:[self speechInNoiseStep].filterFileNameWithExtension intoBuffer:&_filterAudioBuffer];
@@ -124,6 +140,32 @@
}
}
- (void)loadFileAtURL:(NSURL *)url intoBuffer:(AVAudioPCMBuffer * __strong *)buffer {
AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:url error:nil];
AVAudioFormat *audioFileFormat = audioFile.processingFormat;
AVAudioFrameCount audioFileCapacity = (AVAudioFrameCount)audioFile.length;
if (*buffer == _filterAudioBuffer)
{
_speechToneCapacity = audioFileCapacity;
}
else if (*buffer == _noiseAudioBuffer)
{
_noiseToneCapacity = audioFileCapacity;
}
else
{
AVAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
CMTime audioDuration = asset.duration;
_toneDuration = CMTimeGetSeconds(audioDuration);
}
*buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFileFormat frameCapacity:audioFileCapacity];
[audioFile readIntoBuffer:*buffer error:nil];
}
- (void)loadFileName: (NSString *)file intoBuffer: (AVAudioPCMBuffer * __strong *)buffer {
NSArray *fileComponents = [file componentsSeparatedByString:@"."];
NSString *fileName = fileComponents[0];
@@ -131,27 +173,14 @@
NSURL *fileURL = [[NSBundle bundleForClass:[self class]] URLForResource:fileName withExtension:fileExtension];
AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:fileURL error:nil];
AVAudioFormat *audioFileFormat = audioFile.processingFormat;
AVAudioFrameCount audioFileCapacity = (AVAudioFrameCount)audioFile.length;
if (*buffer == _filterAudioBuffer) {
_speechToneCapacity = audioFileCapacity;
} else if (*buffer == _noiseAudioBuffer) {
_noiseToneCapacity = audioFileCapacity;
} else {
AVAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
CMTime audioDuration = asset.duration;
_toneDuration = CMTimeGetSeconds(audioDuration);
}
*buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFileFormat frameCapacity:audioFileCapacity];
[audioFile readIntoBuffer:*buffer error:nil];
[self loadFileAtURL:fileURL intoBuffer:buffer];
}
- (void)installTap {
AVAudioFormat *mainMixerFormat = [[_audioEngine mainMixerNode] outputFormatForBus:0];
[_mixerNode installTapOnBus:0 bufferSize:1024 format:mainMixerFormat block:^(AVAudioPCMBuffer * _Nonnull buffer5, AVAudioTime * _Nonnull when) {
[_mixerNode installTapOnBus:0 bufferSize:64 format:mainMixerFormat block:^(AVAudioPCMBuffer * _Nonnull buffer5, AVAudioTime * _Nonnull when) {
float * const *channelData = [buffer5 floatChannelData];
if (channelData[0]) {
float avgValue = 0;
@@ -176,11 +205,12 @@
[_mixerNode removeTapOnBus:0];
[self finish];
} else {
[self.navigationItem setHidesBackButton:YES animated:YES];
[self installTap];
[_playerNode play];
if ([self speechInNoiseStep].willAudioLoop) {
[_speechInNoiseContentView.playButton setTitle:ORKLocalizedString(@"SPEECH_IN_NOISE_STOP_AUDIO_LABEL", nil)
forState:UIControlStateNormal];
forState:UIControlStateNormal];
[_speechInNoiseContentView.playButton setTintColor:[UIColor ork_redColor]];
} else {
ORKWeakTypeOf(self) weakSelf = self;
@@ -199,4 +229,57 @@
return (ORKSpeechInNoiseStep *)self.step;
}
- (NSString *)filename
{
NSString *filename = nil;
BOOL (^validate)(NSString * _Nullable) = ^BOOL(NSString * _Nullable str) { return str && str.length > 0; };
NSString *path = [[self speechInNoiseStep] speechFilePath];
NSString *file = [path lastPathComponent];
if (validate(file))
{
filename = [file copy];
}
return filename;
}
- (ORKStepResult *)result
{
ORKStepResult *sResult = [super result];
ORKSpeechInNoiseStep *currentStep = (ORKSpeechInNoiseStep *)self.step;
if (currentStep && [currentStep isKindOfClass:[ORKSpeechInNoiseStep class]] && currentStep.targetSentence)
{
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
ORKSpeechInNoiseResult *speechInNoiseResult = [[ORKSpeechInNoiseResult alloc] initWithIdentifier:currentStep.identifier];
speechInNoiseResult.targetSentence = currentStep.targetSentence;
speechInNoiseResult.filename = [self filename];
[results addObject:speechInNoiseResult];
sResult.results = [results copy];
}
return sResult;
}
- (void)finish
{
[_speechInNoiseContentView removeAllSamples];
[super finish];
}
@end
@@ -34,10 +34,20 @@
NS_ASSUME_NONNULL_BEGIN
@class ORKBorderedButton;
@class ORKRecordButton;
@protocol ORKSpeechRecognitionContentViewDelegate <NSObject>
- (void)didPressRecordButton:(ORKRecordButton *)recordButton;
- (void)didPressUseKeyboardButton;
@end
@interface ORKSpeechRecognitionContentView : ORKActiveStepCustomView
@property (nonatomic, weak) id<ORKSpeechRecognitionContentViewDelegate> delegate;
@property (nonatomic, copy, nullable) UIColor *keyColor;
@property (nonatomic, assign) BOOL failed;
@@ -46,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy, nullable) NSArray *samples;
@property (nonatomic) ORKBorderedButton *recordButton;
@property (nonatomic) ORKRecordButton *recordButton;
@property (nonatomic, copy, nullable) UIImage *speechRecognitionImage;
@@ -60,7 +70,9 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateRecognitionText:(NSString *)recognitionText;
- (void)addRecognitionError:(NSString *)errorMsg;
- (void)addRecognitionError:(NSString * _Nullable)errorMsg;
- (void)updateButtonStates;
@end
@@ -29,8 +29,9 @@
*/
#import "ORKSpeechRecognitionContentView.h"
#import "ORKAudioGraphView.h"
#import "ORKAudioMeteringView.h"
#import "ORKHeadlineLabel.h"
#import "ORKSubheadlineLabel.h"
@@ -39,21 +40,27 @@
#import "ORKAccessibility.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKBorderedButton.h"
#import "ORKRecordButton.h"
@interface ORKSpeechRecognitionContentView () <UITextFieldDelegate>
static CGFloat const ORKSpeechRecognitionContentFlamesViewHeightConstant = 150.0;
static CGFloat const ORKSpeechRecognitionContentFlamesViewMaxOffset = 44.0;
static CGFloat const ORKSpeechRecognitionContentRecordButtonVerticalSpacing = 20.0;
static CGFloat const ORKSpeechRecognitionContentBottomLayoutMargin = 44.0;
@property (nonatomic, strong) ORKAudioGraphView *graphView;
@interface ORKSpeechRecognitionContentView () <UITextFieldDelegate, ORKRecordButtonDelegate>
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
@property (nonatomic, strong) ORKSubheadlineLabel *transcriptLabel;
@property (nonatomic, copy) NSArray<NSLayoutConstraint *> *constraints;
@end
@implementation ORKSpeechRecognitionContentView {
NSMutableArray *_samples;
UIColor *_keyColor;
UIImageView *_imageView;
UILabel *_textLabel;
UIButton *_useKeyboardButton;
}
- (instancetype)initWithFrame:(CGRect)frame {
@@ -61,12 +68,12 @@
if (self) {
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
self.translatesAutoresizingMaskIntoConstraints = NO;
[self setupTranscriptLabel];
[self setupGraphView];
[self setupRecordButton];
[self setupImageView];
[self setupTextLabel];
[self setupUseKeyboardButton];
[self updateGraphSamples];
[self applyKeyColor];
[self setUpConstraints];
@@ -74,15 +81,37 @@
return self;
}
- (void)drawRect:(CGRect)rect
{
[self setUpConstraints];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
[self setUpConstraints];
NSAttributedString *attributedTitle = [[NSAttributedString alloc]
initWithString:ORKLocalizedString(@"SPEECH_IN_NOISE_PREDEFINED_USE_KEYBOARD_INSTEAD", nil)
attributes:@{NSFontAttributeName:[self buttonTextFont],
NSForegroundColorAttributeName:self.tintColor}];
[_useKeyboardButton setAttributedTitle:attributedTitle forState:UIControlStateNormal];
}
- (void)setupImageView {
_imageView = [UIImageView new];
_imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
_imageView.backgroundColor = [UIColor redColor];
[self addSubview:_imageView];
}
- (void)setupTextLabel {
_textLabel = [UILabel new];
_textLabel.backgroundColor = [UIColor greenColor];
_textLabel.font = [[UIFontMetrics metricsForTextStyle:UIFontTextStyleTitle2] scaledFontForFont:[UIFont systemFontOfSize:25.0 weight:UIFontWeightMedium]];
_textLabel.textColor = [self tintColor];
_textLabel.textAlignment = NSTextAlignmentCenter;
@@ -94,7 +123,7 @@
}
- (void)setupGraphView {
self.graphView = [ORKAudioGraphView new];
self.graphView = [[ORKAudioMeteringView alloc] init];
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
_graphView.isAccessibilityElement = YES;
_graphView.accessibilityLabel = ORKLocalizedString(@"AX_SPEECH_RECOGNITION_WAVEFORM", nil);
@@ -114,15 +143,100 @@
[self addSubview:_transcriptLabel];
}
- (void)setupRecordButton {
self.recordButton = [[ORKBorderedButton alloc] init];
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.recordButton setTitle:ORKLocalizedString(@"SPEECH_RECOGNITION_START_RECORD_LABEL", nil)
forState:UIControlStateNormal];
self.recordButton.enabled = YES;
self.recordButton.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitStartsMediaSession;
self.recordButton.accessibilityHint = ORKLocalizedString(@"AX_SPEECH_RECOGNITION_START_RECORDING_HINT", nil);
[self addSubview:_recordButton];
- (void)setupRecordButton
{
if (!_recordButton)
{
self.recordButton = [[ORKRecordButton alloc] init];
self.recordButton.delegate = self;
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
self.recordButton.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitStartsMediaSession;
self.recordButton.accessibilityHint = ORKLocalizedString(@"AX_SPEECH_RECOGNITION_START_RECORDING_HINT", nil);
[self addSubview:_recordButton];
}
}
- (void)buttonPressed:(ORKRecordButton *)recordButton
{
if ([self.delegate conformsToProtocol:@protocol(ORKSpeechRecognitionContentViewDelegate)] &&
[self.delegate respondsToSelector:@selector(didPressRecordButton:)])
{
[self.delegate didPressRecordButton:recordButton];
}
switch ([recordButton buttonType])
{
case ORKRecordButtonTypeRecord:
[recordButton setButtonType:ORKRecordButtonTypeStop animated:YES];
[self setKeyboardButtonEnabled:NO];
break;
default:
[recordButton setButtonType:ORKRecordButtonTypeRecord animated:YES];
[self setKeyboardButtonEnabled:YES];
break;
}
}
- (void)updateButtonStates
{
switch ([_recordButton buttonType])
{
case ORKRecordButtonTypeRecord:
[self setKeyboardButtonEnabled:YES];
break;
default:
[self setKeyboardButtonEnabled:NO];
break;
}
}
- (void)setupUseKeyboardButton
{
_useKeyboardButton = [[UIButton alloc] init];
if (@available(iOS 13.0, *))
{
[_useKeyboardButton setImage:[UIImage systemImageNamed:@"keyboard" compatibleWithTraitCollection:self.traitCollection] forState:UIControlStateNormal];
}
_useKeyboardButton.adjustsImageWhenHighlighted = NO;
NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:ORKLocalizedString(@"SPEECH_IN_NOISE_PREDEFINED_USE_KEYBOARD_INSTEAD", nil)
attributes:@{NSFontAttributeName:[self buttonTextFont],
NSForegroundColorAttributeName:self.tintColor}];
[_useKeyboardButton setAttributedTitle:attributedTitle forState:UIControlStateNormal];
[_useKeyboardButton setTranslatesAutoresizingMaskIntoConstraints:NO];
_useKeyboardButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
_useKeyboardButton.titleLabel.textAlignment = NSTextAlignmentCenter;
CGFloat spacing = 8;
_useKeyboardButton.imageEdgeInsets = UIEdgeInsetsMake(0, -(spacing/2), 0, (spacing/2));
_useKeyboardButton.titleEdgeInsets = UIEdgeInsetsMake(0, (spacing/2), 0, -(spacing/2));
_useKeyboardButton.contentEdgeInsets = UIEdgeInsetsMake(0, -spacing, 0, -spacing);
[self addSubview:_useKeyboardButton];
[_useKeyboardButton addTarget:self action:@selector(useKeyboardButtonPressed) forControlEvents:UIControlEventTouchUpInside];
}
- (void)setKeyboardButtonEnabled:(BOOL)enabled
{
_useKeyboardButton.userInteractionEnabled = enabled;
_useKeyboardButton.alpha = enabled ? 1.0 : 0.25;
}
- (void)useKeyboardButtonPressed
{
if ([self.delegate conformsToProtocol:@protocol(ORKSpeechRecognitionContentViewDelegate)] &&
[self.delegate respondsToSelector:@selector(didPressUseKeyboardButton)])
{
[self.delegate didPressUseKeyboardButton];
}
}
- (UIFont *)buttonTextFont
{
CGFloat fontSize = [[UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleCallout] pointSize];
return [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold];
}
- (void)setSpeechRecognitionText:(NSString *)speechRecognitionText {
@@ -145,7 +259,7 @@
- (void)applyKeyColor {
UIColor *keyColor = [self keyColor];
_graphView.keyColor = keyColor;
[_graphView setMeterColor:keyColor];
}
- (UIColor *)keyColor {
@@ -157,60 +271,47 @@
[self applyKeyColor];
}
- (void)setUpConstraints {
NSMutableArray *constraints = [NSMutableArray array];
- (void)setUpConstraints
{
if (self.constraints.count > 0)
{
[NSLayoutConstraint deactivateConstraints:self.constraints];
}
NSLayoutConstraint *centeredGraphOnScreenLayoutConstraint = [_graphView.centerYAnchor constraintLessThanOrEqualToAnchor:self.centerYAnchor constant:-ORKSpeechRecognitionContentFlamesViewMaxOffset];
centeredGraphOnScreenLayoutConstraint.priority = UILayoutPriorityDefaultLow;
NSDictionary *views = NSDictionaryOfVariableBindings(_imageView, _textLabel, _transcriptLabel, _graphView, _recordButton);
const CGFloat graphHeight = 150;
// In case the text on the button is large, ensure that the button can grow larger than the default height if needed
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_imageView]-[_textLabel]-(5)-[_graphView(graphHeight)]-[_transcriptLabel]-buttonGap-[_recordButton(50@250)]-topBottomMargin-|"
options:(NSLayoutFormatOptions)0
metrics:@{
@"graphHeight": @(graphHeight),
@"topBottomMargin" : @(5),
@"buttonGap" : @(20)
}
views:views]];
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
const CGFloat twiceSideMargin = sideMargin * 2;
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_imageView]-|"
options:0
metrics: nil
views:views]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_textLabel]-|"
options:0
metrics: nil
views:views]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
options:0
metrics: @{@"sideMargin": @(sideMargin)}
views:views]];
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_transcriptLabel]-|"
options:0
metrics: @{@"sideMargin": @(sideMargin)}
views:views]];
// In case the text on the button is large, ensure that the button can grow larger than the default width if needed
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-twiceSideMargin@250-[_recordButton(200@250)]-twiceSideMargin@250-|"
options:0
metrics: @{@"twiceSideMargin": @(twiceSideMargin)}
views:views]];
[constraints addObject:[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor]];
[constraints addObject:[_recordButton.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.layoutMarginsGuide.leadingAnchor]];
[constraints addObject:[_recordButton.trailingAnchor constraintLessThanOrEqualToAnchor:self.layoutMarginsGuide.trailingAnchor]];
[NSLayoutConstraint activateConstraints:constraints];
self.constraints = @[
[_imageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_imageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_imageView.topAnchor constraintEqualToAnchor:self.topAnchor],
[_textLabel.topAnchor constraintEqualToAnchor:_imageView.bottomAnchor],
[_textLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_textLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_graphView.topAnchor constraintGreaterThanOrEqualToAnchor:_textLabel.bottomAnchor],
centeredGraphOnScreenLayoutConstraint,
[_graphView.heightAnchor constraintEqualToConstant:ORKSpeechRecognitionContentFlamesViewHeightConstant],
[_graphView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_graphView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_transcriptLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_transcriptLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_transcriptLabel.topAnchor constraintGreaterThanOrEqualToAnchor:_graphView.bottomAnchor],
[_transcriptLabel.bottomAnchor constraintEqualToAnchor:_recordButton.topAnchor constant:-ORKSpeechRecognitionContentRecordButtonVerticalSpacing],
[_recordButton.topAnchor constraintGreaterThanOrEqualToAnchor:_transcriptLabel.bottomAnchor constant:ORKSpeechRecognitionContentRecordButtonVerticalSpacing],
[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_useKeyboardButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_useKeyboardButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_useKeyboardButton.topAnchor constraintEqualToAnchor:_recordButton.bottomAnchor constant:ORKSpeechRecognitionContentRecordButtonVerticalSpacing],
[_useKeyboardButton.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-ORKSpeechRecognitionContentBottomLayoutMargin]
];
[NSLayoutConstraint activateConstraints:self.constraints];
}
- (void)setShouldHideTranscript:(BOOL)shouldHideTranscript {
@@ -221,19 +322,20 @@
}
- (void)updateGraphSamples {
_graphView.values = _samples;
_graphView.samples = _samples;
}
- (void)addSample:(NSNumber *)sample {
NSAssert(sample != nil, @"Sample should be non-nil");
if (!_samples) {
_samples = [NSMutableArray array];
}
[_samples addObject:sample];
// Try to keep around 250 samples
if (_samples.count > 500) {
_samples = [[_samples subarrayWithRange:(NSRange){250, _samples.count - 250}] mutableCopy];
}
_samples = [ORKLastNSamples(_samples, 500) mutableCopy];
[self updateGraphSamples];
}
@@ -243,7 +345,8 @@
}
}
- (void)addRecognitionError:(NSString *)errorMsg {
- (void)addRecognitionError:(NSString * _Nullable)errorMsg
{
_transcriptLabel.textColor = [UIColor ork_redColor];
_transcriptLabel.text = errorMsg;
}
@@ -254,3 +357,4 @@
}
@end
@@ -28,9 +28,11 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <Speech/SFTranscription.h>
#import <Speech/SFTranscriptionSegment.h>
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
#import <Speech/SFSpeechRecognitionMetadata.h>
#endif
#import <ResearchKit/ORKResult.h>
@@ -51,8 +53,10 @@ ORK_CLASS_AVAILABLE
*/
@property (nonatomic, copy, nullable) SFTranscription *transcription;
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
@property (nonatomic, copy, nullable) SFSpeechRecognitionMetadata *recognitionMetadata API_AVAILABLE(ios(14.5));
#endif
@end
NS_ASSUME_NONNULL_END
@@ -38,12 +38,22 @@
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
ORK_ENCODE_OBJ(aCoder, transcription);
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
ORK_ENCODE_OBJ(aCoder, recognitionMetadata);
}
#endif
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ_CLASS(aDecoder, transcription, SFTranscription);
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
ORK_DECODE_OBJ_CLASS(aDecoder, recognitionMetadata, SFSpeechRecognitionMetadata);
}
#endif
}
return self;
}
@@ -53,16 +63,24 @@
}
- (BOOL)isEqual:(id)object {
BOOL isParentSame = [super isEqual:object];
__typeof(self) castObject = object;
return (isParentSame &&
ORKEqualObjects(self.transcription, castObject.transcription));
BOOL isParentSame = [super isEqual:object] && ORKEqualObjects(self.transcription, castObject.transcription);
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
return isParentSame && ORKEqualObjects(self.recognitionMetadata, castObject.recognitionMetadata);
}
#endif
return isParentSame;
}
- (instancetype)copyWithZone:(NSZone *)zone {
ORKSpeechRecognitionResult *result = [super copyWithZone:zone];
result.transcription = [self.transcription copy];
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
if (@available(iOS 14.5, *)) {
result.recognitionMetadata = [self.recognitionMetadata copy];
}
#endif
return result;
}
@@ -68,7 +68,7 @@
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
ORK_DECODE_OBJ(aDecoder, speechRecognizerLocale);
ORK_DECODE_OBJ_CLASS(aDecoder, speechRecognizerLocale, NSString);
}
return self;
}
@@ -39,38 +39,43 @@
#import "ORKTask.h"
#import "ORKActiveStepView.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKBodyItem_Internal.h"
#import "ORKStepContainerView_Private.h"
#import "ORKSpeechRecognitionContentView.h"
#import "ORKStreamingAudioRecorder.h"
#import "ORKAudioStreamer.h"
#import "ORKSpeechRecognizer.h"
#import "ORKSpeechRecognitionStep.h"
#import "ORKSpeechRecognitionError.h"
#import "ORKHelpers_Internal.h"
#import "ORKBorderedButton.h"
#import "ORKRecordButton.h"
#import "ORKSpeechRecognitionResult.h"
#import "ORKResult_Private.h"
#import "ORKCollectionResult_Private.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
#import "ORKTaskViewController.h"
#import "ORKOrderedTask.h"
@interface ORKSpeechRecognitionStepViewController () <ORKStreamingAudioResultDelegate, ORKSpeechRecognitionDelegate, UITextFieldDelegate>
@interface ORKSpeechRecognitionStepViewController () <ORKStreamingAudioResultDelegate, ORKSpeechRecognitionDelegate, UITextFieldDelegate, ORKSpeechRecognitionContentViewDelegate>
@end
@implementation ORKSpeechRecognitionStepViewController {
ORKSpeechRecognitionContentView *_speechRecognitionContentView;
ORKStreamingAudioRecorder *_audioRecorder;
ORKAudioStreamer *_audioRecorder;
ORKSpeechRecognizer *_speechRecognizer;
dispatch_queue_t _speechRecognitionQueue;
ORKSpeechRecognitionResult *_localResult;
BOOL _errorState;
float _peakPower;
BOOL _allowUserToRecordInsteadOnNextStep;
}
- (instancetype)initWithStep:(ORKStep *)step {
@@ -83,26 +88,59 @@
- (void)viewDidLoad {
[super viewDidLoad];
[self setAllowUserToRecordInsteadOnNextStep:NO];
ORKSpeechRecognitionStep *step = (ORKSpeechRecognitionStep *) self.step;
_speechRecognitionContentView = [ORKSpeechRecognitionContentView new];
_speechRecognitionContentView.shouldHideTranscript = step.shouldHideTranscript;
self.activeStepView.customContentFillsAvailableSpace = YES;
self.activeStepView.activeCustomView = _speechRecognitionContentView;
_speechRecognitionContentView.speechRecognitionImage = step.speechRecognitionImage;
_speechRecognitionContentView.speechRecognitionText = step.speechRecognitionText;
[_speechRecognitionContentView.recordButton addTarget:self
action:@selector(recordButtonPressed:)
forControlEvents:UIControlEventTouchDown];
_speechRecognitionContentView.delegate = self;
_errorState = NO;
[ORKSpeechRecognizer requestAuthorization];
[self requestSpeechRecognizerAuthorizationIfNeeded];
_localResult = [[ORKSpeechRecognitionResult alloc] initWithIdentifier:self.step.identifier];
_speechRecognitionQueue = dispatch_queue_create("SpeechRecognitionQueue", DISPATCH_QUEUE_SERIAL);
}
- (void)requestSpeechRecognizerAuthorizationIfNeeded
{
[self handleSpeechRecognizerAuthorizationStatus:[ORKSpeechRecognizer authorizationStatus]];
}
- (void)handleSpeechRecognizerAuthorizationStatus:(SFSpeechRecognizerAuthorizationStatus)status
{
switch (status)
{
case SFSpeechRecognizerAuthorizationStatusAuthorized:
{
[_speechRecognitionContentView.recordButton setButtonState:ORKRecordButtonStateEnabled];
break;
}
case SFSpeechRecognizerAuthorizationStatusRestricted:
case SFSpeechRecognizerAuthorizationStatusDenied:
{
[_speechRecognitionContentView.recordButton setButtonState:ORKRecordButtonStateDisabled];
break;
}
case SFSpeechRecognizerAuthorizationStatusNotDetermined:
{
[ORKSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus authorizationStatus)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self handleSpeechRecognizerAuthorizationStatus:authorizationStatus == SFSpeechRecognizerAuthorizationStatusAuthorized ?
SFSpeechRecognizerAuthorizationStatusAuthorized:
SFSpeechRecognizerAuthorizationStatusDenied];
});
}];
break;
}
}
}
- (void)initializeRecognizer {
_speechRecognizer = [[ORKSpeechRecognizer alloc] init];
@@ -115,36 +153,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 -6
View File
@@ -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
+33 -9
View File
@@ -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;
}
+1 -1
View File
@@ -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