5 // Created by Kevin Delord on 01/04/17.
12 extension UICollectionViewScrollDirection {
13 static func direction(withConfiguration readerConfig: FolioReaderConfig) -> UICollectionViewScrollDirection {
14 return readerConfig.isDirection(.vertical, .horizontal, .horizontal)
18 extension UICollectionViewScrollPosition {
19 static func direction(withConfiguration readerConfig: FolioReaderConfig) -> UICollectionViewScrollPosition {
20 return readerConfig.isDirection(.top, .left, .left)
25 func forDirection(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> CGFloat {
26 return readerConfig.isDirection(self.y, self.x, ((scrollType == .page) ? self.y : self.x))
31 func forDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
32 return readerConfig.isDirection(height, width, height)
35 func forReverseDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
36 return readerConfig.isDirection(width, height, width)
41 func forDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
42 return readerConfig.isDirection(height, width, height)
46 extension ScrollDirection {
47 static func negative(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> ScrollDirection {
48 return readerConfig.isDirection(.down, .right, .right)
51 static func positive(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> ScrollDirection {
52 return readerConfig.isDirection(.up, .left, .left)
60 From: http://stackoverflow.com/a/24318861/517707
62 - parameter delay: Delay in seconds
63 - parameter closure: Closure
65 func delay(_ delay:Double, closure:@escaping ()->()) {
66 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
72 internal extension Bundle {
73 class func frameworkBundle() -> Bundle {
74 return Bundle(for: FolioReader.self)
78 internal extension UIColor {
79 convenience init(rgba: String) {
80 var red: CGFloat = 0.0
81 var green: CGFloat = 0.0
82 var blue: CGFloat = 0.0
83 var alpha: CGFloat = 1.0
85 if rgba.hasPrefix("#") {
86 let index = rgba.index(rgba.startIndex, offsetBy: 1)
87 let hex = String(rgba[index...])
88 let scanner = Scanner(string: hex)
89 var hexValue: CUnsignedLongLong = 0
90 if scanner.scanHexInt64(&hexValue) {
93 red = CGFloat((hexValue & 0xF00) >> 8) / 15.0
94 green = CGFloat((hexValue & 0x0F0) >> 4) / 15.0
95 blue = CGFloat(hexValue & 0x00F) / 15.0
98 red = CGFloat((hexValue & 0xF000) >> 12) / 15.0
99 green = CGFloat((hexValue & 0x0F00) >> 8) / 15.0
100 blue = CGFloat((hexValue & 0x00F0) >> 4) / 15.0
101 alpha = CGFloat(hexValue & 0x000F) / 15.0
104 red = CGFloat((hexValue & 0xFF0000) >> 16) / 255.0
105 green = CGFloat((hexValue & 0x00FF00) >> 8) / 255.0
106 blue = CGFloat(hexValue & 0x0000FF) / 255.0
109 red = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0
110 green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0
111 blue = CGFloat((hexValue & 0x0000FF00) >> 8) / 255.0
112 alpha = CGFloat(hexValue & 0x000000FF) / 255.0
115 print("Invalid RGB string, number of characters after '#' should be either 3, 4, 6 or 8", terminator: "")
119 print("Scan hex error")
122 print("Invalid RGB string, missing '#' as prefix", terminator: "")
124 self.init(red:red, green:green, blue:blue, alpha:alpha)
128 /// Hex string of a UIColor instance.
130 /// from: https://github.com/yeahdongcn/UIColor-Hex-Swift
132 /// - Parameter includeAlpha: Whether the alpha should be included.
133 /// - Returns: Hexa string
134 func hexString(_ includeAlpha: Bool) -> String {
139 self.getRed(&r, green: &g, blue: &b, alpha: &a)
141 if (includeAlpha == true) {
142 return String(format: "#%02X%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255), Int(a * 255))
144 return String(format: "#%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255))
148 // MARK: - color shades
149 // https://gist.github.com/mbigatti/c6be210a6bbc0ff25972
151 func highlightColor() -> UIColor {
153 var hue : CGFloat = 0
154 var saturation : CGFloat = 0
155 var brightness : CGFloat = 0
156 var alpha : CGFloat = 0
158 if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
159 return UIColor(hue: hue, saturation: 0.30, brightness: 1, alpha: alpha)
167 Returns a lighter color by the provided percentage
169 :param: lighting percent percentage
170 :returns: lighter UIColor
172 func lighterColor(_ percent : Double) -> UIColor {
173 return colorWithBrightnessFactor(CGFloat(1 + percent));
177 Returns a darker color by the provided percentage
179 :param: darking percent percentage
180 :returns: darker UIColor
182 func darkerColor(_ percent : Double) -> UIColor {
183 return colorWithBrightnessFactor(CGFloat(1 - percent));
187 Return a modified color using the brightness factor provided
189 :param: factor brightness factor
190 :returns: modified color
192 func colorWithBrightnessFactor(_ factor: CGFloat) -> UIColor {
193 var hue : CGFloat = 0
194 var saturation : CGFloat = 0
195 var brightness : CGFloat = 0
196 var alpha : CGFloat = 0
198 if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
199 return UIColor(hue: hue, saturation: saturation, brightness: brightness * factor, alpha: alpha)
206 internal extension String {
207 /// Truncates the string to length number of characters and
208 /// appends optional trailing string if longer
209 func truncate(_ length: Int, trailing: String? = nil) -> String {
211 let indexOfText = index(startIndex, offsetBy: length)
212 return String(self[..<indexOfText])
218 func stripHtml() -> String {
219 return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression)
222 func stripLineBreaks() -> String {
223 return self.replacingOccurrences(of: "\n", with: "", options: .regularExpression)
227 Converts a clock time such as `0:05:01.2` to seconds (`Double`)
229 Looks for media overlay clock formats as specified [here][1]
231 - Note: this may not be the most efficient way of doing this. It can be improved later on.
233 - Returns: seconds as `Double`
235 [1]: http://www.idpf.org/epub/301/spec/epub-mediaoverlays.html#app-clock-examples
237 func clockTimeToSeconds() -> Double {
239 let val = self.trimmingCharacters(in: CharacterSet.whitespaces)
241 if( val.isEmpty ){ return 0 }
244 "HH:mm:ss.SSS" : "^\\d{1,2}:\\d{2}:\\d{2}\\.\\d{1,3}$",
245 "HH:mm:ss" : "^\\d{1,2}:\\d{2}:\\d{2}$",
246 "mm:ss.SSS" : "^\\d{1,2}:\\d{2}\\.\\d{1,3}$",
247 "mm:ss" : "^\\d{1,2}:\\d{2}$",
248 "ss.SSS" : "^\\d{1,2}\\.\\d{1,3}$",
251 // search for normal duration formats such as `00:05:01.2`
252 for (format, pattern) in formats {
254 if val.range(of: pattern, options: .regularExpression) != nil {
256 let formatter = DateFormatter()
257 formatter.dateFormat = format
258 let time = formatter.date(from: val)
260 if( time == nil ){ return 0 }
262 formatter.dateFormat = "ss.SSS"
263 let seconds = (formatter.string(from: time!) as NSString).doubleValue
265 formatter.dateFormat = "mm"
266 let minutes = (formatter.string(from: time!) as NSString).doubleValue
268 formatter.dateFormat = "HH"
269 let hours = (formatter.string(from: time!) as NSString).doubleValue
271 return seconds + (minutes*60) + (hours*60*60)
275 // if none of the more common formats match, check for other possible formats
278 if val.range(of: "^\\d+ms$", options: .regularExpression) != nil{
279 return (val as NSString).doubleValue / 1000.0
283 if val.range(of: "^\\d+(\\.\\d+)?h$", options: .regularExpression) != nil {
284 return (val as NSString).doubleValue * 60 * 60
288 if val.range(of: "^\\d+(\\.\\d+)?min$", options: .regularExpression) != nil {
289 return (val as NSString).doubleValue * 60
295 func clockTimeToMinutesString() -> String {
297 let val = clockTimeToSeconds()
299 let min = floor(val / 60)
300 let sec = floor(val.truncatingRemainder(dividingBy: 60))
302 return String(format: "%02.f:%02.f", min, sec)
305 // MARK: - NSString helpers
307 var lastPathComponent: String {
308 return (self as NSString).lastPathComponent
311 var deletingLastPathComponent: String {
312 return (self as NSString).deletingLastPathComponent
315 var deletingPathExtension: String {
316 return (self as NSString).deletingPathExtension
319 var pathExtension: String {
320 return (self as NSString).pathExtension
323 var abbreviatingWithTildeInPath: String {
324 return (self as NSString).abbreviatingWithTildeInPath
327 func appendingPathComponent(_ str: String) -> String {
328 return (self as NSString).appendingPathComponent(str)
331 func appendingPathExtension(_ str: String) -> String {
332 return (self as NSString).appendingPathExtension(str) ?? self+"."+str
336 internal extension UIImage {
338 convenience init?(readerImageNamed: String) {
339 self.init(named: readerImageNamed, in: Bundle.frameworkBundle(), compatibleWith: nil)
342 /// Forces the image to be colored with Reader Config tintColor
344 /// - Parameter readerConfig: Current folio reader configuration.
345 /// - Returns: Returns a colored image
346 func ignoreSystemTint(withConfiguration readerConfig: FolioReaderConfig) -> UIImage? {
347 return self.imageTintColor(readerConfig.tintColor)?.withRenderingMode(.alwaysOriginal)
351 Colorize the image with a color
353 - parameter tintColor: The input color
354 - returns: Returns a colored image
356 func imageTintColor(_ tintColor: UIColor) -> UIImage? {
357 UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
359 let context = UIGraphicsGetCurrentContext()
360 context?.translateBy(x: 0, y: self.size.height)
361 context?.scaleBy(x: 1.0, y: -1.0)
362 context?.setBlendMode(CGBlendMode.normal)
364 let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) as CGRect
365 if let cgImage = self.cgImage {
366 context?.clip(to: rect, mask: cgImage)
372 let newImage = UIGraphicsGetImageFromCurrentImageContext()
373 UIGraphicsEndImageContext()
379 Generate a image with a color
381 - parameter color: The input color
382 - returns: Returns a colored image
384 class func imageWithColor(_ color: UIColor?) -> UIImage {
385 let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
386 UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
387 let context = UIGraphicsGetCurrentContext()
389 if let color = color {
392 UIColor.white.setFill()
396 let image = UIGraphicsGetImageFromCurrentImageContext()
397 UIGraphicsEndImageContext()
403 Generates a image with a `CALayer`
405 - parameter layer: The input `CALayer`
406 - returns: Return a rendered image
408 class func imageWithLayer(_ layer: CALayer) -> UIImage {
409 UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.isOpaque, 0.0)
410 layer.render(in: UIGraphicsGetCurrentContext()!)
411 let img = UIGraphicsGetImageFromCurrentImageContext()
412 UIGraphicsEndImageContext()
417 Generates a image from a `UIView`
419 - parameter view: The input `UIView`
420 - returns: Return a rendered image
422 class func imageWithView(_ view: UIView) -> UIImage {
423 UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
424 view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
425 let img = UIGraphicsGetImageFromCurrentImageContext()
426 UIGraphicsEndImageContext()
431 internal extension UIViewController {
433 func setCloseButton(withConfiguration readerConfig: FolioReaderConfig) {
434 let closeImage = UIImage(readerImageNamed: "icon-navbar-close")?.ignoreSystemTint(withConfiguration: readerConfig)
435 self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: closeImage, style: .plain, target: self, action: #selector(dismiss as () -> Void))
438 @objc func dismiss() {
442 func dismiss(_ completion: (() -> Void)?) {
443 DispatchQueue.main.async {
444 self.dismiss(animated: true, completion: {
450 // MARK: - NavigationBar
452 func setTransparentNavigation() {
453 let navBar = self.navigationController?.navigationBar
454 navBar?.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
455 navBar?.hideBottomHairline()
456 navBar?.isTranslucent = true
459 func setTranslucentNavigation(_ translucent: Bool = true, color: UIColor, tintColor: UIColor = UIColor.white, titleColor: UIColor = UIColor.black, andFont font: UIFont = UIFont.systemFont(ofSize: 17)) {
460 let navBar = self.navigationController?.navigationBar
461 navBar?.setBackgroundImage(UIImage.imageWithColor(color), for: UIBarMetrics.default)
462 navBar?.showBottomHairline()
463 navBar?.isTranslucent = translucent
464 navBar?.tintColor = tintColor
465 navBar?.titleTextAttributes = [NSAttributedStringKey.foregroundColor: titleColor, NSAttributedStringKey.font: font]
469 internal extension UINavigationBar {
471 func hideBottomHairline() {
472 let navigationBarImageView = hairlineImageViewInNavigationBar(self)
473 navigationBarImageView!.isHidden = true
476 func showBottomHairline() {
477 let navigationBarImageView = hairlineImageViewInNavigationBar(self)
478 navigationBarImageView!.isHidden = false
481 fileprivate func hairlineImageViewInNavigationBar(_ view: UIView) -> UIImageView? {
482 if view.isKind(of: UIImageView.self) && view.bounds.height <= 1.0 {
483 return (view as! UIImageView)
486 let subviews = (view.subviews)
487 for subview: UIView in subviews {
488 if let imageView: UIImageView = hairlineImageViewInNavigationBar(subview) {
496 extension UINavigationController {
498 open override var preferredStatusBarStyle : UIStatusBarStyle {
499 guard let viewController = visibleViewController else { return .default }
500 return viewController.preferredStatusBarStyle
503 open override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
504 guard let viewController = visibleViewController else { return .portrait }
505 return viewController.supportedInterfaceOrientations
508 open override var shouldAutorotate : Bool {
509 guard let viewController = visibleViewController else { return false }
510 return viewController.shouldAutorotate
515 This fixes iOS 9 crash
516 http://stackoverflow.com/a/32010520/517707
518 extension UIAlertController {
519 open override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
523 open override var shouldAutorotate : Bool {
531 Return index if is safe, if not return nil
532 http://stackoverflow.com/a/30593673/517707
534 subscript(safe index: Int) -> Element? {
535 return indices ~= index ? self[index] : nil