/* Copyright (c) 2019, 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 "ORKStepContentView_Private.h" #import "ORKTitleLabel.h" #import "ORKBodyLabel.h" #import "ORKBodyItem.h" #import "ORKCompletionCheckmarkView.h" #import "ORKBodyContainerView.h" #import "ORKSkin.h" #import "UIImageView+ResearchKit.h" /* +_________________________+ | |<-------------_stepContentView | +-------+ | | | _icon | | | | | | | +-------+ | | | | +---------------------+ | | | _titleLabel | | | |_____________________| | | | | +---------------------+ | | | _textLabel | | | |_____________________| | | | | +---------------------+ | | | _detailTextLabel | | | |_____________________| | | | | +---------------------+ | | | |<-------------_bodyContainerView: UIstackView | | +-----------------+ | | | | | | | | | | |--Title | | | | | |--Text |<-------------- BodyItemStyleText | | |--LearnMore | | | | | |_________________| | | | | | | | | +---+-------------+ | | | | | |--Title | | | | | | o |--Text |<-------------- BodyItemStyleBullet | | | |--LearnMore | | | | | |___|_____________| | | | |_____________________| | | | | +---------------------+ | | | _centeredVertically-| | | | ImageView | | | |_____________________| | |_________________________| */ static const CGFloat ORKStepContentIconImageViewToTitleLabelPadding = 15.0; static const CGFloat ORKStepContentIconToBodyTopPaddingStandard = 20.0; static const CGFloat ORKStepContentIconToBulletTopPaddingStandard = 20.0; static const CGFloat ORKStepContentTagPaddingTop = 15.0; static const CGFloat ORKStepContentBottomPadding = 35.0; NSString * const ORKStepBodyContainerViewAccessibilityIdentifier = @"ORKStepBodyContainerView"; typedef NS_CLOSED_ENUM(NSInteger, ORKUpdateConstraintSequence) { ORKUpdateConstraintSequenceTopContentImageView = 0, ORKUpdateConstraintSequenceIconImageView, ORKUpdateConstraintSequenceTitleLabel, ORKUpdateConstraintSequenceTextLabel, ORKUpdateConstraintSequenceDetailTextLabel, ORKUpdateConstraintSequenceBodyContainerView, ORKUpdateConstraintSequenceCenteredVerticallyImageView } ORK_ENUM_AVAILABLE; @interface ORKStepContentView() @end @implementation ORKStepContentView { CGFloat _additionalTopPaddingForTopLabel; CGFloat _leftRightPadding; NSMutableArray *_updatedConstraints; NSArray *_topContentImageViewConstraints; NSArray *_iconImageViewConstraints; NSArray *_textLabelConstraints; NSArray *_detailTextLabelConstraints; NSArray *_centeredVerticallyImageViewContraints; NSMutableArray *_leftRightPaddingConstraints; NSLayoutConstraint *_iconImageViewTopConstraint; NSLayoutConstraint *_titleLabelTopConstraint; NSLayoutConstraint *_textLabelTopConstraint; NSLayoutConstraint *_detailTextLabelTopConstraint; NSLayoutConstraint *_bodyContainerViewTopConstraint; NSLayoutConstraint *_centeredVerticallyImageViewTopConstraint; NSArray *_bodyContainerLeftRightConstraints; NSLayoutConstraint *_stepContentBottomConstraint; ORKCompletionCheckmarkView *_completionCheckmarkView; BOOL useStandardTextAndFormPadding; } - (instancetype)init { self = [super init]; if (self) { [self setupContentView]; } return self; } - (instancetype)initWithStandardPadding { self = [super init]; if (self) { useStandardTextAndFormPadding = true; [self setupContentView]; } return self; } - (void)setupContentView { [self setupUpdatedConstraints]; [self setStepContentViewBottomConstraint]; _leftRightPadding = ORKStepContainerLeftRightPaddingForWindow(self.window); } // top content image - (void)setStepTopContentImage:(UIImage *)stepTopContentImage { _stepTopContentImage = stepTopContentImage; // 1.) nil Image; updateConstraints if (!stepTopContentImage && _topContentImageView) { [_topContentImageView removeFromSuperview]; _topContentImageView = nil; [self deactivateTopContentImageViewConstraints]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceTopContentImageView]; [self setNeedsUpdateConstraints]; } // 2.) First Image; updateConstraints if (stepTopContentImage && !_topContentImageView) { [self setupTopContentImageView]; _topContentImageView.image = [self topContentAndAuxiliaryImage]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceTopContentImageView]; [self setNeedsUpdateConstraints]; } // 3.) >= second Image; if (stepTopContentImage && _topContentImageView) { _topContentImageView.image = [self topContentAndAuxiliaryImage]; } [self updateViewColors]; [[NSNotificationCenter defaultCenter] postNotificationName:ORKStepTopContentImageChangedKey object:nil]; } - (void)setShouldAutomaticallyAdjustImageTintColor:(BOOL)shouldAutomaticallyAdjustImageTintColor { _shouldAutomaticallyAdjustImageTintColor = shouldAutomaticallyAdjustImageTintColor; [self updateViewColors]; } - (void)updateViewColors { if (!_shouldAutomaticallyAdjustImageTintColor) { return; } // this requires the image to be set to the UIImageRenderingModeAlwaysTemplate rendering mode // in the UIImageRenderingModeAlwaysOriginal rendering mode, tintColor does not apply [_topContentImageView updateRenderingModeForUserInterfaceStyle:self.traitCollection.userInterfaceStyle]; [_iconImageView updateRenderingModeForUserInterfaceStyle:self.traitCollection.userInterfaceStyle]; UIColor *imageViewTintColor = self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? [UIColor whiteColor] : nil; _topContentImageView.tintColor = imageViewTintColor; _iconImageView.tintColor = imageViewTintColor; } - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; [self updateViewColors]; } - (void)setAuxiliaryImage:(UIImage *)auxiliaryImage { _auxiliaryImage = auxiliaryImage; if (_stepTopContentImage) { _topContentImageView.image = [self topContentAndAuxiliaryImage]; } } - (UIImage *)topContentAndAuxiliaryImage { if (!_auxiliaryImage) { return _stepTopContentImage; } CGSize size = _auxiliaryImage.size; UIGraphicsBeginImageContext(size); CGRect rect = CGRectMake(0.0, 0.0, size.width, size.height); [_auxiliaryImage drawInRect:rect]; [_stepTopContentImage drawInRect:rect blendMode:kCGBlendModeNormal alpha:1.0]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } - (void)setupTopContentImageView { if (!_topContentImageView) { _topContentImageView = [UIImageView new]; } _topContentImageView.contentMode = UIViewContentModeScaleAspectFit; [_topContentImageView setBackgroundColor:ORKColor(ORKTopContentImageViewBackgroundColorKey)]; [_topContentImageView setClipsToBounds:YES]; [self addSubview:_topContentImageView]; [self setTopContentImageViewConstraints]; } - (void)setTopContentImageViewConstraints { _topContentImageView.translatesAutoresizingMaskIntoConstraints = NO; _topContentImageViewConstraints = @[ [NSLayoutConstraint constraintWithItem:_topContentImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_topContentImageView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_topContentImageView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_topContentImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:ORKStepContainerTopContentHeightForWindow(self.window)] ]; [_updatedConstraints addObjectsFromArray:_topContentImageViewConstraints]; } - (void)deactivateTopContentImageViewConstraints { [self deactivateConstraints:_topContentImageViewConstraints]; _topContentImageViewConstraints = nil; } // icon image - (void)setTitleIconImage:(UIImage *)titleIconImage { _titleIconImage = titleIconImage; if (!titleIconImage && _iconImageView) { [_iconImageView removeFromSuperview]; _iconImageView = nil; [self deactivateIconImageViewConstraints]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceIconImageView]; [self setNeedsUpdateConstraints]; } if (titleIconImage && !_iconImageView) { [self setupIconImageView]; _iconImageView.image = titleIconImage; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceIconImageView]; [self setNeedsUpdateConstraints]; } if (titleIconImage && _iconImageView) { _iconImageView.image = titleIconImage; } } - (void)updateTitleLabelTopConstraint { if (_titleLabelTopConstraint && _titleLabelTopConstraint.isActive) { [NSLayoutConstraint deactivateConstraints:@[_titleLabelTopConstraint]]; } if ([_updatedConstraints containsObject:_titleLabelTopConstraint]) { [_updatedConstraints removeObject:_titleLabelTopConstraint]; } [self setTitleLabelTopConstraint]; if (_titleLabelTopConstraint) { [_updatedConstraints addObject:_titleLabelTopConstraint]; } } - (void)setupIconImageView { if (!_iconImageView) { _iconImageView = [UIImageView new]; } _iconImageView.contentMode = UIViewContentModeScaleAspectFit; [self addSubview:_iconImageView]; [self updateViewColors]; [self setIconImageViewConstraints]; } - (void)setIconImageViewTopConstraint { if (_iconImageView) { _iconImageViewTopConstraint = [NSLayoutConstraint constraintWithItem:_iconImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_topContentImageView ? : self attribute:_topContentImageView ? NSLayoutAttributeBottom : NSLayoutAttributeTop multiplier:1.0 constant:ORKStepContentIconImageViewToTitleLabelPadding]; // This needs to match what the text padding is } } - (void)updateIconImageViewTopConstraint { if (_iconImageViewTopConstraint) { [self deactivateConstraints:@[_iconImageViewTopConstraint]]; } [self setIconImageViewTopConstraint]; if (_iconImageViewTopConstraint) { [_updatedConstraints addObject:_iconImageViewTopConstraint]; } } - (void)setIconImageViewConstraints { _iconImageView.translatesAutoresizingMaskIntoConstraints = NO; [self setIconImageViewTopConstraint]; _iconImageViewConstraints = @[ _iconImageViewTopConstraint, [NSLayoutConstraint constraintWithItem:_iconImageView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:ORKStepContentIconImageViewDimension], [NSLayoutConstraint constraintWithItem:_iconImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:ORKStepContentIconImageViewDimension]]; [_updatedConstraints addObjectsFromArray:_iconImageViewConstraints]; [self setNeedsUpdateConstraints]; } - (void)deactivateIconImageViewConstraints { [self deactivateConstraints:_iconImageViewConstraints]; _iconImageViewConstraints = nil; } // step title - (void)setStepTitle:(NSString *)stepTitle { _stepTitle = stepTitle; if (!_titleLabel) { [self setupTitleLabel]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceTitleLabel]; [self setNeedsUpdateConstraints]; } NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; [paragraphStyle setHyphenationFactor:0.5]; [paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping]; if (@available(iOS 14.0, *)) { [paragraphStyle setLineBreakStrategy:NSLineBreakStrategyPushOut]; } NSDictionary *hyphenAttribute = @{NSParagraphStyleAttributeName : paragraphStyle}; NSAttributedString *attributedStepTitle = [[NSAttributedString alloc] initWithString:stepTitle ?: @"" attributes:hyphenAttribute]; [_titleLabel setAttributedText:attributedStepTitle]; } - (void)setupTitleLabel { if (!_titleLabel) { _titleLabel = [ORKTitleLabel new]; } _titleLabel.numberOfLines = 0; _titleLabel.textAlignment = _stepHeaderTextAlignment; _titleLabel.accessibilityIdentifier = @"ORKStepContentView_titleLabel"; [self addSubview:_titleLabel]; [self setupTitleLabelConstraints]; [self setContainerLeftRightConstraints]; } - (void)setupTitleLabelConstraints { _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; [self setTitleLabelTopConstraint]; [_updatedConstraints addObjectsFromArray:@[ _titleLabelTopConstraint ]]; [self setNeedsUpdateConstraints]; } - (void)setTitleLabelTopConstraint { if (_titleLabel) { id topItem; NSLayoutAttribute attribute; CGFloat constant; if (_iconImageView) { topItem = _iconImageView; attribute = NSLayoutAttributeBottom; constant = ORKStepContentIconImageViewToTitleLabelPadding; } else if (_topContentImageView) { topItem = _topContentImageView; attribute = NSLayoutAttributeBottom; constant = ORKStepContentIconImageViewToTitleLabelPadding; } else { topItem = self; attribute = NSLayoutAttributeTop; constant = _customTopPadding ? [_customTopPadding floatValue] : ORKStepContentIconImageViewToTitleLabelPadding; } _titleLabelTopConstraint = [NSLayoutConstraint constraintWithItem:_titleLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:topItem attribute:attribute multiplier:1.0 constant:constant]; } } - (void)setCustomTopPadding:(NSNumber *)customTopPadding { _customTopPadding = customTopPadding; [self updateTitleLabelTopConstraint]; } // step text - (void)setStepText:(NSString *)stepText { _stepText = stepText; if (stepText && !_textLabel) { [self setupTextLabel]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceTextLabel]; [self setNeedsUpdateConstraints]; [_textLabel setText:stepText]; } else if (stepText && _textLabel) { [_textLabel setText:_stepText]; } else if (!stepText) { [_textLabel removeFromSuperview]; _textLabel = nil; [self deactivateTextLabelConstraints]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceTextLabel]; [self setNeedsUpdateConstraints]; } } - (void)setupTextLabel { if (!_textLabel) { _textLabel = [ORKBodyLabel new]; } _textLabel.textAlignment = _stepHeaderTextAlignment; _textLabel.numberOfLines = 0; _textLabel.lineBreakMode = NSLineBreakByWordWrapping; _textLabel.accessibilityIdentifier = @"ORKStepContentView_textLabel"; [self addSubview:_textLabel]; [self setupTextLabelConstraints]; [self setContainerLeftRightConstraints]; } - (void)setupTextLabelConstraints { _textLabel.translatesAutoresizingMaskIntoConstraints = NO; [self setTextLabelTopConstraint]; _textLabelConstraints = @[_textLabelTopConstraint]; [_updatedConstraints addObjectsFromArray:_textLabelConstraints]; [self setNeedsUpdateConstraints]; } - (void)setTextLabelTopConstraint { if (_textLabel) { id topItem; NSLayoutAttribute attribute; CGFloat constant; if (_titleLabel) { topItem = _titleLabel; attribute = NSLayoutAttributeBottom; constant = ORKStepContainerTitleToBodyTopPaddingForWindow(self.window); } else if (_iconImageView) { topItem = _iconImageView; attribute = NSLayoutAttributeBottom; constant = ORKStepContentIconToBodyTopPaddingStandard; } else if (_topContentImageView) { topItem = _topContentImageView; attribute = NSLayoutAttributeBottom; constant = ORKStepContainerFirstItemTopPaddingForWindow(self.window); } else { topItem = self; attribute = NSLayoutAttributeTop; constant = ORKStepContainerFirstItemTopPaddingForWindow(self.window) + _additionalTopPaddingForTopLabel; } _textLabelTopConstraint = [NSLayoutConstraint constraintWithItem:_textLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:topItem attribute:attribute multiplier:1.0 constant:constant]; } } - (void)updateTextLabelTopConstraint { if (_textLabelTopConstraint) { [self deactivateConstraints:@[_textLabelTopConstraint]]; } [self setTextLabelTopConstraint]; if (_textLabelTopConstraint) { [_updatedConstraints addObject:_textLabelTopConstraint]; } } - (void)deactivateTextLabelConstraints { [self deactivateConstraints:_textLabelConstraints]; _textLabelConstraints = nil; } // step detail text - (void)setStepDetailText:(NSString *)stepDetailText { _stepDetailText = stepDetailText; if (stepDetailText && !_detailTextLabel) { [self setupDetailTextLabel]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceDetailTextLabel]; [self setNeedsUpdateConstraints]; [_detailTextLabel setText:stepDetailText]; } else if (stepDetailText && _detailTextLabel) { [_detailTextLabel setText:_stepDetailText]; } else if (!stepDetailText) { [_detailTextLabel removeFromSuperview]; _detailTextLabel = nil; [self deactivateDetailTextLabelConstraints]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceDetailTextLabel]; [self setNeedsUpdateConstraints]; } } - (void)setupDetailTextLabel { if (!_detailTextLabel) { _detailTextLabel = [ORKBodyLabel new]; } _detailTextLabel.textAlignment = _stepHeaderTextAlignment; _detailTextLabel.numberOfLines = 0; _detailTextLabel.lineBreakMode = NSLineBreakByWordWrapping; _detailTextLabel.accessibilityIdentifier = @"ORKStepContentView_detailTextLabel"; [self addSubview:_detailTextLabel]; [self setupDetailTextLabelConstraints]; [self setContainerLeftRightConstraints]; } - (void)setStepHeaderTextAlignment:(NSTextAlignment)stepHeaderTextAlignment { _stepHeaderTextAlignment = stepHeaderTextAlignment; _titleLabel.textAlignment = _stepHeaderTextAlignment; _detailTextLabel.textAlignment = _stepHeaderTextAlignment; _textLabel.textAlignment = _stepHeaderTextAlignment; } - (void)setBodyTextAlignment:(NSTextAlignment)bodyTextAlignment { _bodyTextAlignment = bodyTextAlignment; } - (void)setUseExtendedPadding:(BOOL)useExtendedPadding { _useExtendedPadding = useExtendedPadding; _leftRightPadding = useExtendedPadding ? ORKStepContainerExtendedLeftRightPaddingForWindow(self.window) : ORKStepContainerLeftRightPaddingForWindow(self.window); [NSLayoutConstraint deactivateConstraints:_leftRightPaddingConstraints]; [_updatedConstraints removeObjectsInArray:_leftRightPaddingConstraints]; [self setContainerLeftRightConstraints]; } - (void)setupDetailTextLabelConstraints { _detailTextLabel.translatesAutoresizingMaskIntoConstraints = NO; [self setDetailTextLabelTopConstraint]; _detailTextLabelConstraints = @[_detailTextLabelTopConstraint]; [_updatedConstraints addObjectsFromArray:_detailTextLabelConstraints]; [self setNeedsUpdateConstraints]; } - (void)setDetailTextLabelTopConstraint { if (_detailTextLabel) { id topItem; NSLayoutAttribute attribute; CGFloat constant; if (_textLabel) { topItem = _textLabel; attribute = NSLayoutAttributeBottom; constant = ORKBodyToBodyPaddingStandard; } else if (_titleLabel) { topItem = _titleLabel; attribute = NSLayoutAttributeBottom; constant = ORKStepContainerTitleToBodyTopPaddingForWindow(self.window); } else if (_iconImageView) { topItem = _iconImageView; attribute = NSLayoutAttributeBottom; constant = ORKStepContentIconToBodyTopPaddingStandard; } else if (_topContentImageView) { topItem = _topContentImageView; attribute = NSLayoutAttributeBottom; constant = ORKStepContainerFirstItemTopPaddingForWindow(self.window); } else { topItem = self; attribute = NSLayoutAttributeTop; constant = ORKStepContainerFirstItemTopPaddingForWindow(self.window) + _additionalTopPaddingForTopLabel; } _detailTextLabelTopConstraint = [NSLayoutConstraint constraintWithItem:_detailTextLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:topItem attribute:attribute multiplier:1.0 constant:constant]; } } - (void)updateDetailTextLabelTopConstraint { if (_detailTextLabelTopConstraint) { [self deactivateConstraints:@[_detailTextLabelTopConstraint]]; } [self setDetailTextLabelTopConstraint]; if (_detailTextLabelTopConstraint) { [_updatedConstraints addObject:_detailTextLabelTopConstraint]; } } - (void)deactivateDetailTextLabelConstraints { [self deactivateConstraints:_detailTextLabelConstraints]; _detailTextLabelConstraints = nil; } // body container - (void)setBodyItems:(NSArray *)bodyItems { _bodyItems = bodyItems; if (_bodyItems) { if (!_bodyContainerView) { [self setupBodyContainerView]; [self updateStepContentViewBottomConstraint]; [self setNeedsUpdateConstraints]; } else { _bodyContainerView.bodyItems = _bodyItems; } } } - (void)setBuildsInBodyItems:(BOOL)buildsInBodyItems { _buildsInBodyItems = buildsInBodyItems; _bodyContainerView.buildsInBodyItems = _buildsInBodyItems; _bodyContainerView.accessibilityIdentifier = ORKStepBodyContainerViewAccessibilityIdentifier; } - (void)setupBodyContainerView { __weak id weakSelf = self; if (!_bodyContainerView) { _bodyContainerView = [[ORKBodyContainerView alloc] initWithBodyItems:_bodyItems textAlignment:_bodyTextAlignment delegate:weakSelf]; } [self addSubview:_bodyContainerView]; [self setupBodyContainerViewConstraints]; } - (void)updateBodyContainerViewTopConstraint { if (_bodyContainerView) { if (_bodyContainerViewTopConstraint) { [self deactivateConstraints:@[_bodyContainerViewTopConstraint]]; } [self setBodyContainerViewTopConstraint]; if (_bodyContainerViewTopConstraint) { [_updatedConstraints addObject:_bodyContainerViewTopConstraint]; } } } - (void)setupBodyContainerViewConstraints { _bodyContainerView.translatesAutoresizingMaskIntoConstraints = NO; [self setBodyContainerViewTopConstraint]; [_updatedConstraints addObjectsFromArray:@[ _bodyContainerViewTopConstraint, ]]; [self setNeedsUpdateConstraints]; } #pragma mark - Vertical ImageView - (void)setCenteredVerticallyImage:(UIImage *)centeredVerticallyImage { _centeredVerticallyImage = centeredVerticallyImage; if (centeredVerticallyImage && !_centeredVerticallyImageView) { [self setupCenteredVerticallyImageView]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceCenteredVerticallyImageView]; [self setNeedsUpdateConstraints]; [_centeredVerticallyImageView setImage:centeredVerticallyImage]; [_centeredVerticallyImageView setContentMode:UIViewContentModeScaleAspectFit]; } else if (centeredVerticallyImage && _centeredVerticallyImageView) { [_centeredVerticallyImageView setImage:_centeredVerticallyImage]; [_centeredVerticallyImageView setContentMode:UIViewContentModeScaleAspectFit]; } else if (!_centeredVerticallyImage) { [_centeredVerticallyImageView removeFromSuperview]; _centeredVerticallyImageView = nil; [self deactivateCenteredVerticallyImageViewConstraints]; [self updateViewConstraintsForSequence:ORKUpdateConstraintSequenceCenteredVerticallyImageView]; [self setNeedsUpdateConstraints]; } } - (void)deactivateCenteredVerticallyImageViewConstraints { [self deactivateConstraints:_centeredVerticallyImageViewContraints]; _centeredVerticallyImageViewContraints = nil; } - (void)setupCenteredVerticallyImageView { if (!_centeredVerticallyImageView) { _centeredVerticallyImageView = [[UIImageView alloc] init]; } [self addSubview:_centeredVerticallyImageView]; [self setupCenteredVerticallyImageViewContraints]; [self setContainerLeftRightConstraints]; } - (void)updateCenteredVerticallyImageViewTopContraint { if (_centeredVerticallyImageView) { if (_centeredVerticallyImageViewTopConstraint) { [self deactivateConstraints:@[_centeredVerticallyImageViewTopConstraint]]; } [self setCenteredVerticallyImageViewTopConstraint]; if (_centeredVerticallyImageViewTopConstraint) { [_updatedConstraints addObject:_centeredVerticallyImageViewTopConstraint]; } } } - (void)setupCenteredVerticallyImageViewContraints { _centeredVerticallyImageView.translatesAutoresizingMaskIntoConstraints = NO; [self setCenteredVerticallyImageViewTopConstraint]; _centeredVerticallyImageViewContraints = @[_centeredVerticallyImageViewTopConstraint]; [_updatedConstraints addObjectsFromArray:_centeredVerticallyImageViewContraints]; [self setNeedsUpdateConstraints]; } - (void)setCenteredVerticallyImageViewTopConstraint { if (_centeredVerticallyImageView) { id topItem; CGFloat topPadding; NSLayoutAttribute attribute; if (_bodyContainerView) { topItem = _bodyContainerView; attribute = NSLayoutAttributeBottom; topPadding = ORKBodyToBodyPaddingStandard; } else if (_detailTextLabel && ![_stepDetailText isEqualToString:@""]) { topItem = _detailTextLabel; topPadding = ORKBodyToBodyPaddingStandard; attribute = NSLayoutAttributeBottom; } else if (_textLabel && ![_stepText isEqualToString:@""]) { topItem = _textLabel; if (_bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleTag || _bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleHorizontalRule) { topPadding = ORKBodyToBodyParagraphPaddingStandard; } else { topPadding = ORKBodyToBodyPaddingStandard; } attribute = NSLayoutAttributeBottom; } else if (_titleLabel) { topItem = _titleLabel; if (_bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleTag) { topPadding = ORKStepContentTagPaddingTop; } else { topPadding = _bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleText ? ORKStepContainerTitleToBodyTopPaddingForWindow(self.window) : ORKStepContainerTitleToBulletTopPaddingForWindow(self.window); } attribute = NSLayoutAttributeBottom; } else if (_iconImageView) { topItem = _iconImageView; topPadding = _bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleText ? ORKStepContentIconToBodyTopPaddingStandard : ORKStepContentIconToBulletTopPaddingStandard; attribute = NSLayoutAttributeBottom; } else if (_topContentImageView) { topItem = _topContentImageView; topPadding = ORKStepContainerFirstItemTopPaddingForWindow(self.window); attribute = NSLayoutAttributeBottom; } else { topItem = self; topPadding = ORKStepContainerFirstItemTopPaddingForWindow(self.window); attribute = NSLayoutAttributeTop; } _centeredVerticallyImageViewTopConstraint = [NSLayoutConstraint constraintWithItem:_centeredVerticallyImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:topItem attribute:attribute multiplier:1.0 constant:topPadding]; [self setNeedsUpdateConstraints]; } } -(void)updateCenteredVerticallyImageViewTopConstraint { if (_centeredVerticallyImageViewTopConstraint && _centeredVerticallyImageViewTopConstraint.isActive) { [NSLayoutConstraint deactivateConstraints:@[_centeredVerticallyImageViewTopConstraint]]; } if ([_updatedConstraints containsObject:_centeredVerticallyImageViewTopConstraint]) { [_updatedConstraints removeObject:_centeredVerticallyImageViewTopConstraint]; } [self setCenteredVerticallyImageViewTopConstraint]; if (_centeredVerticallyImageViewTopConstraint) { [_updatedConstraints addObject:_centeredVerticallyImageViewTopConstraint]; } } - (void)setContainerLeftRightConstraints { [NSLayoutConstraint deactivateConstraints:_leftRightPaddingConstraints]; [_updatedConstraints removeObjectsInArray:_leftRightPaddingConstraints]; _leftRightPaddingConstraints = [[NSMutableArray alloc] init]; if (_textLabel != nil) { [_leftRightPaddingConstraints addObjectsFromArray:@[ [NSLayoutConstraint constraintWithItem:_textLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0 constant:_leftRightPadding], [NSLayoutConstraint constraintWithItem:_textLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-_leftRightPadding] ]]; } if (_titleLabel != nil) { [_leftRightPaddingConstraints addObjectsFromArray:@[ [NSLayoutConstraint constraintWithItem:_titleLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0 constant:_leftRightPadding], [NSLayoutConstraint constraintWithItem:_titleLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-_leftRightPadding] ]]; } if (_detailTextLabel != nil) { [_leftRightPaddingConstraints addObjectsFromArray:@[ [NSLayoutConstraint constraintWithItem:_detailTextLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0 constant:_leftRightPadding], [NSLayoutConstraint constraintWithItem:_detailTextLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-_leftRightPadding] ]]; } if (_bodyContainerView != nil) { [_leftRightPaddingConstraints addObjectsFromArray:@[ [NSLayoutConstraint constraintWithItem:_bodyContainerView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:_leftRightPadding], [NSLayoutConstraint constraintWithItem:_bodyContainerView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:-_leftRightPadding] ]]; } if (_centeredVerticallyImageView != nil) { [_leftRightPaddingConstraints addObjectsFromArray:@[ [_centeredVerticallyImageView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor constant:0.0] ]]; } if (_iconImageView != nil) { if (_stepHeaderTextAlignment == NSTextAlignmentLeft) { [_leftRightPaddingConstraints addObjectsFromArray:@[ [NSLayoutConstraint constraintWithItem:_iconImageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0 constant:_leftRightPadding] ]]; } else { [_leftRightPaddingConstraints addObjectsFromArray:@[ [NSLayoutConstraint constraintWithItem:_iconImageView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0] ]]; } } [_updatedConstraints addObjectsFromArray:_leftRightPaddingConstraints]; [self setNeedsUpdateConstraints]; } - (void)setBodyContainerViewTopConstraint { id topItem; CGFloat topPadding; NSLayoutAttribute attribute; // Checking empty string as we have an odd case where the label gets created because the text // is an empty string. This causes the layout to be bigger than expected as the label is given // some amount of height and we are calculating vertical padding based off a label that is not // visible. TODO - rework the labels so they do not get created with empty text. if (_detailTextLabel && ![_stepDetailText isEqualToString:@""]) { topItem = _detailTextLabel; topPadding = ORKBodyToBodyPaddingStandard; attribute = NSLayoutAttributeBottom; } else if (_textLabel && ![_stepText isEqualToString:@""]) { topItem = _textLabel; if (_bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleTag || _bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleHorizontalRule) { topPadding = ORKBodyToBodyParagraphPaddingStandard; } else { topPadding = ORKBodyToBodyPaddingStandard; } attribute = NSLayoutAttributeBottom; } else if (_titleLabel) { topItem = _titleLabel; if (_bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleTag) { topPadding = ORKStepContentTagPaddingTop; } else { topPadding = _bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleText ? ORKStepContainerTitleToBodyTopPaddingForWindow(self.window) : ORKStepContainerTitleToBulletTopPaddingForWindow(self.window); } attribute = NSLayoutAttributeBottom; } else if (_iconImageView) { topItem = _iconImageView; topPadding = _bodyItems.firstObject.bodyItemStyle == ORKBodyItemStyleText ? ORKStepContentIconToBodyTopPaddingStandard : ORKStepContentIconToBulletTopPaddingStandard; attribute = NSLayoutAttributeBottom; } else if (_topContentImageView) { topItem = _topContentImageView; topPadding = ORKStepContainerFirstItemTopPaddingForWindow(self.window); attribute = NSLayoutAttributeBottom; } else { topItem = self; topPadding = ORKStepContainerFirstItemTopPaddingForWindow(self.window); attribute = NSLayoutAttributeTop; } _bodyContainerViewTopConstraint = [NSLayoutConstraint constraintWithItem:_bodyContainerView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:topItem attribute:attribute multiplier:1.0 constant:topPadding]; } // Variable constraints methods - (void)updateViewConstraintsForSequence:(ORKUpdateConstraintSequence)sequence { switch (sequence) { case ORKUpdateConstraintSequenceTopContentImageView: [self updateIconImageViewTopConstraint]; case ORKUpdateConstraintSequenceIconImageView: [self updateTitleLabelTopConstraint]; case ORKUpdateConstraintSequenceTitleLabel: [self updateTextLabelTopConstraint]; case ORKUpdateConstraintSequenceTextLabel: [self updateDetailTextLabelTopConstraint]; case ORKUpdateConstraintSequenceDetailTextLabel: [self updateBodyContainerViewTopConstraint]; case ORKUpdateConstraintSequenceCenteredVerticallyImageView: [self updateCenteredVerticallyImageViewTopConstraint]; default: break; } [self updateStepContentViewBottomConstraint]; } - (void)updateStepContentViewBottomConstraint { [self deactivateConstraints:@[_stepContentBottomConstraint]]; [self setStepContentViewBottomConstraint]; [_updatedConstraints addObject:_stepContentBottomConstraint]; } - (void)setStepContentViewBottomConstraint { id bottomItem; NSLayoutAttribute attribute; CGFloat constant = 0.0; if (@available(iOS 15, *)) { if (!useStandardTextAndFormPadding) { constant += ORKStepContentBottomPadding; } } if (_centeredVerticallyImageView) { bottomItem = _centeredVerticallyImageView; attribute = NSLayoutAttributeBottom; } else if (_bodyContainerView) { bottomItem = _bodyContainerView; attribute = NSLayoutAttributeBottom; } else if (_detailTextLabel && ![_stepDetailText isEqualToString:@""]) { bottomItem = _detailTextLabel; attribute = NSLayoutAttributeBottom; } else if (_textLabel && ![_stepText isEqualToString:@""]) { bottomItem = _textLabel; attribute = NSLayoutAttributeBottom; } else if (_titleLabel) { bottomItem = _titleLabel; attribute = NSLayoutAttributeBottom; } else if (_iconImageView) { bottomItem = _iconImageView; attribute = NSLayoutAttributeBottom; } else { bottomItem = nil; attribute = NSLayoutAttributeNotAnAttribute; } _stepContentBottomConstraint = [NSLayoutConstraint constraintWithItem:self attribute:bottomItem ? NSLayoutAttributeBottom : NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:bottomItem attribute:attribute multiplier:1.0 constant:constant]; } - (void)setStepContentViewBottomConstraint:(CGFloat)bottomConstraint { _stepContentBottomConstraint.constant = bottomConstraint; } - (void)setupUpdatedConstraints { _updatedConstraints = [[NSMutableArray alloc] init]; } - (void)deactivateConstraints:(nullable NSArray *)constraints { if (constraints) { [NSLayoutConstraint deactivateConstraints:constraints]; } [self updatedConstraintsWithdrawConstraints:constraints]; [self setNeedsUpdateConstraints]; } - (void)updatedConstraintsWithdrawConstraints:(nullable NSArray *)constraints { if (constraints && constraints.count > 0) { for (NSLayoutConstraint *constraint in constraints) { if ([_updatedConstraints containsObject:constraint]) { [_updatedConstraints removeObject:constraint]; } } } } - (void)updateContentConstraints { [NSLayoutConstraint activateConstraints:_updatedConstraints]; [_updatedConstraints removeAllObjects]; } - (void)updateConstraints { [self updateContentConstraints]; [super updateConstraints]; } // Private methods - (void)setAdditionalTopPaddingForTopLabel:(CGFloat)padding { _additionalTopPaddingForTopLabel = padding; if (!_topContentImageView && !_iconImageView) { if (_titleLabel) { [self updateTitleLabelTopConstraint]; } else if (_textLabel) { [self updateTextLabelTopConstraint]; } else if (_detailTextLabel) { [self updateDetailTextLabelTopConstraint]; } } } - (nullable ORKCompletionCheckmarkView *)completionCheckmarkView { if (!_completionCheckmarkView) { _completionCheckmarkView = [[ORKCompletionCheckmarkView alloc] initWithDefaultDimension]; [self addSubview:_completionCheckmarkView]; self.titleIconImage = [UIImage new]; [_iconImageView setHidden:YES]; _completionCheckmarkView.translatesAutoresizingMaskIntoConstraints = NO; [self setContainerLeftRightConstraints]; [_updatedConstraints addObjectsFromArray:@[ [NSLayoutConstraint constraintWithItem:_completionCheckmarkView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:_iconImageView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_completionCheckmarkView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_iconImageView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_completionCheckmarkView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:_iconImageView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_completionCheckmarkView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:_iconImageView attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0.0] ]]; } return _completionCheckmarkView; } #pragma mark - ORKBodycontainerViewDelegate - (void)bodyContainerLearnMoreButtonPressed:(ORKLearnMoreInstructionStep *)learnMoreStep { [_delegate stepContentLearnMoreButtonPressed:learnMoreStep]; } @end