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

/// Globally maps reference views to layout guides.
///
/// Usage:
/// 1.  Reference a view from a part of the UI under a specific name.
/// 2.  In a different part of the UI, request a layout guide under that name. Add the synced layout
///     guide to the UI. It will track the reference view automatically.
///
/// Details:
/// -   Referenced views and created layout guides are only weakly stored by the layout guide
///     center.
/// -   Referenced views don't have to be laid out by AutoLayout.
/// -   Referenced views and layout guides don't have to be in the same window.
@objc
public class LayoutGuideCenter: NSObject {
  /// MARK: Public

  /// References a view under a specific `name`.
  @objc(referenceView:underName:)
  public func reference(view referenceView: UIView?, under name: String) {
    let oldReferenceView = referencedView(under: name)
    // Early return if `referenceView` is already set.
    if referenceView == oldReferenceView {
      return
    }
    oldReferenceView?.cr_onWindowCoordinatesChanged = nil
    if let referenceView = referenceView {
      referenceViews.setObject(referenceView, forKey: name as NSString)
    } else {
      referenceViews.removeObject(forKey: name as NSString)
    }
    updateGuides(named: name)
    // Schedule updates to the matching layout guides when the reference view
    // moves in its window.
    referenceView?.cr_onWindowCoordinatesChanged = { [weak self] _ in
      self?.updateGuides(named: name)
    }
  }

  /// Returns the referenced view under `name`.
  @objc(referencedViewUnderName:)
  public func referencedView(under name: String) -> UIView? {
    return referenceViews.object(forKey: name as NSString)
  }

  /// Creates a new layout guide tracking the view referenced under a specific `name`.
  ///
  /// If the referenced view doesn't exist or isn't yet part of the view hierarchy, it is still
  /// valid to call this method. The layout guide will be updated as soon as the referenced view
  /// is available.
  @objc(makeLayoutGuideNamed:)
  public func makeLayoutGuide(named name: String) -> UILayoutGuide {
    let layoutGuide = FrameLayoutGuide()
    layoutGuide.onDidMoveToWindow = { [weak self] _ in
      self?.updateGuides(named: name)
    }
    // Keep a weak reference to the layout guide.
    if layoutGuides[name] == nil {
      layoutGuides[name] = NSHashTable<FrameLayoutGuide>.weakObjects()
    }
    layoutGuides[name]?.add(layoutGuide)
    return layoutGuide
  }

  /// MARK: Private

  /// Weakly stores the reference views.
  private var referenceViews = NSMapTable<NSString, UIView>.strongToWeakObjects()
  /// Weakly stores the layout guides.
  private var layoutGuides = [String: NSHashTable<FrameLayoutGuide>]()

  /// Updates all available guides for the given `name`.
  private func updateGuides(named name: String) {
    // Early return if there is no reference window.
    guard let referenceView = referencedView(under: name) else { return }
    guard let referenceWindow = referenceView.window else { return }
    // Early return if there are no layout guides.
    guard let layoutGuidesForName = layoutGuides[name] else { return }

    for layoutGuide in layoutGuidesForName.allObjects {
      // Skip if there is no owning window.
      guard let owningView = layoutGuide.owningView else { continue }
      guard let owningWindow = owningView.window else { continue }

      let frameInReferenceWindow = referenceView.convert(referenceView.bounds, to: nil)
      let frameInOwningWindow = referenceWindow.convert(frameInReferenceWindow, to: owningWindow)
      let frameInOwningView = owningView.convert(frameInOwningWindow, from: nil)
      layoutGuide.constrainedFrame = frameInOwningView
    }
  }
}