// // Extensions.swift // Pods // // Created by Kevin Delord on 01/04/17. // // import Foundation import UIKit extension UICollectionViewScrollDirection { static func direction(withConfiguration readerConfig: FolioReaderConfig) -> UICollectionViewScrollDirection { return readerConfig.isDirection(.vertical, .horizontal, .horizontal) } } extension UICollectionViewScrollPosition { static func direction(withConfiguration readerConfig: FolioReaderConfig) -> UICollectionViewScrollPosition { return readerConfig.isDirection(.top, .left, .left) } } extension CGPoint { func forDirection(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> CGFloat { return readerConfig.isDirection(self.y, self.x, ((scrollType == .page) ? self.y : self.x)) } } extension CGSize { func forDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat { return readerConfig.isDirection(height, width, height) } func forReverseDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat { return readerConfig.isDirection(width, height, width) } } extension CGRect { func forDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat { return readerConfig.isDirection(height, width, height) } } extension ScrollDirection { static func negative(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> ScrollDirection { return readerConfig.isDirection(.down, .right, .right) } static func positive(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> ScrollDirection { return readerConfig.isDirection(.up, .left, .left) } } // MARK: Helpers /** Delay function From: http://stackoverflow.com/a/24318861/517707 - parameter delay: Delay in seconds - parameter closure: Closure */ func delay(_ delay:Double, closure:@escaping ()->()) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) } // MARK: - Extensions internal extension Bundle { class func frameworkBundle() -> Bundle { return Bundle(for: FolioReader.self) } } internal extension UIColor { convenience init(rgba: String) { var red: CGFloat = 0.0 var green: CGFloat = 0.0 var blue: CGFloat = 0.0 var alpha: CGFloat = 1.0 if rgba.hasPrefix("#") { let index = rgba.index(rgba.startIndex, offsetBy: 1) let hex = String(rgba[index...]) let scanner = Scanner(string: hex) var hexValue: CUnsignedLongLong = 0 if scanner.scanHexInt64(&hexValue) { switch (hex.count) { case 3: red = CGFloat((hexValue & 0xF00) >> 8) / 15.0 green = CGFloat((hexValue & 0x0F0) >> 4) / 15.0 blue = CGFloat(hexValue & 0x00F) / 15.0 break case 4: red = CGFloat((hexValue & 0xF000) >> 12) / 15.0 green = CGFloat((hexValue & 0x0F00) >> 8) / 15.0 blue = CGFloat((hexValue & 0x00F0) >> 4) / 15.0 alpha = CGFloat(hexValue & 0x000F) / 15.0 break case 6: red = CGFloat((hexValue & 0xFF0000) >> 16) / 255.0 green = CGFloat((hexValue & 0x00FF00) >> 8) / 255.0 blue = CGFloat(hexValue & 0x0000FF) / 255.0 break case 8: red = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0 green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0 blue = CGFloat((hexValue & 0x0000FF00) >> 8) / 255.0 alpha = CGFloat(hexValue & 0x000000FF) / 255.0 break default: print("Invalid RGB string, number of characters after '#' should be either 3, 4, 6 or 8", terminator: "") break } } else { print("Scan hex error") } } else { print("Invalid RGB string, missing '#' as prefix", terminator: "") } self.init(red:red, green:green, blue:blue, alpha:alpha) } // /// Hex string of a UIColor instance. /// /// from: https://github.com/yeahdongcn/UIColor-Hex-Swift /// /// - Parameter includeAlpha: Whether the alpha should be included. /// - Returns: Hexa string func hexString(_ includeAlpha: Bool) -> String { var r: CGFloat = 0 var g: CGFloat = 0 var b: CGFloat = 0 var a: CGFloat = 0 self.getRed(&r, green: &g, blue: &b, alpha: &a) if (includeAlpha == true) { return String(format: "#%02X%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255), Int(a * 255)) } else { return String(format: "#%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255)) } } // MARK: - color shades // https://gist.github.com/mbigatti/c6be210a6bbc0ff25972 func highlightColor() -> UIColor { var hue : CGFloat = 0 var saturation : CGFloat = 0 var brightness : CGFloat = 0 var alpha : CGFloat = 0 if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) { return UIColor(hue: hue, saturation: 0.30, brightness: 1, alpha: alpha) } else { return self; } } /** Returns a lighter color by the provided percentage :param: lighting percent percentage :returns: lighter UIColor */ func lighterColor(_ percent : Double) -> UIColor { return colorWithBrightnessFactor(CGFloat(1 + percent)); } /** Returns a darker color by the provided percentage :param: darking percent percentage :returns: darker UIColor */ func darkerColor(_ percent : Double) -> UIColor { return colorWithBrightnessFactor(CGFloat(1 - percent)); } /** Return a modified color using the brightness factor provided :param: factor brightness factor :returns: modified color */ func colorWithBrightnessFactor(_ factor: CGFloat) -> UIColor { var hue : CGFloat = 0 var saturation : CGFloat = 0 var brightness : CGFloat = 0 var alpha : CGFloat = 0 if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) { return UIColor(hue: hue, saturation: saturation, brightness: brightness * factor, alpha: alpha) } else { return self; } } } internal extension String { /// Truncates the string to length number of characters and /// appends optional trailing string if longer func truncate(_ length: Int, trailing: String? = nil) -> String { if count > length { let indexOfText = index(startIndex, offsetBy: length) return String(self[.. String { return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression) } func stripLineBreaks() -> String { return self.replacingOccurrences(of: "\n", with: "", options: .regularExpression) } /** Converts a clock time such as `0:05:01.2` to seconds (`Double`) Looks for media overlay clock formats as specified [here][1] - Note: this may not be the most efficient way of doing this. It can be improved later on. - Returns: seconds as `Double` [1]: http://www.idpf.org/epub/301/spec/epub-mediaoverlays.html#app-clock-examples */ func clockTimeToSeconds() -> Double { let val = self.trimmingCharacters(in: CharacterSet.whitespaces) if( val.isEmpty ){ return 0 } let formats = [ "HH:mm:ss.SSS" : "^\\d{1,2}:\\d{2}:\\d{2}\\.\\d{1,3}$", "HH:mm:ss" : "^\\d{1,2}:\\d{2}:\\d{2}$", "mm:ss.SSS" : "^\\d{1,2}:\\d{2}\\.\\d{1,3}$", "mm:ss" : "^\\d{1,2}:\\d{2}$", "ss.SSS" : "^\\d{1,2}\\.\\d{1,3}$", ] // search for normal duration formats such as `00:05:01.2` for (format, pattern) in formats { if val.range(of: pattern, options: .regularExpression) != nil { let formatter = DateFormatter() formatter.dateFormat = format let time = formatter.date(from: val) if( time == nil ){ return 0 } formatter.dateFormat = "ss.SSS" let seconds = (formatter.string(from: time!) as NSString).doubleValue formatter.dateFormat = "mm" let minutes = (formatter.string(from: time!) as NSString).doubleValue formatter.dateFormat = "HH" let hours = (formatter.string(from: time!) as NSString).doubleValue return seconds + (minutes*60) + (hours*60*60) } } // if none of the more common formats match, check for other possible formats // 2345ms if val.range(of: "^\\d+ms$", options: .regularExpression) != nil{ return (val as NSString).doubleValue / 1000.0 } // 7.25h if val.range(of: "^\\d+(\\.\\d+)?h$", options: .regularExpression) != nil { return (val as NSString).doubleValue * 60 * 60 } // 13min if val.range(of: "^\\d+(\\.\\d+)?min$", options: .regularExpression) != nil { return (val as NSString).doubleValue * 60 } return 0 } func clockTimeToMinutesString() -> String { let val = clockTimeToSeconds() let min = floor(val / 60) let sec = floor(val.truncatingRemainder(dividingBy: 60)) return String(format: "%02.f:%02.f", min, sec) } // MARK: - NSString helpers var lastPathComponent: String { return (self as NSString).lastPathComponent } var deletingLastPathComponent: String { return (self as NSString).deletingLastPathComponent } var deletingPathExtension: String { return (self as NSString).deletingPathExtension } var pathExtension: String { return (self as NSString).pathExtension } var abbreviatingWithTildeInPath: String { return (self as NSString).abbreviatingWithTildeInPath } func appendingPathComponent(_ str: String) -> String { return (self as NSString).appendingPathComponent(str) } func appendingPathExtension(_ str: String) -> String { return (self as NSString).appendingPathExtension(str) ?? self+"."+str } } internal extension UIImage { convenience init?(readerImageNamed: String) { self.init(named: readerImageNamed, in: Bundle.frameworkBundle(), compatibleWith: nil) } /// Forces the image to be colored with Reader Config tintColor /// /// - Parameter readerConfig: Current folio reader configuration. /// - Returns: Returns a colored image func ignoreSystemTint(withConfiguration readerConfig: FolioReaderConfig) -> UIImage? { return self.imageTintColor(readerConfig.tintColor)?.withRenderingMode(.alwaysOriginal) } /** Colorize the image with a color - parameter tintColor: The input color - returns: Returns a colored image */ func imageTintColor(_ tintColor: UIColor) -> UIImage? { UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) let context = UIGraphicsGetCurrentContext() context?.translateBy(x: 0, y: self.size.height) context?.scaleBy(x: 1.0, y: -1.0) context?.setBlendMode(CGBlendMode.normal) let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) as CGRect if let cgImage = self.cgImage { context?.clip(to: rect, mask: cgImage) } tintColor.setFill() context?.fill(rect) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage } /** Generate a image with a color - parameter color: The input color - returns: Returns a colored image */ class func imageWithColor(_ color: UIColor?) -> UIImage { let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) UIGraphicsBeginImageContextWithOptions(rect.size, false, 0) let context = UIGraphicsGetCurrentContext() if let color = color { color.setFill() } else { UIColor.white.setFill() } context!.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! } /** Generates a image with a `CALayer` - parameter layer: The input `CALayer` - returns: Return a rendered image */ class func imageWithLayer(_ layer: CALayer) -> UIImage { UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.isOpaque, 0.0) layer.render(in: UIGraphicsGetCurrentContext()!) let img = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return img! } /** Generates a image from a `UIView` - parameter view: The input `UIView` - returns: Return a rendered image */ class func imageWithView(_ view: UIView) -> UIImage { UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0) view.drawHierarchy(in: view.bounds, afterScreenUpdates: true) let img = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return img! } } internal extension UIViewController { func setCloseButton(withConfiguration readerConfig: FolioReaderConfig) { let closeImage = UIImage(readerImageNamed: "icon-navbar-close")?.ignoreSystemTint(withConfiguration: readerConfig) self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: closeImage, style: .plain, target: self, action: #selector(dismiss as () -> Void)) } @objc func dismiss() { self.dismiss(nil) } func dismiss(_ completion: (() -> Void)?) { DispatchQueue.main.async { self.dismiss(animated: true, completion: { completion?() }) } } // MARK: - NavigationBar func setTransparentNavigation() { let navBar = self.navigationController?.navigationBar navBar?.setBackgroundImage(UIImage(), for: UIBarMetrics.default) navBar?.hideBottomHairline() navBar?.isTranslucent = true } func setTranslucentNavigation(_ translucent: Bool = true, color: UIColor, tintColor: UIColor = UIColor.white, titleColor: UIColor = UIColor.black, andFont font: UIFont = UIFont.systemFont(ofSize: 17)) { let navBar = self.navigationController?.navigationBar navBar?.setBackgroundImage(UIImage.imageWithColor(color), for: UIBarMetrics.default) navBar?.showBottomHairline() navBar?.isTranslucent = translucent navBar?.tintColor = tintColor navBar?.titleTextAttributes = [NSAttributedStringKey.foregroundColor: titleColor, NSAttributedStringKey.font: font] } } internal extension UINavigationBar { func hideBottomHairline() { let navigationBarImageView = hairlineImageViewInNavigationBar(self) navigationBarImageView!.isHidden = true } func showBottomHairline() { let navigationBarImageView = hairlineImageViewInNavigationBar(self) navigationBarImageView!.isHidden = false } fileprivate func hairlineImageViewInNavigationBar(_ view: UIView) -> UIImageView? { if view.isKind(of: UIImageView.self) && view.bounds.height <= 1.0 { return (view as! UIImageView) } let subviews = (view.subviews) for subview: UIView in subviews { if let imageView: UIImageView = hairlineImageViewInNavigationBar(subview) { return imageView } } return nil } } extension UINavigationController { open override var preferredStatusBarStyle : UIStatusBarStyle { guard let viewController = visibleViewController else { return .default } return viewController.preferredStatusBarStyle } open override var supportedInterfaceOrientations : UIInterfaceOrientationMask { guard let viewController = visibleViewController else { return .portrait } return viewController.supportedInterfaceOrientations } open override var shouldAutorotate : Bool { guard let viewController = visibleViewController else { return false } return viewController.shouldAutorotate } } /** This fixes iOS 9 crash http://stackoverflow.com/a/32010520/517707 */ extension UIAlertController { open override var supportedInterfaceOrientations : UIInterfaceOrientationMask { return .portrait } open override var shouldAutorotate : Bool { return false } } extension Array { /** Return index if is safe, if not return nil http://stackoverflow.com/a/30593673/517707 */ subscript(safe index: Int) -> Element? { return indices ~= index ? self[index] : nil } }