Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b1153da04 | |||
| 38d161f1bf |
@@ -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
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeAudioData</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<true/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypePhotosorVideos</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<true/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeSensitiveInfo</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeHealth</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<true/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypePreciseLocation</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<true/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -88,6 +88,20 @@ Or, for the latest changes, use the `main` branch:
|
||||
git clone https://github.com/ResearchKit/ResearchKit.git
|
||||
```
|
||||
|
||||
CocoaPods Installation
|
||||
------------
|
||||
For latest stable release
|
||||
|
||||
```
|
||||
pod 'ResearchKit'
|
||||
```
|
||||
|
||||
For early development releases
|
||||
|
||||
```
|
||||
pod 'ResearchKit', :git => 'https://github.com/ResearchKit/ResearchKit.git', :branch => 'main'
|
||||
```
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
@@ -246,6 +260,28 @@ types of steps supported by the *ResearchKit framework* in the first tab, and di
|
||||
results of the last completed task in the second tab. The third tab shows some examples from the *Charts module*.
|
||||
|
||||
|
||||
App Store Submissions
|
||||
-----------------------------
|
||||
|
||||
For apps that don’t use ResearchKit’s HealthKit features:
|
||||
If you are looking to submit your app with ResearchKit to the App Store, you can compile out unnecessary references to HealthKit in your app by editing the following project file:
|
||||
`ResearchKit/Configuration/ResearchKit/ResearchKit-Shared.xcconfig`
|
||||
And changing
|
||||
`ORK_FEATURE_HEALTHKIT_AUTHORIZATION=1`
|
||||
To
|
||||
`ORK_FEATURE_HEALTHKIT_AUTHORIZATION=0`
|
||||
|
||||
With this change, the embedded ResearchKit framework in your app will not contain any HealthKit related functionality or any references to HealthKit API that might trigger AppStore review to require HealthKit entries in your app’s Info.plist.
|
||||
|
||||
Similarly, if you would like to compile out references to CoreLocation. In the same `ResearchKit-Shared.xcconfig`
|
||||
And changing
|
||||
`ORK_FEATURE_CLLOCATIONMANAGER_AUTHORIZATION=1`
|
||||
To
|
||||
`ORK_FEATURE_CLLOCATIONMANAGER_AUTHORIZATION=0`
|
||||
|
||||
Also please ensure that your `info.plist` does not have any HealthKit privacy messages
|
||||
|
||||
Similarly, if you would like to use the ResearchKit’s HealthKit features. Make sure to enable the HealthKit Entitlement
|
||||
|
||||
License<a name="license"></a>
|
||||
=======
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:ResearchKit.xcodeproj">
|
||||
</FileRef>
|
||||
@@ -13,7 +10,4 @@
|
||||
<FileRef
|
||||
location = "group:samples/ORKCatalog/ORKCatalog.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:samples/ORKSample/ORKSample.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ResearchKit'
|
||||
s.version = '2.1.0'
|
||||
s.version = '2.2.16'
|
||||
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/'
|
||||
|
||||
+1112
-859
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
version = "1.3">
|
||||
LastUpgradeVersion = "1200"
|
||||
LastUpgradeVersion = "1310"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "NO">
|
||||
@@ -40,33 +40,21 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B183A4731A8535D100C76870"
|
||||
BuildableName = "ResearchKit.framework"
|
||||
BlueprintName = "ResearchKit"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "86CC8E991AC09332001CCD89"
|
||||
BuildableName = "ResearchKitTests.xctest"
|
||||
BlueprintName = "ResearchKitTests"
|
||||
ReferencedContainer = "container:ResearchKit.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:ResearchKit.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
enableUBSanitizer = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1200"
|
||||
LastUpgradeVersion = "1310"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1200"
|
||||
LastUpgradeVersion = "1310"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -28,8 +28,10 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKDefines.h"
|
||||
|
||||
@import HealthKit;
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
#import <HealthKit/HealthKit.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@@ -60,3 +62,5 @@ typedef NS_OPTIONS(NSInteger, ORKSampleJSONOptions) {
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#endif
|
||||
|
||||
@@ -45,7 +45,7 @@ static NSString *const HKUnitKey = @"unit";
|
||||
static NSString *const HKCorrelatedObjectsKey = @"objects";
|
||||
// static NSString *const HKSourceIdentifierKey = @"sourceBundleIdentifier";
|
||||
|
||||
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
@implementation HKSample (ORKJSONDictionary)
|
||||
|
||||
- (NSMutableDictionary *)ork_JSONMutableDictionaryWithOptions:(ORKSampleJSONOptions)options unit:(HKUnit *)unit {
|
||||
@@ -167,3 +167,4 @@ static NSString *const HKCorrelatedObjectsKey = @"objects";
|
||||
}
|
||||
|
||||
@end
|
||||
#endif // ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
|
||||
@@ -57,7 +57,7 @@ This method signifies that the step is about to end so any necessary clean up be
|
||||
|
||||
You can also optionally pass back an array of ORKResults.
|
||||
*/
|
||||
- (nullable NSArray<ORKResult *> *)provideResults;
|
||||
- (nullable NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ NSNotificationName const ORK3DModelEndStepNotification = @"ORK3DModelEndStepNoti
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_allowsSelection = YES;
|
||||
_allowsSelection = NO;
|
||||
_highlightColor = [UIColor yellowColor];
|
||||
_identifiersOfObjectsToHighlight = nil;
|
||||
}
|
||||
@@ -117,7 +117,7 @@ NSNotificationName const ORK3DModelEndStepNotification = @"ORK3DModelEndStepNoti
|
||||
[NSException raise:@"stepWillEnd not overwitten" format:@"Subclasses must overwrite the stepWillEnd function"];
|
||||
}
|
||||
|
||||
- (NSArray<ORKResult *> *)provideResults {
|
||||
- (NSArray<ORKResult *> *)provideResultsWithIdentifier:(NSString *)identifier {
|
||||
[NSException raise:@"provideResults not overwitten" format:@"Subclasses must overwrite the provideResults function"];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self ) {
|
||||
ORK_DECODE_OBJ(aDecoder, modelManager);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, modelManager, ORK3DModelManager);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -90,8 +90,7 @@
|
||||
return (isParentSame && ORKEqualObjects(self.modelManager, castObject.modelManager));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
- (NSUInteger)hash {
|
||||
return [super hash] ^ [_modelManager hash];
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
@implementation ORK3DModelStepViewController {
|
||||
ORK3DModelManager *_modelManager;
|
||||
ORK3DModelStepContentView *_stepContentview;
|
||||
ORK3DModelStepContentView *_stepContentView;
|
||||
ORK3DModelStep *_step;
|
||||
}
|
||||
|
||||
@@ -62,13 +62,13 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
_stepContentview = [ORK3DModelStepContentView new];
|
||||
_stepContentview.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.activeStepView.activeCustomView = _stepContentview;
|
||||
_stepContentView = [ORK3DModelStepContentView new];
|
||||
_stepContentView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.activeStepView.activeCustomView = _stepContentView;
|
||||
self.activeStepView.customContentFillsAvailableSpace = NO;
|
||||
self.activeStepView.navigationFooterView.neverHasContinueButton = NO;
|
||||
|
||||
[[_stepContentview.bottomAnchor constraintEqualToAnchor:self.activeStepView.navigationFooterView.topAnchor] setActive:YES];
|
||||
[[_stepContentView.bottomAnchor constraintEqualToAnchor:self.activeStepView.navigationFooterView.topAnchor] setActive:YES];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(disableContinueButton:)
|
||||
@@ -97,7 +97,7 @@
|
||||
- (void)activate3DModelManager {
|
||||
_modelManager = _step.modelManager;
|
||||
|
||||
[_modelManager addContentToView:_stepContentview];
|
||||
[_modelManager addContentToView:_stepContentView];
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
@@ -118,7 +118,7 @@
|
||||
ORKStepResult *stepResult = [super result];
|
||||
|
||||
if (_modelManager) {
|
||||
NSArray<ORKResult *> *managerResults = [_modelManager provideResults];
|
||||
NSArray<ORKResult *> *managerResults = [_modelManager provideResultsWithIdentifier:self.step.identifier];
|
||||
if (managerResults) {
|
||||
stepResult.results = [managerResults copy];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKStroopResult.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAccuracyStroopResult : ORKStroopResult
|
||||
|
||||
/**
|
||||
A value that indicates whether the user selected the correct color (i.e. the base display color).
|
||||
*/
|
||||
@property (nonatomic, readonly) BOOL didSelectCorrectColor;
|
||||
|
||||
/**
|
||||
A value that indicates how long it took for the user to make a selection.
|
||||
*/
|
||||
@property (nonatomic, assign) NSTimeInterval timeTakenToSelect;
|
||||
|
||||
/**
|
||||
A value that indicates how far away (in pixels) that the user selected away from the center
|
||||
of the correct circle.
|
||||
*/
|
||||
@property (nonatomic) double distanceToClosestCenter;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAccuracyStroopResult.h"
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@interface ORKAccuracyStroopResult ()
|
||||
@property (readwrite) BOOL didSelectCorrectColor;
|
||||
@end
|
||||
|
||||
@implementation ORKAccuracyStroopResult
|
||||
|
||||
#pragma mark - NSSecureCoding
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
ORK_DECODE_DOUBLE(coder, distanceToClosestCenter);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[super encodeWithCoder:coder];
|
||||
ORK_ENCODE_BOOL(coder, didSelectCorrectColor);
|
||||
ORK_ENCODE_DOUBLE(coder, timeTakenToSelect);
|
||||
ORK_ENCODE_DOUBLE(coder, distanceToClosestCenter);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - NSCopying
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAccuracyStroopResult *result = [super copyWithZone:zone];
|
||||
result.distanceToClosestCenter = self.distanceToClosestCenter;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
self.distanceToClosestCenter == castObject.distanceToClosestCenter);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return [super hash] ^ @(self.didSelectCorrectColor).hash ^ @(self.timeTakenToSelect).hash ^ @(self.distanceToClosestCenter).hash;
|
||||
}
|
||||
|
||||
#pragma mark - ResearchKit
|
||||
|
||||
- (BOOL)didSelectCorrectColor {
|
||||
_didSelectCorrectColor = [self.color isEqualToString:self.colorSelected];
|
||||
return _didSelectCorrectColor;
|
||||
}
|
||||
|
||||
- (NSString *)descriptionWithNumberOfPaddingSpaces:(NSUInteger)numberOfPaddingSpaces {
|
||||
return [NSString stringWithFormat:@"%@; didSelectCorrectColor: %i; timeTakenToSelect: %.3f; distanceToClosestCenter: %.0f %@",
|
||||
[self descriptionPrefixWithNumberOfPaddingSpaces:numberOfPaddingSpaces],
|
||||
self.didSelectCorrectColor,
|
||||
self.timeTakenToSelect,
|
||||
self.distanceToClosestCenter,
|
||||
self.descriptionSuffix];
|
||||
}
|
||||
|
||||
@end
|
||||
+26
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -28,31 +28,40 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
|
||||
#import <ResearchKit/ORKActiveStep.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKVisualConsentStepViewController;
|
||||
@class ORKVisualConsentTransitionAnimator;
|
||||
@protocol ORKVisualConsentTransitionAnimatorDelegate;
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAccuracyStroopStep : ORKActiveStep
|
||||
|
||||
typedef void (^ORKVisualConsentAnimationCompletionHandler)(ORKVisualConsentTransitionAnimator *animator, UIPageViewControllerNavigationDirection direction);
|
||||
/**
|
||||
The color of the label.
|
||||
|
||||
@interface ORKVisualConsentTransitionAnimator : NSObject
|
||||
The base display color is the color that the user must tap on to be correct. The text of
|
||||
the label may match the base display color depending on the `isColorMatching` property.
|
||||
*/
|
||||
@property (nonatomic) UIColor *baseDisplayColor;
|
||||
|
||||
- (instancetype)initWithVisualConsentStepViewController:(ORKVisualConsentStepViewController *)stepViewController
|
||||
movieURL:(NSURL *)movieURL;
|
||||
/**
|
||||
Whether the text and base display color are matching.
|
||||
|
||||
@property (nonatomic, readonly, copy) NSURL *movieURL;
|
||||
If this value is true, the text of the label will spell out the same color as the base display
|
||||
color, making the task easier for the user. If this value is false, the label color and label text
|
||||
will represent different colors, which adds complexity to the puzzle task.
|
||||
*/
|
||||
@property (nonatomic) BOOL isColorMatching;
|
||||
|
||||
- (void)animateTransitionWithDirection:(UIPageViewControllerNavigationDirection)direction
|
||||
loadHandler:(nullable ORKVisualConsentAnimationCompletionHandler)loadHandler
|
||||
completionHandler:(nullable ORKVisualConsentAnimationCompletionHandler)handler;
|
||||
/**
|
||||
The text of the label. (read-only)
|
||||
|
||||
// Call to invalidate display link and remove any observations.
|
||||
- (void)finish;
|
||||
The value of this property is generated based on the `baseDisplayColor` and `isColorMatching`
|
||||
properties. If `isColorMatching` is false, the actual display color will be randomly generated
|
||||
to be a color that is not the base display color.
|
||||
*/
|
||||
@property (nonatomic, readonly) UIColor *actualDisplayColor;
|
||||
|
||||
+ (NSArray <UIColor *> *)colors;
|
||||
|
||||
@end
|
||||
|
||||
+59
-46
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -28,67 +28,80 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKVisualConsentStep.h"
|
||||
|
||||
#import "ORKVisualConsentStepViewController.h"
|
||||
|
||||
#import "ORKConsentDocument_Internal.h"
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKAccuracyStroopStep.h"
|
||||
#import "ORKAccuracyStroopStepViewController.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
|
||||
@implementation ORKVisualConsentStep
|
||||
@implementation ORKAccuracyStroopStep
|
||||
|
||||
+ (Class)stepViewControllerClass {
|
||||
return [ORKVisualConsentStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier document:(ORKConsentDocument *)consentDocument {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.consentDocument = consentDocument;
|
||||
self.showsProgress = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKVisualConsentStep *step = [super copyWithZone:zone];
|
||||
step.consentDocument = self.consentDocument;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, consentDocument, ORKConsentDocument);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, consentDocument);
|
||||
return ORKAccuracyStroopStepViewController.class;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.baseDisplayColor = ORKAccuracyStroopStep.colors[arc4random_uniform(((uint32_t)ORKAccuracyStroopStep.colors.count))];
|
||||
self.isColorMatching = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_BOOL(aDecoder, isColorMatching);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, baseDisplayColor, UIColor);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_BOOL(aCoder, isColorMatching);
|
||||
ORK_ENCODE_OBJ(aCoder, baseDisplayColor);
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAccuracyStroopStep *step = [super copyWithZone:zone];
|
||||
step.isColorMatching = self.isColorMatching;
|
||||
step.baseDisplayColor = [self.baseDisplayColor copy];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
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(((uint32_t) ORKAccuracyStroopStep.colors.count))];
|
||||
}
|
||||
|
||||
@end
|
||||
#pragma clang diagnostic pop
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKStepViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKAccuracyStroopStepViewController : ORKStepViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAccuracyStroopStepViewController.h"
|
||||
#import "ORKAccuracyStroopStep.h"
|
||||
#import "ORKAccuracyStroopResult.h"
|
||||
|
||||
#import "ORKCollectionResult.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "UIColor+String.h"
|
||||
|
||||
@interface ORKAccuracyStroopStepViewController () <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic) NSMutableArray <UIView *> *circles;
|
||||
@property (nonatomic, strong) UILabel *colorLabel;
|
||||
@property (nonatomic) UIView *circlesView;
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *constraints;
|
||||
@property (nonatomic) double distanceToClosestCenter;
|
||||
@property (nonatomic) UIColor *selectedColor;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKAccuracyStroopStepViewController
|
||||
|
||||
- (ORKAccuracyStroopStep *)accuracyStroopStep {
|
||||
return (ORKAccuracyStroopStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
|
||||
if (self.step && [self isViewLoaded]) {
|
||||
[self setupColorLabel];
|
||||
[self setupCirclesView];
|
||||
[self setupConstraints];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self setupCircles];
|
||||
[self setupViewTap];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupViewTap {
|
||||
for (UIGestureRecognizer *recognizer in self.circlesView.gestureRecognizers) {
|
||||
[self.circlesView removeGestureRecognizer:recognizer];
|
||||
}
|
||||
|
||||
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||
tapGestureRecognizer.delegate = self;
|
||||
[self.circlesView addGestureRecognizer:tapGestureRecognizer];
|
||||
}
|
||||
|
||||
- (void)setupCirclesView {
|
||||
[self.circlesView removeFromSuperview];
|
||||
self.circlesView = nil;
|
||||
|
||||
self.circlesView = UIView.new;
|
||||
[self.view addSubview:self.circlesView];
|
||||
}
|
||||
|
||||
- (void)setupColorLabel {
|
||||
[self.colorLabel removeFromSuperview];
|
||||
self.colorLabel = nil;
|
||||
|
||||
self.colorLabel = UILabel.new;
|
||||
self.colorLabel.text = self.accuracyStroopStep.actualDisplayColor.textRepresentation;
|
||||
self.colorLabel.textColor = self.accuracyStroopStep.baseDisplayColor;
|
||||
self.colorLabel.font = [UIFont systemFontOfSize:35.0 weight:UIFontWeightMedium];
|
||||
[self.view addSubview:self.colorLabel];
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
if (self.constraints) {
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
}
|
||||
self.colorLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.circlesView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.constraints = nil;
|
||||
self.constraints = @[
|
||||
[NSLayoutConstraint constraintWithItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:10.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.colorLabel
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeLeading
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeLeading
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeTrailing
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeTrailing
|
||||
multiplier:1.0
|
||||
constant:0.0],
|
||||
[NSLayoutConstraint constraintWithItem:self.circlesView
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:0.0]
|
||||
];
|
||||
[NSLayoutConstraint activateConstraints:self.constraints];
|
||||
}
|
||||
|
||||
- (void)setupCircles {
|
||||
for (UIView *circle in self.circles) {
|
||||
[circle removeFromSuperview];
|
||||
}
|
||||
|
||||
[self.circles removeAllObjects];
|
||||
self.circles = NSMutableArray.array;
|
||||
|
||||
// Constants to use for ball and grid
|
||||
int ballSize = 50;
|
||||
int padding = 10;
|
||||
int cellSize = ballSize + padding * 2;
|
||||
|
||||
// Calculating number of rows/columns in grid to layout color circles
|
||||
uint32_t numRows = (self.circlesView.bounds.size.height) / cellSize;
|
||||
uint32_t numColumns = (self.circlesView.bounds.size.width) / cellSize;
|
||||
|
||||
// Extra padding to ensure that the grid spans the whole screen width
|
||||
int extraHorizontalSpaceForCell = ((int)self.circlesView.bounds.size.width % cellSize) / numColumns;
|
||||
|
||||
// Matrix to keep track of cells that already have a circle --> avoid overlap in O(n)
|
||||
bool cellTakenMatrix[numRows][numColumns];
|
||||
for (uint32_t r = 0; r < numRows; r++) {
|
||||
for (uint32_t c = 0; c < numColumns; c++) {
|
||||
cellTakenMatrix[r][c] = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int colorIndex = 0; colorIndex < ORKAccuracyStroopStep.colors.count; colorIndex++) {
|
||||
// Obtain random location for color circle within bounds
|
||||
int randomR = (int)arc4random_uniform(numRows);
|
||||
int randomC = (int)arc4random_uniform(numColumns);
|
||||
|
||||
ORK_Log_Debug("Trying placement for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
|
||||
|
||||
// If cell is already taken, look at 8 spots around for a free spot
|
||||
if (cellTakenMatrix[randomR][randomC]) {
|
||||
ORK_Log_Debug("Position (r, c): (%d, %d) already taken", randomR, randomC);
|
||||
|
||||
// Loops through the 3x3 grid with randomR,randomC as the center
|
||||
bool shouldBreak = false;
|
||||
for (int r = randomR - 1; !shouldBreak && r <= randomR + 1; r++) {
|
||||
for (int c = randomC - 1; !shouldBreak && c <= randomC + 1; c++) {
|
||||
// If r/c are out of circleView's bounds, then don't consider
|
||||
if ((r < 0 || r >= numRows) || (c < 0 || c >= numColumns)) { continue; }
|
||||
|
||||
// If cell is not taken, then can assign to there and break out of for-loops
|
||||
if (!cellTakenMatrix[r][c]) {
|
||||
randomR = r;
|
||||
randomC = c;
|
||||
shouldBreak = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ORK_Log_Info("Final position for color: %d at (r, c): (%d, %d)", colorIndex, randomR, randomC);
|
||||
|
||||
cellTakenMatrix[randomR][randomC] = true;
|
||||
|
||||
CGFloat circleX = (randomC * (cellSize + extraHorizontalSpaceForCell)) + padding + extraHorizontalSpaceForCell / 2;
|
||||
CGFloat circleY = (randomR * cellSize) + padding;
|
||||
CGRect frame = CGRectMake(circleX, circleY, ballSize, ballSize);
|
||||
UIView *newCircle = [[UIView alloc] initWithFrame:frame];
|
||||
newCircle.backgroundColor = ORKAccuracyStroopStep.colors[colorIndex];
|
||||
newCircle.clipsToBounds = YES;
|
||||
newCircle.layer.cornerRadius = ballSize / 2;
|
||||
newCircle.tag = colorIndex;
|
||||
[self.circles addObject:newCircle];
|
||||
[self.circlesView addSubview:newCircle];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
|
||||
CGPoint touchPoint = [recognizer locationInView:self.circlesView];
|
||||
double minDistance = INFINITY;
|
||||
|
||||
for (UIView *circle in self.circles) {
|
||||
double dx = (touchPoint.x - circle.center.x);
|
||||
double dy = (touchPoint.y - circle.center.y);
|
||||
double distance = sqrt(dx * dx + dy * dy);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
}
|
||||
|
||||
if (CGRectContainsPoint(circle.frame, touchPoint)) {
|
||||
self.selectedColor = ORKAccuracyStroopStep.colors[circle.tag];
|
||||
self.distanceToClosestCenter = distance;
|
||||
[super goForward];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.distanceToClosestCenter = minDistance;
|
||||
[super goForward];
|
||||
}
|
||||
|
||||
- (BOOL)hasPreviousStep {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (ORKStepResult *)result {
|
||||
ORKStepResult *stepResult = [super result];
|
||||
|
||||
ORKAccuracyStroopResult *result = [[ORKAccuracyStroopResult alloc] initWithIdentifier:self.accuracyStroopStep.identifier];
|
||||
result.color = self.accuracyStroopStep.baseDisplayColor.textRepresentation;
|
||||
result.colorSelected = self.selectedColor.textRepresentation;
|
||||
result.distanceToClosestCenter = self.distanceToClosestCenter;
|
||||
result.startDate = stepResult.startDate;
|
||||
result.endDate = stepResult.endDate;
|
||||
result.timeTakenToSelect = [result.endDate timeIntervalSinceDate:result.startDate];
|
||||
|
||||
NSMutableArray *results = [[NSMutableArray alloc] init];
|
||||
if (stepResult.results) {
|
||||
results = [stepResult.results mutableCopy];
|
||||
}
|
||||
|
||||
[results addObject:result];
|
||||
|
||||
stepResult.results = [results copy];
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self stepDidChange];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -30,7 +30,6 @@
|
||||
|
||||
|
||||
@import UIKit;
|
||||
@import HealthKit;
|
||||
#import <ResearchKit/ORKStep.h>
|
||||
|
||||
|
||||
@@ -206,15 +205,6 @@ The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSArray<ORKRecorderConfiguration *> *recorderConfigurations;
|
||||
|
||||
/**
|
||||
A Boolean value that determines if a step is a practice step or not.
|
||||
|
||||
When the value of this property is `YES`, the ResearchKit framework sets the allowsBackNavigation property to 'YES'
|
||||
|
||||
The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL isPractice;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -122,7 +122,6 @@
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, spokenInstruction, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, finishedSpokenInstruction, NSString);
|
||||
ORK_DECODE_OBJ_ARRAY(aDecoder, recorderConfigurations, ORKRecorderConfiguration);
|
||||
ORK_DECODE_BOOL(aDecoder, isPractice);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -143,7 +142,6 @@
|
||||
ORK_ENCODE_OBJ(aCoder, spokenInstruction);
|
||||
ORK_ENCODE_OBJ(aCoder, finishedSpokenInstruction);
|
||||
ORK_ENCODE_OBJ(aCoder, recorderConfigurations);
|
||||
ORK_ENCODE_BOOL(aCoder, isPractice);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
@@ -164,10 +162,10 @@
|
||||
(self.shouldVibrateOnStart == castObject.shouldVibrateOnStart) &&
|
||||
(self.shouldVibrateOnFinish == castObject.shouldVibrateOnFinish) &&
|
||||
(self.shouldContinueOnFinish == castObject.shouldContinueOnFinish) &&
|
||||
(self.shouldUseNextAsSkipButton == castObject.shouldUseNextAsSkipButton) &&
|
||||
(self.isPractice == castObject.isPractice));
|
||||
(self.shouldUseNextAsSkipButton == castObject.shouldUseNextAsSkipButton));
|
||||
}
|
||||
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
- (NSSet<HKObjectType *> *)requestedHealthKitTypesForReading {
|
||||
NSMutableSet<HKObjectType *> *set = [NSMutableSet set];
|
||||
for (ORKRecorderConfiguration *config in self.recorderConfigurations) {
|
||||
@@ -178,6 +176,7 @@
|
||||
}
|
||||
return set;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (ORKPermissionMask)requestedPermissions {
|
||||
ORKPermissionMask mask = [super requestedPermissions];
|
||||
@@ -187,8 +186,4 @@
|
||||
return mask;
|
||||
}
|
||||
|
||||
- (BOOL)allowsBackNavigation {
|
||||
return self.isPractice;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,30 +30,26 @@
|
||||
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@import UIKit;
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
|
||||
static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
static mach_timebase_info_data_t sTimebaseInfo;
|
||||
if ( sTimebaseInfo.denom == 0 ) {
|
||||
(void) mach_timebase_info(&sTimebaseInfo);
|
||||
static mach_timebase_info_data_t sTimebaseInfo;
|
||||
if (sTimebaseInfo.denom == 0) {
|
||||
(void)mach_timebase_info(&sTimebaseInfo);
|
||||
}
|
||||
uint64_t elapsedNano = delta * sTimebaseInfo.numer / sTimebaseInfo.denom;
|
||||
return elapsedNano * 1.0 / NSEC_PER_SEC;
|
||||
}
|
||||
|
||||
|
||||
@implementation ORKActiveStepTimer {
|
||||
uint64_t _startTime;
|
||||
NSTimeInterval _preExistingRuntime;
|
||||
dispatch_queue_t _queue;
|
||||
dispatch_source_t _timer;
|
||||
UIBackgroundTaskIdentifier _backgroundTaskIdentifier;
|
||||
uint32_t _isRunning;
|
||||
}
|
||||
|
||||
@@ -68,7 +64,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
_interval = interval;
|
||||
_handler = [handler copy];
|
||||
_preExistingRuntime = runtime;
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
|
||||
_queue = dispatch_queue_create("active_step", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
@@ -128,7 +123,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
}
|
||||
|
||||
- (void)queue_event {
|
||||
[self queue_assertBackgroundTask];
|
||||
|
||||
NSTimeInterval runtime = [self queue_runtime];
|
||||
BOOL finished = (runtime >= _duration);
|
||||
@@ -138,13 +132,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
_handler(self, finished);
|
||||
dispatch_sync(_queue, ^{
|
||||
|
||||
// If the timer is still NULL here, we can safely release the background task.
|
||||
if (_timer == NULL) {
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,29 +143,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)queue_releaseBackgroundTask {
|
||||
if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
|
||||
return;
|
||||
}
|
||||
UIBackgroundTaskIdentifier identifier = _backgroundTaskIdentifier;
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[[UIApplication sharedApplication] endBackgroundTask:identifier];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)queue_assertBackgroundTask {
|
||||
if (_backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
|
||||
return;
|
||||
}
|
||||
_backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
||||
// This is guaranteed to be called synchronously on the main queue, switch to our queue to invalidate the identifier
|
||||
dispatch_sync(_queue, ^{
|
||||
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)queue_resume {
|
||||
if (_timer != NULL) {
|
||||
// Already resumed
|
||||
@@ -190,11 +154,6 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We want to run in the background if we can, so voice can be played, etc.
|
||||
assert(_backgroundTaskIdentifier == UIBackgroundTaskInvalid);
|
||||
|
||||
[self queue_assertBackgroundTask];
|
||||
|
||||
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
|
||||
0, 0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
if (_timer == NULL) {
|
||||
@@ -228,16 +187,11 @@ static NSTimeInterval timeIntervalFromMachTime(uint64_t delta) {
|
||||
_preExistingRuntime += timeIntervalFromMachTime(now - _startTime);
|
||||
_startTime = 0;
|
||||
|
||||
if (!atFinish) {
|
||||
// If we are atFinish, the task will be released after the handler completes
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)queue_reset {
|
||||
[self queue_clearTimer];
|
||||
_preExistingRuntime = 0;
|
||||
[self queue_releaseBackgroundTask];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
#import "ORKTaskViewController_Internal.h"
|
||||
#import "ORKRecorder_Internal.h"
|
||||
|
||||
#import "ORKStepView_Private.h"
|
||||
#import "ORKStepContentView.h"
|
||||
|
||||
#import "ORKActiveStep_Internal.h"
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKResult.h"
|
||||
@@ -125,6 +128,7 @@
|
||||
if (_customView) {
|
||||
_activeStepView.customContentView = _customView;
|
||||
}
|
||||
_activeStepView.stepContentView.shouldAutomaticallyAdjustImageTintColor = YES;
|
||||
[self.view addSubview:_activeStepView];
|
||||
}
|
||||
|
||||
@@ -295,8 +299,9 @@
|
||||
outputDirectory:self.outputDirectory];
|
||||
recorder.configuration = provider;
|
||||
recorder.delegate = self;
|
||||
|
||||
[recorders addObject:recorder];
|
||||
if (recorder) {
|
||||
[recorders addObject:recorder];
|
||||
}
|
||||
}
|
||||
self.recorders = recorders;
|
||||
|
||||
@@ -566,7 +571,7 @@ static NSString *const _ORKRecorderResultsRestoreKey = @"recorderResults";
|
||||
[super decodeRestorableStateWithCoder:coder];
|
||||
|
||||
self.finished = [coder decodeBoolForKey:_ORKFinishedRestoreKey];
|
||||
_recorderResults = [coder decodeObjectOfClass:[NSArray class] forKey:_ORKRecorderResultsRestoreKey];
|
||||
_recorderResults = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSArray.self, ORKResult.self]] forKey:_ORKRecorderResultsRestoreKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKRecordButton;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, ORKAudioContentViewEvent) {
|
||||
ORKAudioContentViewEventStartRecording = 0,
|
||||
ORKAudioContentViewEventStopRecording
|
||||
};
|
||||
|
||||
typedef void (^ORKAudioStepContentViewEventHandler)(ORKAudioContentViewEvent);
|
||||
|
||||
@interface ORKAudioContentView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic, copy, nullable) UIColor *keyColor;
|
||||
@@ -48,6 +57,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, copy, nullable) NSArray *samples;
|
||||
|
||||
@property (nonatomic) BOOL useRecordButton;
|
||||
|
||||
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler;
|
||||
|
||||
// Samples should be in the range of (0, 1).
|
||||
- (void)addSample:(NSNumber *)sample;
|
||||
- (void)removeAllSamples;
|
||||
@@ -55,3 +68,4 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
#import "ORKAudioContentView.h"
|
||||
#import "ORKAudioGraphView.h"
|
||||
#import "ORKAudioMeteringView.h"
|
||||
|
||||
#import "ORKHeadlineLabel.h"
|
||||
#import "ORKLabel.h"
|
||||
@@ -38,6 +39,7 @@
|
||||
#import "ORKAccessibility.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKRecordButton.h"
|
||||
|
||||
|
||||
// The central blue region.
|
||||
@@ -46,6 +48,8 @@ static const CGFloat GraphViewBlueZoneHeight = 170;
|
||||
// The two bands at top and bottom which are "loud" each have this height.
|
||||
static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
|
||||
static const CGFloat ORKAudioStepContentRecordButtonVerticalSpacing = 20.0;
|
||||
|
||||
@interface ORKAudioTimerLabel : ORKLabel
|
||||
|
||||
@end
|
||||
@@ -61,11 +65,12 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKAudioContentView ()
|
||||
@interface ORKAudioContentView () <ORKRecordButtonDelegate>
|
||||
|
||||
@property (nonatomic, strong) ORKHeadlineLabel *alertLabel;
|
||||
@property (nonatomic, strong) UILabel *timerLabel;
|
||||
@property (nonatomic, strong) ORKAudioGraphView *graphView;
|
||||
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
|
||||
@property (nonatomic, copy, nullable) ORKAudioStepContentViewEventHandler viewEventhandler;
|
||||
|
||||
@end
|
||||
|
||||
@@ -73,19 +78,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
@implementation ORKAudioContentView {
|
||||
NSMutableArray *_samples;
|
||||
UIColor *_keyColor;
|
||||
ORKRecordButton *_recordButton;
|
||||
BOOL _checkAudioLevel;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.layoutMargins = ORKStandardFullScreenLayoutMarginsForView(self);
|
||||
_checkAudioLevel = YES;
|
||||
_useRecordButton = NO;
|
||||
|
||||
self.alertLabel = [ORKHeadlineLabel new];
|
||||
_alertLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.timerLabel = [ORKAudioTimerLabel new];
|
||||
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_timerLabel.textAlignment = NSTextAlignmentRight;
|
||||
self.graphView = [ORKAudioGraphView new];
|
||||
self.graphView = [[ORKAudioMeteringView alloc] init];
|
||||
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
@@ -122,10 +131,22 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
[self updateAlertLabelHidden];
|
||||
}
|
||||
|
||||
- (void)setUseRecordButton:(BOOL)useRecordButton {
|
||||
_useRecordButton = useRecordButton;
|
||||
|
||||
if (_useRecordButton) {
|
||||
_checkAudioLevel = NO;
|
||||
[_timerLabel setHidden: YES];
|
||||
|
||||
[self setupRecordButton];
|
||||
[self setUpConstraints];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applyKeyColor {
|
||||
UIColor *keyColor = [self keyColor];
|
||||
_timerLabel.textColor = keyColor;
|
||||
_graphView.keyColor = keyColor;
|
||||
_graphView.meterColor = keyColor;
|
||||
}
|
||||
|
||||
- (UIColor *)keyColor {
|
||||
@@ -143,6 +164,44 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
_graphView.alertColor = alertColor;
|
||||
}
|
||||
|
||||
- (void)setViewEventHandler:(ORKAudioStepContentViewEventHandler)handler {
|
||||
self.viewEventhandler = [handler copy];
|
||||
}
|
||||
|
||||
- (void)invokeViewEventHandlerWithEvent:(ORKAudioContentViewEvent)event {
|
||||
if (self.viewEventhandler) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
self.viewEventhandler(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)buttonPressed:(ORKRecordButton *)recordButton {
|
||||
switch (recordButton.buttonType) {
|
||||
case ORKRecordButtonTypeRecord:
|
||||
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStartRecording];
|
||||
[_recordButton setButtonType:ORKRecordButtonTypeStop];
|
||||
break;
|
||||
default:
|
||||
[self invokeViewEventHandlerWithEvent:ORKAudioContentViewEventStopRecording];
|
||||
[_recordButton setButtonState:ORKRecordButtonStateDisabled];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupRecordButton {
|
||||
if (!_recordButton) {
|
||||
_recordButton = [[ORKRecordButton alloc] init];
|
||||
_recordButton.delegate = self;
|
||||
_recordButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[_recordButton setButtonType:ORKRecordButtonTypeRecord];
|
||||
|
||||
[self addSubview:_recordButton];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
|
||||
@@ -161,12 +220,23 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
|
||||
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
|
||||
const CGFloat innerMargin = 2;
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
|
||||
views:views]];
|
||||
|
||||
if (_useRecordButton) {
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:@{@"sideMargin": @(sideMargin)}
|
||||
views:views]];
|
||||
|
||||
[constraints addObject:[_recordButton.topAnchor constraintEqualToAnchor:_graphView.bottomAnchor constant:ORKAudioStepContentRecordButtonVerticalSpacing]];
|
||||
[constraints addObject:[_recordButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor]];
|
||||
} else {
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-innerMargin-[_timerLabel]-sideMargin-|"
|
||||
options:NSLayoutFormatAlignAllCenterY
|
||||
metrics:@{@"sideMargin": @(sideMargin), @"innerMargin": @(innerMargin)}
|
||||
views:views]];
|
||||
}
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_graphView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
@@ -206,18 +276,21 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
}
|
||||
|
||||
- (void)updateGraphSamples {
|
||||
_graphView.values = _samples;
|
||||
_graphView.samples = _samples;
|
||||
[self updateAlertLabelHidden];
|
||||
}
|
||||
|
||||
- (void)updateAlertLabelHidden {
|
||||
NSNumber *sample = _samples.lastObject;
|
||||
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
|
||||
|
||||
if (_alertLabel.hidden && show) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
|
||||
if (_checkAudioLevel) {
|
||||
BOOL show = (!_finished && (sample.doubleValue > _alertThreshold)) || _failed;
|
||||
|
||||
if (_alertLabel.hidden && show) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, _alertLabel.text);
|
||||
}
|
||||
_alertLabel.hidden = !show;
|
||||
}
|
||||
_alertLabel.hidden = !show;
|
||||
}
|
||||
|
||||
- (void)setSamples:(NSArray *)samples {
|
||||
@@ -260,3 +333,4 @@ static const CGFloat GraphViewRedZoneHeight = 25;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
#import <ResearchKit/ORKFitnessStep.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKBundleAsset;
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKVocalCue : NSObject <NSSecureCoding, NSCopying>
|
||||
|
||||
@property (atomic) NSTimeInterval time;
|
||||
|
||||
@property (atomic, copy) NSString *spokenText;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithTime:(NSTimeInterval) time
|
||||
spokenText:(NSString *) spokenText;
|
||||
@end
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAudioFitnessStep : ORKFitnessStep
|
||||
|
||||
@property (nonatomic, copy) ORKBundleAsset *audioAsset;
|
||||
|
||||
@property (nonatomic, copy) NSArray<ORKVocalCue *> *vocalCues;
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *) identifier
|
||||
audioAsset:(ORKBundleAsset *) audioAsset
|
||||
vocalCues:(nullable NSArray<ORKVocalCue *> *) vocalCues;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAudioFitnessStep.h"
|
||||
#import "ORKAudioFitnessStepViewController.h"
|
||||
#import "ORKBundleAsset.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
@implementation ORKVocalCue
|
||||
|
||||
- (instancetype)initWithTime:(NSTimeInterval)time
|
||||
spokenText:(NSString *)spokenText {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.time = time;
|
||||
self.spokenText = [spokenText copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if (self) {
|
||||
ORK_DECODE_DOUBLE(coder, time);
|
||||
ORK_DECODE_OBJ_CLASS(coder, spokenText, NSString);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
ORK_ENCODE_DOUBLE(coder, time);
|
||||
ORK_ENCODE_OBJ(coder, spokenText);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
return [[ORKVocalCue alloc] initWithTime:self.time spokenText:self.spokenText];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
if ([self class] != [other class]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__typeof(self) castObject = other;
|
||||
return (self.time == castObject.time &&
|
||||
ORKEqualObjects(self.spokenText, castObject.spokenText));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKAudioFitnessStep
|
||||
|
||||
- (Class)stepViewControllerClass {
|
||||
return [ORKAudioFitnessStepViewController class];
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
audioAsset:(ORKBundleAsset *)audioAsset
|
||||
vocalCues:(nullable NSArray<ORKVocalCue *> *)vocalCues {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.stepDuration = 180;
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
self.audioAsset = [audioAsset copy];
|
||||
self.vocalCues = vocalCues == nil ? [NSArray new] : [vocalCues copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(coder, audioAsset, ORKBundleAsset);
|
||||
ORK_DECODE_OBJ_ARRAY(coder, vocalCues, ORKVocalCue);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
[super encodeWithCoder:coder];
|
||||
ORK_ENCODE_OBJ(coder, audioAsset);
|
||||
ORK_ENCODE_OBJ(coder, vocalCues);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAudioFitnessStep *step = [super copyWithZone:zone];
|
||||
step.audioAsset = [self.audioAsset copy];
|
||||
step.vocalCues = [self.vocalCues copy];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
BOOL superIsEqual = [super isEqual:other];
|
||||
|
||||
__typeof(self) castObject = other;
|
||||
return (superIsEqual &&
|
||||
ORKEqualObjects(self.audioAsset, castObject.audioAsset) &&
|
||||
ORKEqualObjects(self.vocalCues, castObject.vocalCues));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return super.hash ^ self.audioAsset.hash ^ self.vocalCues.hash;
|
||||
}
|
||||
|
||||
@end
|
||||
+11
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Ricardo Sánchez-Sáez.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -28,25 +28,22 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
#import "ORKConsentSceneViewController.h"
|
||||
#import "ORKStepContainerView.h"
|
||||
|
||||
#import <ResearchKit/ResearchKit.h>
|
||||
#import <ResearchKit/ORKFitnessStepViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKConsentSceneView : ORKStepContainerView
|
||||
|
||||
// Test Seam
|
||||
@protocol ORKAudioPlayer
|
||||
- (BOOL)prepareToPlay;
|
||||
- (BOOL)play;
|
||||
- (void)pause;
|
||||
- (void)stop;
|
||||
@end
|
||||
|
||||
@interface ORKAudioFitnessStepViewController : ORKFitnessStepViewController
|
||||
|
||||
@interface ORKConsentSceneViewController ()
|
||||
|
||||
@property (nonatomic, readonly) ORKConsentSceneView *sceneView;
|
||||
|
||||
@property (nonatomic, readonly) UIScrollView *scrollView;
|
||||
|
||||
- (void)scrollToTopAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion;
|
||||
@property (nonatomic) id<ORKAudioPlayer> audioPlayer;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKAudioFitnessStep.h"
|
||||
#import "ORKAudioFitnessStepViewController.h"
|
||||
#import "ORKVoiceEngine.h"
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface ORKAVAudioPlayer : AVAudioPlayer <ORKAudioPlayer>
|
||||
@end
|
||||
|
||||
@implementation ORKAVAudioPlayer
|
||||
@end
|
||||
|
||||
@interface ORKAudioFitnessStepViewController ()
|
||||
@property (nonatomic) BOOL appHasAudioBackgroundMode;
|
||||
@property (nonatomic) NSMutableSet<ORKVocalCue *> *playedCues;
|
||||
@end
|
||||
|
||||
@implementation ORKAudioFitnessStepViewController
|
||||
|
||||
- (ORKAudioFitnessStep *)audioStep {
|
||||
return (ORKAudioFitnessStep *)self.step;
|
||||
}
|
||||
|
||||
- (NSMutableSet *)playedCues {
|
||||
if (!_playedCues) {
|
||||
_playedCues = [NSMutableSet new];
|
||||
}
|
||||
return _playedCues;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self.audioPlayer prepareToPlay];
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
|
||||
if (self.appHasAudioBackgroundMode) {
|
||||
[self enableBackgroundAudioSession:YES];
|
||||
}
|
||||
|
||||
[self.audioPlayer play];
|
||||
}
|
||||
|
||||
- (void)suspend {
|
||||
[super suspend];
|
||||
[self.audioPlayer pause];
|
||||
}
|
||||
|
||||
- (void)resume {
|
||||
[super resume];
|
||||
[self.audioPlayer play];
|
||||
}
|
||||
|
||||
- (void)finish {
|
||||
[super finish];
|
||||
[self.audioPlayer stop];
|
||||
|
||||
if (self.appHasAudioBackgroundMode) {
|
||||
[self enableBackgroundAudioSession:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
|
||||
[super countDownTimerFired:timer finished:finished];
|
||||
|
||||
ORKVoiceEngine *voice = [ORKVoiceEngine sharedVoiceEngine];
|
||||
NSTimeInterval timeRemaining = [timer duration] - [timer runtime];
|
||||
|
||||
for (ORKVocalCue *cue in [self audioStep].vocalCues) {
|
||||
if (cue.time >= timeRemaining && ![self.playedCues containsObject:cue]) {
|
||||
[self.playedCues addObject:cue];
|
||||
[voice speakText: cue.spokenText];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)appHasAudioBackgroundMode {
|
||||
NSArray<NSString *> *backgroundModes = (NSArray<NSString *> *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
|
||||
BOOL hasBackgroundAudioMode = [backgroundModes containsObject:@"audio"];
|
||||
return hasBackgroundAudioMode;
|
||||
}
|
||||
|
||||
- (void)enableBackgroundAudioSession:(BOOL)enabled {
|
||||
NSError *error;
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
|
||||
mode:AVAudioSessionModeDefault
|
||||
routeSharingPolicy:AVAudioSessionRouteSharingPolicyLongFormAudio
|
||||
options:0
|
||||
error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("ORKAudioFitnessStepViewController failed to setup audio session: %@", error);
|
||||
return;
|
||||
}
|
||||
|
||||
[[AVAudioSession sharedInstance] setActive:enabled error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("ORKAudioFitnessViewController failed to start audio session: %@", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (id<ORKAudioPlayer>)audioPlayer {
|
||||
if (!_audioPlayer) {
|
||||
ORKAudioFitnessStep *step = [self audioStep];
|
||||
NSError *error;
|
||||
_audioPlayer = [[ORKAVAudioPlayer alloc] initWithContentsOfURL:step.audioAsset.url error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("ORKAudioFitnessStepViewController Failed to load audio file: %@", error.localizedFailureReason);
|
||||
}
|
||||
}
|
||||
return _audioPlayer;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -28,20 +28,15 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import <ResearchKit/ORKAudioMeteringView.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKAudioGraphView : UIView
|
||||
|
||||
@property (nonatomic, strong) UIColor *keyColor;
|
||||
@property (nonatomic, strong) UIColor *alertColor;
|
||||
|
||||
@property (nonatomic, copy) NSArray *values;
|
||||
|
||||
@property (nonatomic) CGFloat alertThreshold;
|
||||
|
||||
@interface ORKAudioGraphView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -32,11 +32,21 @@
|
||||
#import "ORKAudioGraphView.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
|
||||
static const CGFloat ValueLineWidth = 4.5;
|
||||
static const CGFloat ValueLineMargin = 1.5;
|
||||
static const CGFloat GraphHeight = 150.0;
|
||||
|
||||
@interface ORKAudioGraphView ()
|
||||
|
||||
/// ORKAudioMetering
|
||||
@property (nonatomic, copy, nullable) NSArray<NSNumber *> *samples;
|
||||
@property (nonatomic, assign) float alertThreshold;
|
||||
|
||||
/// ORKAudioMeteringView
|
||||
@property (nonatomic, strong) UIColor *meterColor;
|
||||
@property (nonatomic, strong, nullable) UIColor *alertColor;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKAudioGraphView
|
||||
|
||||
@@ -46,7 +56,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[self setUpConstraints];
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
_values = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
|
||||
_samples = @[ @(0.2), @(0.6), @(0.55), @(0.1), @(0.75), @(0.7) ];
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
@@ -65,26 +75,6 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[NSLayoutConstraint activateConstraints:@[heightConstraint]];
|
||||
}
|
||||
|
||||
- (void)setValues:(NSArray *)values {
|
||||
_values = [values copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setKeyColor:(UIColor *)keyColor {
|
||||
_keyColor = [keyColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor {
|
||||
_alertColor = [alertColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(CGFloat)alertThreshold {
|
||||
_alertThreshold = alertThreshold;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
CGRect bounds = self.bounds;
|
||||
|
||||
@@ -104,7 +94,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[centerLine addLineToPoint:(CGPoint){.x = maxX, .y = midY}];
|
||||
|
||||
CGContextSetLineWidth(context, 1.0 / scale);
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
CGFloat lengths[2] = {3, 3};
|
||||
CGContextSetLineDash(context, 0, lengths, 2);
|
||||
|
||||
@@ -125,7 +115,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
path1.lineWidth = ValueLineWidth;
|
||||
UIBezierPath *path2 = [path1 copy];
|
||||
|
||||
for (NSNumber *value in [_values reverseObjectEnumerator]) {
|
||||
for (NSNumber *value in [_samples reverseObjectEnumerator]) {
|
||||
CGFloat floatValue = value.doubleValue;
|
||||
|
||||
UIBezierPath *path = nil;
|
||||
@@ -134,7 +124,7 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[_alertColor setStroke];
|
||||
} else {
|
||||
path = path2;
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
}
|
||||
[path moveToPoint:(CGPoint){.x = x, .y = midY - floatValue*halfHeight}];
|
||||
[path addLineToPoint:(CGPoint){.x = x, .y = midY + floatValue*halfHeight}];
|
||||
@@ -150,11 +140,41 @@ static const CGFloat GraphHeight = 150.0;
|
||||
[_alertColor setStroke];
|
||||
[path1 stroke];
|
||||
|
||||
[_keyColor setStroke];
|
||||
[_meterColor setStroke];
|
||||
[path2 stroke];
|
||||
|
||||
}
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMetering
|
||||
|
||||
- (void)setSamples:(NSArray<NSNumber *> *)samples
|
||||
{
|
||||
_samples = [samples copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(float)threshold
|
||||
{
|
||||
_alertThreshold = threshold;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
#pragma mark = ORKAudioMeteringView
|
||||
|
||||
- (void)setMeterColor:(UIColor *)meterColor
|
||||
{
|
||||
_meterColor = [meterColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor
|
||||
{
|
||||
_alertColor = [alertColor copy];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@@ -75,3 +75,4 @@ ORK_CLASS_AVAILABLE
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ Float32 const VolumeClamp = 60.0;
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, audioLevelStepIdentifier, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, destinationStepIdentifier, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, recordingSettings, NSDictionary);
|
||||
ORK_DECODE_OBJ_PLIST(aDecoder, recordingSettings);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -216,3 +216,4 @@ Float32 const VolumeClamp = 60.0;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
+14
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -29,26 +29,30 @@
|
||||
*/
|
||||
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@import UIKit;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKConsentSection;
|
||||
extern NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit);
|
||||
|
||||
@interface ORKConsentSceneViewController : UIViewController
|
||||
@protocol ORKAudioMetering <NSObject>
|
||||
|
||||
- (instancetype)initWithSection:(ORKConsentSection *)section;
|
||||
- (void)setSamples:(nullable NSArray<NSNumber *> *)samples;
|
||||
|
||||
@property (nonatomic, readonly, nullable) ORKConsentSection *section;
|
||||
- (void)setAlertThreshold:(float)threshold;
|
||||
|
||||
@property (nonatomic, strong, nullable) UIBarButtonItem *continueButtonItem;
|
||||
@end
|
||||
|
||||
@property (nonatomic, strong, nullable) UIBarButtonItem *cancelButtonItem;
|
||||
@protocol ORKAudioMeteringDisplay
|
||||
|
||||
@property (nonatomic, strong, nullable) NSString *learnMoreButtonTitle;
|
||||
- (void)setMeterColor:(nonnull UIColor *)meterColor;
|
||||
|
||||
@property (nonatomic, assign) BOOL imageHidden;
|
||||
- (void)setAlertColor:(nonnull UIColor *)alertColor;
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKAudioMeteringView : UIView <ORKAudioMetering, ORKAudioMeteringDisplay>
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAudioMeteringView.h"
|
||||
|
||||
|
||||
#import "ORKAudioGraphView.h"
|
||||
|
||||
NSArray<NSNumber *> * ORKLastNSamples(NSArray<NSNumber *> *samples, NSInteger limit) {
|
||||
|
||||
if (samples.count > limit) {
|
||||
|
||||
return [samples subarrayWithRange:(NSRange){samples.count - limit, samples.count - 1}];
|
||||
}
|
||||
|
||||
return [samples copy];
|
||||
}
|
||||
|
||||
@interface ORKAudioMeteringView ()
|
||||
@property (nonatomic, strong) UIView<ORKAudioMetering, ORKAudioMeteringDisplay> *meteringView;
|
||||
@end
|
||||
|
||||
@implementation ORKAudioMeteringView
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self)
|
||||
{
|
||||
[self configureMeteringView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)configureMeteringView
|
||||
{
|
||||
if (!_meteringView) {
|
||||
[self setMeteringView:[[ORKAudioGraphView alloc] init]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
[_meteringView setFrame:[self bounds]];
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden
|
||||
{
|
||||
[super setHidden:hidden];
|
||||
[_meteringView setHidden:hidden];
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
if ([self superview] == nil)
|
||||
{
|
||||
[_meteringView removeFromSuperview];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self addSubview:_meteringView];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMetering
|
||||
|
||||
- (void)setSamples:(NSArray<NSNumber *> *)samples
|
||||
{
|
||||
[_meteringView setSamples:samples];
|
||||
}
|
||||
|
||||
- (void)setAlertThreshold:(float)threshold
|
||||
{
|
||||
[_meteringView setAlertThreshold:threshold];
|
||||
}
|
||||
|
||||
#pragma mark - ORKAudioMeteringDisplay
|
||||
|
||||
- (void)setMeterColor:(UIColor *)meterColor
|
||||
{
|
||||
[_meteringView setMeterColor:meterColor];
|
||||
}
|
||||
|
||||
- (void)setAlertColor:(UIColor *)alertColor
|
||||
{
|
||||
[_meteringView setAlertColor:alertColor];
|
||||
}
|
||||
|
||||
#pragma mark - UIAccessibility
|
||||
|
||||
- (BOOL)isAccessibilityElement {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, recorderSettings, NSDictionary);
|
||||
ORK_DECODE_OBJ_PLIST(aDecoder, recorderSettings);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,18 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKAudioStep : ORKActiveStep
|
||||
|
||||
/**
|
||||
A Boolean value that determines if audio recording will start and stop
|
||||
automatcially or be controlled via a ORKRecordButton
|
||||
|
||||
When set to YES the user will be able to start and stop the audio recording
|
||||
by the ORKRecordButton
|
||||
|
||||
The default value of this property is `NO`.
|
||||
*/
|
||||
@property (nonatomic) BOOL useRecordButton;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -49,16 +49,27 @@
|
||||
if (self) {
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
self.shouldStartTimerAutomatically = YES;
|
||||
self.useRecordButton = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setUseRecordButton:(BOOL)useRecordButton {
|
||||
_useRecordButton = useRecordButton;
|
||||
|
||||
[self setShouldStartTimerAutomatically:!_useRecordButton];
|
||||
|
||||
if (_useRecordButton) {
|
||||
self.stepDuration = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
[super validateParameters];
|
||||
|
||||
NSTimeInterval const ORKAudioTaskMinimumDuration = 5.0;
|
||||
|
||||
if ( self.stepDuration < ORKAudioTaskMinimumDuration) {
|
||||
if ( self.stepDuration < ORKAudioTaskMinimumDuration && !self.useRecordButton) {
|
||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"duration cannot be shorter than %@ seconds.", @(ORKAudioTaskMinimumDuration)] userInfo:nil];
|
||||
}
|
||||
}
|
||||
@@ -67,4 +78,35 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKAudioStep *step = [super copyWithZone:zone];
|
||||
step.useRecordButton = self.useRecordButton;
|
||||
return step;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_BOOL(aDecoder, useRecordButton);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_BOOL(aCoder, useRecordButton);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame && self.useRecordButton == castObject.useRecordButton);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@import Foundation;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
@@ -44,3 +45,4 @@ ORK_CLASS_AVAILABLE
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
ORKAudioContentView *_audioContentView;
|
||||
ORKAudioRecorder *_audioRecorder;
|
||||
ORKActiveStepTimer *_timer;
|
||||
NSTimer *_intervalTimer;
|
||||
NSError *_audioRecorderError;
|
||||
}
|
||||
|
||||
@@ -83,6 +84,12 @@
|
||||
// Do any additional setup after loading the view.
|
||||
_audioContentView = [ORKAudioContentView new];
|
||||
_audioContentView.timeLeft = self.audioStep.stepDuration;
|
||||
_audioContentView.useRecordButton = self.audioStep.useRecordButton && self.audioStep.stepDuration == 0;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[_audioContentView setViewEventHandler:^(ORKAudioContentViewEvent event) {
|
||||
[weakSelf handleContentViewEvent:event];
|
||||
}];
|
||||
|
||||
if (self.alertThreshold > 0) {
|
||||
_audioContentView.alertThreshold = self.alertThreshold;
|
||||
@@ -91,6 +98,19 @@
|
||||
self.activeStepView.activeCustomView = _audioContentView;
|
||||
}
|
||||
|
||||
- (void)handleContentViewEvent:(ORKAudioContentViewEvent)event {
|
||||
|
||||
switch (event) {
|
||||
case ORKAudioContentViewEventStartRecording:
|
||||
[self start];
|
||||
break;
|
||||
|
||||
case ORKAudioContentViewEventStopRecording:
|
||||
[self finish];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)audioRecorderDidChange {
|
||||
_audioRecorder.audioRecorder.meteringEnabled = YES;
|
||||
[self setAvAudioRecorder:_audioRecorder.audioRecorder];
|
||||
@@ -116,42 +136,72 @@
|
||||
if (_audioRecorderError) {
|
||||
return;
|
||||
}
|
||||
|
||||
[_avAudioRecorder updateMeters];
|
||||
float value = [_avAudioRecorder averagePowerForChannel:0];
|
||||
// Assume value is in range roughly -60dB to 0dB
|
||||
float clampedValue = MAX(value / 60.0, -1) + 1;
|
||||
[_audioContentView addSample:@(clampedValue)];
|
||||
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
_audioContentView.timeLeft = [_timer duration] - [_timer runtime];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startNewTimerIfNeeded {
|
||||
if (!_timer) {
|
||||
NSTimeInterval duration = self.audioStep.stepDuration;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf doSample];
|
||||
if (finished) {
|
||||
[strongSelf finish];
|
||||
}
|
||||
}];
|
||||
[_timer resume];
|
||||
if (self.audioStep.useRecordButton) {
|
||||
|
||||
if (!_intervalTimer) {
|
||||
|
||||
_intervalTimer = [NSTimer scheduledTimerWithTimeInterval: 20 / 100
|
||||
target:self selector:@selector(doSample)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
} else {
|
||||
|
||||
if (!_timer) {
|
||||
NSTimeInterval duration = self.audioStep.stepDuration;
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
_timer = [[ORKActiveStepTimer alloc] initWithDuration:duration interval:duration / 100 runtime:0 handler:^(ORKActiveStepTimer *timer, BOOL finished) {
|
||||
ORKStrongTypeOf(self) strongSelf = weakSelf;
|
||||
[strongSelf doSample];
|
||||
if (finished) {
|
||||
[strongSelf finish];
|
||||
}
|
||||
}];
|
||||
[_timer resume];
|
||||
}
|
||||
}
|
||||
|
||||
_audioContentView.finished = NO;
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
[super start];
|
||||
[self audioRecorderDidChange];
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
[self startNewTimerIfNeeded];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
|
||||
[self startNewTimerIfNeeded];
|
||||
}
|
||||
|
||||
- (void)suspend {
|
||||
[super suspend];
|
||||
[_timer pause];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer pause];
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
|
||||
if (_avAudioRecorder) {
|
||||
[_audioContentView addSample:@(0)];
|
||||
}
|
||||
@@ -160,8 +210,12 @@
|
||||
- (void)resume {
|
||||
[super resume];
|
||||
[self audioRecorderDidChange];
|
||||
|
||||
[self startNewTimerIfNeeded];
|
||||
[_timer resume];
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer resume];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finish {
|
||||
@@ -169,8 +223,14 @@
|
||||
return;
|
||||
}
|
||||
[super finish];
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
|
||||
if (!self.audioStep.useRecordButton) {
|
||||
[_timer reset];
|
||||
_timer = nil;
|
||||
} else {
|
||||
[_intervalTimer invalidate];
|
||||
_intervalTimer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
@@ -189,3 +249,4 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import Foundation;
|
||||
@import AVFoundation;
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol ORKAudioStreamingDelegate <ORKRecorderDelegate>
|
||||
|
||||
- (void)audioAvailable:(AVAudioPCMBuffer *)buffer;
|
||||
|
||||
@end
|
||||
|
||||
@class ORKStep;
|
||||
|
||||
@interface ORKAudioStreamer : ORKRecorder
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier step:(nullable ORKStep *)step NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property (nonatomic, strong, readonly, nullable) AVAudioEngine *audioEngine;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKAudioStreamer.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "ORKStep.h"
|
||||
|
||||
#pragma mark - ORKAudioStreamerConfiguration
|
||||
|
||||
@implementation ORKAudioStreamerConfiguration
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (ORKRecorder *)recorderForStep:(ORKStep *)step outputDirectory:(NSURL *)outputDirectory {
|
||||
ORKAudioStreamer *obj = [[ORKAudioStreamer alloc] initWithIdentifier:self.identifier step:step];
|
||||
return obj;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
return [super isEqual:object];
|
||||
}
|
||||
|
||||
- (ORKPermissionMask)requestedPermissionMask {
|
||||
return ORKPermissionAudioRecording;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - ORKAudioStreamer
|
||||
|
||||
@implementation ORKAudioStreamer
|
||||
{
|
||||
NSString *_savedSessionCategory;
|
||||
}
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier step:(ORKStep *)step
|
||||
{
|
||||
self = [super initWithIdentifier:identifier step:step outputDirectory:nil];
|
||||
if (self)
|
||||
{
|
||||
self.continuesInBackground = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)restoreSavedAudioSessionCategory
|
||||
{
|
||||
if (_savedSessionCategory)
|
||||
{
|
||||
NSError *error;
|
||||
if (![[AVAudioSession sharedInstance] setCategory:_savedSessionCategory error:&error])
|
||||
{
|
||||
ORK_Log_Error("Failed to restore the audio session category: %@", [error localizedDescription]);
|
||||
}
|
||||
_savedSessionCategory = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isRecording
|
||||
{
|
||||
return [_audioEngine isRunning];
|
||||
}
|
||||
|
||||
- (NSString *)recorderType
|
||||
{
|
||||
return @"audioStreaming";
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
if (!_audioEngine)
|
||||
{
|
||||
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
|
||||
_savedSessionCategory = audioSession.category;
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success =
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error] &&
|
||||
[[AVAudioSession sharedInstance] setMode:AVAudioSessionModeMeasurement error:&error] &&
|
||||
[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
|
||||
|
||||
if (!success && error)
|
||||
{
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
ORK_Log_Debug("Create audioEngine recorder %p", self);
|
||||
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
AVAudioInputNode *inputnode = _audioEngine.inputNode;
|
||||
AVAudioFormat *recordingFormat = [inputnode inputFormatForBus:0];
|
||||
|
||||
[inputnode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when)
|
||||
{
|
||||
id<ORKAudioStreamingDelegate> delegate = (id<ORKAudioStreamingDelegate>)self.delegate;
|
||||
|
||||
if (delegate && [delegate respondsToSelector:@selector(audioAvailable:)]) {
|
||||
[delegate audioAvailable:buffer];
|
||||
}
|
||||
}];
|
||||
|
||||
[_audioEngine prepare];
|
||||
|
||||
[_audioEngine startAndReturnError:&error];
|
||||
|
||||
if (error != nil)
|
||||
{
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[super start];
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
if (!_audioEngine)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
[self doStopRecording];
|
||||
|
||||
[super stop];
|
||||
}
|
||||
|
||||
- (void)doStopRecording
|
||||
{
|
||||
if (self.isRecording)
|
||||
{
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
_audioEngine = nil;
|
||||
|
||||
[self restoreSavedAudioSessionCategory];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finishRecordingWithError:(NSError *)error
|
||||
{
|
||||
[self doStopRecording];
|
||||
|
||||
[super finishRecordingWithError:error];
|
||||
}
|
||||
|
||||
- (void)reset
|
||||
{
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
_audioEngine = nil;
|
||||
[super reset];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
ORK_Log_Debug("Remove audiorecorder %p", self);
|
||||
|
||||
if ([_audioEngine isRunning])
|
||||
{
|
||||
[_audioEngine stop];
|
||||
[[_audioEngine inputNode] removeTapOnBus:0];
|
||||
}
|
||||
|
||||
_audioEngine = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -242,6 +242,14 @@ static const CGFloat ProgressIndicatorOuterMargin = 1.0;
|
||||
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @(_countDown).stringValue);
|
||||
[_countdownView startAnimateWithDuration:[(ORKActiveStep *)self.step stepDuration]];
|
||||
|
||||
[UIApplication.sharedApplication setIdleTimerDisabled:YES];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[UIApplication.sharedApplication setIdleTimerDisabled:NO];
|
||||
|
||||
[super viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
- (void)updateCountdownLabel {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ORKEnvironmentSPLMeterBarView : UIView
|
||||
|
||||
- (void)setProgress:(CGFloat)progress;
|
||||
- (void)stopAnimation;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
Copyright (c) 2021, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKEnvironmentSPLMeterBarView.h"
|
||||
|
||||
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
static const CGFloat ORKEnvironmentSPLMeterSquareSize = 8.0;
|
||||
static const CGFloat ORKEnvironmentSPLMeterSquareDistance = 4.0;
|
||||
static const int ORKEnvironmentSPLMeterNumberOfRows = 4;
|
||||
|
||||
@interface ORKEnvironmentSPLMeterColumnView : UIView {
|
||||
int _numberOfRows;
|
||||
CGFloat _squareSize;
|
||||
CGFloat _cornerRadius;
|
||||
|
||||
NSArray<CAShapeLayer*> *_dots;
|
||||
}
|
||||
|
||||
- (void)setColor:(UIColor *)color;
|
||||
- (void)setOpacity:(CGFloat)opacity;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterColumnView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_numberOfRows = ORKEnvironmentSPLMeterNumberOfRows;
|
||||
_squareSize = ORKEnvironmentSPLMeterSquareSize;
|
||||
_cornerRadius = ORKEnvironmentSPLMeterSquareDistance;
|
||||
[self initRows];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initRows {
|
||||
CGFloat halfSquareSize = _squareSize * 0.5;
|
||||
CGFloat spacing = _squareSize + halfSquareSize;
|
||||
NSMutableArray<CAShapeLayer*> *dots = [[NSMutableArray alloc] init];
|
||||
for (int i = 0; i < _numberOfRows; i++) {
|
||||
CAShapeLayer *dot = [CAShapeLayer layer];
|
||||
CGRect dotRect = CGRectMake(0,
|
||||
spacing * i,
|
||||
_squareSize, _squareSize);
|
||||
[dot setPath:[UIBezierPath bezierPathWithRoundedRect:dotRect
|
||||
cornerRadius:_cornerRadius].CGPath];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
dot.fillColor = [UIColor systemGray6Color].CGColor;
|
||||
}
|
||||
[[self layer] addSublayer:dot];
|
||||
|
||||
[dots addObject:dot];
|
||||
}
|
||||
|
||||
_dots = [dots copy];
|
||||
|
||||
}
|
||||
|
||||
- (void)setOpacity:(CGFloat)opacity {
|
||||
for (NSInteger i = 0 ; i < _dots.count; i++) {
|
||||
CAShapeLayer *dot = _dots[i];
|
||||
dot.opacity = opacity;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setColor:(UIColor *)color {
|
||||
[_dots makeObjectsPerformSelector:@selector(setFillColor:) withObject:(id)[color CGColor]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKEnvironmentSPLMeterBarView () {
|
||||
NSArray<ORKEnvironmentSPLMeterColumnView *> *_columnViews;
|
||||
|
||||
int _currentIndex;
|
||||
int _targetIndex;
|
||||
int _maximumNumberOfDots;
|
||||
int _greenIndexLimit;
|
||||
|
||||
BOOL _didLayoutViews;
|
||||
BOOL _isAnimating;
|
||||
|
||||
NSTimer *_animationTimer;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterBarView
|
||||
|
||||
- (void)didMoveToSuperview {
|
||||
[super didMoveToSuperview];
|
||||
|
||||
_didLayoutViews = NO;
|
||||
_isAnimating = NO;
|
||||
}
|
||||
|
||||
- (void)setupView {
|
||||
CGFloat width = CGRectGetWidth(self.frame);
|
||||
CGFloat dotSpacing = (ORKEnvironmentSPLMeterSquareSize + ORKEnvironmentSPLMeterSquareDistance);
|
||||
_maximumNumberOfDots = (int) (floor(width/dotSpacing)) + 1;
|
||||
NSMutableArray<ORKEnvironmentSPLMeterColumnView*> *columnViews = [[NSMutableArray alloc] init];
|
||||
_greenIndexLimit = _maximumNumberOfDots * 0.66;
|
||||
_currentIndex = 0;
|
||||
_targetIndex = _greenIndexLimit;
|
||||
|
||||
for (int i = 1 ; i <= _maximumNumberOfDots; i++) {
|
||||
CGRect columnRect = CGRectMake((i - 1) * dotSpacing,
|
||||
0, ORKEnvironmentSPLMeterSquareSize, ORKEnvironmentSPLMeterSquareSize);
|
||||
|
||||
ORKEnvironmentSPLMeterColumnView *columnView = [[ORKEnvironmentSPLMeterColumnView alloc] initWithFrame:columnRect];
|
||||
|
||||
if (i <= _greenIndexLimit - 1) {
|
||||
[columnView setColor:[UIColor systemGreenColor]];
|
||||
} else {
|
||||
[columnView setColor:[UIColor systemOrangeColor]];
|
||||
}
|
||||
|
||||
[self addSubview:columnView];
|
||||
|
||||
[columnViews addObject:columnView];
|
||||
}
|
||||
|
||||
_columnViews = [columnViews copy];
|
||||
|
||||
[self updateViewForIndex:_currentIndex];
|
||||
|
||||
[self animateColumns];
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress {
|
||||
CGFloat resultProgress = progress;
|
||||
if (progress == 20.0) {
|
||||
return;
|
||||
}
|
||||
if(progress < 0) {
|
||||
resultProgress = 0.0;
|
||||
}
|
||||
|
||||
float inMin = 0.0;
|
||||
float inMax = 1.0;
|
||||
float outMin = 0.0;
|
||||
float outMax = 0.66;
|
||||
|
||||
float normalizedIndexValue = outMin + (outMax - outMin) * (resultProgress - inMin) / (inMax - inMin);
|
||||
|
||||
if (resultProgress > 1.0) {
|
||||
inMin = 1.0;
|
||||
inMax = 1.5;
|
||||
outMin = 0.66;
|
||||
outMax = 1.0;
|
||||
|
||||
normalizedIndexValue = outMin + (outMax - outMin) * (resultProgress - inMin) / (inMax - inMin);
|
||||
}
|
||||
|
||||
int newTargetIndex = (int) (floor(normalizedIndexValue * _maximumNumberOfDots) + 1);
|
||||
|
||||
if (newTargetIndex != _targetIndex) {
|
||||
[self stopAnimation];
|
||||
_targetIndex = newTargetIndex;
|
||||
_currentIndex = _targetIndex + (-1 + arc4random_uniform(3));
|
||||
[self updateViewForIndex:newTargetIndex];
|
||||
} else if (!_isAnimating) {
|
||||
int indexDistance = abs(_currentIndex - newTargetIndex);
|
||||
for (int i = 0; i < indexDistance; i++) {
|
||||
int newIndex;
|
||||
if (newTargetIndex < _currentIndex) {
|
||||
newIndex = _currentIndex - i;
|
||||
} else {
|
||||
newIndex = _currentIndex + i;
|
||||
}
|
||||
[self updateViewForIndex:newIndex];
|
||||
}
|
||||
|
||||
[self animateColumns];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)animateColumns {
|
||||
[_animationTimer invalidate];
|
||||
_isAnimating = YES;
|
||||
_animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(timerTicked) userInfo:nil repeats:YES];
|
||||
}
|
||||
|
||||
- (void)timerTicked {
|
||||
if (_currentIndex > _targetIndex) {
|
||||
_currentIndex = _currentIndex - 1;
|
||||
} else if (_currentIndex < _targetIndex) {
|
||||
_currentIndex = _currentIndex + 1;
|
||||
} else {
|
||||
_currentIndex = _currentIndex + (-1 + arc4random_uniform(3));
|
||||
}
|
||||
[self updateViewForIndex:_currentIndex];
|
||||
}
|
||||
|
||||
- (void)updateViewForIndex:(int)index {
|
||||
for (int i = 0 ; i < _maximumNumberOfDots; i++) {
|
||||
ORKEnvironmentSPLMeterColumnView *columnView = _columnViews[i];
|
||||
NSInteger distanceToIndex = i - index;
|
||||
CGFloat opacityFactor = 0.1 * distanceToIndex;
|
||||
UIColor *grayColor;
|
||||
UIColor *greenColor;
|
||||
UIColor *orangeColor;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
|
||||
greenColor = [UIColor systemGreenColor];
|
||||
orangeColor = [UIColor systemOrangeColor];
|
||||
} else {
|
||||
grayColor = [UIColor grayColor];
|
||||
greenColor = [UIColor greenColor];
|
||||
orangeColor = [UIColor orangeColor];
|
||||
}
|
||||
if (i <= _greenIndexLimit) {
|
||||
if (i < index) {
|
||||
[columnView setColor:greenColor];
|
||||
[columnView setOpacity:1.0];
|
||||
} else {
|
||||
if (distanceToIndex < 3){
|
||||
[columnView setColor:greenColor];
|
||||
[columnView setOpacity:0.5 - opacityFactor];
|
||||
} else {
|
||||
[columnView setColor:grayColor];
|
||||
[columnView setOpacity:1.0];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (i < index) {
|
||||
[columnView setColor:orangeColor];
|
||||
[columnView setOpacity:1.0];
|
||||
} else {
|
||||
if (distanceToIndex < 3){
|
||||
[columnView setColor:orangeColor];
|
||||
[columnView setOpacity:0.5 - opacityFactor];
|
||||
} else {
|
||||
[columnView setColor:grayColor];
|
||||
[columnView setOpacity:1.0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopAnimation {
|
||||
_isAnimating = NO;
|
||||
[_animationTimer invalidate];
|
||||
_animationTimer = nil;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
if (!_didLayoutViews) {
|
||||
_didLayoutViews = YES;
|
||||
[self setupView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_animationTimer invalidate];
|
||||
_animationTimer = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ORKEnvironmentSPLMeterBarView;
|
||||
@class ORKRingView;
|
||||
@class ORKRoundTappingButton;
|
||||
@class ORKNavigationContainerView;
|
||||
@@ -50,12 +51,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, weak) id<ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> voiceOverDelegate;
|
||||
|
||||
- (ORKEnvironmentSPLMeterBarView *)barView;
|
||||
|
||||
- (ORKRingView *)ringView;
|
||||
|
||||
- (void)setProgress:(CGFloat)progress;
|
||||
|
||||
- (void)setProgressCircle:(CGFloat)progress;
|
||||
|
||||
- (void)setProgressBar:(CGFloat)progress;
|
||||
|
||||
- (void)reachedOptimumNoiseLevel;
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKEnvironmentSPLMeterContentView.h"
|
||||
|
||||
#import "ORKEnvironmentSPLMeterBarView.h"
|
||||
#import "ORKRoundTappingButton.h"
|
||||
#import "ORKUnitLabel.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
@@ -39,63 +39,25 @@
|
||||
#import "ORKProgressView.h"
|
||||
#import "ORKCompletionCheckmarkView.h"
|
||||
|
||||
static const CGFloat CircleIndicatorMaxDiameter = 150.0;
|
||||
static const CGFloat RingViewTopPadding = 24.0;
|
||||
static const CGFloat InstructionLabelTopPadding = 50.0;
|
||||
static const CGFloat InstructionLabelBottomPadding = 10.0;
|
||||
|
||||
static CGFloat CircleIndicatorViewScaleFactorForProgress(CGFloat progress) {
|
||||
|
||||
CGFloat y1 = 0.5, x1 = 0.8, y2 = 1.4, x2 = 1.2;
|
||||
|
||||
if (progress < x1) // lower limit for diameter
|
||||
{
|
||||
return y1;
|
||||
}
|
||||
else if (progress > x2) // upper limit for diameter
|
||||
{
|
||||
return y2;
|
||||
}
|
||||
else // linear interpolation
|
||||
{
|
||||
return y1 + (y2 - y1)/(x2 - x1) * (progress - x1);
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat CircleIndicatorPulseVarianceForProgress(CGFloat progress) {
|
||||
|
||||
// Linear Interpolation
|
||||
// kMin: Lower bound of interpolation. (Matches above)
|
||||
// kMax: Higher bound of interpolation. (Matches above)
|
||||
// min: Lower bound of variance.
|
||||
// max: Higher bound of variance.
|
||||
CGFloat min = 0.0075, max = 0.025;
|
||||
CGFloat kMin = 0.8, kMax = 1.2;
|
||||
|
||||
if (progress < kMin)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
else if (progress > kMax)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
else
|
||||
{
|
||||
return min + (max - min)/(kMax - kMin) * (progress - kMin);
|
||||
}
|
||||
}
|
||||
static const CGFloat RingViewPadding = 18.0;
|
||||
static const CGFloat InstructionLabelPadding = 8.0;
|
||||
static const CGFloat HalfCircleSize = 14.0;
|
||||
static const CGFloat BarViewHeight = 50.0;
|
||||
|
||||
@interface ORKEnvironmentSPLMeterContentView ()
|
||||
@property(nonatomic, strong) ORKRingView *ringView;
|
||||
@property(nonatomic, strong) ORKEnvironmentSPLMeterBarView *barView;
|
||||
@end
|
||||
|
||||
@implementation ORKEnvironmentSPLMeterContentView {
|
||||
UIView *_circleIndicatorView;
|
||||
UIView *_containerView;
|
||||
UILabel *_DBInstructionLabel;
|
||||
UIImage *_checkmarkImage;
|
||||
UIImage *_xmarkImage;
|
||||
UIImageView *_xmarkView;
|
||||
CGFloat preValue;
|
||||
CGFloat currentValue;
|
||||
UIColor *_circleIndicatorNoiseColor;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
@@ -103,133 +65,144 @@ static CGFloat CircleIndicatorPulseVarianceForProgress(CGFloat progress) {
|
||||
if (self) {
|
||||
preValue = -M_PI_2;
|
||||
currentValue = 0.0;
|
||||
_circleIndicatorNoiseColor = UIColor.systemOrangeColor;
|
||||
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self setupRingView];
|
||||
[self setupCircleIndicatorView];
|
||||
[self setProgressCircle:0.0];
|
||||
[self setupContainerView];
|
||||
[self setupDBInstructionLabel];
|
||||
[self setupRingView];
|
||||
[self setupBarView];
|
||||
[self setupXmarkView];
|
||||
[self setProgressCircle:0.0];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
||||
[super traitCollectionDidChange:previousTraitCollection];
|
||||
_DBInstructionLabel.font = [self title3TextFont];
|
||||
}
|
||||
|
||||
- (UIFont *)title3TextFont {
|
||||
UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleTitle3];
|
||||
UIFontDescriptor *fontDescriptor = [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
|
||||
return [UIFont fontWithDescriptor:fontDescriptor size:[[descriptor objectForKey: UIFontDescriptorSizeAttribute] doubleValue]];
|
||||
}
|
||||
|
||||
- (void)setupContainerView {
|
||||
if (!_containerView) {
|
||||
_containerView = [UIView new];
|
||||
}
|
||||
_containerView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_containerView];
|
||||
|
||||
[[_containerView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:-RingViewPadding] setActive:YES];
|
||||
[[_containerView.topAnchor constraintGreaterThanOrEqualToAnchor:self.topAnchor] setActive:YES];
|
||||
[[_containerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
|
||||
[[_containerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
|
||||
[[_containerView.topAnchor constraintLessThanOrEqualToAnchor:self.bottomAnchor] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)setupXmarkView {
|
||||
if (!_xmarkView) {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageConfiguration *configuration = [UIImageSymbolConfiguration configurationWithPointSize:HalfCircleSize weight:UIImageSymbolWeightBold scale:UIImageSymbolScaleDefault];
|
||||
_xmarkImage = [[UIImage systemImageNamed:@"xmark" withConfiguration:configuration] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
_checkmarkImage = [[UIImage systemImageNamed:@"checkmark" withConfiguration:configuration] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
}
|
||||
_xmarkView = [[UIImageView alloc] initWithImage: _xmarkImage];
|
||||
_xmarkView.tintColor = UIColor.systemOrangeColor;
|
||||
}
|
||||
_xmarkView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_containerView addSubview:_xmarkView];
|
||||
|
||||
[[_xmarkView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
|
||||
[[_xmarkView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
|
||||
_xmarkView.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)setupBarView {
|
||||
if (!_barView) {
|
||||
_barView = [[ORKEnvironmentSPLMeterBarView alloc] initWithFrame:CGRectZero];
|
||||
}
|
||||
|
||||
_barView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_containerView addSubview:_barView];
|
||||
|
||||
[[_barView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor] setActive:YES];
|
||||
[[_barView.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor] setActive:YES];
|
||||
|
||||
[[_barView.heightAnchor constraintEqualToConstant:BarViewHeight] setActive:YES];
|
||||
[[_barView.topAnchor constraintEqualToAnchor:_DBInstructionLabel.bottomAnchor constant:RingViewPadding] setActive:YES];
|
||||
[[_barView.bottomAnchor constraintEqualToAnchor:_containerView.bottomAnchor constant:RingViewPadding] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)setupRingView {
|
||||
if (!_ringView) {
|
||||
_ringView = [ORKRingView new];
|
||||
}
|
||||
_ringView.animationDuration = 0.0;
|
||||
_ringView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_ringView];
|
||||
|
||||
[[_ringView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
|
||||
[[_ringView.topAnchor constraintEqualToAnchor:self.topAnchor constant:RingViewTopPadding] setActive:YES];
|
||||
[_ringView setColor:UIColor.grayColor];
|
||||
}
|
||||
[_containerView addSubview:_ringView];
|
||||
|
||||
- (void)setupCircleIndicatorView {
|
||||
if (!_circleIndicatorView) {
|
||||
_circleIndicatorView = [UIView new];
|
||||
[[_ringView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor] setActive:YES];
|
||||
[[_ringView.centerYAnchor constraintEqualToAnchor:_DBInstructionLabel.centerYAnchor] setActive:YES];
|
||||
[[_ringView.trailingAnchor constraintEqualToAnchor:_DBInstructionLabel.leadingAnchor constant:-InstructionLabelPadding] setActive:YES];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
[_ringView setColor:UIColor.systemGray6Color];
|
||||
}
|
||||
_circleIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self insertSubview:_circleIndicatorView belowSubview:_ringView];
|
||||
|
||||
[[_circleIndicatorView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
|
||||
[[_circleIndicatorView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
|
||||
[[_circleIndicatorView.heightAnchor constraintEqualToConstant:CircleIndicatorMaxDiameter] setActive:YES];
|
||||
[[_circleIndicatorView.widthAnchor constraintEqualToConstant:CircleIndicatorMaxDiameter] setActive:YES];
|
||||
_circleIndicatorView.layer.cornerRadius = CircleIndicatorMaxDiameter * 0.5;
|
||||
}
|
||||
|
||||
- (void)setupDBInstructionLabel {
|
||||
if (!_DBInstructionLabel) {
|
||||
_DBInstructionLabel = [ORKLabel new];
|
||||
_DBInstructionLabel.numberOfLines = 0;
|
||||
_DBInstructionLabel.textColor = UIColor.systemGrayColor;
|
||||
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
|
||||
_DBInstructionLabel.font = [self title3TextFont];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_DBInstructionLabel.textColor = UIColor.labelColor;
|
||||
}
|
||||
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil);
|
||||
}
|
||||
_DBInstructionLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_DBInstructionLabel];
|
||||
|
||||
[[_DBInstructionLabel.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
|
||||
[[_DBInstructionLabel.topAnchor constraintEqualToAnchor:_circleIndicatorView.bottomAnchor constant:InstructionLabelTopPadding] setActive:YES];
|
||||
[[_DBInstructionLabel.bottomAnchor constraintLessThanOrEqualToAnchor:self.bottomAnchor constant:-InstructionLabelBottomPadding] setActive:YES];
|
||||
[_containerView addSubview:_DBInstructionLabel];
|
||||
|
||||
[[_DBInstructionLabel.topAnchor constraintEqualToAnchor:_containerView.topAnchor constant:InstructionLabelPadding] setActive:YES];
|
||||
[[_DBInstructionLabel.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor constant:-InstructionLabelPadding] setActive:YES];
|
||||
}
|
||||
|
||||
- (void)setProgressBar:(CGFloat)progress {
|
||||
[_barView setProgress:progress];
|
||||
}
|
||||
|
||||
- (void)setProgressCircle:(CGFloat)progress {
|
||||
|
||||
CGFloat circleDiameter = CircleIndicatorViewScaleFactorForProgress(progress);
|
||||
CGFloat variance = CircleIndicatorPulseVarianceForProgress(progress);
|
||||
|
||||
[self startPulsingWithTranformScaleFactor:circleDiameter variance:variance];
|
||||
|
||||
if (progress >= ORKRingViewMaximumValue)
|
||||
{
|
||||
|
||||
[_ringView setBackgroundLayerStrokeColor:[UIColor.whiteColor colorWithAlphaComponent:0.3] circleStrokeColor:UIColor.whiteColor withAnimationDuration:0.8];
|
||||
}
|
||||
else
|
||||
{
|
||||
if (progress >= ORKRingViewMaximumValue) {
|
||||
} else {
|
||||
[_ringView resetLayerColors];
|
||||
}
|
||||
|
||||
[UIView animateWithDuration:0.8
|
||||
delay:0
|
||||
options:UIViewAnimationOptionCurveLinear
|
||||
animations:^{
|
||||
_circleIndicatorView.transform = CGAffineTransformMakeScale(circleDiameter, circleDiameter);
|
||||
_circleIndicatorView.backgroundColor = progress >= ORKRingViewMaximumValue ? _circleIndicatorNoiseColor : self.tintColor;
|
||||
} completion:nil];
|
||||
|
||||
|
||||
[self updateInstructionForValue:progress];
|
||||
}
|
||||
|
||||
- (ORKRingView *)ringView
|
||||
{
|
||||
- (ORKRingView *)ringView {
|
||||
return _ringView;
|
||||
}
|
||||
|
||||
- (void)startPulsingWithTranformScaleFactor:(CGFloat)transformScaleFactor variance:(CGFloat)variance {
|
||||
|
||||
[self stopPulsing];
|
||||
|
||||
CAKeyframeAnimation *pulse = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.xy"];
|
||||
pulse.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
||||
pulse.repeatCount = MAXFLOAT;
|
||||
pulse.duration = 0.6;
|
||||
pulse.values = @[
|
||||
@(transformScaleFactor),
|
||||
@(transformScaleFactor * (1 - variance)),
|
||||
@(transformScaleFactor),
|
||||
@(transformScaleFactor * (1 + variance)),
|
||||
@(transformScaleFactor)
|
||||
];
|
||||
|
||||
[_circleIndicatorView.layer addAnimation:pulse forKey:@"pulse"];
|
||||
}
|
||||
|
||||
- (void)stopPulsing {
|
||||
[_circleIndicatorView.layer removeAnimationForKey:@"pulse"];
|
||||
}
|
||||
|
||||
- (void)setProgress:(CGFloat)progress {
|
||||
CGFloat value = progress < ORKRingViewMinimumValue ? ORKRingViewMinimumValue : progress;
|
||||
[_ringView setValue:value];
|
||||
}
|
||||
|
||||
- (void)updateInstructionForValue:(CGFloat)progress
|
||||
{
|
||||
- (void)updateInstructionForValue:(CGFloat)progress {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
NSString *currentInstruction = [_DBInstructionLabel.text copy];
|
||||
NSString *newInstruction = progress >= ORKRingViewMaximumValue ? ORKLocalizedString(@"ENVIRONMENTSPL_NOISE", nil) : ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
|
||||
|
||||
if (![newInstruction isEqualToString:currentInstruction])
|
||||
{
|
||||
BOOL isNoise = (progress >= ORKRingViewMaximumValue);
|
||||
NSString *newInstruction = isNoise ? ORKLocalizedString(@"ENVIRONMENTSPL_NOISE", nil) : ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil);
|
||||
_xmarkView.hidden = !isNoise;
|
||||
|
||||
if (![newInstruction isEqualToString:currentInstruction]) {
|
||||
_DBInstructionLabel.text = newInstruction;
|
||||
|
||||
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)])
|
||||
{
|
||||
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)]) {
|
||||
[self.voiceOverDelegate contentView:self shouldAnnounce:_DBInstructionLabel.text];
|
||||
}
|
||||
}
|
||||
@@ -241,15 +214,19 @@ static CGFloat CircleIndicatorPulseVarianceForProgress(CGFloat progress) {
|
||||
}
|
||||
|
||||
- (void)reachedOptimumNoiseLevel {
|
||||
[self stopPulsing];
|
||||
_ringView.hidden = YES;
|
||||
_circleIndicatorView.hidden = YES;
|
||||
ORKCompletionCheckmarkView *checkmarkView = [[ORKCompletionCheckmarkView alloc] initWithDimension:_ringView.bounds.size.width];
|
||||
checkmarkView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self insertSubview:checkmarkView aboveSubview:_ringView];
|
||||
[[checkmarkView.centerXAnchor constraintEqualToAnchor:_ringView.centerXAnchor] setActive:YES];
|
||||
[[checkmarkView.centerYAnchor constraintEqualToAnchor:_ringView.centerYAnchor] setActive:YES];
|
||||
[checkmarkView setAnimationPoint:1 animated:YES];
|
||||
_xmarkView.hidden = NO;
|
||||
_xmarkView.image = _checkmarkImage;
|
||||
_xmarkView.tintColor = UIColor.systemGreenColor;
|
||||
|
||||
_DBInstructionLabel.text = ORKLocalizedString(@"ENVIRONMENTSPL_OK", nil);
|
||||
|
||||
if (UIAccessibilityIsVoiceOverRunning() && [self.voiceOverDelegate respondsToSelector:@selector(contentView:shouldAnnounce:)]) {
|
||||
[self.voiceOverDelegate contentView:self shouldAnnounce:_DBInstructionLabel.text];
|
||||
}
|
||||
|
||||
[_ringView setBackgroundLayerStrokeColor:UIColor.systemGreenColor circleStrokeColor:UIColor.systemGreenColor withAnimationDuration:0.0];
|
||||
|
||||
[_barView stopAnimation];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
#import "ORKEnvironmentSPLMeterStep.h"
|
||||
#import "ORKEnvironmentSPLMeterStepViewController.h"
|
||||
|
||||
#import "ORKRecorder_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#define ORKEnvironmentSPLMeterTaskDefaultThresholdValue 35.0
|
||||
@@ -58,6 +58,10 @@
|
||||
self.requiredContiguousSamples = ORKEnvironmentSPLMeterTaskDefaultRequiredContiguousSamples;
|
||||
self.stepDuration = CGFLOAT_MAX;
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
|
||||
// This is inserted here because it is required for any task that requires the SPL Meter step
|
||||
ORKAudioStreamerConfiguration *config = [[ORKAudioStreamerConfiguration alloc] initWithIdentifier:[NSString stringWithFormat:@"%@_streamerConfiguration",self.identifier]];
|
||||
self.recorderConfigurations = @[config];
|
||||
}
|
||||
|
||||
- (void)validateParameters {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
#import "ORKEnvironmentSPLMeterStepViewController.h"
|
||||
|
||||
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKStepView.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
@@ -40,6 +41,7 @@
|
||||
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKTaskViewController_Internal.h"
|
||||
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKEnvironmentSPLMeterResult.h"
|
||||
@@ -51,6 +53,8 @@
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
static const NSTimeInterval SPL_METER_PLAY_DELAY_VOICEOVER = 3.0;
|
||||
|
||||
@interface ORKEnvironmentSPLMeterStepViewController ()<ORKRingViewDelegate, ORKEnvironmentSPLMeterContentViewVoiceOverDelegate> {
|
||||
AVAudioEngine *_audioEngine;
|
||||
AVAudioInputNode *_inputNode;
|
||||
@@ -74,6 +78,7 @@
|
||||
AVAudioSessionCategoryOptions _savedSessionCategoryOptions;
|
||||
UINotificationFeedbackGenerator *_notificationFeedbackGenerator;
|
||||
dispatch_semaphore_t _voiceOverAnnouncementSemaphore;
|
||||
NSTimer *_timeoutTimer;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) ORKEnvironmentSPLMeterContentView *environmentSPLMeterContentView;
|
||||
@@ -95,6 +100,8 @@
|
||||
_requiredContiguousSamples = 1;
|
||||
_sensitivityOffset = -23.3;
|
||||
_recordedSamples = [NSMutableArray new];
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -109,20 +116,12 @@
|
||||
_environmentSPLMeterContentView.voiceOverDelegate = self;
|
||||
_environmentSPLMeterContentView.ringView.delegate = self;
|
||||
self.activeStepView.activeCustomView = _environmentSPLMeterContentView;
|
||||
self.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
[self requestMicrophoneAuthorization];
|
||||
|
||||
[self requestRecordPermissionIfNeeded];
|
||||
[self configureAudioSession];
|
||||
_audioEngine = [[AVAudioEngine alloc] init];
|
||||
_eqUnit = [[AVAudioUnitEQ alloc] initWithNumberOfBands:6];
|
||||
_inputNode = [_audioEngine inputNode];
|
||||
_inputNodeOutputFormat = [_inputNode inputFormatForBus:0];
|
||||
_sampleRate = (uint32_t)_inputNodeOutputFormat.sampleRate;
|
||||
_bufferSize = _sampleRate/10;
|
||||
_countToFetch = _sampleRate/(int)_bufferSize;
|
||||
[self configureEQ];
|
||||
[_audioEngine attachNode:_eqUnit];
|
||||
[_audioEngine connect:_inputNode to:_eqUnit format:_inputNodeOutputFormat];
|
||||
[self setupFeedbackGenerator];
|
||||
|
||||
[self.taskViewController setNavigationBarColor:[self.view backgroundColor]];
|
||||
}
|
||||
|
||||
- (void)saveAudioSession {
|
||||
@@ -133,6 +132,7 @@
|
||||
}
|
||||
|
||||
- (void)setNavigationFooterView {
|
||||
|
||||
self.activeStepView.navigationFooterView.continueButtonItem = self.continueButtonItem;
|
||||
self.activeStepView.navigationFooterView.continueEnabled = NO;
|
||||
[self.activeStepView.navigationFooterView updateContinueAndSkipEnabled];
|
||||
@@ -143,6 +143,19 @@
|
||||
_navigationFooterView.continueButtonItem = continueButtonItem;
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (!_audioEngine.isRunning) {
|
||||
[self saveAudioSession];
|
||||
_sensitivityOffset = [self sensitivityOffsetForDevice];
|
||||
[self requestRecordPermissionIfNeeded];
|
||||
[self configureAudioSession];
|
||||
[self setupFeedbackGenerator];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
@@ -151,15 +164,21 @@
|
||||
_samplingInterval = [self environmentSPLMeterStep].samplingInterval;
|
||||
_requiredContiguousSamples = [self environmentSPLMeterStep].requiredContiguousSamples;
|
||||
_thresholdValue = [self environmentSPLMeterStep].thresholdValue;
|
||||
|
||||
[self configureInputNode];
|
||||
[self splWorkBlock];
|
||||
|
||||
if (UIAccessibilityIsVoiceOverRunning()) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SPL_METER_PLAY_DELAY_VOICEOVER * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, ORKLocalizedString(@"ENVIRONMENTSPL_CALCULATING", nil));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
[self stopAudioEngine];
|
||||
[self resetAudioSession];
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
}
|
||||
|
||||
- (NSString *)deviceType {
|
||||
@@ -194,45 +213,89 @@
|
||||
return sResult;
|
||||
}
|
||||
|
||||
- (void)requestMicrophoneAuthorization {
|
||||
[[AVAudioSession sharedInstance] recordPermission];
|
||||
|
||||
- (void)requestRecordPermissionIfNeeded
|
||||
{
|
||||
[self handleRecordPermission:[[AVAudioSession sharedInstance] recordPermission]];
|
||||
}
|
||||
|
||||
- (void)handleRecordPermission:(AVAudioSessionRecordPermission)recordPermission
|
||||
{
|
||||
switch (recordPermission)
|
||||
{
|
||||
case AVAudioSessionRecordPermissionGranted:
|
||||
break;
|
||||
|
||||
case AVAudioSessionRecordPermissionDenied:
|
||||
{
|
||||
ORK_Log_Error("User has denied record permission for a step which requires microphone access.");
|
||||
break;
|
||||
}
|
||||
case AVAudioSessionRecordPermissionUndetermined:
|
||||
{
|
||||
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
|
||||
[self handleRecordPermission:granted ? AVAudioSessionRecordPermissionGranted : AVAudioSessionRecordPermissionDenied];
|
||||
}];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configureAudioSession {
|
||||
NSError *error = nil;
|
||||
|
||||
AVAudioSession * session = [AVAudioSession sharedInstance];
|
||||
|
||||
// Stop any existing audio
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategorySoloAmbient error:&error];
|
||||
[session setCategory:AVAudioSessionCategorySoloAmbient error:&error];
|
||||
if (error) {
|
||||
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&error];
|
||||
[session setActive:YES error:&error];
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
// Force input/output from iOS device
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
|
||||
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error("Setting AVAudioSessionCategory failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
|
||||
// Override Output (and Input) to use built-in mic and speaker.
|
||||
// We need to make sure audio output is to the Headphones and Audio Input is uing the built-in mic.
|
||||
// Although this forces both to the built-in mic AND Speaker, we need to also override the speaker.
|
||||
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
|
||||
if (error)
|
||||
{
|
||||
ORK_Log_Error("Setting AVAudioSessionPortOverrideSpeaker failed with error message: \"%@\"", error.localizedDescription);
|
||||
// When setting the input like this, we do not need to set the input AND the output to the iPhone.
|
||||
NSArray<AVAudioSessionPortDescription *> * inputs = [session availableInputs];
|
||||
for (AVAudioSessionPortDescription* desc in inputs) {
|
||||
if ([desc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
|
||||
// go ahead and set our preferred input to the built-in mic
|
||||
[session setPreferredInput:desc error:&error];
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error("Setting AVAudioSession preferred input failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&error];
|
||||
[session setActive:YES error:&error];
|
||||
|
||||
if (error) {
|
||||
ORK_Log_Error("Activating AVAudioSession failed with error message: \"%@\"", error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configureInputNode {
|
||||
_inputNode = [_audioEngine inputNode];
|
||||
_inputNodeOutputFormat = [_inputNode inputFormatForBus:0];
|
||||
_sampleRate = (uint32_t)_inputNodeOutputFormat.sampleRate;
|
||||
_bufferSize = _sampleRate/10;
|
||||
_countToFetch = _sampleRate > 0 ? _sampleRate/(int)_bufferSize : 0;
|
||||
[self configureEQ];
|
||||
[_audioEngine attachNode:_eqUnit];
|
||||
[_audioEngine connect:_inputNode to:_eqUnit format:_inputNodeOutputFormat];
|
||||
}
|
||||
|
||||
- (void)configureEQ {
|
||||
_eqUnit.globalGain = 0;
|
||||
|
||||
@@ -320,12 +383,23 @@
|
||||
});
|
||||
[self evaluateThreshold:_spl];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
} else {
|
||||
if (rms > 0.0 && _sampleRate > 0.0) {
|
||||
float spl = (20 * log10f(sqrtf(rms/(float)_sampleRate))) - _sensitivityOffset + 96;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.environmentSPLMeterContentView setProgressBar:(spl/_thresholdValue)];
|
||||
});
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.environmentSPLMeterContentView setProgressBar:(_spl/_thresholdValue)];
|
||||
});
|
||||
}
|
||||
}
|
||||
dispatch_semaphore_signal(_semaphoreRms);
|
||||
});
|
||||
dispatch_semaphore_wait(_semaphoreRms, DISPATCH_TIME_FOREVER);
|
||||
} else if ([AVAudioSession sharedInstance].recordPermission == AVAudioSessionRecordPermissionDenied) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
@@ -336,9 +410,7 @@
|
||||
NSError *error = nil;
|
||||
[_audioEngine startAndReturnError:&error];
|
||||
} else {
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
[self stopAudioEngine];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,9 +453,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopAudioEngine {
|
||||
if ([_audioEngine isRunning]) {
|
||||
dispatch_semaphore_signal(_semaphoreRms);
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[_eqUnit removeTapOnBus:0];
|
||||
[_audioEngine stop];
|
||||
[_rmsBuffer removeAllObjects];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reachedOptimumNoiseLevel {
|
||||
[self resetAudioSession];
|
||||
[_audioEngine stop];
|
||||
}
|
||||
|
||||
- (void)stepDidFinish {
|
||||
@@ -403,6 +485,7 @@
|
||||
#pragma mark - ORKRingViewDelegate
|
||||
|
||||
- (void)ringViewDidFinishFillAnimation {
|
||||
[self reachedOptimumNoiseLevel];
|
||||
[self.environmentSPLMeterContentView reachedOptimumNoiseLevel];
|
||||
self.activeStepView.navigationFooterView.continueEnabled = YES;
|
||||
}
|
||||
@@ -417,19 +500,17 @@
|
||||
|
||||
- (void)sendHapticEvent:(UINotificationFeedbackType)eventType
|
||||
{
|
||||
[_notificationFeedbackGenerator notificationOccurred:eventType];
|
||||
[_notificationFeedbackGenerator prepare];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_notificationFeedbackGenerator notificationOccurred:eventType];
|
||||
[_notificationFeedbackGenerator prepare];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - ORKEnvironmentSPLMeterContentViewVoiceOverDelegate
|
||||
|
||||
- (void)contentView:(ORKEnvironmentSPLMeterContentView *)contentView shouldAnnounce:(NSString *)inAnnouncement
|
||||
{
|
||||
if ([_audioEngine isRunning] == NO)
|
||||
{
|
||||
// Only make this announcement if the audio engine is not running.
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, inAnnouncement);
|
||||
}
|
||||
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, inAnnouncement);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -75,6 +75,9 @@ ORK_CLASS_AVAILABLE
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSURL *fileURL;
|
||||
|
||||
|
||||
@property (nonatomic, copy, nullable) NSString *fileName;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_URL(aCoder, fileURL);
|
||||
ORK_ENCODE_OBJ(aCoder, contentType);
|
||||
ORK_ENCODE_OBJ(aCoder, fileName);
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
@@ -52,6 +53,7 @@
|
||||
if (self) {
|
||||
ORK_DECODE_URL(aDecoder, fileURL);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, contentType, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, fileName, NSString);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -66,7 +68,8 @@
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualFileURLs(self.fileURL, castObject.fileURL) &&
|
||||
ORKEqualObjects(self.contentType, castObject.contentType));
|
||||
ORKEqualObjects(self.contentType, castObject.contentType) &&
|
||||
ORKEqualObjects(self.fileName, castObject.fileName));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
@@ -77,6 +80,7 @@
|
||||
ORKFileResult *result = [super copyWithZone:zone];
|
||||
result.fileURL = [self.fileURL copy];
|
||||
result.contentType = [self.contentType copy];
|
||||
result.fileName = [self.fileName copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -34,19 +34,38 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Displays a countdown ring and a timer.
|
||||
//
|
||||
// ------------------------------
|
||||
// | |
|
||||
// | Title Label |
|
||||
// | |
|
||||
// | subtitle label |
|
||||
// | |
|
||||
// | __________ |
|
||||
// | / \ |
|
||||
// | | 2:30 | |
|
||||
// | \ ________ / |
|
||||
// | |
|
||||
// |______________________________|
|
||||
@interface ORKFitnessContentView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic, assign, getter=isFinished) BOOL finished;
|
||||
|
||||
@property (nonatomic) BOOL hasHeartRate;
|
||||
@property (nonatomic) BOOL hasDistance;
|
||||
/// The total amount of time the active task is supposed to be performed for.
|
||||
/// For the six minute walk test, this will typically be 360 seconds.
|
||||
@property (nonatomic) NSTimeInterval duration;
|
||||
|
||||
@property (nonatomic, copy, nullable) NSString *heartRate;
|
||||
@property (nonatomic) double distanceInMeters;
|
||||
/// The amount of time that still remain.
|
||||
@property (nonatomic) NSTimeInterval timeLeft;
|
||||
|
||||
@property (nonatomic, strong, nullable) UIImage *image;
|
||||
/// Whether or not the text label is hidden.
|
||||
@property (nonatomic) BOOL labelHidden;
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval timeLeft;
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithDuration:(NSTimeInterval)duration;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2015, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -31,28 +31,8 @@
|
||||
|
||||
#import "ORKFitnessContentView.h"
|
||||
|
||||
#import "ORKActiveStepQuantityView.h"
|
||||
#import "ORKTintedImageView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKSkin.h"
|
||||
|
||||
@import CoreMotion;
|
||||
@import HealthKit;
|
||||
|
||||
|
||||
// #define LAYOUT_TEST 1
|
||||
// #define LAYOUT_DEBUG 1
|
||||
|
||||
@interface ORKFitnessContentView () {
|
||||
ORKQuantityLabel *_timerLabel;
|
||||
ORKQuantityPairView *_quantityPairView;
|
||||
UIView *_imageSpacer1;
|
||||
UIView *_imageSpacer2;
|
||||
ORKTintedImageView *_imageView;
|
||||
NSLengthFormatter *_lengthFormatter;
|
||||
NSLayoutConstraint *_imageRatioConstraint;
|
||||
NSLayoutConstraint *_topConstraint;
|
||||
UILabel *_timerLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -60,272 +40,72 @@
|
||||
|
||||
@implementation ORKFitnessContentView
|
||||
|
||||
- (ORKActiveStepQuantityView *)distanceView {
|
||||
return _quantityPairView.leftView;
|
||||
}
|
||||
|
||||
- (ORKActiveStepQuantityView *)heartRateView {
|
||||
return _quantityPairView.rightView;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
- (instancetype)initWithDuration:(NSTimeInterval)duration {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_timerLabel = [ORKQuantityLabel new];
|
||||
_quantityPairView = [ORKQuantityPairView new];
|
||||
_imageSpacer1 = [UIView new];
|
||||
_imageSpacer1.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_imageSpacer2 = [UIView new];
|
||||
_imageSpacer2.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_imageSpacer1];
|
||||
[self addSubview:_imageSpacer2];
|
||||
[self heartRateView].image = [UIImage imageNamed:@"heart-fitness" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
|
||||
[self updateLengthFormatter];
|
||||
_imageView = [ORKTintedImageView new];
|
||||
_imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_imageView.shouldApplyTint = YES;
|
||||
_timerLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_quantityPairView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self updateKeylineVisible];
|
||||
|
||||
_timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently;
|
||||
_imageView.isAccessibilityElement = NO;
|
||||
|
||||
self.hasHeartRate = _hasHeartRate;
|
||||
self.hasDistance = _hasDistance;
|
||||
|
||||
#if LAYOUT_TEST
|
||||
self.timeLeft = 60 * 5;
|
||||
self.hasHeartRate = YES;
|
||||
self.hasDistance = YES;
|
||||
self.distanceInMeters = 100;
|
||||
self.heartRate = @"22";
|
||||
#endif
|
||||
#if LAYOUT_DEBUG
|
||||
self.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
|
||||
_quantityPairView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.2];
|
||||
#endif
|
||||
|
||||
[self setDistanceInMeters:0];
|
||||
[self heartRateView].title = ORKLocalizedString(@"FITNESS_HEARTRATE_TITLE", nil);
|
||||
_duration = duration;
|
||||
_timeLeft = duration;
|
||||
|
||||
_timerLabel = [[UILabel alloc] init];
|
||||
_timerLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_timerLabel.font = [self labelFont];
|
||||
_timerLabel.adjustsFontForContentSizeCategory = YES;
|
||||
_timerLabel.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
|
||||
|
||||
[self addSubview:_quantityPairView];
|
||||
[self addSubview:_imageView];
|
||||
[self addSubview:_timerLabel];
|
||||
[self setUpConstraints];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
|
||||
|
||||
[self tintColorDidChange];
|
||||
[self updateTimerLabel];
|
||||
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
- (void)tintColorDidChange {
|
||||
[self setNeedsDisplay];
|
||||
_timerLabel.textColor = self.tintColor;
|
||||
}
|
||||
|
||||
- (void)updateLengthFormatter {
|
||||
_lengthFormatter = [NSLengthFormatter new];
|
||||
_lengthFormatter.numberFormatter.maximumFractionDigits = 1;
|
||||
_lengthFormatter.numberFormatter.maximumSignificantDigits = 3;
|
||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
||||
_timerLabel.font = [self labelFont];
|
||||
}
|
||||
|
||||
- (void)localeDidChange:(NSNotification *)notification {
|
||||
[self updateLengthFormatter];
|
||||
[self setDistanceInMeters:_distanceInMeters];
|
||||
}
|
||||
|
||||
- (void)willMoveToWindow:(UIWindow *)newWindow {
|
||||
[super willMoveToWindow:newWindow];
|
||||
[self updateConstraintConstantsForWindow:newWindow];
|
||||
}
|
||||
|
||||
- (void)updateConstraintConstantsForWindow:(UIWindow *)window {
|
||||
const CGFloat CaptionBaselineToTimerTop = ORKGetMetricForWindow(ORKScreenMetricCaptionBaselineToFitnessTimerTop, window);
|
||||
const CGFloat CaptionBaselineToStepViewTop = ORKGetMetricForWindow(ORKScreenMetricLearnMoreBaselineToStepViewTop, window);
|
||||
_topConstraint.constant = (CaptionBaselineToTimerTop - CaptionBaselineToStepViewTop);
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_timerLabel, _imageView, _quantityPairView, _imageSpacer1, _imageSpacer2);
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_timerLabel][_imageSpacer1(>=0)][_imageView]"
|
||||
options:NSLayoutFormatAlignAllCenterX
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
_topConstraint = [NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:0.0];
|
||||
[constraints addObject:_topConstraint];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self
|
||||
attribute:NSLayoutAttributeCenterX
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_timerLabel
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self attribute:NSLayoutAttributeWidth
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationLessThanOrEqual
|
||||
toItem:self attribute:NSLayoutAttributeWidth
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_imageView][_imageSpacer2(>=0)][_quantityPairView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer2
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
[constraints addObject:[NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_imageSpacer2
|
||||
attribute:NSLayoutAttributeHeight
|
||||
multiplier:1.0
|
||||
constant:0.0]];
|
||||
|
||||
NSLayoutConstraint *imageSpacerHeightConstraint = [NSLayoutConstraint constraintWithItem:_imageSpacer1
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:CGFLOAT_MIN];
|
||||
imageSpacerHeightConstraint.priority = UILayoutPriorityDefaultLow - 1;
|
||||
[constraints addObject:imageSpacerHeightConstraint];
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_quantityPairView]|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:nil
|
||||
views:views]];
|
||||
|
||||
NSLayoutConstraint *maxWidthConstraint = [NSLayoutConstraint constraintWithItem:self
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:ORKScreenMetricMaxDimension];
|
||||
maxWidthConstraint.priority = UILayoutPriorityRequired - 1;
|
||||
[constraints addObject:maxWidthConstraint];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
[self updateConstraintConstantsForWindow:self.window];
|
||||
}
|
||||
|
||||
- (void)setImage:(UIImage *)image {
|
||||
_image = image;
|
||||
_imageView.image = image;
|
||||
|
||||
_imageRatioConstraint.active = NO;
|
||||
|
||||
CGSize size = image.size;
|
||||
if (size.width > 0 && size.height > 0) {
|
||||
_imageRatioConstraint = [NSLayoutConstraint constraintWithItem:_imageView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:_imageView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
multiplier:size.height / size.width
|
||||
constant:0.0];
|
||||
_imageRatioConstraint.active = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHasDistance:(BOOL)hasDistance {
|
||||
_hasDistance = hasDistance;
|
||||
[self distanceView].enabled = _hasDistance;
|
||||
[self updateKeylineVisible];
|
||||
}
|
||||
|
||||
- (void)setHasHeartRate:(BOOL)hasHeartRate {
|
||||
_hasHeartRate = hasHeartRate;
|
||||
[self heartRateView].enabled = _hasHeartRate;
|
||||
[self updateKeylineVisible];
|
||||
}
|
||||
|
||||
- (void)setHeartRate:(NSString *)heartRate {
|
||||
_heartRate = heartRate;
|
||||
[self heartRateView].value = heartRate;
|
||||
}
|
||||
|
||||
- (void)updateKeylineVisible {
|
||||
[_quantityPairView setKeylineHidden:!(_hasDistance && _hasHeartRate)];
|
||||
}
|
||||
|
||||
- (void)setDistanceInMeters:(double)distanceInMeters {
|
||||
_distanceInMeters = distanceInMeters;
|
||||
double displayDistance = _distanceInMeters;
|
||||
NSString *distanceString = nil;
|
||||
NSLengthFormatterUnit unit;
|
||||
NSString *unitString = [_lengthFormatter unitStringFromMeters:displayDistance usedUnit:&unit];
|
||||
|
||||
switch (unit) {
|
||||
case NSLengthFormatterUnitCentimeter:
|
||||
case NSLengthFormatterUnitMillimeter:
|
||||
unit = NSLengthFormatterUnitMeter;
|
||||
// Force showing 0 meters if the distance is sufficiently short to be displayed in cm or mm
|
||||
unitString = [_lengthFormatter unitStringFromValue:0 unit:NSLengthFormatterUnitMeter];
|
||||
displayDistance = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Use HealthKit to convert the unit, so we can use the number formatter directly.
|
||||
HKUnit *hkUnit = [HKUnit unitFromLengthFormatterUnit:unit];
|
||||
double conversionFactor = 1.0;
|
||||
if ([hkUnit isNull] && (unit == NSLengthFormatterUnitYard)) {
|
||||
hkUnit = [HKUnit footUnit];
|
||||
conversionFactor = 1.0 / 3.0;
|
||||
}
|
||||
HKQuantity *quantity = [HKQuantity quantityWithUnit:[HKUnit meterUnit] doubleValue:displayDistance];
|
||||
distanceString = [_lengthFormatter.numberFormatter stringFromNumber:@([quantity doubleValueForUnit:hkUnit]*conversionFactor)];
|
||||
|
||||
[self distanceView].title = [NSString localizedStringWithFormat:ORKLocalizedString(@"FITNESS_DISTANCE_TITLE_FORMAT", nil), unitString];
|
||||
[self distanceView].value = distanceString;
|
||||
- (void)setDuration:(NSTimeInterval)duration {
|
||||
_duration = duration;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)setTimeLeft:(NSTimeInterval)timeLeft {
|
||||
_timeLeft = timeLeft;
|
||||
[self updateTimerLabel];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (BOOL)labelHidden {
|
||||
return _timerLabel.isHidden;
|
||||
}
|
||||
|
||||
- (void)setLabelHidden:(BOOL)labelHidden {
|
||||
[_timerLabel setHidden:labelHidden];
|
||||
}
|
||||
|
||||
- (UIFont*) labelFont {
|
||||
|
||||
UIFont* font = [UIFont preferredFontForTextStyle: UIFontTextStyleLargeTitle];
|
||||
UIFontMetrics* metrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleLargeTitle];
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
UIFontDescriptor* round = [[font fontDescriptor] fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
|
||||
UIFontDescriptor* weighted = [round fontDescriptorByAddingAttributes:@{
|
||||
UIFontDescriptorTraitsAttribute: @{
|
||||
UIFontWeightTrait: @1.5
|
||||
}
|
||||
}];
|
||||
font = [UIFont fontWithDescriptor:weighted size:44];
|
||||
}
|
||||
|
||||
UIFont* scaled = [metrics scaledFontForFont:font];
|
||||
return scaled;
|
||||
}
|
||||
|
||||
- (void)updateTimerLabel {
|
||||
@@ -334,13 +114,44 @@
|
||||
dispatch_once(&onceToken, ^{
|
||||
formatter = [NSDateComponentsFormatter new];
|
||||
formatter.unitsStyle = NSDateComponentsFormatterUnitsStylePositional;
|
||||
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad;
|
||||
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorDropLeading;
|
||||
formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond;
|
||||
});
|
||||
|
||||
NSString *labelString = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
_timerLabel.text = labelString;
|
||||
_timerLabel.hidden = (labelString == nil);
|
||||
_timerLabel.text = [formatter stringFromTimeInterval:MAX(round(_timeLeft),0)];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
|
||||
// The ring should be be centered and fill 1/2 of the view's width
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGFloat strokeWidth = 12;
|
||||
CGFloat xCenter = self.bounds.size.width / 2;
|
||||
CGFloat yCenter = self.bounds.size.height / 2;
|
||||
CGFloat dimension = MIN(self.bounds.size.width, self.bounds.size.height);
|
||||
CGFloat radius = 0.5 * (dimension * 0.5);
|
||||
CGFloat percentFilled = _timeLeft / _duration;
|
||||
CGFloat startAngle = -M_PI_2 - (percentFilled * 2 * M_PI);
|
||||
CGFloat stopAngle = -M_PI_2;
|
||||
bool clockwise = NO;
|
||||
|
||||
CGContextSetLineWidth(context, strokeWidth);
|
||||
CGContextSetLineCap(context, kCGLineCapRound);
|
||||
|
||||
// Draw a circular track
|
||||
if (@available(iOS 13.0, *)) {
|
||||
[[UIColor systemGray5Color] setStroke];
|
||||
} else {
|
||||
[[UIColor lightGrayColor] setStroke];
|
||||
}
|
||||
|
||||
CGContextAddArc(context, xCenter, yCenter, radius, 0, 2 * M_PI, clockwise ? 1 : 0);
|
||||
CGContextStrokePath(context);
|
||||
|
||||
// Fill in the track based on progress
|
||||
[self.tintColor setStroke];
|
||||
CGContextAddArc(context, xCenter, yCenter, radius, startAngle, stopAngle, clockwise ? 1 : 0);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -36,17 +36,11 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Fitness step.
|
||||
|
||||
Displays usual header, a counting-up timer, read outs for distance and/or
|
||||
heart rate if corresponding recorders are attached.
|
||||
|
||||
Also displays an image during the task.
|
||||
*/
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKFitnessStep : ORKActiveStep
|
||||
|
||||
@property (nonatomic, copy) NSDictionary *userInfo;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
|
||||
@@ -43,6 +44,7 @@
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (self) {
|
||||
self.userInfo = [[NSDictionary alloc] init];
|
||||
self.shouldShowDefaultTimer = NO;
|
||||
}
|
||||
return self;
|
||||
@@ -58,13 +60,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKFitnessStep *step = [super copyWithZone:zone];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)startsFinished {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ_PLIST(coder, userInfo);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
[super encodeWithCoder:coder];
|
||||
ORK_ENCODE_OBJ(coder, userInfo);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKFitnessStep *step = [super copyWithZone:zone];
|
||||
step.userInfo = [self.userInfo copy];
|
||||
return step;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
if ([self class] != [other class]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__typeof(self) castObject = other;
|
||||
return ORKEqualObjects(self.userInfo, castObject.userInfo);
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [super hash] ^ self.userInfo.hash;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,27 +30,20 @@
|
||||
|
||||
|
||||
#import "ORKFitnessStepViewController.h"
|
||||
|
||||
#import "ORKActiveStepTimer.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKFitnessContentView.h"
|
||||
#import "ORKVerticalContainerView.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKActiveStepView.h"
|
||||
#import "ORKActiveStepTimer.h"
|
||||
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKHealthQuantityTypeRecorder.h"
|
||||
#import "ORKPedometerRecorder.h"
|
||||
|
||||
#import "ORKNavigationContainerView_Internal.h"
|
||||
#import "ORKActiveStepViewController_Internal.h"
|
||||
#import "ORKFitnessStep.h"
|
||||
#import "ORKStep_Private.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
|
||||
@interface ORKFitnessStepViewController () <ORKHealthQuantityTypeRecorderDelegate, ORKPedometerRecorderDelegate> {
|
||||
NSInteger _intendedSteps;
|
||||
@interface ORKFitnessStepViewController () {
|
||||
ORKFitnessContentView *_contentView;
|
||||
NSNumberFormatter *_hrFormatter;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -70,82 +63,61 @@
|
||||
return (ORKFitnessStep *)self.step;
|
||||
}
|
||||
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
_hrFormatter = [[NSNumberFormatter alloc] init];
|
||||
_hrFormatter.numberStyle = NSNumberFormatterNoStyle;
|
||||
_contentView.timeLeft = self.fitnessStep.stepDuration;
|
||||
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
_contentView = [ORKFitnessContentView new];
|
||||
_contentView.timeLeft = self.fitnessStep.stepDuration;
|
||||
|
||||
_contentView = [[ORKFitnessContentView alloc] initWithDuration:self.fitnessStep.stepDuration];
|
||||
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
self.activeStepView.activeCustomView = _contentView;
|
||||
self.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
self.continueButtonTitle = ORKLocalizedString(@"BUTTON_SKIP_STEP", nil);
|
||||
}
|
||||
|
||||
- (void)updateHeartRateWithQuantity:(HKQuantitySample *)quantity unit:(HKUnit *)unit {
|
||||
if (quantity != nil) {
|
||||
_contentView.hasHeartRate = YES;
|
||||
}
|
||||
if (quantity) {
|
||||
_contentView.heartRate = [_hrFormatter stringFromNumber:@([quantity.quantity doubleValueForUnit:unit])];
|
||||
} else {
|
||||
_contentView.heartRate = @"--";
|
||||
}
|
||||
- (void)finish {
|
||||
[super finish];
|
||||
_contentView.labelHidden = YES;
|
||||
self.continueButtonTitle = ORKLocalizedString(@"BUTTON_NEXT", nil);
|
||||
}
|
||||
|
||||
- (void)updateDistance:(double)distanceInMeters {
|
||||
_contentView.hasDistance = YES;
|
||||
_contentView.distanceInMeters = distanceInMeters;
|
||||
|
||||
}
|
||||
|
||||
- (void)recordersDidChange {
|
||||
[super recordersDidChange];
|
||||
|
||||
ORKPedometerRecorder *pedometerRecorder = nil;
|
||||
ORKHealthQuantityTypeRecorder *heartRateRecorder = nil;
|
||||
for (ORKRecorder *recorder in self.recorders) {
|
||||
if ([recorder isKindOfClass:[ORKPedometerRecorder class]]) {
|
||||
pedometerRecorder = (ORKPedometerRecorder *)recorder;
|
||||
} else if ([recorder isKindOfClass:[ORKHealthQuantityTypeRecorder class]]) {
|
||||
ORKHealthQuantityTypeRecorder *rec1 = (ORKHealthQuantityTypeRecorder *)recorder;
|
||||
if ([[[rec1 quantityType] identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
|
||||
heartRateRecorder = (ORKHealthQuantityTypeRecorder *)recorder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (heartRateRecorder == nil) {
|
||||
_contentView.hasHeartRate = NO;
|
||||
}
|
||||
_contentView.heartRate = @"--";
|
||||
_contentView.hasDistance = (pedometerRecorder != nil);
|
||||
_contentView.distanceInMeters = 0;
|
||||
|
||||
- (void)stepDidChange {
|
||||
[super stepDidChange];
|
||||
_contentView.duration = self.fitnessStep.stepDuration;
|
||||
_contentView.timeLeft = self.fitnessStep.stepDuration;
|
||||
}
|
||||
|
||||
- (void)countDownTimerFired:(ORKActiveStepTimer *)timer finished:(BOOL)finished {
|
||||
_contentView.timeLeft = finished ? 0 : (timer.duration - timer.runtime);
|
||||
_contentView.duration = self.fitnessStep.stepDuration;
|
||||
[super countDownTimerFired:timer finished:finished];
|
||||
}
|
||||
|
||||
#pragma mark - ORKHealthQuantityTypeRecorderDelegate
|
||||
- (void)goForward {
|
||||
|
||||
- (void)healthQuantityTypeRecorderDidUpdate:(ORKHealthQuantityTypeRecorder *)healthQuantityTypeRecorder {
|
||||
if ([[healthQuantityTypeRecorder.quantityType identifier] isEqualToString:HKQuantityTypeIdentifierHeartRate]) {
|
||||
[self updateHeartRateWithQuantity:healthQuantityTypeRecorder.lastSample unit:healthQuantityTypeRecorder.unit];
|
||||
if (self.finished) {
|
||||
[super goForward];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - ORKPedometerRecorderDelegate
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:ORKLocalizedString(@"FITNESS_STOP_TEST_CONFIRMATION", nil)
|
||||
message:ORKLocalizedString(@"FITNESS_STOP_TEST_DETAIL", nil)
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
- (void)pedometerRecorderDidUpdate:(ORKPedometerRecorder *)pedometerRecorder {
|
||||
double distanceInMeters = pedometerRecorder.totalDistance;
|
||||
[self updateDistance:distanceInMeters];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"FITNESS_RESUME_TEST", nil)
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
[alert dismissViewControllerAnimated:YES completion:nil];
|
||||
}]];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_SKIP_STEP", nil)
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * _Nonnull action) {
|
||||
[alert dismissViewControllerAnimated:YES completion:^{
|
||||
[super goForward];
|
||||
}];
|
||||
}]];
|
||||
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -307,7 +307,7 @@ typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
|
||||
{
|
||||
[_startStopButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_START_TITLE", nil) forState:UIControlStateNormal];
|
||||
[_startStopButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
[_startStopButton setBackgroundColor:[UIColor systemBlueColor]];
|
||||
[_startStopButton setBackgroundColor:self.tintColor];
|
||||
|
||||
[_timerLabel setText:ORKLocalizedString(@"FRONT_FACING_CAMERA_START_TIME", nil)];
|
||||
[_timerLabel setTextColor:[UIColor darkGrayColor]];
|
||||
@@ -315,7 +315,7 @@ typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
|
||||
else
|
||||
{
|
||||
[_startStopButton setTitle:ORKLocalizedString(@"FRONT_FACING_CAMERA_STOP_TITLE", nil) forState:UIControlStateNormal];
|
||||
[_startStopButton setTitleColor:[UIColor systemBlueColor] forState:UIControlStateNormal];
|
||||
[_startStopButton setTitleColor:self.tintColor forState:UIControlStateNormal];
|
||||
[_startStopButton setBackgroundColor:[UIColor systemGrayColor]];
|
||||
|
||||
[_timerLabel setTextColor:[UIColor whiteColor]];
|
||||
@@ -352,6 +352,10 @@ typedef NS_CLOSED_ENUM(NSInteger, ORKStartStopButtonState) {
|
||||
_isTextCollapsed = !_isTextCollapsed;
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
self.tintColor = ORKViewTintColor(self);
|
||||
[self setStartStopButtonState:_startStopButtonState];
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ORKFrontFacingCameraStepContentView ()
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#import "ORKResult_Private.h"
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
#import "ORKStepViewController_Internal.h"
|
||||
#import "ORKTaskViewController_Internal.h"
|
||||
|
||||
@interface ORKFrontFacingCameraStepViewController () <AVCaptureFileOutputRecordingDelegate, AVCaptureVideoDataOutputSampleBufferDelegate>
|
||||
|
||||
@@ -89,6 +90,8 @@
|
||||
[self setupContentView];
|
||||
[self setupConstraints];
|
||||
[self startSession];
|
||||
|
||||
[self.taskViewController setNavigationBarColor:[self.view backgroundColor]];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
@@ -349,6 +352,7 @@
|
||||
frontFacingCameraResult.endDate = now;
|
||||
frontFacingCameraResult.contentType = @"video/quicktime";
|
||||
frontFacingCameraResult.fileURL = _savedFileURL;
|
||||
frontFacingCameraResult.fileName = _savedFileName;
|
||||
frontFacingCameraResult.retryCount = retryCount;
|
||||
|
||||
[results addObject:frontFacingCameraResult];
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#import <ResearchKit/ORKRecorder.h>
|
||||
#import <Availability.h>
|
||||
|
||||
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
@@ -68,5 +68,5 @@ API_AVAILABLE(ios(12.0))
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
#endif
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "HKSample+ORKJSONDictionary.h"
|
||||
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
|
||||
@interface ORKHealthClinicalTypeRecorder () {
|
||||
ORKDataLogger *_logger;
|
||||
@@ -205,7 +206,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;
|
||||
}
|
||||
@@ -234,3 +235,4 @@
|
||||
|
||||
@end
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -49,6 +49,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
The `ORKHealthQuantityTypeRecorder` class represents a recorder for collecting real time sample data from HealthKit, such as heart rate, during
|
||||
an active task.
|
||||
*/
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHealthQuantityTypeRecorder : ORKRecorder
|
||||
|
||||
@@ -76,5 +77,5 @@ ORK_CLASS_AVAILABLE
|
||||
outputDirectory:(nullable NSURL *)outputDirectory NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
#import "ORKRecorder_Internal.h"
|
||||
#import "HKSample+ORKJSONDictionary.h"
|
||||
|
||||
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
@interface ORKHealthQuantityTypeRecorder () {
|
||||
ORKDataLogger *_logger;
|
||||
BOOL _isRecording;
|
||||
@@ -368,3 +368,4 @@ static const NSInteger _HealthAnchoredQueryLimit = 100;
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
|
||||
#import "CLLocation+ORKJSONDictionary.h"
|
||||
|
||||
#import <ResearchKit/CLLocationManager+ResearchKit.h>
|
||||
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
|
||||
@@ -71,8 +73,13 @@
|
||||
return @"location";
|
||||
}
|
||||
|
||||
|
||||
// Test Seam - unit tests don't support background updates or pausing.
|
||||
- (CLLocationManager *)createLocationManager {
|
||||
return [[CLLocationManager alloc] init];
|
||||
CLLocationManager *manager = [[CLLocationManager alloc] init];
|
||||
manager.pausesLocationUpdatesAutomatically = NO;
|
||||
manager.allowsBackgroundLocationUpdates = YES;
|
||||
return manager;
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
@@ -88,35 +95,25 @@
|
||||
}
|
||||
|
||||
self.locationManager = [self createLocationManager];
|
||||
|
||||
CLAuthorizationStatus status = kCLAuthorizationStatusNotDetermined;
|
||||
|
||||
if (@available(iOS 14.0, *)) {
|
||||
status = self.locationManager.authorizationStatus;
|
||||
} else {
|
||||
status = [CLLocationManager authorizationStatus];
|
||||
}
|
||||
|
||||
if (status == kCLAuthorizationStatusRestricted || status == kCLAuthorizationStatusNotDetermined) {
|
||||
[self.locationManager requestWhenInUseAuthorization];
|
||||
}
|
||||
self.locationManager.pausesLocationUpdatesAutomatically = NO;
|
||||
self.locationManager.delegate = self;
|
||||
|
||||
if (!self.locationManager) {
|
||||
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
|
||||
code:NSFeatureUnsupportedError
|
||||
userInfo:@{@"recorder": self}];
|
||||
[self finishRecordingWithError:error];
|
||||
return;
|
||||
|
||||
BOOL locationManagerAuthRequestsAllowed = YES;
|
||||
if ([CLLocationManager authorizationStatus] <= kCLAuthorizationStatusDenied) {
|
||||
locationManagerAuthRequestsAllowed = [self.locationManager ork_requestWhenInUseAuthorization];
|
||||
}
|
||||
|
||||
|
||||
self.uptime = [NSProcessInfo processInfo].systemUptime;
|
||||
[self.locationManager startUpdatingLocation];
|
||||
[self.locationManager ork_startUpdatingLocation];
|
||||
|
||||
if (locationManagerAuthRequestsAllowed == NO) {
|
||||
// If we weren't able to perform auth requests, then ResearchKit was compiled with auth requests disabled
|
||||
// We won't be getting any callbacks about location changes, so might as well stop recording
|
||||
[self stop];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)doStopRecording {
|
||||
[self.locationManager stopUpdatingLocation];
|
||||
[self.locationManager ork_stopUpdatingLocation];
|
||||
self.locationManager.delegate = nil;
|
||||
self.locationManager = nil;
|
||||
}
|
||||
@@ -138,7 +135,8 @@
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager
|
||||
didUpdateLocations:(NSArray *)locations {
|
||||
didUpdateLocations:(NSArray<CLLocation *> *)locations {
|
||||
|
||||
BOOL success = YES;
|
||||
NSParameterAssert(locations.count >= 0);
|
||||
NSError *error = nil;
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import "ORKNormalizedReactionTimeStimulusView.h"
|
||||
#import "ORKRoundTappingButton.h"
|
||||
#import <ResearchKit/ORKCustomStepView_Internal.h>
|
||||
#import <ResearchKit/ORKNormalizedReactionTimeStimulusView.h>
|
||||
#import <ResearchKit/ORKRoundTappingButton.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
The `ORKReactionTimeResult` class represents the result of a single successful attempt within an ORKReactionTimeStep.
|
||||
|
||||
|
||||
The `timestamp` property is equal to the value of systemUptime (in NSProcessInfo) when the stimulus occurred.
|
||||
Each entry of motion data in this file contains a time interval which may be directly compared to timestamp in order to determine the elapsed time since the stimulus.
|
||||
|
||||
@@ -56,9 +56,7 @@ ORK_CLASS_AVAILABLE
|
||||
@property (nonatomic, copy) NSDate * timerEndDate;
|
||||
@property (nonatomic, copy, nullable) NSDate * stimulusStartDate;
|
||||
@property (nonatomic, copy, nullable) NSDate * reactionDate;
|
||||
@property (nonatomic) NSNumber *currentInterval;
|
||||
|
||||
|
||||
@property (nonatomic) double currentInterval;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -44,8 +44,7 @@
|
||||
ORK_ENCODE_OBJ(aCoder, timerEndDate);
|
||||
ORK_ENCODE_OBJ(aCoder, stimulusStartDate);
|
||||
ORK_ENCODE_OBJ(aCoder, reactionDate);
|
||||
ORK_ENCODE_OBJ(aCoder, currentInterval);
|
||||
|
||||
ORK_ENCODE_INTEGER(aCoder, currentInterval);
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
@@ -55,7 +54,7 @@
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, timerEndDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, stimulusStartDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, reactionDate, NSDate);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, currentInterval, NSNumber);
|
||||
ORK_DECODE_INTEGER(aDecoder, currentInterval);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -73,8 +72,7 @@
|
||||
ORKEqualObjects(self.timerEndDate, castObject.timerEndDate) &&
|
||||
ORKEqualObjects(self.stimulusStartDate, castObject.stimulusStartDate) &&
|
||||
ORKEqualObjects(self.reactionDate, castObject.reactionDate) &&
|
||||
ORKEqualObjects(self.currentInterval, castObject.currentInterval)) ;
|
||||
|
||||
(self.currentInterval == castObject.currentInterval));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
@@ -87,7 +85,7 @@
|
||||
result.timerEndDate = [self.timerEndDate copy];
|
||||
result.stimulusStartDate = [self.stimulusStartDate copy];
|
||||
result.reactionDate = [self.reactionDate copy];
|
||||
result.currentInterval = [self.currentInterval copy];
|
||||
result.currentInterval = self.currentInterval;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ ORK_CLASS_AVAILABLE
|
||||
|
||||
@property (nonatomic, assign) SystemSoundID failureSound;
|
||||
|
||||
@property (nonatomic) NSNumber *currentInterval;
|
||||
@property (nonatomic) double currentInterval;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
ORK_DECODE_UINT32(aDecoder, timeoutSound);
|
||||
ORK_DECODE_UINT32(aDecoder, failureSound);
|
||||
ORK_DECODE_INTEGER(aDecoder, numberOfAttempts);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, currentInterval, NSNumber);
|
||||
ORK_DECODE_INTEGER(aDecoder, currentInterval);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -119,7 +119,7 @@
|
||||
ORK_ENCODE_UINT32(aCoder, timeoutSound);
|
||||
ORK_ENCODE_UINT32(aCoder, failureSound);
|
||||
ORK_ENCODE_INTEGER(aCoder, numberOfAttempts);
|
||||
ORK_ENCODE_OBJ(aCoder, currentInterval);
|
||||
ORK_ENCODE_INTEGER(aCoder, currentInterval);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKCustomStepView_Internal.h"
|
||||
#import <ResearchKit/ORKCustomStepView_Internal.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
|
||||
|
||||
@import UIKit;
|
||||
#import "ORKDefines.h"
|
||||
#import "ORKActiveStepViewController.h"
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <ResearchKit/ORKActiveStepViewController.h>
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -254,7 +254,8 @@ static const NSTimeInterval OutcomeAnimationDuration = 0.3;
|
||||
- (NSTimeInterval)stimulusInterval {
|
||||
ORKNormalizedReactionTimeStep *step = [self reactionTimeStep];
|
||||
NSNumber* interval = [self getRandomInterval];
|
||||
step.currentInterval = interval;
|
||||
step.currentInterval = interval.doubleValue;
|
||||
|
||||
return [interval doubleValue];
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKPSATKeyboardView.h"
|
||||
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKBorderedButton.h"
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ NSUInteger const ORKPSATMaximumAnswer = 17;
|
||||
- (void)setEnabled:(BOOL)enabled {
|
||||
for (ORKBorderedButton *answerButton in self.answerButtons) {
|
||||
[answerButton setEnabled:enabled];
|
||||
[answerButton setBackgroundColor:ORKViewTintColor(self)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -31,10 +31,13 @@
|
||||
|
||||
|
||||
@import UIKit;
|
||||
@import HealthKit;
|
||||
#import <ResearchKit/ORKDefines.h>
|
||||
#import <Availability.h>
|
||||
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
#import <HealthKit/HealthKit.h>
|
||||
#endif
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -106,7 +109,9 @@ ORK_CLASS_AVAILABLE
|
||||
If your recorder requires or would benefit from read access to HealthKit at
|
||||
runtime during the task, return the appropriate set of `HKSampleType` objects.
|
||||
*/
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
- (nullable NSSet<HKObjectType *> *)requestedHealthKitTypesForReading;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -355,6 +360,8 @@ ORK_CLASS_AVAILABLE
|
||||
of an `ORKActiveStep` object, include that step in a task, and present it with
|
||||
a task view controller.
|
||||
*/
|
||||
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKHealthQuantityTypeRecorderConfiguration : ORKRecorderConfiguration
|
||||
|
||||
@@ -433,7 +440,7 @@ API_AVAILABLE(ios(12.0))
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
#endif
|
||||
/**
|
||||
The `ORKStreamingAudioRecorderConfiguration` class represents a configuration that records streaming
|
||||
audio data during an active step.
|
||||
|
||||
@@ -89,9 +89,12 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
|
||||
- (NSSet<HKObjectType *> *)requestedHealthKitTypesForReading {
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (ORKPermissionMask)requestedPermissionMask {
|
||||
return ORKPermissionNone;
|
||||
}
|
||||
@@ -242,6 +245,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
|
||||
|
||||
+53
-50
@@ -30,7 +30,7 @@
|
||||
|
||||
|
||||
#import "ORKSpeechInNoiseContentView.h"
|
||||
#import "ORKAudioGraphView.h"
|
||||
#import "ORKAudioMeteringView.h"
|
||||
|
||||
#import "ORKHeadlineLabel.h"
|
||||
#import "ORKSubheadlineLabel.h"
|
||||
@@ -41,10 +41,15 @@
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKPlaybackButton.h"
|
||||
|
||||
static CGFloat const ORKSpeechInNoiseContentFlamesViewHeightConstant = 150.0;
|
||||
static CGFloat const ORKSpeechInNoiseContentFlamesViewVerticalSpacing = 44.0;
|
||||
static CGFloat const ORKSpeechInNoiseContentViewVerticalMargin = 44;
|
||||
|
||||
@interface ORKSpeechInNoiseContentView () <UITextFieldDelegate>
|
||||
|
||||
@property (nonatomic, strong) ORKAudioGraphView *graphView;
|
||||
@property (nonatomic, strong) ORKAudioMeteringView *graphView;
|
||||
@property (nonatomic, strong) ORKSubheadlineLabel *transcriptLabel;
|
||||
@property (nonatomic, copy) NSArray<NSLayoutConstraint *> *constraints;
|
||||
|
||||
@end
|
||||
|
||||
@@ -71,6 +76,11 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect
|
||||
{
|
||||
[self setUpConstraints];
|
||||
}
|
||||
|
||||
- (void)setupTextLabel {
|
||||
_textLabel = [ORKSubheadlineLabel new];
|
||||
_textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
@@ -81,9 +91,9 @@
|
||||
}
|
||||
|
||||
- (void)setupGraphView {
|
||||
self.graphView = [ORKAudioGraphView new];
|
||||
self.graphView = [[ORKAudioMeteringView alloc] init];
|
||||
_graphView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[_graphView setMeterColor:[UIColor lightGrayColor]];
|
||||
[self addSubview:_graphView];
|
||||
}
|
||||
|
||||
@@ -121,72 +131,65 @@
|
||||
[self applyAlertColor];
|
||||
}
|
||||
|
||||
- (void)setUpConstraints {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
- (void)setUpConstraints
|
||||
{
|
||||
if (self.constraints.count > 0)
|
||||
{
|
||||
[NSLayoutConstraint deactivateConstraints:self.constraints];
|
||||
}
|
||||
|
||||
NSDictionary *views = NSDictionaryOfVariableBindings(_textLabel, _graphView, _playButton);
|
||||
const CGFloat graphHeight = 150;
|
||||
NSLayoutConstraint *centeredYLayoutConstaint = [_graphView.centerYAnchor constraintLessThanOrEqualToAnchor:self.centerYAnchor constant:-ORKSpeechInNoiseContentFlamesViewVerticalSpacing];
|
||||
centeredYLayoutConstaint.priority = UILayoutPriorityDefaultLow;
|
||||
|
||||
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_textLabel]-(5)-[_graphView(graphHeight)]-buttonGap-[_playButton]-(>=topBottomMargin)-|"
|
||||
options:(NSLayoutFormatOptions)0
|
||||
metrics:@{
|
||||
@"graphHeight": @(graphHeight),
|
||||
@"topBottomMargin" : @(5),
|
||||
@"buttonGap" : @(20)
|
||||
}
|
||||
views:views]];
|
||||
self.constraints = @[
|
||||
[_graphView.topAnchor constraintGreaterThanOrEqualToAnchor:self.topAnchor],
|
||||
centeredYLayoutConstaint,
|
||||
[_graphView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
|
||||
[_graphView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
||||
[_graphView.heightAnchor constraintEqualToConstant:ORKSpeechInNoiseContentFlamesViewHeightConstant],
|
||||
[_playButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
|
||||
[_playButton.topAnchor constraintGreaterThanOrEqualToAnchor:_graphView.bottomAnchor constant:ORKSpeechInNoiseContentFlamesViewVerticalSpacing],
|
||||
[_playButton.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-ORKSpeechInNoiseContentViewVerticalMargin]
|
||||
];
|
||||
|
||||
|
||||
const CGFloat sideMargin = self.layoutMargins.left + (2 * ORKStandardLeftMarginForTableViewCell(self));
|
||||
const CGFloat twiceSideMargin = sideMargin * 2;
|
||||
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_textLabel]-|"
|
||||
options:0
|
||||
metrics: nil
|
||||
views:views]];
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-sideMargin-[_graphView]-sideMargin-|"
|
||||
options:0
|
||||
metrics: @{@"sideMargin": @(sideMargin)}
|
||||
views:views]];
|
||||
|
||||
|
||||
[constraints addObjectsFromArray:
|
||||
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-twiceSideMargin-[_playButton(>=200)]-twiceSideMargin-|"
|
||||
options:0
|
||||
metrics: @{@"twiceSideMargin": @(twiceSideMargin)}
|
||||
views:views]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:constraints];
|
||||
[NSLayoutConstraint activateConstraints:self.constraints];
|
||||
}
|
||||
|
||||
- (void)updateGraphSamples {
|
||||
_graphView.values = _samples;
|
||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
|
||||
{
|
||||
[super traitCollectionDidChange:previousTraitCollection];
|
||||
|
||||
[self setUpConstraints];
|
||||
}
|
||||
|
||||
- (void)setGraphViewHidden:(BOOL)hidden {
|
||||
- (void)updateGraphSamples
|
||||
{
|
||||
_graphView.samples = _samples;
|
||||
}
|
||||
|
||||
- (void)setGraphViewHidden:(BOOL)hidden
|
||||
{
|
||||
[_graphView setHidden:hidden];
|
||||
}
|
||||
|
||||
- (void)addSample:(NSNumber *)sample {
|
||||
- (void)addSample:(NSNumber *)sample
|
||||
{
|
||||
NSAssert(sample != nil, @"Sample should be non-nil");
|
||||
if (!_samples) {
|
||||
_samples = [NSMutableArray array];
|
||||
}
|
||||
[_samples addObject:sample];
|
||||
// Try to keep around 250 samples
|
||||
if (_samples.count > 500) {
|
||||
_samples = [[_samples subarrayWithRange:(NSRange){250, _samples.count - 250}] mutableCopy];
|
||||
}
|
||||
|
||||
_samples = [ORKLastNSamples(_samples, 500) mutableCopy];
|
||||
|
||||
[self updateGraphSamples];
|
||||
}
|
||||
|
||||
|
||||
- (void)removeAllSamples {
|
||||
- (void)removeAllSamples
|
||||
{
|
||||
_samples = nil;
|
||||
[self updateGraphSamples];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder(s) nor the names of any contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission. No license is granted to the trademarks of
|
||||
the copyright holders even if such marks are included in this software.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <ResearchKit/ORKResult.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
|
||||
The `ORKSpeechInNoiseResult` class represents the result of a single successful attempt of an ORKSpeechInNoiseStep.
|
||||
|
||||
A speech-in-noise result contains a single string representing the target sentence to be repeated in subsequent ORKSpeechRecognitionSteps.
|
||||
|
||||
*/
|
||||
|
||||
ORK_CLASS_AVAILABLE
|
||||
@interface ORKSpeechInNoiseResult : ORKResult
|
||||
|
||||
@property (nonatomic, copy, nullable) NSString *filename;
|
||||
|
||||
@property (nonatomic, copy, nullable) NSString *targetSentence;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+51
-31
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (c) 2018, Apple Inc. All rights reserved.
|
||||
Copyright (c) 2020, Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
@@ -28,36 +28,56 @@
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "ORKSpeechInNoiseResult.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKResult_Private.h"
|
||||
|
||||
import Foundation
|
||||
@implementation ORKSpeechInNoiseResult
|
||||
|
||||
@available(watchOSApplicationExtension 5.0, *)
|
||||
class AssessmentManager {
|
||||
private var manager: CMMovementDisorderManager?
|
||||
init() {
|
||||
if CMMovementDisorderManager.isAvailable() {
|
||||
manager = CMMovementDisorderManager()
|
||||
|
||||
monitorForParkinsons()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func monitorForParkinsons() {
|
||||
manager?.monitorKinesias(forDuration: 7 * 24 * 3600)
|
||||
}
|
||||
|
||||
func queryNewAssessments() {
|
||||
let calendar = Calendar.current
|
||||
let toDate = Date()
|
||||
let fromDate: Date = calendar.date(byAdding: .day, value: -7, to: toDate)!
|
||||
|
||||
manager?.queryTremor(from: fromDate, to: toDate, withHandler: { (_/*results*/, _/*error*/) in
|
||||
|
||||
})
|
||||
|
||||
manager?.queryDyskineticSymptom(from: fromDate, to: toDate, withHandler: { (_/*results*/, _/*error*/) in
|
||||
|
||||
})
|
||||
}
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder
|
||||
{
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, filename);
|
||||
ORK_ENCODE_OBJ(aCoder, targetSentence);
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self)
|
||||
{
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, filename, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, targetSentence, NSString);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
BOOL isParentSame = [super isEqual:object];
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame &&
|
||||
ORKEqualObjects(self.targetSentence, castObject.targetSentence) &&
|
||||
ORKEqualObjects(self.filename, castObject.filename));
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
ORKSpeechInNoiseResult *result = [super copyWithZone:zone];
|
||||
result.targetSentence = [self.targetSentence copy];
|
||||
result.filename = [self.filename copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"filename = %@;\r\ntargetSentence = %@;", self.filename, self.targetSentence];
|
||||
}
|
||||
|
||||
@end
|
||||
+10
@@ -41,6 +41,16 @@ ORK_CLASS_AVAILABLE
|
||||
*/
|
||||
@interface ORKSpeechInNoiseStep : ORKActiveStep
|
||||
|
||||
/**
|
||||
This property accepts the speech file Path.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *speechFilePath;
|
||||
|
||||
/**
|
||||
This property acceopts the string representation of the speech to be played.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *targetSentence;
|
||||
|
||||
/**
|
||||
This property accepts the speech file.
|
||||
*/
|
||||
+13
-3
@@ -54,6 +54,8 @@
|
||||
|
||||
- (void)commonInit {
|
||||
_willAudioLoop = NO;
|
||||
_speechFilePath = nil;
|
||||
_targetSentence = nil;
|
||||
_noiseFileNameWithExtension = @ORKSpeechInNoiseDefaultNoiseFileName;
|
||||
_filterFileNameWithExtension = @ORKSpeechInNoiseDefaultFilterFileName;
|
||||
_speechFileNameWithExtension = @ORKSpeechInNoiseDefaultSpeechFileName;
|
||||
@@ -73,6 +75,8 @@
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
ORKSpeechInNoiseStep *step = [super copyWithZone:zone];
|
||||
step.speechFilePath = self.speechFilePath;
|
||||
step.targetSentence = self.targetSentence;
|
||||
step.speechFileNameWithExtension = self.speechFileNameWithExtension;
|
||||
step.noiseFileNameWithExtension = self.noiseFileNameWithExtension;
|
||||
step.filterFileNameWithExtension = self.filterFileNameWithExtension;
|
||||
@@ -85,9 +89,11 @@
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
ORK_DECODE_OBJ(aDecoder, speechFileNameWithExtension);
|
||||
ORK_DECODE_OBJ(aDecoder, noiseFileNameWithExtension);
|
||||
ORK_DECODE_OBJ(aDecoder, filterFileNameWithExtension);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, speechFilePath, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, targetSentence, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, speechFileNameWithExtension, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, noiseFileNameWithExtension, NSString);
|
||||
ORK_DECODE_OBJ_CLASS(aDecoder, filterFileNameWithExtension, NSString);
|
||||
ORK_DECODE_DOUBLE(aDecoder, gainAppliedToNoise);
|
||||
ORK_DECODE_BOOL(aDecoder, willAudioLoop);
|
||||
ORK_DECODE_BOOL(aDecoder, hideGraphView);
|
||||
@@ -97,6 +103,8 @@
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
ORK_ENCODE_OBJ(aCoder, speechFilePath);
|
||||
ORK_ENCODE_OBJ(aCoder, targetSentence);
|
||||
ORK_ENCODE_OBJ(aCoder, speechFileNameWithExtension);
|
||||
ORK_ENCODE_OBJ(aCoder, noiseFileNameWithExtension);
|
||||
ORK_ENCODE_OBJ(aCoder, filterFileNameWithExtension);
|
||||
@@ -114,6 +122,8 @@
|
||||
|
||||
__typeof(self) castObject = object;
|
||||
return (isParentSame
|
||||
&& ORKEqualObjects(self.speechFilePath, castObject.speechFilePath)
|
||||
&& ORKEqualObjects(self.targetSentence, castObject.targetSentence)
|
||||
&& ORKEqualObjects(self.speechFileNameWithExtension, castObject.speechFileNameWithExtension)
|
||||
&& ORKEqualObjects(self.noiseFileNameWithExtension, castObject.noiseFileNameWithExtension)
|
||||
&& ORKEqualObjects(self.filterFileNameWithExtension, castObject.filterFileNameWithExtension)
|
||||
+102
-19
@@ -37,12 +37,17 @@
|
||||
#import "ORKStepContainerView_Private.h"
|
||||
#import "ORKSpeechInNoiseContentView.h"
|
||||
#import "ORKSpeechInNoiseStep.h"
|
||||
#import "ORKSpeechInNoiseResult.h"
|
||||
|
||||
#import "ORKCollectionResult_Private.h"
|
||||
#import "ORKHelpers_Internal.h"
|
||||
#import "ORKRoundTappingButton.h"
|
||||
#import "ORKPlaybackButton.h"
|
||||
#import "ORKSkin.h"
|
||||
#import "ORKTaskViewController.h"
|
||||
#import "ORKTaskViewController_Internal.h"
|
||||
|
||||
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
@import Accelerate;
|
||||
@@ -74,9 +79,10 @@
|
||||
_speechAudioBuffer = [[AVAudioPCMBuffer alloc] init];
|
||||
_filterAudioBuffer = [[AVAudioPCMBuffer alloc] init];
|
||||
_installedTap = NO;
|
||||
|
||||
self.speechInNoiseContentView = [[ORKSpeechInNoiseContentView alloc] init];
|
||||
self.activeStepView.activeCustomView = self.speechInNoiseContentView;
|
||||
self.activeStepView.customContentFillsAvailableSpace = YES;
|
||||
self.activeStepView.customContentFillsAvailableSpace = NO;
|
||||
_speechInNoiseContentView.alertColor = [UIColor blueColor];
|
||||
[self.speechInNoiseContentView.playButton addTarget:self action:@selector(tapButtonPressed) forControlEvents:UIControlEventTouchDown];
|
||||
[_speechInNoiseContentView setGraphViewHidden:[self speechInNoiseStep].hideGraphView];
|
||||
@@ -102,7 +108,17 @@
|
||||
}
|
||||
|
||||
- (void)setupBuffers {
|
||||
[self loadFileName:[self speechInNoiseStep].speechFileNameWithExtension intoBuffer:&_speechAudioBuffer];
|
||||
|
||||
if ([[self speechInNoiseStep] speechFilePath] != nil)
|
||||
{
|
||||
NSURL *url = [NSURL fileURLWithPath:[self speechInNoiseStep].speechFilePath isDirectory:NO];
|
||||
[self loadFileAtURL:url intoBuffer:&_speechAudioBuffer];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self loadFileName:[self speechInNoiseStep].speechFileNameWithExtension intoBuffer:&_speechAudioBuffer];
|
||||
}
|
||||
|
||||
[self loadFileName:[self speechInNoiseStep].noiseFileNameWithExtension intoBuffer:&_noiseAudioBuffer];
|
||||
[self loadFileName:[self speechInNoiseStep].filterFileNameWithExtension intoBuffer:&_filterAudioBuffer];
|
||||
|
||||
@@ -124,6 +140,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)loadFileAtURL:(NSURL *)url intoBuffer:(AVAudioPCMBuffer * __strong *)buffer {
|
||||
|
||||
AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:url error:nil];
|
||||
AVAudioFormat *audioFileFormat = audioFile.processingFormat;
|
||||
AVAudioFrameCount audioFileCapacity = (AVAudioFrameCount)audioFile.length;
|
||||
|
||||
if (*buffer == _filterAudioBuffer)
|
||||
{
|
||||
_speechToneCapacity = audioFileCapacity;
|
||||
}
|
||||
else if (*buffer == _noiseAudioBuffer)
|
||||
{
|
||||
_noiseToneCapacity = audioFileCapacity;
|
||||
}
|
||||
else
|
||||
{
|
||||
AVAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
|
||||
CMTime audioDuration = asset.duration;
|
||||
_toneDuration = CMTimeGetSeconds(audioDuration);
|
||||
}
|
||||
|
||||
*buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFileFormat frameCapacity:audioFileCapacity];
|
||||
|
||||
[audioFile readIntoBuffer:*buffer error:nil];
|
||||
}
|
||||
|
||||
- (void)loadFileName: (NSString *)file intoBuffer: (AVAudioPCMBuffer * __strong *)buffer {
|
||||
NSArray *fileComponents = [file componentsSeparatedByString:@"."];
|
||||
NSString *fileName = fileComponents[0];
|
||||
@@ -131,27 +173,14 @@
|
||||
|
||||
NSURL *fileURL = [[NSBundle bundleForClass:[self class]] URLForResource:fileName withExtension:fileExtension];
|
||||
|
||||
AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:fileURL error:nil];
|
||||
AVAudioFormat *audioFileFormat = audioFile.processingFormat;
|
||||
AVAudioFrameCount audioFileCapacity = (AVAudioFrameCount)audioFile.length;
|
||||
if (*buffer == _filterAudioBuffer) {
|
||||
_speechToneCapacity = audioFileCapacity;
|
||||
} else if (*buffer == _noiseAudioBuffer) {
|
||||
_noiseToneCapacity = audioFileCapacity;
|
||||
} else {
|
||||
AVAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
|
||||
CMTime audioDuration = asset.duration;
|
||||
_toneDuration = CMTimeGetSeconds(audioDuration);
|
||||
}
|
||||
*buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFileFormat frameCapacity:audioFileCapacity];
|
||||
[audioFile readIntoBuffer:*buffer error:nil];
|
||||
[self loadFileAtURL:fileURL intoBuffer:buffer];
|
||||
}
|
||||
|
||||
- (void)installTap {
|
||||
|
||||
|
||||
AVAudioFormat *mainMixerFormat = [[_audioEngine mainMixerNode] outputFormatForBus:0];
|
||||
|
||||
[_mixerNode installTapOnBus:0 bufferSize:1024 format:mainMixerFormat block:^(AVAudioPCMBuffer * _Nonnull buffer5, AVAudioTime * _Nonnull when) {
|
||||
[_mixerNode installTapOnBus:0 bufferSize:64 format:mainMixerFormat block:^(AVAudioPCMBuffer * _Nonnull buffer5, AVAudioTime * _Nonnull when) {
|
||||
float * const *channelData = [buffer5 floatChannelData];
|
||||
if (channelData[0]) {
|
||||
float avgValue = 0;
|
||||
@@ -176,11 +205,12 @@
|
||||
[_mixerNode removeTapOnBus:0];
|
||||
[self finish];
|
||||
} else {
|
||||
[self.navigationItem setHidesBackButton:YES animated:YES];
|
||||
[self installTap];
|
||||
[_playerNode play];
|
||||
if ([self speechInNoiseStep].willAudioLoop) {
|
||||
[_speechInNoiseContentView.playButton setTitle:ORKLocalizedString(@"SPEECH_IN_NOISE_STOP_AUDIO_LABEL", nil)
|
||||
forState:UIControlStateNormal];
|
||||
forState:UIControlStateNormal];
|
||||
[_speechInNoiseContentView.playButton setTintColor:[UIColor ork_redColor]];
|
||||
} else {
|
||||
ORKWeakTypeOf(self) weakSelf = self;
|
||||
@@ -199,4 +229,57 @@
|
||||
return (ORKSpeechInNoiseStep *)self.step;
|
||||
}
|
||||
|
||||
- (NSString *)filename
|
||||
{
|
||||
NSString *filename = nil;
|
||||
|
||||
|
||||
BOOL (^validate)(NSString * _Nullable) = ^BOOL(NSString * _Nullable str) { return str && str.length > 0; };
|
||||
|
||||
NSString *path = [[self speechInNoiseStep] speechFilePath];
|
||||
NSString *file = [path lastPathComponent];
|
||||
|
||||
if (validate(file))
|
||||
{
|
||||
filename = [file copy];
|
||||
}
|
||||
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
||||
- (ORKStepResult *)result
|
||||
{
|
||||
ORKStepResult *sResult = [super result];
|
||||
|
||||
|
||||
ORKSpeechInNoiseStep *currentStep = (ORKSpeechInNoiseStep *)self.step;
|
||||
|
||||
if (currentStep && [currentStep isKindOfClass:[ORKSpeechInNoiseStep class]] && currentStep.targetSentence)
|
||||
{
|
||||
NSMutableArray *results = [NSMutableArray arrayWithArray:sResult.results];
|
||||
|
||||
ORKSpeechInNoiseResult *speechInNoiseResult = [[ORKSpeechInNoiseResult alloc] initWithIdentifier:currentStep.identifier];
|
||||
|
||||
speechInNoiseResult.targetSentence = currentStep.targetSentence;
|
||||
|
||||
speechInNoiseResult.filename = [self filename];
|
||||
|
||||
[results addObject:speechInNoiseResult];
|
||||
|
||||
sResult.results = [results copy];
|
||||
}
|
||||
|
||||
return sResult;
|
||||
}
|
||||
|
||||
- (void)finish
|
||||
{
|
||||
[_speechInNoiseContentView removeAllSamples];
|
||||
|
||||
[super finish];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
BIN
Binary file not shown.
@@ -34,10 +34,20 @@
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@class ORKBorderedButton;
|
||||
@class ORKRecordButton;
|
||||
|
||||
@protocol ORKSpeechRecognitionContentViewDelegate <NSObject>
|
||||
|
||||
- (void)didPressRecordButton:(ORKRecordButton *)recordButton;
|
||||
|
||||
- (void)didPressUseKeyboardButton;
|
||||
|
||||
@end
|
||||
|
||||
@interface ORKSpeechRecognitionContentView : ORKActiveStepCustomView
|
||||
|
||||
@property (nonatomic, weak) id<ORKSpeechRecognitionContentViewDelegate> delegate;
|
||||
|
||||
@property (nonatomic, copy, nullable) UIColor *keyColor;
|
||||
|
||||
@property (nonatomic, assign) BOOL failed;
|
||||
@@ -46,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, copy, nullable) NSArray *samples;
|
||||
|
||||
@property (nonatomic) ORKBorderedButton *recordButton;
|
||||
@property (nonatomic) ORKRecordButton *recordButton;
|
||||
|
||||
@property (nonatomic, copy, nullable) UIImage *speechRecognitionImage;
|
||||
|
||||
@@ -60,7 +70,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (void)updateRecognitionText:(NSString *)recognitionText;
|
||||
|
||||
- (void)addRecognitionError:(NSString *)errorMsg;
|
||||
- (void)addRecognitionError:(NSString * _Nullable)errorMsg;
|
||||
|
||||
- (void)updateButtonStates;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user