chromium/ios/chrome/browser/shared/ui/util/frame_layout_guide.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

/// A layout guide with two distinct additions:
/// 1.  encapsulates the KVO on its owning view's window and triggers a callback closure;
/// 2.  can be constrained to a fixed frame in its owning view coordinates.
///
/// 1. To get notified when the layout guide is added to a new window:
///
///     let layoutGuide = FrameLayoutGuide()
///     layoutGuide.onDidMoveToWindow = { guide in
///       if let window = guide.owningView?.window {
///         print("\(guide) moved to \(guide.owningView?.window)")
///       } else {
///         print("\(guide) was removed from its window")
///       }
///     }
///
///  2. To constrain a frame to the layout guide:
///
///      let layoutGuide = FrameLayoutGuide()
///      layoutGuide.constrainedFrame = CGRect(x: 10, y: 20, width: 30, height: 40)
///
///  The layout guide can then be used as an anchor to place elements related to its position.
@objc
public class FrameLayoutGuide: UILayoutGuide {
  /// MARK: Public

  /// Called when the layout guide's owning view moved to a new window (or was removed from its
  /// window).
  ///
  /// The layout guide is passed as argument to the closure. Use it to avoid retaining the layout
  /// guide in the closure, otherwise the layout guide will leak and never get deinitialized.
  @objc public var onDidMoveToWindow: ((UILayoutGuide) -> Void)?

  /// The frame to force on this layout guide.
  @objc public var constrainedFrame: CGRect {
    get {
      constrainedFrameView.frame
    }
    set {
      constrainedFrameView.frame = newValue
    }
  }

  /// MARK: Private

  /// When `owningView` changes, remove `constrainedFrameView` from the old to the new view, then
  /// reset the constraints anchoring the layout guide on `constrainedFrameView`.
  override open var owningView: UIView? {
    willSet {
      constrainedFrameView.removeFromSuperview()
    }
    didSet {
      if let owningView = owningView {
        owningView.addSubview(constrainedFrameView)
        NSLayoutConstraint.activate([
          leadingAnchor.constraint(equalTo: constrainedFrameView.leadingAnchor),
          trailingAnchor.constraint(equalTo: constrainedFrameView.trailingAnchor),
          topAnchor.constraint(equalTo: constrainedFrameView.topAnchor),
          bottomAnchor.constraint(equalTo: constrainedFrameView.bottomAnchor),
        ])
      }
    }
  }

  /// The observation of the owning view's window property. It's an optional variable because it
  /// can't be initialized before `self` is. See `init`.
  private var observation: NSKeyValueObservation?

  /// The view to inject in the owning view. Its frame will be set to `constrainedFrame`. The layout
  /// guide will anchor itself on that view.
  private let constrainedFrameView: UIView

  @objc
  public override init() {
    constrainedFrameView = UIView()
    constrainedFrameView.backgroundColor = .clear
    constrainedFrameView.isUserInteractionEnabled = false
    super.init()

    // Make sure UIView supports window observing.
    UIView.cr_supportsWindowObserving = true
    // Start observing. It is not possible to initialize `observation` before `super.init()` because
    // the call to `observe(_:changeHandler:)` requires `self` to be initialized.
    // https://developer.apple.com/documentation/swift/cocoa_design_patterns/using_key-value_observing_in_swift
    observation = observe(\.owningView?.window, options: [.old, .new]) { layoutGuide, change in
      // Filter out changes where owning view changed but not the window. This can happen if a
      // layout guide is moved to a different owning view whose window is the same.
      if change.oldValue != change.newValue {
        layoutGuide.onDidMoveToWindow?(layoutGuide)
      }
    }
  }

  required init(coder aDecoder: NSCoder) {
    fatalError("Not using storyboards")
  }
}