chromium/ios/chrome/browser/shared/ui/util/UIView+WindowCoordinates.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 UIKit

/// Lets any UIView communicate when its coordinates in its window change.
///
/// This is useful to know when a view moved on the screen, even when it didn't change frame in its
/// own parent.
///
/// To get notified when the view moved in its window:
///
///     let myView = UIView()
///     myView.cr_onWindowCoordinatesChanged = { view in
///       // Print the window coordinates.
///       print("\(view) moved to \(view convertRect:view.bounds toView:nil)")
///     }
///     let parentView = UIView()
///     parentView.addSubview(myView)
///     let window = UIWindow()
///     window.addSubview(parentView)  // → Calls the closure a first time.
///     parentView.frame = CGRect(x: 10, y: 20, width: 30, height: 40)  // → Calls the closure.
///
///  Even though `myView`'s frame itself was not modified in `parentView`, the closure is called, as
///  actually, `myView` moved transitively in its window.
///
@objc
extension UIView {
  /// MARK: Public

  /// Called when the window coordinates of the view changed.
  ///
  /// The view is passed as argument to the closure. Use it to avoid retaining the view in the
  /// closure, otherwise the view will leak and never get deinitialized.
  @objc public var cr_onWindowCoordinatesChanged: ((UIView) -> Void)? {
    get {
      objc_getAssociatedObject(self, UIView.OnWindowCoordinatesChangedKey) as? (UIView) -> Void
    }
    set {
      objc_setAssociatedObject(
        self, UIView.OnWindowCoordinatesChangedKey, newValue, .OBJC_ASSOCIATION_COPY)
      if newValue != nil {
        // Make sure UIView supports window observing.
        Self.cr_supportsWindowObserving = true
        observation = observe(\.window, options: [.initial]) { [weak self] _, _ in
          guard let self = self else { return }
          if self.window != nil {
            self.addMirrorViewInWindow()
            // Additionally, call the closure here as the view moved to a window.
            self.cr_onWindowCoordinatesChanged?(self)
          } else {
            self.removeMirrorViewInWindow()
          }
        }
      } else {
        observation = nil
      }
    }
  }

  /// MARK: Private

  /// The currently set observation of the window property.
  private var observation: NSKeyValueObservation? {
    get {
      objc_getAssociatedObject(self, UIView.ObservationKey) as? NSKeyValueObservation
    }
    set {
      objc_setAssociatedObject(
        self, UIView.ObservationKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
  }

  /// Inserts a direct subview to the receiver's window, with constraints such that the mirror view
  /// always has the same window coordinates as the receiver. The mirror view calls the
  /// `onWindowCoordinatesChanged` closure when its bounds change.
  private func addMirrorViewInWindow() {
    let mirrorViewInWindow = NotifyingView()
    mirrorViewInWindow.backgroundColor = .clear
    mirrorViewInWindow.isUserInteractionEnabled = false
    mirrorViewInWindow.translatesAutoresizingMaskIntoConstraints = false
    mirrorViewInWindow.onLayoutChanged = { [weak self] _ in
      // Callback on the next turn of the run loop to wait for AutoLayout to have updated the entire
      // hierarchy. (It can happen that AutoLayout updates the mirror view before the mirrored
      // view.)
      DispatchQueue.main.async {
        guard let self = self else { return }
        self.cr_onWindowCoordinatesChanged?(self)
      }
    }

    guard let window = window else { fatalError() }
    window.insertSubview(mirrorViewInWindow, at: 0)
    NSLayoutConstraint.activate([
      mirrorViewInWindow.topAnchor.constraint(equalTo: topAnchor),
      mirrorViewInWindow.bottomAnchor.constraint(equalTo: bottomAnchor),
      mirrorViewInWindow.leadingAnchor.constraint(equalTo: leadingAnchor),
      mirrorViewInWindow.trailingAnchor.constraint(equalTo: trailingAnchor),
    ])

    self.mirrorViewInWindow = mirrorViewInWindow
  }

  /// Removes the mirror view added by a call to `addMirrorViewInWindow`.
  private func removeMirrorViewInWindow() {
    mirrorViewInWindow?.onLayoutChanged = nil
    mirrorViewInWindow?.removeFromSuperview()
    mirrorViewInWindow = nil
  }

  /// The currently set mirror view.
  private var mirrorViewInWindow: NotifyingView? {
    get {
      objc_getAssociatedObject(self, UIView.MirrorViewInWindowKey) as? NotifyingView
    }
    set {
      objc_setAssociatedObject(
        self, UIView.MirrorViewInWindowKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
    }
  }

  /// A simple view that calls a closure when its bounds and center changed.
  private class NotifyingView: UIView {
    var onLayoutChanged: ((UIView) -> Void)?

    override var bounds: CGRect {
      didSet {
        onLayoutChanged?(self)
      }
    }

    override var center: CGPoint {
      didSet {
        onLayoutChanged?(self)
      }
    }
  }

  /// Keys for storing associated objects.
  @UniqueAddress private static var OnWindowCoordinatesChangedKey
  @UniqueAddress private static var ObservationKey
  @UniqueAddress private static var MirrorViewInWindowKey
}

/// A property wrapper to more safely support associated object keys.
/// https://github.com/atrick/swift-evolution/blob/diagnose-implicit-raw-bitwise/proposals/nnnn-implicit-raw-bitwise-conversion.md#associated-object-string-keys
@propertyWrapper
struct UniqueAddress {
  private var _placeholder: Int8 = 0

  var wrappedValue: UnsafeRawPointer {
    mutating get {
      // This is "ok" only as long as the wrapped property appears inside of something with a stable
      // address (a global/static variable or class property) and the pointer is never read or
      // written through, only used for its unique value.
      return withUnsafeBytes(of: &self) {
        return $0.baseAddress.unsafelyUnwrapped
      }
    }
  }
}