306 lines
8.8 KiB
Swift
306 lines
8.8 KiB
Swift
//
|
||
// 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
|
||
}
|
||
|
||
convenience public 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 = minHeight {
|
||
view.autoSetDimension(.height, toSize: minHeight, relation: .greaterThanOrEqual)
|
||
}
|
||
if let maxHeight = 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
|
||
}
|
||
|
||
func addRedBorder() {
|
||
addBorder(with: .red)
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|