chromium/ios/chrome/browser/shared/ui/util/UIView+WindowObserving.swift

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import ObjectiveC
import UIKit

/// Extends UIView to notify via KVO when it moved to a new window.
extension UIView {
  /// MARK: Public

  /// Whether all instances of UIView will notify key-value observers for their `window` property.
  ///
  /// Default is `false`.
  @objc public static var cr_supportsWindowObserving: Bool {
    get {
      return swizzled
    }
    set {
      if swizzled != newValue {
        // Swap implementations.
        guard
          let willMove1 = class_getInstanceMethod(UIView.self, #selector(willMove(toWindow:))),
          let willMove2 = class_getInstanceMethod(UIView.self, #selector(cr_willMove(toWindow:))),
          let didMove1 = class_getInstanceMethod(UIView.self, #selector(didMoveToWindow)),
          let didMove2 = class_getInstanceMethod(UIView.self, #selector(cr_didMoveToWindow)),
          // UITextField's original implementations don't call the super class implementations,
          // which breaks the interposition at the UIView level. Handle UITextField explicitly.
          let textFieldWillMove1 = class_getInstanceMethod(
            UITextField.self, #selector(willMove(toWindow:))),
          let textFieldWillMove2 = class_getInstanceMethod(
            UITextField.self, #selector(cr_willMove(toWindow:))),
          let textFieldDidMove1 = class_getInstanceMethod(
            UITextField.self, #selector(didMoveToWindow)),
          let textFieldDidMove2 = class_getInstanceMethod(
            UITextField.self, #selector(cr_didMoveToWindow))
        else {
          // If it failed here, don't change the `swizzled` state.
          return
        }
        method_exchangeImplementations(willMove1, willMove2)
        method_exchangeImplementations(didMove1, didMove2)
        method_exchangeImplementations(textFieldWillMove1, textFieldWillMove2)
        method_exchangeImplementations(textFieldDidMove1, textFieldDidMove2)
        // Change the `swizzled` state.
        swizzled = newValue
      }
    }
  }

  /// Signals that the `window` key is supported via Manual Change Notification.
  /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-SW3
  @objc class func automaticallyNotifiesObserversOfWindow() -> Bool {
    return false
  }

  /// MARK: Private

  /// Whether the original and alternative implementations have been swapped.
  private static var swizzled = false
}

extension UIView {
  /// Adds a call to the KVO `willChangeValue(forKey:)` method.
  @objc fileprivate func cr_willMove(toWindow newWindow: UIWindow?) {
    cr_willMove(toWindow: newWindow)
    willChangeValue(forKey: "window")
  }

  /// Adds a call to the KVO `didChangeValue(forKey:)` method.
  @objc fileprivate func cr_didMoveToWindow() {
    cr_didMoveToWindow()
    didChangeValue(forKey: "window")
  }
}

extension UITextField {
  /// Adds a call to the KVO `willChangeValue(forKey:)` method.
  ///
  /// This version is necessary, as UITextField's original implementation doesn't call its
  /// superclass implementation, hence escapes the interposition at the UIView level.
  @objc override fileprivate func cr_willMove(toWindow newWindow: UIWindow?) {
    cr_willMove(toWindow: newWindow)
    willChangeValue(forKey: "window")
  }

  /// Adds a call to the KVO `didChangeValue(forKey:)` method.
  ///
  /// This version is necessary, as UITextField's original implementation doesn't call its
  /// superclass implementation, hence escapes the interposition at the UIView level.
  @objc override fileprivate func cr_didMoveToWindow() {
    cr_didMoveToWindow()
    didChangeValue(forKey: "window")
  }
}