// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/web_state/ui/crw_web_view_scroll_view_delegate_proxy.h"
#import <ostream>
#import "base/check_op.h"
#import "base/ios/crb_protocol_observers.h"
#import "ios/web/web_state/ui/crw_web_view_scroll_view_proxy+internal.h"
@interface CRWWebViewScrollViewDelegateProxy ()
@property(nonatomic, weak) CRWWebViewScrollViewProxy* scrollViewProxy;
// Return YES if the user is currently performing a zoom gestures.
@property(nonatomic, assign) BOOL userIsZooming;
@end
// Calls to methods supported by CRWWebViewScrollViewProxyObserver are forwarded
// to both of the delegate and the observers of self.scrollViewProxy.
// Calls to other methods are forwarded only to self.delegateOfProxy
// using -methodSignatoreForSelector: and -forwardInvocation:.
@implementation CRWWebViewScrollViewDelegateProxy
- (instancetype)initWithScrollViewProxy:
(CRWWebViewScrollViewProxy*)scrollViewProxy {
self = [super init];
if (self) {
_scrollViewProxy = scrollViewProxy;
}
return self;
}
#pragma mark - NSObject
- (BOOL)respondsToSelector:(SEL)aSelector {
// This class forwards unimplemented methods to the delegate of the scroll
// view proxy. So it also responds to methods defined in the delegate of the
// scroll view proxy.
return [self.delegateOfProxy respondsToSelector:aSelector] ||
[super respondsToSelector:aSelector];
}
#pragma mark Forwards unimplemented methods
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
// Called when the method is not implemented in this class. Forwards the
// method to the delegate of the scroll view proxy.
// This cast is necessary because -methodSignatureForSelector: is a method of
// NSObject. It is pretty safe to assume that the delegate is an instance of
// NSObject.
NSObject* delegateAsObject = static_cast<NSObject*>(self.delegateOfProxy);
return [delegateAsObject methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation*)invocation {
// Called when the method is not implemented in this class. Forwards the
// method to the delegate of the scroll view proxy.
// Replaces the `sender` argument of the delegate method call with
// [self.scrollViewProxy asUIScrollView]. `sender` should be the first
// argument of every delegate method according to Apple's style guide:
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CodingGuidelines/Articles/NamingMethods.html#//apple_ref/doc/uid/20001282-BCIGIJJF
// and it is true for all methods of UIScrollViewDelegate as of today. But
// here performs a few safety checks to make sure that the first argument is
// `sender`:
// - The method has at least one argument
// - The first argument is typed UIScrollView
// - The first argument is equal to the underlying scroll view
//
// Note that the first (normal) argument is at index 2. Index 0 and 1 are for
// self and _cmd respectively.
NSMethodSignature* signature = invocation.methodSignature;
if (signature.numberOfArguments >= 3 &&
strcmp([signature getArgumentTypeAtIndex:2], @encode(UIScrollView*)) ==
0) {
__unsafe_unretained UIScrollView* sender;
[invocation getArgument:&sender atIndex:2];
if (sender == self.scrollViewProxy.underlyingScrollView) {
sender = [self.scrollViewProxy asUIScrollView];
[invocation setArgument:&sender atIndex:2];
}
}
[invocation invokeWithTarget:self.delegateOfProxy];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
if ([self.delegateOfProxy
respondsToSelector:@selector(scrollViewDidScroll:)]) {
[self.delegateOfProxy
scrollViewDidScroll:[self.scrollViewProxy asUIScrollView]];
}
[self.scrollViewProxy.observers
webViewScrollViewDidScroll:self.scrollViewProxy];
}
- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
if ([self.delegateOfProxy
respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
[self.delegateOfProxy
scrollViewWillBeginDragging:[self.scrollViewProxy asUIScrollView]];
}
[self.scrollViewProxy.observers
webViewScrollViewWillBeginDragging:self.scrollViewProxy];
}
- (void)scrollViewWillEndDragging:(UIScrollView*)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint*)targetContentOffset {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
if ([self.delegateOfProxy respondsToSelector:@selector
(scrollViewWillEndDragging:
withVelocity:targetContentOffset:)]) {
[self.delegateOfProxy
scrollViewWillEndDragging:[self.scrollViewProxy asUIScrollView]
withVelocity:velocity
targetContentOffset:targetContentOffset];
}
[self.scrollViewProxy.observers
webViewScrollViewWillEndDragging:self.scrollViewProxy
withVelocity:velocity
targetContentOffset:targetContentOffset];
}
- (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
willDecelerate:(BOOL)decelerate {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
if ([self.delegateOfProxy respondsToSelector:@selector
(scrollViewDidEndDragging:willDecelerate:)]) {
[self.delegateOfProxy
scrollViewDidEndDragging:[self.scrollViewProxy asUIScrollView]
willDecelerate:decelerate];
}
[self.scrollViewProxy.observers
webViewScrollViewDidEndDragging:self.scrollViewProxy
willDecelerate:decelerate];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
if ([self.delegateOfProxy
respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) {
[self.delegateOfProxy
scrollViewDidEndDecelerating:[self.scrollViewProxy asUIScrollView]];
}
[self.scrollViewProxy.observers
webViewScrollViewDidEndDecelerating:self.scrollViewProxy];
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
if ([self.delegateOfProxy
respondsToSelector:@selector(scrollViewDidEndScrollingAnimation:)]) {
[self.delegateOfProxy
scrollViewDidEndScrollingAnimation:[self.scrollViewProxy
asUIScrollView]];
}
[self.scrollViewProxy.observers
webViewScrollViewDidEndScrollingAnimation:self.scrollViewProxy];
}
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
__block BOOL shouldScrollToTop = YES;
if ([self.delegateOfProxy
respondsToSelector:@selector(scrollViewShouldScrollToTop:)]) {
shouldScrollToTop =
shouldScrollToTop &&
[self.delegateOfProxy
scrollViewShouldScrollToTop:[self.scrollViewProxy asUIScrollView]];
}
[self.scrollViewProxy.observers executeOnObservers:^(id observer) {
if ([observer respondsToSelector:@selector
(webViewScrollViewShouldScrollToTop:)]) {
shouldScrollToTop =
shouldScrollToTop &&
[observer webViewScrollViewShouldScrollToTop:self.scrollViewProxy];
}
}];
return shouldScrollToTop;
}
- (void)scrollViewDidZoom:(UIScrollView*)scrollView {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
if ([self.delegateOfProxy respondsToSelector:@selector(scrollViewDidZoom:)]) {
[self.delegateOfProxy
scrollViewDidZoom:[self.scrollViewProxy asUIScrollView]];
}
if (self.userIsZooming) {
[self.scrollViewProxy.observers
webViewScrollViewDidZoom:self.scrollViewProxy];
} else {
if (@available(iOS 16.0, *)) {
// In iOS < 16 versions, changing the value of `zoomScale` calls
// `scrollViewDidZoom`.
scrollView.zoomScale = scrollView.minimumZoomScale;
}
}
}
- (void)scrollViewWillBeginZooming:(UIScrollView*)scrollView
withView:(UIView*)view {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
self.userIsZooming = YES;
if ([self.delegateOfProxy
respondsToSelector:@selector(scrollViewWillBeginZooming:withView:)]) {
[self.delegateOfProxy
scrollViewWillBeginZooming:[self.scrollViewProxy asUIScrollView]
withView:view];
}
[self.scrollViewProxy.observers
webViewScrollViewWillBeginZooming:self.scrollViewProxy];
}
- (void)scrollViewDidEndZooming:(UIScrollView*)scrollView
withView:(UIView*)view
atScale:(CGFloat)scale {
DCHECK_EQ(self.scrollViewProxy.underlyingScrollView, scrollView);
self.userIsZooming = NO;
if ([self.delegateOfProxy respondsToSelector:@selector
(scrollViewDidEndZooming:withView:atScale:)]) {
[self.delegateOfProxy
scrollViewDidEndZooming:[self.scrollViewProxy asUIScrollView]
withView:view
atScale:scale];
}
[self.scrollViewProxy.observers
webViewScrollViewDidEndZooming:self.scrollViewProxy
atScale:scale];
}
#pragma mark - Helpers
// The delegate of the scroll view proxy.
- (id<UIScrollViewDelegate>)delegateOfProxy {
return [self.scrollViewProxy asUIScrollView].delegate;
}
@end