Files
Pariece McKinney b14e5cfcb0 Public Release 3.0
2024-03-28 19:39:04 -04:00

902 lines
38 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 "ORKSurveyAnswerCellForText.h"
#import "ORKAnswerTextField.h"
#import "ORKAnswerTextView.h"
#import "ORKDontKnowButton.h"
#import "ORKAnswerFormat_Internal.h"
#import "ORKQuestionStep_Internal.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
static const CGFloat TextViewTopPadding = 8.0;
static const CGFloat TextViewBottomPadding = 16.0;
static const CGFloat TextViewMinimumHeight = 140.0;
static const CGFloat ClearTextButtonMinimumHeight = 20.0;
static const CGFloat ClearTextButtonRightPadding = 12.0;
static const CGFloat ErrorLabelTopPadding = 4.0;
static const CGFloat ErrorLabelBottomPadding = 10.0;
static const CGFloat StandardSpacing = 8.0;
static const CGFloat CellBottomPadding = 5.0;
static const CGFloat DontKnowButtonTopBottomPadding = 12.0;
static const CGFloat DividerViewTopPadding = 10.0;
@interface ORKSurveyAnswerCellForText () <UITextViewDelegate>
@property (nonatomic, strong) ORKAnswerTextView *textView;
@property (nonatomic, strong) UILabel *errorLabel;
@end
@implementation ORKSurveyAnswerCellForText {
NSInteger _maxLength;
NSString *_defaultTextAnswer;
UILabel *_textCountLabel;
UIButton *_clearTextViewButton;
ORKDontKnowButton *_dontKnowButton;
UIView *_bottomSeperatorView;
UIView *_dontKnowBackgroundView;
UIView *_dividerView;
NSMutableArray *_constraints;
BOOL _hideClearButton;
BOOL _hideCharacterCountLabel;
BOOL _shouldShowDontKnow;
}
- (void)applyAnswerFormat {
ORKAnswerFormat *answerFormat = [self.step.answerFormat impliedAnswerFormat];
if ([answerFormat isKindOfClass:[ORKTextAnswerFormat class]]) {
ORKTextAnswerFormat *textAnswerFormat = (ORKTextAnswerFormat *)answerFormat;
_maxLength = [textAnswerFormat maximumLength];
_hideClearButton = [textAnswerFormat hideClearButton];
_hideCharacterCountLabel = [textAnswerFormat hideCharacterCountLabel];
_defaultTextAnswer = textAnswerFormat.defaultTextAnswer;
self.textView.autocorrectionType = textAnswerFormat.autocorrectionType;
self.textView.autocapitalizationType = textAnswerFormat.autocapitalizationType;
self.textView.spellCheckingType = textAnswerFormat.spellCheckingType;
self.textView.keyboardType = textAnswerFormat.keyboardType;
self.textView.secureTextEntry = textAnswerFormat.secureTextEntry;
self.textView.textContentType = textAnswerFormat.textContentType;
if (@available(iOS 12.0, *)) {
self.textView.passwordRules = textAnswerFormat.passwordRules;
}
} else {
_maxLength = 0;
}
}
- (BOOL)becomeFirstResponder {
return [self.textView becomeFirstResponder];
}
- (void)setStep:(ORKQuestionStep *)step {
[super setStep:step];
[self applyAnswerFormat];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
self.layoutMargins = ORKStandardLayoutMarginsForTableViewCell(self);
}
- (void)prepareView {
NSMutableArray *accessibilityElements = [NSMutableArray new];
if (self.textView == nil ) {
self.preservesSuperviewLayoutMargins = NO;
self.layoutMargins = ORKStandardLayoutMarginsForTableViewCell(self);
self.textView = [[ORKAnswerTextView alloc] initWithFrame:CGRectZero];
self.textView.delegate = self;
self.textView.editable = YES;
self.textView.backgroundColor = [UIColor secondarySystemGroupedBackgroundColor];
[self addSubview:self.textView];
self.textView.placeholder = self.step.placeholder;
ORKEnableAutoLayoutForViews(@[_textView]);
[self applyAnswerFormat];
[self answerDidChange];
// Avoid exposing both this cell and its inner text view as elements to accessibility
// See also ORKCustomStepView -accessibilityElements
[accessibilityElements addObject:self.textView];
}
if (_bottomSeperatorView == nil) {
_bottomSeperatorView = [UIView new];
[_bottomSeperatorView setBackgroundColor:[UIColor separatorColor]];
_bottomSeperatorView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_bottomSeperatorView];
}
if (_textCountLabel == nil && _maxLength > 0) {
_textCountLabel = [UILabel new];
[_textCountLabel setTextColor:[UIColor labelColor]];
if (!_hideCharacterCountLabel) {
[accessibilityElements addObject:_textCountLabel];
[self updateTextCountLabel];
}
[_textCountLabel setHidden: _hideCharacterCountLabel];
_textCountLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview: _textCountLabel];
}
if (_clearTextViewButton == nil && !_hideClearButton) {
_clearTextViewButton = [UIButton new];
[_clearTextViewButton setTitle:ORKLocalizedString(@"BUTTON_CLEAR", nil) forState:UIControlStateNormal];
[_clearTextViewButton setBackgroundColor:[UIColor clearColor]];
[_clearTextViewButton setTitleColor:self.tintColor forState:UIControlStateNormal];
[_clearTextViewButton addTarget:self action:@selector(clearTextView) forControlEvents:UIControlEventTouchUpInside];
_clearTextViewButton.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview: _clearTextViewButton];
[accessibilityElements addObject:_clearTextViewButton];
}
if (_errorLabel == nil) {
_errorLabel = [UILabel new];
[_errorLabel setTextColor: [UIColor orangeColor]];
[self.errorLabel setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]];
_errorLabel.numberOfLines = 0;
_errorLabel.lineBreakMode = NSLineBreakByWordWrapping;
[_errorLabel setAdjustsFontForContentSizeCategory:YES];
_errorLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_errorLabel];
}
_shouldShowDontKnow = [[self textAnswerFormat] shouldShowDontKnowButton];
if (_shouldShowDontKnow) {
[self setupDontKnowButton];
}
self.accessibilityElements = [accessibilityElements copy];
accessibilityElements = nil;
[self setUpConstraints];
[super prepareView];
}
- (void)assignDefaultAnswer {
if (_defaultTextAnswer) {
[self ork_setAnswer:_defaultTextAnswer];
if (self.textView) {
self.textView.text = _defaultTextAnswer;
}
}
}
- (void)answerDidChange {
id answer = self.answer;
if (answer == [ORKDontKnowAnswer answer] && ![_dontKnowButton active]) {
[self dontKnowButtonWasPressed];
} else if (answer != [ORKDontKnowAnswer answer]) {
if (answer == ORKNullAnswerValue()) {
answer = nil;
}
_textView.text = (NSString *)answer;
[self assignDefaultAnswer];
}
}
- (void)setUpConstraints {
if (_constraints) {
[NSLayoutConstraint deactivateConstraints:_constraints];
}
_constraints = [NSMutableArray array];
//TextView Constraints
[_constraints addObject:[_textView.topAnchor constraintEqualToAnchor:self.topAnchor constant:TextViewTopPadding]];
[_constraints addObject:[_textView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:StandardSpacing]];
[_constraints addObject:[_textView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-StandardSpacing]];
NSLayoutConstraint *heightConstraint = [_textView.heightAnchor constraintGreaterThanOrEqualToConstant:TextViewMinimumHeight];
[heightConstraint setPriority:UILayoutPriorityRequired];
[_constraints addObject:heightConstraint];
UIView *bottomMostElement;
if (_textCountLabel && !_textCountLabel.isHidden) {
bottomMostElement = _textCountLabel;
}
if (_clearTextViewButton && !_clearTextViewButton.isHidden) {
bottomMostElement = _clearTextViewButton;
}
if (_errorLabel.attributedText && (!_textCountLabel || _textCountLabel.isHidden)) {
bottomMostElement = _errorLabel;
}
BOOL createEmptySpaceForPossibleErrorMessage = NO;
if (!bottomMostElement && _textCountLabel) {
bottomMostElement = _bottomSeperatorView;
createEmptySpaceForPossibleErrorMessage = YES;
}
if (_bottomSeperatorView && (bottomMostElement || createEmptySpaceForPossibleErrorMessage)) {
//SeperatorView Constraints
[_constraints addObject:[_bottomSeperatorView.topAnchor constraintEqualToAnchor:_textView.bottomAnchor constant:TextViewTopPadding]];
[_constraints addObject:[_bottomSeperatorView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor]];
[_constraints addObject:[_bottomSeperatorView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]];
[_constraints addObject:[_bottomSeperatorView.heightAnchor constraintEqualToConstant:1.0 / [UIScreen mainScreen].scale]];
}
if (bottomMostElement && !createEmptySpaceForPossibleErrorMessage) {
//TextCountLabel Constraints
if (_textCountLabel && !_textCountLabel.isHidden) {
[_constraints addObject:[_textCountLabel.topAnchor constraintEqualToAnchor:_bottomSeperatorView.bottomAnchor constant:TextViewBottomPadding]];
[_constraints addObject:[_textCountLabel.leadingAnchor constraintEqualToAnchor:_textView.leadingAnchor constant:StandardSpacing]];
[_constraints addObject:[_textCountLabel.heightAnchor constraintGreaterThanOrEqualToConstant:ClearTextButtonMinimumHeight]];
}
//ClearTextViewButton Constraints
if (_clearTextViewButton) {
[_constraints addObject:[_clearTextViewButton.topAnchor constraintEqualToAnchor:_bottomSeperatorView.bottomAnchor constant:TextViewBottomPadding]];
[_constraints addObject:[_clearTextViewButton.trailingAnchor constraintEqualToAnchor:_textView.trailingAnchor constant:-ClearTextButtonRightPadding]];
[_constraints addObject:[_clearTextViewButton.heightAnchor constraintGreaterThanOrEqualToConstant:ClearTextButtonMinimumHeight]];
}
//ErrorLabel Constraints
if (_errorLabel.attributedText && (_textCountLabel && _textCountLabel.isHidden)) {
[_constraints addObject:[_errorLabel.leadingAnchor constraintEqualToAnchor:_textView.leadingAnchor]];
[_constraints addObject:[_errorLabel.topAnchor constraintEqualToAnchor:_bottomSeperatorView.bottomAnchor constant:TextViewBottomPadding]];
[_errorLabel setContentCompressionResistancePriority:1000 forAxis:UILayoutConstraintAxisVertical];
[_constraints addObject:[_errorLabel.heightAnchor constraintGreaterThanOrEqualToConstant:ClearTextButtonMinimumHeight]];
if (_clearTextViewButton) {
[_constraints addObject:[_errorLabel.trailingAnchor constraintEqualToAnchor:_clearTextViewButton.leadingAnchor constant: -5.0]];
[_clearTextViewButton setContentCompressionResistancePriority:1000 forAxis:UILayoutConstraintAxisHorizontal];
} else {
[_constraints addObject:[_errorLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]];
}
}
} else if (!createEmptySpaceForPossibleErrorMessage) {
bottomMostElement = _textView;
}
if (_shouldShowDontKnow) {
[[_dontKnowBackgroundView.topAnchor constraintEqualToAnchor:_dividerView.topAnchor] setActive:YES];
[[_dontKnowBackgroundView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
[[_dontKnowBackgroundView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
[[_dontKnowBackgroundView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
CGFloat separatorHeight = 1.0 / [UIScreen mainScreen].scale;
if (createEmptySpaceForPossibleErrorMessage) {
[[_dividerView.topAnchor constraintEqualToAnchor:bottomMostElement.bottomAnchor constant:CellBottomPadding + TextViewBottomPadding + ClearTextButtonMinimumHeight] setActive:YES];
} else {
[[_dividerView.topAnchor constraintEqualToAnchor:bottomMostElement.bottomAnchor constant:DividerViewTopPadding] setActive:YES];
}
[[_dividerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
[[_dividerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:_dividerView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:separatorHeight];
constraint1.priority = UILayoutPriorityRequired - 1;
constraint1.active = YES;
[[_dontKnowButton.topAnchor constraintEqualToAnchor:_dividerView.bottomAnchor constant:DontKnowButtonTopBottomPadding] setActive:YES];
if (_dontKnowButton.dontKnowButtonStyle == ORKDontKnowButtonStyleStandard) {
[[_dontKnowButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
[[_dontKnowButton.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.leadingAnchor constant:StandardSpacing] setActive:YES];
[[_dontKnowButton.trailingAnchor constraintLessThanOrEqualToAnchor:self.trailingAnchor constant:-StandardSpacing] setActive:YES];
} else {
[[_dontKnowButton.leadingAnchor constraintEqualToAnchor:_textView.leadingAnchor] setActive:YES];
[[_dontKnowButton.trailingAnchor constraintEqualToAnchor:_textView.trailingAnchor] setActive:YES];
[_dontKnowButton setContentCompressionResistancePriority:1000 forAxis:UILayoutConstraintAxisVertical];
}
[_constraints addObject:[self.bottomAnchor constraintGreaterThanOrEqualToAnchor:_dontKnowButton.bottomAnchor constant:DontKnowButtonTopBottomPadding - StandardSpacing]];
} else if (createEmptySpaceForPossibleErrorMessage) {
[_constraints addObject:[self.bottomAnchor constraintGreaterThanOrEqualToAnchor:bottomMostElement.bottomAnchor constant:CellBottomPadding + TextViewBottomPadding + ClearTextButtonMinimumHeight]];
} else {
[_constraints addObject:[self.bottomAnchor constraintGreaterThanOrEqualToAnchor:bottomMostElement.bottomAnchor constant:CellBottomPadding]];
}
// Get full width layout
[_constraints addObject:[self.class fullWidthLayoutConstraint:_textView]];
[NSLayoutConstraint activateConstraints:_constraints];
}
- (void)setupDontKnowButton {
if(!_dontKnowBackgroundView) {
_dontKnowBackgroundView = [UIView new];
_dontKnowBackgroundView.userInteractionEnabled = YES;
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(dontKnowBackgroundViewPressed)];
[_dontKnowBackgroundView addGestureRecognizer:gestureRecognizer];
_dontKnowBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
}
if (!_dontKnowButton) {
_dontKnowButton = [ORKDontKnowButton new];
_dontKnowButton.customDontKnowButtonText = self.step.answerFormat.customDontKnowButtonText;
_dontKnowButton.dontKnowButtonStyle = self.step.answerFormat.dontKnowButtonStyle;
_dontKnowButton.translatesAutoresizingMaskIntoConstraints = NO;
[_dontKnowButton addTarget:self action:@selector(dontKnowButtonWasPressed) forControlEvents:UIControlEventTouchUpInside];
}
if (!_dividerView) {
_dividerView = [UIView new];
_dividerView.translatesAutoresizingMaskIntoConstraints = NO;
[_dividerView setBackgroundColor:[UIColor separatorColor]];
}
[self addSubview:_dontKnowBackgroundView];
[self addSubview:_dontKnowButton];
[self addSubview:_dividerView];
if (self.answer == [ORKDontKnowAnswer answer]) {
[self dontKnowButtonWasPressed];
}
}
+ (BOOL)shouldDisplayWithSeparators {
return NO;
}
- (void)textDidChange {
[self ork_setAnswer:(self.textView.text.length > 0) ? self.textView.text : ORKNullAnswerValue()];
if (self.textView.text.length > 0 && _dontKnowButton && [_dontKnowButton active]) {
[_dontKnowButton setActive:NO];
}
}
- (void)updateTextCountLabel {
if (_maxLength > 0) {
NSString *text = [[self.textView.text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@""];
NSString *textCountLabelText = [[NSString alloc] initWithFormat:@"%lu/%li", (unsigned long)text.length, (long)_maxLength];
_textCountLabel.text = textCountLabelText;
if (_errorLabel && _errorLabel.attributedText) {
[self removeErrorMessage];
}
}
}
- (void)clearTextView {
self.textView.text = @"";
[self textDidChange];
[self updateTextCountLabel];
}
- (void)dontKnowButtonWasPressed {
if (![_dontKnowButton active]) {
[_dontKnowButton setActive:YES];
[_textView setText:nil];
[_textView endEditing:YES];
[self textDidChange];
[self updateTextCountLabel];
[self ork_setAnswer: [ORKDontKnowAnswer answer]];
}
}
- (void)dontKnowBackgroundViewPressed {
if (_dontKnowButton && self.step.answerFormat.dontKnowButtonStyle == ORKDontKnowButtonStyleCircleChoice) {
[self dontKnowButtonWasPressed];
}
}
- (BOOL)shouldContinue {
ORKTextAnswerFormat *answerFormat = (ORKTextAnswerFormat *)[self.step impliedAnswerFormat];
if (self.answer != [ORKDontKnowAnswer answer] && ![answerFormat isAnswerValidWithString:self.textView.text]) {
[self showValidityAlertWithMessage:[[self.step impliedAnswerFormat] localizedInvalidValueStringWithAnswerString:self.answer]];
return NO;
}
return YES;
}
- (void)updateErrorLabelWithMessage:(NSString *)message {
NSString *separatorString = @":";
NSString *stringtoParse = message ? : ORKLocalizedString(@"RANGE_ALERT_TITLE", @"");
NSString *parsedString = [stringtoParse componentsSeparatedByString:separatorString].firstObject;
if (![self.accessibilityElements containsObject:self.errorLabel]) {
self.accessibilityElements = [self.accessibilityElements arrayByAddingObject:self.errorLabel];
}
NSString *errorMessage = [NSString stringWithFormat:@" %@", parsedString];
NSMutableAttributedString *fullString = [[NSMutableAttributedString alloc] initWithString:errorMessage];
NSTextAttachment *imageAttachment = [NSTextAttachment new];
UIImage *exclamationMarkImage = [UIImage systemImageNamed:@"exclamationmark.circle.fill"];
UIImageSymbolConfiguration *imageConfig = [UIImageSymbolConfiguration configurationWithTextStyle:UIFontTextStyleFootnote];
UIImage *configuredImage = [exclamationMarkImage imageByApplyingSymbolConfiguration:imageConfig];
imageAttachment.image = [configuredImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
NSAttributedString *imageString = [NSAttributedString attributedStringWithAttachment:imageAttachment];
[fullString insertAttributedString:imageString atIndex:0];
self.errorLabel.attributedText = fullString;
[self setUpConstraints];
}
- (void)removeErrorMessage {
self.errorLabel.attributedText = nil;
if ([self.accessibilityElements containsObject:self.errorLabel]) {
NSMutableArray *tempArray = [self.accessibilityElements mutableCopy];
[tempArray removeObject:self.errorLabel];
self.accessibilityElements = [tempArray copy];
}
[self setUpConstraints];
}
- (ORKTextAnswerFormat *)textAnswerFormat {
ORKTextAnswerFormat *textAnswerFormat = (ORKTextAnswerFormat *)self.step.answerFormat;
if (![textAnswerFormat isKindOfClass:[ORKTextAnswerFormat class]]) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"the ORKSurveyAnswerCellForText's answerFormat must be a ORKTextAnswerFormat"
userInfo:nil];
}
return textAnswerFormat;
}
#pragma mark - UITextViewDelegate
- (void)textViewDidEndEditing:(UITextView *)textView {
[self textDidChange];
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
NSString *string = [textView.text stringByReplacingCharactersInRange:range withString:text];
// Only need to validate the text if the user enters a character other than a backspace.
// For example, if the `textView.text = researchki` and the `string = researchkit`.
if (textView.text.length < string.length) {
string = [[string componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@""];
if (_maxLength > 0 && string.length > _maxLength) {
if (_textCountLabel.isHidden) {
[self updateErrorLabelWithMessage:ORKLocalizedString(@"MAX_WORD_COUNT_ERROR", @"")];
} else {
[self showValidityAlertWithMessage:[[self.step impliedAnswerFormat] localizedInvalidValueStringWithAnswerString:string]];
}
return NO;
} else if (_errorLabel.attributedText) {
[self removeErrorMessage];
}
}
return YES;
}
- (void)textViewDidChange:(UITextView *)textView {
[self textDidChange];
[self updateTextCountLabel];
}
+ (CGFloat)suggestedCellHeightForView:(UIView *)view {
return 180.0;
}
@end
@interface ORKSurveyAnswerCellForTextField ()
@property (nonatomic, strong) ORKAnswerTextField *textField;
@property (nonatomic, strong) UILabel *errorLabel;
@end
@implementation ORKSurveyAnswerCellForTextField {
NSMutableArray *constraints;
NSString *_defaultTextAnswer;
ORKDontKnowButton *_dontKnowButton;
UIView *_dontKnowBackgroundView;
UIView *_dividerView;
BOOL _shouldShowDontKnow;
}
- (BOOL)becomeFirstResponder {
return [self.textField becomeFirstResponder];
}
- (void)textFieldCell_initialize {
ORKAnswerFormat *answerFormat = [self.step impliedAnswerFormat];
ORKTextAnswerFormat *textAnswerFormat = ORKDynamicCast(answerFormat, ORKTextAnswerFormat);
_defaultTextAnswer = textAnswerFormat.defaultTextAnswer;
_textField = [[ORKAnswerTextField alloc] initWithFrame:CGRectZero];
_textField.text = @"";
NSString *placeholder = textAnswerFormat.placeholder ? :
(self.step.placeholder ? : ORKLocalizedString(@"PLACEHOLDER_TEXT_OR_NUMBER", nil));
_textField.placeholder = placeholder;
_textField.textAlignment = NSTextAlignmentNatural;
_textField.delegate = self;
_textField.keyboardType = UIKeyboardTypeDefault;
[_textField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
self.accessibilityElements = @[_textField];
[self addSubview:_textField];
ORKEnableAutoLayoutForViews(@[_textField]);
if (_errorLabel == nil) {
_errorLabel = [UILabel new];
[_errorLabel setTextColor: [UIColor redColor]];
[self.errorLabel setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]];
_errorLabel.numberOfLines = 0;
_errorLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_errorLabel];
}
_shouldShowDontKnow = NO;
if ([[self textAnswerFormat] shouldShowDontKnowButton]) {
_shouldShowDontKnow = YES;
[self setupDontKnowButton];
self.accessibilityElements = @[_dontKnowButton];
}
[self setUpConstraints];
}
- (void)assignDefaultAnswer {
if (_defaultTextAnswer) {
[self ork_setAnswer:_defaultTextAnswer];
if (self.textField) {
self.textField.text = _defaultTextAnswer;
}
}
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
self.contentView.layoutMargins = ORKStandardLayoutMarginsForTableViewCell(self);
}
- (void)setUpConstraints {
if (constraints != nil) {
[NSLayoutConstraint deactivateConstraints:constraints];
}
constraints = [NSMutableArray new];
// textField constraints
[[_textField.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:StandardSpacing] setActive:YES];
[[_textField.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-StandardSpacing] setActive:YES];
[[_textField.topAnchor constraintEqualToAnchor:self.topAnchor constant:0.0] setActive:YES];
[[_textField.bottomAnchor constraintEqualToAnchor:_errorLabel.topAnchor constant:ErrorLabelTopPadding] setActive:YES];
[_textField setContentCompressionResistancePriority:1000 forAxis:UILayoutConstraintAxisVertical];
// errorLabel constraints
[[_errorLabel.leadingAnchor constraintEqualToAnchor:_textField.leadingAnchor constant:0.0] setActive:YES];
[[_errorLabel.trailingAnchor constraintEqualToAnchor:_textField.trailingAnchor constant:0.0] setActive:YES];
// dontKnowButton constraints
if (_shouldShowDontKnow) {
[[_dontKnowBackgroundView.topAnchor constraintEqualToAnchor:_dividerView.topAnchor] setActive:YES];
[[_dontKnowBackgroundView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
[[_dontKnowBackgroundView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
[[_dontKnowBackgroundView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
CGFloat separatorHeight = 1.0 / [UIScreen mainScreen].scale;
[[_dividerView.topAnchor constraintEqualToAnchor:_errorLabel.bottomAnchor constant:DividerViewTopPadding] setActive:YES];
[[_dividerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor] setActive:YES];
[[_dividerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] setActive:YES];
NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:_dividerView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:separatorHeight];
constraint1.priority = UILayoutPriorityRequired - 1;
constraint1.active = YES;
[[_dontKnowButton.topAnchor constraintEqualToAnchor:_dividerView.bottomAnchor constant:DontKnowButtonTopBottomPadding] setActive:YES];
if (_dontKnowButton.dontKnowButtonStyle == ORKDontKnowButtonStyleStandard) {
[[_dontKnowButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
[[_dontKnowButton.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.leadingAnchor constant:StandardSpacing] setActive:YES];
[[_dontKnowButton.trailingAnchor constraintLessThanOrEqualToAnchor:self.trailingAnchor constant:-StandardSpacing] setActive:YES];
} else {
[[_dontKnowButton.leadingAnchor constraintEqualToAnchor:_textField.leadingAnchor] setActive:YES];
[[_dontKnowButton.trailingAnchor constraintEqualToAnchor:_textField.trailingAnchor] setActive:YES];
[_dontKnowButton setContentCompressionResistancePriority:1000 forAxis:UILayoutConstraintAxisVertical];
}
[[self.bottomAnchor constraintGreaterThanOrEqualToAnchor:_dontKnowButton.bottomAnchor constant:DontKnowButtonTopBottomPadding - StandardSpacing] setActive:YES];
} else {
[[self.bottomAnchor constraintEqualToAnchor:_errorLabel.bottomAnchor constant:ErrorLabelBottomPadding] setActive:YES];
}
// Get a full width layout
[constraints addObject:[self.class fullWidthLayoutConstraint:_textField]];
[NSLayoutConstraint activateConstraints:constraints];
}
- (void)setupDontKnowButton {
if(!_dontKnowBackgroundView) {
_dontKnowBackgroundView = [UIView new];
_dontKnowBackgroundView.userInteractionEnabled = YES;
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(dontKnowBackgroundViewPressed)];
[_dontKnowBackgroundView addGestureRecognizer:gestureRecognizer];
_dontKnowBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
}
if (!_dontKnowButton) {
_dontKnowButton = [ORKDontKnowButton new];
_dontKnowButton.customDontKnowButtonText = self.step.answerFormat.customDontKnowButtonText;
_dontKnowButton.dontKnowButtonStyle = self.step.answerFormat.dontKnowButtonStyle;
_dontKnowButton.translatesAutoresizingMaskIntoConstraints = NO;
[_dontKnowButton addTarget:self action:@selector(dontKnowButtonWasPressed) forControlEvents:UIControlEventTouchUpInside];
}
if (!_dividerView) {
_dividerView = [UIView new];
_dividerView.translatesAutoresizingMaskIntoConstraints = NO;
[_dividerView setBackgroundColor:[UIColor separatorColor]];
}
[self addSubview:_dontKnowBackgroundView];
[self addSubview:_dontKnowButton];
[self addSubview:_dividerView];
if (self.answer == [ORKDontKnowAnswer answer]) {
[self dontKnowButtonWasPressed];
}
}
+ (BOOL)shouldDisplayWithSeparators {
return NO;
}
- (void)prepareView {
if (self.textField == nil ) {
[self textFieldCell_initialize];
}
[self answerDidChange];
[super prepareView];
}
- (BOOL)shouldContinue {
ORKTextAnswerFormat *answerFormat = (ORKTextAnswerFormat *)[self.step impliedAnswerFormat];
if (![_dontKnowButton active] && ![answerFormat isAnswerValidWithString:self.textField.text]) {
[self updateErrorLabelWithMessage:[[self.step impliedAnswerFormat] localizedInvalidValueStringWithAnswerString:self.textField.text]];
return NO;
}
return YES;
}
- (void)dontKnowButtonWasPressed {
if (![_dontKnowButton active]) {
[_dontKnowButton setActive:YES];
[_textField setText:nil];
[_textField endEditing:YES];
[self ork_setAnswer:[ORKDontKnowAnswer answer]];
if (self.errorLabel.attributedText) {
[self removeErrorMessage];
}
}
}
- (void)dontKnowBackgroundViewPressed {
if (_dontKnowButton && self.step.answerFormat.dontKnowButtonStyle == ORKDontKnowButtonStyleCircleChoice) {
[self dontKnowButtonWasPressed];
}
}
- (void)answerDidChange {
id answer = self.answer;
ORKAnswerFormat *answerFormat = [self.step impliedAnswerFormat];
ORKTextAnswerFormat *textFormat = (ORKTextAnswerFormat *)answerFormat;
if (textFormat) {
self.textField.autocorrectionType = textFormat.autocorrectionType;
self.textField.autocapitalizationType = textFormat.autocapitalizationType;
self.textField.spellCheckingType = textFormat.spellCheckingType;
self.textField.keyboardType = textFormat.keyboardType;
self.textField.secureTextEntry = textFormat.secureTextEntry;
self.textField.textContentType = textFormat.textContentType;
if (@available(iOS 12.0, *)) {
self.textField.passwordRules = textFormat.passwordRules;
}
}
if (answer == [ORKDontKnowAnswer answer] && ![_dontKnowButton active]) {
[self dontKnowButtonWasPressed];
} else if (answer != [ORKDontKnowAnswer answer]) {
NSString *displayValue = (answer && answer != ORKNullAnswerValue()) ? answer : nil;
if (displayValue == nil && _defaultTextAnswer) {
[self assignDefaultAnswer];
} else {
self.textField.text = displayValue;
}
}
}
- (void)textFieldDidChange:(UITextField *)textField {
[self checkTextAndSetAnswer];
}
- (void)checkTextAndSetAnswer {
NSString *text = self.textField.text;
ORKTextAnswerFormat *answerFormat = (ORKTextAnswerFormat *)[self.step impliedAnswerFormat];
if (text.length && [answerFormat isAnswerValidWithString:text]) {
[self ork_setAnswer:text];
} else {
[self ork_setAnswer:ORKNullAnswerValue()];
[self removeErrorMessage];
}
if (_dontKnowButton && [_dontKnowButton active]) {
[_dontKnowButton setActive:NO];
}
}
- (void)updateErrorLabelWithMessage:(NSString *)message {
NSString *separatorString = @":";
NSString *stringtoParse = message ? : ORKLocalizedString(@"RANGE_ALERT_TITLE", @"");
NSString *parsedString = [stringtoParse componentsSeparatedByString:separatorString].firstObject;
if (![self.accessibilityElements containsObject:self.errorLabel]) {
self.accessibilityElements = [self.accessibilityElements arrayByAddingObject:self.errorLabel];
}
NSString *errorMessage = [NSString stringWithFormat:@" %@", parsedString];
NSMutableAttributedString *fullString = [[NSMutableAttributedString alloc] initWithString:errorMessage];
NSTextAttachment *imageAttachment = [NSTextAttachment new];
UIImageSymbolConfiguration *imageConfig = [UIImageSymbolConfiguration configurationWithPointSize:12 weight:UIImageSymbolWeightRegular scale:UIImageSymbolScaleMedium];
UIImage *exclamationMarkImage = [UIImage systemImageNamed:@"exclamationmark.circle"];
UIImage *configuredImage = [exclamationMarkImage imageByApplyingSymbolConfiguration:imageConfig];
imageAttachment.image = [configuredImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
NSAttributedString *imageString = [NSAttributedString attributedStringWithAttachment:imageAttachment];
[fullString insertAttributedString:imageString atIndex:0];
self.errorLabel.attributedText = fullString;
[self setUpConstraints];
}
- (void)removeErrorMessage {
self.errorLabel.attributedText = nil;
if ([self.accessibilityElements containsObject:self.errorLabel]) {
NSMutableArray *tempArray = [self.accessibilityElements mutableCopy];
[tempArray removeObject:self.errorLabel];
self.accessibilityElements = [tempArray copy];
}
}
- (ORKTextAnswerFormat *)textAnswerFormat {
ORKTextAnswerFormat *textAnswerFormat = (ORKTextAnswerFormat *)self.step.answerFormat;
if (![textAnswerFormat isKindOfClass:[ORKTextAnswerFormat class]]) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"the ORKSurveyAnswerCellForTextFields's answerFormat must be a ORKTextAnswerFormat"
userInfo:nil];
}
return textAnswerFormat;
}
#pragma mark - UITextFieldDelegate
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
ORKAnswerFormat *impliedFormat = [self.step impliedAnswerFormat];
NSAssert([impliedFormat isKindOfClass:[ORKTextAnswerFormat class]], @"answerFormat should be ORKTextAnswerFormat type instance.");
NSString *text = [textField.text stringByReplacingCharactersInRange:range withString:string];
// Only need to validate the text if the user enters a character other than a backspace.
// For example, if the `textField.text = researchki` and the `text = researchkit`.
if (textField.text.length < text.length) {
text = [[text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@""];
NSInteger maxLength = [(ORKTextAnswerFormat *)impliedFormat maximumLength];
if (maxLength > 0 && text.length > maxLength) {
[self updateErrorLabelWithMessage:[[self.step impliedAnswerFormat] localizedInvalidValueStringWithAnswerString:text]];
return NO;
}
}
[self checkTextAndSetAnswer];
return YES;
}
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
return YES;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[self.textField resignFirstResponder];
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
NSString *text = self.textField.text;
ORKTextAnswerFormat *answerFormat = (ORKTextAnswerFormat *)[self.step impliedAnswerFormat];
if (text.length && ![answerFormat isAnswerValidWithString:text]) {
[self updateErrorLabelWithMessage:[[self.step impliedAnswerFormat] localizedInvalidValueStringWithAnswerString:self.textField.text]];
}
}
@end