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

645 lines
26 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 "ORKStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
#import "ORKCollectionResult.h"
#import "ORKReviewStep_Internal.h"
#import "ORKNavigationContainerView.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKStepContentView.h"
#import "ORKEarlyTerminationConfiguration.h"
#import "UIBarButtonItem+ORKBarButtonItem.h"
static const CGFloat iPadStepTitleLabelPadding = 15.0;
static const CGFloat iPadStepTitleLabelFontSize = 50.0;
@interface ORKStepViewController () {
BOOL _hasBeenPresented;
BOOL _dismissing;
BOOL _presentingAlert;
}
@property (nonatomic, strong,readonly) UIBarButtonItem *flexSpace;
@property (nonatomic, strong,readonly) UIBarButtonItem *fixedSpace;
@end
@implementation ORKStepViewController {
UIView *_iPadBackgroundView;
UIView *_iPadContentView;
UILabel *_iPadStepTitleLabel;
NSArray<NSLayoutConstraint *> *_iPadConstraints;
}
- (void)initializeInternalButtonItems {
_internalContinueButtonItem = [[UIBarButtonItem alloc] initWithTitle:ORKLocalizedString(@"BUTTON_NEXT", nil) style:UIBarButtonItemStylePlain target:self action:@selector(goForward)];
_internalDoneButtonItem = [[UIBarButtonItem alloc] initWithTitle:ORKLocalizedString(@"BUTTON_DONE", nil) style:UIBarButtonItemStyleDone target:self action:@selector(goForward)];
_internalSkipButtonItem = [[UIBarButtonItem alloc] initWithTitle:ORKLocalizedString(@"BUTTON_SKIP", nil) style:UIBarButtonItemStylePlain target:self action:@selector(skip:)];
_backButtonItem = nil;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self initializeInternalButtonItems];
}
return self;
}
#pragma clang diagnostic pop
- (instancetype)initWithStep:(ORKStep *)step {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_wasSkipped = false;
[self initializeInternalButtonItems];
[self setStep:step];
self.restorationIdentifier = step.identifier;
self.restorationClass = self.class;
}
return self;
}
- (instancetype)initWithStep:(ORKStep *)step result:(ORKResult *)result {
// The default implementation ignores the previous result.
return [self initWithStep:step];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = ORKColor(ORKBackgroundColorKey);
if (!_shouldIgnoreiPadDesign && ORKNeedWideScreenDesign(self.view)) {
[self setupiPadBackgroundView];
[self setupiPadContentView];
[self setupiPadStepTitleLabel];
[self setupiPadConstraints];
}
}
- (void)updateBarButtonItems {
if (self.shouldPresentInReview) {
self.navigationItem.leftBarButtonItem = self.cancelButtonItem;
self.navigationItem.rightBarButtonItem = self.internalDoneButtonItem;
} else {
self.navigationItem.leftBarButtonItem = self.backButtonItem;
self.navigationItem.rightBarButtonItem = self.cancelButtonItem;
}
}
- (void)setShouldPresentInReview:(BOOL)shouldPresentInReview {
_shouldPresentInReview = shouldPresentInReview;
[self updateBarButtonItems];
}
- (void)setupiPadBackgroundView {
if (!_iPadBackgroundView) {
_iPadBackgroundView = [UIView new];
}
[self.view addSubview:_iPadBackgroundView];
}
- (void)setupiPadContentView {
if (!_iPadContentView) {
_iPadContentView = [UIView new];
}
[_iPadBackgroundView addSubview:_iPadContentView];
}
- (void)setupiPadStepTitleLabel {
if (!_iPadStepTitleLabel) {
_iPadStepTitleLabel = [UILabel new];
}
_iPadStepTitleLabel.numberOfLines = 0;
_iPadStepTitleLabel.textAlignment = NSTextAlignmentNatural;
[_iPadStepTitleLabel setFont:[UIFont systemFontOfSize:iPadStepTitleLabelFontSize weight:UIFontWeightBold]];
[_iPadStepTitleLabel setAdjustsFontSizeToFitWidth:YES];
[_iPadBackgroundView addSubview:_iPadStepTitleLabel];
}
- (void)setiPadStepTitleLabelText:(NSString *)text {
if (_iPadStepTitleLabel) {
// [_iPadStepTitleLabel setText: text];
}
}
- (void)setiPadBackgroundViewColor:(UIColor *)color {
if (_iPadBackgroundView) {
// [_iPadBackgroundView setBackgroundColor:color];
}
}
- (void)setupiPadConstraints {
_iPadBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
_iPadContentView.translatesAutoresizingMaskIntoConstraints = NO;
_iPadStepTitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
if (_iPadConstraints) {
[NSLayoutConstraint deactivateConstraints:_iPadConstraints];
}
_iPadConstraints = @[
[NSLayoutConstraint constraintWithItem:_iPadBackgroundView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view.safeAreaLayoutGuide
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_iPadBackgroundView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.view.safeAreaLayoutGuide
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:ORKiPadBackgroundViewLeftRightPadding],
[NSLayoutConstraint constraintWithItem:_iPadBackgroundView
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:self.view.safeAreaLayoutGuide
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:-ORKiPadBackgroundViewLeftRightPadding],
[NSLayoutConstraint constraintWithItem:_iPadBackgroundView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view.safeAreaLayoutGuide
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:-ORKiPadBackgroundViewBottomPadding],
[NSLayoutConstraint constraintWithItem:_iPadStepTitleLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_iPadBackgroundView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:ORKiPadBackgroundViewBottomPadding],
[NSLayoutConstraint constraintWithItem:_iPadStepTitleLabel
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:_iPadBackgroundView
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:iPadStepTitleLabelPadding],
[NSLayoutConstraint constraintWithItem:_iPadStepTitleLabel
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:_iPadBackgroundView
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:-iPadStepTitleLabelPadding],
[NSLayoutConstraint constraintWithItem:_iPadContentView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_iPadStepTitleLabel
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:iPadStepTitleLabelPadding],
[NSLayoutConstraint constraintWithItem:_iPadContentView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:_iPadBackgroundView
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:iPadStepTitleLabelPadding],
[NSLayoutConstraint constraintWithItem:_iPadContentView
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:_iPadBackgroundView
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:-iPadStepTitleLabelPadding],
[NSLayoutConstraint constraintWithItem:_iPadContentView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:_iPadBackgroundView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:-iPadStepTitleLabelPadding]
];
[NSLayoutConstraint activateConstraints:_iPadConstraints];
}
- (void)setupButtons {
if (self.hasNextStep == YES) {
self.continueButtonItem = _internalContinueButtonItem;
} else {
self.continueButtonItem = _internalDoneButtonItem;
}
self.skipButtonItem = _internalSkipButtonItem;
}
- (void)setStep:(ORKStep *)step {
if (_hasBeenPresented) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Cannot set step after presenting step view controller" userInfo:nil];
}
if (step && step.identifier == nil) {
ORK_Log_Debug("Step identifier should not be nil.");
}
_step = step;
[step validateParameters];
[self setupButtons];
[self stepDidChange];
}
- (void)stepDidChange {
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
ORK_Log_Debug("%@", self);
// Required here (instead of viewDidLoad) because any custom buttons are set once the delegate responds to the stepViewControllerWillAppear,
// otherwise there is a minor visual glitch, where the original buttons are displayed on the UI for a short period. This is not placed after
// the delegate responds to the stepViewControllerWillAppear, so that the target from the button's item can be used, if the intention is to
// only modify the title of the button.
[self setupButtons];
if ([self.delegate respondsToSelector:@selector(stepViewControllerWillAppear:)]) {
[self.delegate stepViewControllerWillAppear:self];
}
if (!_step) {
@throw [NSException exceptionWithName:NSGenericException reason:@"Cannot present step view controller without a step" userInfo:nil];
}
_hasBeenPresented = YES;
// Set presentedDate on first time viewWillAppear
if (!self.presentedDate) {
self.presentedDate = [NSDate date];
}
// clear dismissedDate
self.dismissedDate = nil;
if (self.step.earlyTerminationConfiguration != nil) {
self.skipButtonTitle = self.step.earlyTerminationConfiguration.buttonText;
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
_dismissing = YES;
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Set endDate if the current ORKStepViewController's view disappears, and is not the topViewController anymore
// That way, if the step view controller is just covered by a modal view, dismissedDate will not be set.
if (self.nextResponder == nil ||
([self.parentViewController isKindOfClass:[UINavigationController class]]
&& ((UINavigationController *)self.parentViewController).topViewController != self)) {
self.dismissedDate = [NSDate date];
}
_dismissing = NO;
}
- (void)willNavigateDirection:(ORKStepViewControllerNavigationDirection)direction {
}
- (void)setContinueButtonTitle:(NSString *)continueButtonTitle {
_continueButtonTitle = continueButtonTitle;
self.internalContinueButtonItem.title = continueButtonTitle;
self.internalDoneButtonItem.title = continueButtonTitle;
self.continueButtonItem = self.internalContinueButtonItem;
}
- (void)showActivityIndicatorInContinueButton:(BOOL)showActivityIndicator {
[_navigationFooterView showActivityIndicator:showActivityIndicator];
}
- (void)setContinueButtonEnabled:(BOOL)continueButtonEnabled {
_navigationFooterView.continueEnabled = continueButtonEnabled;
}
- (void)setSkipButtonEnabled:(BOOL)skipButtonEnabled {
_navigationFooterView.skipEnabled = skipButtonEnabled;
}
- (void)setNavigationDetailText:(NSString *)navigationDetailText {
_navigationFooterView.navigationDetailText = navigationDetailText;
}
- (void)setContinueButtonDisabledStyle:(ORKBorderedButtonDisabledStyle)continueButtonDisabledStyle {
_continueButtonDisabledStyle = continueButtonDisabledStyle;
_navigationFooterView.continueButtonDisabledStyle = continueButtonDisabledStyle;
}
- (void)setLearnMoreButtonTitle:(NSString *)learnMoreButtonTitle {
self.learnMoreButtonItem.title = learnMoreButtonTitle;
self.learnMoreButtonItem = self.learnMoreButtonItem;
}
- (NSString *)learnMoreButtonTitle {
return self.learnMoreButtonItem.title;
}
- (void)setSkipButtonTitle:(NSString *)skipButtonTitle {
self.internalSkipButtonItem.title = skipButtonTitle;
self.skipButtonItem = self.internalSkipButtonItem;
}
- (NSString *)skipButtonTitle {
return self.skipButtonItem.title;
}
- (void)enableBackNavigation {
[self setBackButtonItem:[self goToPreviousPageButtonItem]];
}
- (UIBarButtonItem *)goToPreviousPageButtonItem {
UIBarButtonItem *button = [UIBarButtonItem ork_backBarButtonItemWithTarget:self action:@selector(goBackward)];
button.accessibilityLabel = ORKLocalizedString(@"AX_BUTTON_BACK", nil);
return button;
}
- (void)setBackButtonItem:(UIBarButtonItem *)backButton {
backButton.accessibilityLabel = ORKLocalizedString(@"AX_BUTTON_BACK", nil);
_backButtonItem = backButton;
[self updateBarButtonItems];
}
- (void)setCancelButtonItem:(UIBarButtonItem *)cancelButton {
_cancelButtonItem = cancelButton;
[self updateBarButtonItems];
}
- (BOOL)hasPreviousStep {
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
if (strongDelegate && [strongDelegate respondsToSelector:@selector(stepViewControllerHasPreviousStep:)]) {
return [strongDelegate stepViewControllerHasPreviousStep:self];
}
return NO;
}
- (BOOL)hasNextStep {
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
if (strongDelegate && [strongDelegate respondsToSelector:@selector(stepViewControllerHasNextStep:)]) {
return [strongDelegate stepViewControllerHasNextStep:self];
}
return NO;
}
- (ORKStepResult *)result {
ORKStepResult *stepResult = [[ORKStepResult alloc] initWithStepIdentifier:self.step.identifier results:_addedResults ? : @[]];
stepResult.startDate = self.presentedDate ? : [NSDate date];
stepResult.endDate = self.dismissedDate ? : [NSDate date];
return stepResult;
}
- (void)addResult:(ORKResult *)result {
ORKResult *copy = [result copy];
if (_addedResults == nil) {
_addedResults = @[copy];
} else {
NSUInteger idx = [_addedResults indexOfObject:copy];
if (idx == NSNotFound) {
_addedResults = [_addedResults arrayByAddingObject:copy];
} else {
NSMutableArray *results = [_addedResults mutableCopy];
[results insertObject:copy atIndex:idx];
_addedResults = [results copy];
}
}
}
- (void)notifyDelegateOnResultChange {
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(stepViewControllerResultDidChange:)]) {
[strongDelegate stepViewControllerResultDidChange:self];
}
}
- (BOOL)hasBeenPresented {
return _hasBeenPresented;
}
+ (UIInterfaceOrientationMask)supportedInterfaceOrientations {
// The default values for a view controller'€™s supported interface orientations is set to
// UIInterfaceOrientationMaskAll for the iPad idiom and UIInterfaceOrientationMaskAllButUpsideDown for the iPhone idiom.
UIInterfaceOrientationMask supportedOrientations = UIInterfaceOrientationMaskAllButUpsideDown;
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
supportedOrientations = UIInterfaceOrientationMaskAll;
}
return supportedOrientations;
}
- (BOOL)isBeingReviewed {
return _parentReviewStep != nil;
}
- (BOOL)readOnlyMode {
return self.isBeingReviewed && _parentReviewStep.isStandalone;
}
#pragma mark - Action Handlers
- (void)goForward {
_wasSkipped = false;
[self navigateForward];
}
- (void)navigateForward {
ORKStepViewControllerNavigationDirection direction = self.isBeingReviewed ? ORKStepViewControllerNavigationDirectionReverse : ORKStepViewControllerNavigationDirectionForward;
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
[strongDelegate stepViewController:self didFinishWithNavigationDirection:direction];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
}
- (void)goBackward {
ORKStrongTypeOf(self.delegate) strongDelegate = self.delegate;
[strongDelegate stepViewController:self didFinishWithNavigationDirection:ORKStepViewControllerNavigationDirectionReverse];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
}
- (void)skip:(UIView *)sender {
if (self.isBeingReviewed && !self.readOnlyMode) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_CLEAR_ANSWER", nil)
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
dispatch_async(dispatch_get_main_queue(), ^{
[self skipForward];
});
}]];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_CANCEL", nil)
style:UIAlertActionStyleCancel
handler:nil
]];
alert.popoverPresentationController.sourceView = sender;
alert.popoverPresentationController.sourceRect = sender.bounds;
[self presentViewController:alert animated:YES completion:nil];
} else {
[self skipForward];
}
}
- (void)skipForward {
_wasSkipped = true;
[self navigateForward];
}
- (UIView *)viewForiPadLayoutConstraints {
return _iPadContentView;
}
- (ORKTaskViewController *)taskViewController {
// look to parent view controller for a task view controller
UIViewController *parentViewController = [self parentViewController];
while (parentViewController && ![parentViewController isKindOfClass:[ORKTaskViewController class]]) {
parentViewController = [parentViewController parentViewController];
}
return (ORKTaskViewController *)parentViewController;
}
- (BOOL)showValidityAlertWithMessage:(NSString *)text {
return [self showValidityAlertWithTitle:ORKLocalizedString(@"RANGE_ALERT_TITLE", nil) message:text];
}
- (BOOL)showValidityAlertWithTitle:(NSString *)title message:(NSString *)message {
if (![title length] && ![message length]) {
// No alert if the value is empty
return NO;
}
if (_dismissing || ![self isViewLoaded] || !self.view.window) {
// No alert if not in view chain.
return NO;
}
if (_presentingAlert) {
return NO;
}
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_CANCEL", nil)
style:UIAlertActionStyleDefault
handler:nil]];
_presentingAlert = YES;
[self presentViewController:alert animated:YES completion:^{
_presentingAlert = NO;
}];
return YES;
}
#pragma mark - UIStateRestoring
static NSString *const _ORKStepIdentifierRestoreKey = @"stepIdentifier";
static NSString *const _ORKPresentedDateRestoreKey = @"presentedDate";
static NSString *const _ORKOutputDirectoryKey = @"outputDirectory";
static NSString *const _ORKParentReviewStepKey = @"parentReviewStep";
static NSString *const _ORKAddedResultsKey = @"addedResults";
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[super encodeRestorableStateWithCoder:coder];
[coder encodeObject:_step.identifier forKey:_ORKStepIdentifierRestoreKey];
[coder encodeObject:_presentedDate forKey:_ORKPresentedDateRestoreKey];
[coder encodeObject:ORKBookmarkDataFromURL(_outputDirectory) forKey:_ORKOutputDirectoryKey];
[coder encodeObject:_parentReviewStep forKey:_ORKParentReviewStepKey];
[coder encodeObject:_addedResults forKey:_ORKAddedResultsKey];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
[super decodeRestorableStateWithCoder:coder];
self.outputDirectory = ORKURLFromBookmarkData([coder decodeObjectOfClass:[NSData class] forKey:_ORKOutputDirectoryKey]);
if (!self.step) {
// Just logging to the console in this case, since this can happen during a taskVC restoration of a dynamic task.
// The step VC will get restored, but then never added back to the hierarchy.
ORK_Log_Debug("%@",[NSString stringWithFormat:@"No step provided while restoring %@", NSStringFromClass([self class])]);
}
self.presentedDate = [coder decodeObjectOfClass:[NSDate class] forKey:_ORKPresentedDateRestoreKey];
self.restoredStepIdentifier = [coder decodeObjectOfClass:[NSString class] forKey:_ORKStepIdentifierRestoreKey];
if (self.step && _restoredStepIdentifier && ![self.step.identifier isEqualToString:_restoredStepIdentifier]) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Attempted to restore step with identifier %@ but got step identifier %@", _restoredStepIdentifier, self.step.identifier]
userInfo:nil];
}
self.parentReviewStep = [coder decodeObjectOfClass:[ORKReviewStep class] forKey:_ORKParentReviewStepKey];
_addedResults = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSArray.self, ORKResult.self]] forKey:_ORKAddedResultsKey];
}
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
ORKStepViewController *viewController = [[[self class] alloc] initWithStep:nil];
viewController.restorationIdentifier = identifierComponents.lastObject;
viewController.restorationClass = self;
return viewController;
}
#pragma mark - Accessibility
- (BOOL)accessibilityPerformEscape {
if ([self hasPreviousStep]) {
[self goBackward];
return YES;
}
return NO;
}
@end