Merge branch 'master' into ValidatedText

This commit is contained in:
Steve Cadwallader
2015-10-07 22:05:11 -04:00
11 changed files with 191 additions and 67 deletions
+130
View File
@@ -0,0 +1,130 @@
# ResearchKit Release Notes
## ResearchKit 1.2 Release Notes
*ResearchKit 1.2* supports *iOS* and requires *Xcode 7.0* or newer. The minimum supported *Base SDK* is *8.0*.
In addition to general stability and performance improvements, *ResearchKit 1.2* includes the following new features and enhancements.
- **New Active Tasks**
- **Tower of Hanoi Task**
*Contributed by [coxy1989](https://github.com/coxy1989).*
The *[Tower of Hanoi](https://en.wikipedia.org/wiki/Tower_of_Hanoi#Applications) task* is frequently used in psychological research on problem solving.
It is a mathematical puzzle consisting of three rods and a number of disks of different sizes which can slide onto any rod. The puzzle starts with the disks in a stack in ascending order of size on one rod (the smallest at the top).
The objective of the puzzle is to move the entire stack to another rod, obeying the following rules:
1. Only one disk can be moved at a time.
2. Each move consists of taking the upper disk from one of the stacks and placing it on top of another stack.
3. No disk may be placed on top of a smaller disk.
- **Paced Serial Addition Test Task**
*Contributed by [Julien Therier](https://github.com/julientherier).*
The *Paced Serial Addition Test task* provides adaptations of both the *Paced Auditory Serial Addition Test (PASAT)* and the *Paced Visual Serial Addition Test (PVSAT)*.
The *[PASAT](https://en.wikipedia.org/wiki/Paced_Auditory_Serial_Addition_Test)* is a neuropsychological test used to assess capacity and rate of information processing and sustained and divided attention.
Both tests are documented in the scientific literature ([Fos et al., 2000](http://www.ncbi.nlm.nih.gov/pubmed/11125707); [Nagels et al., 2005](http://www.ncbi.nlm.nih.gov/pubmed/15823678)) as a measure of the [*Multiple Sclerosis Functional Score*](http://www.nationalmssociety.org/For-Professionals/Researchers/Resources-for-Researchers/Clinical-Study-Measures/Multiple-Sclerosis-Functional-Composite-%28MSFC%29).
This task generates a series of single digits (for example, 60 of them), at the specific frequency (for example, one new digit every 2 or 3 seconds). The user must add the newly presented digit to the one prior to it.
- **Timed Walk Task**
*Contributed by [Julien Therier](https://github.com/julientherier).*
The *Timed Walk task* measures gait speed and is an adaptation of the [*Timed 25-Foot Walk*](http://www.nationalmssociety.org/For-Professionals/Researchers/Resources-for-Researchers/Clinical-Study-Measures/Timed-25-Foot-Walk-%28T25-FW%29) in the context of *multiple sclerosis*.
Gait speed has been demonstrated to be a useful and reliable functional measure of walking ability. When administering the *Timed Walk Task*, patients are allowed to use assistive devices (canes, crutches, walkers).
- **Charts Module**
*Contributed by [coxy1989](https://github.com/coxy1989) and [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
A *Charts module* has been implemented. It features three chart types: a *pie chart* (`ORKPieChartView`), a *line graph chart* (`ORKLineGraphChartView`), and a *discrete graph chart* (`ORKDiscreteGraphChartView`).
The views in the *Charts module* can be used independently of the rest of *ResearchKit*. It doesn't automatically connect with any other *ResearchKit* module: the developer has to supply the data to be displayed through the views' `dataSources`, which allows for maximum flexibility.
- **Other Improvements**
- **Scale Answer Format**
*Contributed by [Apple Inc](https://github.com/researchkit).*
*Discrete scales* now support *text choice* labels, and all *scales* support images in place of the minimum and maximum range labels.
- **Result Predicates**
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
The predicate-building methods in `ORKResultPredicate` now use the new `ORKResultSelector` class for unequivocally identifying a *question step result* or a *form item result*.
This eliminates ambiguity when matching results with the same inner scope identifier. For example, a *form item result* can have the same identifier as a *question step result* or as another *form item result* in a different *form step*, and you can now match them separately.
## ResearchKit 1.1 Release Notes
*ResearchKit 1.1* supports *iOS* and requires *Xcode 6.3* or newer. The minimum supported *Base SDK* is *8.0*.
In addition to general stability and performance improvements, *ResearchKit 1.1* includes the following new features and enhancements.
- **Navigable Ordered Task**
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
A new type of *conditional ordered task* (`ORKNavigableOrderedTask`) has been implemented.
The developer can use the `ORKStepNavigationRule` subclasses to dynamically navigate between the task steps:
- `ORKPredicateStepNavigationRule` allows to make conditional jumps by matching previous results (either those of the the ongoing task, or those of any previously stored task result tree). You typically use the class methods in the `ORKResultPredicate` class to match answers in the most commonly used result types.
- `ORKDirectStepNavigationRule` provides support for unconditional jumps.
- **New Active Tasks**
- **Reaction Time Task**
*Contributed by [coxy1989](https://github.com/coxy1989).*
The *Reaction Time Task* is an adaptation of the [*Simple Reaction Time test (SRT)*](http://www.cambridgecognition.com/tests/simple-reaction-time-srt). *SRT* measures reaction time through delivery of a known stimulus to a known location to elicit a known response.
This test is deployed in a range of research questions across fields including medicine, sports science and psychology.
Although it classically involves pressing the space bar or clicking a mouse in response to an event on screen, the *ResearchKit* implementation relies on the study participant shaking the device when she sees a blue circle on the screen, which we think is more correlatable to a true stimulus reaction test.
- **Tone Audiometry Task**
*Contributed by [Vincent Tourraine](https://github.com/vtourraine).*
The *Tone Audiometry Task* is an adaptation of the [*Pure Tone Audiometry test (PTA)*](https://en.wikipedia.org/wiki/Pure_tone_audiometry). *PTA* is a key hearing test used to identify hearing threshold levels of an individual, enabling determination of the degree, type and configuration of a hearing loss.
The *ResearchKit* implementation generates a series of pure sinusoid sounds, with different frequencies and on different channels (left or right). The test starts at the minimum volume and is gradually increased until the participant perceives it and taps a button. At that time, the current sound amplitude, frequency and channel are recorded.
- **Scale Answer Format Enhancements**
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez) and [Bruce Duncan](https://github.com/brucehappy).*
Support for discrete and continuous *vertical scales* has been added. Some questions, like mood measurement or symptom severity measurement queries may be more naturally presented using a *vertical scale*.
The *Scale Answer Format* has also been improved by making it usable within forms.
- **Image Capture Step**
*Contributed by [Bruce Duncan](https://github.com/brucehappy).*
An *Image Capture Step* has been added. The researcher can ask the participant to take pictures of relevant body parts. The researcher can provide a body part image template to facilitate the scale and orientation of the taken pictures.
- **iPad Support**
*Contributed by [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez) and [Apple Inc](https://github.com/researchkit).*
*iPad support* for all orientations has been implemented.
- **iPhone Landscape Support**
*Contributed by [Apple Inc.](https://github.com/researchkit) and [Ricardo Sánchez-Sáez](https://github.com/rsanchezsaez).*
*iPhone landscape orientation support* has been implemented.
-1
View File
@@ -246,7 +246,6 @@ ORK_CLASS_AVAILABLE
calendar:(nullable NSCalendar *)calendar;
+ (ORKTextAnswerFormat *)textAnswerFormat;
+ (ORKTextAnswerFormat *)textAnswerFormatWithMaximumLength:(NSInteger)maximumLength;
+ (ORKTextAnswerFormat *)textAnswerFormatWithValidationExpression:(NSString *)expression invalidMessage:(NSString *)invalidMessage;
+1 -3
View File
@@ -346,7 +346,6 @@ NSNumberFormatterStyle ORKNumberFormattingStyleConvert(ORKNumberFormattingStyle
+ (ORKTextAnswerFormat *)textAnswerFormat {
return [ORKTextAnswerFormat new];
}
+ (ORKTextAnswerFormat *)textAnswerFormatWithMaximumLength:(NSInteger)maximumLength {
return [[ORKTextAnswerFormat alloc] initWithMaximumLength:maximumLength];
}
@@ -362,7 +361,6 @@ NSNumberFormatterStyle ORKNumberFormattingStyleConvert(ORKNumberFormattingStyle
+ (ORKTimeIntervalAnswerFormat *)timeIntervalAnswerFormat {
return [ORKTimeIntervalAnswerFormat new];
}
+ (ORKTimeIntervalAnswerFormat *)timeIntervalAnswerFormatWithDefaultInterval:(NSTimeInterval)defaultInterval
step:(NSInteger)step {
return [[ORKTimeIntervalAnswerFormat alloc] initWithDefaultInterval:defaultInterval step:step];
@@ -1908,7 +1906,7 @@ static NSArray *ork_processTextChoices(NSArray<ORKTextChoice *> *textChoices) {
- (BOOL)isAnswerValid:(id)answer {
BOOL isValid = NO;
if ([answer isKindOfClass:[NSString class]]) {
isValid = [self isAnswerValidWithString:(NSString *)answer];
return [self isAnswerValidWithString:(NSString *)answer];
}
return isValid;
}
+5 -1
View File
@@ -35,9 +35,13 @@
#define STRONGTYPE(x) __strong __typeof(x)
ORK_EXTERN NSBundle *ORKBundle() ORK_AVAILABLE_DECL;
ORK_EXTERN NSBundle *ORKDefaultLocaleBundle();
#define ORKDefaultLocalizedValue(key) \
[ORKDefaultLocaleBundle() localizedStringForKey:key value:@"" table:nil]
#define ORKLocalizedString(key, comment) \
[ORKBundle() localizedStringForKey:(key) value:@"" table:nil]
[ORKBundle() localizedStringForKey:(key) value:ORKDefaultLocalizedValue(key) table:nil]
#define ORKLocalizedStringFromNumber(number) \
[NSNumberFormatter localizedStringFromNumber:number numberStyle:NSNumberFormatterNoStyle]
+1
View File
@@ -155,6 +155,7 @@
// Bundle for video assets
NSBundle *ORKAssetsBundle(void);
NSBundle *ORKBundle();
NSBundle *ORKDefaultLocaleBundle();
// Pass 0xcccccc and get color #cccccc
UIColor *ORKRGB(uint32_t x);
+20 -2
View File
@@ -247,8 +247,26 @@ NSDateFormatter *ORKTimeOfDayLabelFormatter() {
}
NSBundle *ORKBundle() {
NSBundle *bundle = [NSBundle bundleForClass:[ORKStep class]];
return bundle;
static NSBundle *__bundle;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__bundle = [NSBundle bundleForClass:[ORKStep class]];
});
return __bundle;
}
NSBundle *ORKDefaultLocaleBundle() {
static NSBundle *__bundle;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [ORKBundle() pathForResource:[ORKBundle() objectForInfoDictionaryKey:@"CFBundleDevelopmentRegion"] ofType:@"lproj"];
__bundle = [NSBundle bundleWithPath:path];
});
return __bundle;
}
NSDateComponentsFormatter *ORKTimeIntervalLabelFormatter() {
@@ -51,6 +51,8 @@
BOOL _isTouchIdAuthenticated;
BOOL _isPasscodeSaved;
LAContext *_touchContext;
ORKPasscodeType _authenticationPasscodeType;
BOOL _useTouchId;
}
- (ORKPasscodeStep *)passcodeStep {
@@ -78,6 +80,7 @@
_isChangingState = NO;
_isTouchIdAuthenticated = NO;
_isPasscodeSaved = NO;
_useTouchId = YES;
// Set the starting passcode state and textfield based on flow.
switch (_passcodeFlow) {
@@ -89,19 +92,19 @@
case ORKPasscodeFlowAuthenticate:
[self setValuesFromKeychain];
_passcodeStepView.textField.numberOfDigits = [self numberOfDigitsForPasscodeType:self.authenticationPasscodeType];
_passcodeStepView.textField.numberOfDigits = [self numberOfDigitsForPasscodeType:_authenticationPasscodeType];
[self changeStateTo:ORKPasscodeStateEntry];
break;
case ORKPasscodeFlowEdit:
[self setValuesFromKeychain];
_passcodeStepView.textField.numberOfDigits = [self numberOfDigitsForPasscodeType:self.authenticationPasscodeType];
_passcodeStepView.textField.numberOfDigits = [self numberOfDigitsForPasscodeType:_authenticationPasscodeType];
[self changeStateTo:ORKPasscodeStateOldEntry];
break;
}
// If Touch ID was enabled then present it for authentication flow.
if (self.useTouchId &&
if (_useTouchId &&
self.passcodeFlow == ORKPasscodeFlowAuthenticate) {
[self promptTouchId];
}
@@ -124,7 +127,9 @@
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self makePasscodeViewBecomeFirstResponder];
if (!_shouldResignFirstResponder) {
[self makePasscodeViewBecomeFirstResponder];
}
}
- (void)viewWillDisappear:(BOOL)animated {
@@ -219,13 +224,6 @@
return stepResult;
}
- (BOOL)useTouchId {
if (self.passcodeFlow == ORKPasscodeFlowCreate) {
_useTouchId = YES;
}
return _useTouchId;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@@ -253,17 +251,17 @@
}
}
- (void)makePasscodeViewBecomeFirstResponder{
if (! _passcodeStepView.textField.isFirstResponder) {
_shouldResignFirstResponder = NO;
- (void)makePasscodeViewBecomeFirstResponder {
_shouldResignFirstResponder = NO;
if (!_passcodeStepView.textField.isFirstResponder) {
[_passcodeStepView.textField becomeFirstResponder];
}
}
- (void)makePasscodeViewResignFirstResponder {
_shouldResignFirstResponder = YES;
if (_passcodeStepView.textField.isFirstResponder) {
_shouldResignFirstResponder = YES;
[_passcodeStepView.textField endEditing:YES];
[_passcodeStepView.textField resignFirstResponder];
}
}
@@ -272,7 +270,7 @@
_touchContext.localizedFallbackTitle = @"";
// Check to see if the device supports Touch ID.
if (self.useTouchId &&
if (_useTouchId &&
[_touchContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil]) {
/// Device does support Touch ID.
@@ -288,8 +286,6 @@
typeof(self) strongSelf = weakSelf;
[strongSelf makePasscodeViewBecomeFirstResponder];
if (success) {
// Store that user passed authentication.
_isTouchIdAuthenticated = YES;
@@ -305,8 +301,13 @@
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_OK", nil)
style:UIAlertActionStyleDefault
handler:nil]];
handler:^(UIAlertAction * action) {
typeof(self) strongSelf = weakSelf;
[strongSelf makePasscodeViewBecomeFirstResponder];
}]];
[strongSelf presentViewController:alert animated:YES completion:nil];
} else if (error.code == LAErrorUserCancel) {
[strongSelf makePasscodeViewBecomeFirstResponder];
}
[strongSelf finishTouchId];
@@ -390,8 +391,11 @@
}
NSString *storedPasscode = dictionary[KeychainDictionaryPasscodeKey];
self.useTouchId = [dictionary[KeychainDictionaryTouchIdKey] boolValue];
self.authenticationPasscodeType = (storedPasscode.length == 4) ? ORKPasscodeType4Digit : ORKPasscodeType6Digit;
_authenticationPasscodeType = (storedPasscode.length == 4) ? ORKPasscodeType4Digit : ORKPasscodeType6Digit;
if (self.passcodeFlow == ORKPasscodeFlowAuthenticate) {
_useTouchId = [dictionary[KeychainDictionaryTouchIdKey] boolValue];
}
}
- (void)wrongAttempt {
@@ -58,8 +58,6 @@ typedef NS_ENUM(NSUInteger, ORKPasscodeState) {
@property (nonatomic) ORKPasscodeFlow passcodeFlow;
@property (nonatomic, weak) id<ORKPasscodeDelegate> passcodeDelegate;
@property (nonatomic) ORKPasscodeType authenticationPasscodeType;
@property (nonatomic) BOOL useTouchId;
@end
@@ -283,30 +283,6 @@
return YES;
}
- (BOOL)isAnswerValid {
id answer = self.answer;
if (answer == ORKNullAnswerValue()) {
return YES;
}
ORKAnswerFormat *answerFormat = [self.step impliedAnswerFormat];
ORKTextAnswerFormat *textFormat = (ORKTextAnswerFormat *)answerFormat;
return [textFormat isAnswerValidWithString:self.textField.text];
}
// This convenience method checks for answer validity, will display alert
// if text input is not valid...
- (BOOL)isAnswerValidForTextField:(UITextField *)textField {
BOOL isValid = [self isAnswerValid];
if (! isValid) {
[self showValidityAlertWithMessage:[[self.step impliedAnswerFormat] localizedInvalidValueStringWithAnswerString:textField.text]];
isValid = NO;
}
return isValid;
}
- (void)answerDidChange {
id answer = self.answer;
ORKAnswerFormat *answerFormat = [self.step impliedAnswerFormat];
@@ -365,3 +341,4 @@
}
@end
+7 -7
View File
@@ -3074,12 +3074,12 @@ static const CGFloat HeaderSideLayoutMargin = 16.0;
/*
Dismisses the task view controller.
*/
- (void)dismissTaskViewController:(ORKTaskViewController *)taskViewController {
- (void)dismissTaskViewController:(ORKTaskViewController *)taskViewController removeOutputDirectory:(BOOL)removeOutputDirectory {
_currentDocument = nil;
NSURL *dir = taskViewController.outputDirectory;
NSURL *outputDirectoryURL = taskViewController.outputDirectory;
[self dismissViewControllerAnimated:YES completion:^{
if (dir)
if (outputDirectoryURL && removeOutputDirectory)
{
/*
We attempt to clean up the output directory.
@@ -3089,8 +3089,8 @@ static const CGFloat HeaderSideLayoutMargin = 16.0;
delete your data when you've processed it or sent it to a server.
*/
NSError *err = nil;
if (! [[NSFileManager defaultManager] removeItemAtURL:dir error:&err]) {
NSLog(@"Error removing %@: %@", dir, err);
if (! [[NSFileManager defaultManager] removeItemAtURL:outputDirectoryURL error:&err]) {
NSLog(@"Error removing %@: %@", outputDirectoryURL, err);
}
}
}];
@@ -3257,7 +3257,7 @@ stepViewControllerWillAppear:(ORKStepViewController *)stepViewController {
NSLog(@"Error on step %@: %@", taskViewController.currentStepViewController.step, error);
break;
case ORKTaskViewControllerFinishReasonDiscarded:
[self dismissTaskViewController:taskViewController];
[self dismissTaskViewController:taskViewController removeOutputDirectory:YES];
break;
case ORKTaskViewControllerFinishReasonSaved:
{
@@ -3273,7 +3273,7 @@ stepViewControllerWillAppear:(ORKStepViewController *)stepViewController {
if ([task isKindOfClass:[ORKNavigableOrderedTask class]]) {
_savedTasks[task.identifier] = [NSKeyedArchiver archivedDataWithRootObject:task];
}
[self dismissTaskViewController:taskViewController];
[self dismissTaskViewController:taskViewController removeOutputDirectory:NO];
return;
}
break;
@@ -306,11 +306,6 @@ enum TaskListRow: Int, CustomStringConvertible {
case ContinuousVerticalScaleQuestionStep
case TextScaleQuestionStep
case TextVerticalScaleQuestionStep
// Task with an exampled of validated text entry.
case ValidatedTextQuestionTask
case ValidatedTextQuestionStepEmail
case ValidatedTextQuestionStepDomain
// Task with an example of free text entry.
case TextQuestionTask