| // 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. |
| @MainActor |
| @objc |
| public class LayoutGuideCenter: NSObject { |
| /// MARK: Public |
| |
| /// References a view under a specific `name`. |
| /// If forcesSynchronousLayoutUpdates is true, when the window coordinates change, the layout guides will be |
| /// updated synchronously. Otherwise, the layout guides will be updated in the next runloop. |
| @objc(referenceView:underName:forcesSynchronousLayoutUpdates:) |
| public func reference( |
| view referenceView: UIView?, under name: String, forcesSynchronousLayoutUpdates: Bool |
| ) { |
| let oldReferenceView = referencedView(under: name) |
| // Early return if `referenceView` is already set. |
| if referenceView == oldReferenceView { |
| return |
| } |
| oldReferenceView?.cr_forcesSynchronousLayoutUpdates = false |
| 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) |
| referenceView?.cr_forcesSynchronousLayoutUpdates = forcesSynchronousLayoutUpdates |
| // 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) |
| } |
| } |
| |
| /// References a view under a specific `name`. |
| @objc(referenceView:underName:) |
| public func reference(view referenceView: UIView?, under name: String) { |
| self.reference(view: referenceView, under: name, forcesSynchronousLayoutUpdates: false) |
| } |
| |
| /// 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 |
| } |
| } |
| } |