948 lines
47 KiB
Objective-C
948 lines
47 KiB
Objective-C
/*
|
|
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<NSLayoutConstraint *> *_topContentImageViewConstraints;
|
|
|
|
NSLayoutConstraint *_navigationContainerViewTopConstraint;
|
|
NSArray<NSLayoutConstraint *> *_navigationContainerViewConstraints;
|
|
NSLayoutConstraint *_customContentWidthConstraint;
|
|
NSLayoutConstraint *_customContentHeightConstraint;
|
|
NSMutableArray<NSLayoutConstraint *> *_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<NSLayoutConstraint *> *)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<NSLayoutConstraint *> *)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<NSLayoutConstraint *> *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
|