chromium/ios/web/web_state/ui/crw_web_view_scroll_view_delegate_proxy.mm

// 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