/* 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 "ORKStepView_Private.h" #import "ORKStepContainerView_Private.h" #import "ORKTitleLabel.h" #import "ORKBodyItem.h" #import "ORKStepContentView_Private.h" #import "ORKBodyContainerView.h" #import "ORKSkin.h" #import "ORKActiveStep.h" #import "ORKNavigationContainerView_Internal.h" #import "ORKTypes.h" #import "ORKHelpers_Internal.h" /* +-----------------------------------------+ | +-------------------------------------+ |<---_stepContainerView | | _topContentImageView | | | | | | | | | | | |_____________________________________| | | +-------------------------------------+ | | | +-------------------------------+ | | | | | +_________________________+ | | |<-----_scrollView | | | | | | | | | | | | +-------+ | |<----------_scrollContainerView | | | | | _icon | | | | | | | | | | | | | | | | | | | +-------+ |<-------------_stepContentView | | | | | | | | | | | | +---------------------+ | | | | | | | | | _titleLabel | | | | | | | | | |_____________________| | | | | | | | | +---------------------+ | | | | | | | | | _textLabel | | | | | | | | | |_____________________| | | | | | | | | +---------------------+ | | | | | | | | | _detailTextLabel | | | | | | | | | |_____________________| | | | | | | | | | | | | | | | | +---------------------+ | | | | | | | | | |<-------------_bodyContainerView: UIstackView | | | | | +-----------------+ | | | | | | | | | | | | | | | | | | | | | | |--Title | | | | | | | | | | | |--Text |<-------------- BodyItemStyleText | | | | | |--LearnMore | | | | | | | | | | | |_________________| | | | | | | | | | | | | | | | | | | | | +---+-------------+ | | | | | | | | | | | |--Title | | | | | | | | | | | | o |--Text |<-------------- BodyItemStyleBullet | | | | | | |--LearnMore | | | | | | | | | | | |___|_____________| | | | | | | | | | |_____________________| | | | | | | | | | | | | | | | | +---------------------+ | | | | | | | | | _centeredVertically-| | | | | | | | | | ImageView | | | | | | | | | |_____________________| | | | | | | | |_________________________| | | | | | | | | | | | | +-------------------------+ | | | | | | | _CustomContentView | | | | | | | |_________________________| | | | | |__|_______________________________|__| | |____|_______________________________|____| | | | | | +-------------------------+ | | | _navigationFooter | | | |_________________________| | vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv */ static NSString *scrollContentChangedNotification = @"scrollContentChanged"; @interface ScrollView : UIScrollView @end @implementation ScrollView - (void)setContentSize:(CGSize)contentSize { [super setContentSize:contentSize]; [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:scrollContentChangedNotification object:nil]]; } - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { // Workarround to make VO scroll work when the scrollview is scrollable because the added contentInset.bottom if (self.contentInset.bottom > 0 && self.contentSize.height <= self.bounds.size.height) { switch (direction) { case UIAccessibilityScrollDirectionUp: { [self setContentOffset:CGPointMake(self.contentOffset.x, 0) animated:YES]; NSString *announceString = [NSString stringWithFormat:ORKLocalizedString(@"AX_PAGE_NUMBER_FORMAT", nil), 1, 2]; UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, announceString); return YES; } case UIAccessibilityScrollDirectionDown: { CGFloat offsetY = self.contentSize.height - self.bounds.size.height + self.contentInset.bottom; [self setContentOffset:CGPointMake(self.contentOffset.x, offsetY) animated:YES]; NSString *announceString = [NSString stringWithFormat:ORKLocalizedString(@"AX_PAGE_NUMBER_FORMAT", nil), 2, 2]; UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, announceString); return YES; } default: break; } } return NO; } @end @implementation ORKStepContainerView { CGFloat _leftRightPadding; CGFloat _customContentLeftRightPadding; ScrollView *_scrollView; UIView *_scrollContainerView; BOOL _topContentImageShouldScroll; CGFloat _customContentTopPadding; CGFloat _highestContentPosition; BOOL _showScrollIndicator; CGFloat _scrollViewCustomContentInset; UIImageView *_topContentImageView; // variable constraints: NSLayoutConstraint *_scrollViewTopConstraint; NSLayoutConstraint *_scrollViewBottomConstraint; NSLayoutConstraint *_stepContentViewTopConstraint; NSLayoutConstraint *_customContentViewTopConstraint; NSArray *_topContentImageViewConstraints; NSLayoutConstraint *_navigationContainerViewTopConstraint; NSArray *_navigationContainerViewConstraints; NSLayoutConstraint *_customContentWidthConstraint; NSLayoutConstraint *_customContentHeightConstraint; NSMutableArray *_updatedConstraints; NSLayoutConstraint *_scrollContentBottomConstraint; } - (instancetype)init { self = [super init]; if (self) { _customContentLeftRightPadding = ORKStepContainerLeftRightPaddingForWindow(self.window); _leftRightPadding = ORKStepContainerExtendedLeftRightPaddingForWindow(self.window); self.isNavigationContainerScrollable = NO; _highestContentPosition = 0.0; _scrollViewCustomContentInset = ORKCGFloatDefaultValue; [self setupScrollView]; [self setupScrollContainerView]; [self addStepContentView]; [self setupConstraints]; [self setupUpdatedConstraints]; [self placeNavigationContainerView]; _topContentImageShouldScroll = YES; _customContentTopPadding = ORKStepContainerTopCustomContentPaddingStandard; [self setPinNavigationContainer:YES]; // Default behavior is to pin the navigation footer [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollContentChanged) name:scrollContentChangedNotification object:nil]; } return self; } - (void)setPinNavigationContainer:(BOOL)pinNavigationContainer { _pinNavigationContainer = pinNavigationContainer; [self placeNavigationContainerView]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:scrollContentChangedNotification object:nil]; } - (void)setStepTopContentImage:(UIImage *)stepTopContentImage { [super setStepTopContentImage:stepTopContentImage]; if (_topContentImageShouldScroll) { [self.stepContentView setStepTopContentImage:stepTopContentImage]; } else { // 1.) nil Image; updateConstraints if (!stepTopContentImage && _topContentImageView) { [_topContentImageView removeFromSuperview]; _topContentImageView = nil; [self deactivateTopContentImageViewConstraints]; [self updateScrollViewTopConstraint]; [self setNeedsUpdateConstraints]; } // 2.) First Image; updateConstraints if (stepTopContentImage && !_topContentImageView) { [self setupTopContentImageView]; _topContentImageView.image = [self topContentAndAuxiliaryImage]; [self updateScrollViewTopConstraint]; [self setNeedsUpdateConstraints]; } // 3.) >= second Image; if (stepTopContentImage && _topContentImageView) { _topContentImageView.image = [self topContentAndAuxiliaryImage]; } } } - (void)setupScrollView { if (!_scrollView) { _scrollView = [[ScrollView alloc] init]; } _scrollView.showsVerticalScrollIndicator = self.showScrollIndicator; _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; _scrollView.delegate = self; [self addSubview:_scrollView]; } - (void)setShowScrollIndicator:(BOOL)showScrollIndicator { _showScrollIndicator = showScrollIndicator; _scrollView.showsVerticalScrollIndicator = showScrollIndicator; } - (BOOL)showScrollIndicator { return _showScrollIndicator; } - (void)setupScrollContainerView { if (!_scrollContainerView) { _scrollContainerView = [UIView new]; } [_scrollView addSubview:_scrollContainerView]; } - (void)setupUpdatedConstraints { _updatedConstraints = [[NSMutableArray alloc] init]; } - (void)setupTopContentImageView { if (!_topContentImageView) { _topContentImageView = [UIImageView new]; } _topContentImageView.contentMode = UIViewContentModeScaleAspectFit; [_topContentImageView setBackgroundColor:ORKColor(ORKTopContentImageViewBackgroundColorKey)]; [self addSubview:_topContentImageView]; [self setTopContentImageViewConstraints]; } - (void)setStepTopContentImageContentMode:(UIViewContentMode)stepTopContentImageContentMode { [super setStepTopContentImageContentMode:stepTopContentImageContentMode]; if (_topContentImageView) { _topContentImageView.contentMode = stepTopContentImageContentMode; } } - (void)setAuxiliaryImage:(UIImage *)auxiliaryImage { [super setAuxiliaryImage:auxiliaryImage]; if (self.stepTopContentImage) { _topContentImageView.image = [self topContentAndAuxiliaryImage]; } } - (void)addStepContentView { [_scrollContainerView addSubview:self.stepContentView]; } - (void)stepContentViewImageChanged:(NSNotification *)notification { [super stepContentViewImageChanged:notification]; [self updateStepContentViewTopConstraint]; [self setNeedsUpdateConstraints]; } - (void)setStepContentViewConstraints { self.stepContentView.translatesAutoresizingMaskIntoConstraints = NO; [self setStepContentViewTopConstraint]; [NSLayoutConstraint activateConstraints:@[ _stepContentViewTopConstraint, [NSLayoutConstraint constraintWithItem:self.stepContentView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:_scrollContainerView attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:self.stepContentView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:_scrollContainerView attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0] ]]; } - (void)setStepContentViewTopConstraint { _stepContentViewTopConstraint = [NSLayoutConstraint constraintWithItem:self.stepContentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.stepContentView.topContentImageView.image ? _scrollContainerView : _scrollContainerView.safeAreaLayoutGuide attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0]; } - (void)updateStepContentViewTopConstraint { if (_stepContentViewTopConstraint && _stepContentViewTopConstraint.isActive) { [NSLayoutConstraint deactivateConstraints:@[_stepContentViewTopConstraint]]; } if ([_updatedConstraints containsObject:_stepContentViewTopConstraint]) { [_updatedConstraints removeObject:_stepContentViewTopConstraint]; } [self setStepContentViewTopConstraint]; if (_stepContentViewTopConstraint) { [_updatedConstraints addObject:_stepContentViewTopConstraint]; } } - (void)setScrollViewCustomContentInset:(CGFloat)scrollViewCustomContentInset { _scrollViewCustomContentInset = scrollViewCustomContentInset; [self updateScrollViewCustomContentInset]; } - (void)updateScrollViewCustomContentInset { if (_scrollViewCustomContentInset == ORKCGFloatDefaultValue) { return; } if (self.contentHeight > self.frame.size.height) { _scrollView.contentInset = UIEdgeInsetsMake(0, 0, _scrollViewCustomContentInset, 0); } else { _scrollView.contentInset = UIEdgeInsetsZero; } } - (void)setCustomContentView:(UIView *)customContentView { _customContentView = customContentView; [_scrollContainerView addSubview:_customContentView]; [self setupCustomContentViewConstraints]; [self updateNavigationContainerViewTopConstraint]; [self setNeedsUpdateConstraints]; } - (void)removeNavigationFooterView { [self.navigationFooterView removeFromSuperview]; if (_navigationContainerViewConstraints) { [NSLayoutConstraint deactivateConstraints:_navigationContainerViewConstraints]; for (NSLayoutConstraint *constraint in _navigationContainerViewConstraints) { if ([_updatedConstraints containsObject:constraint]) { [_updatedConstraints removeObject:constraint]; } } _navigationContainerViewConstraints = nil; } } - (void)placeNavigationContainerView { [self removeNavigationFooterView]; if (!_pinNavigationContainer) { [_scrollView addSubview:self.navigationFooterView]; } else if (self.isNavigationContainerScrollable) { [_scrollContainerView addSubview:self.navigationFooterView]; } else { [self addSubview:self.navigationFooterView]; } [self setupNavigationContainerViewConstraints]; } - (void)placeNavigationContainerInsideScrollView { self.isNavigationContainerScrollable = YES; [self setupConstraints]; [self placeNavigationContainerView]; } - (void)layoutSubviews { [super layoutSubviews]; [self updateScrollContentConstraints]; // dispatching on main thread to prevent the blur view from popping-up after transition is complete dispatch_async(dispatch_get_main_queue(), ^{ [self updateEffectViewStylingAndAnimate:NO checkCurrentValue:NO]; if (_scrollViewCustomContentInset != ORKCGFloatDefaultValue) { [self updateScrollViewCustomContentInset]; } }); } - (void)updateScrollContentConstraints { if (_scrollContentBottomConstraint != nil) { [NSLayoutConstraint deactivateConstraints:@[_scrollContentBottomConstraint]]; } _scrollContentBottomConstraint = [NSLayoutConstraint constraintWithItem:self.stepContentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationLessThanOrEqual toItem:_scrollContainerView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]; [NSLayoutConstraint activateConstraints:@[_scrollContentBottomConstraint]]; } - (void)setupNavigationContainerViewConstraints { self.navigationFooterView.translatesAutoresizingMaskIntoConstraints = NO; BOOL useScrollableItem = (self.isNavigationContainerScrollable || !_pinNavigationContainer); id boundaryView = useScrollableItem ? _scrollContainerView : self; _navigationContainerViewConstraints = @[ [NSLayoutConstraint constraintWithItem:self.navigationFooterView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:boundaryView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:self.navigationFooterView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:boundaryView attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:self.navigationFooterView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:boundaryView attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0]]; [_updatedConstraints addObjectsFromArray:_navigationContainerViewConstraints]; [self updateNavigationContainerViewTopConstraint]; if (!self.isNavigationContainerScrollable) { [NSLayoutConstraint deactivateConstraints:@[_scrollViewBottomConstraint]]; if ([_updatedConstraints containsObject:_scrollViewBottomConstraint]) { [_updatedConstraints removeObject:_scrollViewBottomConstraint]; } _scrollViewBottomConstraint = [NSLayoutConstraint constraintWithItem:_scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]; [_updatedConstraints addObject:_scrollViewBottomConstraint]; } [self setNeedsUpdateConstraints]; } - (void)setupNavigationContainerViewTopConstraint { BOOL shouldScrollNavigationContainer = (self.isNavigationContainerScrollable || !_pinNavigationContainer); if (self.navigationFooterView && shouldScrollNavigationContainer) { id topItem; NSLayoutAttribute topItemAttribute; if (_customContentView) { topItem = _customContentView; topItemAttribute = NSLayoutAttributeBottom; } else if(self.stepContentView) { topItem = self.stepContentView; topItemAttribute = NSLayoutAttributeBottom; } else { topItem = _scrollContainerView; topItemAttribute = NSLayoutAttributeTop; } _navigationContainerViewTopConstraint = [NSLayoutConstraint constraintWithItem:self.navigationFooterView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:topItem attribute:topItemAttribute multiplier:1.0 constant:ORKStepContainerNavigationFooterTopPaddingStandard]; } } - (void)updateNavigationContainerViewTopConstraint { BOOL shouldScrollNavigationContainer = (self.isNavigationContainerScrollable || !_pinNavigationContainer); if (self.navigationFooterView && shouldScrollNavigationContainer) { [self removeNavigationContainerViewTopConstraint]; [self setupNavigationContainerViewTopConstraint]; [_updatedConstraints addObject:_navigationContainerViewTopConstraint]; } else { [self removeNavigationContainerViewTopConstraint]; } } - (void)removeNavigationContainerViewTopConstraint { if (_navigationContainerViewTopConstraint) { [NSLayoutConstraint deactivateConstraints:@[_navigationContainerViewTopConstraint]]; if ([_updatedConstraints containsObject:_navigationContainerViewTopConstraint]) { [_updatedConstraints removeObject:_navigationContainerViewTopConstraint]; } } } - (void)setupCustomContentViewConstraints { _customContentView.translatesAutoresizingMaskIntoConstraints = NO; [self setCustomContentViewTopConstraint]; [self setCustomContentWidthConstraint]; [_updatedConstraints addObjectsFromArray:@[ _customContentViewTopConstraint, [NSLayoutConstraint constraintWithItem:_customContentView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:_scrollContainerView attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0], _customContentWidthConstraint ]]; [self updateCustomContentHeightConstraint]; [self setNeedsUpdateConstraints]; } - (void)setCustomContentFillsAvailableSpace:(BOOL)customContentFillsAvailableSpace { _customContentFillsAvailableSpace = customContentFillsAvailableSpace; [self updateCustomContentHeightConstraint]; } - (void)setCustomContentHeightConstraint { if (_customContentView) { if ([_customContentView isKindOfClass:[UIImageView class]]) { _customContentHeightConstraint = [_customContentView.heightAnchor constraintLessThanOrEqualToConstant:ImageViewMaxHeightAndWidth]; } else { _customContentHeightConstraint = [NSLayoutConstraint constraintWithItem:_customContentView attribute:NSLayoutAttributeBottom relatedBy:_customContentFillsAvailableSpace ? NSLayoutRelationEqual : NSLayoutRelationLessThanOrEqual toItem:self.isNavigationContainerScrollable ? self.navigationFooterView : _scrollContainerView attribute:self.isNavigationContainerScrollable ? NSLayoutAttributeTop : NSLayoutAttributeBottom multiplier:1.0 constant:self.isNavigationContainerScrollable ? -ORKStepContainerNavigationFooterTopPaddingStandard : 0.0]; } } } - (void)updateCustomContentHeightConstraint { if (_customContentView) { if (_customContentHeightConstraint && _customContentHeightConstraint.isActive) { [NSLayoutConstraint deactivateConstraints:@[_customContentHeightConstraint]]; } if ([_updatedConstraints containsObject:_customContentHeightConstraint]) { [_updatedConstraints removeObject:_customContentHeightConstraint]; } [self setCustomContentHeightConstraint]; if (_customContentHeightConstraint) { [_updatedConstraints addObject:_customContentHeightConstraint]; } } } - (void)setCustomContentWidthConstraint { if (_customContentView) { if ([_customContentView isKindOfClass:[UIImageView class]]) { _customContentWidthConstraint = [_customContentView.widthAnchor constraintLessThanOrEqualToConstant:ImageViewMaxHeightAndWidth]; } else { _customContentWidthConstraint = [NSLayoutConstraint constraintWithItem:_customContentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:_scrollContainerView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:-2*_customContentLeftRightPadding]; } } } - (void)removeCustomContentPadding { _customContentLeftRightPadding = 0.0; if (_customContentWidthConstraint) { _customContentWidthConstraint.constant = _customContentLeftRightPadding; } } - (void)setCustomContentViewTopConstraint { id topItem; NSLayoutAttribute attribute; if (self.stepContentView) { topItem = self.stepContentView; attribute = NSLayoutAttributeBottom; } else { topItem = _scrollContainerView; attribute = NSLayoutAttributeTop; } _customContentViewTopConstraint = [NSLayoutConstraint constraintWithItem:_customContentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:topItem attribute:attribute multiplier:1.0 constant:_customContentTopPadding]; } - (void)setCustomContentView:(UIView *)customContentView withTopPadding:(CGFloat)topPadding { [self setCustomContentView:customContentView withTopPadding:topPadding sidePadding:_customContentLeftRightPadding]; } - (void)setCustomContentView:(UIView *)customContentView withTopPadding:(CGFloat)topPadding sidePadding:(CGFloat)sidePadding { _customContentTopPadding = topPadding; _customContentLeftRightPadding = sidePadding; [self.stepContentView setCustomTopPadding:[NSNumber numberWithFloat:topPadding]]; [self setCustomContentView:customContentView]; } - (void)updateCustomContentViewTopConstraint { if (_customContentView) { if (_customContentViewTopConstraint && _customContentViewTopConstraint.isActive) { [NSLayoutConstraint deactivateConstraints:@[_customContentViewTopConstraint]]; } if ([_updatedConstraints containsObject:_customContentViewTopConstraint]) { [_updatedConstraints removeObject:_customContentViewTopConstraint]; } [self setCustomContentViewTopConstraint]; if (_customContentViewTopConstraint) { [_updatedConstraints addObject:_customContentViewTopConstraint]; } } } - (NSArray *)scrollViewStaticConstraints { _scrollViewBottomConstraint = [NSLayoutConstraint constraintWithItem:_scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]; return @[ [NSLayoutConstraint constraintWithItem:_scrollView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_scrollView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0], _scrollViewBottomConstraint ]; } - (NSArray *)scrollContainerStaticConstraints { NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:_scrollContainerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:_scrollView attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0.0]; heightConstraint.priority = UILayoutPriorityDefaultLow; return @[ [NSLayoutConstraint constraintWithItem:_scrollContainerView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_scrollView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_scrollContainerView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:_scrollView attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_scrollContainerView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:_scrollView attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_scrollContainerView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:_scrollView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:_scrollContainerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:_scrollView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0], heightConstraint ]; } - (void)setScrollViewTopConstraint { _scrollViewTopConstraint = [NSLayoutConstraint constraintWithItem:_scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_topContentImageView ? : self attribute:_topContentImageView ? NSLayoutAttributeBottom : NSLayoutAttributeTop multiplier:1.0 constant:0.0]; [_updatedConstraints addObject:_scrollViewTopConstraint]; } - (void)updateScrollViewTopConstraint { if (_scrollViewTopConstraint) { [NSLayoutConstraint deactivateConstraints:@[_scrollViewTopConstraint]]; } if ([_updatedConstraints containsObject:_scrollViewTopConstraint]) { [_updatedConstraints removeObject:_scrollViewTopConstraint]; } [self setScrollViewTopConstraint]; } - (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 { if (_topContentImageViewConstraints) { [NSLayoutConstraint deactivateConstraints:_topContentImageViewConstraints]; for (NSLayoutConstraint *constraint in _topContentImageViewConstraints) { if ([_updatedConstraints containsObject:constraint]) { [_updatedConstraints removeObject:constraint]; } } } _topContentImageViewConstraints = nil; } - (void)setupConstraints { _scrollView.translatesAutoresizingMaskIntoConstraints = NO; _scrollContainerView.translatesAutoresizingMaskIntoConstraints = NO; [self setScrollViewTopConstraint]; NSMutableArray *staticConstraints = [[NSMutableArray alloc] initWithArray:[self scrollViewStaticConstraints]]; [staticConstraints addObject:_scrollViewTopConstraint]; [staticConstraints addObjectsFromArray:[self scrollContainerStaticConstraints]]; [NSLayoutConstraint activateConstraints:staticConstraints]; [self setStepContentViewConstraints]; } - (void)updateContainerConstraints { [NSLayoutConstraint activateConstraints:_updatedConstraints]; [_updatedConstraints removeAllObjects]; } - (void)updateConstraints { [self updateContainerConstraints]; [super updateConstraints]; } - (void)topContentImageShouldStickToTop { if (self.stepTopContentImage) { UIImage *stepTopContentImage = self.stepTopContentImage; [self setStepTopContentImage:nil]; _topContentImageShouldScroll = NO; [self setStepTopContentImage:stepTopContentImage]; } _topContentImageShouldScroll = NO; } - (void)updatePaddingConstraints { [self.stepContentView setUseExtendedPadding:[self useExtendedPadding]]; [self.navigationFooterView setUseExtendedPadding:[self useExtendedPadding]]; } - (void)setUseExtendedPadding:(BOOL)useExtendedPadding { [super setUseExtendedPadding:useExtendedPadding]; [self updatePaddingConstraints]; } - (void)scrollToBodyItem:(UIView *)bodyItem { CGPoint pointInScrollView = [bodyItem.superview convertPoint:bodyItem.frame.origin toView:_scrollView]; CGFloat bottomOfView = pointInScrollView.y + bodyItem.frame.size.height; CGFloat bottomOfScrollView = _scrollView.frame.size.height - [self navigationFooterView].frame.size.height; if (bottomOfView > bottomOfScrollView) { [_scrollView setContentOffset:CGPointMake(0, (bottomOfView - bottomOfScrollView) + ORKBodyItemScrollPadding) animated:YES]; } } - (CGFloat)contentHeight { CGFloat height = 0.0; for (UIView *view in _scrollContainerView.subviews) { height += view.frame.size.height; } return height; } - (void)updateEffectViewStylingAndAnimate:(BOOL)animated checkCurrentValue:(BOOL)checkCurrentValue { CGFloat startOfFooter = self.navigationFooterView.frame.origin.y; CGFloat endOfFooter = self.navigationFooterView.frame.origin.y + self.navigationFooterView.frame.size.height; // calculating height of all subviews in _scrollContainerView CGFloat height = [self contentHeight]; if (!self.isNavigationContainerScrollable) { CGFloat contentPosition = (height - _scrollView.contentOffset.y); CGFloat newOpacity = (contentPosition < startOfFooter) ? ORKEffectViewOpacityHidden : ORKEffectViewOpacityVisible; [self updateEffectStyleWithNewOpacity:newOpacity animated:animated checkCurrentValue:checkCurrentValue]; // This check is to guard against scenarios when the view can be dragged down even if the content size doesn't allow for scrolling behavior if (contentPosition > _highestContentPosition && (_scrollView.contentOffset.y >= _scrollView.contentInset.top)) { _highestContentPosition = contentPosition; // add contentInset if the contentPosition extends beyond the footerView if ((contentPosition > startOfFooter) && (!self.navigationFooterView.isHidden)) { // Only need to calculate the offset based on content position if the end of the content sits between // the top and the bottom of the navigation footer view if (contentPosition < endOfFooter) { CGFloat offset = contentPosition - startOfFooter; self.scrollViewInset = UIEdgeInsetsMake(0, 0, offset + ORKContentBottomPadding, 0); } else { self.scrollViewInset = UIEdgeInsetsMake(0, 0, self.navigationFooterView.frame.size.height + ORKContentBottomPadding, 0); } } } } else if ([self.navigationFooterView effectViewOpacity] != ORKEffectViewOpacityHidden) { [self updateEffectStyleWithNewOpacity:ORKEffectViewOpacityHidden animated:NO checkCurrentValue:NO]; } } - (void)updateEffectViewStylingAndAnimate:(BOOL)animated checkCurrentValue:(BOOL)checkCurrentValue customView:(UIView *)customView { CGFloat startOfFooter = self.navigationFooterView.frame.origin.y; CGPoint newPoint = [customView convertPoint:customView.frame.origin toView:_scrollView]; CGFloat endOfContent = newPoint.y + customView.frame.size.height; CGFloat newOpacity = (endOfContent < startOfFooter) ? ORKEffectViewOpacityHidden : ORKEffectViewOpacityVisible; [self updateEffectStyleWithNewOpacity:newOpacity animated:animated checkCurrentValue:checkCurrentValue]; } - (void)updateEffectStyleWithNewOpacity:(CGFloat)newOpacity animated:(BOOL)animated checkCurrentValue:(BOOL)checkCurrentValue { CGFloat currentOpacity = [self.navigationFooterView effectViewOpacity]; if (!checkCurrentValue || (newOpacity != currentOpacity)) { // Don't animate transition from hidden to visible as text appears behind during animation if (currentOpacity == ORKEffectViewOpacityHidden) { animated = NO; } [self.navigationFooterView setStylingOpactity:newOpacity animated:animated]; } } - (void)setScrollEnabled:(BOOL)scrollEnabled { [_scrollView setScrollEnabled:scrollEnabled]; } - (BOOL)isScrollEnabled { return _scrollView.scrollEnabled; } - (UIEdgeInsets)scrollViewInset { return _scrollView.contentInset; } - (void)setScrollViewInset:(UIEdgeInsets)scrollViewInset { [_scrollView setContentInset:scrollViewInset]; } - (void)resetScrollViewInset { if (_pinNavigationContainer) { CGFloat offset = [self contentHeight] - self.navigationFooterView.frame.origin.y; self.scrollViewInset = UIEdgeInsetsMake(0.0, 0.0, offset + ORKContentBottomPadding, 0.0); } else { self.scrollViewInset = UIEdgeInsetsZero; } } - (void)scrollToPoint:(CGPoint)point { [_scrollView setContentOffset:point animated:YES]; } // MARK: ScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateEffectViewStylingAndAnimate:YES checkCurrentValue:YES]; } - (void)scrollContentChanged { [self updateEffectViewStylingAndAnimate:NO checkCurrentValue:NO]; } @end