// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation public extension UIEdgeInsets { public init(top: CGFloat, leading: CGFloat, bottom: CGFloat, trailing: CGFloat) { self.init(top: top, left: CurrentAppContext().isRTL ? trailing : leading, bottom: bottom, right: CurrentAppContext().isRTL ? leading : trailing) } } // MARK: - @objc public extension UINavigationController { @objc public func pushViewController(_ viewController: UIViewController, animated: Bool, completion: (() -> Void)?) { CATransaction.begin() CATransaction.setCompletionBlock(completion) pushViewController(viewController, animated: animated) CATransaction.commit() } @objc public func popViewController(animated: Bool, completion: (() -> Void)?) { CATransaction.begin() CATransaction.setCompletionBlock(completion) popViewController(animated: animated) CATransaction.commit() } @objc public func popToViewController(_ viewController: UIViewController, animated: Bool, completion: (() -> Void)?) { CATransaction.begin() CATransaction.setCompletionBlock(completion) self.popToViewController(viewController, animated: animated) CATransaction.commit() } } // MARK: - extension UIView { public func renderAsImage() -> UIImage? { return renderAsImage(opaque: false, scale: UIScreen.main.scale) } public func renderAsImage(opaque: Bool, scale: CGFloat) -> UIImage? { if #available(iOS 10, *) { let format = UIGraphicsImageRendererFormat() format.scale = scale format.opaque = opaque let renderer = UIGraphicsImageRenderer(bounds: self.bounds, format: format) return renderer.image { (context) in self.layer.render(in: context.cgContext) } } else { UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, scale) if let _ = UIGraphicsGetCurrentContext() { drawHierarchy(in: bounds, afterScreenUpdates: true) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } owsFailDebug("Could not create graphics context.") return nil } } @objc public class func spacer(withWidth width: CGFloat) -> UIView { let view = UIView() view.autoSetDimension(.width, toSize: width) return view } @objc public class func spacer(withHeight height: CGFloat) -> UIView { let view = UIView() view.autoSetDimension(.height, toSize: height) return view } @objc public class func hStretchingSpacer() -> UIView { let view = UIView() view.setContentHuggingHorizontalLow() view.setCompressionResistanceHorizontalLow() return view } @objc public class func vStretchingSpacer() -> UIView { let view = UIView() view.setContentHuggingVerticalLow() view.setCompressionResistanceVerticalLow() return view } @objc public func applyScaleAspectFitLayout(subview: UIView, aspectRatio: CGFloat) -> [NSLayoutConstraint] { guard subviews.contains(subview) else { owsFailDebug("Not a subview.") return [] } // This emulates the behavior of contentMode = .scaleAspectFit using // iOS auto layout constraints. // // This allows ConversationInputToolbar to place the "cancel" button // in the upper-right hand corner of the preview content. var constraints = [NSLayoutConstraint]() constraints.append(contentsOf: subview.autoCenterInSuperview()) constraints.append(subview.autoPin(toAspectRatio: aspectRatio)) constraints.append(subview.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)) constraints.append(subview.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)) return constraints } } // MARK: - public extension UIViewController { @objc public func presentAlert(_ alert: UIAlertController) { self.presentAlert(alert, animated: true) } @objc public func presentAlert(_ alert: UIAlertController, animated: Bool) { self.present(alert, animated: animated, completion: { alert.applyAccessibilityIdentifiers() }) } @objc public func presentAlert(_ alert: UIAlertController, completion: @escaping (() -> Void)) { self.present(alert, animated: true, completion: { alert.applyAccessibilityIdentifiers() completion() }) } } // MARK: - public extension CGFloat { public func clamp(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat { return CGFloatClamp(self, minValue, maxValue) } public func clamp01() -> CGFloat { return CGFloatClamp01(self) } // Linear interpolation public func lerp(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat { return CGFloatLerp(minValue, maxValue, self) } // Inverse linear interpolation public func inverseLerp(_ minValue: CGFloat, _ maxValue: CGFloat, shouldClamp: Bool = false) -> CGFloat { let value = CGFloatInverseLerp(self, minValue, maxValue) return (shouldClamp ? CGFloatClamp01(value) : value) } public static let halfPi: CGFloat = CGFloat.pi * 0.5 public func fuzzyEquals(_ other: CGFloat, tolerance: CGFloat = 0.001) -> Bool { return abs(self - other) < tolerance } public var square: CGFloat { return self * self } } // MARK: - public extension Int { public func clamp(_ minValue: Int, _ maxValue: Int) -> Int { assert(minValue <= maxValue) return Swift.max(minValue, Swift.min(maxValue, self)) } } // MARK: - public extension CGPoint { public func toUnitCoordinates(viewBounds: CGRect, shouldClamp: Bool) -> CGPoint { return CGPoint(x: (x - viewBounds.origin.x).inverseLerp(0, viewBounds.width, shouldClamp: shouldClamp), y: (y - viewBounds.origin.y).inverseLerp(0, viewBounds.height, shouldClamp: shouldClamp)) } public func toUnitCoordinates(viewSize: CGSize, shouldClamp: Bool) -> CGPoint { return toUnitCoordinates(viewBounds: CGRect(origin: .zero, size: viewSize), shouldClamp: shouldClamp) } public func fromUnitCoordinates(viewBounds: CGRect) -> CGPoint { return CGPoint(x: viewBounds.origin.x + x.lerp(0, viewBounds.size.width), y: viewBounds.origin.y + y.lerp(0, viewBounds.size.height)) } public func fromUnitCoordinates(viewSize: CGSize) -> CGPoint { return fromUnitCoordinates(viewBounds: CGRect(origin: .zero, size: viewSize)) } public func inverse() -> CGPoint { return CGPoint(x: -x, y: -y) } public func plus(_ value: CGPoint) -> CGPoint { return CGPointAdd(self, value) } public func minus(_ value: CGPoint) -> CGPoint { return CGPointSubtract(self, value) } public func times(_ value: CGFloat) -> CGPoint { return CGPoint(x: x * value, y: y * value) } public func min(_ value: CGPoint) -> CGPoint { // We use "Swift" to disambiguate the global function min() from this method. return CGPoint(x: Swift.min(x, value.x), y: Swift.min(y, value.y)) } public func max(_ value: CGPoint) -> CGPoint { // We use "Swift" to disambiguate the global function max() from this method. return CGPoint(x: Swift.max(x, value.x), y: Swift.max(y, value.y)) } public var length: CGFloat { return sqrt(x * x + y * y) } public static let unit: CGPoint = CGPoint(x: 1.0, y: 1.0) public static let unitMidpoint: CGPoint = CGPoint(x: 0.5, y: 0.5) public func applyingInverse(_ transform: CGAffineTransform) -> CGPoint { return applying(transform.inverted()) } public func fuzzyEquals(_ other: CGPoint, tolerance: CGFloat = 0.001) -> Bool { return (x.fuzzyEquals(other.x, tolerance: tolerance) && y.fuzzyEquals(other.y, tolerance: tolerance)) } public static func tan(angle: CGFloat) -> CGPoint { return CGPoint(x: sin(angle), y: cos(angle)) } public func clamp(_ rect: CGRect) -> CGPoint { return CGPoint(x: x.clamp(rect.minX, rect.maxX), y: y.clamp(rect.minY, rect.maxY)) } } // MARK: - public extension CGSize { var aspectRatio: CGFloat { guard self.height > 0 else { return 0 } return self.width / self.height } var asPoint: CGPoint { return CGPoint(x: width, y: height) } var ceil: CGSize { return CGSizeCeil(self) } } // MARK: - public extension CGRect { public var center: CGPoint { return CGPoint(x: midX, y: midY) } public var topLeft: CGPoint { return origin } public var topRight: CGPoint { return CGPoint(x: maxX, y: minY) } public var bottomLeft: CGPoint { return CGPoint(x: minX, y: maxY) } public var bottomRight: CGPoint { return CGPoint(x: maxX, y: maxY) } } // MARK: - public extension CGAffineTransform { public static func translate(_ point: CGPoint) -> CGAffineTransform { return CGAffineTransform(translationX: point.x, y: point.y) } public static func scale(_ scaling: CGFloat) -> CGAffineTransform { return CGAffineTransform(scaleX: scaling, y: scaling) } public func translate(_ point: CGPoint) -> CGAffineTransform { return translatedBy(x: point.x, y: point.y) } public func scale(_ scaling: CGFloat) -> CGAffineTransform { return scaledBy(x: scaling, y: scaling) } public func rotate(_ angleRadians: CGFloat) -> CGAffineTransform { return rotated(by: angleRadians) } } // MARK: - public extension UIBezierPath { public func addRegion(withPoints points: [CGPoint]) { guard let first = points.first else { owsFailDebug("No points.") return } move(to: first) for point in points.dropFirst() { addLine(to: point) } addLine(to: first) } } // MARK: - public extension UIBarButtonItem { @objc public convenience init(image: UIImage?, style: UIBarButtonItem.Style, target: Any?, action: Selector?, accessibilityIdentifier: String) { self.init(image: image, style: style, target: target, action: action) self.accessibilityIdentifier = accessibilityIdentifier } @objc public convenience init(image: UIImage?, landscapeImagePhone: UIImage?, style: UIBarButtonItem.Style, target: Any?, action: Selector?, accessibilityIdentifier: String) { self.init(image: image, landscapeImagePhone: landscapeImagePhone, style: style, target: target, action: action) self.accessibilityIdentifier = accessibilityIdentifier } @objc public convenience init(title: String?, style: UIBarButtonItem.Style, target: Any?, action: Selector?, accessibilityIdentifier: String) { self.init(title: title, style: style, target: target, action: action) self.accessibilityIdentifier = accessibilityIdentifier } @objc public convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, target: Any?, action: Selector?, accessibilityIdentifier: String) { self.init(barButtonSystemItem: systemItem, target: target, action: action) self.accessibilityIdentifier = accessibilityIdentifier } @objc public convenience init(customView: UIView, accessibilityIdentifier: String) { self.init(customView: customView) self.accessibilityIdentifier = accessibilityIdentifier } }