// UIViewExtensions.swift - Copyright 2024 SwifterSwift
|
|
#if canImport(UIKit) && !os(watchOS)
|
import UIKit
|
|
// MARK: - enums
|
|
public extension UIView {
|
/// SwifterSwift: Shake directions of a view.
|
///
|
/// - horizontal: Shake left and right.
|
/// - vertical: Shake up and down.
|
enum ShakeDirection {
|
/// SwifterSwift: Shake left and right.
|
case horizontal
|
|
/// SwifterSwift: Shake up and down.
|
case vertical
|
}
|
|
/// SwifterSwift: Angle units.
|
///
|
/// - degrees: degrees.
|
/// - radians: radians.
|
enum AngleUnit {
|
/// SwifterSwift: degrees.
|
case degrees
|
|
/// SwifterSwift: radians.
|
case radians
|
}
|
|
/// SwifterSwift: Shake animations types.
|
///
|
/// - linear: linear animation.
|
/// - easeIn: easeIn animation.
|
/// - easeOut: easeOut animation.
|
/// - easeInOut: easeInOut animation.
|
enum ShakeAnimationType {
|
/// SwifterSwift: linear animation.
|
case linear
|
|
/// SwifterSwift: easeIn animation.
|
case easeIn
|
|
/// SwifterSwift: easeOut animation.
|
case easeOut
|
|
/// SwifterSwift: easeInOut animation.
|
case easeInOut
|
}
|
|
/// SwifterSwift: Add gradient directions
|
struct GradientDirection: Sendable {
|
public static let topToBottom = GradientDirection(startPoint: CGPoint(x: 0.5, y: 0.0),
|
endPoint: CGPoint(x: 0.5, y: 1.0))
|
public static let bottomToTop = GradientDirection(startPoint: CGPoint(x: 0.5, y: 1.0),
|
endPoint: CGPoint(x: 0.5, y: 0.0))
|
public static let leftToRight = GradientDirection(startPoint: CGPoint(x: 0.0, y: 0.5),
|
endPoint: CGPoint(x: 1.0, y: 0.5))
|
public static let rightToLeft = GradientDirection(startPoint: CGPoint(x: 1.0, y: 0.5),
|
endPoint: CGPoint(x: 0.0, y: 0.5))
|
|
public let startPoint: CGPoint
|
public let endPoint: CGPoint
|
|
public init(startPoint: CGPoint, endPoint: CGPoint) {
|
self.startPoint = startPoint
|
self.endPoint = endPoint
|
}
|
}
|
}
|
|
// MARK: - Properties
|
|
public extension UIView {
|
/// SwifterSwift: Border color of view; also inspectable from Storyboard.
|
@IBInspectable var layerBorderColor: UIColor? {
|
get {
|
guard let color = layer.borderColor else { return nil }
|
return UIColor(cgColor: color)
|
}
|
set {
|
guard let color = newValue else {
|
layer.borderColor = nil
|
return
|
}
|
// Fix React-Native conflict issue
|
guard String(describing: type(of: color)) != "__NSCFType" else { return }
|
layer.borderColor = color.cgColor
|
}
|
}
|
|
/// SwifterSwift: Border width of view; also inspectable from Storyboard.
|
@IBInspectable var layerBorderWidth: CGFloat {
|
get {
|
return layer.borderWidth
|
}
|
set {
|
layer.borderWidth = newValue
|
}
|
}
|
|
/// SwifterSwift: Corner radius of view; also inspectable from Storyboard.
|
@IBInspectable var layerCornerRadius: CGFloat {
|
get {
|
return layer.cornerRadius
|
}
|
set {
|
layer.masksToBounds = true
|
layer.cornerRadius = abs(CGFloat(Int(newValue * 100)) / 100)
|
}
|
}
|
|
/// SwifterSwift: Height of view.
|
var height: CGFloat {
|
get {
|
return frame.size.height
|
}
|
set {
|
frame.size.height = newValue
|
}
|
}
|
|
/// SwifterSwift: Check if view is in RTL format.
|
var isRightToLeft: Bool {
|
return effectiveUserInterfaceLayoutDirection == .rightToLeft
|
}
|
|
/// SwifterSwift: Take screenshot of view (if applicable).
|
var screenshot: UIImage? {
|
let size = layer.frame.size
|
guard size != .zero else { return nil }
|
return UIGraphicsImageRenderer(size: layer.frame.size).image { context in
|
layer.render(in: context.cgContext)
|
}
|
}
|
|
/// SwifterSwift: Shadow color of view; also inspectable from Storyboard.
|
@IBInspectable var layerShadowColor: UIColor? {
|
get {
|
guard let color = layer.shadowColor else { return nil }
|
return UIColor(cgColor: color)
|
}
|
set {
|
layer.shadowColor = newValue?.cgColor
|
}
|
}
|
|
/// SwifterSwift: Shadow offset of view; also inspectable from Storyboard.
|
@IBInspectable var layerShadowOffset: CGSize {
|
get {
|
return layer.shadowOffset
|
}
|
set {
|
layer.shadowOffset = newValue
|
}
|
}
|
|
/// SwifterSwift: Shadow opacity of view; also inspectable from Storyboard.
|
@IBInspectable var layerShadowOpacity: Float {
|
get {
|
return layer.shadowOpacity
|
}
|
set {
|
layer.shadowOpacity = newValue
|
}
|
}
|
|
/// SwifterSwift: Shadow radius of view; also inspectable from Storyboard.
|
@IBInspectable var layerShadowRadius: CGFloat {
|
get {
|
return layer.shadowRadius
|
}
|
set {
|
layer.shadowRadius = newValue
|
}
|
}
|
|
/// SwifterSwift: Masks to bounds of view; also inspectable from Storyboard.
|
@IBInspectable var masksToBounds: Bool {
|
get {
|
return layer.masksToBounds
|
}
|
set {
|
layer.masksToBounds = newValue
|
}
|
}
|
|
/// SwifterSwift: Size of view.
|
var size: CGSize {
|
get {
|
return frame.size
|
}
|
set {
|
width = newValue.width
|
height = newValue.height
|
}
|
}
|
|
/// SwifterSwift: Get view's parent view controller
|
var parentViewController: UIViewController? {
|
weak var parentResponder: UIResponder? = self
|
while parentResponder != nil {
|
parentResponder = parentResponder!.next
|
if let viewController = parentResponder as? UIViewController {
|
return viewController
|
}
|
}
|
return nil
|
}
|
|
/// SwifterSwift: Width of view.
|
var width: CGFloat {
|
get {
|
return frame.size.width
|
}
|
set {
|
frame.size.width = newValue
|
}
|
}
|
|
/// SwifterSwift: x origin of view.
|
var x: CGFloat {
|
get {
|
return frame.origin.x
|
}
|
set {
|
frame.origin.x = newValue
|
}
|
}
|
|
/// SwifterSwift: y origin of view.
|
var y: CGFloat {
|
get {
|
return frame.origin.y
|
}
|
set {
|
frame.origin.y = newValue
|
}
|
}
|
}
|
|
// MARK: - Methods
|
|
public extension UIView {
|
/// SwifterSwift: Recursively find the first responder.
|
func firstResponder() -> UIView? {
|
var views = [UIView](arrayLiteral: self)
|
var index = 0
|
repeat {
|
let view = views[index]
|
if view.isFirstResponder {
|
return view
|
}
|
views.append(contentsOf: view.subviews)
|
index += 1
|
} while index < views.count
|
return nil
|
}
|
|
/// SwifterSwift: Set some or all corners radiuses of view.
|
///
|
/// - Parameters:
|
/// - corners: array of corners to change (example: [.bottomLeft, .topRight]).
|
/// - radius: radius for selected corners.
|
func roundCorners(_ corners: UIRectCorner, radius: CGFloat) {
|
let maskPath = UIBezierPath(
|
roundedRect: bounds,
|
byRoundingCorners: corners,
|
cornerRadii: CGSize(width: radius, height: radius))
|
|
let shape = CAShapeLayer()
|
shape.path = maskPath.cgPath
|
layer.mask = shape
|
}
|
|
/// SwifterSwift: Make the view circular.
|
///
|
/// - Parameter diameter: This value will be set as the width and height of the view.
|
func makeCircle(diameter: CGFloat) {
|
clipsToBounds = true
|
bounds.size.height = diameter
|
bounds.size.width = diameter
|
layer.cornerRadius = diameter / 2
|
}
|
|
/// SwifterSwift: Add shadow to view.
|
///
|
/// - Note: This method only works with non-clear background color, or if the view has a `shadowPath` set.
|
/// See parameter `opacity` for detail.
|
///
|
/// - Parameters:
|
/// - color: shadow color (default is #137992).
|
/// - radius: shadow radius (default is 3).
|
/// - offset: shadow offset (default is .zero).
|
/// - opacity: shadow opacity (default is 0.5). It will also be affected by the `alpha` of `backgroundColor`.
|
func addShadow(
|
ofColor color: UIColor = UIColor(red: 0.07, green: 0.47, blue: 0.57, alpha: 1.0),
|
radius: CGFloat = 3,
|
offset: CGSize = .zero,
|
opacity: Float = 0.5) {
|
layer.shadowColor = color.cgColor
|
layer.shadowOffset = offset
|
layer.shadowRadius = radius
|
layer.shadowOpacity = opacity
|
layer.masksToBounds = false
|
}
|
|
/// SwifterSwift: Add array of subviews to view.
|
///
|
/// - Parameter subviews: array of subviews to add to self.
|
func addSubviews(_ subviews: [UIView]) {
|
subviews.forEach { addSubview($0) }
|
}
|
|
/// SwifterSwift: Make the view blurry.
|
///
|
/// - Parameter style: UIBlurEffectStyle (default is .light).
|
func blur(withStyle style: UIBlurEffect.Style = .light) {
|
let blurEffect = UIBlurEffect(style: style)
|
let blurEffectView = UIVisualEffectView(effect: blurEffect)
|
blurEffectView.frame = bounds
|
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] // for supporting device rotation
|
addSubview(blurEffectView)
|
clipsToBounds = true
|
}
|
|
/// SwifterSwift: Fade in view.
|
///
|
/// - Parameters:
|
/// - duration: animation duration in seconds (default is 1 second).
|
/// - completion: optional completion handler to run with animation finishes (default is nil).
|
func fadeIn(duration: TimeInterval = 1, completion: ((Bool) -> Void)? = nil) {
|
if isHidden {
|
isHidden = false
|
}
|
UIView.animate(withDuration: duration, animations: {
|
self.alpha = 1
|
}, completion: completion)
|
}
|
|
/// SwifterSwift: Fade out view.
|
///
|
/// - Parameters:
|
/// - duration: animation duration in seconds (default is 1 second).
|
/// - completion: optional completion handler to run with animation finishes (default is nil).
|
func fadeOut(duration: TimeInterval = 1, completion: ((Bool) -> Void)? = nil) {
|
if isHidden {
|
isHidden = false
|
}
|
UIView.animate(withDuration: duration, animations: {
|
self.alpha = 0
|
}, completion: completion)
|
}
|
|
/// SwifterSwift: Load view from nib.
|
///
|
/// - Parameters:
|
/// - name: nib name.
|
/// - bundle: bundle of nib (default is nil).
|
/// - Returns: optional UIView (if applicable).
|
class func loadFromNib(named name: String, bundle: Bundle? = nil) -> UIView? {
|
return UINib(nibName: name, bundle: bundle).instantiate(withOwner: nil, options: nil)[0] as? UIView
|
}
|
|
/// SwifterSwift: Load view of a certain type from nib
|
///
|
/// - Parameters:
|
/// - withClass: UIView type.
|
/// - bundle: bundle of nib (default is nil).
|
/// - Returns: UIView
|
class func loadFromNib<T: UIView>(withClass name: T.Type, bundle: Bundle? = nil) -> T {
|
let named = String(describing: name)
|
guard let view = UINib(nibName: named, bundle: bundle).instantiate(withOwner: nil, options: nil)[0] as? T else {
|
fatalError("First element in xib file \(named) is not of type \(named)")
|
}
|
return view
|
}
|
|
/// SwifterSwift: Remove all subviews in view.
|
func removeSubviews() {
|
subviews.forEach { $0.removeFromSuperview() }
|
}
|
|
/// SwifterSwift: Remove all gesture recognizers from view.
|
func removeGestureRecognizers() {
|
try? gestureRecognizers?.forEach(removeGestureRecognizer)
|
}
|
|
/// SwifterSwift: Attaches gesture recognizers to the view. Attaching gesture recognizers to a view defines the
|
/// scope of the represented gesture, causing it to receive touches hit-tested to that view and all of its subviews.
|
/// The view establishes a strong reference to the gesture recognizers.
|
///
|
/// - Parameter gestureRecognizers: The array of gesture recognizers to be added to the view.
|
func addGestureRecognizers(_ gestureRecognizers: [UIGestureRecognizer]) {
|
for recognizer in gestureRecognizers {
|
addGestureRecognizer(recognizer)
|
}
|
}
|
|
/// SwifterSwift: Detaches gesture recognizers from the receiving view. This method releases gestureRecognizers in
|
/// addition to detaching them from the view.
|
///
|
/// - Parameter gestureRecognizers: The array of gesture recognizers to be removed from the view.
|
func removeGestureRecognizers(_ gestureRecognizers: [UIGestureRecognizer]) {
|
for recognizer in gestureRecognizers {
|
removeGestureRecognizer(recognizer)
|
}
|
}
|
|
/// SwifterSwift: Rotate view by angle on relative axis.
|
///
|
/// - Parameters:
|
/// - angle: angle to rotate view by.
|
/// - type: type of the rotation angle.
|
/// - animated: set true to animate rotation (default is true).
|
/// - duration: animation duration in seconds (default is 1 second).
|
/// - completion: optional completion handler to run with animation finishes (default is nil).
|
func rotate(
|
byAngle angle: CGFloat,
|
ofType type: AngleUnit,
|
animated: Bool = false,
|
duration: TimeInterval = 1,
|
completion: ((Bool) -> Void)? = nil) {
|
let angleWithType = (type == .degrees) ? .pi * angle / 180.0 : angle
|
let aDuration = animated ? duration : 0
|
UIView.animate(withDuration: aDuration, delay: 0, options: .curveLinear, animations: { () in
|
self.transform = self.transform.rotated(by: angleWithType)
|
}, completion: completion)
|
}
|
|
/// SwifterSwift: Rotate view to angle on fixed axis.
|
///
|
/// - Parameters:
|
/// - angle: angle to rotate view to.
|
/// - type: type of the rotation angle.
|
/// - animated: set true to animate rotation (default is false).
|
/// - duration: animation duration in seconds (default is 1 second).
|
/// - completion: optional completion handler to run with animation finishes (default is nil).
|
func rotate(
|
toAngle angle: CGFloat,
|
ofType type: AngleUnit,
|
animated: Bool = false,
|
duration: TimeInterval = 1,
|
completion: ((Bool) -> Void)? = nil) {
|
let angleWithType = (type == .degrees) ? .pi * angle / 180.0 : angle
|
let currentAngle = atan2(transform.b, transform.a)
|
let aDuration = animated ? duration : 0
|
UIView.animate(withDuration: aDuration, animations: {
|
self.transform = self.transform.rotated(by: angleWithType - currentAngle)
|
}, completion: completion)
|
}
|
|
/// SwifterSwift: Scale view by offset.
|
///
|
/// - Parameters:
|
/// - offset: scale offset
|
/// - animated: set true to animate scaling (default is false).
|
/// - duration: animation duration in seconds (default is 1 second).
|
/// - completion: optional completion handler to run with animation finishes (default is nil).
|
func scale(
|
by offset: CGPoint,
|
animated: Bool = false,
|
duration: TimeInterval = 1,
|
completion: ((Bool) -> Void)? = nil) {
|
if animated {
|
UIView.animate(withDuration: duration, delay: 0, options: .curveLinear, animations: { () in
|
self.transform = self.transform.scaledBy(x: offset.x, y: offset.y)
|
}, completion: completion)
|
} else {
|
transform = transform.scaledBy(x: offset.x, y: offset.y)
|
completion?(true)
|
}
|
}
|
|
/// SwifterSwift: Shake view.
|
///
|
/// - Parameters:
|
/// - direction: shake direction (horizontal or vertical), (default is .horizontal).
|
/// - duration: animation duration in seconds (default is 1 second).
|
/// - animationType: shake animation type (default is .easeOut).
|
/// - completion: optional completion handler to run with animation finishes (default is nil).
|
func shake(
|
direction: ShakeDirection = .horizontal,
|
duration: TimeInterval = 1,
|
animationType: ShakeAnimationType = .easeOut,
|
completion: (() -> Void)? = nil) {
|
CATransaction.begin()
|
let animation = switch direction {
|
case .horizontal:
|
CAKeyframeAnimation(keyPath: "transform.translation.x")
|
case .vertical:
|
CAKeyframeAnimation(keyPath: "transform.translation.y")
|
}
|
switch animationType {
|
case .linear:
|
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
case .easeIn:
|
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
|
case .easeOut:
|
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
|
case .easeInOut:
|
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
}
|
CATransaction.setCompletionBlock(completion)
|
animation.duration = duration
|
animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0]
|
layer.add(animation, forKey: "shake")
|
CATransaction.commit()
|
}
|
|
/// SwifterSwift: Add Gradient Colors.
|
///
|
/// view.addGradient(
|
/// colors: [.red, .blue],
|
/// locations: [0.0, 1.0],
|
/// direction: .topToBottom
|
/// )
|
///
|
/// - Parameters:
|
/// - colors: An array of `SFColor` defining the color of each gradient stop.
|
/// - locations: An array of `CGFloat` defining the location of each
|
/// gradient stop as a value in the range [0, 1]. The values must be
|
/// monotonically increasing.
|
/// - direction: Struct type describing the direction of the gradient.
|
func addGradient(colors: [SFColor], locations: [CGFloat] = [0.0, 1.0], direction: GradientDirection) {
|
// <https://github.com/swiftdevcenter/GradientColorExample>
|
let gradientLayer = CAGradientLayer()
|
gradientLayer.frame = bounds
|
gradientLayer.colors = colors.map(\.cgColor)
|
gradientLayer.locations = locations.map { NSNumber(value: $0) }
|
gradientLayer.startPoint = direction.startPoint
|
gradientLayer.endPoint = direction.endPoint
|
layer.addSublayer(gradientLayer)
|
}
|
|
/// SwifterSwift: Removes the applied blur effect from the `UIView`.
|
func removeBlur() {
|
subviews
|
.lazy
|
.compactMap { $0 as? UIVisualEffectView }
|
.filter { $0.effect is UIBlurEffect }
|
.forEach { $0.removeFromSuperview() }
|
}
|
|
/// SwifterSwift: Add Visual Format constraints.
|
///
|
/// - Parameters:
|
/// - withFormat: visual Format language.
|
/// - views: array of views which will be accessed starting with index 0 (example: [v0], [v1], [v2]..).
|
func addConstraints(withFormat: String, views: UIView...) {
|
// https://videos.letsbuildthatapp.com/
|
var viewsDictionary: [String: UIView] = [:]
|
for (index, view) in views.enumerated() {
|
let key = "v\(index)"
|
view.translatesAutoresizingMaskIntoConstraints = false
|
viewsDictionary[key] = view
|
}
|
addConstraints(NSLayoutConstraint.constraints(
|
withVisualFormat: withFormat,
|
options: NSLayoutConstraint.FormatOptions(),
|
metrics: nil,
|
views: viewsDictionary))
|
}
|
|
/// SwifterSwift: Anchor all sides of the view into it's superview.
|
func fillToSuperview() {
|
// https://videos.letsbuildthatapp.com/
|
translatesAutoresizingMaskIntoConstraints = false
|
if let superview {
|
let left = leftAnchor.constraint(equalTo: superview.leftAnchor)
|
let right = rightAnchor.constraint(equalTo: superview.rightAnchor)
|
let top = topAnchor.constraint(equalTo: superview.topAnchor)
|
let bottom = bottomAnchor.constraint(equalTo: superview.bottomAnchor)
|
NSLayoutConstraint.activate([left, right, top, bottom])
|
}
|
}
|
|
/// SwifterSwift: Add anchors from any side of the current view into the specified anchors and returns the newly
|
/// added constraints.
|
///
|
/// - Parameters:
|
/// - top: current view's top anchor will be anchored into the specified anchor.
|
/// - left: current view's left anchor will be anchored into the specified anchor.
|
/// - bottom: current view's bottom anchor will be anchored into the specified anchor.
|
/// - right: current view's right anchor will be anchored into the specified anchor.
|
/// - topConstant: current view's top anchor margin.
|
/// - leftConstant: current view's left anchor margin.
|
/// - bottomConstant: current view's bottom anchor margin.
|
/// - rightConstant: current view's right anchor margin.
|
/// - widthConstant: current view's width.
|
/// - heightConstant: current view's height.
|
/// - Returns: array of newly added constraints (if applicable).
|
@discardableResult
|
func anchor(
|
top: NSLayoutYAxisAnchor? = nil,
|
left: NSLayoutXAxisAnchor? = nil,
|
bottom: NSLayoutYAxisAnchor? = nil,
|
right: NSLayoutXAxisAnchor? = nil,
|
topConstant: CGFloat = 0,
|
leftConstant: CGFloat = 0,
|
bottomConstant: CGFloat = 0,
|
rightConstant: CGFloat = 0,
|
widthConstant: CGFloat = 0,
|
heightConstant: CGFloat = 0) -> [NSLayoutConstraint] {
|
// https://videos.letsbuildthatapp.com/
|
translatesAutoresizingMaskIntoConstraints = false
|
|
var anchors = [NSLayoutConstraint]()
|
|
if let top {
|
anchors.append(topAnchor.constraint(equalTo: top, constant: topConstant))
|
}
|
|
if let left {
|
anchors.append(leftAnchor.constraint(equalTo: left, constant: leftConstant))
|
}
|
|
if let bottom {
|
anchors.append(bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant))
|
}
|
|
if let right {
|
anchors.append(rightAnchor.constraint(equalTo: right, constant: -rightConstant))
|
}
|
|
if widthConstant > 0 {
|
anchors.append(widthAnchor.constraint(equalToConstant: widthConstant))
|
}
|
|
if heightConstant > 0 {
|
anchors.append(heightAnchor.constraint(equalToConstant: heightConstant))
|
}
|
|
NSLayoutConstraint.activate(anchors)
|
|
return anchors
|
}
|
|
/// SwifterSwift: Anchor center X into current view's superview with a constant margin value.
|
///
|
/// - Parameter constant: constant of the anchor constraint (default is 0).
|
func anchorCenterXToSuperview(constant: CGFloat = 0) {
|
// https://videos.letsbuildthatapp.com/
|
translatesAutoresizingMaskIntoConstraints = false
|
if let anchor = superview?.centerXAnchor {
|
centerXAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
|
}
|
}
|
|
/// SwifterSwift: Anchor center Y into current view's superview with a constant margin value.
|
///
|
/// - Parameter withConstant: constant of the anchor constraint (default is 0).
|
func anchorCenterYToSuperview(constant: CGFloat = 0) {
|
// https://videos.letsbuildthatapp.com/
|
translatesAutoresizingMaskIntoConstraints = false
|
if let anchor = superview?.centerYAnchor {
|
centerYAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
|
}
|
}
|
|
/// SwifterSwift: Anchor center X and Y into current view's superview.
|
func anchorCenterSuperview() {
|
// https://videos.letsbuildthatapp.com/
|
anchorCenterXToSuperview()
|
anchorCenterYToSuperview()
|
}
|
|
/// SwifterSwift: Search all superviews until a view with the condition is found.
|
///
|
/// - Parameter predicate: predicate to evaluate on superviews.
|
func ancestorView(where predicate: (UIView?) -> Bool) -> UIView? {
|
if predicate(superview) {
|
return superview
|
}
|
return superview?.ancestorView(where: predicate)
|
}
|
|
/// SwifterSwift: Search all superviews until a view with this class is found.
|
///
|
/// - Parameter name: class of the view to search.
|
func ancestorView<T: UIView>(withClass _: T.Type) -> T? {
|
return ancestorView(where: { $0 is T }) as? T
|
}
|
|
/// SwifterSwift: Returns all the subviews of a given type recursively in the
|
/// view hierarchy rooted on the view it its called.
|
///
|
/// - Parameter ofType: Class of the view to search.
|
/// - Returns: All subviews with a specified type.
|
func subviews<T>(ofType _: T.Type) -> [T] {
|
var views = [T]()
|
for subview in subviews {
|
if let view = subview as? T {
|
views.append(view)
|
} else if !subview.subviews.isEmpty {
|
views.append(contentsOf: subview.subviews(ofType: T.self))
|
}
|
}
|
return views
|
}
|
}
|
|
// MARK: - Constraints
|
|
public extension UIView {
|
/// SwifterSwift: Search constraints until we find one for the given view
|
/// and attribute. This will enumerate ancestors since constraints are
|
/// always added to the common ancestor.
|
///
|
/// - Parameter attribute: the attribute to find.
|
/// - Parameter at: the view to find.
|
/// - Returns: matching constraint.
|
func findConstraint(attribute: NSLayoutConstraint.Attribute, for view: UIView) -> NSLayoutConstraint? {
|
let constraint = constraints.first {
|
($0.firstAttribute == attribute && $0.firstItem as? UIView == view) ||
|
($0.secondAttribute == attribute && $0.secondItem as? UIView == view)
|
}
|
return constraint ?? superview?.findConstraint(attribute: attribute, for: view)
|
}
|
|
/// SwifterSwift: First width constraint for this view.
|
var widthConstraint: NSLayoutConstraint? {
|
findConstraint(attribute: .width, for: self)
|
}
|
|
/// SwifterSwift: First height constraint for this view.
|
var heightConstraint: NSLayoutConstraint? {
|
findConstraint(attribute: .height, for: self)
|
}
|
|
/// SwifterSwift: First leading constraint for this view.
|
var leadingConstraint: NSLayoutConstraint? {
|
findConstraint(attribute: .leading, for: self)
|
}
|
|
/// SwifterSwift: First trailing constraint for this view.
|
var trailingConstraint: NSLayoutConstraint? {
|
findConstraint(attribute: .trailing, for: self)
|
}
|
|
/// SwifterSwift: First top constraint for this view.
|
var topConstraint: NSLayoutConstraint? {
|
findConstraint(attribute: .top, for: self)
|
}
|
|
/// SwifterSwift: First bottom constraint for this view.
|
var bottomConstraint: NSLayoutConstraint? {
|
findConstraint(attribute: .bottom, for: self)
|
}
|
}
|
|
#endif
|