Permalink
executable file 577 lines (513 sloc) 20.5 KB
//
// UIViewExtensions.swift
// SwifterSwift
//
// Created by Omar Albeik on 8/5/16.
// Copyright © 2016 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.
public enum ShakeDirection {
/// Shake left and right.
case horizontal
/// Shake up and down.
case vertical
}
/// SwifterSwift: Angle units.
///
/// - degrees: degrees.
/// - radians: radians.
public enum AngleUnit {
/// degrees.
case degrees
/// radians.
case radians
}
/// SwifterSwift: Shake animations types.
///
/// - linear: linear animation.
/// - easeIn: easeIn animation.
/// - easeOut: easeOut animation.
/// - easeInOut: easeInOut animation.
public enum ShakeAnimationType {
/// linear animation.
case linear
/// easeIn animation.
case easeIn
/// easeOut animation.
case easeOut
/// easeInOut animation.
case easeInOut
}
}
// MARK: - Properties
public extension UIView {
/// SwifterSwift: Border color of view; also inspectable from Storyboard.
@IBInspectable public var borderColor: 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 public var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
/// SwifterSwift: Corner radius of view; also inspectable from Storyboard.
@IBInspectable public var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.masksToBounds = true
layer.cornerRadius = abs(CGFloat(Int(newValue * 100)) / 100)
}
}
/// SwifterSwift: Height of view.
public var height: CGFloat {
get {
return frame.size.height
}
set {
frame.size.height = newValue
}
}
/// SwifterSwift: Check if view is in RTL format.
public var isRightToLeft: Bool {
if #available(iOS 10.0, *, tvOS 10.0, *) {
return effectiveUserInterfaceLayoutDirection == .rightToLeft
} else {
return false
}
}
/// SwifterSwift: Take screenshot of view (if applicable).
public var screenshot: UIImage? {
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0)
defer {
UIGraphicsEndImageContext()
}
guard let context = UIGraphicsGetCurrentContext() else { return nil }
layer.render(in: context)
return UIGraphicsGetImageFromCurrentImageContext()
}
/// SwifterSwift: Shadow color of view; also inspectable from Storyboard.
@IBInspectable public var shadowColor: 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 public var shadowOffset: CGSize {
get {
return layer.shadowOffset
}
set {
layer.shadowOffset = newValue
}
}
/// SwifterSwift: Shadow opacity of view; also inspectable from Storyboard.
@IBInspectable public var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
/// SwifterSwift: Shadow radius of view; also inspectable from Storyboard.
@IBInspectable public var shadowRadius: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue
}
}
/// SwifterSwift: Size of view.
public var size: CGSize {
get {
return frame.size
}
set {
width = newValue.width
height = newValue.height
}
}
/// SwifterSwift: Get view's parent view controller
public 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.
public var width: CGFloat {
get {
return frame.size.width
}
set {
frame.size.width = newValue
}
}
// swiftlint:disable next identifier_name
/// SwifterSwift: x origin of view.
public var x: CGFloat {
get {
return frame.origin.x
}
set {
frame.origin.x = newValue
}
}
// swiftlint:disable next identifier_name
/// SwifterSwift: y origin of view.
public var y: CGFloat {
get {
return frame.origin.y
}
set {
frame.origin.y = newValue
}
}
}
// MARK: - Methods
public extension UIView {
/// SwifterSwift: Recursively find the first responder.
public func firstResponder() -> UIView? {
var views = [UIView](arrayLiteral: self)
var i = 0
repeat {
let view = views[i]
if view.isFirstResponder {
return view
}
views.append(contentsOf: view.subviews)
i += 1
} while i < 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.
public 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: Add shadow to view.
///
/// - 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).
public 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.
public func addSubviews(_ subviews: [UIView]) {
subviews.forEach { addSubview($0) }
}
/// 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)
public 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)
public 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).
public 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: Remove all subviews in view.
public func removeSubviews() {
subviews.forEach({ $0.removeFromSuperview() })
}
/// SwifterSwift: Remove all gesture recognizers from view.
public func removeGestureRecognizers() {
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.
public 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.
public 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).
public 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: { () -> Void 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).
public 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 aDuration = animated ? duration : 0
UIView.animate(withDuration: aDuration, animations: {
self.transform = self.transform.concatenating(CGAffineTransform(rotationAngle: angleWithType))
}, 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).
public 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: { () -> Void 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).
public func shake(direction: ShakeDirection = .horizontal, duration: TimeInterval = 1, animationType: ShakeAnimationType = .easeOut, completion:(() -> Void)? = nil) {
CATransaction.begin()
let animation: CAKeyframeAnimation
switch direction {
case .horizontal:
animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
case .vertical:
animation = 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 Visual Format constraints.
///
/// - Parameters:
/// - withFormat: visual Format language
/// - views: array of views which will be accessed starting with index 0 (example: [v0], [v1], [v2]..)
@available(iOS 9, *) public 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.
@available(iOS 9, *) public func fillToSuperview() {
// https://videos.letsbuildthatapp.com/
translatesAutoresizingMaskIntoConstraints = false
if let superview = 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).
@available(iOS 9, *) @discardableResult public 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 = top {
anchors.append(topAnchor.constraint(equalTo: top, constant: topConstant))
}
if let left = left {
anchors.append(leftAnchor.constraint(equalTo: left, constant: leftConstant))
}
if let bottom = bottom {
anchors.append(bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant))
}
if let right = 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))
}
anchors.forEach({$0.isActive = true})
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).
@available(iOS 9, *) public 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).
@available(iOS 9, *) public 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
@available(iOS 9, *) public 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.
public 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.
public func ancestorView<T: UIView>(withClass name: T.Type) -> T? {
return ancestorView(where: { $0 is T }) as? T
}
}
#endif