jsdw_ios/QuickLocation/UIKit/MXParallaxHeader/MXScrollView.m

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