// 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")
}
}