276 lines
9.4 KiB
Objective-C
276 lines
9.4 KiB
Objective-C
// MXScrollView.m
|
|
//
|
|
// Copyright (c) 2019 Maxime Epain
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
#import "MXScrollView.h"
|
|
|
|
@interface MXScrollViewDelegateForwarder : NSObject <MXScrollViewDelegate>
|
|
@property (nonatomic,weak) id<MXScrollViewDelegate> delegate;
|
|
@end
|
|
|
|
@interface MXScrollView () <UIGestureRecognizerDelegate>
|
|
@property (nonatomic, strong) MXScrollViewDelegateForwarder *forwarder;
|
|
@property (nonatomic, strong) NSMutableArray<UIScrollView *> *observedViews;
|
|
@end
|
|
|
|
@implementation MXScrollView {
|
|
BOOL _isObserving;
|
|
BOOL _lock;
|
|
}
|
|
|
|
static void * const kMXScrollViewKVOContext = (void*)&kMXScrollViewKVOContext;
|
|
|
|
@synthesize delegate = _delegate;
|
|
@synthesize bounces = _bounces;
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
self = [super initWithFrame: frame];
|
|
if (self) {
|
|
[self initialize];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithCoder:(NSCoder *)coder {
|
|
self = [super initWithCoder:coder];
|
|
if (self) {
|
|
[self initialize];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)initialize {
|
|
self.forwarder = [MXScrollViewDelegateForwarder new];
|
|
super.delegate = self.forwarder;
|
|
|
|
self.showsVerticalScrollIndicator = NO;
|
|
self.directionalLockEnabled = YES;
|
|
self.bounces = YES;
|
|
|
|
self.panGestureRecognizer.cancelsTouchesInView = NO;
|
|
|
|
self.observedViews = [NSMutableArray array];
|
|
|
|
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(contentOffset))
|
|
options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
|
|
context:kMXScrollViewKVOContext];
|
|
_isObserving = YES;
|
|
}
|
|
|
|
#pragma mark Properties
|
|
|
|
- (void)setDelegate:(id<MXScrollViewDelegate>)delegate {
|
|
self.forwarder.delegate = delegate;
|
|
// Scroll view delegate caches whether the delegate responds to some of the delegate
|
|
// methods, so we need to force it to re-evaluate if the delegate responds to them
|
|
super.delegate = nil;
|
|
super.delegate = self.forwarder;
|
|
}
|
|
|
|
- (id<MXScrollViewDelegate>)delegate {
|
|
return self.forwarder.delegate;
|
|
}
|
|
|
|
#pragma mark <UIGestureRecognizerDelegate>
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
|
|
if (otherGestureRecognizer.view == self) {
|
|
return NO;
|
|
}
|
|
|
|
// Ignore other gesture than pan
|
|
if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
|
|
return NO;
|
|
}
|
|
|
|
// Lock horizontal pan gesture.
|
|
CGPoint velocity = [(UIPanGestureRecognizer*)gestureRecognizer velocityInView:self];
|
|
if (fabs(velocity.x) > fabs(velocity.y)) {
|
|
return NO;
|
|
}
|
|
|
|
UIView *otherView = otherGestureRecognizer.view;
|
|
// WKWebView on he MXScrollView
|
|
if ([otherView isKindOfClass:NSClassFromString(@"WKContentView")]) {
|
|
otherView = otherView.superview;
|
|
}
|
|
// Consider scroll view pan only
|
|
if (![otherView isKindOfClass:[UIScrollView class]]) {
|
|
return NO;
|
|
}
|
|
|
|
UIScrollView *scrollView = (id)otherView;
|
|
|
|
// Tricky case: UITableViewWrapperView
|
|
if ([scrollView.superview isKindOfClass:[UITableView class]]) {
|
|
return NO;
|
|
}
|
|
//tableview on the MXScrollView
|
|
if ([scrollView.superview isKindOfClass:NSClassFromString(@"UITableViewCellContentView")]) {
|
|
return NO;
|
|
}
|
|
|
|
BOOL shouldScroll = YES;
|
|
if ([self.delegate respondsToSelector:@selector(scrollView:shouldScrollWithSubView:)]) {
|
|
shouldScroll = [self.delegate scrollView:self shouldScrollWithSubView:scrollView];;
|
|
}
|
|
|
|
if (shouldScroll) {
|
|
[self addObservedView:scrollView];
|
|
}
|
|
|
|
return shouldScroll;
|
|
}
|
|
|
|
#pragma mark KVO
|
|
|
|
- (void)addObserverToView:(UIScrollView *)scrollView {
|
|
_lock = (scrollView.contentOffset.y > -scrollView.contentInset.top);
|
|
|
|
[scrollView addObserver:self
|
|
forKeyPath:NSStringFromSelector(@selector(contentOffset))
|
|
options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew
|
|
context:kMXScrollViewKVOContext];
|
|
}
|
|
|
|
- (void)removeObserverFromView:(UIScrollView *)scrollView {
|
|
@try {
|
|
[scrollView removeObserver:self
|
|
forKeyPath:NSStringFromSelector(@selector(contentOffset))
|
|
context:kMXScrollViewKVOContext];
|
|
}
|
|
@catch (NSException *exception) {}
|
|
}
|
|
|
|
//This is where the magic happens...
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
|
|
|
if (context == kMXScrollViewKVOContext && [keyPath isEqualToString:NSStringFromSelector(@selector(contentOffset))]) {
|
|
|
|
CGPoint new = [[change objectForKey:NSKeyValueChangeNewKey] CGPointValue];
|
|
CGPoint old = [[change objectForKey:NSKeyValueChangeOldKey] CGPointValue];
|
|
CGFloat diff = old.y - new.y;
|
|
|
|
if (diff == 0.0 || !_isObserving) return;
|
|
|
|
if (object == self) {
|
|
|
|
//Adjust self scroll offset when scroll down
|
|
if (diff > 0 && _lock) {
|
|
[self scrollView:self setContentOffset:old];
|
|
|
|
} else if (self.contentOffset.y < -self.contentInset.top && !self.bounces) {
|
|
[self scrollView:self setContentOffset:CGPointMake(self.contentOffset.x, -self.contentInset.top)];
|
|
} else if (self.contentOffset.y > -self.parallaxHeader.minimumHeight) {
|
|
[self scrollView:self setContentOffset:CGPointMake(self.contentOffset.x, -self.parallaxHeader.minimumHeight)];
|
|
}
|
|
|
|
} else {
|
|
//Adjust the observed scrollview's content offset
|
|
UIScrollView *scrollView = object;
|
|
_lock = (scrollView.contentOffset.y > -scrollView.contentInset.top);
|
|
|
|
//Manage scroll up
|
|
if (self.contentOffset.y < -self.parallaxHeader.minimumHeight && _lock && diff < 0) {
|
|
[self scrollView:scrollView setContentOffset:old];
|
|
}
|
|
//Disable bouncing when scroll down
|
|
if (!_lock && ((self.contentOffset.y > -self.contentInset.top) || self.bounces)) {
|
|
[self scrollView:scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, -scrollView.contentInset.top)];
|
|
}
|
|
}
|
|
} else {
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
}
|
|
}
|
|
|
|
#pragma mark Scrolling views handlers
|
|
|
|
- (void)addObservedView:(UIScrollView *)scrollView {
|
|
if (![self.observedViews containsObject:scrollView]) {
|
|
[self.observedViews addObject:scrollView];
|
|
[self addObserverToView:scrollView];
|
|
}
|
|
}
|
|
|
|
- (void)removeObservedViews {
|
|
for (UIScrollView *scrollView in self.observedViews) {
|
|
[self removeObserverFromView:scrollView];
|
|
}
|
|
[self.observedViews removeAllObjects];
|
|
}
|
|
|
|
- (void)scrollView:(UIScrollView *)scrollView setContentOffset:(CGPoint)offset {
|
|
_isObserving = NO;
|
|
scrollView.contentOffset = offset;
|
|
_isObserving = YES;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self removeObserver:self forKeyPath:NSStringFromSelector(@selector(contentOffset)) context:kMXScrollViewKVOContext];
|
|
[self removeObservedViews];
|
|
}
|
|
|
|
#pragma mark <UIScrollViewDelegate>
|
|
|
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
|
_lock = NO;
|
|
[self removeObservedViews];
|
|
}
|
|
|
|
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
|
|
if (!decelerate) {
|
|
_lock = NO;
|
|
[self removeObservedViews];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation MXScrollViewDelegateForwarder
|
|
|
|
- (BOOL)respondsToSelector:(SEL)selector {
|
|
return [self.delegate respondsToSelector:selector] || [super respondsToSelector:selector];
|
|
}
|
|
|
|
- (void)forwardInvocation:(NSInvocation *)invocation {
|
|
[invocation invokeWithTarget:self.delegate];
|
|
}
|
|
|
|
#pragma mark <UIScrollViewDelegate>
|
|
|
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
|
[(MXScrollView *)scrollView scrollViewDidEndDecelerating:scrollView];
|
|
if ([self.delegate respondsToSelector:_cmd]) {
|
|
[self.delegate scrollViewDidEndDecelerating:scrollView];
|
|
}
|
|
}
|
|
|
|
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
|
|
[(MXScrollView *)scrollView scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
|
|
if ([self.delegate respondsToSelector:_cmd]) {
|
|
[self.delegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
|
|
}
|
|
}
|
|
|
|
@end
|