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

2434 lines
107 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 "ORKFormStepViewController.h"
#import "ORKCaption1Label.h"
#import "ORKChoiceViewCell_Internal.h"
#import "ORKChoiceViewCell+ORKColorChoice.h"
#import "ORKColorChoiceCellGroup.h"
#import "ORKFormItemCell.h"
#import "ORKFormSectionTitleLabel.h"
#import "ORKStepHeaderView_Internal.h"
#import "ORKTableContainerView.h"
#import "ORKStepContentView.h"
#import "ORKBodyItem.h"
#import "ORKLearnMoreView.h"
#import "ORKBodyItem.h"
#import "ORKLearnMoreStepViewController.h"
#import "ORKSurveyCardHeaderView.h"
#import "ORKTextChoiceCellGroup.h"
#import "ORKAnswerTextView.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
#import "ORKAnswerFormat_Internal.h"
#import "ORKAnswerFormat+FormStepViewControllerAdditions.h"
#import "ORKCollectionResult_Private.h"
#import "ORKQuestionResult_Private.h"
#import "ORKFormItem_Internal.h"
#import "ORKFormStep_Internal.h"
#import "ORKResult_Private.h"
#import "ORKStep_Private.h"
#import "ORKSESSelectionView.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import "ORKChoiceViewCell+ORKTextChoice.h"
#import "ORKAccessibilityFunctions.h"
#import "ORKTagLabel.h"
#import "ORKQuestionStep.h"
#import <Researchkit/ORKFormItemVisibilityRule.h>
static const CGFloat TableViewYOffsetStandard = 30.0;
static const NSTimeInterval DelayBeforeAutoScroll = 0.25;
NSString * const ORKSurveyCardHeaderViewIdentifier = @"SurveyCardHeaderViewIdentifier";
NSString * const ORKFormStepViewAccessibilityIdentifier = @"ORKFormStepView";
@interface ORKFormItem (FormStepViewControllerExtensions)
- (BOOL)requiresSingleSection;
@end
@interface ORKTableCellItemIdentifier : NSObject <NSCopying>
- (instancetype)initWithFormItemIdentifier:(NSString *)formItemIdentifier choiceIndex:(NSInteger)index;
@property (nonatomic, copy, readonly) NSString *formItemIdentifier;
@property (nonatomic, readonly) NSInteger choiceIndex;
@end
@implementation ORKTableCellItemIdentifier
- (instancetype)initWithFormItemIdentifier:(NSString *)formItemIdentifier choiceIndex:(NSInteger)index {
self = [super init];
if (self != nil) {
_formItemIdentifier = [formItemIdentifier copy];
_choiceIndex = index;
}
return self;
}
- (NSUInteger)hash {
return _formItemIdentifier.hash ^ _choiceIndex;
}
- (BOOL)isEqual:(id)object {
if ([self class] != [object class]) {
return NO;
}
__typeof(self) castObject = object;
return (ORKEqualObjects(_formItemIdentifier, castObject->_formItemIdentifier)
&& (_choiceIndex == castObject->_choiceIndex));
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
__typeof(self) copy = [[[self class] alloc] init];
copy->_formItemIdentifier = [_formItemIdentifier copy];
copy->_choiceIndex = _choiceIndex;
return copy;
}
- (NSString *)description {
NSString *indexString = (_choiceIndex == NSNotFound) ? @"NSNotFound" : @(_choiceIndex).stringValue;
return [NSString stringWithFormat:@"[%@ '%@', index: %@]", [super description], _formItemIdentifier, indexString];
}
@end
@interface ORKTableCellItem : NSObject
- (instancetype)initWithFormItem:(ORKFormItem *)formItem;
- (instancetype)initWithFormItem:(ORKFormItem *)formItem choiceIndex:(NSUInteger)index;
@property (nonatomic, copy) ORKFormItem *formItem;
@property (nonatomic, copy) ORKAnswerFormat *answerFormat;
@property (nonatomic, readonly) CGFloat labelWidth;
// For choice types only
@property (nonatomic, copy, readonly) ORKTextChoice *choice;
@end
@implementation ORKTableCellItem
- (instancetype)initWithFormItem:(ORKFormItem *)formItem {
self = [super init];
if (self) {
self.formItem = formItem;
_answerFormat = [[formItem impliedAnswerFormat] copy];
}
return self;
}
- (instancetype)initWithFormItem:(ORKFormItem *)formItem choiceIndex:(NSUInteger)index {
self = [super init];
if (self) {
self.formItem = formItem;
_answerFormat = [[formItem impliedAnswerFormat] copy];
if ([self textChoiceAnswerFormat] != nil) {
_choice = [self.textChoiceAnswerFormat.textChoices[index] copy];
}
}
return self;
}
- (ORKTextChoiceAnswerFormat *)textChoiceAnswerFormat {
if ([self.answerFormat isKindOfClass:[ORKTextChoiceAnswerFormat class]]) {
return (ORKTextChoiceAnswerFormat *)self.answerFormat;
}
return nil;
}
- (CGFloat)labelWidth {
static ORKCaption1Label *sharedLabel;
if (sharedLabel == nil) {
sharedLabel = [ORKCaption1Label new];
}
sharedLabel.text = _formItem.text;
return [sharedLabel textRectForBounds:CGRectInfinite limitedToNumberOfLines:1].size.width;
}
@end
@interface ORKTableSection : NSObject
- (instancetype)initWithSectionIndex:(NSUInteger)index;
@property (nonatomic, assign, readonly) NSUInteger index;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy, nullable) NSString *detailText;
@property (nonatomic) BOOL showsProgress;
@property (nonatomic, nullable) ORKLearnMoreItem *learnMoreItem;
@property (nonatomic, copy, nullable) NSString *tagText;
// ORKTableCellItem
@property (nonatomic, copy, readonly) NSArray *items;
@property (nonatomic, readonly) BOOL hasChoiceRows;
@property (nonatomic, strong) ORKTextChoiceCellGroup *textChoiceCellGroup;
@property (nonatomic, strong) ORKColorChoiceCellGroup *colorChoiceCellGroup;
- (void)addFormItem:(ORKFormItem *)item;
- (BOOL)containsFormItem:(ORKFormItem *)formItem;
@property (nonatomic, readonly) CGFloat maxLabelWidth;
@end
@implementation ORKTableSection
- (instancetype)initWithSectionIndex:(NSUInteger)index {
self = [super init];
if (self) {
_items = [NSMutableArray new];
self.title = nil;
_index = index;
}
return self;
}
- (void)setTitle:(NSString *)title {
_title = title;
}
- (void)addFormItem:(ORKFormItem *)item {
if ([[item impliedAnswerFormat] isKindOfClass:[ORKTextChoiceAnswerFormat class]]) {
_hasChoiceRows = YES;
ORKTextChoiceAnswerFormat *textChoiceAnswerFormat = (ORKTextChoiceAnswerFormat *)[item impliedAnswerFormat];
_textChoiceCellGroup = [[ORKTextChoiceCellGroup alloc] initWithTextChoiceAnswerFormat:textChoiceAnswerFormat
answer:nil
beginningIndexPath:[NSIndexPath indexPathForRow:0 inSection:_index]
immediateNavigation:NO];
[textChoiceAnswerFormat.textChoices enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
ORKTableCellItem *cellItem = [[ORKTableCellItem alloc] initWithFormItem:item choiceIndex:idx];
[(NSMutableArray *)self.items addObject:cellItem];
}];
} else {
if ([[item impliedAnswerFormat] isKindOfClass:[ORKColorChoiceAnswerFormat class]]) {
_hasChoiceRows = YES;
ORKColorChoiceAnswerFormat *colorChoiceAnswerFormat = (ORKColorChoiceAnswerFormat *)[item impliedAnswerFormat];
_colorChoiceCellGroup = [[ORKColorChoiceCellGroup alloc] initWithColorChoiceAnswerFormat:colorChoiceAnswerFormat
answer:nil
beginningIndexPath:[NSIndexPath indexPathForRow:0 inSection:_index]
immediateNavigation:NO];
[colorChoiceAnswerFormat.colorChoices enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
ORKTableCellItem *cellItem = [[ORKTableCellItem alloc] initWithFormItem:item choiceIndex:idx];
[(NSMutableArray *)self.items addObject:cellItem];
}];
return;
}
ORKTableCellItem *cellItem = [[ORKTableCellItem alloc] initWithFormItem:item];
[(NSMutableArray *)self.items addObject:cellItem];
}
}
- (BOOL)containsFormItem:(ORKFormItem *)formItem {
for (ORKTableCellItem *cellItem in _items) {
if (cellItem.formItem.identifier == formItem.identifier) {
return YES;
}
}
return NO;
}
- (CGFloat)maxLabelWidth {
CGFloat max = 0;
for (ORKTableCellItem *item in self.items) {
if (item.labelWidth > max) {
max = item.labelWidth;
}
}
return max;
}
@end
@interface ORKFormSectionHeaderView : UIView
- (instancetype)initWithTitle:(NSString *)title tableView:(UITableView *)tableView firstSection:(BOOL)firstSection;
@property (nonatomic, strong) NSLayoutConstraint *leftMarginConstraint;
@property (nonatomic, weak) UITableView *tableView;
@end
@implementation ORKFormSectionHeaderView {
ORKFormSectionTitleLabel *_label;
BOOL _firstSection;
}
- (instancetype)initWithTitle:(NSString *)title tableView:(UITableView *)tableView firstSection:(BOOL)firstSection {
self = [super init];
if (self) {
_tableView = tableView;
_firstSection = firstSection;
self.backgroundColor = [UIColor whiteColor];
_label = [ORKFormSectionTitleLabel new];
_label.text = title;
_label.numberOfLines = 0;
_label.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_label];
[self setUpConstraints];
}
return self;
}
- (void)setUpConstraints {
const CGFloat LabelFirstBaselineToTop = _firstSection ? 20.0 : 40.0;
const CGFloat LabelLastBaselineToBottom = -10.0;
const CGFloat LabelRightMargin = -4.0;
NSMutableArray *constraints = [NSMutableArray new];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_label
attribute:NSLayoutAttributeFirstBaseline
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:LabelFirstBaselineToTop]];
self.leftMarginConstraint = [NSLayoutConstraint constraintWithItem:_label
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0.0];
[constraints addObject:self.leftMarginConstraint];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_label
attribute:NSLayoutAttributeLastBaseline
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:LabelLastBaselineToBottom]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:_label
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:LabelRightMargin]];
[NSLayoutConstraint activateConstraints:constraints];
}
- (void)updateConstraints {
[super updateConstraints];
self.leftMarginConstraint.constant = _tableView.layoutMargins.left;
}
@end
@interface ORKFormStepViewController () <UITableViewDelegate, ORKFormItemCellDelegate, ORKTableContainerViewDelegate, ORKChoiceOtherViewCellDelegate, ORKLearnMoreViewDelegate>
@property (nonatomic, strong) ORKTableContainerView *tableContainer;
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) UITableViewDiffableDataSource<NSString *, ORKTableCellItemIdentifier *> *diffableDataSource;
@property (nonatomic, strong) ORKStepContentView *headerView;
@property (nonatomic, strong) NSMutableDictionary *savedAnswers;
@property (nonatomic, strong) NSMutableDictionary *savedAnswerDates;
@property (nonatomic, strong) NSMutableDictionary *savedSystemCalendars;
@property (nonatomic, strong) NSMutableDictionary *savedSystemTimeZones;
@property (nonatomic, strong) NSDictionary *originalAnswers;
@property (nonatomic, strong) NSMutableDictionary *savedDefaults;
@end
@implementation ORKFormStepViewController {
ORKAnswerDefaultSource *_defaultSource;
NSMutableSet *_formItemCells;
NSMutableSet<NSString *> *_identifiersOfAnsweredSections;
BOOL _skipped;
BOOL _autoScrollCancelled;
UITableViewCell *_currentFirstResponderCell;
NSArray<NSLayoutConstraint *> *_constraints;
NSInteger _maxLabelWidth;
}
- (instancetype)ORKFormStepViewController_initWithResult:(ORKResult *)result {
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
_defaultSource = [ORKAnswerDefaultSource sourceWithHealthStore:[HKHealthStore new]];
#endif
if (result) {
NSAssert([result isKindOfClass:[ORKStepResult class]], @"Expect a ORKStepResult instance");
NSArray *resultsArray = [(ORKStepResult *)result results];
for (ORKQuestionResult *currentResult in resultsArray) {
id answer = currentResult.answer ? : ORKNullAnswerValue();
[self setAnswer:answer forIdentifier:currentResult.identifier];
}
self.originalAnswers = [[NSDictionary alloc] initWithDictionary:self.savedAnswers];
}
return self;
}
- (instancetype)initWithStep:(ORKStep *)step {
self = [super initWithStep:step];
return [self ORKFormStepViewController_initWithResult:nil];
}
- (instancetype)initWithStep:(ORKStep *)step result:(ORKResult *)result {
self = [super initWithStep:step];
return [self ORKFormStepViewController_initWithResult:result];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self stepDidChange];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
self.view.accessibilityIdentifier = ORKFormStepViewAccessibilityIdentifier;
}
- (BOOL)isContentSizeWithinFrame {
return _tableView.contentSize.height <= _tableView.bounds.size.height;
}
- (BOOL)isContentSizeLargerThanFrame {
BOOL isContentSizeLargerThanBounds = _tableView.contentSize.height > _tableView.bounds.size.height;
BOOL multipleCells = [self visibleFormItems].count >= 2;
return (isContentSizeLargerThanBounds && multipleCells);
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[_tableContainer sizeHeaderToFit];
[_tableContainer resizeFooterToFitUsingMinHeight:NO];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self updateAnsweredSections];
#if ORK_FEATURE_HEALTHKIT_AUTHORIZATION
NSMutableSet *types = [NSMutableSet set];
for (ORKFormItem *item in [self answerableFormItems]) {
ORKAnswerFormat *format = [item answerFormat];
HKObjectType *objType = [format healthKitObjectTypeForAuthorization];
if (objType) {
[types addObject:objType];
}
}
BOOL refreshDefaultsPending = NO;
if (types.count) {
NSSet<HKObjectType *> *alreadyRequested = [[self taskViewController] requestedHealthTypesForRead];
if (![types isSubsetOfSet:alreadyRequested]) {
refreshDefaultsPending = YES;
[_defaultSource.healthStore requestAuthorizationToShareTypes:nil readTypes:types completion:^(BOOL success, NSError *error) {
if (!success) {
ORK_Log_Debug("Authorization: %@",error);
}
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshDefaults];
});
}];
}
}
if (!refreshDefaultsPending) {
[self refreshDefaults];
}
#endif
// Reset skipped flag - result can now be non-empty
_skipped = NO;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
_autoScrollCancelled = NO;
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
_autoScrollCancelled = YES;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
}
/// returns YES if the answeredSections changed
- (BOOL)updateAnsweredSections {
NSSet *oldValue = [_identifiersOfAnsweredSections copy];
NSMutableSet *newValue = [NSMutableSet new];
NSDiffableDataSourceSnapshot<NSString *, ORKTableCellItemIdentifier *> *snapshot = [_diffableDataSource snapshot];
for (NSString *eachSectionIdentifier in [snapshot sectionIdentifiers]) {
for (ORKTableCellItemIdentifier *itemIdentifier in [snapshot itemIdentifiersInSectionWithIdentifier:eachSectionIdentifier]) {
id answer = _savedAnswers[itemIdentifier.formItemIdentifier];
if (ORKIsAnswerEmpty(answer) == NO) {
[newValue addObject:eachSectionIdentifier];
}
}
}
_identifiersOfAnsweredSections = [newValue mutableCopy];
BOOL answeredSectionsChanged = [oldValue isEqualToSet:newValue] ? NO : YES;
return answeredSectionsChanged;
}
- (void)updateDefaults:(NSMutableDictionary *)defaults {
_savedDefaults = defaults;
__auto_type snapshot = [_diffableDataSource snapshot];
NSMutableArray<ORKTableCellItemIdentifier *> *itemIdentifiersToReload = [NSMutableArray array];
for (ORKFormItemCell *cell in [_tableView visibleCells]) {
NSIndexPath *indexPath = [_tableView indexPathForCell:cell];
ORKFormItem *formItem = [self _formItemForIndexPath:indexPath];
NSString *formItemIdentifier = formItem.identifier;
if ([cell isKindOfClass:[ORKChoiceViewCell class]]) {
// Answers need to be saved.
id answer = _savedAnswers[formItemIdentifier];
answer = answer ? : _savedDefaults[formItemIdentifier];
[self setAnswer:answer forIdentifier:formItemIdentifier];
} else {
cell.defaultAnswer = _savedDefaults[formItemIdentifier];
}
[itemIdentifiersToReload addObject:[_diffableDataSource itemIdentifierForIndexPath:indexPath]];
}
_skipped = NO;
[snapshot reloadItemsWithIdentifiers:itemIdentifiersToReload];
[_diffableDataSource applySnapshot:snapshot animatingDifferences:NO];
[self updateButtonStates];
[self notifyDelegateOnResultChange];
}
- (void)refreshDefaults {
// defaults only come from HealthKit
NSArray *formItems = [self allFormItems];
ORKAnswerDefaultSource *source = _defaultSource;
ORKWeakTypeOf(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSMutableDictionary *defaults = [NSMutableDictionary dictionary];
for (ORKFormItem *formItem in formItems) {
[source fetchDefaultValueForAnswerFormat:formItem.answerFormat handler:^(id defaultValue, NSError *error) {
if (defaultValue != nil) {
defaults[formItem.identifier] = defaultValue;
} else if (error != nil) {
ORK_Log_Error("Error fetching default for %@: %@", formItem, error);
}
dispatch_semaphore_signal(semaphore);
}];
}
for (__unused ORKFormItem *formItem in formItems) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
// All fetches have completed.
dispatch_async(dispatch_get_main_queue(), ^{
ORKStrongTypeOf(weakSelf) strongSelf = weakSelf;
[strongSelf updateDefaults:defaults];
});
});
}
- (void)removeAnswerForIdentifier:(NSString *)identifier {
if (identifier == nil) {
return;
}
[_savedAnswers removeObjectForKey:identifier];
_savedAnswerDates[identifier] = [NSDate date];
}
- (void)setAnswer:(id)answer forIdentifier:(NSString *)identifier {
if (answer == nil || identifier == nil) {
return;
}
if (_savedAnswers == nil) {
_savedAnswers = [NSMutableDictionary new];
}
if (_savedAnswerDates == nil) {
_savedAnswerDates = [NSMutableDictionary new];
}
if (_savedSystemCalendars == nil) {
_savedSystemCalendars = [NSMutableDictionary new];
}
if (_savedSystemTimeZones == nil) {
_savedSystemTimeZones = [NSMutableDictionary new];
}
_savedAnswers[identifier] = answer;
_savedAnswerDates[identifier] = [NSDate date];
_savedSystemCalendars[identifier] = [NSCalendar currentCalendar];
_savedSystemTimeZones[identifier] = [NSTimeZone systemTimeZone];
}
// Override to monitor button title change
- (void)setContinueButtonItem:(UIBarButtonItem *)continueButtonItem {
[super setContinueButtonItem:continueButtonItem];
_navigationFooterView.continueButtonItem = continueButtonItem;
[self updateButtonStates];
}
- (void)setSkipButtonItem:(UIBarButtonItem *)skipButtonItem {
[super setSkipButtonItem:skipButtonItem];
_navigationFooterView.skipButtonItem = skipButtonItem;
[self updateButtonStates];
}
- (CGFloat)heightForText:(NSString *)text withFont:(UIFont *)font withLearnMorePadding:(BOOL)useLearnMorePadding {
CGFloat textPaddingMargin = 0;
if (useLearnMorePadding) {
// the learnmore button blocks off another (ORKSurveyItemMargin) padding
textPaddingMargin = ((ORKSurveyTableContainerLeftRightPadding + (ORKSurveyItemMargin * 3)) * 2);
} else {
textPaddingMargin = ((ORKSurveyTableContainerLeftRightPadding + ORKSurveyItemMargin) * 2);
}
CGFloat textScreenWidth = self.view.frame.size.width - textPaddingMargin;
CGRect frame = [text boundingRectWithSize:CGSizeMake(textScreenWidth, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil];
float height = frame.size.height;
return height;
}
- (CGFloat)heightForFormItem:(ORKFormItem *)formItem {
CGFloat headerHeight = 0.0;
if (formItem.text) {
headerHeight = headerHeight + [self heightForText:formItem.text withFont:[ORKSurveyCardHeaderView titleLabelFont] withLearnMorePadding:NO];
}
if (formItem.detailText) {
headerHeight = headerHeight + [self heightForText:formItem.detailText withFont:[ORKSurveyCardHeaderView detailTextLabelFont] withLearnMorePadding:(formItem.learnMoreItem != nil)] + ORKStepContainerTitleToBodyTopPaddingStandard;
}
if (formItem.tagText) {
headerHeight = headerHeight + [self heightForText:formItem.tagText withFont:[ORKTagLabel font] withLearnMorePadding:NO] + ORKStepContainerTitleToBodyTopPaddingStandard;
}
return headerHeight;
}
- (void)stepDidChange {
[super stepDidChange];
[_tableContainer removeFromSuperview];
_tableContainer = nil;
_tableView.delegate = nil;
_tableView.dataSource = nil;
_tableView = nil;
_formItemCells = nil;
_headerView = nil;
_navigationFooterView = nil;
if (self.isViewLoaded && self.step) {
_formItemCells = [NSMutableSet new];
_tableContainer = [[ORKTableContainerView alloc] initWithStyle:UITableViewStyleGrouped pinNavigationContainer:NO];
_tableContainer.tableContainerDelegate = self;
[self.view addSubview:_tableContainer];
_tableContainer.tapOffView = self.view;
_tableView = _tableContainer.tableView;
_tableView.delegate = self;
[self _registerCellClassesInTableView:_tableView];
ORKWeakTypeOf(self) weakSelf = self;
_diffableDataSource = [[UITableViewDiffableDataSource alloc] initWithTableView:_tableView cellProvider:^UITableViewCell * _Nullable(UITableView * _Nonnull tableView, NSIndexPath * _Nonnull indexPath, id _Nonnull itemIdentifier) {
return [weakSelf _tableView:tableView cellForIndexPath:indexPath itemIdentifier:itemIdentifier];
}];
[self buildDataSource:_diffableDataSource withCompletion:nil];
_tableView.dataSource = _diffableDataSource;
_tableView.clipsToBounds = YES;
_tableView.rowHeight = UITableViewAutomaticDimension;
_tableView.sectionHeaderHeight = UITableViewAutomaticDimension;
_tableView.estimatedRowHeight = ORKGetMetricForWindow(ORKScreenMetricTableCellDefaultHeight, self.view.window);
if ([self formStep].useCardView) {
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
if (ORKNeedWideScreenDesign(self.view)) {
[_tableView setBackgroundColor:[UIColor clearColor]];
[self.taskViewController setNavigationBarColor:ORKColor(ORKBackgroundColorKey)];
[self.view setBackgroundColor:ORKColor(ORKBackgroundColorKey)];
}
else {
[_tableView setBackgroundColor:[UIColor systemGroupedBackgroundColor]];
[self.taskViewController setNavigationBarColor:[_tableView backgroundColor]];
[self.view setBackgroundColor:[_tableView backgroundColor]];
}
} else {
[_tableView setBackgroundColor:[UIColor clearColor]];
}
_headerView = _tableContainer.stepContentView;
_headerView.stepTopContentImage = self.step.image;
_headerView.titleIconImage = self.step.iconImage;
_headerView.stepTitle = self.step.title;
_headerView.stepText = self.step.text;
_headerView.stepDetailText = self.step.detailText;
_headerView.stepHeaderTextAlignment = self.step.headerTextAlignment;
_headerView.bodyItems = self.step.bodyItems;
_tableContainer.stepTopContentImageContentMode = self.step.imageContentMode;
_navigationFooterView = _tableContainer.navigationFooterView;
_navigationFooterView.skipButtonItem = self.skipButtonItem;
_navigationFooterView.continueEnabled = [self continueButtonEnabled];
_navigationFooterView.continueButtonItem = self.continueButtonItem;
_navigationFooterView.optional = self.step.optional;
_navigationFooterView.footnoteLabel.text = [self formStep].footnote;
// Form steps should always force the navigation controller to be scrollable
// therefore we should always remove the styling.
[_navigationFooterView removeStyling];
if (self.readOnlyMode) {
_navigationFooterView.optional = YES;
[_navigationFooterView setNeverHasContinueButton:YES];
_navigationFooterView.skipEnabled = [self skipButtonEnabled];
_navigationFooterView.skipButton.accessibilityTraits = UIAccessibilityTraitStaticText;
}
[self setupConstraints];
}
}
- (void)_registerCellClassesInTableView:(UITableView *)tableView {
// Register all of the row cells for our formItems
for (ORKFormItem *eachItem in [self allFormItems]) {
// our cell choices are based on answerFormat
ORKAnswerFormat *answerFormat = eachItem.impliedAnswerFormat;
NSString *reuseIdentifier = eachItem.identifier;
Class class = answerFormat.formStepViewControllerCellClass;
if ((class != nil) && (reuseIdentifier != nil)) {
[tableView registerClass:class forCellReuseIdentifier:reuseIdentifier];
} else if (answerFormat.choices.count > 0) {
for (id eachChoice in answerFormat.choices) {
if ([eachChoice isKindOfClass:[ORKTextChoiceOther class]]) {
[tableView registerClass:[ORKChoiceOtherViewCell class] forCellReuseIdentifier:NSStringFromClass([eachChoice class])];
} else {
[tableView registerClass:[ORKChoiceViewCell class] forCellReuseIdentifier:NSStringFromClass([eachChoice class])];
}
}
} else {
ORK_Log_Debug("Not registering cell class '%@' for formItem with identifier '%@' answerFormat: %@", class, reuseIdentifier, answerFormat);
}
}
// Now register the header cells
[_tableView registerClass:[ORKSurveyCardHeaderView class] forHeaderFooterViewReuseIdentifier:ORKSurveyCardHeaderViewIdentifier];
}
- (void)setupConstraints {
if (_constraints) {
[NSLayoutConstraint deactivateConstraints:_constraints];
}
_tableContainer.translatesAutoresizingMaskIntoConstraints = NO;
_constraints = nil;
_constraints = @[
[NSLayoutConstraint constraintWithItem:_tableContainer
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_tableContainer
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_tableContainer
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:0.0],
[NSLayoutConstraint constraintWithItem:_tableContainer
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0]
];
[NSLayoutConstraint activateConstraints:_constraints];
}
- (void)buildDataSource:(UITableViewDiffableDataSource<NSString *, ORKTableCellItemIdentifier *> *)dataSource withCompletion:(void (^ _Nullable)(void))completion {
NSDiffableDataSourceSnapshot *snapshot = dataSource.snapshot;
NSArray<ORKFormItem *> *formItems = [[self visibleFormItems] copy];
_maxLabelWidth = -1;
// make a brand new snapshot that holds the section and item identifiers that result from analyzing the formItems
NSDiffableDataSourceSnapshot<NSString *, ORKTableCellItemIdentifier *> *newSnapshot = [[NSDiffableDataSourceSnapshot alloc] init];
for (ORKFormItem *eachItem in formItems) {
NSString *formItemIdentifier = eachItem.identifier;
ORKAnswerFormat *answerFormat = eachItem.impliedAnswerFormat;
if (formItemIdentifier == nil) {
ORK_Log_Info("%@ Refusing to deal with formItem missing identifier", self);
} else if (answerFormat == nil) {
// has no answerFormat
// treat these as sections
[newSnapshot appendSectionsWithIdentifiers:@[formItemIdentifier]];
} else {
NSAssert((answerFormat != nil), @"building tableView data source: assumed answerFormat was nonnull");
NSAssert((formItemIdentifier != nil), @"building tableView data source: assumed formItemIdentifier was nonnull");
// if we're here, we expect to add at least one new itemIdentifier
// Step 1/2: Do we need to make a section for this item to land in?
if ((eachItem.requiresSingleSection) || ([newSnapshot numberOfSections] == 0)) {
[newSnapshot appendSectionsWithIdentifiers:@[formItemIdentifier]];
}
// Step 2/2: Are we adding a single identifier for this formItem or exploding the formItem into an identifier per choice?
if (ORKDynamicCast(answerFormat, ORKTextChoiceAnswerFormat) != nil || ORKDynamicCast(answerFormat, ORKColorChoiceAnswerFormat) != nil) {
// Make one row per choice, we probably made a section already since formItems with choice answerFormats are requiresSingleSection==YES
NSArray *choices = answerFormat.choices;
[choices enumerateObjectsUsingBlock:^(id eachChoice, NSUInteger index, BOOL *stop) {
ORKTableCellItemIdentifier *itemIdentifier = [[ORKTableCellItemIdentifier alloc] initWithFormItemIdentifier:formItemIdentifier choiceIndex:index];
[newSnapshot appendItemsWithIdentifiers:@[itemIdentifier]];
}];
} else {
// has answerFormat but no choices
// Convert the formItem itself into a row
ORKTableCellItemIdentifier *itemIdentifier = [[ORKTableCellItemIdentifier alloc] initWithFormItemIdentifier:formItemIdentifier choiceIndex:NSNotFound];
[newSnapshot appendItemsWithIdentifiers:@[itemIdentifier]];
}
}
}
// remove stale sections
{
NSMutableSet<NSString *> *originalSectionIdentifiers = [NSMutableSet setWithArray:[snapshot sectionIdentifiers]];
NSSet<NSString *> *newSectionIdentifiers = [NSSet setWithArray:[newSnapshot sectionIdentifiers]];
[originalSectionIdentifiers minusSet:newSectionIdentifiers];
[snapshot deleteSectionsWithIdentifiers:[originalSectionIdentifiers allObjects]];
}
// remove stale items
{
NSMutableSet<ORKTableCellItemIdentifier *> *originalItemIdentifiers = [NSMutableSet setWithArray:[snapshot itemIdentifiers]];
NSSet<ORKTableCellItemIdentifier *> *newItemIdentifiers = [NSSet setWithArray:[newSnapshot itemIdentifiers]];
[originalItemIdentifiers minusSet:newItemIdentifiers];
[snapshot deleteItemsWithIdentifiers:[originalItemIdentifiers allObjects]];
}
// Now we can run through the original snapshot and update it based on our new snapshot
for (NSString *eachSectionIdentifier in [newSnapshot sectionIdentifiers]) {
// put the section in the right spot
// yes, we could keep a counter outside the for loop. Computing index here so there's less state to manage
NSInteger index = [newSnapshot indexOfSectionIdentifier:eachSectionIdentifier];
NSUInteger originalCountOfSections = [snapshot numberOfSections];
if (originalCountOfSections > index) {
NSString *originalSectionIdentiferAtIndex = [[snapshot sectionIdentifiers] objectAtIndex:index];
if ([originalSectionIdentiferAtIndex isEqual:eachSectionIdentifier]) {
// the same section identifier lives at the same index, no-op
} else if ([snapshot indexOfSectionIdentifier:eachSectionIdentifier] != NSNotFound) {
// the same section identifier lives in both, but at different index in each: move
[snapshot moveSectionWithIdentifier:eachSectionIdentifier beforeSectionWithIdentifier:originalSectionIdentiferAtIndex];
} else {
// the sectionIdentifer doesn't exist in the original snapshot, insert ahead of whatever's currently at this index
[snapshot insertSectionsWithIdentifiers:@[eachSectionIdentifier] beforeSectionWithIdentifier:originalSectionIdentiferAtIndex];
}
} else {
// More sections in the new snapshot than the old, just append
[snapshot appendSectionsWithIdentifiers:@[eachSectionIdentifier]];
}
for (ORKTableCellItemIdentifier *eachItemIdentifier in [newSnapshot itemIdentifiersInSectionWithIdentifier:eachSectionIdentifier]) {
// put the items in the right spot
// yes, we could keep a counter outside this for loop. Computing this index so there's less state to manage
NSInteger itemIndex = [newSnapshot indexOfItemIdentifier:eachItemIdentifier];
NSUInteger originalCountOfItems = [snapshot numberOfItems];
if (originalCountOfItems > itemIndex) {
ORKTableCellItemIdentifier *originalItemIdentiferAtIndex = [[snapshot itemIdentifiers] objectAtIndex:itemIndex];
if ([originalItemIdentiferAtIndex isEqual:eachItemIdentifier]) {
// the same itemIdentifier lives at the same index, no-op
} else if ([snapshot indexOfItemIdentifier:eachItemIdentifier] != NSNotFound) {
// the same itemIdentifier lives in both, but at different index in each: move
[snapshot moveItemWithIdentifier:eachItemIdentifier beforeItemWithIdentifier:originalItemIdentiferAtIndex];
} else if ([eachItemIdentifier.formItemIdentifier isEqual:originalItemIdentiferAtIndex.formItemIdentifier]) {
// the itemIdentifer doesn't exist in the original snapshot, insert ahead of whatever's currently at this index
[snapshot insertItemsWithIdentifiers:@[eachItemIdentifier] beforeItemWithIdentifier:originalItemIdentiferAtIndex];
} else {
// the itemIdentifier doesn't exist in the original snapshot, append to current section
[snapshot appendItemsWithIdentifiers:@[eachItemIdentifier] intoSectionWithIdentifier:eachSectionIdentifier];
}
// There may be a case where we don't support items moving between sections, but that shouldn't happen since the only way formItems can move around like that is if you feed the stepViewController a new step. Resetting the step builds a brand new tableView and datasource, so you shouldn't hit that problem.
} else {
if ([snapshot indexOfItemIdentifier:eachItemIdentifier] != NSNotFound) {
// item was there in original snapshot, moved in new snapshot, beyond the range of old snapshot's last item
id lastItemIdentifier = [[snapshot itemIdentifiers] lastObject];
[snapshot moveItemWithIdentifier:eachItemIdentifier afterItemWithIdentifier:lastItemIdentifier];
} else {
// the itemIdentifer doesn't exist in the original snapshot
[snapshot appendItemsWithIdentifiers:@[eachItemIdentifier] intoSectionWithIdentifier:eachSectionIdentifier];
}
}
}
}
NSDiffableDataSourceSnapshot *originalSnapshot = dataSource.snapshot;
if ([originalSnapshot isEqual:snapshot] == NO) {
BOOL shouldAnimateDifferences = YES;
// don't bother animating if there was nothing in the original snapshot to start with
shouldAnimateDifferences = shouldAnimateDifferences && (originalSnapshot.numberOfItems > 0);
// update progress text of section header views
NSUInteger totalSections = [snapshot numberOfSections];
if (totalSections > 1) {
for (int i = 0; i < totalSections; i++) {
ORKSurveyCardHeaderView *cardHeaderView = (ORKSurveyCardHeaderView *)[_tableView headerViewForSection:i];
NSString *sectionProgressText = [NSString localizedStringWithFormat:ORKLocalizedString(@"FORM_ITEM_PROGRESS", nil) ,ORKLocalizedStringFromNumber(@(i + 1)), ORKLocalizedStringFromNumber(@(snapshot.numberOfSections))];
[cardHeaderView setProgressText:sectionProgressText];
}
} else {
ORKSurveyCardHeaderView *cardHeaderView = (ORKSurveyCardHeaderView *)[_tableView headerViewForSection:0];
[cardHeaderView setProgressText:nil];
}
// If the footer needs to go back to a larger size, we need to resize here, before applying the snapshot.
if (![self isContentSizeLargerThanFrame]) {
[_tableContainer resizeFooterToFitUsingMinHeight:NO];
}
[dataSource applySnapshot:snapshot animatingDifferences:shouldAnimateDifferences completion:^{
if (completion != nil) {
completion();
}
}];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
if (completion != nil) {
completion();
}
});
}
}
- (NSInteger)numberOfAnsweredFormItemsInDictionary:(NSDictionary *)dictionary {
__block NSInteger nonNilCount = 0;
[dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id answer, BOOL *stop) {
if (ORKIsAnswerEmpty(answer) == NO) {
nonNilCount ++;
}
}];
return nonNilCount;
}
- (NSInteger)numberOfAnsweredFormItems {
return [self numberOfAnsweredFormItemsInDictionary:self.savedAnswers];
}
- (BOOL)allAnsweredFormItemsAreValid {
for (ORKFormItem *item in [self answerableFormItems]) {
id answer = _savedAnswers[item.identifier];
if (ORKIsAnswerEmpty(answer) == NO && ![item.impliedAnswerFormat isAnswerValid:answer]) {
return NO;
}
}
return YES;
}
- (BOOL)allNonOptionalFormItemsHaveAnswers {
for (ORKFormItem *item in [self answerableFormItems]) {
if (!item.optional) {
id answer = _savedAnswers[item.identifier];
if (ORKIsAnswerEmpty(answer) || ![item.impliedAnswerFormat isAnswerValid:answer]) {
return NO;
}
}
}
return YES;
}
- (nullable ORKFormItem *)fetchFirstUnansweredNonOptionalFormItem:(NSArray<ORKFormItem *> *)formItems {
for (ORKFormItem *item in formItems) {
if (!item.optional) {
id answer = _savedAnswers[item.identifier];
if (ORKIsAnswerEmpty(answer) || ![item.impliedAnswerFormat isAnswerValid:answer]) {
return item;
}
}
}
return nil;
}
- (nullable NSString *)fetchSectionThatContainsFormItem:(ORKFormItem *)formItem {
ORKTableCellItemIdentifier *identifier = [[ORKTableCellItemIdentifier alloc] initWithFormItemIdentifier:formItem.identifier choiceIndex:NSNotFound];
__auto_type snapshot = [_diffableDataSource snapshot];
NSString *result = [snapshot sectionIdentifierForSectionContainingItemIdentifier:identifier];
// in case the formItem turned into a section with choices instead, try looking for the formItemIdentifier as a sectionIdentifier
result = result ? : formItem.identifier;
return result;
}
- (BOOL)continueButtonEnabled {
BOOL enabled = ([self allAnsweredFormItemsAreValid] && [self allNonOptionalFormItemsHaveAnswers]);
if (self.isBeingReviewed) {
enabled = enabled && ![self.savedAnswers isEqualToDictionary:self.originalAnswers];
}
return enabled;
}
- (BOOL)skipButtonEnabled {
BOOL enabled = self.formStep.optional;
if (self.isBeingReviewed) {
enabled = self.readOnlyMode ? NO : enabled && [self numberOfAnsweredFormItemsInDictionary:self.originalAnswers] > 0;
}
return enabled;
}
- (void)updateButtonStates {
_navigationFooterView.continueEnabled = [self continueButtonEnabled];
_navigationFooterView.skipEnabled = [self skipButtonEnabled];
if (self.shouldPresentInReview && self.navigationItem.rightBarButtonItem) {
self.navigationItem.rightBarButtonItem.enabled = [self continueButtonEnabled];
}
}
- (void)setShouldPresentInReview:(BOOL)shouldPresentInReview {
[super setShouldPresentInReview:shouldPresentInReview];
[_navigationFooterView setHidden:YES];
}
#pragma mark Helpers
- (ORKFormStep *)formStep {
NSAssert(!self.step || [self.step isKindOfClass:[ORKFormStep class]], nil);
return (ORKFormStep *)self.step;
}
- (NSArray<ORKFormItem *> *)allFormItems {
return [[self formStep] formItems];
}
- (BOOL)isFormItemVisible:(ORKFormItem *)formItem withResult:(ORKTaskResult *)result {
ORKFormItemVisibilityRule *rule = formItem.visibilityRule;
BOOL shouldAllowVisibility = (rule == nil) || ([rule formItemVisibilityForTaskResult:result] == YES);
return shouldAllowVisibility;
}
- (NSArray<ORKFormItem *> *)visibleFormItemsFromResult:(ORKTaskResult *)ongoingTaskResult {
NSMutableArray<ORKFormItem *> *visibleItemsMutableArray = [NSMutableArray new];
for (ORKFormItem *eachItem in [self allFormItems]) {
if ([self isFormItemVisible:eachItem withResult:ongoingTaskResult] == YES) {
[visibleItemsMutableArray addObject:eachItem];
}
}
return [visibleItemsMutableArray copy];
}
- (NSArray<ORKFormItem *> *)visibleFormItems {
ORKTaskResult *taskResult = [self _ongoingTaskResult];
NSArray<ORKFormItem *> *visibileFormItems = [self visibleFormItemsFromResult:taskResult];
return visibileFormItems;
}
- (NSArray *)answerableFormItems {
NSMutableArray *array = [NSMutableArray new];
for (ORKFormItem *item in [self visibleFormItems]) {
if (item.answerFormat != nil) {
[array addObject:item];
}
}
return [array copy];
}
- (nullable ORKFormItem *)_formItemForIndexPath:(NSIndexPath *)indexPath {
ORKFormItem *result;
ORKTableCellItemIdentifier *itemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:indexPath];
result = [self _formItemForFormItemIdentifier:itemIdentifier.formItemIdentifier];
return result;
}
- (nullable ORKFormItem *)_formItemForFormItemIdentifier:(NSString *)formItemIdentifier {
ORKFormItem *result;
NSArray<ORKFormItem *> *allFormItems = [self allFormItems];
NSInteger formItemIndex = [allFormItems indexOfObjectPassingTest:^BOOL(ORKFormItem * testItem, NSUInteger testIndex, BOOL *stop) {
BOOL foundIndex = [testItem.identifier isEqualToString:formItemIdentifier];
return foundIndex;
}];
result = (formItemIndex != NSNotFound) ? [allFormItems objectAtIndex:formItemIndex] : nil;
return result;
}
- (NSSet<NSString *> *)hiddenFormItemIdentifiersForTaskResult:(ORKTaskResult *)taskResult {
// make a set of all the identifiers of formItems we want to hide
NSMutableSet *mutableSet = [NSMutableSet new];
// start with all the formItems
[[self allFormItems] enumerateObjectsUsingBlock:^(ORKFormItem *eachItem, NSUInteger idx, BOOL *stop) {
NSString *identifier = eachItem.identifier;
if (identifier != nil) {
[mutableSet addObject:identifier];
}
}];
// Now remove the visible formItem identifiers. The remaining set are the hidden ones
[[self visibleFormItemsFromResult:taskResult] enumerateObjectsUsingBlock:^(ORKFormItem *eachItem, NSUInteger idx, BOOL *stop) {
NSString *identifier = eachItem.identifier;
if (identifier != nil) {
[mutableSet removeObject:identifier];
}
}];
return [mutableSet copy];
}
- (BOOL)showValidityAlertWithMessage:(NSString *)text {
// Ignore if our answer is null
if (_skipped) {
return NO;
}
return [super showValidityAlertWithMessage:text];
}
- (BOOL)hasAnswer {
return (self.savedAnswers != nil);
}
/// Returns the combination of the delegate's stepViewControllerOngoingResult: ORKTaskResult and the full ORKStepResult for this stepViewController (regardless of formItem visibilityRules)
- (nonnull ORKTaskResult *)_ongoingTaskResult {
ORKTaskResult *taskResult = nil;
id <ORKStepViewControllerDelegate> delegate = [self delegate];
if ([delegate respondsToSelector:@selector(stepViewControllerOngoingResult:)]) {
// make a copy of the taskResult since we're going to change its results
taskResult = [[delegate stepViewControllerOngoingResult:self] copy];
}
// in case no taskResult was returned, make one up
if (taskResult == nil) {
taskResult = [[ORKTaskResult alloc] initWithTaskIdentifier:@"" taskRunUUID:[NSUUID new] outputDirectory:nil];
}
// start with all the stepResults regardless of visibilityRules
ORKStepResult *stepResult = [self _stepResultFromFormItems:[self allFormItems]];
// merge the results with the current ongoing task result.
taskResult.results = [taskResult.results arrayByAddingObject:stepResult];
return taskResult;
}
// Not to use `ImmediateNavigation` when current step already has an answer.
// So user is able to review the answer when it is present.
- (BOOL)isStepImmediateNavigation {
// return [[self formStep] isFormatImmediateNavigation] && [self hasAnswer] == NO && !self.isBeingReviewed;
return NO;
}
- (ORKStepResult *)result {
ORKTaskResult *taskResult = [self _ongoingTaskResult];
// get the stepResult, which should be the last result in the taskResult.results array
// this stepResult contains everything regardless of visibility rules
ORKStepResult *stepResult = ORKDynamicCast(taskResult.results.lastObject, ORKStepResult);
// Make a mutable copy of the stepResult's results array. We're going to remove items from this array
// rather than build a new array from an empty one. This way we preserve the results that may
// have been added through ORKStepViewController's `addResult:` API
NSMutableArray<ORKResult *> *mutableResults = [stepResult.results mutableCopy];
// walk through the array in reverse so we can use cheap removeObjectAtIndex: to remove results that should be hidden
NSSet<NSString *> *hiddenFormItemIdentifiers = [self hiddenFormItemIdentifiersForTaskResult:taskResult];
[stepResult.results enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(ORKResult *eachResult, NSUInteger index, BOOL *stop) {
NSString *identifier = eachResult.identifier;
if ([hiddenFormItemIdentifiers containsObject:identifier]) {
[mutableResults removeObjectAtIndex:index];
}
}];
stepResult.results = [mutableResults copy];
return stepResult;
}
- (ORKStepResult *)_stepResultFromFormItems:(NSArray<ORKFormItem *> *)formItems {
ORKStepResult *parentResult = [super result];
// "Now" is the end time of the result, which is either actually now,
// or the last time we were in the responder chain.
NSDate *now = parentResult.endDate;
NSMutableArray *qResults = [NSMutableArray new];
for (ORKFormItem *item in formItems) {
// Only process formItems for which we would have an answerFormat
if (item.answerFormat == nil) {
continue;
}
// Skipped forms report a "null" value for every item -- by skipping, the user has explicitly said they don't want
// to report any values from this form.
id answer = ORKNullAnswerValue();
NSDate *answerDate = now;
NSCalendar *systemCalendar = [NSCalendar currentCalendar];
NSTimeZone *systemTimeZone = [NSTimeZone systemTimeZone];
if (!_skipped) {
answer = _savedAnswers[item.identifier];
answerDate = _savedAnswerDates[item.identifier] ? : now;
systemCalendar = _savedSystemCalendars[item.identifier];
NSAssert(answer == nil || answer == ORKNullAnswerValue() || systemCalendar != nil, @"systemCalendar NOT saved");
systemTimeZone = _savedSystemTimeZones[item.identifier];
NSAssert(answer == nil || answer == ORKNullAnswerValue() || systemTimeZone != nil, @"systemTimeZone NOT saved");
}
ORKQuestionResult *result = [item.answerFormat resultWithIdentifier:item.identifier answer:answer];
ORKAnswerFormat *impliedAnswerFormat = [item impliedAnswerFormat];
if ([impliedAnswerFormat isKindOfClass:[ORKDateAnswerFormat class]]) {
ORKDateQuestionResult *dqr = (ORKDateQuestionResult *)result;
if (dqr.dateAnswer) {
NSCalendar *usedCalendar = [(ORKDateAnswerFormat *)impliedAnswerFormat calendar] ? : systemCalendar;
dqr.calendar = [NSCalendar calendarWithIdentifier:usedCalendar.calendarIdentifier];
dqr.timeZone = systemTimeZone;
}
} else if ([impliedAnswerFormat isKindOfClass:[ORKNumericAnswerFormat class]]) {
ORKNumericQuestionResult *nqr = (ORKNumericQuestionResult *)result;
if (nqr.unit == nil) {
nqr.unit = [(ORKNumericAnswerFormat *)impliedAnswerFormat unit];
nqr.displayUnit = [(ORKNumericAnswerFormat *)impliedAnswerFormat displayUnit];
}
}
result.startDate = answerDate;
result.endDate = answerDate;
[qResults addObject:result];
}
parentResult.results = [parentResult.results arrayByAddingObjectsFromArray:qResults] ? : qResults;
return parentResult;
}
- (void)skipForward {
// This _skipped flag is a hack so that the -result method can return an empty
// result after the skip action, without having to generate the result
// in advance.
_skipped = YES;
[self notifyDelegateOnResultChange];
[super skipForward];
}
- (void)goBackward {
if (self.isBeingReviewed) {
self.savedAnswers = [[NSMutableDictionary alloc] initWithDictionary:self.originalAnswers];
}
[super goBackward];
}
- (NSInteger)maxLabelWidth {
if (_maxLabelWidth == -1) {
NSInteger labelWidth = 0;
for (ORKFormItem* formItemForMaxWidth in [self allFormItems]) {
labelWidth = MAX(labelWidth, ORKLabelWidth(formItemForMaxWidth.text));
}
_maxLabelWidth = labelWidth;
}
return _maxLabelWidth;
}
// Return NO if we didn't autoscroll
- (BOOL)didAutoScrollToNextItem:(ORKFormItemCell *)cell {
if (![self _isAutoScrollEnabled]) {
return NO;
}
NSIndexPath *currentIndexPath = [self.tableView indexPathForCell:cell];
if (cell.isLastItem) {
return NO;
} else {
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:currentIndexPath.row + 1 inSection:currentIndexPath.section];
ORKQuestionType type = [self _formItemForIndexPath:nextIndexPath].impliedAnswerFormat.questionType;
if ([self doesTableCellTypeUseKeyboard:type]) {
[_tableView deselectRowAtIndexPath:currentIndexPath animated:NO];
return [self focusUnansweredCell:cell];
} else {
return NO;
}
}
return YES;
}
- (BOOL)shouldAutoScrollToNextSection:(NSIndexPath *)indexPath {
if (![self _isAutoScrollEnabled]) {
return NO;
}
BOOL result = YES;
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:0 inSection:(indexPath.section + 1)];
// Technically, cellForRowAtIndexPath could return nil if the tableView hasn't decided to cache the cell
// Guarantee ourselves a cell by using dequeueReusableCellWithIdentifier—the only reason we need the cell is to test
// the cell's type, not for actual display, so using any cell paired with the reuseIdentifier should be fine
// do *not* use dequeueReusableCellWithIdentifier:indexPath: since that method should only be called within the dataSource
// tableView:cellForRowAtIndexPath: method
ORKFormItem *nextFormItem = [self _formItemForIndexPath:nextIndexPath];
ORKTableCellItemIdentifier *nextItemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:nextIndexPath];
NSString *nextReuseIdentifier = [self cellReuseIdentifierFromFormItem:nextFormItem cellItemIdentifier:nextItemIdentifier];
result = result && (_autoScrollCancelled == NO);
// can't autoscroll to something that doesn't exist
UITableViewCell *nextCell = [_tableView dequeueReusableCellWithIdentifier:nextReuseIdentifier];
result = result && (nextCell != nil);
// don't autoscroll to a cell that already has an answer
result = result && (self.savedAnswers[nextFormItem.identifier] == nil);
return result;
}
- (void)autoScrollToNextSection:(NSIndexPath *)indexPath {
if (![self _isAutoScrollEnabled]) {
return;
}
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:0 inSection:(indexPath.section + 1)];
UITableView *tableView = self.tableView;
UITableViewCell *nextCell = [tableView cellForRowAtIndexPath:nextIndexPath];
BOOL didChangeFocus = [self focusUnansweredCell:nextCell];
if (didChangeFocus == YES) {
// if we did change focus, then that will perform the scrolling
} else {
// if we didn't change focus, then we need to handle the scrolling here
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, DelayBeforeAutoScroll * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[tableView scrollToRowAtIndexPath:nextIndexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
});
}
}
- (void)scrollToFirstUnansweredSection {
if (![self _isAutoScrollEnabled]) {
return;
}
ORKFormItem *formItem = [self fetchFirstUnansweredNonOptionalFormItem:[self answerableFormItems]];
[self scrollToFormItem:formItem];
}
- (void)scrollToFooter {
if (![self _isAutoScrollEnabled]) {
return;
}
CGRect tableFooterRect = [self.tableView convertRect:self.tableView.tableFooterView.bounds fromView:self.tableView.tableFooterView];
[self.tableView scrollRectToVisible:tableFooterRect animated:YES];
}
- (void)scrollToFormItem:(ORKFormItem *)formItem {
if (![self _isAutoScrollEnabled]) {
return;
}
NSString *sectionIdentifier = [self fetchSectionThatContainsFormItem:formItem];
NSInteger section = [[_diffableDataSource snapshot] indexOfSectionIdentifier:sectionIdentifier];
if (section != NSNotFound) {
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:0 inSection:section];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, DelayBeforeAutoScroll * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[_tableView scrollToRowAtIndexPath:nextIndexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
});
}
}
- (BOOL)_isAutoScrollEnabled {
ORKFormStep *formStep = [self formStep];
return formStep.autoScrollEnabled;
}
- (nullable ORKTextChoiceAnswerFormat *)textChoiceAnswerFormatForIndexPath:(NSIndexPath *)indexPath {
ORKTextChoiceAnswerFormat *result = nil;
ORKTableCellItemIdentifier *itemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:indexPath];
ORKFormItem *formItem = [self _formItemForFormItemIdentifier:itemIdentifier.formItemIdentifier];
result = ORKDynamicCast(formItem.impliedAnswerFormat, ORKTextChoiceAnswerFormat);
return result;
}
- (BOOL)exclusiveChoiceSelectedForSectionIdentifier:(NSString *)sectionIdentifier withCell:(UITableViewCell *)cell {
BOOL result = NO;
ORKChoiceViewCell *choiceViewCell = ORKDynamicCast(cell, ORKChoiceViewCell);
if (choiceViewCell != nil) {
// section identifiers are formItem identifiers, and formItem identifiers are the keys into answers
id answer = _savedAnswers[sectionIdentifier];
result = (answer != nil && choiceViewCell.isExclusive);
}
return result;
}
- (BOOL)doesTableCellTypeUseKeyboard:(ORKQuestionType)questionType {
switch (questionType) {
case ORKQuestionTypeDecimal:
case ORKQuestionTypeInteger:
case ORKQuestionTypeText:
return YES;
default:
return NO;
}
}
- (void)saveAnswer:(id)answer forItemIdentifier:(ORKTableCellItemIdentifier *)itemIdentifier {
NSString *formItemIdentifier = [itemIdentifier formItemIdentifier];
if (formItemIdentifier != nil) {
if (answer != nil) {
[self setAnswer:answer forIdentifier:formItemIdentifier];
} else {
[self removeAnswerForIdentifier:formItemIdentifier];
}
}
NSIndexPath *indexPath = [_diffableDataSource indexPathForItemIdentifier:itemIdentifier];
[self answerChangedForIndexPath:indexPath];
}
/// returns NO if we couldn't make the cell become first responder, or the cell has an answer already
- (BOOL)focusUnansweredCell:(UITableViewCell *)cell {
BOOL result = NO;
ORKFormItemCell *formItemCell = ORKDynamicCast(cell, ORKFormItemCell);
// don't try to make cell first responder if it already has an answer
BOOL cellNeedsBecomeFirstResponder = (self.savedAnswers[formItemCell.formItem.identifier] == nil);
if (cellNeedsBecomeFirstResponder == YES) {
result = [formItemCell becomeFirstResponder];
}
return result;
}
#pragma mark NSNotification methods
- (void)keyboardWillShow:(NSNotification *)notification {
if (_currentFirstResponderCell) {
if ([_currentFirstResponderCell isKindOfClass:[ORKChoiceOtherViewCell class]]) {
CGRect keyboardFrame = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect convertedKeyboardFrame = [self.view convertRect:keyboardFrame fromView:nil];
if (CGRectGetMaxY(_currentFirstResponderCell.frame) >= CGRectGetMinY(convertedKeyboardFrame)) {
UITableView *tableView = self.tableView;
[tableView setContentInset:UIEdgeInsetsMake(0, 0, CGRectGetHeight(convertedKeyboardFrame), 0)];
NSIndexPath *currentFirstResponderCellIndex = [tableView indexPathForCell:_currentFirstResponderCell];
if (currentFirstResponderCellIndex && [self _isAutoScrollEnabled]) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, DelayBeforeAutoScroll * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[tableView scrollToRowAtIndexPath:currentFirstResponderCellIndex atScrollPosition:UITableViewScrollPositionBottom animated:YES];
});
}
}
} else {
CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
if ((_currentFirstResponderCell.frame.origin.y + CGRectGetHeight(_currentFirstResponderCell.frame)) >= (CGRectGetHeight(self.view.frame) - keyboardSize.height)) {
_tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardSize.height + TableViewYOffsetStandard, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, DelayBeforeAutoScroll * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[_tableContainer scrollCellVisible:_currentFirstResponderCell animated:YES];
});
}
}
}
}
- (void)keyboardWillHide:(NSNotification *)notification {
_tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
}
- (void)resizeORKChoiceOtherViewCell:(ORKChoiceOtherViewCell *)choiceOtherViewCell withTextChoice:(ORKTextChoiceOther *)textChoice {
[_tableView beginUpdates];
[choiceOtherViewCell setupWithText:textChoice.textViewText placeholderText:textChoice.textViewPlaceholderText];
[_tableView endUpdates];
}
- (BOOL)isChoiceSelected:(id)value atIndex:(NSUInteger)index answer:(id)answer {
BOOL isSelected = NO;
if (answer != nil && answer != ORKNullAnswerValue()) {
if ([answer isKindOfClass:[NSArray class]]) {
if (value) {
isSelected = [(NSArray *)answer containsObject:value];
} else {
isSelected = [(NSArray *)answer containsObject:@(index)];
}
} else {
if (value) {
isSelected = ([answer isEqual:value]);
} else {
isSelected = (((NSNumber *)answer).integerValue == index);
}
}
}
return isSelected;
}
- (NSString *)cellReuseIdentifierFromFormItem:(ORKFormItem *)formItem cellItemIdentifier:(ORKTableCellItemIdentifier *)itemIdentifier {
NSString *result;
NSString *formItemIdentifier = itemIdentifier.formItemIdentifier;
if (itemIdentifier.choiceIndex == NSNotFound) {
result = formItemIdentifier;
} else {
ORKAnswerFormat *answerFormat = formItem.impliedAnswerFormat;
id choice = [answerFormat.choices objectAtIndex:itemIdentifier.choiceIndex];
result = NSStringFromClass([choice class]);
}
return result;
}
- (UITableViewCell *)_tableView:(UITableView *)tableView cellForIndexPath:(NSIndexPath *)indexPath itemIdentifier:(ORKTableCellItemIdentifier *)itemIdentifier {
NSString *formItemIdentifier = itemIdentifier.formItemIdentifier;
ORKFormItem *formItem = [self _formItemForFormItemIdentifier:formItemIdentifier];
NSString *reuseIdentifier = [self cellReuseIdentifierFromFormItem:formItem cellItemIdentifier:itemIdentifier];
NSAssert((reuseIdentifier != nil), @"reuseIdentifier cannot be nil");
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];
cell.userInteractionEnabled = !self.readOnlyMode;
cell.accessibilityIdentifier = [NSString stringWithFormat:@"%@_%ld", formItemIdentifier, (long)indexPath.row];
ORKFormItemCell *formCell = ORKDynamicCast(cell, ORKFormItemCell);
ORKChoiceViewCell *choiceViewCell = ORKDynamicCast(cell, ORKChoiceViewCell);
__auto_type snapshot = [_diffableDataSource snapshot];
if (formCell != nil) {
id answer = _savedAnswers[formItemIdentifier];
NSInteger maxLabelWidth = [self maxLabelWidth];
[formCell configureWithFormItem:formItem answer:answer maxLabelWidth:maxLabelWidth delegate:self];
[formCell setExpectedLayoutWidth:tableView.bounds.size.width];
formCell.selectionStyle = UITableViewCellSelectionStyleNone;
formCell.defaultAnswer = _savedDefaults[formItemIdentifier];
if (!_savedAnswers) {
_savedAnswers = [NSMutableDictionary new];
}
formCell.savedAnswers = _savedAnswers;
formCell.useCardView = [self formStep].useCardView;
formCell.cardViewStyle = [self formStep].cardViewStyle;
formCell.isLastItem = ^{
NSInteger section = indexPath.section;
NSInteger rowCountInSection = [_diffableDataSource tableView:tableView numberOfRowsInSection:section];
BOOL isLastItem = rowCountInSection == indexPath.row + 1;
return isLastItem;
}();
formCell.isFirstItemInSectionWithoutTitle = ^{
NSString *sectionFormItemIdentifier = [snapshot sectionIdentifierForSectionContainingItemIdentifier:itemIdentifier];
ORKFormItem *sectionFormItem = [self _formItemForFormItemIdentifier:sectionFormItemIdentifier];
BOOL isFirstItemWithSectionWithoutTitle = (indexPath.row == 0) && (sectionFormItem.text == nil); // formItem.text is section.title
return isFirstItemWithSectionWithoutTitle;
}();
} else if (choiceViewCell != nil) {
// check whether this cell should be selected, based on answer
ORKAnswerFormat *answerFormat = formItem.impliedAnswerFormat;
NSInteger choiceIndex = itemIdentifier.choiceIndex;
if (choiceIndex != NSNotFound) {
BOOL isLastItem = (choiceIndex + 1) == answerFormat.choices.count;
if ([answerFormat isKindOfClass:[ORKTextChoiceAnswerFormat class]]) {
ORKTextChoice *textChoice = [answerFormat.choices objectAtIndex:choiceIndex];
[choiceViewCell configureWithTextChoice:textChoice isLastItem:isLastItem];
}
if ([answerFormat isKindOfClass:[ORKColorChoiceAnswerFormat class]]) {
ORKColorChoice *colorChoice = [answerFormat.choices objectAtIndex:choiceIndex];
[choiceViewCell configureWithColorChoice:colorChoice isLastItem:isLastItem];
}
id answer = _savedAnswers[formItemIdentifier];
ORKChoiceAnswerFormatHelper *helper = [[ORKChoiceAnswerFormatHelper alloc] initWithAnswerFormat:answerFormat];
NSArray *selectedIndexes = [helper selectedIndexesForAnswer:answer];
if ([selectedIndexes containsObject:@(choiceIndex)]) {
[choiceViewCell setCellSelected:YES highlight:NO];
} else {
[choiceViewCell setCellSelected:NO highlight:NO];
}
} else {
ORK_Log_Debug("[FORMSTEP] choiceIndex was NSNotFound");
}
ORKChoiceOtherViewCell *choiceOtherViewCell = ORKDynamicCast(choiceViewCell, ORKChoiceOtherViewCell);
// This code used to be executed only once, when the cell was being created.
// Now that we use dequeue to always create a cell, that logic doesn't apply anymore
if (choiceOtherViewCell != nil) {
ORKTextChoice *textChoice = [answerFormat.choices objectAtIndex:choiceIndex];
ORKTextChoiceOther *textChoiceOther = ORKDynamicCast(textChoice, ORKTextChoiceOther);
if (textChoiceOther != nil) {
[choiceOtherViewCell setupWithText:textChoiceOther.textViewText placeholderText:textChoiceOther.textViewPlaceholderText];
}
}
choiceOtherViewCell.delegate = self;
choiceViewCell.tintColor = ORKViewTintColor(self.view);
choiceViewCell.useCardView = [self formStep].useCardView;
choiceViewCell.cardViewStyle = [self formStep].cardViewStyle;
// choiceViewCell.isLastItem = isLastItem;
// choiceViewCell.isFirstItemInSectionWithoutTitle = isFirstItemWithSectionWithoutTitle;
[choiceViewCell layoutSubviews];
} else {
NSString *sectionIdentifier = [[snapshot sectionIdentifiers] objectAtIndex:indexPath.section];
ORK_Log_Debug("[FORMSTEP] _tableView:CellForIndexPath: at index: %@ for section: '%@' cell type is '%@'", @(indexPath.row), sectionIdentifier, NSStringFromClass([cell class]));
}
return cell;
}
static CGFloat ORKLabelWidth(NSString *text) {
static ORKCaption1Label *sharedLabel;
if (sharedLabel == nil) {
sharedLabel = [ORKCaption1Label new];
}
sharedLabel.text = text;
return [sharedLabel textRectForBounds:CGRectInfinite limitedToNumberOfLines:1].size.width;
}
#pragma mark UITableViewDelegate
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)didSelectChoiceOtherViewCellWithItemIdentifier:(ORKTableCellItemIdentifier *)itemIdentifier
choiceOtherViewCell:(ORKChoiceOtherViewCell *)choiceOtherViewCell {
if (choiceOtherViewCell.textView.text.length <= 0) {
[self reloadItems:@[itemIdentifier]];
[_tableContainer resizeFooterToFitUsingMinHeight:([self isContentSizeWithinFrame])];
}
}
- (void)reloadItems:(NSArray<ORKTableCellItemIdentifier *> *)itemIdentifiers {
NSDiffableDataSourceSnapshot<NSString *, ORKTableCellItemIdentifier *> * snapshot = [_diffableDataSource snapshot];
[snapshot reloadItemsWithIdentifiers:itemIdentifiers];
[_diffableDataSource applySnapshot:snapshot animatingDifferences:false];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
ORKTableCellItemIdentifier *itemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:indexPath];
ORKFormItem *formItem = [self _formItemForFormItemIdentifier:itemIdentifier.formItemIdentifier];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
ORKFormItemCell *formItemCell = ORKDynamicCast(cell, ORKFormItemCell);
if (formItemCell != nil) {
[formItemCell becomeFirstResponder];
if ([self _isAutoScrollEnabled]) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, DelayBeforeAutoScroll * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
});
}
}
ORKTextChoiceAnswerFormat *textChoiceAnswerFormat = ORKDynamicCast(formItem.impliedAnswerFormat, ORKTextChoiceAnswerFormat);
ORKChoiceViewCell *choiceViewCell = ORKDynamicCast(cell, ORKChoiceViewCell);
if (choiceViewCell != nil) {
// Dismiss other textField's keyboard
[tableView endEditing:NO];
// find all the row/cell peers to this indexPath
// the formItem
BOOL shouldAllowMultiSelection = YES; // assume multiple selection by default
ORKColorChoiceAnswerFormat *colorChoiceAnswerFormat = ORKDynamicCast(formItem.impliedAnswerFormat, ORKColorChoiceAnswerFormat);
if (textChoiceAnswerFormat != nil || colorChoiceAnswerFormat != nil) {
ORKChoiceAnswerFormatHelper *helper = [[ORKChoiceAnswerFormatHelper alloc] initWithAnswerFormat:textChoiceAnswerFormat ? : colorChoiceAnswerFormat];
// does the answerFormat want multiple selection?
BOOL answerFormatAllowsMultiSelection = textChoiceAnswerFormat ? (textChoiceAnswerFormat.style == ORKChoiceAnswerStyleMultipleChoice) : (colorChoiceAnswerFormat.style == ORKChoiceAnswerStyleMultipleChoice);
id answer = _savedAnswers[itemIdentifier.formItemIdentifier];
NSMutableSet* selectedIndexes = [[NSMutableSet alloc] initWithArray:[helper selectedIndexesForAnswer:answer]];
shouldAllowMultiSelection = shouldAllowMultiSelection && answerFormatAllowsMultiSelection;
// does the selected cell allow multiple choice?
shouldAllowMultiSelection = shouldAllowMultiSelection && (choiceViewCell.isExclusive == NO);
// does the cell representing the current answer allow multiple choice?
NSNumber *previousSingleSelectionValue = [helper selectedIndexForAnswer:_savedAnswers[itemIdentifier.formItemIdentifier]];
NSInteger previousSingleSelection = previousSingleSelectionValue ? previousSingleSelectionValue.integerValue : NSNotFound;
BOOL choiceIsExclusive = NO;
if (textChoiceAnswerFormat) {
ORKTextChoice *selectedChoice = (previousSingleSelection != NSNotFound) ? [helper textChoiceAtIndex:previousSingleSelection] : nil;
choiceIsExclusive = selectedChoice.exclusive;
}
if (!textChoiceAnswerFormat) {
ORKColorChoice *selectedChoice = (previousSingleSelection != NSNotFound) ? [helper colorChoiceAtIndex:previousSingleSelection] : nil;
choiceIsExclusive = selectedChoice.exclusive;
}
shouldAllowMultiSelection = shouldAllowMultiSelection && !choiceIsExclusive;
NSRange range = NSMakeRange(0, helper.choiceCount);
NSIndexSet *relatedChoiceRows = [NSIndexSet indexSetWithIndexesInRange:range];
NSInteger eachIndex = relatedChoiceRows.firstIndex;
while (eachIndex != NSNotFound) {
NSIndexPath *testIndexPath = [NSIndexPath indexPathForRow:eachIndex inSection:indexPath.section];
ORKChoiceViewCell *testCell = [tableView cellForRowAtIndexPath:testIndexPath];
if (shouldAllowMultiSelection) {
// allowing multi selection means allowing toggling cells on and off when tapped
if (testCell == choiceViewCell) {
BOOL newSelectedState = !choiceViewCell.isCellSelected;
[testCell setCellSelected:newSelectedState highlight:YES];
}
if (testCell.isCellSelected) {
ORK_Log_Debug("[MULTI SELECTION] adding index %@", @(eachIndex));
[selectedIndexes addObject:@(eachIndex)];
ORK_Log_Debug("[MULTI SELECTION] selected indexes are %@", selectedIndexes);
} else if (testCell && testCell.isCellSelected == NO) {
ORK_Log_Debug("[SELECTION] removing index %@", @(eachIndex));
[selectedIndexes removeObject:@(eachIndex)];
}
} else if (testCell == choiceViewCell) {
// only allow a single cell to be selected at a time
ORK_Log_Debug("[SELECTION] setting cell selected");
[testCell setCellSelected:YES highlight:YES];
[selectedIndexes addObject:@(eachIndex)];
} else {
// we're not allowing multi-selection, but this isn't the selected cell either, unhighlight
[testCell setCellSelected:NO highlight:NO];
ORK_Log_Debug("[SELECTION] removing index %@", @(eachIndex));
[selectedIndexes removeObject:@(eachIndex)];
}
eachIndex = [relatedChoiceRows indexGreaterThanIndex:eachIndex];
}
NSArray *uniqueSelectedIndexes = [selectedIndexes allObjects];
uniqueSelectedIndexes = [uniqueSelectedIndexes sortedArrayUsingComparator:^NSComparisonResult(NSNumber *obj1, NSNumber *obj2) {
return [obj1 compare:obj2];
}];
int textChoiceOtherIndex = 0;
for (ORKTextChoice *textChoice in formItem.impliedAnswerFormat.choices) {
ORKTextChoiceOther *textChoiceOther = ORKDynamicCast(textChoice, ORKTextChoiceOther);
if (textChoiceOther != nil) {
ORKChoiceOtherViewCell *choiceOtherViewCell = [_tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:textChoiceOtherIndex inSection:indexPath.section]];
[self resizeORKChoiceOtherViewCell:choiceOtherViewCell withTextChoice:textChoiceOther];
}
textChoiceOtherIndex = textChoiceOtherIndex + 1;
}
answer = [helper answerForSelectedIndexes:uniqueSelectedIndexes];
[self saveAnswer:answer forItemIdentifier:itemIdentifier];
ORK_Log_Debug("saved answers are now %@'", [self savedAnswers]);
} else {
ORK_Log_Debug("[FORMSTEP] NOT textChoice: row for item %@ selected: answerFormat is '%@'", itemIdentifier, formItem.impliedAnswerFormat);
}
} else {
ORK_Log_Debug("[FORMSTEP] NOT ORKChoiceViewCell: row for indexPath %@ selected. Cell: %@", indexPath, cell);
}
ORKChoiceOtherViewCell *choiceOtherViewCell = ORKDynamicCast(cell, ORKChoiceOtherViewCell);
if (choiceOtherViewCell != nil && textChoiceAnswerFormat != nil) {
// we need to call this at the end of didSelect, because the cell will have `_selected` property to `YES`
// calling this earlier, would cause us to
// [reload tableView] -> which calls -> layoutSubviews on ORKChoiceViewCell -> which calls ->
// updateSelectedItem, which has `_checked` as false, and sets the checkmark to grey
// if we defer this call to the end, it works nice, and by using diffabledatasource to reload it animates nicely
[self didSelectChoiceOtherViewCellWithItemIdentifier:itemIdentifier choiceOtherViewCell:choiceOtherViewCell];
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
__auto_type snapshot = [_diffableDataSource snapshot];
NSString *sectionIdentifier = [[snapshot sectionIdentifiers] objectAtIndex:section];
ORKFormItem *sectionFormItem = [self _formItemForFormItemIdentifier:sectionIdentifier];
NSString *title = sectionFormItem.text;
// Make first section header view zero height when there is no title
return [self formStep].useCardView ? UITableViewAutomaticDimension : (title.length > 0) ? UITableViewAutomaticDimension : ((section == 0) ? 0 : UITableViewAutomaticDimension);
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section {
__auto_type snapshot = [_diffableDataSource snapshot];
NSString *sectionIdentifier = [[snapshot sectionIdentifiers] objectAtIndex:section];
ORKFormItem *sectionFormItem = [self _formItemForFormItemIdentifier:sectionIdentifier];
return [self heightForFormItem:sectionFormItem] + (ORKIsAccessibilityLargeTextEnabled() ? ORKFormStepLargeTextMinimumHeaderHeight : ORKFormStepMinimumHeaderHeight);
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
__auto_type snapshot = [_diffableDataSource snapshot];
NSArray<NSString *> *sectionIdentifiers = [snapshot sectionIdentifiers];
NSString *sectionIdentifier = [sectionIdentifiers objectAtIndex:section];
ORKFormItem *sectionFormItem = [self _formItemForFormItemIdentifier:sectionIdentifier];
NSString *title = sectionFormItem.text;
NSString *detailText = sectionFormItem.detailText;
NSString *sectionProgressText = nil;
ORKLearnMoreView *learnMoreView;
NSString *tagText = sectionFormItem.tagText;
BOOL hasMultipleChoiceFormItem = NO;
BOOL shouldIgnoreDarkMode = NO;
if (sectionFormItem.showsProgress) {
if ([self.delegate respondsToSelector:@selector(stepViewControllerTotalProgressInfoForStep:currentStep:)]) {
ORKTaskTotalProgress progressInfo = [self.delegate stepViewControllerTotalProgressInfoForStep:self currentStep:self.step];
if (progressInfo.stepShouldShowTotalProgress) {
sectionProgressText = [NSString localizedStringWithFormat:ORKLocalizedString(@"FORM_ITEM_PROGRESS", nil) ,ORKLocalizedStringFromNumber(@(section + progressInfo.currentStepStartingProgressPosition)), ORKLocalizedStringFromNumber(@(progressInfo.total))];
}
}
if (!sectionProgressText) {
// only display progress label if there are more than 1 sections in the form step
if (snapshot.numberOfSections > 1) {
sectionProgressText = [NSString localizedStringWithFormat:ORKLocalizedString(@"FORM_ITEM_PROGRESS", nil) ,ORKLocalizedStringFromNumber(@(section + 1)), ORKLocalizedStringFromNumber(@(snapshot.numberOfSections))];
}
}
}
if (sectionFormItem.learnMoreItem) {
learnMoreView = [ORKLearnMoreView learnMoreViewWithItem:sectionFormItem.learnMoreItem];
learnMoreView.delegate = self;
}
hasMultipleChoiceFormItem = (sectionFormItem.impliedAnswerFormat.questionType == ORKQuestionTypeMultipleChoice) ? YES : NO;
shouldIgnoreDarkMode = [sectionFormItem.impliedAnswerFormat isKindOfClass:[ORKColorChoiceAnswerFormat class]];
ORKSurveyCardHeaderView *cardHeaderView = (ORKSurveyCardHeaderView *)[tableView dequeueReusableHeaderFooterViewWithIdentifier: ORKSurveyCardHeaderViewIdentifier];
[cardHeaderView configureWithTitle:title
detailText:detailText
learnMoreView:learnMoreView
progressText:sectionProgressText
tagText:tagText
showBorder:([self formStep].cardViewStyle == ORKCardViewStyleBordered)
hasMultipleChoiceItem:hasMultipleChoiceFormItem
shouldIgnoreDarkMode:shouldIgnoreDarkMode];
return cardHeaderView;
}
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
ORKFormStep *formStep = [self formStep];
if (formStep.footerText != nil && (section == (tableView.numberOfSections - 1))) {
return formStep.footerText;
}
return nil;
}
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return section == tableView.numberOfSections - 1 ? UITableViewAutomaticDimension : 10;
}
#pragma mark ORKFormItemCellDelegate
- (void)formItemCellDidBecomeFirstResponder:(ORKFormItemCell *)cell {
if (_currentFirstResponderCell) {
ORKFormItemTextFieldBasedCell *previousSelectedCell = (ORKFormItemTextFieldBasedCell*)_currentFirstResponderCell;
if (previousSelectedCell != nil && [previousSelectedCell respondsToSelector:@selector(removeEditingHighlight)]) {
[previousSelectedCell removeEditingHighlight];
}
}
_currentFirstResponderCell = cell;
NSIndexPath *path = [_tableView indexPathForCell:cell];
if (path) {
ORKTableContainerView *tableContainer = _tableContainer;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, DelayBeforeAutoScroll * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[tableContainer scrollCellVisible:cell animated:YES];
});
}
}
- (void)formItemCellDidResignFirstResponder:(ORKFormItemCell *)cell {
if (_currentFirstResponderCell == cell) {
_currentFirstResponderCell = nil;
}
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
ORKTableCellItemIdentifier *cellItemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:indexPath];
if ([cell isKindOfClass:[ORKFormItemPickerCell class]] || [cell isKindOfClass:[ORKFormItemTextCell class]]) {
__weak typeof(self) weakSelf = self;
[self buildDataSource:_diffableDataSource withCompletion:^{
[weakSelf finishHandlingFormItemCellDidResignFirstResponder:cellItemIdentifier];
}];
} else {
[self finishHandlingFormItemCellDidResignFirstResponder:cellItemIdentifier];
}
}
- (void)finishHandlingFormItemCellDidResignFirstResponder:(ORKTableCellItemIdentifier *)cellItemIdentifier {
//determines if the table should autoscroll to the next section
__auto_type snapshot = [_diffableDataSource snapshot];
NSIndexPath *indexPath = [_diffableDataSource indexPathForItemIdentifier:cellItemIdentifier];
NSString *sectionIdentifier = [[snapshot sectionIdentifiers] objectAtIndex:indexPath.section];
ORKFormItemCell *cell = ORKDynamicCast([self.tableView cellForRowAtIndexPath:indexPath], ORKFormItemCell);
if (cell.isLastItem && [self shouldAutoScrollToNextSection:indexPath]) {
[self autoScrollToNextSection:indexPath];
return;
} else if (cell.isLastItem && indexPath.section == (snapshot.numberOfSections - 1) && ![_identifiersOfAnsweredSections containsObject:sectionIdentifier]) {
if (![self allNonOptionalFormItemsHaveAnswers]) {
[self scrollToFirstUnansweredSection];
} else {
[self scrollToFooter];
}
}
if (indexPath) {
NSInteger numberOfItemsInSection = [snapshot numberOfItemsInSection:sectionIdentifier];
if (indexPath.row < numberOfItemsInSection - 1) {
NSIndexPath *nextPath = [NSIndexPath indexPathForRow:(indexPath.row + 1) inSection:indexPath.section];
NSString *nextFormItemIdentifier = [[_diffableDataSource itemIdentifierForIndexPath:nextPath] formItemIdentifier];
BOOL cellNeedsBecomeFirstResponder = (self.savedAnswers[nextFormItemIdentifier] == nil);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, DelayBeforeAutoScroll * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
if (_currentFirstResponderCell == nil) {
if (cellNeedsBecomeFirstResponder == YES) {
ORKFormItemCell *formItemCell = ORKDynamicCast([_tableView cellForRowAtIndexPath:nextPath], ORKFormItemCell);
[formItemCell becomeFirstResponder];
}
if ([self _isAutoScrollEnabled]) {
[_tableView scrollToRowAtIndexPath:nextPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
}
});
}
}
}
- (void)formItemCell:(ORKFormItemCell *)cell invalidInputAlertWithMessage:(NSString *)input {
[self showValidityAlertWithMessage:input];
}
- (void)formItemCell:(ORKFormItemCell *)cell invalidInputAlertWithTitle:(NSString *)title message:(NSString *)message {
[self showValidityAlertWithTitle:title message:message];
}
- (void)formItemCell:(ORKFormItemCell *)cell answerDidChangeTo:(id)answer {
if (cell.superview != nil) {
ORKTableCellItemIdentifier *cellItemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:[_tableView indexPathForCell:cell]];
[self saveAnswer:answer forItemIdentifier:cellItemIdentifier];
} else {
// if the cell isn't in the view hierarchy, this change is coming from configuring the cell
// ignore
}
}
- (BOOL)formItemCellShouldDismissKeyboard:(ORKFormItemCell *)cell {
if ([self didAutoScrollToNextItem:cell]) {
return NO;
}
return YES;
}
#pragma mark ORKTableContainerViewDelegate
- (UITableViewCell *)currentFirstResponderCellForTableContainerView:(ORKTableContainerView *)tableContainerView {
return _currentFirstResponderCell;
}
#pragma mark UIStateRestoration
static NSString *const _ORKSavedAnswersRestoreKey = @"savedAnswers";
static NSString *const _ORKSavedAnswerDatesRestoreKey = @"savedAnswerDates";
static NSString *const _ORKSavedSystemCalendarsRestoreKey = @"savedSystemCalendars";
static NSString *const _ORKSavedSystemTimeZonesRestoreKey = @"savedSystemTimeZones";
static NSString *const _ORKOriginalAnswersRestoreKey = @"originalAnswers";
static NSString *const _ORKAnsweredSectionIdentifiersRestoreKey = @"answeredSectionIdentifiers";
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[super encodeRestorableStateWithCoder:coder];
[coder encodeObject:_savedAnswers forKey:_ORKSavedAnswersRestoreKey];
[coder encodeObject:_savedAnswerDates forKey:_ORKSavedAnswerDatesRestoreKey];
[coder encodeObject:_savedSystemCalendars forKey:_ORKSavedSystemCalendarsRestoreKey];
[coder encodeObject:_savedSystemTimeZones forKey:_ORKSavedSystemTimeZonesRestoreKey];
[coder encodeObject:_originalAnswers forKey:_ORKOriginalAnswersRestoreKey];
[coder encodeObject:_identifiersOfAnsweredSections forKey:_ORKAnsweredSectionIdentifiersRestoreKey];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
[super decodeRestorableStateWithCoder:coder];
NSSet *decodableAnswerTypes = [NSSet setWithObjects:NSMutableDictionary.self, NSString.self, NSNumber.self, NSDate.self, nil];
_savedAnswers = [coder decodeObjectOfClasses:decodableAnswerTypes forKey:_ORKSavedAnswersRestoreKey];
[self removeInvalidSavedAnswers];
_savedAnswerDates = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSMutableDictionary.self, NSString.self, NSDate.self]] forKey:_ORKSavedAnswerDatesRestoreKey];
_savedSystemCalendars = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSMutableDictionary.self, NSString.self, NSCalendar.self]] forKey:_ORKSavedSystemCalendarsRestoreKey];
_savedSystemTimeZones = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSMutableDictionary.self, NSString.self, NSTimeZone.self]] forKey:_ORKSavedSystemTimeZonesRestoreKey];
_originalAnswers = [coder decodeObjectOfClasses:decodableAnswerTypes forKey:_ORKOriginalAnswersRestoreKey];
_identifiersOfAnsweredSections = [coder decodeObjectOfClasses:[NSSet setWithArray:@[NSMutableSet.self, NSString.self]] forKey:_ORKAnsweredSectionIdentifiersRestoreKey];
}
- (void)removeInvalidSavedAnswers {
for (ORKFormItem *item in [self allFormItems]) {
id answer = _savedAnswers[item.identifier];
ORKAnswerFormat *answerFormat = item.impliedAnswerFormat;
ORKTextChoiceAnswerFormat *textChoiceAnswerFormat = ORKDynamicCast(answerFormat, ORKTextChoiceAnswerFormat);
if ([textChoiceAnswerFormat isAnswerInvalid:answer]) {
ORK_Log_Error("unexpected answer %@ on answerFormat of %@", answer, item.impliedAnswerFormat);
_savedAnswers[item.identifier] = nil;
_savedAnswerDates[item.identifier] = nil;
}
}
}
#pragma mark Rotate
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
for (ORKFormItemCell *cell in _formItemCells) {
[cell setExpectedLayoutWidth:size.width];
}
}
#pragma mark FormItemCell AnswerChanged Updates
- (void)answerChangedForIndexPath:(NSIndexPath *)indexPath {
// stash the itemIdentifier before buildDataSource
// We do not expect that editing a formItem would remove that same formItem from the dataSource
ORKTableCellItemIdentifier *itemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:indexPath];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell.superview == nil) {
return;
}
_skipped = NO;
[self updateButtonStates];
[self notifyDelegateOnResultChange];
BOOL skipRebuildDataSource = NO;
//For picker cells, wait for the "done" button to resign first responder before trying to rebuild
skipRebuildDataSource = skipRebuildDataSource || [cell isKindOfClass:[ORKFormItemPickerCell class]];
// For text cells, don't rebuild during typing
skipRebuildDataSource = skipRebuildDataSource || [cell isKindOfClass:[ORKFormItemTextCell class]];
// Only allow skipping if the answer was changed to something non-nil. The answer will be nullAnswer when users
// hit the 'clear' button on the textCell. Normally, the answer changes multiple times to different non-nil values
// before we're given the chance to process the new value through formItemCellDidResignFirstResponder
skipRebuildDataSource = skipRebuildDataSource && (_savedAnswers[itemIdentifier.formItemIdentifier] != ORKNullAnswerValue());
if (skipRebuildDataSource == YES) {
[self finishHandlingAnswerChangedForItemIdentifier:itemIdentifier];
} else {
__weak typeof(self) weakSelf = self;
[self buildDataSource:_diffableDataSource withCompletion:^{
[weakSelf finishHandlingAnswerChangedForItemIdentifier:itemIdentifier];
}];
}
}
- (void)finishHandlingAnswerChangedForItemIdentifier:(ORKTableCellItemIdentifier *)itemIdentifier {
// find the new indexPath of the saved itemIdentifier (almost certainly the same indexPath as before)
NSIndexPath *updatedIndexPath = [_diffableDataSource indexPathForItemIdentifier:itemIdentifier];
ORKFormItemCell *cell = [self.tableView cellForRowAtIndexPath:updatedIndexPath];
BOOL handled = NO;
// avoid auto-scrolling when typing in the ORKChoiceOtherViewCell changes the answer
handled = handled || [cell isKindOfClass:[ORKChoiceOtherViewCell class]];
handled = handled || [self scrollNextSectionToVisibleFromIndexPath:updatedIndexPath];
handled = handled || [self scrollFirstUnansweredSectionToVisibleFromIndexPath:updatedIndexPath];
handled = handled || [self scrollFooterToVisibleFromIndexPath:updatedIndexPath];
NSAssert(handled == YES, @"Answer change went unhandled");
if (handled && [self isContentSizeLargerThanFrame]) {
[_tableContainer resizeFooterToFitUsingMinHeight:YES];
}
// Delay updating answered sections so our autoscroll logic can check for the case where a section is answered for the first time
// This way we don't try to autoscroll if you've changed an answer in a section. Instead we only autoscroll the first time you put an answer in for a section.
[self updateAnsweredSections];
}
- (BOOL)scrollNextSectionToVisibleFromIndexPath:(NSIndexPath *)indexPath {
BOOL handledAutoScroll = NO;
__auto_type snapshot = [_diffableDataSource snapshot];
ORKTableCellItemIdentifier *cellItemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:indexPath];
NSString *formItemIdentifier = cellItemIdentifier.formItemIdentifier;
ORKFormItem *formItem = [self _formItemForFormItemIdentifier:formItemIdentifier];
id savedAnswer = self.savedAnswers[formItemIdentifier];
BOOL allowAutoScrolling = NO;
// allow autoscroll if you hit don't know
allowAutoScrolling = allowAutoScrolling || [savedAnswer isKindOfClass:[ORKDontKnowAnswer class]];
// allow autoscroll if the question is SES
allowAutoScrolling = allowAutoScrolling || (formItem.impliedAnswerFormat.questionType == ORKQuestionTypeSES);
ORKTextChoiceAnswerFormat *answerFormat = [self textChoiceAnswerFormatForIndexPath:indexPath];
if (answerFormat != nil) {
// allow scrolling for single-choice answer formats
allowAutoScrolling = allowAutoScrolling || (answerFormat.style == ORKChoiceAnswerStyleSingleChoice);
// allow scrolling after choosing an exclusive choice
allowAutoScrolling = allowAutoScrolling || answerFormat.textChoices[cellItemIdentifier.choiceIndex].exclusive;
}
if (allowAutoScrolling == YES) {
// only allow autoscroll to the next section if this was the first time providing an answer in this section
// this test works because we expect updateAnsweredSections runs *after* we do
NSString *sectionIdentifier = [snapshot sectionIdentifierForSectionContainingItemIdentifier:cellItemIdentifier];
allowAutoScrolling = allowAutoScrolling && ([_identifiersOfAnsweredSections containsObject:sectionIdentifier] == NO);
ORKFormItem *nextUnansweredFormItem = nil;
{
NSArray<ORKTableCellItemIdentifier *> *sectionCellItemIdentifiers = [snapshot itemIdentifiersInSectionWithIdentifier:sectionIdentifier];
// Get the index of the cell whose indexPath was just answered.
NSUInteger index = [sectionCellItemIdentifiers indexOfObject:cellItemIdentifier];
NSAssert(index != NSNotFound, @"Expected cellItemIdentifier to be present in section");
// find the next answerable unanswered formItem in this section
while (index < sectionCellItemIdentifiers.count) {
// Find the formItemIdentifier. Check to see whether there is an answer for this identifier.
NSString *testFormItemIdentifier = sectionCellItemIdentifiers[index].formItemIdentifier;
id testAnswer = self.savedAnswers[testFormItemIdentifier];
ORKFormItem *testFormItem = [self _formItemForFormItemIdentifier:testFormItemIdentifier];
// Find formItems that are answerable, but not yet answered
if ((testFormItem.impliedAnswerFormat != nil) && (testAnswer == nil)) {
nextUnansweredFormItem = testFormItem;
break;
}
index += 1;
}
}
// only allow autoscrolling if this formItem is the last unanswered answerable formItem in this section
allowAutoScrolling = allowAutoScrolling && (nextUnansweredFormItem == nil);
}
if (allowAutoScrolling == YES) {
// only allow autoscrolling to the next section if the next section exists
if ((indexPath.section + 1) < [snapshot numberOfSections]) {
[self autoScrollToNextSection:indexPath];
handledAutoScroll = YES;
} else {
// We would go to the next section, but we literally can't
// Let the caller come up with a backup plan
}
} else {
// allowAutoScrolling == NO means prevent autoscrolling completely
// so we claim we handled autoscroll
handledAutoScroll = YES;
}
return handledAutoScroll;
}
- (BOOL)scrollFirstUnansweredSectionToVisibleFromIndexPath:(NSIndexPath *)indexPath {
BOOL handled = NO;
BOOL shouldScroll = YES;
// only allow scrolling if this is the last section in the tableView
shouldScroll = shouldScroll && ((indexPath.section + 1) == [_tableView numberOfSections]);
if (shouldScroll == YES) {
if ([self allNonOptionalFormItemsHaveAnswers] == NO) {
[self scrollToFirstUnansweredSection];
handled = YES;
} else {
// we would scroll to the first unanswered section, but none exist
// not handled
}
} else {
// Decided we should not scroll at all
handled = YES;
}
return handled;
}
- (BOOL)scrollFooterToVisibleFromIndexPath:(NSIndexPath *)indexPath {
BOOL shouldScroll = YES;
// only allow scrolling if this is the last section in the tableView
shouldScroll = shouldScroll && ((indexPath.section + 1) == [_tableView numberOfSections]);
// only allow scrolling if all non-optional questions are answered
shouldScroll = shouldScroll && ([self allNonOptionalFormItemsHaveAnswers] == YES);
if (shouldScroll == YES) {
[self scrollToFooter];
}
// nothing we can't handle
return YES;
}
#pragma mark - ORKChoiceOtherViewCellDelegate
- (void)textChoiceOtherCellDidBecomeFirstResponder:(ORKChoiceOtherViewCell *)choiceOtherViewCell {
_currentFirstResponderCell = choiceOtherViewCell;
NSIndexPath *path = [_tableView indexPathForCell:choiceOtherViewCell];
if (path) {
ORKTableContainerView *tableContainer = _tableContainer;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, DelayBeforeAutoScroll * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[tableContainer scrollCellVisible:choiceOtherViewCell animated:YES];
});
}
}
- (void)updateTextChoiceOtherWithText:(NSString *)text choiceOtherCell:(ORKChoiceOtherViewCell *)choiceOtherViewCell {
if (_currentFirstResponderCell == choiceOtherViewCell) {
_currentFirstResponderCell = nil;
}
// we need to use `indexPathForRowAtPoint` because `indexPathForCell`
// will return nil if the cell is off the screen, which will happen if we are scrolling
NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:choiceOtherViewCell.center];
ORKTableCellItemIdentifier *itemIdentifier = [_diffableDataSource itemIdentifierForIndexPath:indexPath];
ORKFormItem *formItem = [self _formItemForFormItemIdentifier:itemIdentifier.formItemIdentifier];
ORKTextChoiceOther *textChoice = [[[formItem answerFormat] choices] objectAtIndex:itemIdentifier.choiceIndex];
ORK_Log_Debug("[FORMSTEP] textChoiceOtherCellDidResignFirstResponder found textChoice %@ with value of %@", textChoice, textChoice.textViewText);
id answer;
if (text.length > 0) {
textChoice.textViewText = text;
[self didSelectChoiceOtherViewCellWithItemIdentifier:itemIdentifier choiceOtherViewCell:choiceOtherViewCell];
answer = @[textChoice.textViewText];
} else {
textChoice.textViewText = nil;
if (!textChoice.textViewInputOptional) {
[choiceOtherViewCell setCellSelected:NO highlight:NO];
}
// textChoice doesn't have any custom text from the textView
// the answer should be the default text option
answer = @[textChoice.text];
}
if (answer) {
[self saveTextChoiceAnswer:answer
formItem:formItem
indexPath:indexPath
itemIdentifier:itemIdentifier];
}
[self resizeORKChoiceOtherViewCell:choiceOtherViewCell withTextChoice:textChoice];
}
- (void)textChoiceOtherCellDidChangeText:(NSString *)text choiceOtherCell:(ORKChoiceOtherViewCell *)choiceOtherViewCell {
[self updateTextChoiceOtherWithText:text choiceOtherCell:choiceOtherViewCell];
}
- (void)textChoiceOtherCellDidResignFirstResponder:(ORKChoiceOtherViewCell *)choiceOtherViewCell {
[self updateTextChoiceOtherWithText:choiceOtherViewCell.textView.text choiceOtherCell:choiceOtherViewCell];
}
- (void)saveTextChoiceAnswer:(id)answer
formItem:(ORKFormItem*)formItem
indexPath:(NSIndexPath*)indexPath
itemIdentifier:(ORKTableCellItemIdentifier*)itemIdentifier {
ORKTextChoiceAnswerFormat *textChoiceAnswerFormat = ORKDynamicCast(formItem.impliedAnswerFormat, ORKTextChoiceAnswerFormat);
ORKChoiceAnswerFormatHelper *helper = [[ORKChoiceAnswerFormatHelper alloc] initWithAnswerFormat:textChoiceAnswerFormat];
NSArray *selectedIndexes = [helper selectedIndexesForAnswer:answer];
// regenerate answer to pick up the changed text from choiceOtherViewCell
answer = [helper answerForSelectedIndexes:selectedIndexes];
_savedAnswers[itemIdentifier.formItemIdentifier] = answer;
[self answerChangedForIndexPath:indexPath];
}
#pragma mark - ORKlearnMoreStepViewControllerDelegate
- (void)learnMoreButtonPressedWithStep:(ORKLearnMoreInstructionStep *)learnMoreStep {
[self.taskViewController learnMoreButtonPressedWithStep:learnMoreStep fromStepViewController:self];
}
@end
@implementation ORKFormItem (FormStepViewControllerExtensions)
- (BOOL)requiresSingleSection {
ORKAnswerFormat *answerFormat = [self impliedAnswerFormat];
ORKQuestionType questionType = answerFormat.questionType;
NSArray *singleSectionTypes = @[@(ORKQuestionTypeBoolean),
@(ORKQuestionTypeSingleChoice),
@(ORKQuestionTypeMultipleChoice),
@(ORKQuestionTypeLocation),
@(ORKQuestionTypeSES)];
BOOL multiCellChoices = ([singleSectionTypes containsObject:@(questionType)] &&
NO == [answerFormat isKindOfClass:[ORKValuePickerAnswerFormat class]]);
BOOL scale = (questionType == ORKQuestionTypeScale);
// Items require individual section
if (multiCellChoices || scale) {
return YES;
}
return NO;
}
@end