Files
ResearchKit/ResearchKitUI/Common/Task/ORKTaskViewController.m
Pariece McKinney 5c5d295bd5 Public release 3.1.0
2024-10-15 17:05:47 -04:00

1889 lines
81 KiB
Objective-C

/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKTaskViewController.h"
#import "ORKInstructionStepViewController_Internal.h"
#import "ORKFormStepViewController.h"
#import "ORKReviewStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
#import "ORKLearnMoreStepViewController.h"
#import "ORKActiveStep.h"
#import "ORKCollectionResult_Private.h"
#import "ORKFormStep.h"
#import "ORKInstructionStep.h"
#import "ORKOrderedTask.h"
#import "ORKQuestionStep.h"
#import "ORKResult_Private.h"
#import "ORKReviewStep_Internal.h"
#import "ORKStep_Private.h"
#import "ORKViewControllerProviding.h"
#import "ORKHelpers_Internal.h"
#import "ORKObserver.h"
#import "ORKSkin.h"
#import "ORKBorderedButton.h"
#import "ORKTaskReviewViewController.h"
@import AVFoundation;
@import CoreMotion;
#if ORK_FEATURE_CLLOCATIONMANAGER_AUTHORIZATION
#import <CoreLocation/CLLocationManagerDelegate.h>
#import <ResearchKit/CLLocationManager+ResearchKit.h>
typedef void (^_ORKLocationAuthorizationRequestHandler)(BOOL success);
@interface ORKLocationAuthorizationRequester : NSObject <CLLocationManagerDelegate>
- (instancetype)initWithHandler:(_ORKLocationAuthorizationRequestHandler)handler;
- (void)resume;
@end
#endif
#if ORK_FEATURE_CLLOCATIONMANAGER_AUTHORIZATION
@implementation ORKLocationAuthorizationRequester {
CLLocationManager *_manager;
_ORKLocationAuthorizationRequestHandler _handler;
BOOL _started;
}
- (instancetype)initWithHandler:(_ORKLocationAuthorizationRequestHandler)handler {
self = [super init];
if (self) {
_handler = handler;
_manager = [CLLocationManager new];
_manager.delegate = self;
}
return self;
}
- (void)dealloc {
_manager.delegate = nil;
}
- (void)resume {
if (_started) {
return;
}
_started = YES;
NSString *whenInUseKey = (NSString *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"];
NSString *alwaysKey = (NSString *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysAndWhenInUseUsageDescription"];
CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
if ((status == kCLAuthorizationStatusNotDetermined) && (whenInUseKey || alwaysKey)) {
BOOL requestWasDelivered = YES;
if (alwaysKey) {
requestWasDelivered = [_manager ork_requestAlwaysAuthorization];
} else {
requestWasDelivered = [_manager ork_requestWhenInUseAuthorization];
}
if (requestWasDelivered == NO) {
[self finishWithResult:NO];
}
} else {
[self finishWithResult:(status != kCLAuthorizationStatusDenied)];
}
}
- (void)finishWithResult:(BOOL)result {
if (_handler) {
_handler(result);
_handler = nil;
}
}
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
if (_handler && _started && status != kCLAuthorizationStatusNotDetermined) {
[self finishWithResult:(status != kCLAuthorizationStatusDenied)];
}
}
@end
#endif
/// An interface for managing a task in a view.
///
//// Some task workflows may be longer than others, so participants might switch to another app. To keep continuity between steps in a task, ``ORKTaskViewController`` supports saving and restoring a task's progress and user state.
@interface ORKTaskViewController () <ORKTaskReviewViewControllerDelegate, UINavigationControllerDelegate> {
ORKScrollViewObserver *_scrollViewObserver;
BOOL _hasBeenPresented;
BOOL _hasRequestedHealthData;
ORKPermissionMask _grantedPermissions;
NSSet<HKObjectType *> *_requestedHealthTypesForRead;
NSSet<HKObjectType *> *_requestedHealthTypesForWrite;
NSURL *_outputDirectory;
NSDate *_presentedDate;
NSDate *_dismissedDate;
NSString *_lastBeginningInstructionStepIdentifier;
NSString *_lastRestorableStepIdentifier;
BOOL _hasAudioSession; // does not need state restoration - temporary
NSString *_restoredTaskIdentifier;
UINavigationController *_childNavigationController;
UIViewController *_previousToTopControllerInNavigationStack;
NSString *_forcedNextStepIdentifier;
}
@property (nonatomic, strong) ORKStepViewController *currentStepViewController;
@property (nonatomic) ORKTaskReviewViewController *taskReviewViewController;
@end
@implementation ORKTaskViewController
@synthesize taskRunUUID=_taskRunUUID;
static NSString *const _ChildNavigationControllerRestorationKey = @"childNavigationController";
- (void)setUpChildNavigationController {
_previousToTopControllerInNavigationStack = nil;
UIViewController *emptyViewController = [[UIViewController alloc] initWithNibName:nil bundle:nil];
_childNavigationController = [[UINavigationController alloc] initWithRootViewController:emptyViewController];
_childNavigationController.delegate = self;
[_childNavigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
[_childNavigationController.navigationBar setShadowImage:[UIImage new]];
[_childNavigationController.navigationBar setTranslucent:NO];
[_childNavigationController.navigationBar setBarTintColor:ORKColor(ORKBackgroundColorKey)];
[_childNavigationController.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName : [UIColor secondaryLabelColor]}];
_childNavigationController.navigationBar.prefersLargeTitles = NO;
[_childNavigationController.view setBackgroundColor:UIColor.clearColor];
[self addChildViewController:_childNavigationController];
_childNavigationController.view.frame = self.view.frame;
_childNavigationController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_childNavigationController.view];
[_childNavigationController didMoveToParentViewController:self];
_childNavigationController.restorationClass = [self class];
_childNavigationController.restorationIdentifier = _ChildNavigationControllerRestorationKey;
}
- (instancetype)commonInitWithTask:(id<ORKTask>)task taskRunUUID:(NSUUID *)taskRunUUID {
[self setTask:task];
self.showsProgressInNavigationBar = YES;
self.discardable = NO;
self.skipSaveResultsConfirmation = NO;
self.progressMode = ORKTaskViewControllerProgressModeQuestionsPerStep;
_managedResults = [NSMutableDictionary dictionary];
_managedStepIdentifiers = [NSMutableArray array];
self.taskRunUUID = taskRunUUID ?: [NSUUID UUID];
// Ensure taskRunUUID has non-nil valuetaskRunUUID
(void)[self taskRunUUID];
self.restorationClass = [ORKTaskViewController class];
return self;
}
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
return [self commonInitWithTask:nil taskRunUUID:nil];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
return [self commonInitWithTask:nil taskRunUUID:nil];
}
#pragma clang diagnostic pop
- (instancetype)initWithTask:(id<ORKTask>)task taskRunUUID:(NSUUID *)taskRunUUID {
self = [super initWithNibName:nil bundle:nil];
return [self commonInitWithTask:task taskRunUUID:taskRunUUID];
}
- (instancetype)initWithTask:(id<ORKTask>)task restorationData:(NSData *)data delegate:(id<ORKTaskViewControllerDelegate>)delegate error:(NSError* __autoreleasing *)errorOut {
self = [[super initWithNibName:nil bundle:nil] commonInitWithTask:task taskRunUUID:nil];
if (self) {
self.delegate = delegate;
if (data != nil) {
self.restorationClass = [self class];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:errorOut];
[self decodeRestorableStateWithCoder:unarchiver];
[unarchiver finishDecoding];
[self applicationFinishedRestoringState];
if (unarchiver == nil && errorOut != nil) {
*errorOut = [NSError errorWithDomain:ORKErrorDomain code:ORKErrorException userInfo:@{NSLocalizedDescriptionKey: ORKLocalizedString(@"RESTORE_ERROR_CANNOT_DECODE", nil)}];
}
}
}
return self;
}
- (instancetype)initWithTask:(id<ORKTask>)task
ongoingResult:(nullable ORKTaskResult *)ongoingResult
defaultResultSource:(nullable id<ORKTaskResultSource>)defaultResultSource
delegate:(id<ORKTaskViewControllerDelegate>)delegate {
self = [[super initWithNibName:nil bundle:nil] commonInitWithTask:task taskRunUUID:nil];
if (self) {
_delegate = delegate;
_defaultResultSource = defaultResultSource;
if (ongoingResult != nil) {
for (ORKResult *stepResult in ongoingResult.results) {
NSString *stepResultIdentifier = stepResult.identifier;
if ([task stepWithIdentifier:stepResultIdentifier] == nil) {
ORK_Log_Error("ongoingResults has results for identifiers not found within the task steps, skipping adding result for step %@", stepResultIdentifier);
continue;
}
[_managedStepIdentifiers addObject:stepResultIdentifier];
_managedResults[stepResultIdentifier] = stepResult;
}
_restoredStepIdentifier = ongoingResult.results.lastObject.identifier;
}
}
return self;
}
- (instancetype)initWithTask:(id<ORKTask>)task
ongoingResult:(ORKTaskResult *)ongoingResult
restoreAtFirstStep:(BOOL)restoreAtFirstStep
defaultResultSource:(id<ORKTaskResultSource>)defaultResultSource
delegate:(id<ORKTaskViewControllerDelegate>)delegate {
self = [self initWithTask:task ongoingResult:ongoingResult defaultResultSource:defaultResultSource delegate:delegate];
if (self) {
if (restoreAtFirstStep && ongoingResult != nil) {
_restoredStepIdentifier = ongoingResult.results.firstObject.identifier;
}
}
return self;
}
- (void)setTaskRunUUID:(NSUUID *)taskRunUUID {
if (_hasBeenPresented) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Cannot change task instance UUID after presenting task controller" userInfo:nil];
}
_taskRunUUID = [taskRunUUID copy];
}
- (void)setTask:(id<ORKTask>)task {
if (_hasBeenPresented) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Cannot change task after presenting task controller" userInfo:nil];
}
if (task) {
if (![task conformsToProtocol:@protocol(ORKTask)]) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Expected a task" userInfo:nil];
}
if (task.identifier == nil) {
ORK_Log_Debug("Task identifier should not be nil.");
}
if ([task respondsToSelector:@selector(validateParameters)]) {
[task validateParameters];
}
}
_hasRequestedHealthData = NO;
_task = task;
}
- (UIBarButtonItem *)defaultCancelButtonItem {
return [[UIBarButtonItem alloc] initWithTitle:ORKLocalizedString(@"BUTTON_CANCEL", nil) style:UIBarButtonItemStylePlain target:self action:@selector(cancelAction:)];
}
- (UIBarButtonItem *)defaultLearnMoreButtonItem {
return [[UIBarButtonItem alloc] initWithTitle:ORKLocalizedString(@"BUTTON_LEARN_MORE", nil) style:UIBarButtonItemStylePlain target:self action:@selector(learnMoreAction:)];
}
- (void)requestHealthStoreAccessWithReadTypes:(NSSet *)readTypes
writeTypes:(NSSet *)writeTypes
handler:(void (^)(void))handler {
NSParameterAssert(handler != nil);
BOOL needsHealthKitAuthRequest = NO;
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
needsHealthKitAuthRequest = ([HKHealthStore isHealthDataAvailable]);
needsHealthKitAuthRequest = needsHealthKitAuthRequest && ((readTypes != nil) || (writeTypes != nil));
#endif
if (needsHealthKitAuthRequest == NO) {
_requestedHealthTypesForRead = nil;
_requestedHealthTypesForWrite = nil;
dispatch_async(dispatch_get_main_queue(), handler);
return;
}
_requestedHealthTypesForRead = readTypes;
_requestedHealthTypesForWrite = writeTypes;
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
__block HKHealthStore *healthStore = [HKHealthStore new];
[healthStore requestAuthorizationToShareTypes:writeTypes readTypes:readTypes completion:^(BOOL success, NSError *error) {
ORK_Log_Error("Health access: error=%@", error);
dispatch_async(dispatch_get_main_queue(), handler);
// Clear self-ref.
healthStore = nil;
}];
#endif
}
- (void)requestPedometerAccessWithHandler:(void (^)(BOOL success))handler {
NSParameterAssert(handler != nil);
if (![CMPedometer isStepCountingAvailable]) {
handler(NO);
return;
}
__block CMPedometer *pedometer = [CMPedometer new];
[pedometer queryPedometerDataFromDate:[NSDate dateWithTimeIntervalSinceNow:-100]
toDate:[NSDate date]
withHandler:^(CMPedometerData *pedometerData, NSError *error) {
ORK_Log_Error("Pedometer access: error=%@", error);
BOOL success = YES;
if ([[error domain] isEqualToString:CMErrorDomain]) {
switch (error.code) {
case CMErrorMotionActivityNotAuthorized:
case CMErrorNotAuthorized:
case CMErrorNotAvailable:
case CMErrorNotEntitled:
case CMErrorMotionActivityNotAvailable:
case CMErrorMotionActivityNotEntitled:
success = NO;
break;
default:
break;
}
}
dispatch_async(dispatch_get_main_queue(), ^(void) { handler(success); });
// Clear self ref to release.
pedometer = nil;
}];
}
- (void)requestAudioRecordingAccessWithHandler:(void (^)(BOOL success))handler {
NSParameterAssert(handler != nil);
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(granted);
});
}];
}
- (void)requestCameraAccessWithHandler:(void (^)(BOOL success))handler {
NSParameterAssert(handler != nil);
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(granted);
});
}];
}
#if ORK_FEATURE_CLLOCATIONMANAGER_AUTHORIZATION
- (void)requestLocationAccessWithHandler:(void (^)(BOOL success))handler {
NSParameterAssert(handler != nil);
// Self-retain; clear the retain cycle in the handler block.
__block ORKLocationAuthorizationRequester *requester =
[[ORKLocationAuthorizationRequester alloc]
initWithHandler:^(BOOL success) {
handler(success);
requester = nil;
}];
[requester resume];
}
#endif
- (ORKPermissionMask)desiredPermissions {
ORKPermissionMask permissions = ORKPermissionNone;
if ([self.task respondsToSelector:@selector(requestedPermissions)]) {
permissions = [self.task requestedPermissions];
}
return permissions;
}
- (void)requestHealthAuthorizationWithCompletion:(void (^)(void))completion {
if (_hasRequestedHealthData) {
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
return;
}
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
NSSet *readTypes = nil;
if ([self.task respondsToSelector:@selector(requestedHealthKitTypesForReading)]) {
readTypes = [self.task requestedHealthKitTypesForReading];
}
NSSet *writeTypes = nil;
if ([self.task respondsToSelector:@selector(requestedHealthKitTypesForWriting)]) {
writeTypes = [self.task requestedHealthKitTypesForWriting];
}
#endif
ORKPermissionMask permissions = [self desiredPermissions];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
ORK_Log_Debug("Requesting health access");
[self requestHealthStoreAccessWithReadTypes:readTypes
writeTypes:writeTypes
handler:^{
dispatch_semaphore_signal(semaphore);
}];
#endif
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (permissions & ORKPermissionCoreMotionAccelerometer) {
_grantedPermissions |= ORKPermissionCoreMotionAccelerometer;
}
if (permissions & ORKPermissionCoreMotionActivity) {
dispatch_async(dispatch_get_main_queue(), ^{
ORK_Log_Debug("Requesting pedometer access");
[self requestPedometerAccessWithHandler:^(BOOL success) {
if (success) {
_grantedPermissions |= ORKPermissionCoreMotionActivity;
} else {
_grantedPermissions &= ~ORKPermissionCoreMotionActivity;
}
dispatch_semaphore_signal(semaphore);
}];
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
if (permissions & ORKPermissionAudioRecording) {
dispatch_async(dispatch_get_main_queue(), ^{
ORK_Log_Debug("Requesting audio access");
[self requestAudioRecordingAccessWithHandler:^(BOOL success) {
[self handleResponseFromAudioRequest: success];
dispatch_semaphore_signal(semaphore);
}];
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
#if ORK_FEATURE_CLLOCATIONMANAGER_AUTHORIZATION
if (permissions & ORKPermissionCoreLocation) {
dispatch_async(dispatch_get_main_queue(), ^{
ORK_Log_Debug("Requesting location access");
[self requestLocationAccessWithHandler:^(BOOL success) {
if (success) {
_grantedPermissions |= ORKPermissionCoreLocation;
} else {
_grantedPermissions &= ~ORKPermissionCoreLocation;
}
dispatch_semaphore_signal(semaphore);
}];
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
#endif
if (permissions & ORKPermissionCamera) {
dispatch_async(dispatch_get_main_queue(), ^{
ORK_Log_Debug("Requesting camera access");
[self requestCameraAccessWithHandler:^(BOOL success) {
if (success) {
_grantedPermissions |= ORKPermissionCamera;
} else {
_grantedPermissions &= ~ORKPermissionCamera;
}
dispatch_semaphore_signal(semaphore);
}];
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
_hasRequestedHealthData = YES;
dispatch_async(dispatch_get_main_queue(), ^{
_hasRequestedHealthData = YES;
if (completion) completion();
});
});
}
- (void)handleResponseFromAudioRequest:(BOOL)success {
if (success) {
_grantedPermissions |= ORKPermissionAudioRecording;
} else {
_grantedPermissions &= ~ORKPermissionAudioRecording;
}
}
- (void)startAudioPromptSessionIfNeeded {
id<ORKTask> task = self.task;
if ([task isKindOfClass:[ORKOrderedTask class]]) {
if ([(ORKOrderedTask *)task providesBackgroundAudioPrompts]) {
NSError *error = nil;
if (![self startAudioPromptSessionWithError:&error]) {
// User-visible console log message
ORK_Log_Error("Failed to start audio prompt session: %@", error);
}
}
}
}
- (BOOL)startAudioPromptSessionWithError:(NSError **)errorOut {
NSError *error = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
BOOL success = YES;
// Use PlayAndRecord to avoid overwriting the category being used by
// recording configurations.
if (![session setCategory:AVAudioSessionCategoryPlayback
withOptions:0
error:&error]) {
success = NO;
ORK_Log_Error("Could not start audio session: %@", error);
}
// We are setting the session active so that we can stay live to play audio
// in the background.
if (success && ![session setActive:YES withOptions:0 error:&error]) {
success = NO;
ORK_Log_Error("Could not set audio session active: %@", error);
}
if (errorOut != NULL) {
*errorOut = error;
}
_hasAudioSession = _hasAudioSession || success;
if (_hasAudioSession) {
ORK_Log_Debug("*** Started audio session");
}
return success;
}
- (void)finishAudioPromptSession {
if (_hasAudioSession) {
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error = nil;
if (![session setActive:NO withOptions:0 error:&error]) {
ORK_Log_Error("Could not deactivate audio session: %@", error);
} else {
ORK_Log_Debug("*** Finished audio session");
}
}
}
- (NSSet<HKObjectType *> *)requestedHealthTypesForRead {
return _requestedHealthTypesForRead;
}
- (NSSet<HKObjectType *> *)requestedHealthTypesForWrite {
return _requestedHealthTypesForWrite;
}
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:CGRectZero];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setUpChildNavigationController];
if (_restoredStepIdentifier) {
[self applicationFinishedRestoringState];
}
[self setNavigationBarColor:[UIColor systemGroupedBackgroundColor]];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (!_task) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Attempted to present task view controller without a task" userInfo:nil];
}
if (!_hasBeenPresented) {
// Add first step viewController
ORKStep *step = [self nextStep];
if ([self shouldPresentStep:step]) {
if (![step isKindOfClass:[ORKInstructionStep class]]) {
[self startAudioPromptSessionIfNeeded];
[self requestHealthAuthorizationWithCompletion:nil];
}
ORKStepViewController *firstViewController = [self viewControllerForStep:step];
[self showStepViewController:firstViewController goForward:YES animated:NO];
}
_hasBeenPresented = YES;
}
// Record TaskVC's start time.
// TaskVC is one time use only, no need to update _startDate later.
if (!_presentedDate) {
_presentedDate = [NSDate date];
}
// Clear endDate if current TaskVC got presented again
_dismissedDate = nil;
if ([self isSafeToSkipConfirmation] == NO) {
self.modalInPresentation = YES;
}
if (_taskReviewViewController) {
[_childNavigationController setViewControllers:@[_taskReviewViewController] animated:NO];
[self setTaskReviewViewControllerNavbar];
}
if (_currentStepViewController) {
[self setUpProgressLabelForStepViewController:_currentStepViewController];
}
self.presentationController.delegate = self;
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Set endDate on TaskVC is dismissed,
// because nextResponder is not nil when current TaskVC is covered by another modal view
if (self.nextResponder == nil) {
_dismissedDate = [NSDate date];
}
}
- (NSArray *)managedResultsArray {
NSMutableArray *results = [NSMutableArray new];
[_managedStepIdentifiers enumerateObjectsUsingBlock:^(NSString *identifier, NSUInteger idx, BOOL *stop) {
id <NSCopying> key = identifier;
ORKResult *result = _managedResults[key];
NSAssert2(result, @"Result should not be nil for identifier %@ with key %@", identifier, key);
[results addObject:result];
}];
return [results copy];
}
- (void)setManagedResult:(ORKStepResult *)result forKey:(NSString *)aKey {
if (aKey == nil) {
return;
}
if (result == nil || ![result isKindOfClass:[ORKStepResult class]]) {
@throw [NSException exceptionWithName:NSGenericException reason:[NSString stringWithFormat: @"Expect result object to be `ORKStepResult` type and not nil: {%@ : %@}", aKey, result] userInfo:nil];
return;
}
// Manage last result tracking (used in predicate navigation)
// If the previous result and the replacement result are the same result then `isPreviousResult`
// will be set to `NO` otherwise it will be marked with `YES`.
ORKStepResult *previousResult = _managedResults[aKey];
previousResult.isPreviousResult = YES;
result.isPreviousResult = NO;
if (_managedResults == nil) {
_managedResults = [NSMutableDictionary new];
}
_managedResults[aKey] = result;
}
- (ORKTaskResult *)result {
ORKTaskResult *result = [self _resultIncludingUpdatedCurrentStepViewControllerResult:YES];
return result;
}
- (ORKTaskResult *)_resultIncludingUpdatedCurrentStepViewControllerResult:(BOOL)shouldIncludeUpdatedCurrentStepViewControllerResult {
// TODO: update current implementation.
// setManagedResult for currentStepViewController should not be called every single time this method is called.
ORKTaskResult *result = [[ORKTaskResult alloc] initWithTaskIdentifier:[self.task identifier] taskRunUUID:self.taskRunUUID outputDirectory:self.outputDirectory];
result.startDate = _presentedDate ? : [NSDate date];
result.endDate = _dismissedDate ? : [NSDate date];
if (shouldIncludeUpdatedCurrentStepViewControllerResult) {
[self setManagedResult:[self.currentStepViewController result] forKey:self.currentStepViewController.step.identifier];
result.results = [self managedResultsArray];
} else {
// we may have saved results from the currentStepViewController, but we don't want to include stale results either,
// so go through and remove that result from our local copy before returning
NSString *targetIdentifier = self.currentStepViewController.step.identifier;
NSMutableArray *mutableResultsArray = [[self managedResultsArray] mutableCopy];
NSUInteger index = [mutableResultsArray indexOfObjectPassingTest:^BOOL(ORKResult *eachResult, NSUInteger idx, BOOL * _Nonnull stop) {
if ([eachResult.identifier isEqualToString:targetIdentifier]) {
*stop = YES;
return YES;
}
return NO;
}];
if (index != NSNotFound) {
[mutableResultsArray removeObjectAtIndex:index];
}
result.results = [mutableResultsArray copy];
}
return result;
}
- (NSData *)restorationData {
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
[self encodeRestorableStateWithCoder:archiver];
[archiver finishEncoding];
return [archiver encodedData];
}
- (void)ensureDirectoryExists:(NSURL *)outputDirectory {
// Only verify existence if the output directory is non-nil.
// But, even if the output directory is nil, we still set it and forward to the step VC.
if (outputDirectory != nil) {
BOOL isDirectory = NO;
BOOL directoryExists = [[NSFileManager defaultManager] fileExistsAtPath:outputDirectory.path isDirectory:&isDirectory];
if (!directoryExists) {
NSError *error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtURL:outputDirectory withIntermediateDirectories:YES attributes:nil error:&error]) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Could not create output directory and output directory does not exist" userInfo:@{@"error": error}];
}
isDirectory = YES;
} else if (!isDirectory) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Desired outputDirectory is not a directory or could not be created." userInfo:nil];
}
}
}
- (void)setOutputDirectory:(NSURL *)outputDirectory {
if (_hasBeenPresented) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Cannot change outputDirectory after presenting task controller" userInfo:nil];
}
[self ensureDirectoryExists:outputDirectory];
_outputDirectory = [outputDirectory copy];
[[self currentStepViewController] setOutputDirectory:_outputDirectory];
}
- (void)setRegisteredScrollView:(UIScrollView *)registeredScrollView {
if (_registeredScrollView != registeredScrollView) {
_registeredScrollView = registeredScrollView;
// Stop old observer
_scrollViewObserver = nil;
}
}
- (void)learnMoreButtonPressedWithStep:(ORKLearnMoreInstructionStep *)learnMoreInstructionStep fromStepViewController:(nonnull ORKStepViewController *)stepViewController {
if ([self.delegate respondsToSelector:@selector(taskViewController:learnMoreButtonPressedWithStep:forStepViewController:)]) {
[self.delegate taskViewController:self learnMoreButtonPressedWithStep:learnMoreInstructionStep forStepViewController:stepViewController];
}
}
- (void)goForward {
[_currentStepViewController goForward];
}
- (void)goBackward {
[_childNavigationController popViewControllerAnimated:YES];
}
// Note that this method respects logic related to Review mode, Task termination,
// earlyTerminationConfiguration, and the -shouldPresentStep: check implemented in
// -flipToNextPageFrom:animated:
- (void)goToStepWithIdentifier:(NSString *)identifier {
if ([self.task stepWithIdentifier:_forcedNextStepIdentifier] == nil) {
ORK_Log_Info("goToStepWithIdentifier: step for %@ identifier not found, ignoring navigation", _forcedNextStepIdentifier);
return;
}
_forcedNextStepIdentifier = [identifier copy];
[self goForward];
_forcedNextStepIdentifier = nil;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
UIInterfaceOrientationMask supportedOrientations;
if (self.currentStepViewController) {
supportedOrientations = self.currentStepViewController.supportedInterfaceOrientations;
} else {
supportedOrientations = [self.nextStep makeViewControllerWithResult:nil].supportedInterfaceOrientations;
}
return supportedOrientations;
}
#pragma mark - internal helpers
- (void)updateLastBeginningInstructionStepIdentifierForStep:(ORKStep *)step
goForward:(BOOL)goForward {
if (NO == goForward) {
// Going backward, check current step to nil saved state
if (_lastBeginningInstructionStepIdentifier != nil &&
[_currentStepViewController.step.identifier isEqualToString:_lastBeginningInstructionStepIdentifier]) {
_lastBeginningInstructionStepIdentifier = nil;
}
// Don't return here, because the *next* step might NOT be an instruction step
// the next time we look.
}
ORKStep *nextStep = [self.task stepAfterStep:step withResult:[self result]];
BOOL isNextStepInstructionStep = [nextStep isKindOfClass:[ORKInstructionStep class]];
if (_lastBeginningInstructionStepIdentifier == nil &&
nextStep && NO == isNextStepInstructionStep) {
_lastBeginningInstructionStepIdentifier = step.identifier;
}
}
- (BOOL)isStepLastBeginningInstructionStep:(ORKStep *)step {
if (!step) {
return NO;
}
return (_lastBeginningInstructionStepIdentifier != nil &&
[step isKindOfClass:[ORKInstructionStep class]]&&
[step.identifier isEqualToString:_lastBeginningInstructionStepIdentifier]);
}
- (BOOL)grantedAtLeastOnePermission {
// Return YES, if no desired permission or granted at least one permission.
ORKPermissionMask desiredMask = [self desiredPermissions];
return (desiredMask == 0 || ((desiredMask & _grantedPermissions) != 0));
}
- (BOOL)handlePermissionRequestsDeniedForStep:(ORKStep *)step error:(NSError **)outError {
if (outError != nil) {
*outError = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSUserCancelledError
userInfo:@{@"reason": @"Required permissions not granted."}];
}
return NO;
}
- (void)showStepViewController:(ORKStepViewController *)stepViewController goForward:(BOOL)goForward animated:(BOOL)animated {
if (nil == stepViewController) {
return;
}
ORKStep *step = stepViewController.step;
[self updateLastBeginningInstructionStepIdentifierForStep:step goForward:goForward];
ORKStepViewController *fromController = self.currentStepViewController;
if (fromController && animated && [self isStepLastBeginningInstructionStep:fromController.step]) {
[self startAudioPromptSessionIfNeeded];
if ( [self grantedAtLeastOnePermission] == NO) {
// Do the health request and THEN proceed.
[self requestHealthAuthorizationWithCompletion:^{
// If we are able to collect any data, proceed.
// An alternative rule would be to never proceed if any permission fails.
// However, since iOS does not re-present requests for access, we
// can easily fail even if the user does not see a dialog, which would
// be highly unexpected.
BOOL canRetryShowStepViewController = YES;
// if we were granted at least one permission, it's worth trying again to show the step
canRetryShowStepViewController = canRetryShowStepViewController && [self grantedAtLeastOnePermission];
// even if we weren't granted permission, maybe step doesn't require it or the task can handle it some other way?
NSError *handlerErrorOrNil = nil;
if (canRetryShowStepViewController == NO) {
BOOL handledRequestsDenied = [self handlePermissionRequestsDeniedForStep:fromController.step error:&handlerErrorOrNil];
canRetryShowStepViewController = canRetryShowStepViewController && handledRequestsDenied;
}
if (canRetryShowStepViewController == YES) {
[self showStepViewController:stepViewController goForward:goForward animated:animated];
} else {
[self reportError:handlerErrorOrNil onStep:fromController.step];
}
}];
return;
}
}
if (step.identifier && ![_managedStepIdentifiers.lastObject isEqualToString:step.identifier]) {
[_managedStepIdentifiers addObject:step.identifier];
}
if ([step isRestorable] && !(stepViewController.isBeingReviewed && stepViewController.parentReviewStep.isStandalone)) {
_lastRestorableStepIdentifier = step.identifier;
}
ORKStepViewControllerNavigationDirection stepDirection = goForward ? ORKStepViewControllerNavigationDirectionForward : ORKStepViewControllerNavigationDirectionReverse;
[stepViewController willNavigateDirection:stepDirection];
ORK_Log_Debug("%@ %@", self, stepViewController);
self.registeredScrollView = nil;
// Switch to non-animated transition if the application is not in the foreground.
animated = animated && ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground);
// Update currentStepViewController now, so we don't accept additional transition requests
// from the same VC.
_currentStepViewController = stepViewController;
[self setUpProgressLabelForStepViewController:stepViewController];
NSMutableArray<UIViewController *> *newViewControllers = [NSMutableArray new];
// Add at most two previous step view controllers to support the back action on the navigation controller stack
_previousToTopControllerInNavigationStack = nil;
if (stepViewController.hasPreviousStep) {
ORKStep *previousStep = [self.task stepBeforeStep:step withResult:self.result];
if (previousStep) {
ORKStepViewController *previousStepViewController = [self viewControllerForStep:previousStep isPreviousViewController:YES];
previousStepViewController.navigationItem.title = nil; // Make sure the back button shows "Back"
if (previousStepViewController.hasPreviousStep) {
ORKStep *previousToPreviousStep = [self.task stepBeforeStep:previousStep withResult:self.result];
if (previousToPreviousStep) {
ORKStepViewController *previousToPreviousStepViewController = [self viewControllerForStep:previousToPreviousStep isPreviousViewController:YES];
previousToPreviousStepViewController.navigationItem.title = nil; // Make sure the back button shows "Back"
[newViewControllers addObject:previousToPreviousStepViewController];
}
}
_previousToTopControllerInNavigationStack = previousStepViewController;
[newViewControllers addObject:previousStepViewController];
}
}
ORKStepViewController *lastStepViewController = (ORKStepViewController *)_childNavigationController.viewControllers.lastObject;
if (!goForward && [stepViewController.step.identifier isEqual:lastStepViewController.step.identifier]) {
stepViewController = lastStepViewController;
_currentStepViewController = lastStepViewController;
}
[newViewControllers addObject:stepViewController];
if ([newViewControllers isEqual:_childNavigationController.viewControllers] == NO) {
[_childNavigationController setViewControllers:newViewControllers animated:animated];
}
}
- (BOOL)shouldPresentStep:(ORKStep *)step {
BOOL shouldPresent = (step != nil);
if (shouldPresent && [self.delegate respondsToSelector:@selector(taskViewController:shouldPresentStep:)]) {
shouldPresent = [self.delegate taskViewController:self shouldPresentStep:step];
}
return shouldPresent;
}
- (ORKStep *)nextStep {
ORKStep *step = nil;
if (_forcedNextStepIdentifier != nil) {
step = [self.task stepWithIdentifier:_forcedNextStepIdentifier];
}
if (step == nil && [self.task respondsToSelector:@selector(stepAfterStep:withResult:)]) {
step = [self.task stepAfterStep:self.currentStepViewController.step withResult:[self result]];
}
return step;
}
- (ORKStep *)prevStep {
ORKStep *step = nil;
if ([self.task respondsToSelector:@selector(stepBeforeStep:withResult:)]) {
step = [self.task stepBeforeStep:self.currentStepViewController.step withResult:[self result]];
}
return step;
}
- (NSArray<ORKStep *> *)stepsForReviewStep:(ORKReviewStep *)reviewStep {
NSMutableArray<ORKStep *> *steps = [[NSMutableArray<ORKStep *> alloc] init];
if (reviewStep.isStandalone) {
steps = nil;
} else {
ORKWeakTypeOf(self) weakSelf = self;
[_managedStepIdentifiers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
ORKStrongTypeOf(self) strongSelf = weakSelf;
ORKStep *nextStep = [strongSelf.task stepWithIdentifier:(NSString*) obj];
if (nextStep && ![nextStep.identifier isEqualToString:reviewStep.identifier]) {
[steps addObject:nextStep];
} else {
*stop = YES;
}
}];
}
return [steps copy];
}
-(ORKLearnMoreStepViewController *)learnMoreViewControllerForStep:(ORKLearnMoreInstructionStep *)step {
if (step == nil) {
return nil;
}
ORKLearnMoreStepViewController *learnMoreViewController = nil;
if ([self.delegate respondsToSelector:@selector(taskViewController:learnMoreViewControllerForStep:)]) {
learnMoreViewController = [self.delegate taskViewController:self learnMoreViewControllerForStep:step];
}
if (!learnMoreViewController) {
learnMoreViewController = [[ORKLearnMoreStepViewController alloc] initWithStep:step];
}
learnMoreViewController.view.tintColor = ORKViewTintColor(self.view);
return learnMoreViewController;
}
- (ORKStepViewController *)viewControllerForStep:(ORKStep *)step isPreviousViewController:(BOOL)isPreviousViewController {
if (step == nil) {
return nil;
}
ORKStepViewController *stepViewController = nil;
UIColor *tintColor = ORKViewTintColor(self.view);
if ([self.delegate respondsToSelector:@selector(taskViewController:viewControllerForStep:)]) {
// NOTE: While the delegate does not have direct access to the defaultResultSource,
// it is assumed that it can set results as needed on the custom implementation of an
// ORKStepViewController that it returns.
stepViewController = [self.delegate taskViewController:self viewControllerForStep:step];
}
// If the delegate did not return a step view controller then instantiate one
if (!stepViewController) {
// Special-case the ORKReviewStep
if ([step isKindOfClass:[ORKReviewStep class]]) {
ORKReviewStep *reviewStep = (ORKReviewStep *)step;
NSArray *steps = [self stepsForReviewStep:reviewStep];
id<ORKTaskResultSource> resultSource = reviewStep.isStandalone ? reviewStep.resultSource : self.result;
stepViewController = [[ORKReviewStepViewController alloc] initWithReviewStep:(ORKReviewStep *) step steps:steps resultSource:resultSource];
ORKReviewStepViewController *reviewStepViewController = (ORKReviewStepViewController *) stepViewController;
reviewStepViewController.view.tintColor = tintColor;
reviewStepViewController.reviewDelegate = self;
}
else {
// Get the step result associated with this step
ORKStepResult *result = nil;
ORKStepResult *previousResult = _managedResults[step.identifier];
// Check the default source first
BOOL alwaysCheckForDefaultResult = ([self.defaultResultSource respondsToSelector:@selector(alwaysCheckForDefaultResult)] &&
[self.defaultResultSource alwaysCheckForDefaultResult]);
if ((previousResult == nil) || alwaysCheckForDefaultResult) {
result = [self.defaultResultSource stepResultForStepIdentifier:step.identifier];
}
// If nil, assign to the previous result (if available) otherwise create new instance
if (!result) {
result = previousResult ? : [[ORKStepResult alloc] initWithIdentifier:step.identifier];
}
stepViewController = [step makeViewControllerWithResult:result];
}
}
// Throw an exception if the created step view controller is not a subclass of ORKStepViewController
ORKThrowInvalidArgumentExceptionIfNil(stepViewController);
if (![stepViewController isKindOfClass:[ORKStepViewController class]]) {
@throw [NSException exceptionWithName:NSGenericException reason:[NSString stringWithFormat:@"View controller should be of class %@", [ORKStepViewController class]] userInfo:@{@"viewController": stepViewController}];
}
// If this is a restorable task view controller, check that the restoration identifier and class
// are set on the step result. If not, do so here. This gives the instantiator the opportunity to
// set this value, but ensures that it is set to the default if the instantiator does not do so.
if ([self.delegate respondsToSelector:@selector(taskViewControllerSupportsSaveAndRestore:)] &&
[self.delegate taskViewControllerSupportsSaveAndRestore:self]){
if (stepViewController.restorationIdentifier == nil) {
stepViewController.restorationIdentifier = step.identifier;
}
if (stepViewController.restorationClass == nil) {
stepViewController.restorationClass = [stepViewController class];
}
}
stepViewController.outputDirectory = self.outputDirectory;
stepViewController.delegate = self;
if (!isPreviousViewController) {
[self setManagedResult:stepViewController.result forKey:step.identifier];
}
if (stepViewController.cancelButtonItem == nil) {
stepViewController.cancelButtonItem = [self defaultCancelButtonItem];
}
if ([self.delegate respondsToSelector:@selector(taskViewController:hasLearnMoreForStep:)] &&
[self.delegate taskViewController:self hasLearnMoreForStep:step]) {
stepViewController.learnMoreButtonItem = [self defaultLearnMoreButtonItem];
}
return stepViewController;
}
- (nullable ORKResult *)getCurrentStepResult:(ORKStep *)step {
ORKResult *result = [self.result resultForIdentifier:step.identifier];
if (result) {
return result;
}
ORKStepResult *previousResult = _managedResults[step.identifier];
// Check the default source first
BOOL alwaysCheckForDefaultResult = ([self.defaultResultSource respondsToSelector:@selector(alwaysCheckForDefaultResult)] &&
[self.defaultResultSource alwaysCheckForDefaultResult]);
if ((previousResult == nil) || alwaysCheckForDefaultResult) {
result = [self.defaultResultSource stepResultForStepIdentifier:step.identifier];
}
// If nil, assign to the previous result (if available) otherwise create new instance
if (!result) {
result = previousResult ? : [[ORKStepResult alloc] initWithIdentifier:step.identifier];
}
return result;
}
- (ORKStepViewController *)viewControllerForStep:(ORKStep *)step {
return [self viewControllerForStep:step isPreviousViewController:NO];
}
- (BOOL)shouldDisplayProgressLabelWithStepViewController:(ORKStepViewController *)stepViewController {
return self.showsProgressInNavigationBar && [_task respondsToSelector:@selector(progressOfCurrentStep:withResult:)] && stepViewController.step.showsProgress && !(stepViewController.parentReviewStep.isStandalone);
}
- (void)setUpProgressLabelForStepViewController:(ORKStepViewController *)stepViewController {
NSString *progressLabel = nil;
if ([self shouldDisplayProgressLabelWithStepViewController:stepViewController]) {
ORKTaskProgress progress = [_task progressOfCurrentStep:stepViewController.step withResult:[self result]];
if (progress.shouldBePresented) {
progressLabel = [NSString localizedStringWithFormat:ORKLocalizedString(@"STEP_PROGRESS_FORMAT", nil), ORKLocalizedStringFromNumber(@(progress.current+1)), ORKLocalizedStringFromNumber(@(progress.total))];
}
}
stepViewController.navigationItem.title = progressLabel;
}
#pragma mark - internal action Handlers
- (void)finishWithReason:(ORKTaskFinishReason)reason error:(nullable NSError *)error {
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(taskViewController:didFinishWithReason:error:)]) {
[strongDelegate taskViewController:self didFinishWithReason:reason error:error];
}
[self didFinishWithReason:reason error:error];
}
// For subclasses
- (void)didFinishWithReason:(ORKTaskFinishReason)reason error:(nullable NSError *)error {
// no-op
}
- (void)showRestorationStateAlertControllerWithSender:(id)sender
saveable:(BOOL)saveable
supportSaving:(BOOL)supportSaving {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
if ([sender isKindOfClass:[ORKBorderedButton class]]) {
UIView *cancelButtonView = (UIView *)sender;
alert.popoverPresentationController.sourceView = cancelButtonView;
alert.popoverPresentationController.sourceRect = CGRectMake(CGRectGetMidX(cancelButtonView.bounds), CGRectGetMidY(cancelButtonView.bounds),0,0);
}
else if ([sender isKindOfClass:[UIBarButtonItem class]]) {
alert.popoverPresentationController.barButtonItem = sender;
}
NSString *discardTitle = saveable ? ORKLocalizedString(@"BUTTON_OPTION_DISCARD", nil) : ORKLocalizedString(@"BUTTON_OPTION_STOP_TASK", nil);
if (supportSaving && saveable) {
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_OPTION_SAVE", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
dispatch_async(dispatch_get_main_queue(), ^{
[self finishWithReason:ORKTaskFinishReasonSaved error:nil];
});
}]];
}
[alert addAction:[UIAlertAction actionWithTitle:discardTitle
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
dispatch_async(dispatch_get_main_queue(), ^{
[self finishWithReason:ORKTaskFinishReasonDiscarded error:nil];
});
}]];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_CANCEL", nil)
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)presentCancelOptionsWithSender:(id)sender {
BOOL saveable = [self hasSaveableResults];
if ([self.delegate respondsToSelector:@selector(taskViewControllerShouldConfirmCancel:)] &&
![self.delegate taskViewControllerShouldConfirmCancel:self]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self finishWithReason:ORKTaskFinishReasonDiscarded error:nil];
});
return;
}
BOOL supportSaving = NO;
if ([self.delegate respondsToSelector:@selector(taskViewControllerSupportsSaveAndRestore:)]) {
supportSaving = [self.delegate taskViewControllerSupportsSaveAndRestore:self];
}
if (_skipSaveResultsConfirmation && supportSaving && saveable) {
[self finishWithReason:ORKTaskFinishReasonSaved error:nil];
} else {
[self showRestorationStateAlertControllerWithSender:self
saveable:saveable
supportSaving:supportSaving];
}
}
- (IBAction)cancelAction:(UIBarButtonItem *)sender {
if (self.discardable) {
[self finishWithReason:ORKTaskFinishReasonDiscarded error:nil];
} else {
[self presentCancelOptionsWithSender:sender];
}
}
/// Compute whether it's safe to discard results because there wouldn't be any user data to lose. When `isSafeToSkipConfirmation` returns
/// YES, we know there's no need to present a confirmation dialog about potential data loss from ending the task.
- (BOOL)isSafeToSkipConfirmation {
BOOL result = NO;
ORKStep *currentStep = self.currentStepViewController.step;
// true if currentStep is NOT capable of contributing results, AND there are no other saveable results
result = result || (([self.currentStepViewController.step isKindOfClass:[ORKInstructionStep class]]) && ([self hasSaveableResults] == NO));
// true if currentStep is a standalone reviewStep. No results will be lost by dismissing.
result = result || (ORKDynamicCast(currentStep, ORKReviewStep).isStandalone);
// true if currentStep is in readOnly mode. Nothing can be lost.
result = result || self.currentStepViewController.readOnlyMode;
return result;
}
- (BOOL)hasSaveableResults {
/* [self result] would not include any results beyond current step, because managedResultsArray
[self result] depends on only iterates over _managedStepIdentifiers. If the user hits the back button
during the task, the _managedStepIdentifiers is truncated so it ends with the current step. This is even
thought _managedResults still retains results from a step that comes *after* the current one.
So, instead, use _managedResults to check all completed results for isSaveable.
*/
NSArray *results = _managedResults.allValues;
for (ORKStepResult *stepResult in results) {
if ([stepResult isSaveable]) {
return YES;
}
}
return NO;
}
- (IBAction)learnMoreAction:(id)sender {
if (self.delegate && [self.delegate respondsToSelector:@selector(taskViewController:learnMoreForStep:)]) {
[self.delegate taskViewController:self learnMoreForStep:self.currentStepViewController];
}
}
- (void)reportError:(NSError *)error onStep:(ORKStep *)step {
[self finishWithReason:ORKTaskFinishReasonFailed error:error];
}
- (void)flipToNextPageFrom:(ORKStepViewController *)fromController animated:(BOOL)animated {
if (fromController != _currentStepViewController) {
return;
}
ORKStep *step = fromController.parentReviewStep;
BOOL isEarlyTermination = fromController.wasSkipped == YES && fromController.step.earlyTerminationConfiguration != nil;
if (!step && isEarlyTermination == YES) {
step = fromController.step.earlyTerminationConfiguration.earlyTerminationStep;
}
if (!step) {
step = [self nextStep];
}
if (step == nil) {
if ([self.delegate respondsToSelector:@selector(taskViewController:didChangeResult:)]) {
[self.delegate taskViewController:self didChangeResult:[self result]];
}
[self finishAudioPromptSession];
if (self.reviewMode == ORKTaskViewControllerReviewModeStandalone) {
[_taskReviewViewController removeFromParentViewController];
_taskReviewViewController = nil;
if ([self.task isKindOfClass:[ORKOrderedTask class]]) {
ORKOrderedTask *orderedTask = (ORKOrderedTask *)self.task;
if (!_taskReviewViewController) {
_taskReviewViewController = [[ORKTaskReviewViewController alloc] initWithResultSource:self.result forSteps:orderedTask.steps withContentFrom:_reviewInstructionStep];
_taskReviewViewController.delegate = self;
[_childNavigationController setViewControllers:@[_taskReviewViewController] animated:YES];
[self setTaskReviewViewControllerNavbar];
}
}
}
else {
if (fromController.isEarlyTerminationStep == YES) {
[self finishWithReason:ORKTaskFinishReasonEarlyTermination error:nil];
} else {
[self finishWithReason:ORKTaskFinishReasonCompleted error:nil];
}
}
} else if ([self shouldPresentStep:step]) {
ORKStepViewController *stepViewController = [self viewControllerForStep:step];
NSAssert(stepViewController != nil, @"A non-nil step should always generate a step view controller");
if (fromController.isBeingReviewed) {
[_managedStepIdentifiers removeLastObject];
}
stepViewController.isEarlyTerminationStep = (isEarlyTermination == YES);
[self showStepViewController:stepViewController goForward:YES animated:animated];
}
}
- (void)setTaskReviewViewControllerNavbar {
if (_taskReviewViewController && _taskReviewViewController.navigationController) {
_taskReviewViewController.navigationController.navigationBar.topItem.title = @"";
[_taskReviewViewController.navigationController.navigationBar setBackgroundColor:ORKColor(ORKBackgroundColorKey)];
}
}
- (void)restartTask {
ORKStep *firstStep = [_task stepAfterStep:nil withResult:[self result]];
if (firstStep) {
[self.managedStepIdentifiers removeAllObjects];
[self.managedResults removeAllObjects];
self.restoredStepIdentifier = nil;
[self showStepViewController:[self viewControllerForStep:firstStep] goForward:YES animated:NO];
}
}
- (void)flipToFirstPage {
ORKStep *firstStep = [_task stepAfterStep:nil withResult:[self result]];
if (firstStep) {
[self.managedStepIdentifiers removeAllObjects];
[self showStepViewController:[self viewControllerForStep:firstStep] goForward:NO animated:NO];
}
}
- (void)flipToLastPage {
ORKStep *initialCurrentStep = _currentStepViewController.step;
ORKStep *lastStep = nil;
ORKStep *nextStep = _currentStepViewController.step;
do {
lastStep = nextStep;
nextStep = [_task stepAfterStep:lastStep withResult:[self result]];
} while (nextStep != nil);
if (lastStep != initialCurrentStep) {
[self showStepViewController:[self viewControllerForStep:lastStep] goForward:YES animated:YES];
}
}
- (void)flipToPreviousPageFrom:(ORKStepViewController *)fromController animated:(BOOL)animated {
if (fromController != _currentStepViewController) {
return;
}
ORKStep *step = fromController.parentReviewStep;
if (!step) {
step = [self prevStep];
}
ORKStepViewController *stepViewController = nil;
if ([self shouldPresentStep:step]) {
ORKStep *currentStep = _currentStepViewController.step;
NSString *itemId = currentStep.identifier;
stepViewController = [self viewControllerForStep:step];
if (stepViewController) {
// Remove the identifier from the list
assert([itemId isEqualToString:_managedStepIdentifiers.lastObject]);
[_managedStepIdentifiers removeLastObject];
[self showStepViewController:stepViewController goForward:NO animated:animated];
}
}
}
// Note that this method skips logic related to Review mode, Task termination,
// earlyTerminationConfiguration, and the -shouldPresentStep: check implemented in
// -flipToNextPageFrom:animated: and -flipToPreviousPageFrom:animated:
- (void)flipToPageWithIdentifier:(NSString *)identifier forward:(BOOL)forward animated:(BOOL)animated
{
ORKStep *step = [self.task stepWithIdentifier:identifier];
if (step) {
[self showStepViewController:[self viewControllerForStep:step] goForward:forward animated:animated];
}
}
#pragma mark - ORKStepViewControllerDelegate
- (void)stepViewControllerWillAppear:(ORKStepViewController *)viewController {
// waiting until here to update stepViewController.view's tintColor so we don't load the view prematurely
viewController.view.tintColor = ORKViewTintColor(self.view);
if ([self.delegate respondsToSelector:@selector(taskViewController:stepViewControllerWillAppear:)]) {
[self.delegate taskViewController:self stepViewControllerWillAppear:viewController];
}
}
- (void)stepViewController:(ORKStepViewController *)stepViewController didFinishWithNavigationDirection:(ORKStepViewControllerNavigationDirection)direction
animated:(BOOL)animated {
if (!stepViewController.readOnlyMode) {
// Add step result object
[self setManagedResult:[stepViewController result] forKey:stepViewController.step.identifier];
}
// Alert the delegate that the step is finished
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(taskViewController:stepViewControllerWillDisappear:navigationDirection:)]) {
[strongDelegate taskViewController:self stepViewControllerWillDisappear:stepViewController navigationDirection:direction];
}
if (direction == ORKStepViewControllerNavigationDirectionForward) {
[self flipToNextPageFrom:stepViewController animated:animated];
} else {
[self flipToPreviousPageFrom:stepViewController animated:animated];
}
}
- (void)stepViewController:(ORKStepViewController *)stepViewController didFinishWithNavigationDirection:(ORKStepViewControllerNavigationDirection)direction {
[self stepViewController:stepViewController didFinishWithNavigationDirection:direction animated:(direction == ORKStepViewControllerNavigationDirectionForward)];
}
- (void)stepViewControllerDidFail:(ORKStepViewController *)stepViewController withError:(NSError *)error {
[self finishWithReason:ORKTaskFinishReasonFailed error:error];
}
- (void)stepViewControllerResultDidChange:(ORKStepViewController *)stepViewController {
if (!stepViewController.readOnlyMode) {
[self setManagedResult:stepViewController.result forKey:stepViewController.step.identifier];
}
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(taskViewController:didChangeResult:)]) {
[strongDelegate taskViewController:self didChangeResult:[self result]];
}
}
- (BOOL)stepViewControllerHasPreviousStep:(ORKStepViewController *)stepViewController {
ORKStep *thisStep = stepViewController.step;
if (!thisStep) {
return NO;
}
ORKStep *previousStep = stepViewController.parentReviewStep;
if (!previousStep) {
previousStep = [self.task stepBeforeStep:thisStep withResult:self.result];
}
if ([previousStep isKindOfClass:[ORKActiveStep class]] || ([thisStep allowsBackNavigation] == NO)) {
previousStep = nil; // Can't go back to an active step
}
return (previousStep != nil);
}
- (BOOL)stepViewControllerHasNextStep:(ORKStepViewController *)stepViewController {
ORKStep *thisStep = stepViewController.step;
if (!thisStep) {
return NO;
}
ORKStep *nextStep = [self.task stepAfterStep:thisStep withResult:self.result];
return (nextStep != nil);
}
- (void)stepViewController:(ORKStepViewController *)stepViewController recorder:(ORKRecorder *)recorder didFailWithError:(NSError *)error {
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(taskViewController:recorder:didFailWithError:)]) {
[strongDelegate taskViewController:self recorder:recorder didFailWithError:error];
}
}
- (ORKTaskTotalProgress)stepViewControllerTotalProgressInfoForStep:(ORKStepViewController *)stepViewController currentStep:(ORKStep *)currentStep {
ORKTaskTotalProgress progressData = [self.task totalProgressOfCurrentStep:currentStep];
if (self.progressMode != ORKTaskViewControllerProgressModeTotalQuestions) {
progressData.stepShouldShowTotalProgress = NO;
} else {
progressData.stepShouldShowTotalProgress = YES;
}
return progressData;
}
- (nullable ORKTaskResult *)stepViewControllerOngoingResult:(ORKTaskViewController *)taskViewController {
return [self _resultIncludingUpdatedCurrentStepViewControllerResult:NO];
}
#pragma mark - ORKReviewStepViewControllerDelegate
- (void)reviewStepViewController:(ORKReviewStepViewController *)reviewStepViewController
willReviewStep:(ORKStep *)step {
id<ORKTaskResultSource> resultSource = _defaultResultSource;
if (reviewStepViewController.reviewStep && reviewStepViewController.reviewStep.isStandalone) {
_defaultResultSource = reviewStepViewController.reviewStep.resultSource;
}
ORKStepViewController *stepViewController = [self viewControllerForStep:step];
_defaultResultSource = resultSource;
NSAssert(stepViewController != nil, @"A non-nil step should always generate a step view controller");
stepViewController.continueButtonTitle = ORKLocalizedString(@"BUTTON_SAVE", nil);
stepViewController.parentReviewStep = (ORKReviewStep *) reviewStepViewController.step;
[stepViewController enableBackNavigation];
stepViewController.skipButtonTitle = stepViewController.readOnlyMode ? ORKLocalizedString(@"BUTTON_READ_ONLY_MODE", nil) : ORKLocalizedString(@"BUTTON_CLEAR_ANSWER", nil);
if (stepViewController.parentReviewStep.isStandalone) {
stepViewController.navigationItem.title = stepViewController.parentReviewStep.title;
}
[self showStepViewController:stepViewController goForward:YES animated:YES];
}
#pragma mark - UIStateRestoring
static NSString *const _ORKTaskRunUUIDRestoreKey = @"taskRunUUID";
static NSString *const _ORKShowsProgressInNavigationBarRestoreKey = @"showsProgressInNavigationBar";
static NSString *const _ORKDiscardableTaskRestoreKey = @"discardableTask";
static NSString *const _ORKManagedResultsRestoreKey = @"managedResults";
static NSString *const _ORKManagedStepIdentifiersRestoreKey = @"managedStepIdentifiers";
static NSString *const _ORKHasSetProgressLabelRestoreKey = @"hasSetProgressLabel";
static NSString *const _ORKHasRequestedHealthDataRestoreKey = @"hasRequestedHealthData";
static NSString *const _ORKRequestedHealthTypesForReadRestoreKey = @"requestedHealthTypesForRead";
static NSString *const _ORKRequestedHealthTypesForWriteRestoreKey = @"requestedHealthTypesForWrite";
static NSString *const _ORKOutputDirectoryRestoreKey = @"outputDirectory";
static NSString *const _ORKLastBeginningInstructionStepIdentifierKey = @"lastBeginningInstructionStepIdentifier";
static NSString *const _ORKTaskIdentifierRestoreKey = @"taskIdentifier";
static NSString *const _ORKStepIdentifierRestoreKey = @"stepIdentifier";
static NSString *const _ORKPresentedDate = @"presentedDate";
static NSString *const _ORKProgressMode = @"progressMode";
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[super encodeRestorableStateWithCoder:coder];
[coder encodeObject:_taskRunUUID forKey:_ORKTaskRunUUIDRestoreKey];
[coder encodeBool:self.showsProgressInNavigationBar forKey:_ORKShowsProgressInNavigationBarRestoreKey];
[coder encodeBool:self.discardable forKey:_ORKDiscardableTaskRestoreKey];
[coder encodeObject:_managedResults forKey:_ORKManagedResultsRestoreKey];
[coder encodeObject:_managedStepIdentifiers forKey:_ORKManagedStepIdentifiersRestoreKey];
[coder encodeObject:_requestedHealthTypesForRead forKey:_ORKRequestedHealthTypesForReadRestoreKey];
[coder encodeObject:_requestedHealthTypesForWrite forKey:_ORKRequestedHealthTypesForWriteRestoreKey];
[coder encodeObject:_presentedDate forKey:_ORKPresentedDate];
[coder encodeInteger:_progressMode forKey:_ORKProgressMode];
[coder encodeObject:ORKBookmarkDataFromURL(_outputDirectory) forKey:_ORKOutputDirectoryRestoreKey];
[coder encodeObject:_lastBeginningInstructionStepIdentifier forKey:_ORKLastBeginningInstructionStepIdentifierKey];
[coder encodeObject:_task.identifier forKey:_ORKTaskIdentifierRestoreKey];
ORKStep *step = [_currentStepViewController step];
if ([step isRestorable] && !(_currentStepViewController.isBeingReviewed && _currentStepViewController.parentReviewStep.isStandalone)) {
[coder encodeObject:step.identifier forKey:_ORKStepIdentifierRestoreKey];
} else if (_lastRestorableStepIdentifier) {
[coder encodeObject:_lastRestorableStepIdentifier forKey:_ORKStepIdentifierRestoreKey];
}
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
[super decodeRestorableStateWithCoder:coder];
NSUUID *decodedTaskRunUUID = [coder decodeObjectOfClass:[NSUUID class] forKey:_ORKTaskRunUUIDRestoreKey];
if (decodedTaskRunUUID != nil) {
_taskRunUUID = decodedTaskRunUUID;
} else {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Restored task data taskRunUUID was nil"]
userInfo:nil];
}
self.showsProgressInNavigationBar = [coder decodeBoolForKey:_ORKShowsProgressInNavigationBarRestoreKey];
self.discardable = [coder decodeBoolForKey:_ORKDiscardableTaskRestoreKey];
self.progressMode = [coder decodeIntegerForKey:_ORKProgressMode];
_outputDirectory = ORKURLFromBookmarkData([coder decodeObjectOfClass:[NSData class] forKey:_ORKOutputDirectoryRestoreKey]);
[self ensureDirectoryExists:_outputDirectory];
// Must have a task object already provided by this point in the restoration, in order to restore any other state.
if (_task) {
_managedResults = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSMutableDictionary.self, NSString.self, ORKResult.self, ORKStepResult.self]] forKey:_ORKManagedResultsRestoreKey];
_managedStepIdentifiers = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSMutableArray.self, NSString.self]] forKey:_ORKManagedStepIdentifiersRestoreKey];
_restoredTaskIdentifier = [coder decodeObjectOfClass:[NSString class] forKey:_ORKTaskIdentifierRestoreKey];
if (_restoredTaskIdentifier) {
if (![_task.identifier isEqualToString:_restoredTaskIdentifier]) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Restored task identifier %@ does not match task %@ provided",_restoredTaskIdentifier,_task.identifier]
userInfo:nil];
}
}
if ([_task respondsToSelector:@selector(stepWithIdentifier:)]) {
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
_requestedHealthTypesForRead = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSSet.self, HKObjectType.self]] forKey:_ORKRequestedHealthTypesForReadRestoreKey];
_requestedHealthTypesForWrite = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSSet.self, HKObjectType.self]] forKey:_ORKRequestedHealthTypesForWriteRestoreKey];
#else
_requestedHealthTypesForRead = nil;
_requestedHealthTypesForWrite = nil;
#endif
_presentedDate = [coder decodeObjectOfClass:[NSDate class] forKey:_ORKPresentedDate];
_lastBeginningInstructionStepIdentifier = [coder decodeObjectOfClass:[NSString class] forKey:_ORKLastBeginningInstructionStepIdentifierKey];
_restoredStepIdentifier = [coder decodeObjectOfClass:[NSString class] forKey:_ORKStepIdentifierRestoreKey];
} else {
ORK_Log_Info("Not restoring current step of task %@ because it does not implement -stepWithIdentifier:", _task.identifier);
}
}
}
- (void)applicationFinishedRestoringState {
[super applicationFinishedRestoringState];
if (!_task) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Task must be provided to restore task view controller"
userInfo:nil];
}
if (_restoredStepIdentifier) {
ORKStepViewController *stepViewController = _currentStepViewController;
if (stepViewController) {
stepViewController.delegate = self;
if (stepViewController.cancelButtonItem == nil) {
stepViewController.cancelButtonItem = [self defaultCancelButtonItem];
}
if ([self.delegate respondsToSelector:@selector(taskViewController:hasLearnMoreForStep:)] &&
[self.delegate taskViewController:self hasLearnMoreForStep:stepViewController.step]) {
stepViewController.learnMoreButtonItem = [self defaultLearnMoreButtonItem];
}
} else if ([_task respondsToSelector:@selector(stepWithIdentifier:)]) {
stepViewController = [self viewControllerForStep:[_task stepWithIdentifier:_restoredStepIdentifier]];
} else {
stepViewController = [self viewControllerForStep:[_task stepAfterStep:nil withResult:[self result]]];
}
if (stepViewController != nil) {
[self showStepViewController:stepViewController goForward:YES animated:NO];
_hasBeenPresented = YES;
}
}
}
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
if ([identifierComponents.lastObject isEqualToString:_ChildNavigationControllerRestorationKey]) {
UINavigationController *navigationController = [UINavigationController new];
navigationController.restorationIdentifier = identifierComponents.lastObject;
navigationController.restorationClass = self;
return navigationController;
}
ORKTaskViewController *taskViewController = [[ORKTaskViewController alloc] initWithTask:nil taskRunUUID:nil];
taskViewController.restorationIdentifier = identifierComponents.lastObject;
taskViewController.restorationClass = self;
return taskViewController;
}
#pragma mark UINavigationController pass-throughs
- (void)setNavigationBarHidden:(BOOL)navigationBarHidden {
_childNavigationController.navigationBarHidden = navigationBarHidden;
}
- (BOOL)isNavigationBarHidden {
return _childNavigationController.navigationBarHidden;
}
- (void)setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated {
[_childNavigationController setNavigationBarHidden:hidden animated:YES];
}
- (UINavigationBar *)navigationBar {
return _childNavigationController.navigationBar;
}
#pragma mark Review mode
- (void)addStepResultsUntilStepWithIdentifier:(NSString *)stepIdentifier {
ORKTaskResult *taskResult = (ORKTaskResult *)_defaultResultSource;
for (ORKStepResult * stepResult in taskResult.results) {
if (![stepIdentifier isEqualToString: stepResult.identifier]) {
if (![_managedStepIdentifiers containsObject:stepResult.identifier]) {
[_managedStepIdentifiers addObject:stepResult.identifier];
}
_managedResults[stepResult.identifier] = stepResult;
}
else {
break;
}
}
}
- (void)updateResultWithSource:(id<ORKTaskResultSource>)resultSource {
ORKTaskResult *taskResult = (ORKTaskResult *)resultSource;
for (ORKStepResult * stepResult in taskResult.results) {
if (![_managedStepIdentifiers containsObject:stepResult.identifier]) {
[_managedStepIdentifiers addObject:stepResult.identifier];
}
_managedResults[stepResult.identifier] = stepResult;
}
}
- (void)setDefaultResultSource:(id<ORKTaskResultSource>)defaultResultSource {
_defaultResultSource = defaultResultSource;
[self setReviewMode:_reviewMode];
}
- (void)setReviewMode:(ORKTaskViewControllerReviewMode)reviewMode {
if (_hasBeenPresented) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Cannot change review mode after presenting the task controller for now." userInfo:nil];
}
_reviewMode = reviewMode;
[self setupTaskReviewViewController];
}
- (void)setupTaskReviewViewController {
if (_reviewMode == ORKTaskViewControllerReviewModeNever) {
_taskReviewViewController = nil;
return;
}
_taskReviewViewController = nil;
if ([self.task isKindOfClass:[ORKOrderedTask class]]) {
ORKOrderedTask *orderedTask = (ORKOrderedTask *)self.task;
_taskReviewViewController = [[ORKTaskReviewViewController alloc] initWithResultSource:_defaultResultSource forSteps:orderedTask.steps withContentFrom:_reviewInstructionStep];
_taskReviewViewController.delegate = self;
}
}
- (void)setReviewInstructionStep:(ORKInstructionStep *)reviewInstructionStep {
_reviewInstructionStep = reviewInstructionStep;
if (_taskReviewViewController) {
_taskReviewViewController = nil;
}
[self setupTaskReviewViewController];
}
#pragma mark ORKTaskReviewViewControllerDelegate
- (void)doneButtonTappedWithResultSource:(id<ORKTaskResultSource>)resultSource {
[self updateResultWithSource:resultSource];
[self finishWithReason:ORKTaskFinishReasonCompleted error:nil];
}
- (void)editAnswerTappedForStepWithIdentifier:(NSString *)stepIdentifier {
[self addStepResultsUntilStepWithIdentifier:stepIdentifier];
[self showStepViewController:[self viewControllerForStep:[self.task stepWithIdentifier:stepIdentifier]] goForward:YES animated:YES];
}
#pragma mark - UINavigationController delegate
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (viewController == _previousToTopControllerInNavigationStack && [viewController isKindOfClass:[ORKStepViewController class]]) {
// Make sure that the previous step view controller that will shows during an (interactive or non-interactive)
// pop action shows the progress in the navigation bar when appropriate
[self setUpProgressLabelForStepViewController:(ORKStepViewController *)viewController];
}
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (viewController == _previousToTopControllerInNavigationStack && [viewController isKindOfClass:[ORKStepViewController class]]) {
// _childNavigationController has completed either: a non-interactive animated pop transition by tapping on the
// back button; or an interactive animated pop transition by completing a drag-from-the-edge action. Update view
// controller stack and task view controller state.
[_currentStepViewController goBackward];
}
}
#pragma mark - UIAdaptivePresentationControllerDelegate
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
// If dismissed with a swipe, `finishWithReason:error:` should be called with `discarded`
[self finishWithReason:ORKTaskFinishReasonDiscarded error:nil];
}
#pragma mark - UINavigationBarAppearance
- (void)setNavigationBarColor:(UIColor *)color {
UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];
[appearance configureWithOpaqueBackground];
appearance.shadowColor = nil;
appearance.backgroundColor = color;
self.navigationBar.standardAppearance = appearance;
self.navigationBar.scrollEdgeAppearance = appearance;
self.navigationBar.compactAppearance = appearance;
}
@end