Path: blob/main/SignalUI/UIKitExtensions/UIView+SignalUI.swift
1 views
//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public import SignalServiceKit
import UIKit
// MARK: - SpacerView
public class SpacerView: UIView {
private var preferredSize: CGSize
override open class var layerClass: AnyClass {
CATransformLayer.self
}
public convenience init(preferredWidth: CGFloat = UIView.noIntrinsicMetric, preferredHeight: CGFloat = UIView.noIntrinsicMetric) {
self.init(preferredSize: CGSize(width: preferredWidth, height: preferredHeight))
}
public init(preferredSize: CGSize = CGSize(square: UIView.noIntrinsicMetric)) {
self.preferredSize = preferredSize
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public var intrinsicContentSize: CGSize {
get { preferredSize }
set { preferredSize = newValue }
}
}
// MARK: -
public extension UIView {
class func spacer(withWidth width: CGFloat) -> UIView {
let view = TransparentView()
view.autoSetDimension(.width, toSize: width)
return view
}
class func spacer(withHeight height: CGFloat) -> UIView {
let view = TransparentView()
view.autoSetDimension(.height, toSize: height)
return view
}
class func spacer(matchingHeightOf matchView: UIView, withMultiplier multiplier: CGFloat) -> UIView {
let spacer = TransparentView()
spacer.autoMatch(.height, to: .height, of: matchView, withMultiplier: multiplier)
return spacer
}
class func hStretchingSpacer() -> UIView {
let view = TransparentView()
view.setContentHuggingHorizontalLow()
view.setCompressionResistanceHorizontalLow()
return view
}
class func vStretchingSpacer(minHeight: CGFloat? = nil, maxHeight: CGFloat? = nil) -> UIView {
let view = TransparentView()
view.setContentHuggingVerticalLow()
view.setCompressionResistanceVerticalLow()
if let minHeight {
view.autoSetDimension(.height, toSize: minHeight, relation: .greaterThanOrEqual)
}
if let maxHeight {
NSLayoutConstraint.autoSetPriority(.defaultLow) {
view.autoSetDimension(.height, toSize: maxHeight)
}
}
return view
}
class func transparentSpacer() -> UIView {
let view = TransparentView()
view.setContentHuggingHorizontalLow()
view.setCompressionResistanceHorizontalLow()
return view
}
class TransparentView: UIView {
override open class var layerClass: AnyClass {
CATransformLayer.self
}
}
func setShadow(radius: CGFloat = 2.0, opacity: Float = 0.66, offset: CGSize = .zero, color: UIColor = UIColor.black) {
layer.shadowRadius = radius
layer.shadowOpacity = opacity
layer.shadowOffset = offset
layer.shadowColor = color.cgColor
}
class func accessibilityIdentifier(in container: NSObject, name: String) -> String {
"\(type(of: container)).\(name)"
}
class func accessibilityIdentifier(containerName: String, name: String) -> String {
"\(containerName).\(name)"
}
func setAccessibilityIdentifier(in container: NSObject, name: String) {
self.accessibilityIdentifier = UIView.accessibilityIdentifier(in: container, name: name)
}
func removeAllSubviews() {
for subview in subviews {
subview.removeFromSuperview()
}
}
var sizeThatFitsMaxSize: CGSize {
sizeThatFits(CGSize(
width: CGFloat.greatestFiniteMagnitude,
height: CGFloat.greatestFiniteMagnitude,
))
}
static func container() -> UIView {
let view = UIView()
view.layoutMargins = .zero
return view
}
// If the container doesn't need a background color, it's
// more efficient to use a non-rendering view.
static func transparentContainer() -> UIView {
let view = TransparentView()
view.layoutMargins = .zero
return view
}
func addBorder(with color: UIColor) {
layer.borderColor = color.cgColor
layer.borderWidth = 1
}
@discardableResult
func addRedBorder() -> Self {
addBorder(with: .red)
return self
}
func addCircleBadge(color: UIColor) {
let badge = OWSLayerView.circleView(size: 12)
badge.backgroundColor = color
self.addSubview(badge)
badge.autoPinEdge(toSuperviewEdge: .top, withInset: -3)
badge.autoPinEdge(toSuperviewEdge: .trailing, withInset: -3)
}
}
// MARK: - Manual Layout
public extension UIView {
var left: CGFloat { frame.minX }
var right: CGFloat { frame.maxX }
var top: CGFloat { frame.minY }
var bottom: CGFloat { frame.maxY }
var width: CGFloat { frame.width }
var height: CGFloat { frame.height }
}
// MARK: - Debug
#if DEBUG
public extension UIView {
func logFrame(withLabel label: String = "") {
Logger.verbose("\(label) \(Self.self) \(accessibilityLabel ?? "") frame: \(frame), hidden: \(isHidden), opacity: \(layer.opacity), layoutMargins: \(layoutMargins)")
}
func logFrameLater(withLabel label: String = "") {
DispatchQueue.main.async {
self.logFrame(withLabel: label)
}
}
func logHierarchyUpward(withLabel label: String) {
let prefix = "\(label) ----"
DispatchQueue.main.async {
Logger.verbose(prefix)
}
traverseHierarchyUpward { view in
view.logFrame(withLabel: prefix.appending("\t"))
}
}
func logHierarchyUpwardLater(withLabel label: String) {
let prefix = "\(label) ----"
DispatchQueue.main.async {
Logger.verbose(prefix)
}
traverseHierarchyUpward { view in
view.logFrameLater(withLabel: prefix.appending("\t"))
}
}
func logHierarchyDownward(withLabel label: String) {
let prefix = "\(label) ----"
DispatchQueue.main.async {
Logger.verbose(prefix)
}
traverseHierarchyDownward { view in
view.logFrame(withLabel: prefix.appending("\t"))
}
}
func logHierarchyDownwardLater(withLabel label: String) {
let prefix = "\(label) ----"
DispatchQueue.main.async {
Logger.verbose(prefix)
}
traverseHierarchyDownward { view in
view.logFrameLater(withLabel: prefix.appending("\t"))
}
}
}
#endif
// MARK: - Misc
public extension UIView {
typealias UIViewVisitorBlock = (UIView) -> Void
func traverseHierarchyUpward(with visitor: UIViewVisitorBlock) {
AssertIsOnMainThread()
visitor(self)
var responder: UIResponder? = self
while responder != nil {
if let view = responder as? UIView {
visitor(view)
}
responder = responder?.next
}
}
func traverseHierarchyDownward(with visitor: UIViewVisitorBlock) {
AssertIsOnMainThread()
visitor(self)
for subview in subviews {
subview.traverseHierarchyDownward(with: visitor)
}
}
func firstAncestor<T>(ofType type: T.Type) -> T? {
guard let superview else { return nil }
return superview as? T ?? superview.firstAncestor(ofType: type)
}
/// Returns a Boolean value indicating whether a gesture is located
/// within the bounds of the receiver, optionally inset by a hot area.
/// - Parameters:
/// - gestureRecognizer: The gesture to check the location of.
/// - hotAreaInsets: A hot area to inset the view's bounds by when checking
/// location. **Use negative inset values to increase tappable area.**
/// - Returns: true if `gestureRecognizer` is inside the receiver’s bounds
/// inset by `hotAreaInsets`; otherwise, false.
func containsGestureLocation(
_ gestureRecognizer: UIGestureRecognizer,
hotAreaInsets: UIEdgeInsets? = nil,
) -> Bool {
let location = gestureRecognizer.location(in: self)
var hotArea = bounds
if let hotAreaInsets {
owsAssertDebug(hotAreaInsets.isNonEmpty)
// Permissive hot area to make it easier to perform gesture.
hotArea = hotArea.inset(by: hotAreaInsets)
}
return hotArea.contains(location)
}
}
// MARK: - Bottom Stroke
public extension UIView {
func addBottomStroke() -> UIView {
return addBottomStroke(color: .ows_middleGray, strokeWidth: .hairlineWidth)
}
@discardableResult
func addBottomStroke(color: UIColor, strokeWidth: CGFloat) -> UIView {
let strokeView = UIView()
strokeView.backgroundColor = color
addSubview(strokeView)
strokeView.autoSetDimension(.height, toSize: strokeWidth)
strokeView.autoPinWidthToSuperview()
strokeView.autoPinEdge(toSuperviewEdge: .bottom)
return strokeView
}
}
// MARK: -
public extension UIApplication {
func hideKeyboard() {
sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
}
}