wenweiwei 3 lat temu
rodzic
commit
513aaef648
100 zmienionych plików z 23092 dodań i 9 usunięć
  1. 10 2
      BFCommonKit.podspec
  2. 0 0
      BFCommonKit/Classes/BFCategorys/.gitkeep
  3. 22 0
      BFCommonKit/Classes/BFCategorys/BFBundle+Ext.swift
  4. 46 0
      BFCommonKit/Classes/BFCategorys/BFColor+Ext.swift
  5. 109 0
      BFCommonKit/Classes/BFCategorys/BFInt+Ext.swift
  6. 204 0
      BFCommonKit/Classes/BFCategorys/BFString+Ext.swift
  7. 63 0
      BFCommonKit/Classes/BFCategorys/BFUIButton+ext.swift
  8. 234 0
      BFCommonKit/Classes/BFCategorys/BFUIImage+Ext.swift
  9. 458 0
      BFCommonKit/Classes/BFCategorys/BFUIView+Ext.swift
  10. 176 0
      BFCommonKit/Classes/BFCategorys/NXFundation+Ext.swift
  11. 392 0
      BFCommonKit/Classes/BFCategorys/NXUI+Ext.swift
  12. 77 0
      BFCommonKit/Classes/BFCategorys/NXUIColor+Ext.swift
  13. 32 0
      BFCommonKit/Classes/BFCategorys/UIControl+NXCategory.h
  14. 68 0
      BFCommonKit/Classes/BFCategorys/UIControl+NXCategory.m
  15. 0 0
      BFCommonKit/Classes/BFCustomViews/.gitkeep
  16. 0 0
      BFCommonKit/Classes/BFCustomViews/controllers/.gitkeep
  17. 0 0
      BFCommonKit/Classes/BFCustomViews/views/.gitkeep
  18. 114 0
      BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/NXBadgeControl.swift
  19. 28 0
      BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/NXBadgeView.swift
  20. 125 0
      BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/UIBarButtonItem+NXBadgeView.swift
  21. 126 0
      BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/UITabBarItem+NXBadgeView.swift
  22. 260 0
      BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/UIView+NXBadgeView.swift
  23. 166 0
      BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXBubbleLayer.swift
  24. 23 0
      BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXContainView.swift
  25. 73 0
      BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXInteractiveView.swift
  26. 80 0
      BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXNormalBubbleView.swift
  27. 40 0
      BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXTextBubbleView.swift
  28. 184 0
      BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXVoiceBubbleView.swift
  29. 0 0
      BFCommonKit/Classes/BFDBManager/.gitkeep
  30. 0 0
      BFCommonKit/Classes/BFDebug/.gitkeep
  31. 174 0
      BFCommonKit/Classes/BFDebug/NXLogger.swift
  32. 68 0
      BFCommonKit/Classes/BFDebug/NXLoggerManager.swift
  33. 188 0
      BFCommonKit/Classes/BFDebug/NXLoggerVC.swift
  34. 0 0
      BFCommonKit/Classes/BFMacro/.gitkeep
  35. 13 0
      BFCommonKit/Classes/BFMacro/NXConfig.swift
  36. 0 0
      BFCommonKit/Classes/BFNetworkManger/.gitkeep
  37. 0 0
      BFCommonKit/Classes/BFUtility/.gitkeep
  38. 159 0
      BFCommonKit/Classes/BFUtility/NXAudioRecorder.swift
  39. 113 0
      BFCommonKit/Classes/BFUtility/NXDeviceManager.swift
  40. 43 0
      BFCommonKit/Classes/BFUtility/NXFileManager.swift
  41. 85 0
      BFCommonKit/Classes/BFUtility/PQBFConfig.swift
  42. 22 0
      BFCommonKit/Classes/BFUtility/PQBridgeObject.h
  43. 56 0
      BFCommonKit/Classes/BFUtility/PQBridgeObject.m
  44. 794 0
      BFCommonKit/Classes/BFUtility/PQCommonMethodUtil.swift
  45. 274 0
      BFCommonKit/Classes/BFUtility/PQConstant.swift
  46. 180 0
      BFCommonKit/Classes/BFUtility/PQCreateEmptyWAV.swift
  47. 526 0
      BFCommonKit/Classes/BFUtility/PQLZStringUtil.swift
  48. 780 0
      BFCommonKit/Classes/BFUtility/PQPHAssetVideoParaseUtil.swift
  49. 95 0
      BFCommonKit/Classes/BFUtility/PQVideoSnapshotUtil.swift
  50. 48 0
      BFCommonKit/Classes/BFUtility/PQWeakTimer.swift
  51. 156 0
      BFCommonKit/Classes/BFUtility/SWNetRequest.swift
  52. 203 0
      BFCommonKit/Classes/Base/Controller/PQBaseViewController.swift
  53. 184 0
      BFCommonKit/Classes/Base/Controller/PQBaseWebViewController.swift
  54. 46 0
      BFCommonKit/Classes/Base/Controller/PQNavigatinController.swift
  55. 182 0
      BFCommonKit/Classes/Base/Model/PQBaseModel.swift
  56. 121 0
      BFCommonKit/Classes/Base/View/PQFollowButton.swift
  57. 41 0
      BFCommonKit/Classes/Base/View/PQGIFImageView.swift
  58. 53 0
      BFCommonKit/Classes/Base/View/PQHeartAnimation.swift
  59. 130 0
      BFCommonKit/Classes/Base/View/PQLoadingHUB.swift
  60. 515 0
      BFCommonKit/Classes/Base/View/PQRemindView.swift
  61. 27 0
      BFCommonKit/Classes/Base/View/PQTabBar.swift
  62. 104 0
      BFCommonKit/Classes/Base/View/PQTextView.swift
  63. 791 0
      BFCommonKit/Classes/Enums/Enums.swift
  64. 25 6
      Example/BFCommonKit.xcodeproj/project.pbxproj
  65. 10 0
      Example/BFCommonKit.xcworkspace/contents.xcworkspacedata
  66. 8 0
      Example/BFCommonKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  67. 1 1
      Example/Podfile
  68. 60 0
      Example/Podfile.lock
  69. 19 0
      Example/Pods/Alamofire/LICENSE
  70. 234 0
      Example/Pods/Alamofire/README.md
  71. 460 0
      Example/Pods/Alamofire/Source/AFError.swift
  72. 465 0
      Example/Pods/Alamofire/Source/Alamofire.swift
  73. 37 0
      Example/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift
  74. 580 0
      Example/Pods/Alamofire/Source/MultipartFormData.swift
  75. 238 0
      Example/Pods/Alamofire/Source/NetworkReachabilityManager.swift
  76. 55 0
      Example/Pods/Alamofire/Source/Notifications.swift
  77. 483 0
      Example/Pods/Alamofire/Source/ParameterEncoding.swift
  78. 660 0
      Example/Pods/Alamofire/Source/Request.swift
  79. 574 0
      Example/Pods/Alamofire/Source/Response.swift
  80. 715 0
      Example/Pods/Alamofire/Source/ResponseSerialization.swift
  81. 300 0
      Example/Pods/Alamofire/Source/Result.swift
  82. 310 0
      Example/Pods/Alamofire/Source/ServerTrustPolicy.swift
  83. 725 0
      Example/Pods/Alamofire/Source/SessionDelegate.swift
  84. 899 0
      Example/Pods/Alamofire/Source/SessionManager.swift
  85. 466 0
      Example/Pods/Alamofire/Source/TaskDelegate.swift
  86. 136 0
      Example/Pods/Alamofire/Source/Timeline.swift
  87. 321 0
      Example/Pods/Alamofire/Source/Validation.swift
  88. 58 0
      Example/Pods/FDFullscreenPopGesture/FDFullscreenPopGesture/UINavigationController+FDFullscreenPopGesture.h
  89. 227 0
      Example/Pods/FDFullscreenPopGesture/FDFullscreenPopGesture/UINavigationController+FDFullscreenPopGesture.m
  90. 22 0
      Example/Pods/FDFullscreenPopGesture/LICENSE
  91. 62 0
      Example/Pods/FDFullscreenPopGesture/README.md
  92. 22 0
      Example/Pods/KeychainAccess/LICENSE
  93. 3074 0
      Example/Pods/KeychainAccess/Lib/KeychainAccess/Keychain.swift
  94. 635 0
      Example/Pods/KeychainAccess/README.md
  95. 22 0
      Example/Pods/Kingfisher/LICENSE
  96. 240 0
      Example/Pods/Kingfisher/README.md
  97. 117 0
      Example/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift
  98. 584 0
      Example/Pods/Kingfisher/Sources/Cache/DiskStorage.swift
  99. 118 0
      Example/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift
  100. 849 0
      Example/Pods/Kingfisher/Sources/Cache/ImageCache.swift

+ 10 - 2
BFCommonKit.podspec

@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
   s.name             = 'BFCommonKit'
   s.version          = '0.1.0'
   s.summary          = 'A short description of BFCommonKit.'
-
+  s.swift_version    = '5.0'
 # This description is used to generate tags and improve search results.
 #   * Think: What does it do? Why did you write it? What is the focus?
 #   * Try to keep it short, snappy and to the point.
@@ -28,7 +28,7 @@ TODO: Add long description of the pod here.
   s.source           = { :git => 'https://github.com/wenweiwei/BFCommonKit.git', :tag => s.version.to_s }
   # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
 
-  s.ios.deployment_target = '9.0'
+  s.ios.deployment_target = '10.0'
 
   s.source_files = 'BFCommonKit/Classes/**/*'
   
@@ -38,5 +38,13 @@ TODO: Add long description of the pod here.
 
   # s.public_header_files = 'Pod/Classes/**/*.h'
   # s.frameworks = 'UIKit', 'MapKit'
+  s.frameworks = 'UIKit', 'AVFoundation','CoreMedia', 'QuartzCore'
   # s.dependency 'AFNetworking', '~> 2.3'
+  s.dependency 'Kingfisher','6.3.0'
+  s.dependency 'KingfisherWebP','1.3.0'
+  s.dependency 'SnapKit','5.0.1'
+  s.dependency 'Alamofire','4.9.1'
+  s.dependency 'KeychainAccess','4.2.2'
+  s.dependency 'Toast-Swift','5.0.1'
+  s.dependency 'FDFullscreenPopGesture'
 end

+ 0 - 0
BFCommonKit/Classes/ReplaceMe.swift → BFCommonKit/Classes/BFCategorys/.gitkeep


+ 22 - 0
BFCommonKit/Classes/BFCategorys/BFBundle+Ext.swift

@@ -0,0 +1,22 @@
+//
+//  Bundle+Ext.swift
+//  BFFramework
+//
+//  Created by ak on 2021/6/2.
+//  取 BFFramework 资源目录
+
+import Foundation
+
+extension Bundle {
+    
+    // bf main bundle url
+   public func BF_mainbundle_URL() -> URL {
+        let bundle:Bundle = Bundle.init(for: PQBaseViewController.self)
+        return bundle.url(forResource: "BFFramework", withExtension: "bundle")!
+    }
+    // bf main bundle
+    public func BF_mainbundle() -> Bundle {
+        return  Bundle.init(url: BF_mainbundle_URL())!
+    }
+ 
+}

+ 46 - 0
BFCommonKit/Classes/BFCategorys/BFColor+Ext.swift

@@ -0,0 +1,46 @@
+//
+//  UIColor+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+extension UIColor {
+    public  class func hexColor(hexadecimal: String) -> UIColor {
+        var cstr = hexadecimal.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).uppercased() as NSString
+        if cstr.length < 6 {
+            return UIColor.clear
+        }
+        if cstr.hasPrefix("0X") {
+            cstr = cstr.substring(from: 2) as NSString
+        }
+        if cstr.hasPrefix("#") {
+            cstr = cstr.substring(from: 1) as NSString
+        }
+        if cstr.length != 6 {
+            return UIColor.clear
+        }
+        var range = NSRange()
+        range.location = 0
+        range.length = 2
+        // r
+        let rStr = cstr.substring(with: range)
+        // g
+        range.location = 2
+        let gStr = cstr.substring(with: range)
+        // b
+        range.location = 4
+        let bStr = cstr.substring(with: range)
+        var r: UInt32 = 0x0
+        var g: UInt32 = 0x0
+        var b: UInt32 = 0x0
+        Scanner(string: rStr).scanHexInt32(&r)
+        Scanner(string: gStr).scanHexInt32(&g)
+        Scanner(string: bStr).scanHexInt32(&b)
+        return UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1)
+    }
+}

+ 109 - 0
BFCommonKit/Classes/BFCategorys/BFInt+Ext.swift

@@ -0,0 +1,109 @@
+//
+//  Int+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/7/20.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+extension Int {
+    /// 分数字格式化如413200->4,132.09
+
+    /// 是否保留2位小数
+    /// - Parameter isDecimal: <#isDecimal description#>
+    /// - Returns: <#description#>
+   public func paraseDecimalFormatterValue(isDecimal: Bool = false) -> String? {
+        let decimal = self % 100
+        let nonDecimal = self / 100
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        if let title = formatter.string(from: NSNumber(value: nonDecimal)) {
+            if isDecimal {
+                return title + String(format: ".%02d", decimal)
+            } else {
+                return title
+            }
+        }
+        return nil
+    }
+
+    /// 改变单位为万
+    /// @param originUnit <#originUnit description#>
+    public func changeUnit() -> String {
+        var unitStr: String = ""
+        if self < 10000 {
+            unitStr = "\(self)"
+        } else if self <= 1_000_000 {
+            let divisor = pow(10.0, Double(1))
+            let decimal = ((Double(self) / 10000) * divisor).rounded() / divisor
+            unitStr = "\(decimal)万"
+        } else {
+            unitStr = "\(self / 10000)万"
+        }
+        BFLog(message: "转化单位:\(self) = \(unitStr)")
+        return unitStr
+    }
+}
+
+// MARK: - Float64 double类型扩展
+
+/// Float64 double类型扩展
+
+extension Float64 {
+    /// 时长转化为分秒 62'52"
+    /// - Returns: <#description#>
+    public func formatDurationToMS() -> String {
+        let duration = lround(self)
+        var text = ""
+        let min = duration / 60
+        let second = duration % 60
+        if min > 0, second > 0 {
+            text = "\(min)" + "\'" + "\(second)" + "\""
+        } else if min > 0 {
+            text = "\(min)" + "\'" + "0\""
+        } else {
+            text = "\(second)" + "\""
+        }
+        return text
+    }
+
+    /// 时长转化成时分秒 01:02:52
+    /// - Parameter value: <#value description#>
+    /// - Returns: <#description#>
+    public  func formatDurationToHMS() -> String {
+        var theTime = lround(self)
+        var theTime1 = 0 // 分
+        var theTime2 = 0 // 小时
+        if theTime < 60 {
+            if theTime < 10 {
+                return "00:0\(theTime)"
+            }
+            return "00:\(theTime)"
+        }
+        theTime1 = theTime / 60
+        theTime = theTime % 60
+        if theTime1 > 60 {
+            theTime2 = theTime1 / 60
+            theTime1 = theTime1 % 60
+        }
+        var result = "\(theTime)"
+        if theTime < 10 {
+            result = "0" + result
+        }
+        if theTime1 > 0 {
+            result = "\(theTime1):" + result
+            if theTime1 < 10 {
+                result = "0" + result
+            }
+        }
+        if theTime2 > 0 {
+            result = "\(theTime2):" + result
+            if theTime2 < 10 {
+                result = "0" + result
+            }
+        }
+        return result
+    }
+}

+ 204 - 0
BFCommonKit/Classes/BFCategorys/BFString+Ext.swift

@@ -0,0 +1,204 @@
+//
+//  String+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/7/22.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import MobileCoreServices
+import CommonCrypto
+
+extension String {
+    /// md5加密
+   public var md5: String {
+        let str = cString(using: String.Encoding.utf8)
+        let strLen = CUnsignedInt(lengthOfBytes(using: String.Encoding.utf8))
+        let digestLen = Int(CC_MD5_DIGEST_LENGTH)
+        let result = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: digestLen)
+        CC_MD5(str!, strLen, result)
+        let hash = NSMutableString()
+        for i in 0..<digestLen {
+            hash.appendFormat("%02x", result[i])
+        }
+        result.deallocate()
+        return hash as String
+    }
+    
+    // 文件后缀名
+    public  var pathExtension: String {
+        return (self as NSString).pathExtension
+    }
+
+    public func ga_widthForComment(font: UIFont, height: CGFloat = 15) -> CGFloat {
+
+        let rect = NSString(string: self).boundingRect(with: CGSize(width: CGFloat(MAXFLOAT), height: height), options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
+        return ceil(rect.width)
+    }
+//
+//    public  func ga_heightForComment(fontSize: CGFloat, width: CGFloat) -> CGFloat {
+//        let font = UIFont.systemFont(ofSize: fontSize)
+//        let rect = NSString(string: self).boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
+//        return ceil(rect.height)
+//    }
+//
+//    public  func ga_heightForComment(fontSize: CGFloat, width: CGFloat, maxHeight: CGFloat) -> CGFloat {
+//        let font = UIFont.systemFont(ofSize: fontSize)
+//        let rect = NSString(string: self).boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
+//        return ceil(rect.height)>maxHeight ? maxHeight : ceil(rect.height)
+//    }
+ 
+
+    public  func enumerateSearchText(searchText: String?, complate: (_ idx: Int, _ range: NSRange) -> Void) {
+        if searchText == nil || (searchText?.count ?? 0) <= 0 || !contains(searchText!) {
+            return
+        }
+        let len: Int = searchText!.count
+        var loc: Int = 0
+        let subStrings: [String] = NSString(string: self).components(separatedBy: searchText!)
+        for (idx, subText) in subStrings.enumerated() {
+            loc = loc + subText.count
+            let range = NSRange(location: loc, length: len)
+            complate(idx, range)
+            loc = loc + len
+            if idx == subStrings.count - 2 {
+                break
+            }
+        }
+    }
+
+    public  func attributedTextWithSearchText(searchText: String?, textColor: UIColor, textFont: UIFont, searchTextColor: UIColor, searchTextFont: UIFont) -> NSMutableAttributedString {
+        let attbText = NSMutableAttributedString(string: self, attributes: [NSAttributedString.Key.font: textFont, NSAttributedString.Key.foregroundColor: textColor])
+        if count <= 0 || searchText == nil || (searchText?.count ?? 0) <= 0 {
+            return attbText
+        }
+        for tempStr in searchText! {
+            BFLog(message: "self = \(self),searchText = \(searchText ?? ""),tempStr = \(tempStr)")
+            enumerateSearchText(searchText: "\(tempStr)") { _, range in
+                attbText.setAttributes([NSAttributedString.Key.font: searchTextFont, NSAttributedString.Key.foregroundColor: searchTextColor], range: range)
+            }
+        }
+        return attbText
+    }
+
+    // 判断是否为空
+    public  var isSpace: Bool {
+        return allSatisfy { $0.isWhitespace }
+    }
+
+    /// 通过 文件路径/文件名/文件后缀 获取mimeType(文件媒体类型)
+    /// - Parameter pathExtension: <#pathExtension description#>
+    /// - Returns: <#description#>
+    public func mimeType() -> String {
+        if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (self as NSString).pathExtension as CFString, nil)?.takeRetainedValue() {
+            if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?
+                .takeRetainedValue()
+            {
+                return mimetype as String
+            }
+        }
+        return "application/octet-stream"
+    }
+
+    // 将原始的url编码为合法的url
+    public  func urlEncoded() -> String {
+        let encodeUrlString = addingPercentEncoding(withAllowedCharacters:
+            .urlQueryAllowed)
+        return encodeUrlString ?? ""
+    }
+
+    // 将编码后的url转换回原始的url
+    public func urlDecoded() -> String {
+        return removingPercentEncoding ?? ""
+    }
+
+    // 判断是否包含Emoji表情
+    public func isEmoji() -> Bool {
+        let numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
+        guard !numbers.contains(self) else {
+            return false
+        }
+        let tempEmoji: [Unicode.Scalar] = ["\u{26F8}", "\u{26F7}", "\u{263A}", "\u{2639}", "\u{2620}", "\u{270C}", "\u{261D}", "\u{0001F441}", "\u{0001F5E3}", "\u{0001F575}", "\u{0001F574}", "\u{26D1}", "\u{0001F576}", "\u{0001F577}", "\u{0001F54A}", "\u{0001F43F}", "\u{2618}", "\u{0001F32A}", "\u{2600}", "\u{0001F324}", "\u{0001F325}", "\u{2601}", "\u{0001F326}", "\u{0001F327}", "\u{26C8}", "\u{0001F329}", "\u{0001F328}", "\u{2744}", "\u{2603}", "\u{0001F32C}", "\u{2602}", "\u{0001F32B}", "\u{0001F336}", "\u{0001F37D}"]
+        let scalars = unicodeScalars.map { $0.value }
+        for element in scalars {
+            if let scalar = Unicode.Scalar(element) {
+                if #available(iOS 10.2, *) {
+                    if (scalar.properties.isEmoji && scalar.properties.isEmojiPresentation) || tempEmoji.contains(scalar) {
+                        return true
+                    } else {
+                        BFLog(message: "是表情==\(element),\(scalar)")
+                    }
+                }
+            }
+        }
+        return false
+    }
+
+    var isContainsEmoji: Bool {
+        for scalar in unicodeScalars {
+            switch scalar.value {
+            case 0x1F600...0x1F64F, // Emoticons
+                 0x1F300...0x1F5FF, // Misc Symbols and Pictographs
+                 0x1F680...0x1F6FF, // Transport and Map
+                 0x2600...0x26FF, // Misc symbols
+                 0x2700...0x27BF, // Dingbats
+                 0xFE00...0xFE0F: // Variation Selectors
+                return true
+            default:
+                continue
+            }
+        }
+        return false
+    }
+
+    // 是否包含表情
+    var containsEmoji: Bool {
+        for scalar in unicodeScalars {
+            switch scalar.value {
+            case
+                0x00A0...0x00AF,
+                0x2030...0x204F,
+                0x2120...0x213F,
+                0x2190...0x21AF,
+                0x2310...0x329F,
+                0x1F000...0x1F9CF:
+                return true
+            default:
+                continue
+            }
+        }
+        return false
+    }
+
+    /**
+     * 字母、数字、中文正则判断(不包括空格)
+     *注意: 因为考虑到输入习惯,许多人习惯使用九宫格,这里在正常选择全键盘输入错误的时候,进行九宫格判断,九宫格对应的是下面➋➌➍➎➏➐➑➒的字符
+     */
+    static func isInputRuleNotBlank(str: String) -> Bool {
+        let pattern = "^[a-zA-Z\\u4E00-\\u9FA5\\d]*$"
+        let pred = NSPredicate(format: "SELF MATCHES %@", pattern)
+        let isMatch = pred.evaluate(with: str)
+        if !isMatch {
+            let other = "➋➌➍➎➏➐➑➒"
+            let len = str.count
+            for i in 0..<len {
+                let tmpStr = str as NSString
+                let tmpOther = other as NSString
+                let c = tmpStr.character(at: i)
+
+                if !(isalpha(Int32(c)) > 0 || isalnum(Int32(c)) > 0 || (Int(c) == "_".hashValue) || (Int(c) == "-".hashValue) || (c >= 0x4E00 && c <= 0x9FA6) || (tmpOther.range(of: str).location != NSNotFound)) {
+                    return false
+                }
+                return true
+            }
+        }
+        return isMatch
+    }
+}
+
+extension Optional where Wrapped == String {
+    public var isSpace: Bool {
+        return self?.isSpace ?? true
+    }
+}

+ 63 - 0
BFCommonKit/Classes/BFCategorys/BFUIButton+ext.swift

@@ -0,0 +1,63 @@
+//
+//  UIButton+ext.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/8/14.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+/**
+ UIButton图像文字同时存在时---图像相对于文字的位置
+
+ - top:    图像在上
+ - left:   图像在左
+ - right:  图像在右
+ - bottom: 图像在下
+ */
+public enum PQButtonImageEdgeInsetsStyle {
+    case top, left, right, bottom
+}
+
+import Foundation
+extension UIButton {
+    public func imagePosition(at style: PQButtonImageEdgeInsetsStyle, space: CGFloat) {
+        guard let imageV = imageView else { return }
+        guard let titleL = titleLabel else { return }
+        // 获取图像的宽和高
+        let imageWidth = imageV.frame.size.width
+        let imageHeight = imageV.frame.size.height
+        // 获取文字的宽和高
+        let labelWidth = titleL.frame.size.width
+        let labelHeight = titleL.frame.size.height
+
+        var imageEdgeInsets = UIEdgeInsets.zero
+        var labelEdgeInsets = UIEdgeInsets.zero
+        // UIButton同时有图像和文字的正常状态---左图像右文字,间距为0
+        switch style {
+        case .left:
+            // 正常状态--只不过加了个间距
+            imageEdgeInsets = UIEdgeInsets(top: 0, left: -space * 0.5, bottom: 0, right: space * 0.5)
+            labelEdgeInsets = UIEdgeInsets(top: 0, left: space * 0.5, bottom: 0, right: -space * 0.5)
+        case .right:
+            // 切换位置--左文字右图像
+            // 图像:UIEdgeInsets的left是相对于UIButton的左边移动了labelWidth + space * 0.5,right相对于label的左边移动了-labelWidth - space * 0.5
+            imageEdgeInsets = UIEdgeInsets(top: 0, left: labelWidth + space * 0.5, bottom: 0, right: -labelWidth - space * 0.5)
+            labelEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth - space * 0.5, bottom: 0, right: imageWidth + space * 0.5)
+        case .top:
+            // 切换位置--上图像下文字
+            /** 图像的中心位置向右移动了labelWidth * 0.5,向上移动了-imageHeight * 0.5 - space * 0.5
+              *文字的中心位置向左移动了imageWidth * 0.5,向下移动了labelHeight*0.5+space*0.5
+             */
+            imageEdgeInsets = UIEdgeInsets(top: -(imageHeight * 0.5 - space), left: labelWidth * 0.5, bottom: imageHeight * 0.5 - space, right: -labelWidth * 0.5)
+            labelEdgeInsets = UIEdgeInsets(top: labelHeight * 0.5 + space * 2, left: -imageWidth * 0.5, bottom: -(labelHeight * 0.5 + space * 2), right: imageWidth * 0.5)
+        case .bottom:
+            // 切换位置--下图像上文字
+            /** 图像的中心位置向右移动了labelWidth * 0.5,向下移动了imageHeight * 0.5 + space * 0.5
+             *文字的中心位置向左移动了imageWidth * 0.5,向上移动了labelHeight*0.5+space*0.5
+             */
+            imageEdgeInsets = UIEdgeInsets(top: imageHeight * 0.5 + space * 0.5, left: labelWidth * 0.5, bottom: -imageHeight * 0.5 - space * 0.5, right: -labelWidth * 0.5)
+            labelEdgeInsets = UIEdgeInsets(top: -labelHeight * 0.5 - space * 0.5, left: -imageWidth * 0.5, bottom: labelHeight * 0.5 + space * 0.5, right: imageWidth * 0.5)
+        }
+        titleEdgeInsets = labelEdgeInsets
+        self.imageEdgeInsets = imageEdgeInsets
+    }
+}

+ 234 - 0
BFCommonKit/Classes/BFCategorys/BFUIImage+Ext.swift

@@ -0,0 +1,234 @@
+//
+//  UIImage+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/19.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+public extension UIImage {
+    // 从BFframwork bundle 中取图片
+    func BF_Image(named: String) -> UIImage {
+        let image: UIImage = UIImage(named: named, in: Bundle().BF_mainbundle(), compatibleWith: nil) ?? UIImage()
+        return image
+    }
+
+    func cropImage(ratio: CGFloat) -> UIImage {
+        // 计算最终尺寸
+        let newSize: CGSize = CGSize(width: size.width, height: size.width * ratio)
+        // 图片绘制区域
+        var rect = CGRect.zero
+        rect.size.width = size.width
+        rect.size.height = size.height
+        rect.origin.x = (newSize.width - size.width) / 2.0
+        rect.origin.y = (newSize.height - size.height) / 2.0
+
+        UIGraphicsBeginImageContext(newSize)
+        draw(in: rect)
+        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return scaledImage!
+    }
+
+    func cropImage(newSize: CGSize) -> UIImage {
+        //// 图片绘制区域
+        var rect = CGRect.zero
+        rect.size.width = newSize.width
+        rect.size.height = newSize.width * (size.height / size.width)
+        // 绘制并获取最终图片
+        UIGraphicsBeginImageContext(newSize)
+        draw(in: rect)
+        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return scaledImage!
+    }
+
+    func imageWithImage(scaledToSize newSize: CGSize) -> UIImage {
+        UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
+        draw(in: CGRect(origin: CGPoint.zero, size: newSize))
+        let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
+        UIGraphicsEndImageContext()
+        return newImage
+    }
+
+    /// 旋转角度
+    /// - Parameter image: <#image description#>
+    /// - Returns: <#description#>
+    func rotateImage(rotate: Int, originWidth: CGFloat, originHeight: CGFloat) -> UIImage {
+        let rotate: CGFloat = CGFloat(3 * Double.pi / 2)
+        let rect = CGRect(x: 0, y: 0, width: originWidth, height: originHeight)
+        let translateX: CGFloat = -rect.size.height
+        let translateY: CGFloat = 0
+        let scaleY = rect.size.width / rect.size.height
+        let scaleX = rect.size.height / rect.size.width
+        UIGraphicsBeginImageContext(rect.size)
+        let context = UIGraphicsGetCurrentContext()
+        //        context!.translateBy(x: 0.0, y: rect.size.height)
+        //        context!.scaleBy(x: 1.0, y: -1.0)
+        context!.rotate(by: rotate)
+        context!.translateBy(x: translateX, y: translateY)
+        context!.scaleBy(x: scaleX, y: scaleY)
+        draw(in: CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height))
+        return UIGraphicsGetImageFromCurrentImageContext()!
+    }
+
+    /// 生成三角图
+    /// - Parameters:
+    ///   - size: <#size description#>
+    ///   - tintColor: <#tintColor description#>
+    ///   - convert:是否倒置
+    /// - Returns: <#description#>
+    class func triangleImage(size: CGSize, tintColor: UIColor, direction: moveDirection = .moveDirectionDown) -> UIImage {
+        var startPoint: CGPoint = CGPoint.zero
+        var middlePoint: CGPoint = CGPoint.zero
+        var endPoint: CGPoint = CGPoint.zero
+        switch direction {
+        case .moveDirectionLeft:
+            startPoint = CGPoint(x: size.width, y: 0)
+            middlePoint = CGPoint(x: 0, y: size.height / 2.0)
+            endPoint = CGPoint(x: size.width, y: size.height)
+        case .moveDirectionRight:
+            startPoint = CGPoint(x: 0, y: 0)
+            middlePoint = CGPoint(x: size.width, y: size.height / 2.0)
+            endPoint = CGPoint(x: 0, y: size.height)
+        case .moveDirectionUp:
+            startPoint = CGPoint(x: 0, y: size.height)
+            middlePoint = CGPoint(x: size.width / 2.0, y: 0)
+            endPoint = CGPoint(x: size.width, y: size.height)
+        default:
+            startPoint = CGPoint(x: 0, y: 0)
+            middlePoint = CGPoint(x: size.width / 2.0, y: size.height)
+            endPoint = CGPoint(x: size.width, y: 0)
+        }
+        UIGraphicsBeginImageContextWithOptions(size, false, 0)
+        let ctx = UIGraphicsGetCurrentContext()
+        let path = UIBezierPath()
+        path.move(to: startPoint)
+        path.addLine(to: middlePoint)
+        path.addLine(to: endPoint)
+        path.close()
+        ctx?.setFillColor(tintColor.cgColor)
+        path.fill()
+        let image = UIGraphicsGetImageFromCurrentImageContext()!
+        UIGraphicsEndImageContext()
+        return image
+    }
+
+    /// 按照最短边缩放  add by ak
+    /// - Parameter maxLength: 边长最大值
+    func nx_scaleWithMaxLength(maxLength: CGFloat) -> UIImage {
+        if size.width > maxLength || size.height > maxLength {
+            var maxWidth: CGFloat = maxLength
+            var maxHeight: CGFloat = maxLength
+
+            if size.width != size.height {
+                if size.width > size.height {
+                    // 按照宽 来缩放
+                    let imageScale: CGFloat = maxLength / size.width
+
+                    maxHeight = size.height * imageScale
+                } else if size.width < size.height {
+                    let imageScale: CGFloat = maxLength / size.height
+
+                    maxWidth = size.width * imageScale
+                }
+            }
+            // 返回新的改变大小后的图片
+            return nx_scaleToSize(size: CGSize(width: maxWidth, height: maxHeight))
+        }
+
+        return self
+    }
+
+    /// 缩放到指定大小 add by ak
+    /// - Parameter size: 新的大小
+    func nx_scaleToSize(size: CGSize) -> UIImage {
+        var width: CGFloat = CGFloat(cgImage!.width)
+        var height: CGFloat = CGFloat(cgImage!.height)
+
+        let verticalRadio: CGFloat = size.height * 1.0 / height
+        let horizontalRadio: CGFloat = size.width * 1.0 / width
+
+        var radio: CGFloat = 1
+        if verticalRadio > 1, horizontalRadio > 1 {
+            radio = verticalRadio > horizontalRadio ? horizontalRadio : verticalRadio
+        } else {
+            radio = verticalRadio < horizontalRadio ? verticalRadio : horizontalRadio
+        }
+
+        width = width * radio
+        height = height * radio
+
+        let xPos: CGFloat = (size.width - width) / 2
+        let yPos: CGFloat = (size.height - height) / 2
+
+        // 创建一个bitmap的context
+        // 并把它设置成为当前正在使用的context
+        UIGraphicsBeginImageContext(size)
+
+        // 绘制改变大小的图片
+        var rect = CGRect.zero
+        rect.size.width = width
+        rect.size.height = height
+        rect.origin.x = xPos
+        rect.origin.y = yPos
+
+        draw(in: rect)
+
+        // 从当前context中创建一个改变大小后的图片
+        let scaledImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
+
+        // 使当前的context出堆栈
+        UIGraphicsEndImageContext()
+
+        // 返回新的改变大小后的图片
+        return scaledImage
+    }
+
+    // 将图片裁剪成指定比例(多余部分自动删除)let image3 = image.crop(ratio: 1) /将图片转成 1:1 比例(正方形)
+    func nxcrop(ratio: CGFloat) -> UIImage {
+        // 计算最终尺寸
+        var newSize: CGSize!
+        if size.width / size.height > ratio {
+            newSize = CGSize(width: size.height * ratio, height: size.height)
+        } else {
+            newSize = CGSize(width: size.width, height: size.width / ratio)
+        }
+
+        ////图片绘制区域
+        var rect = CGRect.zero
+        rect.size.width = size.width
+        rect.size.height = size.height
+        rect.origin.x = (newSize.width - size.width) / 2.0
+        rect.origin.y = (newSize.height - size.height) / 2.0
+
+        // 绘制并获取最终图片
+        UIGraphicsBeginImageContext(newSize)
+        draw(in: rect)
+        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return scaledImage!
+    }
+    
+    /// 改变图片主题颜色
+    /// - Parameters:
+    ///   - color: <#color description#>
+    ///   - blendMode: <#blendMode description#>
+    /// - Returns: <#description#>
+    func tintImage(color: UIColor, blendMode: CGBlendMode) -> UIImage? {
+        let rect = CGRect(origin: CGPoint.zero, size: size)
+        UIGraphicsBeginImageContextWithOptions(size, false, scale)
+        color.setFill()
+        UIRectFill(rect)
+        draw(in: rect, blendMode: blendMode, alpha: 1.0)
+        let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return tintedImage
+    }
+}
+

+ 458 - 0
BFCommonKit/Classes/BFCategorys/BFUIView+Ext.swift

@@ -0,0 +1,458 @@
+//
+//  UICollectionView+Ext.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/6.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import KingfisherWebP
+import UIKit
+
+// MARK: - UIView的分类扩展
+
+/// UIView的分类扩展
+extension UIView {
+      public func addCorner(roundingCorners: UIRectCorner = .allCorners, corner: CGFloat = cDefaultMargin) {
+        if roundingCorners == .allCorners {
+            layer.cornerRadius = corner
+            layer.masksToBounds = true
+        } else {
+            let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: roundingCorners, cornerRadii: CGSize(width: corner, height: corner))
+            let cornerLayer = CAShapeLayer()
+            cornerLayer.frame = bounds
+            cornerLayer.path = path.cgPath
+            layer.mask = cornerLayer
+        }
+    }
+
+    /// 添加阴影
+    /// - Parameters:
+    ///   - color: <#color description#>
+    ///   - offset: <#offset description#>
+    /// - Returns: <#description#>
+     public func addShadowLayer(isAll: Bool = false, color: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5), offset: CGSize = CGSize(width: 1, height: 1)) {
+        layer.shadowColor = color.cgColor
+        layer.shadowOffset = offset
+        layer.shadowRadius = 0.5
+        layer.shadowOpacity = 1.0
+        if isAll {
+            layer.shadowColor = color.cgColor
+            layer.shadowOffset = CGSize.zero
+            // 设置偏移量为0,四周都有阴影
+            layer.shadowRadius = 0.5 // 阴影半径
+            layer.shadowOpacity = 0.3 // 阴影透明度
+            layer.masksToBounds = false
+            layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
+        }
+    }
+
+    /// 添加虚线条
+     public func addBorderToLayer(frame: CGRect? = nil) {
+        // 线条颜色
+        let borderLayer: CAShapeLayer = CAShapeLayer()
+        borderLayer.strokeColor = UIColor.hexColor(hexadecimal: "#FFFFFF").cgColor
+        borderLayer.fillColor = nil
+        borderLayer.path = UIBezierPath(rect: frame == nil ? bounds : frame!).cgPath
+        borderLayer.frame = bounds
+        borderLayer.lineWidth = 2.0
+        borderLayer.lineCap = .round
+        // 第一位是 线条长度   第二位是间距 nil时为实线
+        borderLayer.lineDashPattern = [5, 5]
+        layer.addSublayer(borderLayer)
+    }
+
+     public func animateZoom() {
+        transform = CGAffineTransform(scaleX: 0.4, y: 0.4)
+        UIView.animate(withDuration: 0.5, animations: {
+            self.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
+        }) { _ in
+            UIView.animate(withDuration: 0.5, animations: {
+                self.transform = .identity
+            }) { _ in
+            }
+        }
+    }
+
+    /// 添加抖动功能
+    /// - Parameters:
+    ///   - fromValue: <#fromValue description#>
+    ///   - toValue: <#toValue description#>
+    ///   - duration: <#duration description#>
+    ///   - repeatCount: <#repeatCount description#>
+    /// - Returns: <#description#>
+     public func shakeAnimation(_ fromValue: Float, _ toValue: Float, _ duration: Float, _: Float) {
+        layer.removeAllAnimations()
+        let shake = CABasicAnimation(keyPath: "transform.rotation.z")
+        shake.fromValue = fromValue
+        shake.toValue = toValue
+        shake.duration = CFTimeInterval(duration)
+        shake.autoreverses = true
+        shake.repeatCount = Float(CGFloat.greatestFiniteMagnitude)
+        shake.isRemovedOnCompletion = false
+        layer.add(shake, forKey: "imageView")
+        // 增加锚点
+        layer.anchorPoint = CGPoint(x: 0.5, y: 1)
+    }
+
+    /// 添加心跳动画
+    /// - Parameters:
+    ///   - duration: <#duration description#>
+    ///   - isRepeat: <#isRepeat description#>
+    ///   - multiple: <#multiple description#>
+    /// - Returns: <#description#>
+     public func heartbeatAnimate(duration: TimeInterval, isRepeat: Bool, multiple: CGFloat) {
+        UIView.animateKeyframes(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {
+            self.transform = CGAffineTransform(scaleX: 1.0 + multiple, y: 1.0 + multiple)
+        }) { _ in
+            UIView.animateKeyframes(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {
+                self.transform = .identity
+            }) { _ in
+                UIView.animateKeyframes(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {
+                    self.transform = CGAffineTransform(scaleX: 1.0 + multiple * 2, y: 1.0 + multiple * 2)
+                }) { _ in
+                    UIView.animateKeyframes(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {
+                        self.transform = .identity
+                    }) { _ in
+                        if isRepeat {
+                            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.3) {
+                                self.heartbeatAnimate(duration: duration, isRepeat: isRepeat, multiple: multiple)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /// 活动心跳动画
+    /// - Returns: <#description#>
+     public func activityHeartbeatAnimate() {
+        layer.removeAllAnimations()
+        UIView.animateKeyframes(withDuration: 0.45, delay: 0, options: .allowUserInteraction, animations: {
+            self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
+        }) { _ in
+            UIView.animateKeyframes(withDuration: 0.45, delay: 0, options: .allowUserInteraction, animations: {
+                self.transform = .identity
+            }) { _ in
+                self.activityHeartbeatAnimate()
+            }
+        }
+    }
+
+    /// 活动心跳动画
+    /// - Returns: <#description#>
+     public func heartbeatAnimate() {
+        layer.removeAllAnimations()
+        UIView.animateKeyframes(withDuration: 1, delay: 0, options: .allowUserInteraction, animations: {
+            self.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
+        }) { isFinished in
+            UIView.animateKeyframes(withDuration: 1, delay: 0, options: .allowUserInteraction, animations: {
+                self.transform = .identity
+            }) { isFinished in
+                if isFinished {
+                    self.heartbeatAnimate()
+                }
+            }
+        }
+    }
+
+     public func addScaleBasicAnimation() {
+        let animation = CABasicAnimation(keyPath: "transform.scale")
+        animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
+        animation.duration = 0.5
+        animation.repeatCount = 1000
+        animation.autoreverses = true
+        animation.fromValue = 0.8
+        animation.toValue = 1.1
+        layer.add(animation, forKey: nil)
+    }
+
+     public func addScaleYBasicAnimation() {
+        let animation = CAKeyframeAnimation(keyPath: "transform.translation.y")
+        animation.duration = 0.5
+        animation.repeatCount = 100
+        animation.isRemovedOnCompletion = true
+        //        animation.
+        animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
+        layer.add(animation, forKey: nil)
+        //            CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
+        //            CGFloat duration = 1.f;
+        //            CGFloat height = 7.f;
+        //            CGFloat currentY = self.animationView.transform.ty;
+        //            animation.duration = duration;
+        //            animation.values = @[@(currentY),@(currentY - height/4),@(currentY - height/4*2),@(currentY - height/4*3),@(currentY - height),@(currentY - height/ 4*3),@(currentY - height/4*2),@(currentY - height/4),@(currentY)];
+        //            animation.keyTimes = @[ @(0), @(0.025), @(0.085), @(0.2), @(0.5), @(0.8), @(0.915), @(0.975), @(1) ];
+        //            animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+        //            animation.repeatCount = HUGE_VALF;
+        //            [self.animationView.layer addAnimation:animation forKey:@"kViewShakerAnimationKey"];
+        //        }
+    }
+
+    /// 将view生成一张图片
+    /// - Returns: <#description#>
+     public func graphicsGetImage() -> UIImage? {
+        UIGraphicsBeginImageContextWithOptions(frame.size, true, 0.0)
+        layer.render(in: UIGraphicsGetCurrentContext()!)
+        let newImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return newImage
+    }
+
+    /// 动画显示View
+    /// - Returns: <#description#>
+     public func showViewAnimate(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) {
+        UIView.animate(withDuration: duration, animations: { [weak self] in
+            self?.frame = CGRect(x: 0, y: cScreenHeigth - self!.frame.height, width: self!.frame.width, height: self!.frame.height)
+        }) { isFinished in
+            if completion != nil {
+                completion!(isFinished)
+            }
+        }
+    }
+
+    /// 动画隐藏view
+    /// - Returns: <#description#>
+     public func dismissViewAnimate(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) {
+        UIView.animate(withDuration: duration, animations: { [weak self] in
+            self?.frame = CGRect(x: 0, y: cScreenHeigth, width: self!.frame.width, height: self!.frame.height)
+        }) { isFinished in
+            if completion != nil {
+                completion!(isFinished)
+            }
+        }
+    }
+
+    /// add  by ak 添加虚线框
+    /// - Parameter color: 框色
+    /// - Parameter lineWidth: 框宽
+     public func addBorderToLayer(color: CGColor, lineWidth: CGFloat) {
+        let border = CAShapeLayer()
+
+        //  线条颜色
+        border.strokeColor = color
+        border.fillColor = nil
+
+        border.path = UIBezierPath(rect: bounds).cgPath
+
+        border.frame = bounds
+        border.lineWidth = lineWidth
+        border.lineCap = .square
+
+        //  第一位是 线条长度   第二位是间距 nil时为实线
+        border.lineDashPattern = [9, 4]
+        layer.addSublayer(border)
+    }
+}
+
+// MARK: - UICollectionView的分类扩展
+
+/// UICollectionView的分类扩展
+extension UICollectionView {
+    /// 获取当前cell
+    /// - Returns: <#description#>
+     public func visibleCell() -> UICollectionViewCell? {
+        let visibleRect = CGRect(origin: contentOffset, size: bounds.size)
+        let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
+        guard let visibleIndexPath = indexPathForItem(at: visiblePoint) else { return nil }
+        return cellForItem(at: visibleIndexPath)
+    }
+
+     public func indexPathsForElements(in rect: CGRect) -> [IndexPath] {
+        let allLayoutAttributes = collectionViewLayout.layoutAttributesForElements(in: rect)!
+        return allLayoutAttributes.map { $0.indexPath }
+    }
+}
+
+// MARK: - UITabBar的分类扩展
+
+/// UITabBar的分类扩展
+extension UITabBar {
+    /// 展示小红点
+    /// - Parameter index: <#index description#>
+    /// - Returns: <#description#>
+     public func showPoint(index: Int) {
+        let pointW: CGFloat = 8
+        let pointView = UIView()
+        pointView.tag = 11111 + index
+        pointView.layer.cornerRadius = pointW / 2
+        pointView.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+        let percentX: CGFloat = CGFloat(Double(index) + 0.7) / CGFloat(items?.count ?? 1)
+        let pointX = ceil(percentX * frame.width)
+        let pointY = ceil(0.1 * frame.height)
+        pointView.frame = CGRect(x: pointX, y: pointY, width: pointW, height: pointW)
+        addSubview(pointView)
+    }
+
+    /// 移除小红点
+    /// - Parameter index: <#index description#>
+    /// - Returns: <#description#>
+     public func removePoint(index: Int) {
+        for item in subviews {
+            if item.tag == 11111 + index {
+                item.removeFromSuperview()
+            }
+        }
+    }
+
+    /// 展示创作视频引导
+    /// - Parameter index: <#index description#>
+    /// - Returns: <#description#>
+     public func showVideoMakeRemindView() {
+        let isOldUploadClick: String? = getUserDefaults(key: cIsUploadClick) as? String
+        let isUploadClick: String? = getUserDefaultsForJson(key: cIsUploadClick) as? String
+        let isVerticalSlip: String? = getUserDefaults(key: cIsVerticalSlip) as? String
+        if isOldUploadClick == nil && isVerticalSlip != nil && isVerticalSlip == "1", isUploadClick == nil || isUploadClick?.count ?? 0 <= 0 || isUploadClick != "2" {
+            let width: CGFloat = 275 // 275
+            let height: CGFloat = 107 // 107
+            let videoMakeRemindBtn = UIButton(frame: CGRect(x: 0, y: -height + 5, width: width, height: height))
+            videoMakeRemindBtn.tag = cVideoMakeRemindTag
+            videoMakeRemindBtn.setBackgroundImage(UIImage(named: "videomk_guide"), for: .normal)
+            addSubview(videoMakeRemindBtn)
+            videoMakeRemindBtn.center.x = center.x
+            videoMakeRemindBtn.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
+            videoMakeRemindBtn.addScaleBasicAnimation()
+            if isUploadClick == "1" {
+                saveUserDefaultsToJson(key: cIsUploadClick, value: "2")
+            } else {
+                saveUserDefaultsToJson(key: cIsUploadClick, value: "1")
+            }
+            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { [weak self] in
+                self?.removeVideoMakeRemindView()
+            }
+        }
+    }
+
+    @objc  public func dismiss() {
+  
+    }
+
+    /// 移除创作视频引导
+    /// - Returns: <#description#>
+     public func removeVideoMakeRemindView() {
+        viewWithTag(cVideoMakeRemindTag)?.removeFromSuperview()
+    }
+}
+
+// MARK: - UILabel的分类扩展
+
+/// UILabel的分类扩展
+extension UILabel {
+    var isTruncated: Bool {
+        guard let labelText = text else {
+            return false
+        }
+
+        // 计算理论上显示所有文字需要的尺寸
+        let rect = CGSize(width: bounds.width, height: CGFloat.greatestFiniteMagnitude)
+        let labelTextSize = (labelText as NSString)
+            .boundingRect(with: rect, options: .usesLineFragmentOrigin,
+                          attributes: [NSAttributedString.Key.font: font as Any], context: nil)
+
+        // 计算理论上需要的行数
+        let labelTextLines = Int(ceil(CGFloat(labelTextSize.height) / font.lineHeight))
+
+        // 实际可显示的行数
+        var labelShowLines = Int(floor(CGFloat(bounds.size.height) / font.lineHeight))
+        if numberOfLines != 0 {
+            labelShowLines = min(labelShowLines, numberOfLines)
+        }
+
+        // 比较两个行数来判断是否需要截断
+        return labelTextLines > labelShowLines
+    }
+
+    /// 添加阴影
+    /// - Parameters:
+    ///   - color: <#color description#>
+    ///   - offset: <#offset description#>
+    /// - Returns: <#description#>
+     public func addShadow(color: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5), offset: CGSize = CGSize(width: 1, height: 1)) {
+        shadowColor = color
+        shadowOffset = offset
+    }
+}
+
+extension UIImageView {
+    /// imageView加载网络图片
+    /// - Parameters:
+    ///   - url: 网络url
+     public func setNetImage(url: String?, placeholder: UIImage = UIImage.init().BF_Image(named: "placehold_image")) {
+        if url == nil || (url?.count ?? 0) <= 0 {
+            BFLog(message: "设置按钮网络图片地址为空")
+            return
+        }
+        kf.setImage(with: URL(string: url!), placeholder: placeholder, options: url?.suffix(5) == ".webp" ? [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)] : nil, progressBlock: { _, _ in
+
+        }) {  _ in
+        }
+    }
+
+    /// 展示加载中动画
+    /// - Returns: <#description#>
+     public func showLoadingAnimation(duration: Double = 1) {
+        let rotationAnim = CABasicAnimation(keyPath: "transform.rotation.z")
+        rotationAnim.fromValue = 0
+        rotationAnim.toValue = Double.pi * 2
+        rotationAnim.repeatCount = MAXFLOAT
+        rotationAnim.duration = duration
+        rotationAnim.isRemovedOnCompletion = false
+        layer.add(rotationAnim, forKey: nil)
+    }
+
+    /// 播放GIF
+    /// - Parameters:
+    ///   - data : 图片二进制数据
+    ///   - images: 图片
+    ///   - repeatCount: 循环次数
+    ///   - duration: 时长
+    /// - Returns: <#description#>
+     public func displayGIF(data: Data? = nil, images: [UIImage]? = nil, repeatCount: Int = Int.max, duration: Double = 1) {
+        if images != nil, (images?.count ?? 0) > 0, !isAnimating {
+            layer.removeAllAnimations()
+            stopAnimating()
+            animationImages = images
+            animationDuration = duration
+            animationRepeatCount = repeatCount
+            startAnimating()
+        } else if images == nil && data != nil {
+            PQPHAssetVideoParaseUtil.parasGIFImage(data: data!) { [weak self] _, images, duration in
+                if images != nil, (images?.count ?? 0) > 0 {
+                    self?.displayGIF(images: images!, repeatCount: repeatCount, duration: duration ?? 1)
+                }
+            }
+        }
+    }
+    
+    /// 移除
+    public func removePlayGIF() {
+        layer.removeAllAnimations()
+        stopAnimating()
+    }
+}
+
+extension UIButton {
+    /// UIButton加载网络图片
+    /// - Parameters:
+    ///   - url: 网络url
+     public func setNetImage(url: String?, placeholder: UIImage = UIImage.init().BF_Image(named: "placehold_image")) {
+        if url == nil || (url?.count ?? 0) <= 0 {
+            BFLog(message: "设置按钮网络图片地址为空")
+            return
+        }
+        kf.setImage(with: URL(string: url!), for: .normal, placeholder: placeholder, options: url?.suffix(5) == ".webp" ? [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)] : nil, progressBlock: { _, _ in
+
+        }) { _ in
+        }
+    }
+
+    /// UIButton加载网络背景图片
+    /// - Parameters:
+    ///   - url: 网络url
+     public func setNetBackgroundImage(url: String, placeholder: UIImage = UIImage.init().BF_Image(named: "placehold_image")) {
+        kf.setBackgroundImage(with: URL(string: url), for: .normal, placeholder: placeholder, options: url.suffix(5) == ".webp" ? [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)] : nil, progressBlock: { _, _ in
+
+        }) { _ in
+        }
+    }
+}

+ 176 - 0
BFCommonKit/Classes/BFCategorys/NXFundation+Ext.swift

@@ -0,0 +1,176 @@
+//
+//  NXFundation+Ext.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+
+public extension Date {
+    var millisecondsSince1970:Int {
+        return Int((self.timeIntervalSince1970 * 1000.0).rounded())
+    }
+    
+    var secondsSince1970:Int {
+        return Int((self.timeIntervalSince1970).rounded())
+    }
+    
+    init(milliseconds:Int) {
+        self = Date(timeIntervalSince1970: TimeInterval(milliseconds / 1000))
+    }
+    
+    func getCurrentTimeString(_ format: String = "yyyy-MM-dd HH:mm:ss") -> String {
+        let nowDate = Date()
+        let formatter = DateFormatter()
+        formatter.dateFormat = format
+        formatter.locale = Locale(identifier: "en_US_POSIX")
+        formatter.timeZone = Foundation.TimeZone(identifier: "UTC")
+        //formatter.dateStyle = .MediumStyle
+        //formatter.timeStyle = .MediumStyle
+        return formatter.string(from: nowDate)
+    }
+    
+    var iso8601: String {
+        return Formatter.iso8601.string(from: self)
+    }
+}
+
+
+public extension Formatter {
+    static let iso8601: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.calendar = Calendar(identifier: .chinese)
+        formatter.locale = Locale(identifier: "Asia/Shanghai")
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
+        return formatter
+    }()
+}
+
+public extension String {
+    var dateFromISO8601: Date? {
+        return Formatter.iso8601.date(from: self)
+    }
+    
+    func appendLineToURL(fileURL: URL) throws {
+        try (self + "\n").appendToURL(fileURL: fileURL)
+    }
+    
+    func appendToURL(fileURL: URL) throws {
+        let data = self.data(using: String.Encoding.utf8)!
+        try data.append2File(fileURL: fileURL)
+    }
+
+    ///
+    func substring(to index: Int) -> String {
+        guard let end_Index = validEndIndex(original: index) else {
+            return self
+        }
+        return String(self[startIndex..<end_Index])
+    }
+    ///
+    func substring(from index: Int) -> String {
+        guard let start_index = validStartIndex(original: index)  else {
+            return self
+        }
+        return String(self[start_index..<endIndex])
+    }
+    ///
+    func sliceString(_ range: CountableRange<Int>) -> String {
+        guard
+            let startIndex = validStartIndex(original: range.lowerBound),
+            let endIndex   = validEndIndex(original: range.upperBound),
+            startIndex <= endIndex
+            else {
+                return ""
+        }
+        return String(self[startIndex..<endIndex])
+    }
+    ///
+    func sliceString(_ range: CountableClosedRange<Int>) -> String {
+        guard
+            let start_Index = validStartIndex(original: range.lowerBound),
+            let end_Index   = validEndIndex(original: range.upperBound),
+            startIndex <= endIndex
+            else {
+                return ""
+        }
+        if endIndex.encodedOffset <= end_Index.encodedOffset {
+            return String(self[start_Index..<endIndex])
+        }
+        return String(self[start_Index...end_Index])
+    }
+    
+    private func validIndex(original: Int) -> String.Index {
+        switch original {
+        case ...startIndex.encodedOffset : return startIndex
+        case endIndex.encodedOffset...   : return endIndex
+        default                          : return index(startIndex, offsetBy: original)
+        }
+    }
+    
+    private func validStartIndex(original: Int) -> String.Index? {
+        guard original <= endIndex.encodedOffset else { return nil }
+        return validIndex(original: original)
+    }
+    
+    private func validEndIndex(original: Int) -> String.Index? {
+        guard original >= startIndex.encodedOffset else { return nil }
+        return validIndex(original: original)
+    }
+    
+    ///
+    func toDate(formatter: String) -> Date {
+        let dateFormatter = DateFormatter()
+        dateFormatter.locale = Locale.current
+        dateFormatter.dateFormat = formatter
+        let date = dateFormatter.date(from: self)
+        return date!
+    }
+    
+}
+
+extension Data {
+    func append2File(fileURL: URL) throws {
+        if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
+            defer {
+                fileHandle.closeFile()
+            }
+            fileHandle.seekToEndOfFile()
+            fileHandle.write(self)
+        }
+        else {
+            try write(to: fileURL, options: .atomic)
+        }
+    }
+}
+
+public extension UInt32 {
+    
+    var double: Double {
+        return Double(self)
+    }
+    
+}
+
+public extension UIApplication {
+    class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
+        if let navigationController = controller as? UINavigationController {
+            return topViewController(controller: navigationController.visibleViewController)
+        }
+        if let tabController = controller as? UITabBarController {
+            if let selected = tabController.selectedViewController {
+                return topViewController(controller: selected)
+            }
+        }
+        if let presented = controller?.presentedViewController {
+            return topViewController(controller: presented)
+        }
+        return controller
+    }
+}
+

+ 392 - 0
BFCommonKit/Classes/BFCategorys/NXUI+Ext.swift

@@ -0,0 +1,392 @@
+//
+//  NXUI+Ext.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+
+import UIKit
+
+public extension UIViewController {
+    func showAlert(withTitle title: String?, message: String?) {
+        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+        let action = UIAlertAction(title: "OK", style: .cancel, handler: nil)
+        alert.addAction(action)
+        
+        present(alert, animated: true, completion: nil)
+    }
+    
+    var contentViewController: UIViewController {
+        if let navcon = self as? UINavigationController {
+            return navcon.visibleViewController ?? self
+        } else {
+            return self
+        }
+    }
+}
+
+// Shake oritention
+public enum ShakeDirection: Int {
+    case horizontal
+    case vertical
+}
+
+public extension UIView {
+    func shake(direction: ShakeDirection = .horizontal, times: Int = 5,
+                      interval: TimeInterval = 0.1, delta: CGFloat = 2,
+                      completion: (() -> Void)? = nil) {
+        UIView.animate(withDuration: interval, animations: { () -> Void in
+            switch direction {
+            case .horizontal:
+                self.layer.setAffineTransform( CGAffineTransform(translationX: delta, y: 0))
+                break
+            case .vertical:
+                self.layer.setAffineTransform( CGAffineTransform(translationX: 0, y: delta))
+                break
+            }
+        }) { (complete) -> Void in
+            if (times == 0) {
+                // last shaking finish, reset location, callback
+                UIView.animate(withDuration: interval, animations: { () -> Void in
+                    self.layer.setAffineTransform(CGAffineTransform.identity)
+                }, completion: { (complete) -> Void in
+                    completion?()
+                })
+            }
+            else {
+                // not last shaking, continue
+                self.shake(direction: direction, times: times - 1,  interval: interval,
+                           delta: delta * -1, completion:completion)
+            }
+        }
+    }
+}
+
+
+extension UIView {
+    var x: CGFloat {
+        set {
+            var frame = self.frame
+            frame.origin.x = newValue
+            self.frame = frame
+        }
+        get {
+            return self.frame.origin.x
+        }
+    }
+    
+    var y: CGFloat {
+        set {
+            var frame = self.frame
+            frame.origin.y = newValue
+            self.frame = frame
+        }
+        get {
+            return self.frame.origin.y
+        }
+    }
+    
+    var centerX: CGFloat {
+        set {
+            var center = self.center
+            center.x = newValue
+            self.center = center
+        }
+        get {
+            return self.center.x
+        }
+    }
+    
+    var centerY: CGFloat {
+        set {
+            var center = self.center
+            center.y = newValue
+            self.center = center
+        }
+        get {
+            return self.center.y
+        }
+    }
+    
+    var width: CGFloat {
+        set {
+            var frame = self.frame
+            frame.size.width = newValue
+            self.frame = frame
+        }
+        get {
+            return self.frame.size.width
+        }
+    }
+    
+    var height: CGFloat {
+        set {
+            var frame = self.frame
+            frame.size.height = newValue
+            self.frame = frame
+        }
+        get {
+            return self.frame.size.height
+        }
+    }
+    
+    var size: CGSize {
+        set {
+            var frame = self.frame
+            frame.size = newValue
+            self.frame = frame
+        }
+        get {
+            return self.frame.size
+        }
+    }
+    
+    var origin: CGPoint {
+        set {
+            var frame = self.frame
+            frame.origin = newValue
+            self.frame = frame
+        }
+        get {
+            return self.frame.origin
+        }
+    }
+    
+    var bottomY: CGFloat {
+        set {
+            var frame = self.frame
+            frame.origin.y = newValue - frame.size.height
+            self.frame = frame
+        }
+        get {
+            return self.height + self.y
+        }
+    }
+    
+    var rightX: CGFloat {
+        set {
+            var frame = self.frame
+            frame.origin.x = newValue - frame.size.width
+            self.frame = frame
+        }
+        get {
+            return self.width + self.x
+        }
+    }
+    
+    // MARK: - UIView round corner
+    ///
+    /// - Parameter cornerRadius: radius
+    func roundedCorners(cornerRadius: CGFloat) {
+        roundedCorners(cornerRadius: cornerRadius, borderWidth: 0, borderColor: nil)
+    }
+    
+    ///
+    /// - Parameters:
+    ///   - cornerRadius:
+    ///   - borderWidth:
+    ///   - borderColor:
+    func roundedCorners(cornerRadius: CGFloat?, borderWidth: CGFloat?, borderColor: UIColor?) {
+        self.layer.cornerRadius = cornerRadius!
+        self.layer.borderWidth = borderWidth!
+        self.layer.borderColor = borderColor?.cgColor
+        self.layer.masksToBounds = true
+    }
+    
+    ///
+    /// - Parameters:
+    ///   - cornerRadius:
+    ///   - rectCorner:
+    func roundedCorners(cornerRadius: CGFloat?, rectCorner: UIRectCorner?) {
+        let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: rectCorner!, cornerRadii: CGSize(width: cornerRadius!, height: cornerRadius!))
+        let layer = CAShapeLayer()
+        layer.frame = self.bounds
+        layer.path = path.cgPath
+        self.layer.mask = layer
+    }
+    
+    ///
+    /// - Parameters:
+    ///   - colors:
+    ///   - locations:
+    ///   - startPoint: [0...1]
+    ///   - endPoint: [0...1]
+    func gradientColor(colors: [CGColor], locations: [NSNumber], startPoint: CGPoint, endPoint: CGPoint) {
+        let gradientLayer = CAGradientLayer()
+        gradientLayer.colors = colors
+        gradientLayer.locations = locations
+        /*
+         // vertical
+         gradientLayer.startPoint = CGPoint(x: 0, y: 0)
+         gradientLayer.endPoint = CGPoint(x: 0, y: 1)
+         */
+        gradientLayer.startPoint = startPoint
+        gradientLayer.endPoint = endPoint
+        gradientLayer.frame = self.frame
+        self.layer.insertSublayer(gradientLayer, at: 0)
+    }
+    
+    // MARK: - UIView blur
+    ///
+    /// - Parameter style: UIBlurEffectStyle
+    func addBlurEffect(style: UIBlurEffect.Style) {
+        let effect = UIBlurEffect(style: UIBlurEffect.Style.light)
+        let effectView = UIVisualEffectView(effect: effect)
+        effectView.frame = self.bounds
+        self.backgroundColor = .clear
+        self.addSubview(effectView)
+        self.sendSubviewToBack(effectView)
+    }
+}
+public extension UIView {
+    
+    /// 往当前视图添加一个子视图
+    /// - Parameters:
+    ///   - rect: 子视图大小
+    ///   - bgColor: 子视图背景色
+    /// - Returns: 子视图
+    func nx_addView(rect:CGRect = .zero,bgColor:UIColor = .white) ->UIView{
+        let view = UIView(frame: rect)
+        view.backgroundColor = bgColor
+        self.addSubview(view)
+        return view
+    }
+    
+    /// 往当前视图添加UIImageView
+    /// - Parameters:
+    ///   - image: 图片对象
+    ///   - rect: UIImageView
+    ///   - contentMode: 图片填充模式
+    /// - Returns: 图片
+     func nx_addImageView(image:UIImage?,rect:CGRect = .zero, contentMode:ContentMode = .scaleAspectFit)->UIImageView{
+        let imageView = UIImageView(frame: rect);
+        imageView.image = image
+        imageView.contentMode = contentMode
+        self.addSubview(imageView)
+        return imageView
+    }
+    
+    /// 添加文本控件
+    /// - Parameters:
+    ///   - fontSize: 文本大小
+    ///   - text: 文本
+    ///   - textColor: 文本颜色
+    ///   - bgColor: 背景颜色
+    /// - Returns: 文本控件
+    func nx_addLabel(fontSize: CGFloat, text: String, textColor: UIColor, bgColor: UIColor) -> UILabel {
+        return nx_addLabel(font: UIFont.systemFont(ofSize: fontSize),
+                        text: text,
+                        textColor: textColor,
+                        bgColor: bgColor)
+    }
+    
+    /// 添加文本控件
+    /// - Parameters:
+    ///   - font: 文本大小
+    ///   - text: 文本
+    ///   - textColor: 文本颜色
+    ///   - bgColor: 背景颜色
+    /// - Returns: 文本控件
+    func nx_addLabel(font: UIFont, text: String, textColor: UIColor, bgColor: UIColor) -> UILabel {
+        let label = UILabel(frame: .zero)
+        label.font = font
+        label.text = text
+        label.textColor = textColor
+        label.backgroundColor = bgColor
+        self.addSubview(label)
+        return label
+    }
+    
+    /// 添加按钮控件
+    /// - Parameters:
+    ///   - rect: 控件大小
+    ///   - title: 标题
+    ///   - titleColor: 标题颜色
+    ///   - font: 字体
+    ///   - image: 图片
+    ///   - bgImg: 背景图片
+    ///   - target: 事件响应者
+    ///   - action: 事件响应方法
+    ///   - event: 响应事件
+    /// - Returns: 按钮
+    func nx_addButton(rect: CGRect, title: String, titleColor: UIColor, font: UIFont, image: UIImage?, bgImg: UIImage?, target: Any?, action: Selector?, event: UIControl.Event?) -> UIButton {
+            let btn = UIButton(type: .custom)
+            btn.frame = rect
+            btn.setTitle(title, for: .normal)
+            btn.setTitle(title, for: .highlighted)
+            btn.setTitleColor(titleColor, for: .normal)
+            btn.setTitleColor(titleColor, for: .highlighted)
+            btn.setImage(image, for: .normal)
+            btn.setImage(image, for: .highlighted)
+            btn.setBackgroundImage(bgImg, for: .normal)
+            btn.setBackgroundImage(bgImg, for: .highlighted)
+            btn.titleLabel?.font = font
+            if let sel = action, let e = event {
+                btn.addTarget(target, action: sel, for: e)
+            }
+            addSubview(btn)
+            return btn
+        }
+    
+    /// 添加一个文本类型的按钮控件
+    /// - Parameters:
+    ///   - rect: 按钮大小
+    ///   - title: 文本
+    ///   - titleColor: 文本颜色
+    ///   - target: 事件响应者
+    ///   - action: 事件响应方法
+    ///   - event:响应事件
+    /// - Returns: 按钮控件
+        func nx_addButton(rect: CGRect, title: String, titleColor: UIColor, target: Any?, action: Selector?, event: UIControl.Event?) -> UIButton {
+            return nx_addButton(rect: rect,
+                             title: title,
+                             titleColor: titleColor,
+                             font: UIFont.systemFont(ofSize: 14),
+                             image: nil,
+                             bgImg: nil,
+                             target: target,
+                             action: action,
+                             event: event)
+        }
+    
+    /// 添加图片类型按钮
+    /// - Parameters:
+    ///   - rect: 按钮大小
+    ///   - image: 图片
+    ///   - target: 事件响应者
+    ///   - action: 事件响应方法
+    ///   - event: 响应事件
+    /// - Returns: 按钮控件
+        func nx_addButton(rect: CGRect, image: UIImage, target: Any?, action: Selector?, event: UIControl.Event?) -> UIButton {
+            return nx_addButton(rect: rect,
+                             title: "",
+                             titleColor: .white,
+                             font: UIFont.systemFont(ofSize: 14),
+                             image: image,
+                             bgImg: nil,
+                             target: target,
+                             action: action,
+                             event: event)
+        }
+    
+    /// 添加tableView
+    /// - Parameters:
+    ///   - rect: 大小
+    ///   - delegate: delegate对象
+    ///   - dataSource: dataSource 对象
+    /// - Returns: 表视图
+    func nx_addTableView(rect: CGRect, delegate: UITableViewDelegate?,dataSource:UITableViewDataSource?) -> UITableView {
+          let tableView = UITableView(frame: rect)
+          tableView.delegate = delegate
+          tableView.dataSource = dataSource
+          backgroundColor = .white
+          tableView.tableFooterView = UIView()
+          if #available(iOS 11.0, *) {
+              tableView.contentInsetAdjustmentBehavior = .never
+          }
+          return tableView
+      }
+
+}
+ 

+ 77 - 0
BFCommonKit/Classes/BFCategorys/NXUIColor+Ext.swift

@@ -0,0 +1,77 @@
+//
+//  NXUIColor+Ext.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+
+import UIKit
+
+extension UIColor {
+    
+    // MARK: - hex (0x000000) -> UIColor
+    ///
+    /// - Parameter hex (0x000000)
+    /// - Returns: UIColor
+    class func hex(hex: Int) -> UIColor {
+        return UIColor.hex(hex: hex, alpha: 1.0)
+    }
+    ///
+    /// - Parameters:
+    /// - Returns: UIColor
+    class func hex(hex: Int, alpha: CGFloat) -> UIColor {
+        return UIColor(red: CGFloat((hex >> 16) & 0xFF)/255.0, green: CGFloat((hex >> 8) & 0xFF)/255.0, blue: CGFloat(hex & 0xFF)/255.0, alpha: alpha)
+    }
+ 
+    private class func colorComponent(hex: String, start: Int, length: Int) -> CGFloat {
+        let subString = hex.sliceString(start..<(start + length))
+        let fullHex = length == 2 ? subString : (subString + subString)
+        var val: CUnsignedInt = 0
+        Scanner(string: fullHex).scanHexInt32(&val)
+        return CGFloat(val) / 255.0
+    }
+    
+    var hex: String {
+        var color = self
+        if color.cgColor.numberOfComponents < 4 {
+            let components = color.cgColor.components
+            
+            color = UIColor(red: components![0], green: components![0], blue: components![0], alpha: components![1])
+        }
+        if color.cgColor.colorSpace?.model != CGColorSpaceModel.rgb {
+            return "#FFFFFF"
+        }
+        return String(format: "#%02X%02X%02X", Int(color.cgColor.components![0]*255.0), Int(color.cgColor.components![1]*255.0), Int(color.cgColor.components![2]*255.0))
+    }
+    
+    // MARK: - RGB -> UIColor
+    class func rgba(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) -> UIColor {
+        return UIColor(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: alpha)
+    }
+    // MARK: - RGBA -> UIColor
+    class func rgb(red: CGFloat, green: CGFloat, blue: CGFloat) -> UIColor {
+        return rgba(red: red, green: green, blue: blue, alpha: 1.0)
+    }
+    
+    var rgba: [Int] {
+        var red: CGFloat = 0
+        var green: CGFloat = 0
+        var blue: CGFloat = 0
+        var alpha: CGFloat = 0
+        self.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
+        return [Int(red*255.0), Int(green*255.0), Int(blue*255.0), Int(alpha)]
+    }
+    
+    class func randomColor() -> UIColor {
+        let red = CGFloat(arc4random()%255)
+        let green = CGFloat(arc4random()%255)
+        let blue = CGFloat(arc4random()%255)
+        let color = UIColor(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0)
+        return color
+    }
+    
+    
+    
+}
+

+ 32 - 0
BFCommonKit/Classes/BFCategorys/UIControl+NXCategory.h

@@ -0,0 +1,32 @@
+//
+//  UIControl+acceptEventInterval.h
+//  NXlib
+//
+//  Created by AK on 15/9/15.
+//  Copyright (c) 2015年 AK. All rights reserved.
+//
+
+// 本类的作用
+// 防止多次连续点击事件,加一个两次点击的时间间隔,专治各种测试人员-_-!
+// 使用 addTarget:action:forControlEvents 给对象添加事件的都可以加时间间隔 如
+// UIButton、UISwitch、UITextField
+
+/*
+ *	UIButton * testBtn;
+ *  testBtn.uxy_acceptEventInterval = 2.5;
+ */
+#import <UIKit/UIKit.h>
+
+@interface UIControl (NXCategory)
+
+/**
+ *  设置重复点击加间隔。
+ */
+@property(nonatomic, assign) NSTimeInterval uxy_acceptEventInterval;
+
+/**
+ *  忽略本次点击。
+ */
+@property(nonatomic) BOOL ignoreEvent;
+
+@end

+ 68 - 0
BFCommonKit/Classes/BFCategorys/UIControl+NXCategory.m

@@ -0,0 +1,68 @@
+//
+//  UIControl+acceptEventInterval.m
+//  NXlib
+//
+//  Created by AK on 15/9/15.
+//  Copyright (c) 2015年 AK. All rights reserved.
+//
+
+#import "UIControl+NXCategory.h"
+
+#if TARGET_OS_IPHONE
+#import <objc/message.h>
+#import <objc/runtime.h>
+#else
+#import <objc/objc-class.h>
+#endif
+
+@implementation UIControl (NXCategory)
+
+static const char *UIControl_acceptEventInterval = "UIControl_acceptEventInterval";
+static const char *UIControl_ignoreEvent = "UIControl_ignoreEvent";
+
+//改变两个方法的实现。在类第一次使用的时候回调用这个方法
++ (void)load
+{
+    Method a = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
+    Method b = class_getInstanceMethod(self, @selector(__uxy_sendAction:to:forEvent:));
+    //改变两个方法的实现
+    method_exchangeImplementations(a, b);  // isnt
+}
+//通过关联对象重写get和set方法
+- (NSTimeInterval)uxy_acceptEventInterval
+{
+    return [objc_getAssociatedObject(self, UIControl_acceptEventInterval) doubleValue];
+}
+
+- (void)setUxy_acceptEventInterval:(NSTimeInterval)uxy_acceptEventInterval
+{
+    objc_setAssociatedObject(self, UIControl_acceptEventInterval, @(uxy_acceptEventInterval),
+                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+#pragma mark 现在是否可点的get和set。通过关联对象。
+- (void)setIgnoreEvent:(BOOL)ignoreEvent
+{
+    objc_setAssociatedObject(self, UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+- (BOOL)ignoreEvent { return [objc_getAssociatedObject(self, UIControl_ignoreEvent) boolValue]; }
+- (void)__uxy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
+{
+    if (self.ignoreEvent)
+    {
+        NSLog(@"无效点击!!!!!!!!!!");
+        return;
+    }
+    if (self.uxy_acceptEventInterval > 0)
+    {
+        self.ignoreEvent = YES;
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.uxy_acceptEventInterval * NSEC_PER_SEC)),
+                       dispatch_get_main_queue(), ^{
+                           self.ignoreEvent = NO;
+                       });
+    }
+
+    //调用系统实现
+    [self __uxy_sendAction:action to:target forEvent:event];
+}
+
+@end

+ 0 - 0
BFCommonKit/Classes/BFCustomViews/.gitkeep


+ 0 - 0
BFCommonKit/Classes/BFCustomViews/controllers/.gitkeep


+ 0 - 0
BFCommonKit/Classes/BFCustomViews/views/.gitkeep


+ 114 - 0
BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/NXBadgeControl.swift

@@ -0,0 +1,114 @@
+//
+//  NXBadgeControl.swift
+//  NXFramework-Swift
+//
+//  Created by ak on 2020/11/11.
+//
+
+
+import UIKit
+
+
+open class NXBadgeControl: UIControl {
+    
+    /// 记录Badge的偏移量 Record the offset of Badge
+    public var offset: CGPoint = CGPoint(x: 0, y: 0)
+    
+    /// Badge伸缩的方向, Default is NXBadgeViewFlexModeTail
+    public var flexMode: NXBadgeViewFlexMode = .tail
+    
+    private lazy var textLabel: UILabel = UILabel()
+    
+    private lazy var imageView: UIImageView = UIImageView()
+    
+    private var badgeViewColor: UIColor?
+    private var badgeViewHeightConstraint: NSLayoutConstraint?
+    
+    public class func `default`() -> Self {
+        return self.init(frame: .zero)
+    }
+    
+    required override public init(frame: CGRect) {
+        super.init(frame: frame)
+        setupSubviews()
+    }
+    
+    required public init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    /// Set Text
+    open var text: String? {
+        didSet {
+            textLabel.text = text
+        }
+    }
+    
+    /// Set AttributedText
+    open var attributedText: NSAttributedString? {
+        didSet {
+            textLabel.attributedText = attributedText
+        }
+    }
+    
+    /// Set Font
+    open var font: UIFont? {
+        didSet {
+            textLabel.font = font
+        }
+    }
+    
+    /// Set background image
+    open var backgroundImage: UIImage? {
+        didSet {
+            imageView.image = backgroundImage
+            if let _ = backgroundImage {
+                if let constraint = heightConstraint() {
+                    badgeViewHeightConstraint = constraint
+                    removeConstraint(constraint)
+                }
+                backgroundColor = UIColor.clear
+            } else {
+                if heightConstraint() == nil, let constraint = badgeViewHeightConstraint {
+                    addConstraint(constraint)
+                }
+                backgroundColor = badgeViewColor
+            }
+        }
+    }
+    
+    open override var backgroundColor: UIColor? {
+        didSet {
+            super.backgroundColor = backgroundColor
+            if let color = backgroundColor, color != .clear {
+                badgeViewColor = backgroundColor
+            }
+        }
+    }
+    
+    private func setupSubviews() {
+        layer.masksToBounds = true
+        layer.cornerRadius = 9.0
+        translatesAutoresizingMaskIntoConstraints = false
+        backgroundColor = UIColor.red
+        textLabel.textColor = UIColor.white
+        textLabel.font = UIFont.systemFont(ofSize: 13)
+        textLabel.textAlignment = .center
+        addSubview(textLabel)
+        addSubview(imageView)
+        addLayout(with: imageView, leading: 0, trailing: 0)
+        addLayout(with: textLabel, leading: 5, trailing: -5)
+    }
+    
+    private func addLayout(with view: UIView, leading: CGFloat, trailing: CGFloat) {
+        view.translatesAutoresizingMaskIntoConstraints = false
+        let topConstraint = NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0)
+        let leadingConstraint = NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: leading)
+        let bottomConstraint = NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0)
+        let trailingConstraint = NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: trailing)
+        leadingConstraint.priority = UILayoutPriority(rawValue: 999)
+        trailingConstraint.priority = UILayoutPriority(rawValue: 999)
+        addConstraints([topConstraint, leadingConstraint, bottomConstraint, trailingConstraint])
+    }
+}

+ 28 - 0
BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/NXBadgeView.swift

@@ -0,0 +1,28 @@
+//
+//  NXBadgeView.swift.swift
+//  NXFramework-Swift
+//
+//  Created by ak on 2020/11/11.
+//
+
+
+import UIKit
+
+public struct NX<Base> {
+    public let base: Base
+    public init(_ base: Base) {
+        self.base = base
+    }
+}
+
+public extension NSObjectProtocol {
+    var nx: NX<Self> {
+        return NX(self)
+    }
+}
+
+public enum NXBadgeViewFlexMode {
+    case head    // 左伸缩 Head Flex    : <==●
+    case tail    // 右伸缩 Tail Flex    : ●==>
+    case middle  // 左右伸缩 Middle Flex : <=●=>
+}

+ 125 - 0
BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/UIBarButtonItem+NXBadgeView.swift

@@ -0,0 +1,125 @@
+//
+//  UIBarButtonItem+NXBadgeView.swift
+//  NXFramework-Swift
+//
+//  Created by ak on 2020/11/11.
+//
+
+import UIKit
+
+public extension NX where Base: UIBarButtonItem {
+    
+    public var badgeView: NXBadgeControl {
+        return _bottomView.nx.badgeView
+    }
+    
+    /// 添加带文本内容的Badge, 默认右上角, 红色, 18pts
+    ///
+    /// Add Badge with text content, the default upper right corner, red backgroundColor, 18pts
+    ///
+    /// - Parameter text: 文本字符串
+     public  func addBadge(text: String) {
+        _bottomView.nx.addBadge(text: text)
+    }
+    
+    /// 添加带数字的Badge, 默认右上角,红色,18pts
+    ///
+    /// Add the Badge with numbers, the default upper right corner, red backgroundColor, 18pts
+    ///
+    /// - Parameter number: 整形数字
+     public  func addBadge(number: Int) {
+        _bottomView.nx.addBadge(number: number)
+    }
+    
+    /// 添加带颜色的小圆点, 默认右上角, 红色, 8pts
+    ///
+    /// Add small dots with color, the default upper right corner, red backgroundColor, 8pts
+    ///
+    /// - Parameter color: 颜色
+     public  func addDot(color: UIColor?) {
+        _bottomView.nx.addDot(color: color)
+    }
+    
+    /// 设置Badge的偏移量, Badge中心点默认为其父视图的右上角
+    ///
+    /// Set Badge offset, Badge center point defaults to the top right corner of its parent view
+    ///
+    /// - Parameters:
+    ///   - x: X轴偏移量 (x<0: 左移, x>0: 右移) axis offset (x <0: left, x> 0: right)
+    ///   - y: Y轴偏移量 (y<0: 上移, y>0: 下移) axis offset (Y <0: up,   y> 0: down)
+     public  func moveBadge(x: CGFloat, y: CGFloat) {
+        _bottomView.nx.moveBadge(x: x, y: y)
+    }
+    
+    /// 设置Badge伸缩的方向
+    ///
+    /// Setting the direction of Badge expansion
+    ///
+    /// NXBadgeViewFlexModeHead,    左伸缩 Head Flex    : <==●
+    /// NXBadgeViewFlexModeTail,    右伸缩 Tail Flex    : ●==>
+    /// NXBadgeViewFlexModeMiddle   左右伸缩 Middle Flex : <=●=>
+    /// - Parameter flexMode : Default is PPBadgeViewFlexModeTail
+     public  func setBadge(flexMode: NXBadgeViewFlexMode = .tail) {
+        _bottomView.nx.setBadge(flexMode: flexMode)
+    }
+    
+    /// 设置Badge的高度,因为Badge宽度是动态可变的,通过改变Badge高度,其宽度也按比例变化,方便布局
+    ///
+    /// (注意: 此方法需要将Badge添加到控件上后再调用!!!)
+    ///
+    /// Set the height of Badge, because the Badge width is dynamically and  variable.By changing the Badge height in proportion to facilitate the layout.
+    ///
+    /// (Note: this method needs to add Badge to the controls and then use it !!!)
+    ///
+    /// - Parameter points: 高度大小
+     public  func setBadge(height: CGFloat) {
+        _bottomView.nx.setBadge(height: height)
+    }
+    
+    /// 显示Badge
+     public  func showBadge() {
+        _bottomView.nx.showBadge()
+    }
+    
+    /// 隐藏Badge
+     public  func hiddenBadge() {
+        _bottomView.nx.hiddenBadge()
+    }
+    
+    // MARK: - 数字增加/减少, 注意:以下方法只适用于Badge内容为纯数字的情况
+    // MARK: - Digital increase /decrease, note: the following method applies only to cases where the Badge content is purely numeric
+    /// badge数字加1
+     public  func increase() {
+        _bottomView.nx.increase()
+    }
+    
+    /// badge数字加number
+     public  func increaseBy(number: Int) {
+        _bottomView.nx.increaseBy(number: number)
+    }
+    
+    /// badge数字加1
+     public  func decrease() {
+        _bottomView.nx.decrease()
+    }
+    
+    /// badge数字减number
+     public  func decreaseBy(number: Int) {
+        _bottomView.nx.decreaseBy(number: number)
+    }
+
+    /// 通过Xcode视图调试工具找到UIBarButtonItem的Badge所在父视图为:UIImageView
+    private var _bottomView: UIView {
+        let navigationButton = (self.base.value(forKey: "_view") as? UIView) ?? UIView()
+        let systemVersion = (UIDevice.current.systemVersion as NSString).doubleValue
+        let controlName = (systemVersion < 11.0 ? "UIImageView" : "UIButton" )
+        for subView in navigationButton.subviews {
+            if subView.isKind(of: NSClassFromString(controlName)!) {
+                subView.layer.masksToBounds = false
+                return subView
+            }
+        }
+        return navigationButton
+    }
+}
+

+ 126 - 0
BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/UITabBarItem+NXBadgeView.swift

@@ -0,0 +1,126 @@
+
+//
+//  UITabBarItem+NXBadgeView.swift
+//  NXFramework-Swift
+//
+//  Created by ak on 2020/11/11.
+//
+
+import Foundation
+
+public extension NX where Base: UITabBarItem {
+    
+    var badgeView: NXBadgeControl {
+        return _bottomView.nx.badgeView
+    }
+    
+    /// 添加带文本内容的Badge, 默认右上角, 红色, 18pts
+    ///
+    /// Add Badge with text content, the default upper right corner, red backgroundColor, 18pts
+    ///
+    /// - Parameter text: 文本字符串
+    func addBadge(text: String) {
+        _bottomView.nx.addBadge(text: text)
+        _bottomView.nx.moveBadge(x: 4, y: 3)
+    }
+    
+    /// 添加带数字的Badge, 默认右上角,红色,18pts
+    ///
+    /// Add the Badge with numbers, the default upper right corner, red backgroundColor, 18pts
+    ///
+    /// - Parameter number: 整形数字
+    func addBadge(number: Int) {
+        _bottomView.nx.addBadge(number: number)
+        _bottomView.nx.moveBadge(x: 4, y: 3)
+    }
+    
+    /// 添加带颜色的小圆点, 默认右上角, 红色, 8pts
+    ///
+    /// Add small dots with color, the default upper right corner, red backgroundColor, 8pts
+    ///
+    /// - Parameter color: 颜色
+    func addDot(color: UIColor?) {
+        _bottomView.nx.addDot(color: color)
+    }
+    
+    /// 设置Badge的偏移量, Badge中心点默认为其父视图的右上角
+    ///
+    /// Set Badge offset, Badge center point defaults to the top right corner of its parent view
+    ///
+    /// - Parameters:
+    ///   - x: X轴偏移量 (x<0: 左移, x>0: 右移) axis offset (x <0: left, x> 0: right)
+    ///   - y: Y轴偏移量 (y<0: 上移, y>0: 下移) axis offset (Y <0: up,   y> 0: down)
+    func moveBadge(x: CGFloat, y: CGFloat) {
+        _bottomView.nx.moveBadge(x: x, y: y)
+    }
+    
+    /// 设置Badge伸缩的方向
+    ///
+    /// Setting the direction of Badge expansion
+    ///
+    /// PPBadgeViewFlexModeHead,    左伸缩 Head Flex    : <==●
+    /// PPBadgeViewFlexModeTail,    右伸缩 Tail Flex    : ●==>
+    /// PPBadgeViewFlexModeMiddle   左右伸缩 Middle Flex : <=●=>
+    /// - Parameter flexMode : Default is PPBadgeViewFlexModeTail
+    func setBadge(flexMode: NXBadgeViewFlexMode = .tail) {
+        _bottomView.nx.setBadge(flexMode: flexMode)
+    }
+    
+    /// 设置Badge的高度,因为Badge宽度是动态可变的,通过改变Badge高度,其宽度也按比例变化,方便布局
+    ///
+    /// (注意: 此方法需要将Badge添加到控件上后再调用!!!)
+    ///
+    /// Set the height of Badge, because the Badge width is dynamically and  variable.By changing the Badge height in proportion to facilitate the layout.
+    ///
+    /// (Note: this method needs to add Badge to the controls and then use it !!!)
+    ///
+    /// - Parameter height: 高度大小
+    func setBadge(height: CGFloat) {
+        _bottomView.nx.setBadge(height: height)
+    }
+    
+    
+    /// 显示Badge
+    func showBadge() {
+        _bottomView.nx.showBadge()
+    }
+    
+    /// 隐藏Badge
+    func hiddenBadge() {
+        _bottomView.nx.hiddenBadge()
+    }
+    
+    // MARK: - 数字增加/减少, 注意:以下方法只适用于Badge内容为纯数字的情况
+    // MARK: - Digital increase /decrease, note: the following method applies only to cases where the Badge content is purely numeric
+    /// badge数字加1
+    func increase() {
+        _bottomView.nx.increase()
+    }
+    
+    /// badge数字加number
+    func increaseBy(number: Int) {
+        _bottomView.nx.increaseBy(number: number)
+    }
+    
+    /// badge数字加1
+    func decrease() {
+        _bottomView.nx.decrease()
+    }
+    
+    /// badge数字减number
+    func decreaseBy(number: Int) {
+        _bottomView.nx.decreaseBy(number: number)
+    }
+    
+    /// 通过Xcode视图调试工具找到UITabBarItem原生Badge所在父视图
+    private var _bottomView: UIView {
+        let tabBarButton = (self.base.value(forKey: "_view") as? UIView) ?? UIView()
+        for subView in tabBarButton.subviews {
+            guard let superclass = subView.superclass else { return tabBarButton }
+            if superclass == NSClassFromString("UIImageView") {
+                return subView
+            }
+        }
+        return tabBarButton
+    }
+}

+ 260 - 0
BFCommonKit/Classes/BFCustomViews/views/NXBadgeView/UIView+NXBadgeView.swift

@@ -0,0 +1,260 @@
+//
+//  UIView+NXBadgeView.swift
+//  NXFramework-Swift
+//
+//  Created by ak on 2020/11/11.
+//
+
+import UIKit
+
+private var kBadgeView = "kNXBadgeView"
+
+// MARK: - add Badge
+public extension NX where Base: UIView {
+    
+    var badgeView: NXBadgeControl {
+        return base.badgeView
+    }
+    
+    /// 添加带文本内容的Badge, 默认右上角, 红色, 18pts
+    ///
+    /// Add Badge with text content, the default upper right corner, red backgroundColor, 18pts
+    ///
+    /// - Parameter text: 文本字符串
+    func addBadge(text: String?) {
+        showBadge()
+        base.badgeView.text = text
+        setBadge(flexMode: base.badgeView.flexMode)
+        if text == nil {
+            if base.badgeView.widthConstraint()?.relation == .equal { return }
+            base.badgeView.widthConstraint()?.isActive = false
+            let constraint = NSLayoutConstraint(item: base.badgeView, attribute: .width, relatedBy: .equal, toItem: base.badgeView, attribute: .height, multiplier: 1.0, constant: 0)
+            base.badgeView.addConstraint(constraint)
+        } else {
+            if base.badgeView.widthConstraint()?.relation == .greaterThanOrEqual { return }
+            base.badgeView.widthConstraint()?.isActive = false
+            let constraint = NSLayoutConstraint(item: base.badgeView, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: base.badgeView, attribute: .height, multiplier: 1.0, constant: 0)
+            base.badgeView.addConstraint(constraint)
+        }
+    }
+    
+    /// 添加带数字的Badge, 默认右上角,红色,18pts
+    ///
+    /// Add the Badge with numbers, the default upper right corner, red backgroundColor, 18pts
+    ///
+    /// - Parameter number: 整形数字
+    func addBadge(number: Int) {
+        if number <= 0 {
+            addBadge(text: "0")
+            hiddenBadge()
+            return
+        }
+        addBadge(text: "\(number)")
+    }
+    
+    /// 添加带颜色的小圆点, 默认右上角, 红色, 8pts
+    ///
+    /// Add small dots with color, the default upper right corner, red backgroundColor, 8pts
+    ///
+    /// - Parameter color: 颜色
+    func addDot(color: UIColor? = .red) {
+        addBadge(text: nil)
+        setBadge(height: 8.0)
+        base.badgeView.backgroundColor = color
+    }
+    
+    /// 设置Badge的偏移量, Badge中心点默认为其父视图的右上角
+    ///
+    /// Set Badge offset, Badge center point defaults to the top right corner of its parent view
+    ///
+    /// - Parameters:
+    ///   - x: X轴偏移量 (x<0: 左移, x>0: 右移) axis offset (x <0: left, x> 0: right)
+    ///   - y: Y轴偏移量 (y<0: 上移, y>0: 下移) axis offset (Y <0: up,   y> 0: down)
+    func moveBadge(x: CGFloat, y: CGFloat) {
+        base.badgeView.offset = CGPoint(x: x, y: y)
+        base.centerYConstraint(with: base.badgeView)?.constant = y
+        
+        let badgeHeight = base.badgeView.heightConstraint()?.constant ?? 0
+        switch base.badgeView.flexMode {
+        case .head:
+            base.centerXConstraint(with: base.badgeView)?.isActive = false
+            base.leadingConstraint(with: base.badgeView)?.isActive = false
+            if let constraint = base.trailingConstraint(with: base.badgeView) {
+                constraint.constant = badgeHeight * 0.5 + x
+                return
+            }
+            let trailingConstraint = NSLayoutConstraint(item: base.badgeView, attribute: .trailing, relatedBy: .equal, toItem: base, attribute: .trailing, multiplier: 1.0, constant: badgeHeight * 0.5 + x)
+            base.addConstraint(trailingConstraint)
+            
+        case .tail:
+            base.centerXConstraint(with: base.badgeView)?.isActive = false
+            base.trailingConstraint(with: base.badgeView)?.isActive = false
+            if let constraint = base.leadingConstraint(with: base.badgeView) {
+                constraint.constant = x - badgeHeight * 0.5
+                return
+            }
+            let leadingConstraint = NSLayoutConstraint(item: base.badgeView, attribute: .leading, relatedBy: .equal, toItem: base, attribute: .trailing, multiplier: 1.0, constant: x - badgeHeight * 0.5)
+            base.addConstraint(leadingConstraint)
+            
+        case .middle:
+            base.leadingConstraint(with: base.badgeView)?.isActive = false
+            base.trailingConstraint(with: base.badgeView)?.isActive = false
+            base.centerXConstraint(with: base.badgeView)?.constant = x
+            if let constraint = base.centerXConstraint(with: base.badgeView) {
+                constraint.constant = x
+                return
+            }
+            let centerXConstraint = NSLayoutConstraint(item: base.badgeView, attribute: .centerX, relatedBy: .equal, toItem: base, attribute: .centerX, multiplier: 1.0, constant: x)
+            base.addConstraint(centerXConstraint)
+        }
+    }
+    
+    /// 设置Badge伸缩的方向
+    ///
+    /// Setting the direction of Badge expansion
+    ///
+    /// NXBadgeViewFlexModeHead,    左伸缩 Head Flex    : <==●
+    /// NXBadgeViewFlexModeTail,    右伸缩 Tail Flex    : ●==>
+    /// NXBadgeViewFlexModeMiddle   左右伸缩 Middle Flex : <=●=>
+    /// - Parameter flexMode : Default is PPBadgeViewFlexModeTail
+    func setBadge(flexMode: NXBadgeViewFlexMode = .tail) {
+        base.badgeView.flexMode = flexMode
+        moveBadge(x: base.badgeView.offset.x, y: base.badgeView.offset.y)
+    }
+    
+    /// 设置Badge的高度,因为Badge宽度是动态可变的,通过改变Badge高度,其宽度也按比例变化,方便布局
+    ///
+    /// (注意: 此方法需要将Badge添加到控件上后再调用!!!)
+    ///
+    /// Set the height of Badge, because the Badge width is dynamically and  variable.By changing the Badge height in proportion to facilitate the layout.
+    ///
+    /// (Note: this method needs to add Badge to the controls and then use it !!!)
+    ///
+    /// - Parameter height: 高度大小
+    func setBadge(height: CGFloat) {
+        base.badgeView.layer.cornerRadius = height * 0.5
+        base.badgeView.heightConstraint()?.constant = height
+        moveBadge(x: base.badgeView.offset.x, y: base.badgeView.offset.y)
+    }
+    
+    /// 显示Badge
+    func showBadge() {
+        base.badgeView.isHidden = false
+    }
+    
+    /// 隐藏Badge
+    func hiddenBadge() {
+        base.badgeView.isHidden = true
+    }
+    
+    // MARK: - 数字增加/减少, 注意:以下方法只适用于Badge内容为纯数字的情况
+    // MARK: - Digital increase /decrease, note: the following method applies only to cases where the Badge content is purely numeric
+    /// badge数字加1
+    func increase() {
+        increaseBy(number: 1)
+    }
+    
+    /// badge数字加number
+    func increaseBy(number: Int) {
+        let label = base.badgeView
+        let result = (Int(label.text ?? "0") ?? 0) + number
+        if result > 0 {
+            showBadge()
+        }
+        label.text = "\(result)"
+    }
+    
+    /// badge数字加1
+    func decrease() {
+        decreaseBy(number: 1)
+    }
+    
+    /// badge数字减number
+    func decreaseBy(number: Int) {
+        let label = base.badgeView
+        let result = (Int(label.text ?? "0") ?? 0) - number
+        if (result <= 0) {
+            hiddenBadge()
+            label.text = "0"
+            return
+        }
+        label.text = "\(result)"
+    }
+}
+
+extension UIView {
+    
+      public  func addBadgeViewLayoutConstraint() {
+        badgeView.translatesAutoresizingMaskIntoConstraints = false
+        let centerXConstraint = NSLayoutConstraint(item: badgeView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: 0)
+        let centerYConstraint = NSLayoutConstraint(item: badgeView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0)
+        let widthConstraint = NSLayoutConstraint(item: badgeView, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: badgeView, attribute: .height, multiplier: 1.0, constant: 0)
+        let heightConstraint = NSLayoutConstraint(item: badgeView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 18)
+        addConstraints([centerXConstraint, centerYConstraint])
+        badgeView.addConstraints([widthConstraint, heightConstraint])
+    }
+}
+
+// MARK: - getter/setter
+extension UIView {
+
+    public var badgeView: NXBadgeControl {
+        get {
+            if let aValue = objc_getAssociatedObject(self, &kBadgeView) as? NXBadgeControl {
+                return aValue
+            }
+            else {
+                let badgeControl = NXBadgeControl.default()
+                self.addSubview(badgeControl)
+                self.bringSubviewToFront(badgeControl)
+                self.badgeView = badgeControl
+                self.addBadgeViewLayoutConstraint()
+                return badgeControl
+            }
+        }
+        set {
+            objc_setAssociatedObject(self, &kBadgeView, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+    
+       public  func topConstraint(with item: AnyObject?) -> NSLayoutConstraint? {
+        return constraint(with: item, attribute: .top)
+    }
+    
+       public  func leadingConstraint(with item: AnyObject?) -> NSLayoutConstraint? {
+        return constraint(with: item, attribute: .leading)
+    }
+    
+       public  func bottomConstraint(with item: AnyObject?) -> NSLayoutConstraint? {
+        return constraint(with: item, attribute: .bottom)
+    }
+
+       public  func trailingConstraint(with item: AnyObject?) -> NSLayoutConstraint? {
+        return constraint(with: item, attribute: .trailing)
+    }
+    
+       public  func widthConstraint() -> NSLayoutConstraint? {
+        return constraint(with: self, attribute: .width)
+    }
+    
+       public  func heightConstraint() -> NSLayoutConstraint? {
+        return constraint(with: self, attribute: .height)
+    }
+
+       public  func centerXConstraint(with item: AnyObject?) -> NSLayoutConstraint? {
+        return constraint(with: item, attribute: .centerX)
+    }
+    
+       public  func centerYConstraint(with item: AnyObject?) -> NSLayoutConstraint? {
+        return constraint(with: item, attribute: .centerY)
+    }
+    
+       public  func constraint(with item: AnyObject?, attribute: NSLayoutConstraint.Attribute) -> NSLayoutConstraint? {
+        for constraint in constraints {
+            if let isSame = constraint.firstItem?.isEqual(item), isSame, constraint.firstAttribute == attribute {
+                return constraint
+            }
+        }
+        return nil
+    }
+}

+ 166 - 0
BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXBubbleLayer.swift

@@ -0,0 +1,166 @@
+//
+//  NXBubbleLayer.swift
+//  bubbleLayer_swift
+//
+//  Created by liuming on 2020/8/23.
+//  Copyright © 2020 liuming. All rights reserved.
+//
+
+import UIKit
+// 箭头方向枚举
+public enum ArrowDirection: Int {
+    case right = 0 // 指向右边, 即在圆角矩形的右边
+    case bottom = 1 // 指向下边
+    case left = 2 // 指向左边
+    case top = 3 // 指向上边
+}
+
+class NXBubbleLayer: NSObject {
+    // 矩形的圆角的半径
+    var cornerRadius: CGFloat = 8
+    // 箭头位置的圆角半径
+    var arrowRadius: CGFloat = 3
+    // 箭头的高度
+    var arrowHeight: CGFloat = 12
+    // 箭头的宽度
+    var arrowWidth: CGFloat = 30
+    // 箭头方向
+    var arrowDirection: ArrowDirection = .bottom
+    // 箭头的相对位置
+    var arrowPosition: CGFloat = 0.5
+    // 这里的size是需要mask成气泡形状的view的size
+    public var size: CGSize = CGSize.zero
+
+    /// 气泡layer 在视图层的位置
+    public var bubbleLayerRect: CGRect = .zero
+
+    init(originalSize: CGSize) {
+        size = originalSize
+    }
+
+    // 最终拿这个layer去设置mask
+    func layer() -> CAShapeLayer {
+        let layer = CAShapeLayer()
+        layer.path = bubblePath()
+        return layer
+    }
+
+    // 绘制气泡形状,获取path
+    func bubblePath() -> CGPath? {
+        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
+        let ctx = UIGraphicsGetCurrentContext()
+
+        // 获取绘图所需要的关键点
+        let points = keyPoints()
+
+        // 第一步是要画箭头的“第一个支点”所在的那个角,所以要把“笔”放在这个支点顺时针顺序的上一个点
+        // 所以把“笔”放在最后才画的矩形框的角的位置, 准备开始画箭头
+        let currentPoint = points[6]
+        ctx?.move(to: currentPoint)
+
+        // 用于 CGContextAddArcToPoint函数的变量
+        var pointA = CGPoint.zero
+        var pointB = CGPoint.zero
+        var radius: CGFloat = 0
+        var count: Int = 0
+
+        while count < 7 {
+            // 整个过程需要画七个圆角(矩形框的四个角和箭头处的三个角),所以分为七个步骤
+
+            // 箭头处的三个圆角和矩形框的四个圆角不一样
+            radius = count < 3 ? arrowRadius : cornerRadius
+
+            pointA = points[count]
+            pointB = points[(count + 1) % 7]
+            // 画矩形框最后一个角的时候,pointB就是points[0]
+
+            ctx?.addArc(tangent1End: pointA, tangent2End: pointB, radius: radius)
+
+            count = count + 1
+        }
+
+        ctx?.closePath()
+        UIGraphicsEndImageContext()
+
+        return ctx?.path?.copy()
+    }
+
+    // 关键点: 绘制气泡形状前,需要计算箭头的三个点和矩形的四个角的点的坐标
+    func keyPoints() -> [CGPoint] {
+        // 先确定箭头的三个点
+        var beginPoint = CGPoint.zero // 按顺时针画箭头时的第一个支点,例如箭头向上时的左边的支点
+        var topPoint = CGPoint.zero // 顶点
+        var endPoint = CGPoint.zero // 另外一个支点
+
+        // 箭头顶点topPoint的X坐标(或Y坐标)的范围(用来计算arrowPosition)
+        let tpXRange = size.width - 2 * cornerRadius - arrowWidth
+        let tpYRange = size.height - 2 * cornerRadius - arrowWidth
+
+        // 用于表示矩形框的位置和大小
+        var rX: CGFloat = 0
+        var rY: CGFloat = 0
+        var rWidth = size.width
+        var rHeight = size.height
+
+        // 计算箭头的位置,以及调整矩形框的位置和大小
+        switch arrowDirection {
+        case .right: // 箭头在右时
+            topPoint = CGPoint(x: size.width, y: size.height / 2 + tpYRange * (arrowPosition - 0.5))
+            beginPoint = CGPoint(x: topPoint.x - arrowHeight, y: topPoint.y - arrowWidth / 2)
+            endPoint = CGPoint(x: beginPoint.x, y: beginPoint.y + arrowWidth)
+
+            rWidth = rWidth - arrowHeight // 矩形框右边的位置“腾出”给箭头
+
+        case .bottom: // 箭头在下时
+            topPoint = CGPoint(x: size.width / 2 + tpXRange * (arrowPosition - 0.5), y: size.height)
+            beginPoint = CGPoint(x: topPoint.x + arrowWidth / 2, y: topPoint.y - arrowHeight)
+            endPoint = CGPoint(x: beginPoint.x - arrowWidth, y: beginPoint.y)
+
+            rHeight = rHeight - arrowHeight
+
+        case .left: // 箭头在左时
+            topPoint = CGPoint(x: 0, y: size.height / 2 + tpYRange * (arrowPosition - 0.5))
+            beginPoint = CGPoint(x: topPoint.x + arrowHeight, y: topPoint.y + arrowWidth / 2)
+            endPoint = CGPoint(x: beginPoint.x, y: beginPoint.y - arrowWidth)
+
+            rX = arrowHeight
+            rWidth = rWidth - arrowHeight
+
+        case .top: // 箭头在上时
+            topPoint = CGPoint(x: size.width / 2 + tpXRange * (arrowPosition - 0.5), y: 0)
+            beginPoint = CGPoint(x: topPoint.x - arrowWidth / 2, y: topPoint.y + arrowHeight)
+            endPoint = CGPoint(x: beginPoint.x + arrowWidth, y: beginPoint.y)
+
+            rY = arrowHeight
+            rHeight = rHeight - arrowHeight
+
+        default:
+            ()
+        }
+        bubbleLayerRect = CGRect(x: rX, y: rY, width: rWidth, height: rHeight)
+        // 先把箭头的三个点放进关键点数组中
+        var points = [beginPoint, topPoint, endPoint]
+
+        // 确定圆角矩形的四个点
+        let bottomRight = CGPoint(x: rX + rWidth, y: rY + rHeight) // 右下角的点
+        let bottomLeft = CGPoint(x: rX, y: rY + rHeight)
+        let topLeft = CGPoint(x: rX, y: rY)
+        let topRight = CGPoint(x: rX + rWidth, y: rY)
+
+        // 先放在一个临时数组, 放置顺序跟下面紧接着的操作有关
+        let rectPoints = [bottomRight, bottomLeft, topLeft, topRight]
+
+        // 绘制气泡形状的时候,从箭头开始,顺时针地进行
+        // 箭头向右时,画完箭头之后会先画到矩形框的右下角
+        // 所以此时先把矩形框右下角的点放进关键点数组,其他三个点按顺时针方向添加
+        // 箭头在其他方向时,以此类推
+
+        var rectPointIndex: Int = arrowDirection.rawValue
+        for _ in 0...3 {
+            points.append(rectPoints[rectPointIndex])
+            rectPointIndex = (rectPointIndex + 1) % 4
+        }
+
+        return points
+    }
+}

+ 23 - 0
BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXContainView.swift

@@ -0,0 +1,23 @@
+//
+//  NXContainView.swift
+//  bubbleLayer_swift
+//
+//  Created by liuming on 2020/9/1.
+//  Copyright © 2020 liuming. All rights reserved.
+//
+
+import UIKit
+public class NXBubbleContainView: UIView {
+    override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+        var view = super.hitTest(point, with: event)
+        if view == nil {
+            subviews.forEach { subView in
+                let p = subView.convert(point, from: self)
+                if subView.bounds.contains(p) {
+                    view = subView
+                }
+            }
+        }
+        return view
+    }
+}

+ 73 - 0
BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXInteractiveView.swift

@@ -0,0 +1,73 @@
+//
+//  NXInteractiveView.swift
+//  bubbleLayer_swift
+//
+//  Created by liuming on 2020/8/23.
+//  Copyright © 2020 liuming. All rights reserved.
+//
+
+import UIKit
+public class NXInteractiveView: UIView {
+    public var tapGestureRecognizer: UITapGestureRecognizer?
+    public var longPressGestureRecognizer: UILongPressGestureRecognizer?
+    // 点击回调
+    public var tapGestureHander: (() -> Void)?
+    // 长按回调
+    public var longPressGestureHander: (() -> Void)?
+
+    override public init(frame: CGRect) {
+        super.init(frame: frame)
+        addTagGestureRecognizer()
+        addLongGestureRecogizer()
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // 单机方法
+    private func addTagGestureRecognizer() {
+        let tap = UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognizerHandler(sender:)))
+        tap.numberOfTouchesRequired = 1
+        tap.numberOfTapsRequired = 1
+        addGestureRecognizer(tap)
+        tapGestureRecognizer = tap
+    }
+
+    @objc
+    public func tapGestureRecognizerHandler(sender _: UITapGestureRecognizer) {
+        print("---- tapGestureRecognizerHandler -----")
+        if tapGestureHander != nil {
+            tapGestureHander!()
+        }
+    }
+
+    // 长按事件
+    private func addLongGestureRecogizer() {
+        let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longGestureRecognizerHandler(sender:)))
+        longPressGestureRecognizer.numberOfTapsRequired = 1
+        longPressGestureRecognizer.numberOfTouchesRequired = 1
+        addGestureRecognizer(longPressGestureRecognizer)
+        self.longPressGestureRecognizer = longPressGestureRecognizer
+    }
+
+    @objc
+    public func longGestureRecognizerHandler(sender _: UILongPressGestureRecognizer) {
+        print("---- longGestureRecognizerHandler -----")
+        if longPressGestureHander != nil {
+            longPressGestureHander!()
+        }
+    }
+
+    public func removeTapGestureRecognizer() {
+        if let tap = tapGestureRecognizer {
+            removeGestureRecognizer(tap)
+        }
+    }
+
+    public func removeLongPressGestureRecognizer() {
+        if let long = longPressGestureRecognizer {
+            removeGestureRecognizer(long)
+        }
+    }
+}

+ 80 - 0
BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXNormalBubbleView.swift

@@ -0,0 +1,80 @@
+//
+//  NXBaseBubbleView.swift
+//  bubbleLayer_swift
+//
+//  Created by liuming on 2020/8/23.
+//  Copyright © 2020 liuming. All rights reserved.
+//
+
+import Foundation
+import UIKit
+public class NXNormalBubbleView: NXInteractiveView {
+    let bubbleLayer = NXBubbleLayer(originalSize: .zero)
+    var currentLayer: CALayer?
+    /// 内部内容控件
+    public var containView = NXBubbleContainView(frame: .zero)
+
+    // 矩形的圆角的半径
+    public var cornerRadius: CGFloat = 8
+    // 箭头位置的圆角半径
+    public var arrowRadius: CGFloat = 3
+    // 箭头的高度
+    public var arrowHeight: CGFloat = 12
+    // 箭头的宽度
+    public var arrowWidth: CGFloat = 30
+    // 箭头方向
+    public var arrowDirection: ArrowDirection = .bottom
+    // 箭头的相对位置
+    public var arrowPosition: CGFloat = 0.5
+
+    public var bubbleColor: UIColor = .white {
+        didSet {
+            self.setNeedsLayout()
+            self.layoutIfNeeded()
+        }
+    }
+
+    override public init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(containView)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override public func layoutSubviews() {
+        super.layoutSubviews()
+        if let layer = currentLayer {
+            layer.removeFromSuperlayer()
+        }
+        bubbleLayer.size = frame.size
+        bubbleLayer.cornerRadius = cornerRadius
+        bubbleLayer.arrowRadius = arrowRadius
+        bubbleLayer.arrowHeight = arrowHeight
+        bubbleLayer.arrowWidth = arrowWidth
+        bubbleLayer.arrowDirection = arrowDirection
+        bubbleLayer.arrowPosition = arrowPosition
+        let layer = bubbleLayer.layer()
+        layer.fillColor = bubbleColor.cgColor
+        self.layer.insertSublayer(layer, at: 0)
+        currentLayer = layer
+        // 调整 contain坐标
+        containView.frame = bubbleLayer.bubbleLayerRect
+
+        backgroundColor = .clear
+        containView.backgroundColor = .clear
+    }
+
+    // MARK: - 重写交互层的长按和点击事件
+
+    override public func tapGestureRecognizerHandler(sender: UITapGestureRecognizer) {
+        super.tapGestureRecognizerHandler(sender: sender)
+        print("点击了普通气泡")
+    }
+
+    override public func longGestureRecognizerHandler(sender: UILongPressGestureRecognizer) {
+        super.longGestureRecognizerHandler(sender: sender)
+        print("长按了 点击了普通气泡")
+    }
+}

+ 40 - 0
BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXTextBubbleView.swift

@@ -0,0 +1,40 @@
+//
+//  NXTextBubbleView.swift
+//  bubbleLayer_swift
+//
+//  Created by liuming on 2020/8/23.
+//  Copyright © 2020 liuming. All rights reserved.
+//
+
+import SnapKit
+import UIKit
+
+class NXTextBubbleView: NXNormalBubbleView {
+    public let textLabel = UILabel(frame: .zero)
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        initSubViews()
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    private func initSubViews() {
+        containView.addSubview(textLabel)
+        textLabel.snp.makeConstraints { make in
+            make.edges.equalTo(UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5))
+        }
+    }
+
+    // MARK: - 重写交互层的长按和点击事件
+
+    override public func tapGestureRecognizerHandler(sender _: UITapGestureRecognizer) {
+        print("点击了文字气泡")
+    }
+
+    override public func longGestureRecognizerHandler(sender _: UILongPressGestureRecognizer) {
+        print("长按了 点击了文字气泡")
+    }
+}

+ 184 - 0
BFCommonKit/Classes/BFCustomViews/views/bubbleLayer/NXVoiceBubbleView.swift

@@ -0,0 +1,184 @@
+//
+//  NXVoiceBubbleView.swift
+//  bubbleLayer_swift
+//
+//  Created by liuming on 2020/8/23.
+//  Copyright © 2020 liuming. All rights reserved.
+//
+
+import UIKit
+public class NXVoiceBubbleView: NXNormalBubbleView {
+    /// 关闭按钮
+    private let closeBtn = UIButton(type: .custom)
+    /// 声音图片
+    private let voiceImgView = UIImageView(frame: .zero)
+
+    /// 语音时间文本
+    public let durationLabel = UILabel(frame: .zero)
+    /// 语音图片序列帧动画时间
+    public var animationDuration: TimeInterval = 0.5 {
+        didSet {
+            self.initVoiceAnimation()
+        }
+    }
+
+    // 加载圈
+    lazy var activityIndicator: UIActivityIndicatorView = {
+        let activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style:
+            .gray)
+        return activityIndicator
+    }()
+
+    /// 语音图片帧图片
+    public var animationImages: [UIImage] = Array() {
+        didSet {
+            initVoiceAnimation()
+        }
+    }
+
+    /// 语音图片动画重复次数
+    public var animationRepeatCount: Int = 1 {
+        didSet {
+            initVoiceAnimation()
+        }
+    }
+
+    /// 音频显示的总时间
+    public var duration: Float64 = 0 {
+        didSet {
+            showTime()
+            activityIndicatorStop()
+        }
+    }
+
+    public var closeBtnClickedHander: (() -> Void)?
+    override public init(frame: CGRect) {
+        super.init(frame: frame)
+        initSubviews()
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    private func initSubviews() {
+        containView.addSubview(voiceImgView)
+        voiceImgView.image = UIImage(named: "icon_voice")
+
+        containView.addSubview(durationLabel)
+
+        closeBtn.setImage(UIImage(named: "videomk_serach_clear"), for: .normal)
+        closeBtn.setImage(UIImage(named: "videomk_serach_clear"), for: .highlighted)
+        closeBtn.addTarget(self, action: #selector(closeBtnClicked(sender:)), for: .touchUpInside)
+        containView.addSubview(closeBtn)
+
+        containView.addSubview(activityIndicator)
+
+        // 布局
+        voiceImgView.snp.makeConstraints { maker in
+            maker.centerY.equalTo(self.containView)
+            maker.left.equalTo(self.containView).offset(10)
+            maker.width.height.equalTo(20)
+        }
+        durationLabel.snp.makeConstraints { maker in
+            maker.right.equalTo(self.containView).offset(-10)
+            maker.centerY.equalTo(self.containView)
+            maker.height.equalTo(20)
+        }
+        closeBtn.snp.makeConstraints { maker in
+
+            maker.right.equalTo(self.containView).offset(10)
+            maker.top.equalTo(self.containView).offset(-8)
+            maker.width.height.equalTo(20)
+        }
+
+        activityIndicator.snp.makeConstraints { maker in
+
+            maker.right.equalTo(self.containView).offset(-10)
+            maker.centerY.equalTo(self.containView)
+            maker.height.equalTo(20)
+        }
+    }
+
+    private func showTime() {
+        durationLabel.text = duration < 1 ? "1'" : duration.formatDurationToMS()
+    }
+
+    private func initVoiceAnimation() {
+        if animationImages.count > 0 {
+            voiceImgView.animationImages = animationImages
+            voiceImgView.animationDuration = animationDuration
+            voiceImgView.animationRepeatCount = animationRepeatCount
+        }
+    }
+
+    /// 加载圈开始动画
+    public func activityIndicatorStart() {
+        durationLabel.text = ""
+        activityIndicator.startAnimating()
+    }
+
+    /// 加载圈结束动画
+    public func activityIndicatorStop() {
+        activityIndicator.stopAnimating()
+    }
+
+    /// 开始动画
+    public func startAnimation() {
+        voiceImgView.startAnimating()
+    }
+
+    /// 结束动画
+    public func stopAnimation() {
+        voiceImgView.stopAnimating()
+    }
+
+    /// 按照中心点抖动
+    func animation() {
+        let animati = CAKeyframeAnimation(keyPath: "transform.rotation")
+        // rotation 旋转,需要添加弧度值
+        // 角度转弧度
+        animati.values = [angle2Radion(angle: -50), angle2Radion(angle: 50), angle2Radion(angle: -50)]
+        animati.repeatCount = 4
+        layer.add(animati, forKey: nil)
+    }
+
+    // MARK: - 重写交互层的长按和点击事件
+
+    override public func tapGestureRecognizerHandler(sender: UITapGestureRecognizer) {
+        super.tapGestureRecognizerHandler(sender: sender)
+        startAnimation()
+        print("点击了语音气泡")
+    }
+
+    override public func longGestureRecognizerHandler(sender: UILongPressGestureRecognizer) {
+        super.longGestureRecognizerHandler(sender: sender)
+        print("长按了 点击了语音气泡")
+        animation()
+    }
+
+    func angle2Radion(angle: Float) -> Float {
+        return angle / Float(180.0 * Double.pi)
+    }
+
+    // MARK: 关闭按钮点击事件
+
+    @objc
+    func closeBtnClicked(sender _: UIButton) {
+        print("点击了关闭按钮")
+        if let block = closeBtnClickedHander {
+            block()
+        }
+    }
+
+    override public func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
+        if bounds.contains(point) {
+            return true
+        }
+        let p = convert(point, to: closeBtn)
+        if closeBtn.bounds.contains(p) {
+            return true
+        }
+        return false
+    }
+}

+ 0 - 0
BFCommonKit/Classes/BFDBManager/.gitkeep


+ 0 - 0
BFCommonKit/Classes/BFDebug/.gitkeep


+ 174 - 0
BFCommonKit/Classes/BFDebug/NXLogger.swift

@@ -0,0 +1,174 @@
+//
+//  NXLogger.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+
+import UIKit
+
+
+public enum NXLoggerLevel: Int {
+    case info = 1
+    case debug = 2
+    case warning = 3
+    case error = 4
+    case none = 5
+    
+    var name: String {
+        switch self {
+            case .info: return "i"
+            case .debug: return "d"
+            case .warning: return "w"
+            case .error: return "e"
+            case .none: return "N"
+        }
+    }
+}
+
+public enum LoggerOutput: String {
+    case debuggerConsole
+    case deviceConsole
+    case fileOnly
+    case debugerConsoleAndFile
+    case deviceConsoleAndFile
+}
+
+
+private let fileExtension = "txt"
+private let LOG_BUFFER_SIZE = 10
+
+public class NXLogger: NSObject {
+
+    // MARK: - Output
+    public var tag: String?
+    public var level: NXLoggerLevel = .none
+    public var ouput: LoggerOutput = .debuggerConsole
+    public var showThread: Bool = false
+    
+    // MARK: - Init
+    private let isolationQueue = DispatchQueue(label: "com.nxframework.isolation", qos: .background, attributes: .concurrent)
+    private let serialQueue = DispatchQueue(label: "com.nxframework.swiftylog")
+    private let logSubdiretory = NXFileManager.documentDirectoryURL.appendingPathComponent(fileExtension)
+
+    public static let shared = NXLogger()
+    
+    private var _data: [String] = []
+    private var data: [String] {
+        get { return isolationQueue.sync { return _data } }
+        set { isolationQueue.async(flags: .barrier) { self._data = newValue } }
+    }
+    
+    private var logUrl: URL? {
+        let fileName = "NSFrameworkSwiftyLog"
+        try? FileManager.default.createDirectory(at: logSubdiretory, withIntermediateDirectories: false)
+        let url = logSubdiretory.appendingPathComponent(fileName).appendingPathExtension(fileExtension)
+        return url
+    }
+    
+    private override init() {
+        super.init()
+     
+        NotificationCenter.default.addObserver(self, selector: #selector(appMovedToBackground), name:   UIApplication.willResignActiveNotification, object: nil)
+   
+    }
+    
+    // MARK: - Methods
+    @objc private func appMovedToBackground() {
+         self.saveAsync()
+    }
+    
+    func saveAsync() {
+        guard let url = logUrl else { return }
+        serialQueue.async { [weak self] in
+            guard let count = self?.data.count, count > 0 else { return }
+
+            var stringsData = Data()
+            
+            self?.data.forEach { (string) in
+                if let stringData = (string + "\n").data(using: String.Encoding.utf8) {
+                    stringsData.append(stringData)
+                } else {
+                    print("MutalbeData failed")
+                }
+            }
+
+            do {
+                try stringsData.append2File(fileURL: url)
+                self?.data.removeAll()
+            } catch let error as NSError {
+                print("wrote failed: \(url.absoluteString), \(error.localizedDescription)")
+            }
+        }
+    }
+    
+    func removeAllAsync() {
+        guard let url = logUrl else { return }
+        DispatchQueue.global(qos: .userInitiated).async {
+            try? FileManager.default.removeItem(at: url)
+        }
+    }
+    
+    func load() -> [String]? {
+        guard let url = logUrl else { return nil }
+        guard let strings = try? String(contentsOf: url, encoding: String.Encoding.utf8) else { return nil }
+
+        return strings.components(separatedBy: "\n")
+    }
+
+    private func log(_ level: NXLoggerLevel, message: String, currentTime: Date, fileName: String , functionName: String, lineNumber: Int, thread: Thread) {
+        
+        guard level.rawValue >= self.level.rawValue else { return }
+        
+        
+        let _fileName = fileName.split(separator: "/")
+        let text = "\(level.name)-\(showThread ? thread.description : "")[\(_fileName.last ?? "?")#\(functionName)#\(lineNumber)]\(tag ?? ""): \(message)"
+        
+        switch self.ouput {
+            case .fileOnly:
+                addToBuffer(text: "\(currentTime.iso8601) \(text)")
+            case .debuggerConsole:
+                print("\(currentTime.iso8601) \(text)")
+            case .deviceConsole:
+                NSLog(text)
+            case .debugerConsoleAndFile:
+                print("\(currentTime.iso8601) \(text)")
+                addToBuffer(text: "\(currentTime.iso8601) \(text)")
+            case .deviceConsoleAndFile:
+                NSLog(text)
+                addToBuffer(text: "\(currentTime.iso8601) \(text)")
+        }
+    }
+    
+    private func addToBuffer(text: String) {
+        isolationQueue.async(flags: .barrier) { self._data.append(text) }
+        if data.count > LOG_BUFFER_SIZE {
+            saveAsync()
+        }
+    }
+    
+}
+
+// MARK: - Output
+extension NXLogger {
+    public func i(_ message: String, currentTime: Date = Date(), fileName: String = #file, functionName: String = #function, lineNumber: Int = #line, thread: Thread = Thread.current ) {
+        log(.info, message: message, currentTime: currentTime, fileName: fileName, functionName: functionName, lineNumber: lineNumber, thread: thread)
+    }
+    public func d(_ message: String, currentTime: Date = Date(), fileName: String = #file, functionName: String = #function, lineNumber: Int = #line, thread: Thread = Thread.current ) {
+        log(.debug, message: message, currentTime: currentTime, fileName: fileName, functionName: functionName, lineNumber: lineNumber, thread: thread)
+    }
+    public func w(_ message: String, currentTime: Date = Date(), fileName: String = #file, functionName: String = #function, lineNumber: Int = #line, thread: Thread = Thread.current ) {
+        log(.warning, message: message, currentTime: currentTime, fileName: fileName, functionName: functionName, lineNumber: lineNumber, thread: thread)
+    }
+    public func e(_ message: String, currentTime: Date = Date(), fileName: String = #file, functionName: String = #function, lineNumber: Int = #line, thread: Thread = Thread.current ) {
+        log(.error, message: message, currentTime: currentTime, fileName: fileName, functionName: functionName, lineNumber: lineNumber, thread: thread)
+    }
+    
+    public func synchronize() {
+        saveAsync()
+    }
+}
+
+
+

+ 68 - 0
BFCommonKit/Classes/BFDebug/NXLoggerManager.swift

@@ -0,0 +1,68 @@
+//
+//  NXLoggerManager.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+
+import UIKit
+
+extension UIWindow {
+    open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
+        guard NXLogger.shared.level != .none else { return }
+        guard NXLogger.shared.ouput == .debugerConsoleAndFile
+            || NXLogger.shared.ouput == .deviceConsoleAndFile
+            || NXLogger.shared.ouput == .fileOnly else { return }
+        
+        NXLogger.shared.saveAsync()
+        let manager = LoggerManager()
+        manager.show()
+    }
+}
+
+protocol LoggerAction {
+    func removeAll()
+}
+
+class LoggerManager: NSObject {
+    let controller = NXLoggerVC()
+    public func show() {
+        guard let topViewController = UIApplication.topViewController() else { return }
+        guard topViewController .isKind(of: NXLoggerVC.self) == false else { return }
+        
+        controller.data = " \(loadLog())\(deviceInfo())"
+        controller.delegate = self
+        
+        topViewController.present(controller, animated: true, completion: nil)
+    }
+    
+    private func loadLog() -> String {
+        var texts: [String] = []
+        
+        guard let data = NXLogger.shared.load() else { return "" }
+        
+        data.forEach { (string) in
+            texts.append("<pre style=\"line-height:8px;\">\(string)</pre>")
+        }
+        
+        return texts.joined()
+    }
+    
+    private func deviceInfo() -> String {
+        var texts:[String] = []
+        
+        texts.append("<pre style=\"line-height:8px;\">==============================================</pre>")
+        NXDeviceManager.info().forEach { (string) in
+            texts.append("<pre style=\"line-height:8px;\">\(string)</pre>")
+        }
+        return texts.joined()
+    }
+}
+
+extension LoggerManager: LoggerAction {
+    func removeAll() {
+        NXLogger.shared.removeAllAsync()
+        controller.data = deviceInfo()
+    }
+}

+ 188 - 0
BFCommonKit/Classes/BFDebug/NXLoggerVC.swift

@@ -0,0 +1,188 @@
+//
+//  NXLoggerVC.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+ 
+import UIKit
+import MessageUI
+import WebKit
+
+
+private let screenWidth = UIScreen.main.bounds.width
+private let screenHeight = UIScreen.main.bounds.height
+private let keyWindow = UIApplication.shared.keyWindow
+
+let themeColor: UIColor = UIColor.hex(hex: 0x00B3C4)
+
+class NXLoggerVC: UIViewController {
+
+    var delegate: LoggerAction?
+    
+    var data: String = "" {
+        didSet {
+            loadWebView()
+        }
+    }
+
+    var webView: WKWebView = {
+        
+        let source: String = "var meta = document.createElement('meta');" +
+            "meta.name = 'viewport';" +
+            "meta.content = 'width=device-width, initial-scale=0.6, maximum-scale=0.8, user-scalable=yes';" +
+            "var head = document.getElementsByTagName('head')[0];" + "head.appendChild(meta);";
+        let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
+        let userContentController: WKUserContentController = WKUserContentController()
+        let conf = WKWebViewConfiguration()
+        conf.userContentController = userContentController
+        userContentController.addUserScript(script)
+        let view = WKWebView(frame: CGRect.zero, configuration: conf)
+        
+        /*
+        view.scrollView.isScrollEnabled = true               // Make sure our view is interactable
+        view.scrollView.bounces = true                    // Things like this should be handled in web code
+        view.allowsBackForwardNavigationGestures = false   // Disable swiping to navigate
+         */
+        return view
+    }()
+    
+    var textView: UITextView = {
+        let view = UITextView()
+        view.isEditable = false
+        view.backgroundColor = UIColor.lightGray
+        return view
+    }()
+    
+    var btnSend: UIButton = {
+        let button = UIButton(type: .system)
+        button.backgroundColor = themeColor
+        button.setTitleColor(.white, for: .normal)
+        button.roundedCorners(cornerRadius: 5)
+        button.setTitle("Send email", for: .normal)
+        button.addTarget(self, action: #selector(btnSendPressed(_:)), for: .touchUpInside)
+        
+        return button
+    }()
+    
+    var btnRemove: UIButton = {
+        let button = UIButton(type: .system)
+        button.backgroundColor = themeColor
+        button.setTitleColor(.white, for: .normal)
+        button.roundedCorners(cornerRadius: 5)
+        button.setTitle("Remove All", for: .normal)
+        button.addTarget(self, action: #selector(btnRemovePressed(_:)), for: .touchUpInside)
+        return button
+    }()
+    var btnCancel: UIButton = {
+        let button = UIButton(type: .system)
+        button.backgroundColor = themeColor
+        button.setTitleColor(.white, for: .normal)
+        button.roundedCorners(cornerRadius: 5)
+        button.setTitle("Cancel", for: .normal)
+        button.addTarget(self, action: #selector(btnCancelPressed(_:)), for: .touchUpInside)
+        return button
+    }()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        addSubViews()
+        
+        loadWebView()
+
+    }
+    
+    private func addSubViews() {
+        self.view.backgroundColor = UIColor.white
+        
+        [webView, btnSend, btnRemove, btnCancel].forEach { (subView: UIView) in
+            subView.translatesAutoresizingMaskIntoConstraints = false
+            view.addSubview(subView)
+        }
+        
+        let views: [String:UIView] = ["webView": webView, "btnSend": btnSend, "btnRemove": btnRemove, "btnCancel": btnCancel]
+        
+        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|[webView]|", options: [], metrics: nil, views: views))
+        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-(16)-[btnSend]-(16)-|", options: [], metrics: nil, views: views))
+        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-(16)-[btnRemove]-(16)-|", options: [], metrics: nil, views: views))
+        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-(16)-[btnCancel]-(16)-|", options: [], metrics: nil, views: views))
+        view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-(20)-[webView]-[btnSend(==32)]-[btnRemove(==32)]-[btnCancel(==32)]-(8)-|", options: [], metrics: nil, views: views))
+    }
+    
+    @objc func btnCancelPressed(_ button: UIButton) {
+        self.dismiss(animated: true, completion: nil)
+    }
+    
+    @objc func btnSendPressed(_ button: UIButton) {
+        sendEmail()
+    }
+    
+    @objc func btnRemovePressed(_ button: UIButton) {
+        delegate?.removeAll()
+    }
+    
+    private func sendEmail() {
+        guard MFMailComposeViewController.canSendMail() == true else {
+            self.showAlert(withTitle: "No email client", message: "Please configure your email client first")
+            return
+        }
+
+        let mailComposer = MFMailComposeViewController()
+        mailComposer.mailComposeDelegate = self as! MFMailComposeViewControllerDelegate
+        
+        var body = "Host App: \(Bundle.main.bundleIdentifier ?? "")\n"
+        if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
+            let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
+            body += "Host App Version: \(version).\(buildNumber)\n"
+        }
+        if let venderId = UIDevice.current.identifierForVendor {
+            body += "identifierForVendor: \(venderId)\n"
+        }
+
+        mailComposer.setSubject("Log of \(Bundle.main.bundleIdentifier ?? "")")
+        mailComposer.setMessageBody(body, isHTML: false)
+
+
+        webView.evaluateJavaScript("document.documentElement.outerHTML.toString()") { (html, error) in
+            if let string = html as? String, let data = string.data(using: String.Encoding.utf16) {
+                
+                mailComposer.addAttachmentData(data, mimeType: "html", fileName: "\(Bundle.main.bundleIdentifier ?? "log").html" )
+            } else {
+                NXLogger.shared.e("get data from webview failed")
+            }
+        }
+        /*
+        if let data = try? Data(html) {
+            mailComposer.addAttachmentData(data, mimeType: "text/txt", fileName: "SwiftyLog.txt")
+        }
+        */
+        self.present(mailComposer, animated: true, completion: nil)
+    }
+    
+    private func loadWebView() {
+        webView.loadHTMLString(data, baseURL: nil)
+    }
+}
+    
+extension NXLoggerVC: MFMailComposeViewControllerDelegate {
+    func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
+        controller.dismiss(animated: true, completion: nil)
+        switch result {
+            case .cancelled:
+                self.showAlert(withTitle: "Cancel", message: "Send email canceled")
+                break
+            case .sent:
+                break
+            case .failed:
+                self.showAlert(withTitle: "Failed", message: "Send email failed")
+                break
+            case .saved:
+                break
+        @unknown default:
+            fatalError("default ")
+        }
+        self.dismiss(animated: true, completion: nil)
+    }
+}

+ 0 - 0
BFCommonKit/Classes/BFMacro/.gitkeep


+ 13 - 0
BFCommonKit/Classes/BFMacro/NXConfig.swift

@@ -0,0 +1,13 @@
+//
+//  NXConfig.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+
+import UIKit
+
+class NXConfig: NSObject {
+
+}

+ 0 - 0
BFCommonKit/Classes/BFNetworkManger/.gitkeep


+ 0 - 0
BFCommonKit/Classes/BFUtility/.gitkeep


+ 159 - 0
BFCommonKit/Classes/BFUtility/NXAudioRecorder.swift

@@ -0,0 +1,159 @@
+//
+import AVFoundation
+import Foundation
+//  NXAudioRecorder.swift
+//  PQSpeed
+//
+//  Created by ak on 2021/1/23.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//  本类功能:录制声音,并转换成 MP3
+//  alse see https://www.jianshu.com/p/971fff236881
+import UIKit
+
+// 录制时长
+public typealias  RecorderProgross = (_ time: Float64) -> Void
+
+public class NXAudioRecorder {
+    public let recorder: AVAudioRecorder
+
+    public var finishClosure: ((_ isSuccess: Bool, _ url: String) -> Void)? {
+        return delegateHandler.finishClosure
+    }
+
+    /// 由于AVAudioRecorderDelegate继承NSObjectProtocol 所以引入这个类处理代理避免污染主类
+    public  var delegateHandler = EditAudioRecorderDelegateHandler()
+
+    public  var recorderProgross: RecorderProgross?
+    public  var session: AVAudioSession!
+    public  var recordFilePath: String!
+
+    public  var displayLink: CADisplayLink?
+
+    /// 初始化录音器
+    /// - Parameter path: 保存的文件全路径,注意文件后缀一定要是 caf
+    /// - Throws: description
+    public init(path: String) throws {
+        // 1,判断目录文件夹是否存在
+        recordFilePath = path
+        BFLog(message: "recorder file path is \(String(describing: recordFilePath))")
+
+        // 2,参数
+        let fileURL = URL(fileURLWithPath: recordFilePath)
+        // 注意设置参数 设置不对就无法录制
+        let settings: [String: Any] = [
+            AVFormatIDKey: kAudioFormatLinearPCM,
+            AVSampleRateKey: 16000.0,
+            AVNumberOfChannelsKey: 1,
+            AVEncoderBitDepthHintKey: 16,
+//            AVLinearPCMIsFloatKey:true, // 不要打开ios 13有杂音
+            AVEncoderAudioQualityKey: AVAudioQuality.max.rawValue, // 录音质量
+        ]
+        recorder = try AVAudioRecorder(url: fileURL, settings: settings)
+        recorder.isMeteringEnabled = true
+        recorder.delegate = delegateHandler
+        recorder.prepareToRecord()
+    }
+
+    /// 开始录制
+    public func startRecord() {
+        if recorder.isRecording {
+            BFLog(message: "正在录制中。。")
+            return
+        }
+
+        startTimer()
+
+        if AVAudioSession.sharedInstance().category != .playAndRecord {
+            do {
+                try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: .defaultToSpeaker)
+                try AVAudioSession.sharedInstance().setActive(true)
+            } catch {
+                BFLog(message: error)
+            }
+        }
+
+        session = AVAudioSession.sharedInstance()
+        session.requestRecordPermission { granted in
+            if granted {
+                DispatchQueue.global().async {
+                    DispatchQueue.main.async {}
+                }
+            } else {}
+        }
+
+        recorder.record()
+    }
+
+    // 暂停录制
+    public func pauseRecord() {
+        recorder.pause()
+    }
+
+    // 停止录制
+    public func stopRecord(_ closure: @escaping (_ isSuccess: Bool, _ url: String) -> Void) {
+        if !recorder.isRecording {
+            BFLog(message: "不是录制状态")
+        }
+
+        stopTimer()
+
+        delegateHandler.finishClosure = closure
+
+        recorder.stop()
+    }
+
+    @objc func displayLinkClick(_: CADisplayLink) {
+        recorder.updateMeters()
+
+        BFLog(message: "当前录制时间长 \(String(describing: recorder.currentTime)) 波值:\(String(describing: recorder.averagePower(forChannel: 0)))")
+        if recorderProgross != nil {
+            recorderProgross!(recorder.currentTime)
+        }
+    }
+
+    // 开始计时
+    public func startTimer() {
+        if displayLink == nil {
+            // 创建对象
+            displayLink = CADisplayLink(target: self, selector: #selector(displayLinkClick(_:)))
+            // 设置触发频率 这个周期可以通过frameInterval属性设置,CADisplayLink的selector每秒调用次数=60/frameInterval。比如当frameInterval设为2,每秒调用就变成30次
+//            if #available(iOS 10.0, *) {
+//                displayLink?.preferredFramesPerSecond = 1
+//            } else {
+            displayLink?.frameInterval = 1
+//            }
+            // 加入循环
+            displayLink?.add(to: RunLoop.main, forMode: RunLoop.Mode.default)
+        }
+    }
+
+    // 停止计时
+    public  func stopTimer() {
+        if displayLink != nil {
+            displayLink?.isPaused = true
+            // 将定时器移除主循环
+            displayLink?.remove(from: RunLoop.main, forMode: RunLoop.Mode.default)
+            // 停止定时器
+            displayLink?.invalidate()
+            displayLink = nil
+        }
+    }
+}
+
+public class EditAudioRecorderDelegateHandler: NSObject {
+    public var finishClosure: ((_ flag: Bool, _ url: String) -> Void)?
+}
+
+extension EditAudioRecorderDelegateHandler: AVAudioRecorderDelegate {
+    public  func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
+        BFLog(message: "完成录音结果 is \(flag) url is \(recorder.url)")
+        if flag {
+            finishClosure?(true, recorder.url.relativePath)
+        }
+    }
+
+    public  func audioRecorderEncodeErrorDidOccur(_: AVAudioRecorder, error: Error?) {
+        guard let error = error else { return }
+        BFLog(message: error)
+    }
+}

+ 113 - 0
BFCommonKit/Classes/BFUtility/NXDeviceManager.swift

@@ -0,0 +1,113 @@
+//
+//  NXDeviceManager.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+
+import UIKit
+
+class NXDeviceManager: NSObject {
+    
+    class func info() -> [String] {
+        var data: [String] = []
+        
+        data.append("Device Name: \(deviceNameAlias())")
+        if let bundleId = Bundle.main.bundleIdentifier {
+            data.append("Bundle Identifier: \(bundleId)")
+        }
+        
+        if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
+            let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
+            data.append( "Host App Version: \(version).\(buildNumber)" )
+        }
+        
+        if let venderId = UIDevice.current.identifierForVendor {
+            data.append( "Identifier For Vendor: \(venderId)" )
+        }
+        
+        data.append("System Version: \(getSystemVersion())")
+        data.append("Model: \(platformModelString())")
+        
+//        data.append("Total Disk Space(MB): \(UIDevice.totalDiskSpaceInMB)")
+//        data.append("Free Disk Space(MB): \(UIDevice.freeDiskSpaceInMB)")
+        
+        let lastRestarted = Date(timeIntervalSince1970: TimeInterval(Date().timeIntervalSince1970 - Double(uptime())))
+        data.append("Uptime: \(uptime())/\(lastRestarted)")
+        
+        return data
+    }
+    
+    class var isIpad:Bool {
+        if #available(iOS 8.0, *) {
+            return UIScreen.main.traitCollection.userInterfaceIdiom == .pad
+        } else {
+            return UIDevice.current.userInterfaceIdiom == .pad
+        }
+    }
+    class var isIphone:Bool {
+        if #available(iOS 8.0, *) {
+            return UIScreen.main.traitCollection.userInterfaceIdiom == .phone
+        } else {
+            return UIDevice.current.userInterfaceIdiom == .phone
+        }
+    }
+    
+    ///Name of the devices, like Baudins's Iphone
+    class func deviceNameAlias() -> String {
+        return  UIDevice.current.name
+    }
+    
+    class func processorCount() -> Int {
+        return ProcessInfo.processInfo.activeProcessorCount
+    }
+    
+    //Verion of the OS, like 9.0.1
+    class func osVersion()-> String {
+        return UIDevice.current.systemVersion;
+    }
+    
+    class func platformModelString() -> String {
+        if let key = "hw.machine".cString(using: String.Encoding.utf8) {
+            var size: Int = 0
+            sysctlbyname(key, nil, &size, nil, 0)
+            var machine = [CChar](repeating: 0, count: Int(size))
+            sysctlbyname(key, &machine, &size, nil, 0)
+            return String(cString: machine)
+        }
+        return "Unknown"
+    }
+    
+    /** uptime in seconds **/
+    class func uptime()  -> Int {
+        var currentTime = time_t()
+        var bootTime    = timeval()
+        var mib         = [CTL_KERN, KERN_BOOTTIME]
+        
+        var size = MemoryLayout<timeval>.stride
+        
+        if sysctl(&mib, u_int(mib.count), &bootTime, &size, nil, 0) != -1 && bootTime.tv_sec != 0 {
+            time(&currentTime)
+            
+            if (currentTime < bootTime.tv_sec) {
+                return 0
+            }
+            
+            return  currentTime - bootTime.tv_sec
+        }
+        return 0
+    }
+    
+    class func getScreenBrightness() -> CGFloat {
+        return UIScreen.main.brightness
+    }
+    
+    class func getPhysicalMemory() -> UInt64 {
+        return ProcessInfo.processInfo.physicalMemory
+    }
+    
+    class func getSystemVersion() -> String {
+        return UIDevice.current.systemVersion
+    }
+}

+ 43 - 0
BFCommonKit/Classes/BFUtility/NXFileManager.swift

@@ -0,0 +1,43 @@
+//
+//  NXFileManager.swift
+//  NXFramework-Swift-Demo
+//
+//  Created by ak on 2020/10/26.
+//  Copyright © 2020 NXFramework-Swift. All rights reserved.
+//
+/*
+
+本类功能, 文件操作.(ios, mac)
+
+一,iOS目录结构说明
+1,沙盒目录结构
+├── Documents - 存储用户数据或其它应该定期备份的
+├── Library
+│   ├── Caches -
+用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息
+│   │   └── Snapshots
+│   │       └── com.youyouxingyuan.re
+│   │           ├── A85B73F0-26A8-44E4-A761-446CAB8DAB38@2x.png
+│   │           └── BFAD5885-B767-4320-9A4B-555EC881C50D@2x.png
+│   └── Preferences - 偏好设置文件 NSUserDefaults 保存的数据
+└── tmp - 这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息
+
+2,在iOS8之后,应用每一次重启,沙盒路径都动态的发生了变化但不用担心数据问题,苹果会把你上一个路径中的数据转移到你新的路径中。你上一个路径也会被苹果毫无保留的删除,只保留最新的路径。
+
+@see  <Foundation/NSPathUtilities.h>
+
+*/
+import UIKit
+
+class NXFileManager: NSObject {
+    
+    static var documentDirectoryURL: URL {
+      return try! FileManager.default.url(
+        for: .documentDirectory,
+        in: .userDomainMask,
+        appropriateFor: nil,
+        create: false
+      )
+    }
+
+}

+ 85 - 0
BFCommonKit/Classes/BFUtility/PQBFConfig.swift

@@ -0,0 +1,85 @@
+//
+//  PQBFConfig.swift
+//  BFFramework
+//
+//  Created by SanW on 2021/6/4.
+//  111
+
+import UIKit
+
+public class PQBFConfig: NSObject {
+    public static let shared = PQBFConfig()
+    /**
+     // 主题适配方案一
+     styleColor = nomal
+     statusBarStyle = .light
+     backgroundColor = UIColor.hexColor(hexadecimal: "#191919")
+     styleTitleColor: UIColor = UIColor.white
+     cutViewStyleColor: UIColor = UIColor.white
+     cutViewTintColor: UIColor = UIColor.black
+     materialDeleteImage = UIImage.init().BF_Image(named:"icon_search_delete")
+     cutDurationColor = UIColor.init(red: 238.0 / 255.0, green: 0 / 255.0, blue: 81.0 / 255.0, alpha: 0.1)
+     hiddenMusicMask = false
+     otherTintColor = UIColor.hexColor(hexadecimal: "#333333")
+     //主题适配方案二
+     styleColor = green
+     statusBarStyle = .dark
+     backgroundColor = UIColor.white
+     styleTitleColor: UIColor = UIColor.black
+     cutViewStyleColor: UIColor = UIColor.hexColor(hexadecimal: "#3DC1C1")
+     cutViewTintColor: UIColor = UIColor.white
+     materialDeleteImage = UIImage.init().BF_Image(named:"deleteAudio")
+     cutDurationColor = UIColor.init(red: 61.0 / 255.0, green: 193.0 / 255.0, blue: 193.0 / 255.0, alpha: 0.1)
+     hiddenMusicMask = true
+     otherTintColor = UIColor.hexColor(hexadecimal: "#F2F2F2")
+     */
+    // 主题色
+    public var styleColor: styleColor = .green
+    // statusBarStyle
+    public var statusBarStyle: statusBarStyle = .dark
+    // 背景色
+    public var styleBackGroundColor: UIColor = UIColor.white
+    // 主题标题色
+    public var styleTitleColor: UIColor = UIColor.black
+    // 裁剪主题色
+    public var cutViewStyleColor: UIColor = UIColor.hexColor(hexadecimal: "#3DC1C1")
+    // 素材删除图
+    public var materialDeleteImage: UIImage? = UIImage().BF_Image(named: "deleteAudio")
+    // 裁剪主题色61, 193, 193
+    public var cutDurationColor: UIColor = UIColor(red: 61.0 / 255.0, green: 193.0 / 255.0, blue: 193.0 / 255.0, alpha: 0.1)
+    // 裁剪主题色
+    public var cutViewTintColor: UIColor = UIColor.white
+    // 裁剪主题色
+    public var hiddenMusicMask: Bool = true
+    public var otherTintColor: UIColor = UIColor.hexColor(hexadecimal: "#F2F2F2")
+
+    // 微信登陆信息
+//    public var appInfo: WXApiInfo?
+    // 渠道
+    public let channelID: String = "AppStore"
+    // 友盟账号 快乐星球:60b9fa644d0228352bbc8937 闪音:60b9f95b4d0228352bbc87e0
+    public var umAppkey: String = "60b9f95b4d0228352bbc87e0"
+    // bugly账号 快乐星球:b7411e0d-f214-433c-9343-366b9ca99a53
+    // 闪音:6c481442-aeb4-49ec-a2ae-21359bfef44a
+    public var buglyAppkey: String = "6c481442-aeb4-49ec-a2ae-21359bfef44a"
+    
+    //add by ak 公用参数使用
+    public var appType:String = "15"
+    //苹果后台创建的 APPID
+    public var appId:String = "1570572849"
+    
+    //是否可打印 LOG
+    public var enableBFLog:Bool = false
+
+    override private init() {
+        super.init()
+    }
+
+    override public func copy() -> Any {
+        return self
+    }
+
+    override public func mutableCopy() -> Any {
+        return self
+    }
+}

+ 22 - 0
BFCommonKit/Classes/BFUtility/PQBridgeObject.h

@@ -0,0 +1,22 @@
+//
+//  PQBridgeObject.h
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/20.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#include <ifaddrs.h>
+#include <arpa/inet.h>
+#include <net/if.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface PQBridgeObject : NSObject
++ (NSString *)getByteRate;
++ (long long) getInterfaceBytes;
++ (NSString *)formatNetWork:(long long int)rate;
+@end
+
+NS_ASSUME_NONNULL_END

+ 56 - 0
BFCommonKit/Classes/BFUtility/PQBridgeObject.m

@@ -0,0 +1,56 @@
+//
+//  PQBridgeObject.m
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/20.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+#import "PQBridgeObject.h"
+
+@implementation PQBridgeObject
++ (NSString *)getByteRate {
+    long long intcurrentBytes = [PQBridgeObject getInterfaceBytes];
+    NSString *rateStr = [PQBridgeObject formatNetWork:intcurrentBytes];
+    return rateStr;
+}
+
++ (long long) getInterfaceBytes {
+    struct ifaddrs *ifa_list = 0, *ifa;
+    if (getifaddrs(&ifa_list) == -1) {
+        return 0;
+    }
+    uint32_t iBytes = 0;
+    uint32_t oBytes = 0;
+    for (ifa = ifa_list; ifa; ifa = ifa->ifa_next) {
+        if (AF_LINK != ifa->ifa_addr->sa_family)
+            continue;
+        if (!(ifa->ifa_flags & IFF_UP) && !(ifa->ifa_flags & IFF_RUNNING))
+            continue;
+        if (ifa->ifa_data == 0)
+            continue;
+        /* Not a loopback device. */
+        if (strncmp(ifa->ifa_name, "lo", 2)){
+            struct if_data *if_data = (struct if_data *)ifa->ifa_data;
+            iBytes += if_data->ifi_ibytes;
+            
+            oBytes += if_data->ifi_obytes;
+        }
+    }
+    freeifaddrs(ifa_list);
+    NSLog(@"\n[getInterfaceBytes-Total]%d,%d",iBytes,oBytes);
+    return (iBytes + oBytes);
+}
+
++ (NSString *)formatNetWork:(long long int)rate {
+    if (rate < 1024) {
+        return [NSString stringWithFormat:@"%lldB/s", rate];
+    } else if (rate >= 1024 && rate < 1024 * 1024) {
+        return [NSString stringWithFormat:@"%.1fK/s", (double)rate /1024];
+    } else if (rate >= 1024 * 1024 && rate < 1024 * 1024 * 1024) {
+        return [NSString stringWithFormat:@"%.1fM/s", (double)rate / (1024*1024)];
+    } else {
+        return @"0B/s";
+    };
+}
+@end

+ 794 - 0
BFCommonKit/Classes/BFUtility/PQCommonMethodUtil.swift

@@ -0,0 +1,794 @@
+//
+//  PQCommonMethodUtil.swift
+//  PQSpeed
+//
+//  Created by lieyunye on 2020/5/29.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import AdSupport
+import Alamofire
+import Foundation
+import KeychainAccess
+import Kingfisher
+import KingfisherWebP
+import Photos
+import Toast_Swift
+/// Home文件地址
+public let homeDirectory = NSHomeDirectory()
+/// docdocumens文件地址
+public let documensDirectory = homeDirectory + "/Documents"
+/// library文件地址
+public let libraryDirectory = homeDirectory + "/Library"
+
+/// 本地存储资源地址
+public let resourceDirectory = documensDirectory + "/Resource"
+/// 播放视频缓冲本地沙河目录
+public let videoCacheDirectory = resourceDirectory + "/VideoCache"
+/// 相册视频导出到本地沙河目录
+public let photoLibraryDirectory = resourceDirectory + "/PhotoLibrary/"
+/// 背景音乐导出到本地沙河目录
+public let bgMusicDirectory = resourceDirectory + "/BGMusic/"
+/// 网络视频素材下载到本地沙河目录
+public let downloadDirectory = resourceDirectory + "/Download/"
+/// 网络图片、GIF 素材下载到本地沙河目录
+public let downloadImagesDirectory = resourceDirectory + "/DownloadImages/"
+/// 临时缓存本地沙河目录地址
+public let tempDirectory = resourceDirectory + "/Temp/"
+/// 导出声音的本地沙盒目录v
+public let exportAudiosDirectory = resourceDirectory + "/ExportAudios/"
+/// 导出合成视频的本地沙盒目录
+public let exportVideosDirectory = resourceDirectory + "/ExportVideos/"
+// 版本构建号
+public let versionCode = "\(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "1")"
+// 版本号
+public let versionName = "\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "1.0.0")"
+/// 创建目录文件
+/// - Returns: <#description#>
+public func createDirectory(path: String) {
+    let fileManager = FileManager.default
+    if !fileManager.fileExists(atPath: path) {
+        try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
+    }
+}
+
+/// 判断文件夹是否存在
+/// - Parameter dicPath:文件夹 目录
+public func directoryIsExists(dicPath: String) -> Bool {
+    BFLog(message: " dir path is: \(dicPath)")
+    var directoryExists = ObjCBool(false)
+    let fileExists = FileManager.default.fileExists(atPath: dicPath, isDirectory: &directoryExists)
+    return fileExists && directoryExists.boolValue
+}
+
+/// 判断文件是否存在
+/// - Parameter filepath: 文件目录
+public func fileIsExists(filePath: String) -> Bool {
+    BFLog(message: "file path is: \(filePath)")
+
+    let fileExists = FileManager.default.fileExists(atPath: filePath)
+    return fileExists
+}
+
+/// 创建沙河文件地址
+/// - Parameter url: 原地址
+/// - Returns: <#description#>
+public func createFilePath(url: String) -> Bool {
+    let fileManager = FileManager.default
+    if !fileManager.fileExists(atPath: url) {
+        let isFinished = fileManager.createFile(atPath: url, contents: nil, attributes: nil)
+        return isFinished
+    }
+    return true
+}
+
+public func cIPHONE_X() -> Bool {
+    guard #available(iOS 11.0, *) else {
+        return false
+    }
+    let isX = (UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0) > 0
+    return isX
+}
+
+/// 给按钮/imageView加载网络图片
+///
+/// - Parameters:
+///   - url: 网络url
+///   - mainView: 需要加载的视图
+public func netImage(url: String, mainView: Any, placeholder: UIImage = UIImage().BF_Image(named: "placehold_image")) {
+    if mainView is UIImageView {
+        (mainView as! UIImageView).kf.setImage(with: URL(string: url), placeholder: placeholder, options: url.suffix(5) == ".webp" ? [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)] : nil, progressBlock: { _, _ in
+
+        }) { _ in
+        }
+    } else if mainView is UIButton {
+        (mainView as! UIButton).kf.setImage(with: URL(string: url), for: .normal, placeholder: placeholder, options: url.suffix(5) == ".webp" ? [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)] : nil, progressBlock: { _, _ in
+
+        }) { _ in
+        }
+    }
+}
+
+/** 获取Kingfisher缓存的图片的data */
+public func kf_imageCacheData(originUrl: String) -> Data? {
+    let diskCachePath = ImageCache.default.cachePath(forKey: originUrl)
+    let data = try? Data(contentsOf: URL(fileURLWithPath: diskCachePath))
+    return data
+}
+
+/** 获取Kingfisher缓存的图片 */
+public func kf_imageCacheImage(originUrl: String, completeHandle: @escaping (_ image: UIImage?, _ error: Error?) -> Void) {
+    ImageCache.default.retrieveImageInDiskCache(forKey: originUrl, options: [.cacheOriginalImage]) { result in
+        DispatchQueue.main.async {
+            switch result {
+            case let .success(image):
+                completeHandle(image, nil)
+            case let .failure(error):
+                completeHandle(nil, error)
+            }
+        }
+    }
+}
+
+/** 打印 */
+public func BFLog<T>(message: T) {
+    let logger = NXLogger.shared
+
+    logger.level = .info
+    logger.ouput = .debuggerConsole
+
+    logger.d(message as? String ?? "")
+}
+
+// MARK: 获取公共参数
+
+public func commonParams() -> [String: Any] {
+    let model = UIDevice.current.model
+    let systemName = UIDevice.current.systemName
+    let systemVersion = UIDevice.current.systemVersion
+    let localizedModel = UIDevice.current.localizedModel
+    let machineInfo: [String: Any] = [
+        "model": model, "system": systemName + " " + systemVersion, "brand": localizedModel, "platform": "iOS", "networkType": networkStatus(), "clientIp": ipAddress(),
+    ]
+    var commParams: [String: Any] = [
+        "appVersionCode": versionCode,
+        "versionCode": versionCode,
+        "system": systemName + " " + systemVersion,
+        "systemVersion": systemName + " " + systemVersion,
+        "appType": PQBFConfig.shared.appType,
+        "appId": PQBFConfig.shared.appId,
+        "machineCode": getMachineCode(),
+        "networkType": networkStatus(),
+        "ipAddress": ipAddress(),
+        "clientTimestamp": Int64(Date().timeIntervalSince1970 * 1000),
+        "platform": "iOS",
+        "versionName": versionName,
+//        "sessionId": PQSingletoMemoryUtil.shared.sessionId,
+//        "subSessionId": PQSingletoMemoryUtil.shared.subSessionid ?? PQSingletoMemoryUtil.shared.sessionId,
+        "mid": getMachineCode(),
+        "machineInfo": dictionaryToJsonString(machineInfo) ?? "",
+//        "abInfoData": dictionaryToJsonString(PQSingletoMemoryUtil.shared.abInfoData) ?? "",
+        "requestId": getUniqueId(desc: "requestId"),
+        "idfa": ASIdentifierManager.shared().advertisingIdentifier.uuidString,
+        "idfv": UIDevice.current.identifierForVendor?.uuidString ?? "",
+//        "deviceToken": PQSingletoMemoryUtil.shared.deviceToken,
+    ]
+//    if BFLoginUserInfo.shared.accessToken.count > 0 {
+//        commParams["token"] = BFLoginUserInfo.shared.accessToken
+//    }
+//    if BFLoginUserInfo.shared.uid.count > 0 {
+//        commParams["loginUid"] = BFLoginUserInfo.shared.uid
+//        commParams["uid"] = BFLoginUserInfo.shared.uid
+//    }
+//    showAlertVc(title: "公参", message: dictionaryToJsonString(commParams))
+    return commParams
+}
+
+/// 获取网络状态
+/// - Returns: <#description#>
+public func networkStatus() -> String {
+    let status = NetworkReachabilityManager(host: "www.baidu.com")?.networkReachabilityStatus
+    var statusStr: String!
+
+    switch status {
+    case .unknown:
+        statusStr = "NETWORK_UNKNOWN"
+    case .notReachable:
+        statusStr = "NETWORK_NO"
+    case .reachable(.wwan):
+        statusStr = "4G"
+    case .reachable(.ethernetOrWiFi):
+        statusStr = "Wi-Fi"
+    default:
+        statusStr = "NETWORK_UNKNOWN"
+    }
+    return statusStr
+}
+
+/// 判断是否有网
+/// - Returns: <#description#>
+public func isNetConnected() -> Bool {
+    return NetworkReachabilityManager(host: "www.baidu.com")?.networkReachabilityStatus != .notReachable
+}
+
+/// 获取ip地址
+/// - Returns: <#description#>
+public func ipAddress() -> String {
+    var addresses = [String]()
+    var ifaddr: UnsafeMutablePointer<ifaddrs>?
+    if getifaddrs(&ifaddr) == 0 {
+        var ptr = ifaddr
+        while ptr != nil {
+            let flags = Int32(ptr!.pointee.ifa_flags)
+            var addr = ptr!.pointee.ifa_addr.pointee
+            if (flags & (IFF_UP | IFF_RUNNING | IFF_LOOPBACK)) == (IFF_UP | IFF_RUNNING) {
+                if addr.sa_family == UInt8(AF_INET) || addr.sa_family == UInt8(AF_INET6) {
+                    var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
+                    if getnameinfo(&addr, socklen_t(addr.sa_len), &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) == 0 {
+                        if let address = String(validatingUTF8: hostname) {
+                            addresses.append(address)
+                        }
+                    }
+                }
+            }
+            ptr = ptr!.pointee.ifa_next
+        }
+        freeifaddrs(ifaddr)
+    }
+    return addresses.first ?? "0.0.0.0"
+}
+
+/// 生成唯一ID / 分享跟冷启动
+/// - Parameter desc: <#desc description#>
+/// - Returns: <#description#>
+public func getUniqueId(desc: String) -> String {
+    let timeStr: String = "\(Date().timeIntervalSince1970)"
+    let uuid: String = getMachineCode()
+    let code: String = "\(arc4random_uniform(1_000_000_000))"
+    let uniqueId = (timeStr + desc + uuid + code).md5.md5
+    BFLog(message: "生成唯一码:desc = \(desc),timeStr = \(timeStr),uuid = \(uuid),code = \(code),uniqueId = \(uniqueId)")
+    return uniqueId
+}
+
+// MARK: 字典转字符串
+
+public func dictionaryToJsonString(_ dic: [String: Any]) -> String? {
+    BFLog(message: "dictionaryToJsonString = \(dic)")
+    if !JSONSerialization.isValidJSONObject(dic) {
+        return ""
+    }
+    guard let data = try? JSONSerialization.data(withJSONObject: dic, options: []) else {
+        return ""
+    }
+    BFLog(message: "dictionaryToJsonString - data = \(data)")
+    let str = String(data: data, encoding: String.Encoding.utf8)
+    BFLog(message: "dictionaryToJsonString - str = \(String(describing: str))")
+    return str
+}
+
+// MARK: 字符串转字典
+
+public func jsonStringToDictionary(_ str: String) -> [String: Any]? {
+    let data = str.data(using: String.Encoding.utf8)
+    if data == nil {
+        return [:]
+    }
+    if let dict = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String: Any] {
+        return dict
+    }
+    return [:]
+}
+
+// MARK: 字符串转数组
+
+public func jsonStringToArray(_ str: String) -> [[String: String]]? {
+    let data = str.data(using: String.Encoding.utf8)
+    if let array = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as? [[String: String]] {
+        return array
+    }
+    return nil
+}
+
+/// 数组转为string
+/// - Parameter array: <#array description#>
+/// - Returns: <#description#>
+public func arrayToJsonString(_ array: [Any]) -> String {
+    if !JSONSerialization.isValidJSONObject(array) {
+        BFLog(message: "无法解析String")
+        return ""
+    }
+    let data: NSData! = try? JSONSerialization.data(withJSONObject: array, options: []) as NSData?
+    let JSONString = NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue)
+    return JSONString! as String
+}
+
+/// jsonString转为数组
+/// - Parameter jsonString: <#jsonString description#>
+/// - Returns: <#description#>
+public func jsonStringToArray(jsonString: String) -> [Any]? {
+    let data = jsonString.data(using: String.Encoding.utf8)
+    if data == nil {
+        return nil
+    }
+    if let array = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [Any] {
+        return array
+    }
+    return nil
+}
+
+/// 计算字符串大小
+/// - Parameters:
+///   - text: <#text description#>
+///   - font: <#font description#>
+///   - size: <#size description#>
+/// - Returns: <#description#>
+public func sizeWithText(text: String, font: UIFont, size: CGSize) -> CGSize {
+    let attributes = [NSAttributedString.Key.font: font]
+    let option = NSStringDrawingOptions.usesLineFragmentOrigin
+    let rect: CGRect = text.boundingRect(with: size, options: option, attributes: attributes, context: nil)
+    return rect.size
+}
+
+/// 根据行数计算字符串大小
+/// - Parameters:
+///   - text: <#text description#>
+///   - numberOfLines: <#numberOfLines description#>
+///   - font: <#font description#>
+///   - maxSize: <#maxSize description#>
+/// - Returns: <#description#>
+public func sizeTextFits(attributedText: NSMutableAttributedString?, text: String?, numberOfLines: Int, font: UIFont, maxSize: CGSize) -> CGSize {
+    var newSize: CGSize = CGSize(width: 0, height: 0)
+    let label = UILabel(frame: CGRect.zero)
+    label.font = font
+    label.numberOfLines = numberOfLines
+    if attributedText != nil {
+        label.attributedText = attributedText
+    } else {
+        label.text = text
+    }
+    newSize = label.sizeThatFits(maxSize)
+
+    return newSize
+}
+
+public func textNumberOfLines(text: String, font: UIFont, maxSize _: CGSize) -> Int {
+    let label = UILabel(frame: CGRect.zero)
+    label.font = font
+    label.numberOfLines = 0
+    label.text = text
+    return label.numberOfLines
+}
+
+/// 生成渐变色
+/// - Parameters:
+///   - size: <#size description#>
+///   - endPoint: <#endPoint description#>
+///   - startColor: <#startColor description#>
+///   - endColor: <#endColor description#>
+/// - Returns: <#description#>
+public func gradientColor(size: CGSize, endPoint: CGPoint, startColor: UIColor, endColor: UIColor) -> UIColor {
+    let gradientLayer = CAGradientLayer()
+    gradientLayer.frame = CGRect(origin: CGPoint(), size: size)
+    gradientLayer.startPoint = CGPoint.zero
+    gradientLayer.endPoint = endPoint
+    gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
+    UIGraphicsBeginImageContext(size)
+    gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
+    let image = UIGraphicsGetImageFromCurrentImageContext()!
+    return UIColor(patternImage: image)
+}
+
+/// 获取设备ID
+/// - Returns: <#description#>
+public func getMachineCode() -> String {
+    let userInfo: [String: Any]? = jsonStringToDictionary(UserDefaults.standard.string(forKey: cUserInfoStorageKey) ?? "")
+    if userInfo != nil && ((userInfo?.keys.contains("isVirtualUser") ?? false) && !(userInfo?["isVirtualUser"] is NSNull) && ((userInfo?["isVirtualUser"] as? Bool) ?? false)) && ((userInfo?.keys.contains("mid") ?? false) && !(userInfo?["mid"] is NSNull)) {
+        BFLog(message: "虚拟账号mid:\("\(userInfo?["mid"] ?? "")")")
+        return "\(userInfo?["mid"] ?? "")"
+    }
+    let keychain = Keychain(service: "com.piaoquan.pqspeed")
+    var uuid: String = keychain["machineCode"] ?? ""
+    if uuid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+        uuid = NSUUID().uuidString
+        keychain["machineCode"] = uuid
+    }
+    BFLog(message: "正式账号mid:\(uuid)")
+    return uuid
+}
+
+/// 显示加载中视图
+/// - Parameters:
+///   - superView: <#superView description#>
+///   - msg: <#msg description#>
+/// - Returns: <#description#>
+public func cShowHUB(superView: UIView?, msg: String?) {
+    DispatchQueue.main.async {
+        if superView == nil {
+            if msg == nil {
+                UIApplication.shared.keyWindow?.makeToastActivity(.center)
+            } else {
+                UIApplication.shared.keyWindow?.makeToast(msg, duration: 3.0, position: .center)
+            }
+        } else {
+            if msg == nil {
+                superView!.makeToastActivity(.center)
+            } else {
+                superView!.makeToast(msg, duration: 3.0, position: .center)
+            }
+        }
+    }
+}
+
+/// 隐藏加载中视图
+/// - Parameter superView: <#superView description#>
+/// - Returns: <#description#>
+public func cHiddenHUB(superView: UIView?) {
+    DispatchQueue.main.async {
+        if superView == nil {
+            UIApplication.shared.keyWindow?.hideAllToasts()
+            UIApplication.shared.keyWindow?.hideToastActivity()
+        } else {
+            superView!.hideAllToasts()
+            superView?.hideToastActivity()
+        }
+    }
+}
+
+/// 获取存储值
+/// - Parameter key: key description
+/// - Returns: description
+public func getUserDefaults(key: String) -> Any? {
+    return UserDefaults.standard.object(forKey: key)
+}
+
+/// 存储数据
+/// - Parameters:
+///   - key: key description
+///   - value: value description
+/// - Returns: description
+public func saveUserDefaults(key: String, value: String) {
+    UserDefaults.standard.set(value, forKey: key)
+    UserDefaults.standard.synchronize()
+}
+
+/// 存储数据带版本号
+/// - Parameters:
+///   - key: <#key description#>
+///   - value: <#value description#>
+public func saveUserDefaultsToJson(key: String, value: Any) {
+    UserDefaults.standard.set(dictionaryToJsonString([key: value, "appVersionCode": versionCode, "versionName": versionName]), forKey: key)
+    UserDefaults.standard.synchronize()
+}
+
+/// 获取数据带版本号
+/// - Parameter key: <#key description#>
+/// - Returns: <#description#>
+public func getUserDefaultsForJson(key: String) -> Any? {
+    let jsonStr = UserDefaults.standard.object(forKey: key)
+    if jsonStr != nil {
+        return jsonStringToDictionary(jsonStr as! String)?[key]
+    }
+    return UserDefaults.standard.object(forKey: key)
+}
+
+/// 清空数据
+/// - Parameters:
+///   - key: key description
+///   - value: value description
+/// - Returns: description
+public func removeUserDefaults(key: String) {
+    UserDefaults.standard.removeObject(forKey: key)
+    UserDefaults.standard.synchronize()
+}
+
+/// 存储数据
+/// - Parameters:
+///   - key: key description
+///   - value: value description
+/// - Returns: description
+public func saveUserDefaults(key: String, value: Any) {
+    UserDefaults.standard.set(value, forKey: key)
+    UserDefaults.standard.synchronize()
+}
+
+/// 保存自定义model   as NSArray  当 OBJ 是数组时不能使用 Array 要使用 NSArray
+/// - Parameter object: <#object description#>
+/// - Parameter key: <#key description#>
+public func saveCustomObject(customObject object: NSCoding, key: String) {
+    let encodedObject = NSKeyedArchiver.archivedData(withRootObject: object)
+    UserDefaults.standard.set(encodedObject, forKey: key)
+    UserDefaults.standard.synchronize()
+    BFLog(message: "保存自定义类成功 key is \(key) \(encodedObject.count)")
+}
+
+/// 取自定义model
+/// - Parameter key: <#key description#>
+public func getCustomObject(forKey key: String) -> AnyObject? {
+    let decodedObject = UserDefaults.standard.object(forKey: key) as? Data
+
+    if decodedObject == nil {
+        BFLog(message: "key is \(key)  decodedObject is nil")
+    }
+    if let decoded = decodedObject {
+        let object = NSKeyedUnarchiver.unarchiveObject(with: decoded as Data)
+
+        return object as AnyObject?
+    }
+
+    return nil
+}
+
+/// 添加通知
+/// - Parameters:
+///   - observer: <#observer description#>
+///   - aSelectorName: <#aSelectorName description#>
+///   - aName: <#aName description#>
+///   - anObject: <#anObject description#>
+/// - Returns: <#description#>
+public func addNotification(_ observer: Any, selector aSelectorName: Selector, name aName: String, object anObject: Any?) {
+    PQNotification.addObserver(observer, selector: aSelectorName, name: NSNotification.Name(rawValue: aName), object: anObject)
+}
+
+/// 发送通知
+/// - Parameter aName: <#aName description#>
+/// - Returns: <#description#>
+public func postNotification(name aName: String, userInfo: [AnyHashable: Any]? = nil) {
+    PQNotification.post(name: NSNotification.Name(aName), object: nil, userInfo: userInfo)
+}
+
+/// 获取是否打开推送
+/// - Parameter completeHander: <#completeHander description#>
+/// - Returns: <#description#>
+public func pushNotificationIsOpen(completeHander: ((_ isOpen: Bool) -> Void)?) {
+    if #available(iOS 10.0, *) {
+        UNUserNotificationCenter.current().getNotificationSettings { setttings in
+            completeHander!(setttings.authorizationStatus == .authorized)
+        }
+    } else {
+        completeHander!(UIApplication.shared.currentUserNotificationSettings?.types.contains(UIUserNotificationType.alert) ?? false)
+    }
+}
+
+/// 发送上传本地推送
+/// - Parameter isSuccess: 是否上传成功
+/// - Returns: <#description#>
+public func sendUploadNotification(isSuccess: Bool) {
+    let title: String = isSuccess ? "上传完成了!" : "上传失败了!"
+    let body: String = isSuccess ? "请点击发布,完成上传。否则,您的视频可能丢失" : "快来看看怎么了?"
+    sendLocalNotification(title: title, body: body)
+}
+
+/// 发送本地推送
+/// - Parameters:
+///   - title: 标题
+///   - body: 内容
+/// - Returns: <#description#>
+public func sendLocalNotification(title: String, body: String) {
+    // 设置推送内容
+    if #available(iOS 10.0, *) {
+        let content = UNMutableNotificationContent()
+        content.title = title
+        content.body = body
+        content.badge = 1
+        // 设置通知触发器
+        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
+        // 设置请求标识符
+        let requestIdentifier = getUniqueId(desc: "notification\(title)")
+        // 设置一个通知请求
+        let request = UNNotificationRequest(identifier: requestIdentifier,
+
+                                            content: content, trigger: trigger)
+        // 将通知请求添加到发送中心
+        UNUserNotificationCenter.current().add(request) { error in
+            if error == nil {
+                print("Time Interval Notification scheduled: \(requestIdentifier)")
+            }
+        }
+    } else {
+        // Fallback on earlier versions
+        let notification = UILocalNotification()
+        notification.alertBody = body
+        notification.alertTitle = title
+        notification.applicationIconBadgeNumber = 1
+        notification.fireDate = Date(timeIntervalSinceNow: 0)
+        UIApplication.shared.scheduledLocalNotifications = [notification]
+    }
+}
+
+/// 打开应用设置
+public func openAppSetting() {
+    if UIApplication.shared.canOpenURL(URL(string: UIApplication.openSettingsURLString)!) {
+        UIApplication.shared.openURL(URL(string: UIApplication.openSettingsURLString)!)
+    }
+}
+
+/// dns解析
+/// - Parameter hostUrl: speed.piaoquantv.com /
+/// - Returns: <#description#>
+public func parseDNS(hostUrl: String) -> [String: Any]? {
+    let host: CFHost? = CFHostCreateWithName(nil, hostUrl as CFString).takeRetainedValue()
+    let start = CFAbsoluteTimeGetCurrent()
+    var success: DarwinBoolean = false
+    var addressList: [String] = Array<String>.init()
+    var addresses: NSArray?
+    if CFHostStartInfoResolution(host!, .addresses, nil) {
+        addresses = (CFHostGetAddressing(host!, &success)?.takeUnretainedValue())
+    }
+    if success == true {
+        for case let theAddress as NSData in addresses! {
+            var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
+            if getnameinfo(theAddress.bytes.assumingMemoryBound(to: sockaddr.self), socklen_t(theAddress.length),
+                           &hostname, socklen_t(hostname.count), nil, 0, NI_NUMERICHOST) == 0
+            {
+                let numAddress = String(cString: hostname)
+                addressList.append("\(hostUrl)/\(numAddress)")
+            }
+        }
+    }
+    let end = CFAbsoluteTimeGetCurrent()
+    let duration = end - start
+    BFLog(message: "duration = \(duration)")
+    BFLog(message: "addressList = \(addressList)")
+    if addressList.count > 0 {
+        return ["dnsResult": arrayToJsonString(addressList), "duration": duration * 1000, "hostName": hostUrl, "networkType": networkStatus()]
+    } else {
+        return nil
+    }
+}
+
+/// 获取当前日期
+/// - Returns: <#description#>
+public func systemCurrentDate() -> String {
+    let dateFormatter = DateFormatter()
+    dateFormatter.dateFormat = "YYYY-MM-dd"
+    return dateFormatter.string(from: Date())
+}
+
+/// 时间戳转日期
+/// - Parameter timeInterval: <#timeInterval description#>
+/// - Returns: <#description#>
+public func timeIntervalToDateString(timeInterval: TimeInterval) -> String {
+    let date = Date(timeIntervalSince1970: timeInterval)
+    let dateFormatter = DateFormatter()
+    dateFormatter.dateFormat = "MM月dd日 HH:mm"
+    return dateFormatter.string(from: date)
+}
+
+/// 判断字符串或者字典是否为空
+/// - Parameter object: <#object description#>
+/// - Returns: <#description#>
+public func isEmpty(object: Any?) -> Bool {
+    if object == nil {
+        return true
+    }
+    if object is String {
+        return (object as! String).count <= 0
+    }
+    if object is [String: Any] {
+        return (object as! [String: Any]).keys.count <= 0
+    }
+    return false
+}
+
+public func isEmptyObject(object: Any?) -> Bool {
+    if object == nil {
+        return true
+    }
+    if object is String {
+        return object == nil || ((object as? String)?.count ?? 0) <= 0
+    }
+    if object is [String: Any] {
+        return object == nil || ((object as? [String: Any])?.keys.count ?? 0) <= 0
+    }
+//    if object is List<Object> {
+//        return object == nil || ((object as? List<Object>)?.count ?? 0) <= 0
+//    }
+    return false
+}
+
+/// <#Description#>
+/// - Parameter string: <#string description#>
+/// - Returns: <#description#>
+public func isIncludeChineseIn(string: String) -> Bool {
+    for (_, value) in string.enumerated() {
+        if value >= "\u{4E00}", value <= "\u{9FA5}" {
+            return true
+        }
+    }
+    return false
+}
+
+/// 获取文件内容的MD5
+/// - Parameters:
+///   - path: 地址
+///   - data: data
+/// - Returns: <#description#>
+public func contentMD5(path: String? = nil, data _: Data? = nil) -> String? {
+    if path == nil || (path?.count ?? 0) <= 0 || !FileManager.default.fileExists(atPath: path ?? "") {
+        BFLog(message: "生成内容md5值:地址错误或者不存在\(String(describing: path))")
+        return ""
+    }
+    let att = try? FileManager.default.attributesOfItem(atPath: path ?? "")
+    let size = Int64(att?[FileAttributeKey.size] as! UInt64)
+    if size <= 0 {
+        BFLog(message: "生成内容md5值:文件大小为0\(size)")
+        return ""
+    }
+//    let hash: String = OSSUtil.base64Md5(forFilePath: path)
+    let hash: String = ""
+    BFLog(message: "生成内容md5值:contentMD5 = \(hash)")
+    return hash
+}
+
+/// 自适应宽
+/// - Parameters:
+///   - width: <#width description#>
+///   - baseWidth: <#baseWidth description#>
+/// - Returns: <#description#>
+public func adapterWidth(width: CGFloat, baseWidth: CGFloat = 375) -> CGFloat {
+    return width / baseWidth * cScreenWidth
+}
+
+/// 自适应高
+/// - Parameters:
+///   - height: <#height description#>
+///   - baseHeight: <#baseHeight description#>
+/// - Returns: <#description#>
+public func adapterHeight(height: CGFloat, baseHeight: CGFloat = 812) -> CGFloat {
+    return height / baseHeight * cScreenHeigth
+}
+
+/// 检测URL
+/// - Parameter url: <#url description#>
+/// - Returns: <#description#>
+public func isValidURL(url: String?) -> Bool {
+    if url == nil || (url?.count ?? 0) <= 4 || (!(url?.hasPrefix("http") ?? false) && !(url?.hasPrefix("https") ?? false)) {
+        return false
+    }
+    return true
+}
+
+/// 相册数据按创建时间排序
+public var creaFetchOptions: PHFetchOptions = {
+    let fetchOptions = PHFetchOptions()
+    fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
+    return fetchOptions
+}()
+
+/// 相册数据按修改时间排序
+public var modiFetchOptions: PHFetchOptions = {
+    let fetchOptions = PHFetchOptions()
+    fetchOptions.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
+    return fetchOptions
+}()
+
+/// 获取本地素材
+public var avAssertOptions: [String: Any]? = {
+    [AVURLAssetPreferPreciseDurationAndTimingKey: NSNumber(value: true)]
+}()
+
+/// 播放动画图
+public var playGifImages: [UIImage] = {
+    var gifImages = Array<UIImage>.init()
+    for i in 0 ... 44 {
+        gifImages.append(UIImage(named: "\(i).png")!)
+    }
+
+    return gifImages
+}()
+
+/// 压缩图片
+/// - Parameter image: <#image description#>
+/// -
+/// - Returns: <#description#>
+public func zipImage(image: UIImage?, size: Int) -> Data? {
+    var data = image?.pngData()
+    var dataKBytes = Int(data?.count ?? 0) / 1000
+    var maxQuality = 0.9
+    while dataKBytes > size, maxQuality > 0.01 {
+        maxQuality = maxQuality - 0.01
+        data = image?.jpegData(compressionQuality: CGFloat(maxQuality))
+        dataKBytes = (data?.count ?? 0) / 1000
+    }
+    return data
+}

+ 274 - 0
BFCommonKit/Classes/BFUtility/PQConstant.swift

@@ -0,0 +1,274 @@
+//
+//  PQConstant.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+import Kingfisher
+import UIKit
+
+public let cScreenWidth: CGFloat = UIScreen.main.bounds.width
+public let cScreenHeigth: CGFloat = UIScreen.main.bounds.height
+
+// 屏幕适配系数  iponneX?
+public let cAdaptatWidth = cScreenWidth / 375
+public let cAdaptatHeigth = cScreenHeigth / 667
+/// 图库大小
+public let photoItemSize = CGSize(width: (cScreenWidth - cDefaultMargin) / 3, height: (cScreenWidth - cDefaultMargin) / 3) // cell 大小
+// add by ak 视频制作工具视频画布大小
+// 1:1
+public let cVideoCannvasSizeOneToOne = CGSize(width: 1080.0, height: 1080.0)
+// 16:9
+public let cVideoCannvasSizeSixteenToNine = CGSize(width: 1920.0, height: 1080.0)
+// 9:16
+public let cVideoCannvasSizeNineToSixteen = CGSize(width: 1080.0, height: 1920.0)
+
+// add by ak 声频 hz
+public let cEditAudioSampleRate = 44100
+
+// 获取安全区域大小
+public let cSafeAreaHeight: CGFloat = cIPHONE_X() == true ? 34.0 : 0.0
+
+// 获取安全区域大小
+public let cAKSafeAreaHeight: CGFloat = cIPHONE_X() == true ? 25.0 : 0.0
+
+public let cDevice_iPhoneStatusBarHei: CGFloat = cIPHONE_X() == true ? 44.0 : 20.0
+public let cDevice_iPhoneNavBarHei: CGFloat = 44.0
+public let cDevice_iPhoneNavBarAndStatusBarHei: CGFloat = cDevice_iPhoneStatusBarHei + cDevice_iPhoneNavBarHei
+public let cDevice_iPhoneTabBarHei: CGFloat = cSafeAreaHeight + 49.0
+// 遮罩颜色
+public let cShadowColor: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5)
+/// 通知
+public let PQNotification: NotificationCenter = NotificationCenter.default
+
+/** 默认间隔 */
+public let cDefaultMargin: CGFloat = 10
+// 时间精度
+public let playerTimescale: Float64 = 1000.0
+// 时间精度Ints
+public let playerTimescaleInt: Int32 = 1000
+// 最大上传大小 10G
+public let maxUploadSize: Float64 = 10.0 * 1024.0 * 1024.0 * 1024.0
+// 最大素材大小 500M
+public let maxMartialSize: Float64 = 500.0 * 1024.0 * 1024.0
+/// 音频频率范围值
+public let cFrequency: [CGFloat] = [9.0, 12.0, 16.5, 18, 24, 21, 18, 16.5, 12, 16.5, 18, 16.5, 12]
+/**************** tag *******************/
+
+public let cCellTag: Int = 100_001
+/** 引导 tag */
+public let cGuideTag: Int = 100_002
+/** 视频封面tag */
+public let cCoverInfoTag: Int = 100_003
+/** 协议tag */
+public let cProtocalViewTag: Int = 100_004
+/** 心形tag */
+public let cHeartTag: Int = 100_005
+/** 订阅提示tag */
+public let cSubcribeRemindTag: Int = 100_006
+/** 订阅提示tag */
+public let cPushRemindTag: Int = 100_007
+/** 活动提示tag */
+public let cActivityRemindTag: Int = 100_008
+/** pay提示tag */
+public let cPayInfoTag: Int = 100_009
+/** 绑定手机号提示tag */
+public let cBandinPhoneTag: Int = 100_010
+/// 上传最大viewTag
+public let cUploadMaxCountRemindTag: Int = 100_011
+/// 上传提示tag
+public let cUploadViewRemindTag: Int = 100_012
+/// 视频制作播放背景音乐的tag
+public let cVideoMKBGMPlayTag: Int = 100_013
+/// 上滑提示tag
+public let cUpSlideViewRemindTag: Int = 100_014
+/// 创作视频引导提示tag
+public let cVideoMakeRemindTag: Int = 100_015
+/// 操作弹出tag
+public let cOprationRemindTag: Int = 100_016
+/// 气泡提示视图tag
+public let cBubbleRemindViewTag: Int = 100_017
+/// 素材推荐提示tag
+public let cRecommendMaterialViewTag: Int = 100_018
+/// 气泡提示视图tag
+public let cPageMaterialGuidTag: Int = 100_019
+/// 信息流视频广告tag
+public let cAdDrawVideoViewTag: Int = 100_020
+/****************** 私有key ***************/
+
+// 创作工具-项目id前缀
+public let cProjectIdPrefix: String = "app_no_projectdata_"
+
+public let cPrimarykey: String = "@taiziliudong_2020"
+// ---
+public let cRedEnvoStorageKey: String = "redEnvo\(cPrimarykey)"
+public let cOriginStorageKey: String = "origin\(cPrimarykey)"
+public let cZhiFStorageKey: String = "zhiF\(cPrimarykey)"
+public let cCoinStorageKey: String = "coin\(cPrimarykey)"
+public let cMoneStorageKey: String = "mone\(cPrimarykey)"
+public let cAccouStorageKey: String = "accou\(cPrimarykey)"
+public let cTasStorageKey: String = "tas\(cPrimarykey)"
+public let cGameStorageKey: String = "game\(cPrimarykey)"
+public let cSepraStorageKey: String = "sepra\(cPrimarykey)"
+public let cFreeStorageKey: String = "free\(cPrimarykey)"
+
+public let cRedImageStorageKey: String = "ic_hbs_enter\(cPrimarykey)"
+public let cNewImageStorageKey: String = "icon_hbs_free\(cPrimarykey)"
+
+// 用户信息key
+public let cUserInfoStorageKey: String = "userInfo\(cPrimarykey)"
+// 是否加载过引导页
+public let cGuidedInfoStorageKey: String = "guidedInfo\(cPrimarykey)"
+// 是否第一次安装
+public let cFirstInstall: String = "firstInstall\(cPrimarykey)"
+// 第一次冷启动未上传参数
+public let cFirstParams: String = "firstParams\(cPrimarykey)"
+
+// 是否显示了协议
+public let cShowProtocal: String = "showProtocal\(cPrimarykey)"
+
+// 是否竖滑过
+public let cIsVerticalSlip: String = "isVerticalSlip\(cPrimarykey)"
+// 是否横滑过
+public let cIsSideslip: String = "isSideslip\(cPrimarykey)"
+// 是否在浏览态/操作态 单击过(不包含播放结束)
+public let cIsSingleClick: String = "isSingleClick\(cPrimarykey)"
+// 是否显示过竖滑提示
+public let cIsVerticalSlipTip: String = "isVerticalSlipTip\(cPrimarykey)"
+// 是否显示过竖滑提示
+public let cIsSideslipTip: String = "isSideslipTip\(cPrimarykey)"
+// 是否显示过单击提示
+public let cIsSingleClickTip: String = "isSingleClickTip\(cPrimarykey)"
+// 是否显示过单击提示
+public let cIsLikeTip: String = "isLikeTip\(cPrimarykey)"
+// 是否显示过横屏提示
+public let cIsLandscapeTip: String = "isLandscapeTip\(cPrimarykey)"
+// 是否点击过
+public let cIsUploadClick: String = "isUploadClick\(cPrimarykey)"
+// 当前选中的tab
+public let cSelectedTabIndex: String = "selectedTabIndex\(cPrimarykey)"
+// 登录用户当前视频数
+public let cMineVideos: String = "mineVideos\(cPrimarykey)"
+// 登录用户当前视频数
+public let cMineFans: String = "mineFans\(cPrimarykey)"
+// 别人订阅我的数量
+public let cOtherSubscribes: String = "otherSubscribes\(cPrimarykey)"
+/// 登录用户关注数
+public let cMineIdols: String = "mineIdols\(cPrimarykey)"
+// 登录用户头像
+public let cAvatarUrl: String = "avatarUrl\(cPrimarykey)"
+// 绑定手机号
+public let cUpdatePhone: String = "updatePhone\(cPrimarykey)"
+// 添加视频
+public let cInsertVideo: String = "InsertVideo\(cPrimarykey)"
+// 是否是新用户参加RedActivity
+public let cIsNewRedActivityUser: String = "isNewRedActivityUser\(cPrimarykey)"
+// 是否已经显示过昨天完成
+public let cIsYesdayFinishRedActivity: String = "isYesdayFinishRedActivity\(cPrimarykey)"
+// 是否已经显示过未完成昨天RedActivity
+public let cIsYesdayNoFinishRedActivity: String = "isYesdayNoFinishRedActivity\(cPrimarykey)"
+// 是否已经显示过今日已完成
+public let cIsTodayFinishRedActivity: String = "isTodayFinishRedActivity\(cPrimarykey)"
+// 是否已经显示过我的制作提示
+public let cIsShowDraftBoxEntranceRemind: String = "isShowDraftBoxEntranceRemind\(cPrimarykey)"
+// 是否展示过定位提醒
+public let cIsShoWedLocationView: String = "isShoWedLocationView\(cPrimarykey)"
+// 是否展示编辑封面提示
+public let cIsShoWedEditCoverRemindView: String = "isShoWedEditCoverRemindView\(cPrimarykey)"
+// 创作工具是否添加音乐去预览时提示
+public let cIsShoWedAddBGMPreRemindView: String = "isShoWedAddBGMPreRemindView\(cPrimarykey)"
+// 创作工具添加音乐提示
+public let cShoWedAddBGMRemindView: String = "shoWedAddBGMRemindView\(cPrimarykey)"
+// 刷新已读数
+public let cUpdateMsgNoReadCount: String = "updateMsgNoReadCount\(cPrimarykey)"
+
+// add by ak 是否显示过裁剪引导
+public let cIsShowImageCropGuid: String = "isShowImageCropGuid\(cPrimarykey)"
+// 编辑界面引导是否显示过 add by ak
+public let cEditPageGuidIsShow: String = "cEditPageGuidIsShow\(cPrimarykey)"
+// 画布引导
+public let cEditPageCanvasGuidIsShow: String = "cEditPageCanvasGuidIsShow\(cPrimarykey)"
+// 素材移动引导
+public let cEditPageMaterialGuidIsShow: String = "cEditPageMaterialGuidIsShow\(cPrimarykey)"
+// ******************* 通知key ******************* //
+
+// 关注
+public let cAttendtionNotiKey: String = "attendtionNoti\(cPrimarykey)"
+// 订阅
+public let cSubscribeNotiKey: String = "subscribeNoti\(cPrimarykey)"
+// 加入黑名单
+public let cBannedNotiKey: String = "bannedNoti\(cPrimarykey)"
+// 喜欢
+public let cFavoriteNotiKey: String = "favoriteNoti\(cPrimarykey)"
+// 退出登录
+public let cQuitSuccesssNotiKey: String = "quitSuccesssNoti\(cPrimarykey)"
+// 登录成功
+public let cLoginSuccesssNotiKey: String = "loginSuccesssNoti\(cPrimarykey)"
+// 更新用户数据
+public let cUpdateInfoNotiKey: String = "updateInfoNoti\(cPrimarykey)"
+// 是否显示手机登录 1-显示 0-不显示
+public let cNeedLoginKey: String = "needLogin\(cPrimarykey)"
+// 发布视频成功通知
+public let cPublishSuccessKey: String = "publishSuccess\(cPrimarykey)"
+// 发布卡点视频成功通知
+public let cPublishStuckPointSuccessKey: String = "publishStuckPointSuccess\(cPrimarykey)"
+public let cUploadSuccessKey: String = "uploadSuccess\(cPrimarykey)"
+/// 更新视频成功
+public let cUpdateVideoSuccessKey: String = "updateVideoSuccess\(cPrimarykey)"
+/// 绑定手机号成功的通知
+public let cBandingPhoneSuccessKey: String = "bandingPhoneSuccessKey\(cPrimarykey)"
+/// 图库添加图片的通知
+public let cSelectedImageSuccessKey: String = "selectedImageSuccess\(cPrimarykey)"
+/// 删除视频的通知
+public let cDeleteVideoSuccessKey: String = "deleteVideoSuccess\(cPrimarykey)"
+/// 导出背景音乐成功的通知
+public let cExportBGMAudioSuccessKey: String = "exportBGMxAudioSuccess\(cPrimarykey)"
+/// 下载资源成功/失败的key
+public let cDownloadMatrialSuccessKey: String = "downloadMatrialSuccess\(cPrimarykey)"
+/// 素材处理成功key
+public let cVideoDealWithSuccessKey: String = "videoDealWithSuccess\(cPrimarykey)"
+
+/// 批量下载资源成功/失败的key
+public let cBatchDownloadMatrialSuccessKey: String = "batchDownloadMatrialSuccess\(cPrimarykey)"
+/// 批量上传资源成功/失败的key
+public let cBatchUploadMatrialSuccessKey: String = "batchUploadMatrialSuccessKey\(cPrimarykey)"
+/// 再创作成功的key
+public let cReCreateVideoSuccessKey: String = "reCreateVideoSuccess\(cPrimarykey)"
+/// 收藏/取消背景音乐的通知
+public let cFavoriteBgmKey: String = "favoriteBgm\(cPrimarykey)"
+/// 保存素材成功通知
+public let cSaveMaterialSuccessKey: String = "saveMaterialSuccess\(cPrimarykey)"
+/// 音频素材转换成文字成功
+public let cAudioTransferSubTitleSuccessKey: String = "audioTransferSubTitleSuccess\(cPrimarykey)"
+/// 无网
+public let cLostNet: String = "lostNet\(cPrimarykey)"
+// 取消视频发布引导弹框
+public let cDismissVideoMakeGuideKey: String = "dismissVideoMakeGuide\(cPrimarykey)"
+// 点击完成或发布完成跳转
+public let cFinishedPublishedNotiKey: String = "finishedPublishedNotiKey\(cPrimarykey)"
+
+// oss 上传文件进度 add by ak
+public let cOSSUploadFileProgress: String = "OSSUploadFileProgress\(cPrimarykey)"
+
+// 渠道
+public let channelID = "AppStore"
+// 推送渠道
+public let cPushChannel = "APPLE_TYPE"
+
+// MARK: 账号
+
+// 友盟账号
+public let cUMAppkey: String = "5ee4a806978eea081640dfe4"
+// bugly账号
+public let cBuglyAppkey: String = "39065c15d0"
+// 阿里反馈账号
+public let cAliFeedbackAppkey: String = "23591190"
+// 阿里反馈AppSecret
+public let cAliFeedbackAppSecret: String = "3b831b2068aef36607886a3912ef4fbb"
+// 百度定位key
+public let cBMKLocationAppkey: String = "buprQzBBojKbvDvED8Xix7xHA91ShAnL"
+// 分享小程序ID
+public let cShareWeappRawId: String = "gh_ecd1ea0b84cf"

+ 180 - 0
BFCommonKit/Classes/BFUtility/PQCreateEmptyWAV.swift

@@ -0,0 +1,180 @@
+//
+//  PQCreateEmptyWAV.swift
+//  PQSpeed
+//
+//  Created by ak on 2020/9/16.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//  功能: 生成指定长度的空 WAV 文件
+/*
+    e.g.
+    let tool = LYEmptyWAV(sampleRate: 44100,
+                                channel: 1,
+                                duration: 20,
+                                bit: 16);
+
+          var documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! as String
+          documentPath.append("/test.wav")
+          tool.createEmptyWAVFile(url: URL(fileURLWithPath: documentPath))
+
+     600 s  622028807.468433  622028844.650877
+ */
+
+import UIKit
+
+import AudioToolbox
+import Foundation
+public class PQCreateEmptyWAV {
+    // 采样率
+    private var mSampleRate: Double = 44100
+
+    // 声道
+    private var mChannel: Double = 1
+    // 时长
+    private var mduration: Double = 5.0
+    // 采样位数
+    private var mbit: Double = 16
+    public init(sampleRate: Double = 44100, channel: Double = 1, duration: Double, bit: Double = 16) {
+        mSampleRate = sampleRate
+        mChannel = channel
+        mduration = duration
+        mbit = bit
+        if mduration == 0 {
+            BFLog(message: "时长为0??")
+        }
+        BFLog(message: "mSampleRate is\(mSampleRate) mChannel is \(mChannel) mduration is:\(mduration) mbit is \(mbit)")
+    }
+
+    public  func createEmptyWAVFile(url: URL, completeHander: @escaping (_ fileURL: URL?) -> Void) {
+        DispatchQueue.global().async {
+            // 数字音频文件大小(Byte) = 采样频率(Hz)× 采样时长(S)×(采样位数 / 8)× 声道数(单声道为1,立体声为2)
+//        BFLog(message: "createEmptyWAVFile 1")
+            let size = Int64(self.mSampleRate * self.mduration * (self.mbit / 8) * self.mChannel)
+
+            let bufer = Array(repeating: 0, count: Int(size))
+//        BFLog(message: "createEmptyWAVFile 2 \(size)")
+            let data = Data(bytes: bufer, count: Int(size))
+            let totalAudioLen = size
+            let totalDataLen = totalAudioLen + 44
+//        BFLog(message: "createEmptyWAVFile 3")
+            self.writewaveFileHeader(output: url,
+                                     totalAudioLen: totalAudioLen,
+                                     totalDataLen: totalDataLen,
+                                     longSampleRate: Int64(self.mSampleRate),
+                                     channels: Int(self.mChannel),
+                                     byteRate: size,
+                                     audioData: data)
+
+            DispatchQueue.main.async {
+                completeHander(url)
+            }
+        }
+    }
+    
+    public  func createEmptyWAVFile(url: URL) {
+ 
+            // 数字音频文件大小(Byte) = 采样频率(Hz)× 采样时长(S)×(采样位数 / 8)× 声道数(单声道为1,立体声为2)
+//        BFLog(message: "createEmptyWAVFile 1")
+            let size = Int64(self.mSampleRate * self.mduration * (self.mbit / 8) * self.mChannel)
+
+            let bufer = Array(repeating: 0, count: Int(size))
+//        BFLog(message: "createEmptyWAVFile 2 \(size)")
+            let data = Data(bytes: bufer, count: Int(size))
+            let totalAudioLen = size
+            let totalDataLen = totalAudioLen + 44
+//        BFLog(message: "createEmptyWAVFile 3")
+            self.writewaveFileHeader(output: url,
+                                     totalAudioLen: totalAudioLen,
+                                     totalDataLen: totalDataLen,
+                                     longSampleRate: Int64(self.mSampleRate),
+                                     channels: Int(self.mChannel),
+                                     byteRate: size,
+                                     audioData: data)
+
+        
+    }
+
+
+    public  func writewaveFileHeader(output: URL, totalAudioLen: Int64,
+                             totalDataLen: Int64,
+                             longSampleRate: Int64,
+                             channels: Int,
+                             byteRate: Int64,
+                             audioData: Data)
+    {
+        BFLog(message: "createEmptyWAVFile 4")
+        var header: [UInt8] = Array(repeating: 0, count: 44)
+
+        // RIFF/WAVE header
+        header[0] = UInt8(ascii: "R")
+        header[1] = UInt8(ascii: "I")
+        header[2] = UInt8(ascii: "F")
+        header[3] = UInt8(ascii: "F")
+        header[4] = (UInt8)(totalDataLen & 0xFF)
+        header[5] = (UInt8)((totalDataLen >> 8) & 0xFF)
+        header[6] = (UInt8)((totalDataLen >> 16) & 0xFF)
+        header[7] = (UInt8)((totalDataLen >> 24) & 0xFF)
+
+        // WAVE
+        header[8] = UInt8(ascii: "W")
+        header[9] = UInt8(ascii: "A")
+        header[10] = UInt8(ascii: "V")
+        header[11] = UInt8(ascii: "E")
+
+        // 'fmt' chunk
+        header[12] = UInt8(ascii: "f")
+        header[13] = UInt8(ascii: "m")
+        header[14] = UInt8(ascii: "t")
+        header[15] = UInt8(ascii: " ")
+
+        // 4 bytes: size of 'fmt ' chunk
+        header[16] = 16
+        header[17] = 0
+        header[18] = 0
+        header[19] = 0
+
+        // format = 1
+        header[20] = 1
+        header[21] = 0
+        header[22] = UInt8(channels)
+        header[23] = 0
+
+        header[24] = (UInt8)(longSampleRate & 0xFF)
+        header[25] = (UInt8)((longSampleRate >> 8) & 0xFF)
+        header[26] = (UInt8)((longSampleRate >> 16) & 0xFF)
+        header[27] = (UInt8)((longSampleRate >> 24) & 0xFF)
+
+        header[28] = (UInt8)(byteRate & 0xFF)
+        header[29] = (UInt8)((byteRate >> 8) & 0xFF)
+        header[30] = (UInt8)((byteRate >> 16) & 0xFF)
+        header[31] = (UInt8)((byteRate >> 24) & 0xFF)
+
+        // block align
+        header[32] = UInt8(2 * 16 / 8)
+        header[33] = 0
+
+        // bits per sample
+        header[34] = 16
+        header[35] = 0
+
+        // data
+        header[36] = UInt8(ascii: "d")
+        header[37] = UInt8(ascii: "a")
+        header[38] = UInt8(ascii: "t")
+        header[39] = UInt8(ascii: "a")
+        header[40] = UInt8(totalAudioLen & 0xFF)
+        header[41] = UInt8((totalAudioLen >> 8) & 0xFF)
+        header[42] = UInt8((totalAudioLen >> 16) & 0xFF)
+        header[43] = UInt8((totalAudioLen >> 24) & 0xFF)
+
+//        guard let writeHandler = try? FileHandle(forWritingTo: output) else { return }
+        var data = Data(header)
+        data.append(audioData)
+        BFLog(message: "createEmptyWAVFile 5")
+        do {
+            try data.write(to: output, options: .atomicWrite)
+            BFLog(message: "createEmptyWAVFile 6 \(data.count)")
+        } catch {
+            BFLog(message: " write file error \(error)")
+        }
+    }
+}

+ 526 - 0
BFCommonKit/Classes/BFUtility/PQLZStringUtil.swift

@@ -0,0 +1,526 @@
+import Foundation
+
+private let keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
+private let keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"
+
+private let getCharFromInt: (Int) -> Character = { a in
+    if let scalar = Unicode.Scalar(a) {
+        return Character(scalar)
+    } else {
+        return Character(" ")
+    }
+}
+
+private var baseReserveDict = [String: [Character: Int]]()
+
+private typealias GetCharFromInt<T> = (Int) -> T
+private typealias GetNextValue = (Int) -> Int
+
+private typealias DecompressData = (value: Int, position: Int, index: Int)
+private typealias CompressContext<T> = (dict: [String: Int], dictCreate: [String: Bool], data: T, val: Int, position: Int) where T: RangeReplaceableCollection
+
+private func getBaseValue(alphabet: String, char: Character) -> Int {
+    if let charcter = baseReserveDict[alphabet]?[char] {
+        return charcter
+    } else {
+        baseReserveDict[alphabet] = [Character: Int]()
+        for (index, char) in alphabet.enumerated() {
+            baseReserveDict[alphabet]![char] = index
+        }
+
+        return baseReserveDict[alphabet]![char]!
+    }
+}
+
+public func compressToBase64(input: String) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    let result = _compress(input: input, bitPerChar: 6, charFromInt: { a in String(keyStrBase64[a]) })
+    switch result.count % 4 {
+    case 0:
+        return result
+    case 1:
+        return result + "==="
+    case 2:
+        return result + "=="
+    case 3:
+        return result + "="
+    default:
+        return ""
+    }
+}
+
+public func decompressFromBase64(input: String) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    return _decompress(length: input.count, resetValue: 32, nextValue: { a in getBaseValue(alphabet: keyStrBase64, char: input[a]) })
+}
+
+public func compressToUTF16(input: String) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    return _compress(input: input, bitPerChar: 15, charFromInt: { a in String(getCharFromInt(a + 32)) }) + " "
+}
+
+public func decompressFromUTF16(input: String) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    return _decompress(length: input.utf16.count, resetValue: 16384, nextValue: { i in Int(input.utf16[i]) - 32 })
+}
+
+public func compressToUInt8Array(input: String) -> [UInt8] {
+    let compressed: Data = compress(input: input)
+    var buffer = [UInt8](repeating: 0, count: compressed.count)
+
+    for i in 0..<(compressed.count / 2) {
+        buffer[i * 2] = compressed[i * 2 + 1]
+        buffer[i * 2 + 1] = compressed[i * 2]
+    }
+
+    return buffer
+}
+
+public func decompressFromUInt8Array(input: [UInt8]) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    return _decompress(length: input.count / 2, resetValue: 32768, nextValue: { i in
+        let lower = Int(input[i * 2]) * 256
+        let upper = Int(input[i * 2 + 1])
+        return upper + lower
+    })
+}
+
+public func compressToEncodedURIComponent(input: String) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    return _compress(input: input, bitPerChar: 6, charFromInt: { i in String(keyStrUriSafe[i]) })
+}
+
+public func decompressFromEncodedURIComponent(input: String) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    let replaced = input.replacingOccurrences(of: " ", with: "+")
+
+    return _decompress(length: replaced.count, resetValue: 32, nextValue: { a in getBaseValue(alphabet: keyStrUriSafe, char: input[a]) })
+}
+
+public func compress(input: String) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    return _compress(input: input, bitPerChar: 16, charFromInt: { a in String(getCharFromInt(a)) })
+}
+
+public func compress(input: String) -> Data {
+    guard !input.isEmpty else {
+        return Data()
+    }
+
+    return _compress(input: input, bitPerChar: 16, charFromInt: { a in
+        Data(bytes: [UInt8(a % 256), UInt8(a >> 8)])
+    })
+}
+
+// TODO: Change Generics
+private func _compress<T: RangeReplaceableCollection>(input: String, bitPerChar: Int, charFromInt: GetCharFromInt<T>) -> T {
+    guard !input.isEmpty else {
+        return T()
+    }
+
+    var value = 0
+    var wc = ""
+    var w = ""
+    var enlargeIn = 2
+    var dictSize = 3
+    var numBits = 2
+    var context = (dict: [String: Int](), dictCreate: [String: Bool](), data: T(), val: 0, position: 0)
+
+    for c in input {
+        let s = String(c)
+
+        if context.dict.index(forKey: s) == nil {
+            context.dict[s] = dictSize
+            context.dictCreate[s] = true
+            dictSize += 1
+        }
+
+        wc = w + s
+
+        if context.dict[wc] != nil {
+            w = wc
+        } else {
+            if context.dictCreate.index(forKey: w) != nil {
+                if let scalar = w.unicodeScalars.first, scalar.value < 256 {
+                    for _ in 0..<numBits {
+                        context.val <<= 1
+
+                        if context.position == bitPerChar - 1 {
+                            context.position = 0
+                            context.data += charFromInt(context.val)
+                            context.val = 0
+                        } else {
+                            context.position += 1
+                        }
+                    }
+
+                    value = Int(w.unicodeScalars.first!.value)
+
+                    for _ in 0..<8 {
+                        context.val = (context.val << 1) | (value & 1)
+
+                        if context.position == bitPerChar - 1 {
+                            context.position = 0
+                            context.data += charFromInt(context.val)
+                            context.val = 0
+                        } else {
+                            context.position += 1
+                        }
+
+                        value >>= 1
+                    }
+                } else {
+                    value = 1
+
+                    for _ in 0..<numBits {
+                        context.val = (context.val << 1) | value
+
+                        if context.position == bitPerChar - 1 {
+                            context.position = 0
+                            context.data += charFromInt(context.val)
+                            context.val = 0
+                        } else {
+                            context.position += 1
+                        }
+                        value = 0
+                    }
+
+                    value = Int(w.unicodeScalars.first!.value)
+
+                    for _ in 0..<16 {
+                        context.val = (context.val << 1) | (value & 1)
+
+                        if context.position == bitPerChar - 1 {
+                            context.position = 0
+                            context.data += charFromInt(context.val)
+                            context.val = 0
+                        } else {
+                            context.position += 1
+                        }
+
+                        value >>= 1
+                    }
+                }
+
+                enlargeIn -= 1
+
+                if enlargeIn == 0 {
+                    enlargeIn = 2 << (numBits - 1)
+                    numBits += 1
+                }
+
+                context.dictCreate.removeValue(forKey: w)
+            } else {
+                value = context.dict[w]!
+
+                for _ in 0..<numBits {
+                    context.val = (context.val << 1) | (value & 1)
+
+                    if context.position == bitPerChar - 1 {
+                        context.position = 0
+                        context.data += charFromInt(context.val)
+                        context.val = 0
+                    } else {
+                        context.position += 1
+                    }
+
+                    value >>= 1
+                }
+            }
+            enlargeIn -= 1
+
+            if enlargeIn == 0 {
+                enlargeIn = 2 << (numBits - 1)
+                numBits += 1
+            }
+            context.dict[wc] = dictSize
+            dictSize += 1
+            w = s
+        }
+    }
+
+    if w != "" {
+        if context.dictCreate.index(forKey: w) != nil {
+            if let scalar = w.unicodeScalars.first, scalar.value < 256 {
+                for _ in 0..<numBits {
+                    context.val <<= 1
+
+                    if context.position == bitPerChar - 1 {
+                        context.position = 0
+                        context.data += charFromInt(context.val)
+                        context.val = 0
+                    } else {
+                        context.position += 1
+                    }
+                }
+
+                value = Int(w.unicodeScalars.first!.value)
+
+                for _ in 0..<8 {
+                    context.val = (context.val << 1) | (value & 1)
+
+                    if context.position == bitPerChar - 1 {
+                        context.position = 0
+                        context.data += charFromInt(context.val)
+                        context.val = 0
+                    } else {
+                        context.position += 1
+                    }
+
+                    value >>= 1
+                }
+            } else {
+                value = 1
+
+                for _ in 0..<numBits {
+                    context.val = (context.val << 1) | value
+
+                    if context.position == bitPerChar - 1 {
+                        context.position = 0
+                        context.data += charFromInt(context.val)
+                        context.val = 0
+                    } else {
+                        context.position += 1
+                    }
+
+                    value = 0
+                }
+
+                value = Int(w.unicodeScalars.first!.value)
+
+                for _ in 0..<16 {
+                    context.val = (context.val << 1) | (value & 1)
+
+                    if context.position == bitPerChar - 1 {
+                        context.position = 0
+                        context.data += charFromInt(context.val)
+                        context.val = 0
+                    } else {
+                        context.position += 1
+                    }
+
+                    value >>= 1
+                }
+            }
+
+            enlargeIn -= 1
+
+            if enlargeIn == 0 {
+                enlargeIn = 2 << (numBits - 1)
+                numBits += 1
+            }
+
+            context.dictCreate.removeValue(forKey: w)
+        } else {
+            value = context.dict[w]!
+
+            for _ in 0..<numBits {
+                context.val = (context.val << 1) | (value & 1)
+
+                if context.position == bitPerChar - 1 {
+                    context.position = 0
+                    context.data += charFromInt(context.val)
+                    context.val = 0
+                } else {
+                    context.position += 1
+                }
+
+                value >>= 1
+            }
+        }
+
+        enlargeIn -= 1
+
+        if enlargeIn == 0 {
+            enlargeIn = 2 << (numBits - 1)
+            numBits += 1
+        }
+    }
+    value = 2
+
+    for _ in 0..<numBits {
+        context.val = (context.val << 1) | (value & 1)
+
+        if context.position == bitPerChar - 1 {
+            context.position = 0
+            context.data += charFromInt(context.val)
+            context.val = 0
+        } else {
+            context.position += 1
+        }
+
+        value >>= 1
+    }
+
+    while true {
+        context.val <<= 1
+
+        if context.position == bitPerChar - 1 {
+            context.data += charFromInt(context.val)
+            break
+        } else {
+            context.position += 1
+        }
+    }
+
+    return context.data
+}
+
+public func decompress(input: String) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    return _decompress(length: input.utf16.count, resetValue: 32768, nextValue: { i in Int(input.utf16[i]) })
+}
+
+public func decompress(input: Data) -> String {
+    guard !input.isEmpty else {
+        return ""
+    }
+
+    return _decompress(length: input.count / 2, resetValue: 32768, nextValue: { i in
+        let lower = Int(input[i * 2])
+        let upper = Int(input[i * 2 + 1]) * 256
+        return upper + lower
+    })
+}
+
+private func _decompress(length: Int, resetValue: Int, nextValue: @escaping GetNextValue) -> String {
+    var dict: [Int: String] = [0: "\u{0}", 1: "\u{1}", 2: "\u{2}"]
+    var next = 0
+    var enlargeIn = 4
+    var dictSize = 4
+    var numBits = 3
+    var bits = 0
+    var c = 0
+    var entry = ""
+    var w = ""
+    var result = ""
+    var data = (value: nextValue(0), position: resetValue, index: 1)
+
+    func _slide(data: inout DecompressData, maxpower: Int) -> Int {
+        var bits = 0
+        var power = 1
+
+        while power != maxpower {
+            let resb = data.value & data.position
+            data.position >>= 1
+
+            if data.position == 0 {
+                data.position = resetValue
+                data.value = nextValue(data.index)
+                data.index += 1
+            }
+
+            bits |= (resb > 0 ? 1 : 0) * power
+            power <<= 1
+        }
+
+        return bits
+    }
+
+    bits = _slide(data: &data, maxpower: 2 << 1)
+    next = bits
+
+    if next == 0 {
+        bits = _slide(data: &data, maxpower: 2 << 7)
+        c = bits
+    } else if next == 1 {
+        bits = _slide(data: &data, maxpower: 2 << 15)
+        c = bits
+    } else if next == 2 {
+        return ""
+    }
+
+    w = String(Unicode.Scalar(c)!)
+    dict[3] = w
+    result += w
+
+    while true {
+        guard data.index <= length else {
+            return ""
+        }
+
+        bits = _slide(data: &data, maxpower: 2 << (numBits - 1))
+        c = bits
+
+        if c == 0 {
+            bits = _slide(data: &data, maxpower: 2 << 7)
+            dict[dictSize] = String(getCharFromInt(bits))
+            dictSize += 1
+            c = dictSize - 1
+            enlargeIn -= 1
+        } else if c == 1 {
+            bits = _slide(data: &data, maxpower: 2 << 15)
+            dict[dictSize] = String(getCharFromInt(bits))
+            dictSize += 1
+            c = dictSize - 1
+            enlargeIn -= 1
+        } else if c == 2 {
+            return result
+        }
+
+        if enlargeIn == 0 {
+            enlargeIn = 2 << (numBits - 1)
+            numBits += 1
+        }
+
+        if let e = dict[c] {
+            entry = e
+        } else {
+            if c == dictSize {
+                entry = w + String(w[0])
+            } else {
+                return ""
+            }
+        }
+
+        result += entry
+        dict[dictSize] = w + String(entry[0])
+        dictSize += 1
+        enlargeIn -= 1
+        w = entry
+
+        if enlargeIn == 0 {
+            enlargeIn = 2 << (numBits - 1)
+            numBits += 1
+        }
+    }
+}
+
+extension String {
+    subscript(pos: Int) -> Character {
+        return self[String.Index(encodedOffset: pos)]
+    }
+}
+
+extension String.UTF16View {
+    subscript(pos: Int) -> Unicode.UTF16.CodeUnit {
+        return self[String.UTF16View.Index(encodedOffset: pos)]
+    }
+}

+ 780 - 0
BFCommonKit/Classes/BFUtility/PQPHAssetVideoParaseUtil.swift

@@ -0,0 +1,780 @@
+//
+//  PQPHAssetVideoParaseUtil.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/3.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import CoreServices
+import Photos
+import UIKit
+
+var currentExportSession: AVAssetExportSession?
+open class PQPHAssetVideoParaseUtil: NSObject {
+    static var imagesOptions: PHImageRequestOptions = {
+        let imagesOptions = PHImageRequestOptions()
+        imagesOptions.isSynchronous = false
+        imagesOptions.deliveryMode = .fastFormat
+        imagesOptions.resizeMode = .fast
+        imagesOptions.version = .current
+        return imagesOptions
+    }()
+
+    static var singleImageOptions: PHImageRequestOptions = {
+        let singleImageOptions = PHImageRequestOptions()
+        singleImageOptions.isSynchronous = true
+        singleImageOptions.isNetworkAccessAllowed = true
+        singleImageOptions.deliveryMode = .highQualityFormat
+        singleImageOptions.resizeMode = .none
+        singleImageOptions.version = .current
+        return singleImageOptions
+    }()
+
+    static var videoRequestOptions: PHVideoRequestOptions = {
+        let videoRequestOptions = PHVideoRequestOptions()
+        // 解决慢动作视频返回AVComposition而不是AVURLAsset
+//        videoRequestOptions.version = .original
+        videoRequestOptions.version = .current
+        // 下载iCloud视频
+        videoRequestOptions.isNetworkAccessAllowed = true
+        videoRequestOptions.deliveryMode = .mediumQualityFormat
+        return videoRequestOptions
+    }()
+
+    /// PHAsset解析为AVPlayerItem
+    /// - Parameters:
+    ///   - asset: <#asset description#>
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func parasToAVPlayerItem(phAsset: PHAsset, isHighQuality: Bool = false, resultHandler: @escaping (AVPlayerItem?, Float64, [AnyHashable: Any]?) -> Void) {
+        PHImageManager().requestPlayerItem(forVideo: phAsset, options: videoRequestOptions) { playerItem, info in
+            if isHighQuality, (playerItem?.asset as? AVURLAsset)?.url.absoluteString.components(separatedBy: "/").last?.contains(".medium.") ?? false {
+                let tempVideoOptions = PHVideoRequestOptions()
+                tempVideoOptions.version = .original
+                // 下载iCloud视频
+                tempVideoOptions.isNetworkAccessAllowed = true
+                tempVideoOptions.deliveryMode = .highQualityFormat
+                tempVideoOptions.progressHandler = { progress, error, pointer, info in
+                    BFLog(message: "导出playerItem-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
+                }
+                PHImageManager().requestPlayerItem(forVideo: phAsset, options: tempVideoOptions) { playerItem, info in
+                    let size = try! (playerItem?.asset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
+                    BFLog(message: "size = \(String(describing: size))")
+                    resultHandler(playerItem, Float64(size?.fileSize ?? 0), info)
+                }
+            } else {
+                let size = try! (playerItem?.asset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
+                BFLog(message: "size = \(String(describing: size))")
+                resultHandler(playerItem, Float64(size?.fileSize ?? 0), info)
+            }
+        }
+    }
+
+    /// PHAsset解析为AVAsset
+    /// - Parameters:
+    ///   - asset: <#asset description#>
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func parasToAVAsset(phAsset: PHAsset, isHighQuality: Bool = true, resultHandler: @escaping (AVAsset?, Int, AVAudioMix?, [AnyHashable: Any]?) -> Void) {
+        PHImageManager.default().requestAVAsset(forVideo: phAsset, options: videoRequestOptions) { avAsset, audioMix, info in
+            if isHighQuality, (avAsset as? AVURLAsset)?.url.absoluteString.components(separatedBy: "/").last?.contains(".medium.") ?? false {
+                let tempVideoOptions = PHVideoRequestOptions()
+                tempVideoOptions.version = .original
+                // 下载iCloud视频
+                tempVideoOptions.isNetworkAccessAllowed = true
+                tempVideoOptions.deliveryMode = .highQualityFormat
+                tempVideoOptions.progressHandler = { progress, error, pointer, info in
+                    BFLog(message: "导出playerItem-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
+                }
+                PHImageManager.default().requestAVAsset(forVideo: phAsset, options: tempVideoOptions) { tempAvAsset, tempAudioMix, tempInfo in
+                    let size = try! (tempAvAsset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
+                    BFLog(message: "size = \(String(describing: size))")
+                    resultHandler(tempAvAsset, size?.fileSize ?? 0, tempAudioMix, tempInfo)
+                }
+            } else {
+                let size = try! (avAsset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
+                resultHandler(avAsset, size?.fileSize ?? 0, audioMix, info)
+                BFLog(message: "size = \(String(describing: size))")
+            }
+        }
+    }
+
+    /// PHAsset 转码为.mp4保存本地
+    /// - Parameters:
+    ///   - phAsset: <#phAsset description#>
+    ///   - isAdjustRotationAngle: 是否调整旋转角度
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func exportPHAssetToMP4(phAsset: PHAsset, isAdjustRotationAngle: Bool = true, isCancelCurrentExport: Bool = false, deliveryMode: PHVideoRequestOptionsDeliveryMode? = .automatic, resultHandler: @escaping (_ phAsset: PHAsset, _ aVAsset: AVAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) {
+        BFLog(message: "导出相册视频-开始导出:phAsset = \(phAsset)")
+        if isCancelCurrentExport {
+            currentExportSession?.cancelExport()
+        }
+        PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: phAsset) { avAsset, fileSize, _, _ in
+            if avAsset is AVURLAsset {
+                // 创建目录
+                createDirectory(path: photoLibraryDirectory)
+                let fileName = (avAsset as! AVURLAsset).url.absoluteString
+                let filePath = photoLibraryDirectory + fileName.md5.md5 + ".mp4"
+                let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+                if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+                    BFLog(message: "导出相册视频-已经导出完成:\(filePath)")
+                    DispatchQueue.main.async {
+                        resultHandler(phAsset, avAsset, filePath, nil)
+                    }
+                } else {
+//                    let tempExportSession = PQSingletoMemoryUtil.shared.allExportSession[phAsset]
+                    let tempExportSession : AVAssetExportSession? = nil
+                    if tempExportSession != nil {
+                        BFLog(message: "导出相册视频-正在导出")
+                        return
+                    }
+                    BFLog(message: "导出相册视频-未导出视频过,开始导出:phAsset = \(phAsset)")
+                    // 删除以创建地址
+                    if FileManager.default.fileExists(atPath: filePath) {
+                        do {
+                            try FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
+                        } catch {
+                            BFLog(message: "导出相册视频-error == \(error)")
+                        }
+                    }
+                    let requestOptions = PHVideoRequestOptions()
+                    // 解决慢动作视频返回AVComposition而不是AVURLAsset
+                    //        videoRequestOptions.version = .original
+                    requestOptions.version = .current
+                    // 下载iCloud视频
+                    requestOptions.isNetworkAccessAllowed = false
+                    requestOptions.progressHandler = { progress, error, pointer, info in
+                        BFLog(message: "导出相册视频-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
+                    }
+                    requestOptions.deliveryMode = deliveryMode ?? .automatic
+                    PHImageManager.default().requestExportSession(forVideo: phAsset, options: requestOptions, exportPreset: (deliveryMode == .automatic || deliveryMode == .mediumQualityFormat) ? AVAssetExportPresetMediumQuality : (deliveryMode == .highQualityFormat ? AVAssetExportPresetHighestQuality : AVAssetExportPresetLowQuality), resultHandler: { avAssetExportSession, _ in
+                        BFLog(message: "导出相册视频-请求到导出 avAssetExportSession = \(String(describing: avAssetExportSession))")
+                        currentExportSession = avAssetExportSession
+                        if avAssetExportSession != nil {
+//                            PQSingletoMemoryUtil.shared.allExportSession[phAsset] = avAssetExportSession!
+                        }
+                        avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
+                        avAssetExportSession?.shouldOptimizeForNetworkUse = true
+                        avAssetExportSession?.outputFileType = .mp4
+                        if isAdjustRotationAngle {
+                            let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: avAsset!)
+                            // mdf by ak 统一导出的视频为30FPS
+                            var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
+                            var mixedTransform: CGAffineTransform = CGAffineTransform()
+                            let videoComposition = AVMutableVideoComposition()
+                            videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
+                            let tracks = avAsset?.tracks(withMediaType: .video)
+                            let firstTrack = tracks?.first
+
+                            videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+
+                            mixedTransform = centerTranslate.rotated(by: 0)
+
+                            if rotationAngle == 90 {
+                                centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
+                                mixedTransform = centerTranslate.rotated(by: .pi / 2)
+                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+                            } else if rotationAngle == 180 {
+                                centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
+                                mixedTransform = centerTranslate.rotated(by: .pi)
+                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+                            } else if rotationAngle == 270 {
+                                centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
+                                mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
+                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+                            }
+                            let roateInstruction = AVMutableVideoCompositionInstruction()
+                            roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: avAsset?.duration ?? CMTime.zero)
+                            if firstTrack != nil {
+                                let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
+                                layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
+                                roateInstruction.layerInstructions = [layRoateInstruction]
+                                videoComposition.instructions = [roateInstruction]
+                                avAssetExportSession?.videoComposition = videoComposition
+                            } else {
+                                BFLog(message: "firstTrack is error !!!")
+                            }
+                        }
+                        avAssetExportSession?.exportAsynchronously(completionHandler: {
+                            BFLog(message: "导出相册视频-progress = \(avAssetExportSession?.progress ?? 0),status = \(String(describing: avAssetExportSession?.status))")
+                            switch avAssetExportSession?.status {
+                            case .unknown:
+                                DispatchQueue.main.async {
+                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                                }
+                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+                                BFLog(message: "导出相册视频-发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                            case .waiting:
+                                BFLog(message: "导出相册视频-等待导出mp4:\(filePath)")
+                            case .exporting:
+                                BFLog(message: "导出相册视频-导出相册视频中...:\(filePath)")
+                            case .completed:
+                                DispatchQueue.main.async {
+                                    resultHandler(phAsset, avAsset, filePath, nil)
+                                }
+                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+                                BFLog(message: "导出相册视频-导出完成:\(filePath)")
+                            case .failed:
+                                DispatchQueue.main.async {
+                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                                }
+                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+                                BFLog(message: "导出相册视频-导出失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                            case .cancelled:
+                                DispatchQueue.main.async {
+                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                                }
+                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+                                BFLog(message: "导出相册视频-取消导出:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                            default:
+                                break
+                            }
+                        })
+                    })
+                }
+            } else if avAsset is AVComposition {
+                BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))")
+                let assetResources = PHAssetResource.assetResources(for: phAsset)
+                var resource: PHAssetResource?
+                for assetRes in assetResources {
+                    if assetRes.type == .video || assetRes.type == .pairedVideo {
+                        resource = assetRes
+                    }
+                }
+                if phAsset.mediaType == .video, resource != nil {
+                    let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "")
+                    let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+                    let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+                    if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+                        DispatchQueue.main.async {
+                            resultHandler(phAsset, avAsset, filePath, nil)
+                        }
+                    } else {
+                        PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in
+                            DispatchQueue.main.async {
+                                resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil)
+                            }
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        resultHandler(phAsset, avAsset, nil, nil)
+                    }
+                }
+            } else {
+                DispatchQueue.main.async {
+                    resultHandler(phAsset, avAsset, nil, nil)
+                }
+            }
+        }
+    }
+
+    /// PHAsset 转码为.mp4保存本地
+    /// - Parameters:
+    ///   - phAsset: <#phAsset description#>
+    ///   - isAdjustRotationAngle: 是否调整旋转角度
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func writePHAssetDataToMP4(phAsset: PHAsset, isAdjustRotationAngle _: Bool = true, isCancelCurrentExport: Bool = false, deliveryMode _: PHVideoRequestOptionsDeliveryMode? = .automatic, resultHandler: @escaping (_ phAsset: PHAsset, _ aVAsset: AVAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) {
+        BFLog(message: "导出相册视频-开始导出:phAsset = \(phAsset)")
+        if isCancelCurrentExport {
+            currentExportSession?.cancelExport()
+        }
+        PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: phAsset) { avAsset, fileSize, _, _ in
+            if avAsset is AVURLAsset {
+                // 创建目录
+                createDirectory(path: photoLibraryDirectory)
+                let fileName = (avAsset as! AVURLAsset).url.absoluteString
+                let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+                let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+                if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+                    BFLog(message: "导出相册视频-已经导出完成:\(filePath)")
+                    DispatchQueue.main.async {
+                        resultHandler(phAsset, avAsset, filePath, nil)
+                    }
+                } else {
+//                    let tempExportSession = PQSingletoMemoryUtil.shared.allExportSession[phAsset]
+                    let tempExportSession : AVAssetExportSession? = nil
+                    if tempExportSession != nil {
+                        BFLog(message: "导出相册视频-正在导出")
+                        return
+                    }
+                    BFLog(message: "导出相册视频-未导出视频过,开始导出:phAsset = \(phAsset)")
+                    // 删除以创建地址
+                    if FileManager.default.fileExists(atPath: filePath) {
+                        do {
+                            try FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
+                        } catch {
+                            BFLog(message: "导出相册视频-error == \(error)")
+                        }
+                    }
+                    do {
+                        try FileManager.default.copyItem(at: (avAsset as! AVURLAsset).url, to: URL(fileURLWithPath: filePath))
+                    } catch {
+                        BFLog(message: "导出相册视频-error == \(error)")
+                    }
+
+//                    NSError *error;
+//                            AVURLAsset *avurlasset = (AVURLAsset*)asset;
+//                            NSURL *fileURL = [NSURL fileURLWithPath:savePath];
+//
+//                            if ([[NSFileManager defaultManager] copyItemAtURL:avurlasset.URL toURL:fileURL error:&error]) {
+//                                CBLog(@"保存成功");
+//                                dispatch_async(dispatch_get_main_queue(), ^{
+//                                    if (result) {
+//                                        result(savePath,[savePath lastPathComponent]);
+//                                    }
+//                                });
+//                            }else{
+//                                CBLog(@"error=%@",error);
+//                                [[NSFileManager defaultManager]removeItemAtPath:savePath error:nil];
+//                                dispatch_async(dispatch_get_main_queue(), ^{
+//                                    failure(error.description);
+//                                });
+//                            }
+//                    let requestOptions = PHVideoRequestOptions()
+//                    // 解决慢动作视频返回AVComposition而不是AVURLAsset
+//                    //        videoRequestOptions.version = .original
+//                    requestOptions.version = .current
+//                    // 下载iCloud视频
+//                    requestOptions.isNetworkAccessAllowed = false
+//                    requestOptions.progressHandler = { progress, error, pointer, info in
+//                        BFLog(message: "导出相册视频-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
+//                    }
+//                    requestOptions.deliveryMode = deliveryMode ?? .automatic
+
+//                    PHImageManager.default().requestExportSession(forVideo: phAsset, options: requestOptions, exportPreset: (deliveryMode == .automatic || deliveryMode == .mediumQualityFormat) ? AVAssetExportPreset1920x1080 :(deliveryMode == .highQualityFormat ? AVAssetExportPresetHighestQuality : AVAssetExportPresetLowQuality) , resultHandler: { avAssetExportSession, _ in
+//                        BFLog(message: "导出相册视频-请求到导出 avAssetExportSession = \(avAssetExportSession)")
+//                        currentExportSession = avAssetExportSession
+//                        if avAssetExportSession != nil {
+//                            PQSingletoMemoryUtil.shared.allExportSession[phAsset] = avAssetExportSession!
+//                        }
+//                        avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
+//                        avAssetExportSession?.shouldOptimizeForNetworkUse = true
+//                        avAssetExportSession?.outputFileType = .mp4
+//                        if isAdjustRotationAngle {
+//                            let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: avAsset!)
+//                            // mdf by ak 统一导出的视频为30FPS
+//                            var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
+//                            var mixedTransform: CGAffineTransform = CGAffineTransform()
+//                            let videoComposition = AVMutableVideoComposition()
+//                            videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
+//                            let tracks = avAsset?.tracks(withMediaType: .video)
+//                            let firstTrack = tracks?.first
+//
+//                            videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+//
+//                            mixedTransform = centerTranslate.rotated(by: 0)
+//
+//                            if rotationAngle == 90 {
+//                                centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
+//                                mixedTransform = centerTranslate.rotated(by: .pi / 2)
+//                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+//                            } else if rotationAngle == 180 {
+//                                centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
+//                                mixedTransform = centerTranslate.rotated(by: .pi)
+//                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+//                            } else if rotationAngle == 270 {
+//                                centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
+//                                mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
+//                                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+//                            }
+//                            let roateInstruction = AVMutableVideoCompositionInstruction()
+//                            roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: avAsset?.duration ?? CMTime.zero)
+//                            if firstTrack != nil {
+//                                let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
+//                                layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
+//                                roateInstruction.layerInstructions = [layRoateInstruction]
+//                                videoComposition.instructions = [roateInstruction]
+//                                avAssetExportSession?.videoComposition = videoComposition
+//                            } else {
+//                                BFLog(message: "firstTrack is error !!!")
+//                            }
+//                        }
+//                        avAssetExportSession?.shouldOptimizeForNetworkUse = true
+//                        avAssetExportSession?.exportAsynchronously(completionHandler: {
+//                            BFLog(message: "导出相册视频-progress = \(avAssetExportSession?.progress ?? 0),status = \(String(describing: avAssetExportSession?.status))")
+//                            switch avAssetExportSession?.status {
+//                            case .unknown:
+//                                DispatchQueue.main.async {
+//                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+//                                }
+//                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+//                                BFLog(message: "导出相册视频-发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+//                            case .waiting:
+//                                BFLog(message: "导出相册视频-等待导出mp4:\(filePath)")
+//                            case .exporting:
+//                                BFLog(message: "导出相册视频-导出相册视频中...:\(filePath)")
+//                            case .completed:
+//                                DispatchQueue.main.async {
+//                                    resultHandler(phAsset, avAsset, filePath, nil)
+//                                }
+//                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+//                                BFLog(message: "导出相册视频-导出完成:\(filePath)")
+//                            case .failed:
+//                                DispatchQueue.main.async {
+//                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+//                                }
+//                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+//                                BFLog(message: "导出相册视频-导出失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+//                            case .cancelled:
+//                                DispatchQueue.main.async {
+//                                    resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
+//                                }
+//                                avAssetExportSession?.cancelExport()
+//                                PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
+//                                BFLog(message: "导出相册视频-取消导出:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+//                            default:
+//                                break
+//                            }
+//                        })
+//                    })
+                }
+            } else if avAsset is AVComposition {
+                BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))")
+                let assetResources = PHAssetResource.assetResources(for: phAsset)
+                var resource: PHAssetResource?
+                for assetRes in assetResources {
+                    if assetRes.type == .video || assetRes.type == .pairedVideo {
+                        resource = assetRes
+                    }
+                }
+                if phAsset.mediaType == .video, resource != nil {
+                    let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "")
+                    let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+                    let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+                    if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+                        DispatchQueue.main.async {
+                            resultHandler(phAsset, avAsset, filePath, nil)
+                        }
+                    } else {
+                        PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in
+                            DispatchQueue.main.async {
+                                resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil)
+                            }
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        resultHandler(phAsset, avAsset, nil, nil)
+                    }
+                }
+            } else {
+                DispatchQueue.main.async {
+                    resultHandler(phAsset, avAsset, nil, nil)
+                }
+            }
+        }
+    }
+
+    /// 导出相册视频
+    /// - Parameters:
+    ///   - aVAsset: <#aVAsset description#>
+    ///   - isAdjustRotationAngle: <#isAdjustRotationAngle description#>
+    ///   - resultHandler: <#resultHandler description#>
+    public class func exportAVAssetToMP4(aVAsset: AVURLAsset, isAdjustRotationAngle: Bool = true, resultHandler: @escaping (_ aVAsset: AVURLAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) {
+        currentExportSession?.cancelExport()
+        BFLog(message: "开始导出相册视频:url = \(aVAsset.url.absoluteString)")
+        // 创建目录
+        createDirectory(path: photoLibraryDirectory)
+        let fileName = aVAsset.url.absoluteString
+        let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
+        let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+        let fileSize = try! aVAsset.url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
+        if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
+            DispatchQueue.main.async {
+                resultHandler(aVAsset, filePath, nil)
+            }
+        } else {
+            BFLog(message: "未导出视频过,开始导出:aVAsset = \(aVAsset)")
+            // 删除以创建地址
+            try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
+            let avAssetExportSession = AVAssetExportSession(asset: aVAsset, presetName: AVAssetExportPreset1280x720)
+            currentExportSession = avAssetExportSession
+            avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
+            avAssetExportSession?.shouldOptimizeForNetworkUse = false
+            avAssetExportSession?.outputFileType = .mp4
+            if isAdjustRotationAngle {
+                let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: aVAsset)
+                // mdf by ak 统一导出的视频为30FPS
+                var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
+                var mixedTransform: CGAffineTransform = CGAffineTransform()
+                let videoComposition = AVMutableVideoComposition()
+                videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
+                let tracks = aVAsset.tracks(withMediaType: .video)
+                let firstTrack = tracks.first
+                videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+
+                mixedTransform = centerTranslate.rotated(by: 0)
+
+                if rotationAngle == 90 {
+                    centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
+                    mixedTransform = centerTranslate.rotated(by: .pi / 2)
+                    videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+                } else if rotationAngle == 180 {
+                    centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
+                    mixedTransform = centerTranslate.rotated(by: .pi)
+                    videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
+                } else if rotationAngle == 270 {
+                    centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
+                    mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
+                    videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
+                }
+                let roateInstruction = AVMutableVideoCompositionInstruction()
+                roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: aVAsset.duration)
+                let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
+                layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
+                roateInstruction.layerInstructions = [layRoateInstruction]
+                videoComposition.instructions = [roateInstruction]
+                avAssetExportSession?.videoComposition = videoComposition
+            }
+
+            avAssetExportSession?.shouldOptimizeForNetworkUse = true
+            avAssetExportSession?.exportAsynchronously(completionHandler: {
+                BFLog(message: "导出相册视频progress = \(avAssetExportSession?.progress ?? 0)")
+                switch avAssetExportSession?.status {
+                case .unknown:
+                    DispatchQueue.main.async {
+                        resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                    }
+                    BFLog(message: "导出相册视频发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                case .waiting:
+                    BFLog(message: "等待导出mp4:\(filePath)")
+                case .exporting:
+                    BFLog(message: "导出相册视频中...:\(filePath)")
+                case .completed:
+                    DispatchQueue.main.async {
+                        resultHandler(aVAsset, filePath, nil)
+                    }
+                    BFLog(message: "导出相册视频完成:\(filePath)")
+                case .failed:
+                    DispatchQueue.main.async {
+                        resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                    }
+                    BFLog(message: "导出相册视频失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                case .cancelled:
+                    DispatchQueue.main.async {
+                        resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
+                    }
+                    BFLog(message: "取消导出相册视频:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
+                default:
+                    break
+                }
+            })
+        }
+    }
+
+    /// 获取视频资源的旋转角度
+    /// - Parameter assert: <#assert description#>
+    /// - Returns: <#description#>
+    public class func videoRotationAngle(assert: AVAsset) -> Int {
+        var rotationAngle: Int = 0
+
+        let tracks = assert.tracks(withMediaType: .video)
+        if tracks.count > 0 {
+            let firstTrack = tracks.first
+            let transform = firstTrack?.preferredTransform
+            if transform?.a == 0, transform?.b == 1.0, transform?.c == -1.0, transform?.d == 0 {
+                rotationAngle = 90
+            } else if transform?.a == -1.0, transform?.b == 0, transform?.c == 0, transform?.d == -1.0 {
+                rotationAngle = 180
+            } else if transform?.a == 0, transform?.b == -1.0, transform?.c == 1.0, transform?.d == 0 {
+                rotationAngle = 270
+            } else if transform?.a == 1.0, transform?.b == 0, transform?.c == 0, transform?.d == 1.0 {
+                rotationAngle = 0
+            }
+        }
+        return rotationAngle
+    }
+
+    /// 裁剪背景音乐并导出
+    /// - Parameters:
+    ///   - url: 原始地址
+    ///   - startTime: 开始时间
+    ///   - endTime: 结束时间
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func cutAudioToLocal(url: String, startTime: Float, endTime: Float, resultHandler: @escaping (_ url: String, _ filePath: String?, _ startTime: Float, _ endTime: Float, _ errorMsg: String?) -> Void) {
+        // 创建目录
+        createDirectory(path: bgMusicDirectory)
+        let filePath = bgMusicDirectory + url.md5 + ".mp3"
+        let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
+        if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > 0 {
+            DispatchQueue.main.async {
+                resultHandler(url, filePath, startTime, endTime, nil)
+            }
+        } else {
+            // 删除以创建地址
+            try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
+            let audioAsset = AVURLAsset(url: URL(string: url)!)
+            audioAsset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
+                let status = audioAsset.statusOfValue(forKey: "tracks", error: nil)
+                switch status {
+                case .loaded: // 加载完成
+                    // AVAssetExportPresetPassthrough /AVAssetExportPresetAppleM4A
+                    let exportSession = AVAssetExportSession(asset: audioAsset, presetName: AVAssetExportPresetHighestQuality)
+                    exportSession?.outputURL = URL(fileURLWithPath: filePath)
+                    exportSession?.outputFileType = .mp3
+                    exportSession?.timeRange = CMTimeRange(start: CMTime(seconds: Double(startTime), preferredTimescale: 1000), end: CMTime(seconds: Double(endTime), preferredTimescale: 1000))
+                    exportSession?.exportAsynchronously(completionHandler: {
+                        switch exportSession?.status {
+                        case .waiting:
+                            BFLog(message: "等待导出mp3:\(filePath)")
+                        case .exporting:
+                            BFLog(message: "导出中...:\(filePath)")
+                        case .completed:
+                            DispatchQueue.main.async {
+                                resultHandler(url, filePath, startTime, endTime, nil)
+                            }
+                            BFLog(message: "导出完成:\(filePath)")
+
+                        case .cancelled, .failed, .unknown:
+                            DispatchQueue.main.async {
+                                resultHandler(url, nil, startTime, endTime, exportSession?.error?.localizedDescription)
+                            }
+                            BFLog(message: "导出失败:\(filePath),\(exportSession?.error?.localizedDescription ?? "")")
+                        default:
+                            break
+                        }
+                    })
+                case .loading:
+                    BFLog(message: "加载中...:\(url)")
+                case .failed, .cancelled, .unknown:
+                    DispatchQueue.main.async {
+                        resultHandler(url, nil, startTime, endTime, "导出失败")
+                    }
+                default:
+                    break
+                }
+            }
+        }
+    }
+
+    /// 创建本地保存地址
+    /// - Parameters:
+    ///   - sourceFilePath: <#sourceFilePath description#>
+    ///   - completeHandle: <#completeHandle description#>
+    /// - Returns: <#description#>
+    public class func createLocalFile(sourceFilePath: String, completeHandle: (_ isFileExists: Bool, _ isCreateSuccess: Bool, _ filePath: String) -> Void) {
+        let cLocalPath = NSString(string: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("\(sourceFilePath.md5).mp4")
+        if FileManager.default.fileExists(atPath: cLocalPath) {
+            BFLog(message: "文件已经存在:\(cLocalPath)")
+            completeHandle(true, false, cLocalPath)
+        } else {
+            let result = FileManager.default.createFile(atPath: cLocalPath, contents: nil, attributes: nil)
+            BFLog(message: "文件创建:\(cLocalPath),\(result)")
+            completeHandle(false, result, cLocalPath)
+        }
+    }
+
+    /// 获取图库图片
+    /// - Parameters:
+    ///   - asset: <#asset description#>
+    ///   - itemSize: <#itemSize description#>
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func requestAssetImage(asset: PHAsset, itemSize: CGSize, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) {
+        PHCachingImageManager().requestImage(for: asset, targetSize: itemSize, contentMode: .aspectFill, options: imagesOptions, resultHandler: { image, info in
+            BFLog(message: "info = \(info ?? [:])")
+            if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0" {
+                resultHandler(image, info)
+            }
+        })
+    }
+
+    /// 获取图库原图
+    /// - Parameters:
+    ///   - asset: <#asset description#>
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func requestAssetOringinImage(asset: PHAsset, resultHandler: @escaping (_ isGIF: Bool, _ data: Data?, UIImage?, [AnyHashable: Any]?) -> Void) {
+        PHCachingImageManager().requestImageData(for: asset, options: singleImageOptions) { data, _, _, info in
+            var image: UIImage?
+            if data != nil {
+                image = UIImage(data: data!)
+            }
+            if info?.keys.contains("PHImageFileUTIKey") ?? false, "\(info?["PHImageFileUTIKey"] ?? "")" == "com.compuserve.gif" {
+                resultHandler(true, data, image, info)
+            } else {
+                resultHandler(false, data, image, info)
+            }
+        }
+    }
+
+    /// 获取gif帧跟时长
+    /// - Parameters:
+    ///   - data: <#data description#>
+    ///   - isRenderingTemplate
+    ///   - resultHandler: <#resultHandler description#>
+    /// - Returns: <#description#>
+    public class func parasGIFImage(data: Data, isRenderingColor: UIColor? = nil, resultHandler: @escaping (_ data: Data, _ images: [UIImage]?, _ duration: Double?) -> Void) {
+        let info: [String: Any] = [
+            kCGImageSourceShouldCache as String: true,
+            kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF,
+        ]
+        guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
+            resultHandler(data, nil, nil)
+            BFLog(message: "获取gifimageSource 失败")
+            return
+        }
+        // 获取帧数
+        let frameCount = CGImageSourceGetCount(imageSource)
+        var gifDuration = 0.0
+        var images = [UIImage]()
+        for i in 0 ..< frameCount {
+            // 取出索引对应的图片
+            guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else {
+                BFLog(message: "取出对应的图片失败")
+                return
+            }
+            if frameCount == 1 {
+                // 单帧
+                gifDuration = .infinity
+            } else {
+                // 1.获取gif没帧的时间间隔
+                // 获取到该帧图片的属性字典
+                guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as? [String: Any] else {
+                    BFLog(message: "取出对应的图片属性失败")
+                    return
+                }
+                // 获取该帧图片中的GIF相关的属性字典
+                guard let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] else {
+                    BFLog(message: "取出对应的图片属性失败")
+                    return
+                }
+                let defaultFrameDuration = 0.1
+                // 获取该帧图片的播放时间
+                let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
+                // 如果通过kCGImagePropertyGIFUnclampedDelayTime没有获取到播放时长,就通过kCGImagePropertyGIFDelayTime来获取,两者的含义是相同的;
+                let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
+                let duration = unclampedDelayTime ?? delayTime
+                guard let frameDuration = duration else {
+                    BFLog(message: "获取帧时间间隔失败")
+                    return
+                }
+                // 对于播放时间低于0.011s的,重新指定时长为0.100s;
+                let gifFrameDuration = frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration
+                // 计算总时间
+                gifDuration += gifFrameDuration
+                // 2.图片
+                var frameImage: UIImage? = UIImage(cgImage: imageRef, scale: 1.0, orientation: .up)
+                if isRenderingColor != nil {
+                    frameImage = frameImage?.tintImage(color: isRenderingColor!, blendMode: .destinationIn)
+                }
+                if frameImage != nil {
+                    images.append(frameImage!)
+                }
+            }
+        }
+        resultHandler(data, images, gifDuration)
+    }
+}

+ 95 - 0
BFCommonKit/Classes/BFUtility/PQVideoSnapshotUtil.swift

@@ -0,0 +1,95 @@
+//
+//  PQVideoSnapshotUtil.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/8/14.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import AVFoundation
+import AVKit
+import UIKit
+
+public class PQVideoSnapshotUtil: NSObject {
+    /// AVPlayer截屏
+    /// - Parameter playerItem: <#playerItem description#>
+    /// - Returns: <#description#>
+    class public  func snapshotImage(avPlayer: AVPlayer, complateHandle: @escaping (_ image: UIImage?) -> Void) {
+        guard let playerItem = avPlayer.currentItem else { // playerItem is AVPlayerItem
+            complateHandle(nil)
+            return
+        }
+        let videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: nil)
+        playerItem.add(videoOutput)
+        let time = videoOutput.itemTime(forHostTime: CACurrentMediaTime())
+        if videoOutput.hasNewPixelBuffer(forItemTime: time) {
+            let lastSnapshotPixelBuffer = videoOutput.copyPixelBuffer(forItemTime: time, itemTimeForDisplay: nil)
+            if lastSnapshotPixelBuffer != nil {
+                let ciImage = CIImage(cvPixelBuffer: lastSnapshotPixelBuffer!)
+                let context = CIContext(options: nil)
+                let rect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(CVPixelBufferGetWidth(lastSnapshotPixelBuffer!)), height: CGFloat(CVPixelBufferGetHeight(lastSnapshotPixelBuffer!)))
+                let cgImage = context.createCGImage(ciImage, from: rect)
+                if cgImage != nil {
+                    complateHandle(UIImage(cgImage: cgImage!))
+                    return
+                }
+            }
+        } else {
+            complateHandle(nil)
+        }
+    }
+
+    /// 非m3u8视频截屏
+    /// - Parameters:
+    ///   - videoURL: 视频地址
+    ///   - time: 视频某个时间点
+    /// - Returns: <#description#>
+    class public  func videoSnapshot(videoURL: URL, time: TimeInterval) -> UIImage? {
+        let asset = AVURLAsset(url: videoURL, options: avAssertOptions)
+        let assetImageGenerator = AVAssetImageGenerator(asset: asset)
+        assetImageGenerator.appliesPreferredTrackTransform = true
+        assetImageGenerator.requestedTimeToleranceBefore = CMTime.zero
+        assetImageGenerator.requestedTimeToleranceAfter = CMTime.zero
+        assetImageGenerator.apertureMode = .encodedPixels
+        let thumbnailCGImage: CGImage?
+        let thumbnailImageTime: CFTimeInterval = time
+        var thumbnailImage: UIImage?
+        do {
+            thumbnailCGImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: Int64(thumbnailImageTime), timescale: 1), actualTime: nil)
+            if let cgImage = thumbnailCGImage {
+                thumbnailImage = UIImage(cgImage: cgImage)
+            }
+        } catch {}
+        return thumbnailImage
+    }
+
+    /// 获取非m3u8多张视频截图
+    /// - Parameters:
+    ///   - videoURL: 视频地址
+    ///   - duration: 视频时长
+    ///   - count: 截取数量
+    /// - Returns: <#description#>
+    class public  func videoSnapshot(videoURL: URL, duration: TimeInterval, count: Int, complateHandle: @escaping ([UIImage]?) -> Void) {
+        DispatchQueue.global().async {
+            var images: [UIImage]? = Array()
+            let asset = AVURLAsset(url: videoURL, options: avAssertOptions)
+            let assetImageGenerator = AVAssetImageGenerator(asset: asset)
+            assetImageGenerator.requestedTimeToleranceBefore = CMTime.zero
+            assetImageGenerator.requestedTimeToleranceAfter = CMTime.zero
+            assetImageGenerator.appliesPreferredTrackTransform = true
+            assetImageGenerator.apertureMode = .encodedPixels
+            for index in 0...count - 1 {
+                let thumbnailCGImage: CGImage?
+                let thumbnailImageTime: CFTimeInterval = ((duration / Double(count)) * Double(index))
+                BFLog(message: "截取视频时长 =(\(videoURL),duration = \(duration),count = \(count),line = \(thumbnailImageTime)")
+                do {
+                    thumbnailCGImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: Int64(thumbnailImageTime), timescale: 1), actualTime: nil)
+                    if let cgImage = thumbnailCGImage {
+                        images?.append(UIImage(cgImage: cgImage))
+                    }
+                } catch {}
+            }
+            complateHandle(images)
+        }
+    }
+}

+ 48 - 0
BFCommonKit/Classes/BFUtility/PQWeakTimer.swift

@@ -0,0 +1,48 @@
+//
+//  PQWeakTimer.swift
+//  PQSpeed
+//
+//  Created by lieyunye on 2020/6/18.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+public class PQWeakTimer {
+     public weak var timer: Timer?
+    public weak var target: AnyObject?
+    public let action: (Timer) -> Void
+
+    public init(timeInterval: TimeInterval,
+                 target: AnyObject,
+                 repeats: Bool,
+                 action: @escaping (Timer) -> Void)
+    {
+        self.target = target
+        self.action = action
+        timer = Timer.scheduledTimer(timeInterval: timeInterval,
+                                     target: self,
+                                     selector: #selector(fire),
+                                     userInfo: nil,
+                                     repeats: repeats)
+    }
+
+    public class func scheduledTimer(timeInterval: TimeInterval,
+                              target: AnyObject,
+                              repeats: Bool,
+                              action: @escaping (Timer) -> Void) -> Timer
+    {
+        return PQWeakTimer(timeInterval: timeInterval,
+                           target: target,
+                           repeats: repeats,
+                           action: action).timer!
+    }
+
+    @objc public func fire(timer: Timer) {
+        if target != nil {
+            action(timer)
+        } else {
+            timer.invalidate()
+        }
+    }
+}

+ 156 - 0
BFCommonKit/Classes/BFUtility/SWNetRequest.swift

@@ -0,0 +1,156 @@
+//
+//  SWNetRequest.swift
+//  LiteraryHeaven
+//
+//  Created by SanW on 2017/8/2.
+//  Copyright © 2017年 ONON. All rights reserved.
+//
+
+import Alamofire
+import UIKit
+
+// 默认超时时间
+public let timeoutInterval: TimeInterval = 30
+
+// MARK: - 错误
+
+/// 错误
+public struct PQError: Error {
+    public var msg: String? // 提示信息
+    public var code: Int // 错误吗
+
+    init(msg: String?, code: Int = 0) {
+        self.msg = msg
+        self.code = code
+    }
+
+    public var localizedDescription: String {
+        return msg ?? ""
+    }
+}
+
+// MARK: - 网络请求
+
+/// 网络请求
+public class SWNetRequest: NSObject {
+    static let sessionManager: Alamofire.SessionManager = {
+        let configuration = URLSessionConfiguration.default
+        configuration.timeoutIntervalForRequest = timeoutInterval
+        return Alamofire.SessionManager(configuration: configuration)
+    }()
+
+    static let reTrySessionManager: Alamofire.SessionManager = {
+        let configuration = URLSessionConfiguration.default
+        configuration.timeoutIntervalForRequest = 5
+        return Alamofire.SessionManager(configuration: configuration)
+    }()
+
+    /// 回调方法
+    public typealias completeHander = (_ responseobject: Any?, _ extData: [String: Any]?, _ error: PQError?, _ timeline: Timeline) -> Void
+    /// get请求
+    public class func getRequestData(url: String, parames: [String: Any]?, encoding: ParameterEncoding = URLEncoding.default, timeoutInterval: TimeInterval = timeoutInterval, response: @escaping completeHander) {
+        requestData(method: .get, encoding: encoding, url: url, parames: parames, timeoutInterval: timeoutInterval) { responseObject, extData, error, timeline in
+            response(responseObject, extData, error, timeline)
+        }
+    }
+
+    /// post请求
+    public class func postRequestData(url: String, parames: [String: Any]?, encoding: ParameterEncoding = URLEncoding.default, timeoutInterval: TimeInterval = timeoutInterval, response: @escaping completeHander) {
+        requestData(method: .post, encoding: encoding, url: url, parames: parames, timeoutInterval: timeoutInterval) { responseObject, extData, error, timeline in
+            response(responseObject, extData, error, timeline)
+        }
+    }
+
+    /// 发送head请求
+    /// - Parameter url: <#url description#>
+    /// - Returns: <#description#>
+    class public func headRequestData(url: String, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
+        BFLog(message: "发起head请求 = \(url)")
+        if !isValidURL(url: url) {
+            completionHandler(nil, nil, PQError(msg: "url错误", code: 1001))
+            return
+        }
+        // http://www.mxfjx.com/uploadfiles/2014/4/小鸡%20鸡蛋%20蛋生鸡%20鸡生蛋_conew1.gif
+        url.urlEncoded()
+        let reqUrl = URL(string: url)!
+        var request = URLRequest(url: reqUrl)
+        request.timeoutInterval = 1 // 设置超时1s
+        request.httpMethod = "HEAD"
+        let session = URLSession.shared
+        let headTask = session.dataTask(with: request) { data, response, error in
+            completionHandler(data, response, error)
+            BFLog(message: "head请求返回 = \(url),code = \((response as? HTTPURLResponse)?.statusCode ?? 0),response = \(String(describing: response))")
+        }
+        headTask.resume()
+    }
+
+    /// put请求
+    public class func putRequestData(url: String, parames: [String: Any]?, encoding: ParameterEncoding = URLEncoding.default, timeoutInterval: TimeInterval = timeoutInterval, response: @escaping completeHander) {
+        requestData(method: .put, encoding: encoding, url: url, parames: parames, timeoutInterval: timeoutInterval) { responseObject, extData, error, timeline in
+            response(responseObject, extData, error, timeline)
+        }
+    }
+
+    /// delete请求
+    public class func deleteRequestData(url: String, parames: [String: Any]?, encoding: ParameterEncoding = URLEncoding.default, timeoutInterval: TimeInterval = timeoutInterval, response: @escaping completeHander) {
+        requestData(method: .delete, encoding: encoding, url: url, parames: parames, timeoutInterval: timeoutInterval) { responseObject, extData, error, timeline in
+            response(responseObject, extData, error, timeline)
+        }
+    }
+
+    /// 网络请求
+    fileprivate class func requestData(method: HTTPMethod, encoding: ParameterEncoding, url: String, parames: [String: Any]?, timeoutInterval _: TimeInterval = timeoutInterval, response: @escaping completeHander) {
+        var requestParams: [String: Any] = Dictionary<String, Any>.init()
+        if encoding is JSONEncoding {
+            requestParams["baseInfo"] = commonParams()
+            requestParams["params"] = parames
+        } else {
+            requestParams = commonParams()
+            if parames != nil, parames?.count ?? 0 > 0 {
+                if parames?.keys.contains("abInfoData") ?? false, "\(parames?["abInfoData"] ?? "")".count > 0, "\(parames?["abInfoData"] ?? "")" != "{}" {
+                    requestParams.removeValue(forKey: "abInfoData")
+                }
+                for (key, value) in parames!.reversed() {
+                    requestParams[key] = value
+                }
+            }
+        }
+        BFLog(message: "发起请求:\(url),params:\(requestParams)")
+        (timeoutInterval <= 5 ? reTrySessionManager : sessionManager).request(url, method: method, parameters: requestParams, encoding: encoding, headers: nil).responseJSON { jsonResponse in
+            BFLog(message: "jsonResponse = \(jsonResponse.timeline)")
+            /// 返回值
+            if jsonResponse.result.isSuccess {
+                let respondDict: [String: Any] = try! JSONSerialization.jsonObject(with: jsonResponse.data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! [String: Any]
+                BFLog(message: "请求成功:\(url),respond:\(respondDict)")
+                if respondDict.keys.contains("code") && "\(respondDict["code"] ?? "")" == "0" && respondDict.keys.contains("data") {
+                    let extData = respondDict["extData"] as? [String: Any]
+                    if extData != nil && (extData?.keys.contains("abInfoData") ?? false) {
+//                        PQSingletoMemoryUtil.shared.parasABinfoData(abInfo: extData?["abInfoData"] as? String)
+                    }
+                    response(respondDict["data"], extData, nil, jsonResponse.timeline)
+                } else if (respondDict.keys.contains("msg") && "\(respondDict["msg"] ?? "")".count > 0) || (respondDict.keys.contains("message") && "\(respondDict["message"] ?? "")".count > 0) {
+                    response(respondDict["data"], nil, PQError(msg: "\(respondDict["msg"] ?? "")", code: Int("\(respondDict["code"] ?? "")") ?? 10001), jsonResponse.timeline)
+                } else {
+                    response(nil, nil, PQError(msg: jsonResponse.result.error?.localizedDescription, code: 10001), jsonResponse.timeline)
+                }
+            } else {
+                BFLog(message: "请求失败:\(url),error:\(jsonResponse.result.error?.localizedDescription ?? "")")
+                let code: Int? = (jsonResponse.result.error as NSError?)?.code
+                response(nil, nil, PQError(msg: (code == -1009 || code == -1001) ? "网络不可用" : jsonResponse.result.error?.localizedDescription, code: code ?? 10001), jsonResponse.timeline)
+            }
+        }
+    }
+
+    /// 取消网络请求
+    /// - Parameter url: 某一个地址,空则取消所有
+    /// - Returns: <#description#>
+    public class func cancelTask(url: String?) {
+        sessionManager.session.getAllTasks(completionHandler: { tasks in
+            tasks.forEach { task in
+                if task.currentRequest?.url?.absoluteString == url {
+                    task.cancel()
+                }
+            }
+        })
+    }
+}

+ 203 - 0
BFCommonKit/Classes/Base/Controller/PQBaseViewController.swift

@@ -0,0 +1,203 @@
+//
+//  PQBaseViewController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+// import MediaPlayer
+import Alamofire
+import UIKit
+open class PQBaseViewController: UIViewController, UIGestureRecognizerDelegate {
+    // 侧滑拦截返回
+    public var popGestureHandle: (() -> Void)?
+    public var naviTitle: String? // 标题
+    public var rightButton: UIButton? // 右边按钮
+    public var backButton: UIButton? // 左边按钮
+    public var navTitleLabel: UILabel? // 标题
+    public var navHeadImageView: UIImageView? // 导航条
+    public var lineView: UIView? // 导航分隔线
+    public var isHiddenStatus: Bool = false { // 更新状态栏
+        didSet {
+            setNeedsStatusBarAppearanceUpdate()
+        }
+    }
+
+    /// <#Description#>
+    public var isPresent: Bool {
+        var isPresent = true
+        let viewcontrollers = navigationController?.viewControllers
+        if (viewcontrollers?.count ?? 0) > 1 {
+            if viewcontrollers?[(viewcontrollers?.count ?? 1) - 1] == self {
+                isPresent = false
+            }
+        }
+        return isPresent
+    }
+
+    public lazy var manager: NetworkReachabilityManager? = {
+        let manager = NetworkReachabilityManager(host: "www.baidu.com")
+        manager?.listener = { status in
+            if status == .reachable(.wwan) || status == .reachable(.ethernetOrWiFi) {}
+        }
+        return manager
+    }()
+
+    override open func viewDidLoad() {
+        super.viewDidLoad()
+        navigationController?.isNavigationBarHidden = true
+        view.backgroundColor = PQBFConfig.shared.styleBackGroundColor
+        navHeadImageView = UIImageView(image: UIImage())
+        navHeadImageView?.isUserInteractionEnabled = true
+        navHeadImageView?.backgroundColor = PQBFConfig.shared.styleBackGroundColor
+        navHeadImageView?.frame = CGRect(x: 0, y: 0, width: cScreenWidth, height: cDevice_iPhoneNavBarAndStatusBarHei)
+        view.addSubview(navHeadImageView!)
+
+        lineView = UIView(frame: CGRect(x: 0, y: (navHeadImageView?.frame.maxY ?? cDevice_iPhoneNavBarAndStatusBarHei) - 0.5, width: cScreenWidth, height: 0.5))
+        lineView?.backgroundColor = PQBFConfig.shared.styleBackGroundColor
+        view.addSubview(lineView!)
+        UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
+        automaticallyAdjustsScrollViewInsets = false
+        navigationController?.interactivePopGestureRecognizer?.delegate = self
+//        fd_prefersNavigationBarHidden = true
+    }
+
+    public func hiddenNavigation() {
+        navHeadImageView?.isHidden = true
+        lineView?.isHidden = true
+    }
+
+    public func showNavigation() {
+        if navHeadImageView != nil {
+            navHeadImageView?.isHidden = false
+            lineView?.isHidden = false
+            view.bringSubviewToFront(navHeadImageView!)
+        }
+    }
+
+    open func leftBackButton() {
+        leftButton(image: "icon_detail_back")
+    }
+
+    public func leftButton(image: String?, tintColor: UIColor? = nil) {
+        let leftButton = UIButton(type: .custom)
+        leftButton.frame = CGRect(x: 0, y: cDevice_iPhoneStatusBarHei, width: cDefaultMargin * 4, height: cDefaultMargin * 4)
+        leftButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -5, right: 0)
+        if tintColor != nil {
+            leftButton.tintColor = tintColor
+            leftButton.setImage(UIImage().BF_Image(named: image ?? "icon_detail_back").withRenderingMode(.alwaysTemplate), for: .normal)
+        } else {
+            leftButton.setImage(UIImage().BF_Image(named: image ?? "icon_detail_back"), for: .normal)
+        }
+        leftButton.addTarget(self, action: #selector(backBtnClick), for: .touchUpInside)
+        navHeadImageView?.addSubview(leftButton)
+        backButton = leftButton
+    }
+
+    public func rightButtonItem(image: String?, title: String?) {
+        let rightButtonItem = UIButton(type: .custom)
+        var rightW: CGFloat = cDefaultMargin
+        if title != nil, title?.count ?? 0 > 0 {
+            rightW = rightW + sizeWithText(text: title ?? "", font: UIFont.systemFont(ofSize: 16), size: CGSize(width: CGFloat.greatestFiniteMagnitude, height: cDefaultMargin * 4)).width
+            rightButtonItem.setTitle(title, for: .normal)
+            rightButtonItem.setTitleColor(UIColor.hexColor(hexadecimal: "#242F44"), for: .normal)
+            rightButtonItem.titleLabel?.font = UIFont.systemFont(ofSize: 16)
+        }
+        if image != nil, image?.count ?? 0 > 0 {
+            rightW = rightW + cDefaultMargin * 4
+            rightButtonItem.setImage(UIImage(named: image ?? ""), for: .normal)
+            rightButtonItem.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: (title != nil && title?.count ?? 0 > 0) ? 0 : -5, right: 0)
+        }
+        rightButtonItem.adjustsImageWhenHighlighted = false
+        rightButtonItem.frame = CGRect(x: cScreenWidth - rightW, y: cDevice_iPhoneStatusBarHei, width: rightW, height: cDefaultMargin * 4)
+        rightButtonItem.addTarget(self, action: #selector(rightBtnClick(sender:)), for: .touchUpInside)
+        rightButtonItem.contentHorizontalAlignment = .center
+        navHeadImageView?.addSubview(rightButtonItem)
+        rightButton = rightButtonItem
+    }
+
+    public func setTitle(title: String?, color: UIColor = UIColor.white) {
+        naviTitle = title
+        if navTitleLabel == nil {
+            let titleLabel = UILabel(frame: CGRect(x: cDefaultMargin * 5, y: cDevice_iPhoneStatusBarHei, width: cScreenWidth - 100, height: cDefaultMargin * 4))
+            titleLabel.textColor = color
+            titleLabel.textAlignment = .center
+            navTitleLabel = titleLabel
+            navHeadImageView?.addSubview(titleLabel)
+        }
+        navTitleLabel?.text = title
+    }
+
+    @objc open func rightBtnClick(sender _: UIButton) {}
+
+    @objc open func backBtnClick() {
+        if isPresent {
+            dismiss(animated: true, completion: nil)
+        } else {
+            navigationController?.popViewController(animated: true)
+        }
+    }
+
+    override open func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+    }
+
+    override open func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        PQLoadingHUB.shared.dismissHUB()
+    }
+
+    override open func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+
+        if view.viewWithTag(cGuideTag) != nil {
+            view.viewWithTag(cGuideTag)?.removeFromSuperview()
+        }
+    }
+
+    deinit {
+        PQNotification.removeObserver(self)
+        BFLog(message: "\(String(describing: type(of: self)))被销毁")
+    }
+
+    override open var preferredStatusBarStyle: UIStatusBarStyle {
+        if PQBFConfig.shared.statusBarStyle == .dark {
+            if #available(iOS 13.0, *) {
+                return .darkContent
+            } else {
+                return .default
+            }
+        } else {
+            return .lightContent
+        }
+    }
+
+    override open var prefersStatusBarHidden: Bool {
+        return isHiddenStatus
+    }
+
+    /// 禁止滑动返回
+    /// - Returns: <#description#>
+    public func disablePopGesture() -> PQBaseViewController {
+        let traget = navigationController?.interactivePopGestureRecognizer?.delegate
+        let pan = UIPanGestureRecognizer(target: traget, action: #selector(popGesture(panGes:)))
+        view.addGestureRecognizer(pan)
+        return self
+    }
+
+    /// 拦截侧滑手势
+    /// - Returns: <#description#>
+    @objc private  func popGesture(panGes: UIPanGestureRecognizer) {
+        if panGes.state == .ended, popGestureHandle != nil {
+            popGestureHandle!()
+        }
+    }
+
+    open func gestureRecognizer(_: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
+        if touch.view is UISlider {
+            return false
+        }
+        return true
+    }
+}

+ 184 - 0
BFCommonKit/Classes/Base/Controller/PQBaseWebViewController.swift

@@ -0,0 +1,184 @@
+
+//
+//  PQBaseWebViewController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/27.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+import WebKit
+
+open class PQBaseWebViewController: PQBaseViewController {
+   public var emptyData: PQEmptyModel? = {
+        let emptyData = PQEmptyModel()
+        emptyData.title = "网页加载失败,请重试~"
+        emptyData.emptyImage = "pic_network"
+        return emptyData
+    }()
+
+    lazy public var webView: WKWebView = {
+        let config: WKWebViewConfiguration = WKWebViewConfiguration()
+        config.allowsInlineMediaPlayback = true
+        let webView: WKWebView = WKWebView(frame: CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth, height: cScreenHeigth - cDevice_iPhoneNavBarAndStatusBarHei), configuration: config)
+        webView.backgroundColor = UIColor.white
+        if #available(iOS 11.0, *) {
+            webView.scrollView.contentInsetAdjustmentBehavior = .never
+        } else {
+            automaticallyAdjustsScrollViewInsets = false
+        }
+        webView.navigationDelegate = self
+        return webView
+    }()
+
+    lazy public var progresslayer: CALayer = {
+        let progresslayer = CALayer()
+        progresslayer.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth * 0.1, height: 2)
+        progresslayer.backgroundColor = UIColor.hexColor(hexadecimal: "#FF9500").cgColor
+        return progresslayer
+    }()
+
+    @objc public var baseUrl: String? {
+        didSet {
+            if baseUrl != nil, baseUrl?.count ?? 0 > 0 {
+                webView.load(URLRequest(url: NSURL(string: baseUrl ?? "")! as URL, cachePolicy: .reloadIgnoringCacheData))
+                // 添加属性监听
+                webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
+                isAddObserve = true
+                view.layer.addSublayer(progresslayer)
+            }
+        }
+    }
+
+    @objc public var baseTitle: String?
+    public var evaluateJavaScript: String? // 交互
+    var isAddObserve: Bool = false
+    override open func viewDidLoad() {
+        super.viewDidLoad()
+        // Do any additional setup after loading the view.
+        view.addSubview(webView)
+        leftButton(image: "icon_blanc_back")
+        navHeadImageView?.backgroundColor = UIColor.white
+    }
+
+    override public func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        if (UIApplication.shared.keyWindow?.viewWithTag(cProtocalViewTag)) != nil {
+            (UIApplication.shared.keyWindow?.viewWithTag(cProtocalViewTag))?.isHidden = true
+        }
+    }
+
+    override public func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        if (UIApplication.shared.keyWindow?.viewWithTag(cProtocalViewTag)) != nil {
+            (UIApplication.shared.keyWindow?.viewWithTag(cProtocalViewTag))?.isHidden = false
+        }
+    }
+
+    deinit {
+        if isAddObserve {
+            webView.removeObserver(self, forKeyPath: "estimatedProgress")
+        }
+    }
+}
+
+extension PQBaseWebViewController: WKNavigationDelegate {
+   public func refreshClick() {
+        if baseUrl != nil, baseUrl?.count ?? 0 > 0 {
+            webView.load(URLRequest(url: NSURL(string: baseUrl ?? "")! as URL, cachePolicy: .useProtocolCachePolicy))
+        }
+    }
+
+    @objc public func back() {
+        if webView.canGoBack {
+            webView.goBack()
+        } else if navigationController != nil {
+            navigationController?.popViewController(animated: true)
+        } else {
+            dismiss(animated: true, completion: nil)
+        }
+    }
+
+    override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "estimatedProgress" {
+            progresslayer.opacity = 1
+            let float = (change?[NSKeyValueChangeKey.newKey] as! NSNumber).floatValue
+            progresslayer.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: cScreenWidth * CGFloat(float), height: 3)
+            if float == 1 {
+                weak var weakself = self
+                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
+                    weakself?.progresslayer.opacity = 0
+                }
+                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.8) {
+                    weakself?.progresslayer.frame = CGRect(x: 0, y: cDevice_iPhoneNavBarAndStatusBarHei, width: 0, height: 3)
+                }
+            }
+        } else {
+            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
+        }
+    }
+
+    public func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
+        if baseTitle == nil || baseTitle?.count ?? 0 <= 0 {
+            webView.evaluateJavaScript("document.title") { [weak self] (any, _) -> Void in
+                self?.setTitle(title: any as? String)
+                self?.baseTitle = any as? String
+            }
+        }
+        if evaluateJavaScript != nil, (evaluateJavaScript?.count ?? 0) > 0 {
+            webView.evaluateJavaScript(evaluateJavaScript!) { _, _ in
+            }
+        }
+    }
+
+    public func webView(_ webView: WKWebView, didFail _: WKNavigation!, withError error: Error) {
+        BFLog(message: error)
+        if baseTitle == nil || baseTitle?.count ?? 0 <= 0 {
+            webView.evaluateJavaScript("document.title") { [weak self] (any, _) -> Void in
+                self?.setTitle(title: any as? String)
+                self?.baseTitle = any as? String
+            }
+        }
+    }
+
+    public func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation!, withError _: Error) {}
+
+    public func webView(_: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
+        BFLog(message: "navigationResponse:\(String(describing: navigationResponse))")
+        decisionHandler(.allow)
+    }
+
+    public func webView(_: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
+        BFLog(message: "didStartProvisionalNavigation:\(String(describing: navigation))")
+    }
+
+    public func webView(_: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
+        BFLog(message: "didReceiveServerRedirectForProvisionalNavigation:\(String(describing: navigation))")
+    }
+
+    public func webView(_: WKWebView, didCommit navigation: WKNavigation!) {
+        BFLog(message: "\(String(describing: navigation))")
+    }
+
+//    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
+//        let url : String = navigationAction.request.url?.absoluteString ?? "";
+//        if(url.count == 0 || url == "about:blank"){
+//            decisionHandler(.cancel)
+//            return
+//        }
+//        let vc = PQBaseWebViewController.init()
+//        vc.baseUrl = url
+//        navigationController?.pushViewController(vc, animated: true)
+//        BFLog(message: "decidePolicyFor \(String(describing: navigationAction))")
+//        decisionHandler(.allow)
+//    }
+    override public var preferredStatusBarStyle: UIStatusBarStyle {
+        if #available(iOS 13.0, *) {
+            return .darkContent
+        } else {
+            // Fallback on earlier versions
+            return .default
+        }
+    }
+}

+ 46 - 0
BFCommonKit/Classes/Base/Controller/PQNavigatinController.swift

@@ -0,0 +1,46 @@
+//
+//  PQNavigatinController.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+open class PQNavigatinController: UINavigationController, UIGestureRecognizerDelegate {
+    public var isPop: Bool = false
+
+   open  override func viewDidLoad() {
+        super.viewDidLoad()
+
+        // Do any additional setup after loading the view.
+    }
+
+   open override func pushViewController(_ viewController: UIViewController, animated _: Bool) {
+        if viewControllers.count > 0 {
+            viewController.hidesBottomBarWhenPushed = true
+        }
+        super.pushViewController(viewController, animated: true)
+    }
+
+   public func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
+        return topViewController!.supportedInterfaceOrientations
+    }
+
+   public func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
+        return topViewController!.preferredInterfaceOrientationForPresentation
+    }
+
+   open override var childForStatusBarStyle: UIViewController? {
+        return topViewController
+    }
+
+   open override var childForStatusBarHidden: UIViewController? {
+        return topViewController
+    }
+
+   public func gestureRecognizerShouldBegin(_: UIGestureRecognizer) -> Bool {
+        return viewControllers.count > 1
+    }
+}

+ 182 - 0
BFCommonKit/Classes/Base/Model/PQBaseModel.swift

@@ -0,0 +1,182 @@
+//
+//  PQBaseModel.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/5/25.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+open class PQBaseModel: NSObject {
+    @objc dynamic public var uniqueId: String? // 唯一ID
+    @objc dynamic public var videoId: Int = 0 // 视频ID
+    @objc dynamic public var eventId: String? // 事件ID
+    @objc dynamic public var title: String? // 标题
+    @objc dynamic public var attributedTitle: NSMutableAttributedString? // 富文本标题
+    @objc dynamic public var summary: String? // 描述
+    @objc dynamic public var imageUrl: String = "" // 图片地址
+    @objc dynamic public var selectedImage: String = "" // 图片地址
+    @objc dynamic public var isSelected: Bool = false
+    @objc dynamic public var recommendLogVO: String? // 推荐日志对象
+    @objc dynamic public var abInfoData: String? // AB
+    @objc dynamic public var pageCategoryId: Int = 0 // 页面分类ID
+    @objc dynamic public var version: String = versionName // 版本号
+    @objc dynamic public var mid = getMachineCode() // 设备ID
+    @objc dynamic public var date: Int = 0 // 当前时间戳  CGFloat(Date.init().timeIntervalSince1970) * 1000
+    @objc dynamic public var itemWidth: Float = 0 // cell宽
+    @objc dynamic public var primaryKeys: String? // 区分存储唯一值
+
+    public override required  init() {
+        super.init()
+        uniqueId = getUniqueId(desc: "uniqueId")
+    }
+
+
+    @objc func toString() -> String {
+        var json: [String: Any] = [
+            "version": version,
+            "mid": mid,
+            "pageCategoryId": pageCategoryId,
+            "selectedImage": selectedImage,
+            "isSelected": isSelected,
+            "imageUrl": imageUrl,
+        ]
+        if uniqueId != nil {
+            json["uniqueId"] = uniqueId
+            json["videoId"] = videoId
+        }
+        if eventId != nil {
+            json["eventId"] = eventId
+        }
+        if title != nil {
+            json["title"] = title
+        }
+        if summary != nil {
+            json["summary"] = summary
+        }
+        if recommendLogVO != nil {
+            json["recommendLogVO"] = recommendLogVO
+        }
+        return dictionaryToJsonString(json) ?? ""
+    }
+
+    public init(jsonDict: [String: Any]) {
+        super.init()
+        if jsonDict.keys.contains("id") {
+            uniqueId = "\(jsonDict["id"] ?? "")"
+            videoId = Int(uniqueId ?? "0") ?? 0
+        }
+        if jsonDict.keys.contains("uniqueId") {
+            uniqueId = "\(jsonDict["uniqueId"] ?? "")"
+            videoId = Int(uniqueId ?? "0") ?? 0
+        }
+        if jsonDict.keys.contains("eventId") {
+            eventId = "\(jsonDict["eventId"] ?? "")"
+        }
+        if jsonDict.keys.contains("title") {
+            title = "\(jsonDict["title"] ?? "")"
+        }
+        if jsonDict.keys.contains("summary") {
+            summary = "\(jsonDict["summary"] ?? "")"
+        }
+        if jsonDict.keys.contains("imageUrl") {
+            imageUrl = "\(jsonDict["imageUrl"] ?? "")"
+        }
+        if jsonDict.keys.contains("selectedImage") {
+            selectedImage = "\(jsonDict["selectedImage"] ?? "")"
+        }
+        if jsonDict.keys.contains("isSelected") {
+            isSelected = jsonDict["isSelected"] as! Bool
+        }
+        if jsonDict.keys.contains("recommendLogVO") {
+            recommendLogVO = "\(jsonDict["recommendLogVO"] ?? "")"
+        }
+        if jsonDict.keys.contains("pageCategoryId") {
+            pageCategoryId = Int("\(jsonDict["pageCategoryId"] ?? "0")") ?? 0
+        }
+        if jsonDict.keys.contains("version") {
+            version = "\(jsonDict["version"] ?? "")"
+        }
+        if jsonDict.keys.contains("mid") {
+            mid = "\(jsonDict["mid"] ?? "")"
+        }
+    }
+}
+
+// MARK: - 当前应用本地存储的model
+
+/// 当前应用本地存储的model
+public  class PQLocalStoreModel: PQBaseModel {
+    @objc dynamic public var currentDate: String?
+    @objc required init() {
+        super.init()
+        currentDate = systemCurrentDate()
+    }
+}
+
+// MARK: - oss上传model
+
+/// oss上传model
+public class PQOssUploadModel: NSObject {
+    public var accessKeyId: String?
+    public var secretKeyId: String?
+    public var securityToken: String?
+    public var endpoint: String?
+    public var endpoints: [String]?
+    public var bucketName: String?
+    public var fileName: String?
+    public var uploadID: String?
+    public var expiration: String? // 过期时间
+    public init(jsonDict: [String: Any]) {
+        super.init()
+
+        if jsonDict.keys.contains("AccessKeyId") {
+            accessKeyId = "\(jsonDict["AccessKeyId"] ?? "")"
+        }
+        if jsonDict.keys.contains("AccessKeySecret") {
+            secretKeyId = "\(jsonDict["AccessKeySecret"] ?? "")"
+        }
+        if jsonDict.keys.contains("SecurityToken") {
+            securityToken = "\(jsonDict["SecurityToken"] ?? "")"
+        }
+        if jsonDict.keys.contains("Hosts") {
+            endpoints = jsonDict["Hosts"] as? [String]
+        }
+        if jsonDict.keys.contains("Host") {
+            endpoint = "\(jsonDict["Host"] ?? "")"
+            if endpoint != nil {
+                if endpoints == nil {
+                    endpoints = [endpoint!]
+                } else {
+                    endpoints?.append(endpoint!)
+                }
+            }
+        }
+        if jsonDict.keys.contains("Bucket") {
+            bucketName = "\(jsonDict["Bucket"] ?? "")"
+        }
+        if jsonDict.keys.contains("FileName") {
+            fileName = "\(jsonDict["FileName"] ?? "")"
+        }
+        if jsonDict.keys.contains("Upload") {
+            uploadID = "\(jsonDict["Upload"] ?? "")"
+        }
+        if jsonDict.keys.contains("Expiration") {
+            expiration = "\(jsonDict["Expiration"] ?? "")"
+        }
+    }
+}
+
+// MARK: - 空白页面model
+
+/// 空白页面model
+public class PQEmptyModel: NSObject {
+    public var title: String? // 标题
+    public var summary: String? // 描述
+    public var emptyImage: String? // 空白提示图
+    public var isRefreshHidden: Bool = true // 是否隐藏刷新按钮
+    public var refreshImage: String? // 刷新按钮图片
+    public var refreshTitle: NSMutableAttributedString? // 刷新按钮文字
+    public var refreshBgColor: UIColor? // 刷新按钮背景颜色
+}

+ 121 - 0
BFCommonKit/Classes/Base/View/PQFollowButton.swift

@@ -0,0 +1,121 @@
+//
+//  PQFollowButton.swift
+//  PQSpeed
+//
+//  Created by lieyunye on 2020/6/18.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+public class PQFollowButton: UIButton {
+    public  let bgLayer = CAShapeLayer()
+
+    public  var attenBtn: UIButton = {
+        let attenBtn = UIButton(type: .custom)
+        attenBtn.isUserInteractionEnabled = false
+        attenBtn.setTitle("", for: .selected)
+        attenBtn.setImage(UIImage.init().BF_Image(named: "icon_oder"), for: .selected)
+        attenBtn.setTitle("+", for: .normal)
+        attenBtn.setImage(nil, for: .normal)
+        attenBtn.setTitleColor(UIColor.white, for: .normal)
+        attenBtn.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .bold)
+        attenBtn.layer.cornerRadius = 10
+//        attenBtn.layer.masksToBounds = true
+        attenBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+        attenBtn.tag = 2
+        attenBtn.titleEdgeInsets = UIEdgeInsets(top: -3, left: 0, bottom: 0, right: 0)
+        return attenBtn
+    }()
+
+    override public  init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(attenBtn)
+    }
+
+    required public  init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override public func layoutSubviews() {
+        super.layoutSubviews()
+        attenBtn.snp.makeConstraints { make in
+            make.edges.equalTo(self).inset(UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0))
+        }
+    }
+
+    public  func reset() {
+        attenBtn.isHidden = false
+        attenBtn.layer.removeAllAnimations()
+        bgLayer.removeAllAnimations()
+        bgLayer.sublayers?.forEach {
+            $0.removeAllAnimations()
+            $0.removeFromSuperlayer()
+        }
+        bgLayer.removeFromSuperlayer()
+    }
+
+    public  func start() {
+        let bounds = self.bounds
+
+        bgLayer.frame = bounds
+        bgLayer.cornerRadius = bounds.height / 2.0
+        bgLayer.masksToBounds = true
+        layer.addSublayer(bgLayer)
+
+        let bgScale = CABasicAnimation(keyPath: "transform.scale")
+        bgScale.fromValue = 1
+        bgScale.toValue = 1.2
+        bgScale.duration = 0.2
+        attenBtn.layer.add(bgScale, forKey: nil)
+
+        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
+            self.attenBtn.isHidden = true
+            let opacity = CAKeyframeAnimation(keyPath: "backgroundColor")
+            opacity.values = [UIColor.red.cgColor, UIColor.white.cgColor]
+            opacity.duration = 0.5
+            opacity.fillMode = .forwards
+            opacity.isRemovedOnCompletion = false
+            self.bgLayer.add(opacity, forKey: nil)
+
+            let bgLayer1 = CAShapeLayer()
+            let frame = CGRect(x: (bounds.size.width - bounds.size.height) / 2.0, y: 0, width: bounds.size.height, height: bounds.size.height)
+            bgLayer1.frame = frame
+            self.bgLayer.addSublayer(bgLayer1)
+
+            let linePath = UIBezierPath()
+
+            let radius: CGFloat = frame.size.width / 2.0
+            linePath.move(to: CGPoint(x: radius * 0.45, y: radius * 1.0))
+            linePath.addLine(to: CGPoint(x: radius * 0.84, y: radius * 1.32))
+            linePath.addLine(to: CGPoint(x: radius * 1.48, y: radius * 0.68))
+
+            let layer = CAShapeLayer()
+
+            layer.path = linePath.cgPath
+            layer.fillColor = UIColor.clear.cgColor
+            layer.strokeColor = UIColor.hexColor(hexadecimal: "#EE0051").cgColor
+            layer.lineWidth = 2.5
+            bgLayer1.addSublayer(layer)
+
+            let animation = CABasicAnimation()
+            animation.duration = 0.5
+            animation.keyPath = "strokeEnd"
+            animation.fromValue = 0
+            animation.toValue = 1
+            animation.fillMode = .forwards
+            animation.isRemovedOnCompletion = false
+
+            layer.add(animation, forKey: "strokeEnd")
+
+            let scale = CABasicAnimation(keyPath: "transform.scale")
+            scale.fromValue = 1
+            scale.toValue = 0
+            scale.duration = 0.2
+            scale.beginTime = CACurrentMediaTime() + 1.5
+            scale.fillMode = .forwards
+            scale.isRemovedOnCompletion = false
+            self.bgLayer.add(scale, forKey: nil)
+        }
+    }
+}

+ 41 - 0
BFCommonKit/Classes/Base/View/PQGIFImageView.swift

@@ -0,0 +1,41 @@
+//
+//  PQGIFImageView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/9/2.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+public class PQGIFImageView: UIImageView {
+   public var imagesDara: [String]? {
+        didSet {
+            generateImages()
+            displayGIF(1, Int.max)
+        }
+    }
+
+    public var images: [UIImage] = Array<UIImage>.init()
+    public func generateImages() {
+        if imagesDara != nil, (imagesDara?.count ?? 0) > 0 {
+            for item in imagesDara! {
+                let image = UIImage(named: item)!
+                images.append(image)
+            }
+        }
+    }
+
+    public func displayGIF(_ duration: TimeInterval, _ repeatCount: Int) {
+        if !isAnimating {
+            layer.removeAllAnimations()
+            if images.count <= 0 {
+                return
+            }
+            animationImages = images
+            animationDuration = duration
+            animationRepeatCount = repeatCount
+            startAnimating()
+        }
+    }
+}

+ 53 - 0
BFCommonKit/Classes/Base/View/PQHeartAnimation.swift

@@ -0,0 +1,53 @@
+//
+//  PQHeartAnimation.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/11.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+public class PQHeartAnimation: NSObject {
+    static let angleArr: [CGFloat] = [CGFloat.pi / 4.0, -CGFloat.pi / 4.0, 0.0]
+    var isRepeat: Bool = false
+
+    static public func showAnimation(isRepeat: Bool = false, point: CGPoint, size: CGFloat = 80.0, baseView: UIView, completeHander: @escaping (_ isFinised: Bool) -> Void) {
+        if isRepeat, baseView.viewWithTag(cHeartTag) != nil {
+            return
+        }
+        let imgV = UIImageView(frame: CGRect(x: point.x - size / 2.0, y: point.y - size / 2.0, width: size, height: size))
+        imgV.tag = cHeartTag
+        imgV.image = UIImage.init().BF_Image(named:  "ic_heart")
+        imgV.contentMode = .scaleAspectFill
+        baseView.addSubview(imgV)
+
+        // 偏移角度
+        var num = 2
+        if !isRepeat {
+            num = Int(arc4random_uniform(3))
+        }
+//      BFLog(message: "num = \(num)")
+        imgV.transform = CGAffineTransform(rotationAngle: angleArr[num])
+        // 放大动画
+        let animation = CAKeyframeAnimation(keyPath: "transform.scale")
+        animation.duration = 0.5
+        animation.calculationMode = CAAnimationCalculationMode.cubic
+        animation.values = [1.3, 0.8, 1.0]
+        imgV.layer.add(animation, forKey: "transform.scale")
+
+        UIView.animate(withDuration: 1, delay: 0.5, options: .layoutSubviews, animations: {
+            imgV.alpha = 0.0
+            var newFrame = imgV.frame
+            newFrame.origin.x -= (isRepeat ? -cDefaultMargin * 2 : cDefaultMargin)
+            newFrame.origin.y -= 45.0
+            newFrame.size.height += 10.0
+            newFrame.size.width += 10.0
+            imgV.frame = newFrame
+        }) { isOK in
+            imgV.layer.removeAllAnimations()
+            imgV.removeFromSuperview()
+            completeHander(isOK)
+        }
+    }
+}

+ 130 - 0
BFCommonKit/Classes/Base/View/PQLoadingHUB.swift

@@ -0,0 +1,130 @@
+//
+//  PQLoadingHUB.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/5.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+public class PQLoadingHUBView: UIView {
+    // gif每一帧图
+    public var gifImages: [UIImage]?
+    // gif播放时长
+    public var duration: Double?
+
+    public lazy var loadingImage: UIImageView = {
+        let loadingImage = UIImageView()
+        loadingImage.tintColor = UIColor.hexColor(hexadecimal: PQBFConfig.shared.styleColor.rawValue)
+        return loadingImage
+    }()
+
+    override public init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(loadingImage)
+        isUserInteractionEnabled = false
+        let data = try? Data(contentsOf: URL(fileURLWithPath: Bundle().BF_mainbundle().path(forResource: "stuckPoint_music_loading", ofType: ".gif")!))
+        if data != nil {
+            PQPHAssetVideoParaseUtil.parasGIFImage(data: data!, isRenderingColor: UIColor.hexColor(hexadecimal: PQBFConfig.shared.styleColor.rawValue)) { [weak self] _, images, duration in
+                self?.gifImages = images
+                self?.duration = duration
+            }
+        }
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override public func layoutSubviews() {
+        super.layoutSubviews()
+        // 334 * 307
+        let imageW: CGFloat = 67
+        let imageH: CGFloat = 62
+        loadingImage.frame = CGRect(x: (frame.width - imageW) / 2, y: (frame.height - imageW) / 2, width: imageW, height: imageH)
+    }
+
+    /// 开始加载
+    public func loading() {
+        loadingImage.displayGIF(data: nil, images: gifImages, repeatCount: .max, duration: duration ?? 2)
+    }
+
+    /// 停止加载
+    public func endLoading() {
+        loadingImage.removePlayGIF()
+    }
+
+    override public func removeFromSuperview() {
+        loadingImage.removePlayGIF()
+        loadingImage.removeFromSuperview()
+    }
+}
+
+public class PQLoadingHUB: NSObject {
+    public static let shared = PQLoadingHUB()
+    public let viewTag = 11111
+    public var isLoading: Bool = false
+
+    public func showHUB() {
+        DispatchQueue.main.async { [weak self] in
+            let window = UIApplication.shared.keyWindow
+            if (window?.viewWithTag(self!.viewTag)) == nil {
+                let loadingHUB: PQLoadingHUBView = PQLoadingHUBView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
+                loadingHUB.tag = self!.viewTag
+                window?.addSubview(loadingHUB)
+                loadingHUB.center = window?.center as! CGPoint
+                loadingHUB.loading()
+                self?.isLoading = true
+            }
+        }
+    }
+
+    public func dismissHUB() {
+        DispatchQueue.main.async { [weak self] in
+            let window = UIApplication.shared.keyWindow
+            if (window?.viewWithTag(self!.viewTag)) != nil {
+                window?.viewWithTag(self!.viewTag)?.removeFromSuperview()
+                self?.isLoading = false
+            }
+        }
+    }
+
+    public func showHUB(superView: UIView, isVerticality: Bool = false) {
+        DispatchQueue.main.async { [weak self] in
+            if superView.viewWithTag(self!.viewTag) == nil {
+                let hubW: CGFloat = 100
+                let supW: CGFloat = superView.frame.width
+                let supH: CGFloat = superView.frame.height
+                let hubY: CGFloat = isVerticality ? ((supW - hubW) / 2) : ((supH - hubW) / 2)
+                let hubX: CGFloat = isVerticality ? ((supH - hubW) / 2) : ((supW - hubW) / 2)
+                let loadingHUB: PQLoadingHUBView = PQLoadingHUBView(frame: CGRect(x: hubX, y: hubY, width: 100, height: 100))
+                loadingHUB.tag = self!.viewTag
+                superView.addSubview(loadingHUB)
+                loadingHUB.loading()
+                self?.isLoading = true
+            }
+        }
+    }
+
+    public func dismissHUB(superView: UIView) {
+        DispatchQueue.main.async { [weak self] in
+            if superView.viewWithTag(self!.viewTag) != nil {
+                superView.viewWithTag(self!.viewTag)?.removeFromSuperview()
+                self?.isLoading = false
+            }
+        }
+    }
+
+    override private init() {
+        super.init()
+    }
+
+    override public func copy() -> Any {
+        return self
+    }
+
+    override public func mutableCopy() -> Any {
+        return self
+    }
+}

+ 515 - 0
BFCommonKit/Classes/Base/View/PQRemindView.swift

@@ -0,0 +1,515 @@
+
+// MARK: - 设置页退出登录跟注销账号提示视图
+
+/// 设置页退出登录跟注销账号提示视图
+open class PQRemindView: UIView {
+    public var isBanned: Bool = false // 是否是拉黑用户提示
+    public var isBlank: Bool = false { // 是否是黑色弹窗
+        didSet {
+            if isBlank {
+                contentView.backgroundColor = UIColor.hexColor(hexadecimal: "#212223")
+                titleLab.textColor = UIColor.white
+                contentLab.textColor = UIColor.white
+                cancelBtn.setTitleColor(UIColor.white, for: .normal)
+                confirmBtn.setTitleColor(UIColor.white, for: .normal)
+            } else {
+                contentView.backgroundColor = UIColor.white
+                contentLab.textColor = UIColor.hexColor(hexadecimal: "#666666")
+                titleLab.textColor = UIColor.black
+                cancelBtn.setTitleColor(UIColor.black, for: .normal)
+                confirmBtn.setTitleColor(UIColor.black, for: .normal)
+            }
+        }
+    }
+
+    public var remindBlock: ((_ sender: UIButton, _ remindData: PQBaseModel?) -> Void)?
+
+    lazy public var contentView: UIView = {
+        let contentView = UIView()
+        contentView.backgroundColor = UIColor.white
+        contentView.addCorner(corner: 4)
+        return contentView
+    }()
+
+    lazy public var titleLab: UILabel = {
+        let titleLab = UILabel()
+        titleLab.font = UIFont.systemFont(ofSize: 18, weight: .medium)
+        titleLab.textAlignment = .center
+        titleLab.numberOfLines = 0
+        titleLab.textColor = UIColor.black
+        return titleLab
+    }()
+
+    lazy public var contentLab: UILabel = {
+        //        let contentLab = TYAttributedLabel.init()
+        //        contentLab.textAlignment = CTTextAlignment.center
+        //        contentLab.verticalAlignment = .center
+        //        contentLab.highlightedLinkBackgroundColor = UIColor.white
+        //        contentLab.numberOfLines = 0
+        //        contentLab.font = UIFont.systemFont(ofSize: 14)
+        //        contentLab.textColor = UIColor.hexColor(hexadecimal: "#666666")
+        let contentLab = UILabel()
+        contentLab.font = UIFont.systemFont(ofSize: 16)
+        contentLab.textAlignment = .center
+        contentLab.numberOfLines = 0
+        contentLab.textColor = UIColor.hexColor(hexadecimal: "#666666")
+        return contentLab
+    }()
+
+    lazy public var cancelBtn: UIButton = {
+        let cancelBtn = UIButton(type: .custom)
+        cancelBtn.setTitle("取消", for: .normal)
+        cancelBtn.setTitleColor(UIColor.black, for: .normal)
+        cancelBtn.tag = 1
+        cancelBtn.addTarget(self, action: #selector(btnClck(sender:)), for: .touchUpInside)
+        cancelBtn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
+        return cancelBtn
+    }()
+
+    lazy public var confirmBtn: UIButton = {
+        let confirmBtn = UIButton(type: .custom)
+        confirmBtn.setTitle("确定", for: .normal)
+        confirmBtn.setTitleColor(UIColor.black, for: .normal)
+        confirmBtn.tag = 2
+        confirmBtn.addTarget(self, action: #selector(btnClck(sender:)), for: .touchUpInside)
+        confirmBtn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
+        return confirmBtn
+    }()
+
+    lazy public var verticalLine: UIView = {
+        let verticalLine = UIView()
+        verticalLine.backgroundColor = UIColor.hexColor(hexadecimal: "#E5E5E5")
+        return verticalLine
+    }()
+
+    lazy public var horizonLine: UIView = {
+        let horizonLine = UIView()
+        horizonLine.backgroundColor = UIColor.hexColor(hexadecimal: "#E5E5E5")
+        return horizonLine
+    }()
+
+    override public init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(contentView)
+        contentView.addSubview(titleLab)
+        contentView.addSubview(contentLab)
+        contentView.addSubview(cancelBtn)
+        contentView.addSubview(confirmBtn)
+        contentView.addSubview(verticalLine)
+        contentView.addSubview(horizonLine)
+        backgroundColor = cShadowColor
+    }
+
+    required public init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    public var  remindData: PQBaseModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    open func addData() {
+        titleLab.text = remindData?.title
+        contentLab.text = remindData?.summary
+        cancelBtn.setTitleColor(isBanned ? (isBlank ? UIColor.white : UIColor.black) : (isBlank ? UIColor.white : UIColor.hexColor(hexadecimal: "#666666")), for: .normal)
+        if isBanned {
+            titleLab.textAlignment = .center
+        } else {
+            titleLab.textAlignment = .left
+        }
+    }
+
+   open func addLayout() {
+        var summH: CGFloat = 0
+        var titleH: CGFloat = 0
+        if (remindData?.summary?.count ?? 0) > 0 {
+            summH = sizeWithText(text: remindData?.summary ?? "", font: UIFont.systemFont(ofSize: 16, weight: .medium), size: CGSize(width: cScreenWidth - cDefaultMargin * 12, height: CGFloat.greatestFiniteMagnitude)).height + cDefaultMargin
+        }
+        if (remindData?.title?.count ?? 0) > 0 {
+            titleH = sizeWithText(text: remindData?.title ?? "", font: UIFont.systemFont(ofSize: 18, weight: .medium), size: CGSize(width: cScreenWidth - cDefaultMargin * 12, height: CGFloat.greatestFiniteMagnitude)).height + cDefaultMargin
+            if titleH < cDefaultMargin * 2 {
+                titleH = cDefaultMargin * 2
+            }
+        }
+        let contentH: CGFloat = cDefaultMargin * 2 + (titleH > 0 ? titleH + cDefaultMargin : 0) + (summH > 0 ? summH + cDefaultMargin : 0) + cDefaultMargin * 3 + (isBanned ? cDefaultMargin * 2 : 0)
+        contentView.snp.makeConstraints { make in
+            make.left.equalTo(self).offset(cDefaultMargin * 4)
+            make.right.equalTo(self).offset(-cDefaultMargin * 4)
+            make.height.equalTo(contentH)
+            make.center.equalTo(self)
+        }
+        titleLab.snp.makeConstraints { make in
+            make.left.equalTo(contentView).offset(cDefaultMargin * 2)
+            make.right.equalTo(contentView).offset(-cDefaultMargin * 2)
+            make.height.equalTo(titleH)
+            make.top.equalTo(contentView).offset(cDefaultMargin * 2)
+        }
+        contentLab.snp.makeConstraints { make in
+            make.top.equalTo(titleLab.snp_bottom).offset(summH > 0 ? cDefaultMargin : 0)
+            make.left.right.equalTo(titleLab)
+            make.height.equalTo(summH)
+        }
+        if isBanned {
+            let btnW: CGFloat = (cScreenWidth - 1 - cDefaultMargin * 8) / 2
+            verticalLine.snp.makeConstraints { make in
+                make.top.equalTo(contentLab.snp_bottom).offset(cDefaultMargin)
+                make.height.equalTo(1)
+                make.left.width.equalToSuperview()
+            }
+            cancelBtn.snp.makeConstraints { make in
+                make.left.equalToSuperview()
+                make.top.equalTo(verticalLine.snp_bottom)
+                make.width.equalTo(btnW)
+                make.bottom.equalTo(contentView)
+            }
+            horizonLine.snp.makeConstraints { make in
+                make.left.equalTo(cancelBtn.snp_right)
+                make.height.top.equalTo(cancelBtn)
+                make.width.equalTo(1)
+            }
+            confirmBtn.snp.makeConstraints { make in
+                make.right.equalToSuperview()
+                make.height.width.bottom.equalTo(cancelBtn)
+            }
+        } else {
+            confirmBtn.snp.makeConstraints { make in
+                make.right.equalTo(titleLab)
+                make.height.equalTo(cDefaultMargin * 4)
+                make.width.equalTo(cDefaultMargin * 6)
+                make.bottom.equalTo(contentView).offset(-cDefaultMargin)
+            }
+            cancelBtn.snp.makeConstraints { make in
+                make.right.equalTo(confirmBtn.snp_left).offset(-cDefaultMargin)
+                make.height.equalTo(cDefaultMargin * 4)
+                make.width.equalTo(cDefaultMargin * 6)
+                make.bottom.equalTo(confirmBtn)
+            }
+        }
+    }
+
+    @objc open func btnClck(sender: UIButton) {
+        removeFromSuperview()
+        if remindBlock != nil {
+            remindBlock!(sender, remindData!)
+        }
+    }
+}
+
+// MARK: 空白提示页
+
+/// 空白提示页
+open class PQEmptyRemindView: UIView {
+    // 回调
+    public var fullRefreshBloc: ((_ isNetConnected: Bool, _ emptyData: PQEmptyModel?) -> Void)?
+
+    lazy public var imageView: UIImageView = {
+        let imageView = UIImageView()
+        imageView.backgroundColor = UIColor.clear
+        imageView.contentMode = .scaleAspectFit
+        return imageView
+    }()
+
+    lazy public var remindLab: UILabel = {
+        let remindLab = UILabel()
+        remindLab.font = UIFont.systemFont(ofSize: 16)
+        remindLab.numberOfLines = 1
+        remindLab.textAlignment = NSTextAlignment.center
+        remindLab.textColor = UIColor.white
+        return remindLab
+    }()
+
+//    lazy public var shimmeringView: FBShimmeringView = {
+//        let shimmeringView = FBShimmeringView()
+//        shimmeringView.isShimmering = false
+//        shimmeringView.shimmeringBeginFadeDuration = 0.3
+//        shimmeringView.shimmeringEndFadeDuration = 0.1
+//        shimmeringView.shimmeringOpacity = 0.2
+//        shimmeringView.shimmeringSpeed = 300
+//        shimmeringView.contentView = remindLab
+//        return shimmeringView
+//    }()
+
+    lazy public var remindSubLab: UILabel = {
+        let remindSubLab = UILabel()
+        remindSubLab.font = UIFont.systemFont(ofSize: 14)
+        remindSubLab.numberOfLines = 1
+        remindSubLab.textAlignment = NSTextAlignment.center
+        remindSubLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
+        return remindSubLab
+    }()
+
+    lazy public var refreshBtn: UIButton = {
+        let refreshBtn = UIButton(type: .custom)
+        refreshBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+        refreshBtn.titleLabel?.font = UIFont.systemFont(ofSize: 15)
+        refreshBtn.setTitleColor(UIColor.white, for: .normal)
+        refreshBtn.setTitle("刷新", for: .normal)
+        refreshBtn.setTitle("重新连接网络", for: .selected)
+        refreshBtn.addCorner(corner: cDefaultMargin * 2)
+        refreshBtn.isHidden = true
+        refreshBtn.addTarget(self, action: #selector(fullRefresh), for: .touchUpInside)
+        return refreshBtn
+    }()
+
+    override public init(frame: CGRect) {
+        super.init(frame: frame)
+        addSubview(imageView)
+//        addSubview(shimmeringView)
+        addSubview(remindSubLab)
+        addSubview(refreshBtn)
+        let ges = UITapGestureRecognizer(target: self, action: #selector(fullRefresh))
+        addGestureRecognizer(ges)
+        backgroundColor = PQBFConfig.shared.styleBackGroundColor
+    }
+
+    required public init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    @objc public var emptyData: PQEmptyModel? {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+}
+
+extension PQEmptyRemindView {
+    public func addData() {
+        if !isNetConnected() {
+            remindLab.text = "网络连接失败,请检查网络后重试"
+            remindSubLab.isHidden = true
+            refreshBtn.isHidden = false
+            refreshBtn.backgroundColor = UIColor.hexColor(hexadecimal: "#EE0051")
+            refreshBtn.setTitleColor(UIColor.white, for: .normal)
+            refreshBtn.setTitle("刷新", for: .normal)
+            imageView.image = UIImage.init().BF_Image(named:  "pic_network")
+        } else {
+            if emptyData?.emptyImage != nil, emptyData?.emptyImage?.count ?? 0 > 0 {
+                imageView.image = UIImage.init().BF_Image(named:  emptyData?.emptyImage ?? "")
+            } else {
+                imageView.image = nil
+            }
+            remindLab.text = emptyData?.title
+            remindSubLab.text = emptyData?.summary
+            refreshBtn.isHidden = emptyData?.isRefreshHidden ?? true
+            if emptyData?.refreshImage != nil, (emptyData?.refreshImage?.count ?? 0) > 0 {
+                refreshBtn.setImage(UIImage.init().BF_Image(named:  emptyData?.refreshImage ?? ""), for: .normal)
+            } else {
+                refreshBtn.setImage(nil, for: .normal)
+            }
+            refreshBtn.setAttributedTitle(emptyData?.refreshTitle, for: .normal)
+            if emptyData?.refreshBgColor != nil {
+                refreshBtn.backgroundColor = emptyData?.refreshBgColor
+            }
+        }
+    }
+
+    override public var isHidden: Bool {
+        didSet {
+            addData()
+            addLayout()
+        }
+    }
+
+    public func addLayout() {
+        var imageH: CGFloat = cDefaultMargin * 7
+        var contentH: CGFloat = 0
+
+        if (emptyData?.emptyImage != nil &&  emptyData?.emptyImage?.count ?? 0 > 0) || !isNetConnected() {
+            contentH = contentH + imageH + cDefaultMargin
+        } else {
+            imageH = 0
+        }
+        if emptyData?.title != nil, emptyData?.title?.count ?? 0 > 0 {
+            contentH = contentH + cDefaultMargin * 2 + cDefaultMargin
+        }
+        if emptyData?.summary != nil, emptyData?.summary?.count ?? 0 > 0 {
+            contentH = contentH + cDefaultMargin * 2 + cDefaultMargin
+        }
+        if !refreshBtn.isHidden {
+            contentH = contentH + cDefaultMargin + cDefaultMargin * 4
+        }
+        let topY = (frame.height - contentH) / 2
+        imageView.snp.remakeConstraints { make in
+            make.top.equalTo(topY)
+            make.centerX.equalTo(self)
+            make.height.equalTo(imageH)
+        }
+//        shimmeringView.snp.makeConstraints { make in
+//            make.left.right.equalTo(self)
+//            make.top.equalTo(imageView.snp_bottom).offset(cDefaultMargin)
+//        }
+        remindLab.snp.remakeConstraints { make in
+            make.size.equalToSuperview()
+        }
+        remindSubLab.snp.makeConstraints { make in
+            make.left.right.equalTo(self)
+            make.top.equalTo(remindLab.snp_bottom).offset(cDefaultMargin)
+        }
+        refreshBtn.snp.makeConstraints { make in
+            make.width.equalTo(cDefaultMargin * 13)
+            make.height.equalTo(cDefaultMargin * 4)
+            make.top.equalTo(remindSubLab.snp_bottom).offset(cDefaultMargin)
+            make.centerX.equalToSuperview()
+        }
+    }
+
+    @objc func fullRefresh() {
+        let isConnected: Bool = isNetConnected()
+        if !isConnected {
+            cShowHUB(superView: nil, msg: "网络不给力")
+        }
+        if fullRefreshBloc != nil {
+            fullRefreshBloc!(isConnected, emptyData)
+        }
+    }
+
+    func addShimmeringView() {}
+
+    func removeShimmeringView() {}
+}
+
+// MARK: - 上传提示框
+
+/// 上传提示框
+public class PQUploadRemindView: PQRemindView {
+    public var canMoreOpration: Bool = false // 是否还有更多操作
+    public var confirmTitle: String = "我知道了"
+    public var cancelTitle: String = "取消"
+    public var cacelColor: UIColor = UIColor.hexColor(hexadecimal: "#666666")
+    public var attributedTitle: NSAttributedString?
+
+    override public func addData() {
+        titleLab.textAlignment = .center
+        if attributedTitle != nil {
+            titleLab.attributedText = attributedTitle
+        } else {
+            titleLab.text = remindData?.title
+        }
+        if remindData?.attributedTitle != nil {
+            contentLab.attributedText = remindData?.attributedTitle
+        } else {
+            contentLab.text = remindData?.summary
+        }
+        cancelBtn.setTitleColor(cacelColor, for: .normal)
+        confirmBtn.setTitleColor(UIColor.hexColor(hexadecimal: "#EE0051"), for: .normal)
+        confirmBtn.setTitle(confirmTitle, for: .normal)
+        cancelBtn.setTitle(cancelTitle, for: .normal)
+        let ges = UITapGestureRecognizer(target: self, action: #selector(removeView))
+        addGestureRecognizer(ges)
+    }
+
+    override public func addLayout() {
+        let titleH: CGFloat = ((remindData?.title != nil && (remindData?.title?.count ?? 0) > 0) || attributedTitle != nil) ? cDefaultMargin * 2 : 0
+
+        let summH: CGFloat = sizeTextFits(attributedText: remindData?.attributedTitle != nil ? remindData?.attributedTitle : NSMutableAttributedString(string: remindData?.summary ?? ""), text: nil, numberOfLines: 0, font: UIFont.systemFont(ofSize: 14), maxSize: CGSize(width: cScreenWidth - cDefaultMargin * 12, height: CGFloat.greatestFiniteMagnitude)).height + cDefaultMargin
+
+        let contentH: CGFloat = (titleH == 0 ? 0 : cDefaultMargin * 3) + cDefaultMargin * 2 + summH + cDefaultMargin + cDefaultMargin * 5
+
+        contentView.snp.makeConstraints { make in
+            make.left.equalTo(self).offset(cDefaultMargin * 3)
+            make.right.equalTo(self).offset(-cDefaultMargin * 3)
+            make.height.equalTo(contentH)
+            make.center.equalTo(self)
+        }
+        titleLab.snp.makeConstraints { make in
+            make.left.equalTo(contentView).offset(cDefaultMargin * 2)
+            make.right.equalTo(contentView).offset(-cDefaultMargin * 2)
+            make.height.equalTo(titleH)
+            make.top.equalTo(contentView).offset(titleH == 0 ? 0 : cDefaultMargin * 2)
+        }
+        contentLab.snp.makeConstraints { make in
+            make.top.equalTo(titleLab.snp_bottom).offset(cDefaultMargin)
+            make.left.right.equalTo(titleLab)
+            make.height.equalTo(summH)
+        }
+        if canMoreOpration {
+            let btnW: CGFloat = (cScreenWidth - 1 - cDefaultMargin * 8) / 2
+            verticalLine.snp.makeConstraints { make in
+                make.top.equalTo(contentLab.snp_bottom).offset(cDefaultMargin)
+                make.height.equalTo(1)
+                make.left.width.equalToSuperview()
+            }
+            cancelBtn.snp.makeConstraints { make in
+                make.left.equalToSuperview()
+                make.top.equalTo(verticalLine.snp_bottom)
+                make.width.equalTo(btnW)
+                make.bottom.equalTo(contentView)
+            }
+            horizonLine.snp.makeConstraints { make in
+                make.left.equalTo(cancelBtn.snp_right)
+                make.height.top.equalTo(cancelBtn)
+                make.width.equalTo(1)
+            }
+            confirmBtn.snp.makeConstraints { make in
+                make.right.equalToSuperview()
+                make.height.width.bottom.equalTo(cancelBtn)
+            }
+        } else {
+            verticalLine.snp.makeConstraints { make in
+                make.top.equalTo(contentLab.snp_bottom).offset(cDefaultMargin)
+                make.height.equalTo(1)
+                make.left.width.equalToSuperview()
+            }
+            confirmBtn.snp.makeConstraints { make in
+                make.right.left.equalTo(titleLab)
+                make.top.equalTo(verticalLine.snp_bottom)
+                make.bottom.equalTo(contentView)
+            }
+        }
+    }
+
+    @objc func removeView() {
+        removeFromSuperview()
+    }
+
+    @objc public override func btnClck(sender: UIButton) {
+        removeFromSuperview()
+        if remindBlock != nil {
+            remindBlock!(sender, remindData!)
+        }
+    }
+
+    /// 快速生成
+    /// - Parameters:
+    ///   - title: <#title description#>
+    ///   - summary: <#summary description#>
+    ///   - confirmTitle: <#confirmTitle description#>
+    ///   - remindHandle: <#remindHandle description#>
+    /// - Returns: <#description#>
+    class public func showUploadRemindView(title: String?, attributedTitle: NSAttributedString? = nil, summary: String, canMoreOpration: Bool = false, confirmTitle: String?, cancelTitle: String? = nil, cancelColor: UIColor? = nil, remindHandle: @escaping (_ sender: UIButton, _ baseModel: PQBaseModel?) -> Void) {
+        if UIApplication.shared.keyWindow?.viewWithTag(cUploadViewRemindTag) != nil {
+            UIApplication.shared.keyWindow?.viewWithTag(cUploadViewRemindTag)?.removeFromSuperview()
+        }
+        let remindData = PQBaseModel()
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineSpacing = 3.0
+        paragraphStyle.alignment = .center
+        remindData.title = title
+        remindData.attributedTitle = NSMutableAttributedString(string: summary, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14), NSAttributedString.Key.foregroundColor: UIColor.hexColor(hexadecimal: "#666666"), NSAttributedString.Key.paragraphStyle: paragraphStyle])
+        let remindView = PQUploadRemindView(frame: CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenHeigth))
+        remindView.canMoreOpration = canMoreOpration
+        if confirmTitle != nil {
+            remindView.confirmTitle = confirmTitle!
+        }
+        if cancelTitle != nil {
+            remindView.cancelTitle = cancelTitle!
+        }
+        if cancelColor != nil {
+            remindView.cacelColor = cancelColor!
+        }
+        if attributedTitle != nil {
+            remindView.attributedTitle = attributedTitle
+        }
+        remindView.remindBlock = { sender, baseModel in
+            remindHandle(sender, baseModel)
+        }
+        remindView.tag = cUploadViewRemindTag
+        UIApplication.shared.keyWindow?.addSubview(remindView)
+        remindView.remindData = remindData
+    }
+}

+ 27 - 0
BFCommonKit/Classes/Base/View/PQTabBar.swift

@@ -0,0 +1,27 @@
+//
+//  PQTabBar.swift
+//  PQSpeed
+//
+//  Created by SanW on 2021/1/18.
+//  Copyright © 2021 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+public class PQTabBar: UITabBar {
+    override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+        if isHidden { // 如果隐藏的话不处理事件
+            return super.hitTest(point, with: event)
+        }
+        let view = super.hitTest(point, with: event)
+        if view == nil {
+            for subView in subviews {
+                let myPoint = subView.convert(point, from: self)
+                if subView.bounds.contains(myPoint) {
+                    return subView
+                }
+            }
+        }
+        return view
+    }
+}

+ 104 - 0
BFCommonKit/Classes/Base/View/PQTextView.swift

@@ -0,0 +1,104 @@
+//
+//  PQTextView.swift
+//  PQSpeed
+//
+//  Created by SanW on 2020/6/11.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import UIKit
+
+open class PQTextView: UITextView {
+    private let textValuesQueue = DispatchQueue(label: "com.BytesFlow.textValuesQueue", qos: .default)
+
+   public var textDidChangedHandle: ((_ textView: UITextView) -> Void)?
+    // 最多输入的个数
+    public  var maxTextLength : Int?
+    // 输入个数超过时提示
+    public  var maxTextLengthRemind : String?
+    /// setNeedsDisplay调用drawRect
+    public  var placeHolder: String = "" {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    /// placeHolder是否居中
+    public  var isCenter: Bool = false {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    public var placeHolderColor: UIColor = UIColor.gray {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    open override var font: UIFont? {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    open override var text: String! {
+        didSet{
+           setNeedsLayout()
+        }
+    }
+
+    open override var attributedText: NSAttributedString! {
+        didSet {
+            setNeedsDisplay()
+        }
+    }
+
+    public override init(frame: CGRect, textContainer: NSTextContainer?) {
+        super.init(frame: frame, textContainer: textContainer)
+        /// default字号
+        PQNotification.addObserver(self, selector: #selector(textDidChanged(noti:)), name: UITextView.textDidChangeNotification, object: self)
+    }
+
+    required public init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    @objc func textDidChanged(noti _: NSNotification) {
+        if maxTextLength != nil && text.count > maxTextLength! {
+            text = String(text.prefix(maxTextLength!))
+            cShowHUB(superView: nil, msg: (maxTextLengthRemind != nil && (maxTextLengthRemind?.count ?? 0) > 0) ? maxTextLengthRemind : "最多输入\( maxTextLength!)个字符")
+        }
+        setNeedsDisplay()
+        if textDidChangedHandle != nil {
+            textDidChangedHandle!(self)
+        }
+    }
+
+    open override func draw(_ rect: CGRect) {
+        if hasText {
+            return
+        }
+        let size = sizeWithText(text: placeHolder, font: font ?? UIFont.systemFont(ofSize: 14), size: rect.size)
+        var newRect = CGRect()
+        newRect.size.width = size.width
+        newRect.size.height = size.height
+        if isCenter {
+            newRect.origin.x = (rect.width - size.width) / 2
+            newRect.origin.y = (rect.height - size.height) / 2
+        } else {
+            newRect.origin.x = placeHolder.contains("未识别到文字") ? 0 : 5
+            newRect.origin.y = placeHolder.contains("未识别到文字") ? 0 : 7
+        }
+        (placeHolder as NSString).draw(in: newRect, withAttributes: [NSAttributedString.Key.font: font ?? UIFont.systemFont(ofSize: 14), NSAttributedString.Key.foregroundColor: placeHolderColor])
+    }
+
+    open override func layoutSubviews() {
+        super.layoutSubviews()
+        setNeedsDisplay()
+    }
+
+    deinit {
+        PQNotification.removeObserver(self, name: UITextView.textDidChangeNotification, object: self)
+    }
+}

+ 791 - 0
BFCommonKit/Classes/Enums/Enums.swift

@@ -0,0 +1,791 @@
+//
+//  public  enum s.swift
+//  PQSpeed
+//
+//  Created by lieyunye on 2020/5/29.
+//  Copyright © 2020 BytesFlow. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - 视频播放页面类型
+
+/// 视频播放页面类型
+public  enum  PQVIDEOPAGETYPE {
+    case PQVIDEOPAGETYPE_Normal // 默认-单个视频详情
+    case PQVIDEOPAGETYPE_RECOMM // 默认-推荐
+    case PQVIDEOPAGETYPE_ATTEN // 关注
+    case PQVIDEOPAGETYPE_DETAIL // 用户发布视频详情
+    case PQVIDEOPAGETYPE_LIKEDETAIL // 我的喜欢视频详情
+    case PQVIDEOPAGETYPE_ATTUSERDETAIL // 关注用户详情
+    case PQVIDEOPAGETYPE_SEARCHDETAIL // 搜索详情
+    case PQVIDEOPAGETYPE_HOTVIDEODETAIL // 热门视频详情
+    case PQVIDEOPAGETYPE_GUIDEVIDEODETAIL // 引导视频详情
+    case PQVIDEOPAGETYPE_SINGLEVIDEODETAIL // 单个视频详情
+    case PQVIDEOPAGETYPE_RECREATDETAIL // 再创作详情页
+}
+
+// MARK: - 视频播放状态
+
+/// 视频播放状态
+public  enum  PQVIDEO_PLAY_STATUS {
+    case PQVIDEO_PLAY_STATUS_LOADING // 加载中
+    case PQVIDEO_PLAY_STATUS_BEGIN // 开始播放
+    case PQVIDEO_PLAY_STATUS_END // 播放结束
+    case PQVIDEO_PLAY_STATUS_DISCONNECT // 重连失败
+    case PQVIDEO_PLAY_STATUS_NOT_FOUND // 播放文件不存在
+    case PQVIDEO_PLAY_STATUS_RECONNECT // 重连连接
+}
+
+// MARK: - 页面场景
+
+/// 页面场景
+public  enum  PAGESOURCE: String {
+    /*************** 视频相关pageSource ***************/
+    case sp_category = "speedApp-category" //   首页-单列
+    case sp_category_recommend = "speedApp-category_recommend" // 由首页单列右划
+    case sp_categoryDouble = "speedApp-categoryDouble" // 首页-双列
+    case sp_videoDetail = "speedApp-videoDetail" // 视频详情页
+    case sp_videoDetail_search = "speedApp-videoDetail_search" //    由搜索结果列表进入
+    case sp_videoDetail_search_recommend = "speedApp-videoDetail_search_recommend" // 由搜索结果列表进入
+    case sp_videoDetail_upload = "speedApp-videoDetail_upload" //     由我的tab中“作品”列表进入
+    case sp_videoDetail_upload_recommend = "speedApp-videoDetail_upload_recommend" // 、由我的tab中“作品”列表进入
+    case sp_videoDetail_favorite = "speedApp-videoDetail_favorite" // 由我的喜欢列表进入
+    case sp_videoDetail_share = "speedApp-videoDetail_share" // 由我的分享列表进入
+    case sp_videoDetail_favorite_recommend = "speedApp-videoDetail_favorite_recommend" // 由我的喜欢列表推荐
+    case sp_userHomePage = "speedApp-userHomePage" // 用户个人主页
+    case sp_videoDetail_userHomePage = "speedApp-videoDetail_userHomePage" // 由用户个人主页进入
+    case sp_videoDetail_userHomePage_recommend = "speedApp-videoDetail_userHomePage_recommend" // 由用户个人主页进入
+    case sp_recommendBottom = "speedApp-recommendBottom" //    底部展示的3个相关推荐视频
+    case sp_follow = "speedApp-follow" // 关注tab
+    case sp_follow_recommend = "speedApp-follow_recommend" // 由关注列表右划
+    case sp_followSingle = "speedApp-followSingle" // 关注tab点击顶部个人头像进入某个人的关注
+    case sp_followSingle_recommend = "speedApp-followSingle_recommend" // 关注tab点击顶部个人头像进入某个人的关注
+    case sp_mine = "speedApp-mine" // 我的tab
+    case sp_search = "speedApp-search" // 搜索页
+    case sp_uploadVideo = "speedApp-uploadVideo" // 视频制作入口
+
+    /*************** 活动相关pageSource ***************/
+    case sp_activity_entranceButton = "speedApp_playActivity_entranceButton" // 活动活动入口按键
+    case sp_activity_invite = "speedApp_playActivity_invite" // H5页面内邀请好友按键
+    case sp_activity_threeDot = "speedApp_playActivity_threeDot" // APP右上角…按键
+    case sp_activity_shareFriend = "speedApp_playActivity_shareFriend" // 进一步点击分享好友
+    case sp_activity_shareMoment = "speedApp_playActivity_shareMoment" // 进一步点击分享朋友圈
+    case sp_activity_openH5 = "speedApp_playActivity_completeOpenH5" // 当天完成活动的弹窗->去查看
+    case sp_activity_openH5AndPayment = "speedApp_playActivity_completeOpenH5AndPayment" // 当天完成活动的弹窗->报名明日
+    case sp_activity_failOpenH5 = "speedApp_playActivity_missionFailWindowOpenH5" // 未成功弹窗->继续活动
+    case sp_activity_success_share = "speedApp_playActivity_missionSuccessWindow_share" // 成功弹窗->分享给好友炫耀下
+    case sp_activity_entrance_close = "speedApp_playActivity_entrance_close" // 活动入口弹窗->关闭按钮
+    case sp_activity_entranceWindow = "speedApp_playActivity_entranceWindow" // 活动入口弹窗
+    case sp_activity_successWindow = "speedApp_playActivity_missionSuccessWindow" // 成功弹窗
+    case sp_activity_failWindow = "speedApp_playActivity_missionFailWindow" // 未成功弹窗
+    case sp_activity_completeWindow = "speedApp_playActivity_missionCompleteWindow" //  当天完成活动的弹窗
+
+    /*************** 上传相关pageSource ***************/
+    case sp_upload_videoSelect = "speedApp-upload_videoSelect" //  选视频-进入页面事件
+    case sp_upload_coverSelect = "speedApp-upload_coverSelect" //  选封面-进入页面事件
+    case sp_upload_videoPublish = "speedApp-upload_videoPublish" //  发布-进入页面事件
+
+    /*************** tab点击相关pageSource ***************/
+    case sp_categoryTabButton = "speedApp-categoryTabButton" //  首页tab
+    case sp_followTabButton = "speedApp-followTabButton" //  关注tab
+    case sp_uploadTabButton = "speedApp-uploadTabButton" //  上传tab
+    case sp_mineTabButton = "speedApp-mineTabButton" //  我的tab
+    case sp_msg_tabBtn = "speedApp_msgTabButton" //  消息tab
+
+    /*************** 发布pageSource ***************/
+    case sp_videoMaking = "speedApp-videoMaking" // 视频创作
+    case sp_videoCompose_guid = "speedApp-videoCompose_guid" // 预览页面
+    case sp_videoCompose_edit = "speedApp-videoCompose_edit" // 编辑页面
+    case sp_videoCompose_overview = "speedApp-videoCompose_overview" // 总览页面
+    case sp_videoCompose_composition = "speedApp-videoCompose_composition" // 合成页面
+    case sp_material_search = "vlog-pages/user-videos-share" // 素材搜索上报
+
+    /*************** 消息pageSource ***************/
+    case sp_msg_shareSpace = "speedApp-message" // 分享空间
+    case sp_msg_shareSpace_detail = "speedApp-message_shareSpace" // 分享空间详情
+    case sp_msg_share = "speedApp-message_share" // 分享
+    case sp_msg_like = "speedApp-message_like" // 喜欢
+    case sp_msg_fans = "speedApp-message_fans" // 粉丝
+    case sp_msg_push = "speedApp-message_push" // 通知
+
+    /*************** 草稿箱相关pageSource ***************/
+    case sp_draft_projectList = "speedApp-projectList" // 草稿箱列表
+    case sp_reproduce_childList = "speedApp-reproduceCollection_child" // 再制作子列表页面
+    case sp_reproduce_fatherList = "speedApp-reproduceCollection_father" // 再制作父列表页面
+    case sp_reproduce_chilDetailList = "speedApp-videoDetail_reproduceCollection_child" // 再制作子列表页面
+    case sp_reproduce_fatherDetailList = "speedApp-videoDetail_reproduceCollection_father" // 再制作父列表页面
+    // add by ak
+    case sp_speedApp_upload2Compose = "speedApp-upload2Compose" // 上传转创作
+
+    /*************** 卡点视频相关pageSource ***************/
+    case sp_stuck_selectMaterial = "speedApp-selectSyncedUpMaterial" // 卡点视频素材选择页
+    case sp_stuck_selectSynceedUpMusic = "speedApp-selectSynceedUpMusic" // 卡点视频音乐选择页
+    case sp_stuck_previewSyncedUp = "speedApp_previewSyncedUp" // 预览页面曝光上报
+    case sp_stuck_searchSyncedUpMusic = "speedApp_searchSyncedUpMusic" // 音乐素材搜索页
+    case sp_stuck_publishSyncedUp = "speedApp_publishSyncedUp" // 合成发布页
+}
+
+// MARK: - objectType
+
+/// objectType
+public  enum  objectType: String {
+    /*************** tab点击相关pageSource ***************/
+
+    case ot_home_tabBtn = "speedApp-categoryTabButton" //  首页tab
+    case ot_follow_tabBtn = "speedApp-followTabButton" //  关注tab
+    case ot_up_tabBtn = "speedApp-uploadTabButton" //  上传tab
+    case ot_mine_tabBtn = "speedApp-mineTabButton" //  我的tab
+    case ot_public_tabBtn = "speedApp-publicTabButton" //  发布tab
+    case ot_msg_tabBtn = "speedApp_msgTabButton" //  消息tab
+
+    /*************** 上传相关objectType ***************/
+    case ot_up_backBtn = "speedApp-uploadBackButton" // 点击左下角叉子
+    case ot_up_nextBtn = "speedApp-uploadNextButton" // 点击右下角下一步
+    case ot_up_coverBtn = "speedApp-uploadCoverButton" // 点击上传封面
+    case ot_up_pickCoverBtn = "speedApp-pickCoverButton" // 点击截取封面
+    case ot_up_publishBtn = "speedApp-videoPublishButton" // 点击发布
+    case ot_up_changeCoverBtn = "speedApp-changeCoverButton" // 点击选封面返回
+    case ot_up_start = "speedApp-uploadStart" // 上传开始事件
+    case ot_up_fail = "speedApp-uploadFail" // 上传中断/失败事件
+    case ot_up_restart = "speedApp-uploadRestart" // 上传重试事件
+    case ot_up_success = "speedApp-uploadSuccess" // 上传完成
+    case ot_up_publishSuccess = "speedApp-videoPublishSuccess" // 发布完成
+    case ot_up_viewPopup_guideUsersToShare = "speedApp_viewPopup_guideUsersToShare_pubishVideo" // 发布成功弹出分享界面
+    case ot_up_clickWechatMoments_guideUsersToShare = "speedApp_clickWechatMoments_guideUsersToShare_pubishVideo" // 分享介面点击微信朋友圈
+    case ot_up_clickWechat_guideUsersToShare = "speedApp_clickWechat_guideUsersToShare_pubishVideo"
+    case ot_selectVideoProductionMode = "speedApp_viewWindow_selectVideoProductionMode"
+    // 分享界面点击微信好友
+    /*************** 创作工具相关objectType ***************/
+    case ot_makevideo_video = "video" // 视频
+    case ot_makevideo_gif = "gif" // 动态图
+    case ot_makevideo_jpg = "jpg" // 图片
+    case ot_enterComposeToolButton = "speedApp_enterComposeToolButton" // 点击上传tab后-点击发布视频 add by ak & 视频合成入口
+    case ot_enterVideoUploadButton = "speedApp_enterVideoUploadButton" // 点击上传tab后-点击上传视频
+    case ot_videoCompose_overviewButton = "speedApp_videoCompose_overviewButton" // 点击总览
+    case ot_videoCompose_videoCompositeButton = "speedApp_videoCompose_videoCompositeButton" // 点击去发布
+    case ot_videoCompose_videoPublish = "speedApp_videoPublishButton" // 发布视频
+    case ot_speedApp_searchButton = "speedApp_searchButton" // 发布视频
+    case ot_pageView = "pageView" // 页面访问
+    //
+    // 图文入口
+    case ot_speedApp_clickButton_imageAndTextGenerateVideo = "speedApp_clickButton_imageAndTextGenerateVideo"
+    // 电子相册
+    case ot_speedApp_clickButton_electronicAlbum = "speedApp_clickButton_electronicAlbum"
+    /*************** 消息相关objectType ***************/
+    case ot_msg_fansMsgButton = "speedApp_msgTab_fansMsgButton" //  粉丝消息入口
+    case ot_msg_likeMsgButton = "speedApp_msgTab_likeMsgButton" //  喜欢消息入口
+    case ot_msg_shareMsgButton = "speedApp_msgTab_shareMsgButton" //  分享消息入口
+    case ot_msg_commentMsgButton = "speedApp_msgTab_commentMsgButton" //  评论消息入口
+    case ot_msg_systemMsgButton = "speedApp_msgTab_systemMsgButton" //  通知消息入口
+
+    /*************** 消息详情相关objectType ***************/
+    case ot_msg_shareSpaceViewTab = "speedApp_message_shareSpace_viewTab" // 分享空间详情页观看
+    case ot_msg_shareSpaceLikeTab = "speedApp_message_shareSpace_likeTab" // 分享空间详情页喜欢
+    case ot_msg_shareSpaceCommentTab = "speedApp_message_shareSpace_commentTab" // 分享空间详情页评论
+    case ot_msg_shareSpaceShareTab = "speedApp_message_shareSpace_shareTab" // 分享空间详情页分享
+
+    /*************** 草稿箱相关objectType ***************/
+    case ot_draft_clicktButton = "speedApp_clickOpenProjectListButton" // 草稿箱点击入口
+    case ot_draft_editProject = "speedApp_editProject" // - 点击 Project Item(草稿箱中的每个项目)
+    case ot_draft_viewProject = "speedApp_viewProject" // - 看到 Project Item(草稿箱中的每个项目)
+    case ot_draft_clickEditProject = "speedApp_clickEditProject" // - 点击 Project Item 弹出菜单的「编辑」按钮
+    case ot_draft_clickPublishProject = "speedApp_clickPublishProject" // - 点击 Project Item 弹出菜单的「去发布」按钮
+    case ot_draft_clickRenameProject = "speedApp_clickRenameProject" // - 点击 Project Item 弹出菜单的「重命名」按钮
+    case ot_draft_clickCopyProject = "speedApp_clickCopyProject" // - 点击 Project Item 弹出菜单的「复制」按钮
+    case ot_draft_clickDeleteProject = "speedApp_clickDeleteProject" // - 点击 Project Item 弹出菜单的「删除」按钮
+    case ot_draft_uploadMaterial = "speedApp_uploadMaterial" // - 素材开始上传事件
+    case ot_draft_uploadMatrialSuccess = "speedApp_uploadMatrialSuccess" // - 素材上传成功事件
+    case ot_draft_downloadMaterial = "speedApp_downloadMaterial" // - 素材开始下载事件
+    case ot_draft_downloadMaterialSuccess = "speedApp_downloadMaterialSuccess" // - 素材下载成功事件
+    case ot_videoCompose_mux_complete = "speedApp_videoCompose_mux_complete" // 创作工具「合成成功」添加上报参数
+
+    case ot_reproduce_collectionClicButton = "speedApp_clickReproduceButton_collection" // - 再创作集合页的「再创作按钮」
+    case ot_reproduce_clickButton = "speedApp_clickReProduceButton" // 再创作按钮(右上角)点击上报
+    case ot_reproduce_collectionBar = "speedApp_clickReproduceCollectionBar" // 再创作按钮(左下角)点击上报
+    case ot_reproduce_collectionVideo = "speedApp_clickReproduceCollectionVideo" // 再创作集合页的「视频」点击上报
+    case ot_reproduce_sameSourceButton = "speedApp_viewSameSourceButton" // 再创作查看同款来源按钮
+    case ot_reproduce_saveProjectToDraftBox = "speedApp-saveProjectToDraftBox" // 创作工具「保存项目成功」添加上报参数
+    // add by ak
+    case speedApp_viewWindow_upload2Compose // 「上传转创作」:窗口曝光
+    case speedApp_clickButton_upload2Compose_addMusic // 「上传转创作」:加音乐 - 按钮点击
+    case speedApp_clickButton_upload2Compose_addText // 「上传转创作」:加语音 - 按钮点击
+    case speedApp_clickButton_upload2Compose_addVoice // 「上传转创作」:加语音 - 按钮点击
+    case speedApp_clickButton_upload2Compose_addCompose // 上传转创作」:加多段拼接 - 按钮点击
+    case speedApp_clickButton_upload2Compose_processingVideo // 「上传转创作」:加工视频 - 按钮点击
+    case speedApp_clickButton_upload2Compose_publish // 「上传转创作」:直接发布 - 按钮点击
+    case speedApp_viewButton_uploadCoverButton // 「发布页」:修改封面 - 按钮曝光
+    case speedApp_viewButton_uploadCoverTipButton // 修改封面提示 - 按钮曝光
+    case speedApp_clickButton_uploadCoverButton // 「发布页」:修改封面 - 按钮点击
+    case speedApp_clickButton_uploadCoverTipButton
+
+    case speedApp_viewButton_addMusic // 「创作工具页」:添加音乐 - 按钮曝光
+    case speedApp_clickButton_addMusic // 「创作工具页」:添加音乐 - 按钮点击
+    case speedApp_viewButton_addMusicTip // 创作工具页」:添加音乐提示 - 按钮曝光
+    case speedApp_clickButton_addMusicTip // 「创作工具页」:添加音乐提示 - 按钮点击
+
+    /*************** 卡点视频相关objectType ***************/
+    case ot_click_syncedUpMusic = "speedApp_clickButton_syncedUpMusic" // 弹出面板中点击「卡点视频」按键
+    case ot_view_selectSyncedUpMaterial = "speedApp_viewWindow_selectSyncedUpMaterial" // 曝光上报:卡点视频素材选择页
+    case ot_click_confirmMaterial = "speedApp_clickButton_confirmSyncedUpMaterial" // 点击上报:卡点视频素材确认按钮
+    case ot_click_back = "speedApp_clickButton_back" // 点击上报:返回按钮
+    case ot_view_selectSyncedUpMusic = "speedApp_viewWindow_selectSyncedUpMusic" // 曝光上报:卡点视频音乐选择页
+    case ot_view_syncedUpMusic = "speedApp_viewButton_syncedUpMusic" // 曝光上报:音乐素材曝光
+    case ot_click_auditionMusic = "speedApp_clickButton_auditionMusic" // 点击上报:音乐素材试听
+    case ot_click_chooseMusic = "speedApp_clickButton_chooseMusic" // 点击上报:选择音乐素材
+    case ot_click_chooseMusicCategory = "speedApp_clickButton_chooseMusicCategory" // 点击上报:选择音乐分类
+    case ot_click_chooseMusicCategoryTag = "speedApp_clickButton_chooseMusicCategoryTag" // 点击上报:选择音乐分类下的 TAG
+    case ot_view_previewSyncedUp = "speedApp_viewWindow_previewSyncedUp" // 曝光上报:预览页面曝光上报
+    case ot_click_selectMusic = "speedApp_clickButton_selectMusic" // 点击上报:重新选择音乐
+    case ot_click_selectRhythm = "speedApp_clickButton_selectRhythm" // 点击上报:选择节奏
+    case ot_click_dragFront = "speedApp_clickButton_dragFront" // 点击上报:拖动拖拽条(左部分)
+    case ot_click_dragBehind = "speedApp_clickButton_dragBehind" // 点击上报:拖动拖拽条(右部分)
+    case ot_click_commit = "sppedApp_clickButton_commit" // 点击上报:去合成
+    case ot_view_searchSyncedUpMusic = "speedApp_viewWindow_searchSyncedUpMusic" // 曝光上报:音乐素材搜索页
+    case ot_click_searchSyncedUpMusic = "speedApp_clickButton_searchSyncedUpMusic" // 点击上报:用户在搜索框输入文字然后按回车
+    case ot_view_searchMusic = "speedApp_viewButton_searchMusic" // 曝光上报:搜索结果音乐素材曝光
+    case ot_click_auditionSearchMusic = "speedApp_clickButton_auditionSearchMusic" // 点击上报:试听音乐素材
+    case ot_click_chooseSearchMusic = "speedApp_clickButton_chooseSearchMusic" // 点击上报:选择音乐素材
+    case ot_view_publishSyncedUp = "speedApp_viewWindow_publishSyncedUp" // 曝光上报:窗口曝光
+    case ot_click_shareWechat = "speedApp_clickButton_shareWechat" // 点击上报:分享微信
+    case ot_click_shareWechatMoment = "speedApp_clickButton_shareWechatMoment" // 点击上报:分享朋友圈
+    case ot_click_finished = "speedApp_clickButton_finished" // 点击上报:完成
+}
+
+// MARK: - 视频上报类型
+
+/// 视频上报类型
+public  enum  businessType: String {
+    /*************** 视频相关businessType ***************/
+    case bt_videoView = "videoView" //  视频展示到屏幕,在此APP中 首页中:视频展示但未播放时,用于后端算法相关逻辑 相关推荐:在播放中下部展示出相关推荐的3个视频时,上报这3个视频的videoView。同一次播放下不重复上报3个推荐视频的videoView
+    case bt_videoPreView = "videoPreView"
+    case bt_videoPlaySlow = "videoPlaySlow"
+    case bt_videoPlayError = "videoPlayError" // 播放失败
+    case bt_aliasBindingError = "aliasBindingError" // 绑定别名失败
+    case bt_videoPlaySuccessTime = "videoPlaySuccessTime" // 加载时长
+    case bt_videoPlayException = "videoPlayException"
+    case bt_videoPlay_normal = "videoPlay"
+    public  enum  bt_videoPlay: String {
+        case bt_videoPlay_userSlideSingle = "userSlideSingle" // 用户在沉浸态下滑动切换视频(极速版、爱电影)
+        case bt_videoPlay_userClickCover = "userClickCover" // 用户点击封面播放
+        case bt_videoPlay_userSlideList = "userSlideList" // 用户在列表中滑动停止导致的播放
+        case bt_videoPlay_autoNext = "autoNext" // 自动播放下一个视频
+        case bt_videoPlay_autoStart = "autoStart" // 1、启动后自动播第一个视频 2、播放器出问题导致重新播放 3、别的页面返回之前的页面播放
+    }
+
+    case bt_videoShareH5 = "videoShareH5" // 点击分享朋友圈按键
+    case bt_videoShareFriend = "videoShareFriend" // 点击分享好友按键
+    case bt_videoSemiRealPlay = "videoSemiRealPlay" // 视频播放到10s时上报
+    case bt_videoRealPlay = "videoRealPlay" // 视频播放到20s或播放到总时长30%,哪个先到为准
+    case bt_videoPlaySuccess = "videoPlaySuccess" // 视频缓冲完成开始播放
+    case bt_videoPlayEnd = "videoPlayEnd" // 视频播放到总时长100%
+    case bt_videoFavorite = "videoFavorite" // 视频喜欢的上报
+    case bt_videoEnterUser = "videoEnterUserHomepage" // 在视频页点击 Up 主头像进入个人主页
+    case bt_videoSwipeRight = "videoSwipeRight" // 右滑查看视频的相关推荐
+    case bt_videoEnterRecom = "videoEnterRecomendation" // 点击视频的相关推荐
+
+    /*************** 活动相关businessType ***************/
+    case bt_buttonClick = "buttonClick" // 按键点击
+    case bt_buttonView = "buttonView" // 按键曝光
+    case bt_windowView = "windowView" // 弹窗曝光
+    case bt_autoJump = "autoJump" // 自动跳转
+    case bt_pageView = "pageView" // 页面访问
+
+    /*************** 供给稳定性相关businessType ***************/
+    case bt_fetchSlow = "FetchSlow" // 拉取卡顿率
+    case bt_fetchFail = "FetchFail" // 拉取失败率
+    case bt_fetchDuration = "FetchDuration" // 拉取时长
+    case bt_dnsParseCostTime = "dnsParseCostTime" // DNS上报 "speed.piaoquantv.com", "rescdn.yishihui.com"
+
+    /*************** 上传相关businessType ***************/
+    case bt_up_process = "videoUploadProcess" // 上传开始事件
+    case bt_publish_error = "publishError" // 发布失败
+    case bt_publish_success = "publishSuccess" // 发布成功
+
+    /*************** 创作工具相关businessType ***************/
+    case bt_makeVideos = "makeVideos" // 创作视频
+    case bt_materialView = "materialView" // 素材曝光
+    case bt_materialUse = "materialUse" // 用户选取
+    case bt_materialCompose = "materialCompose" // 素材合成
+
+    /*************** 消息相关businessType ***************/
+    case bt_draft_downloadMaterialSuccess = "downloadMaterialSuccess" // 素材下载成功事件
+    case bt_draft_downloadMaterial = "downloadMaterial" // - 素材开始下载事件
+    case bt_draft_uploadMaterialSuccess = "uploadMaterialSuccess" // - 素材上传成功事件
+    case bt_draft_uploadMaterial = "uploadMaterial" // - 素材开始上传事件
+    case bt_videoCompose_muxAction = "muxAction" // 创作工具「合成成功」添加上报参数
+
+    /*************** 广告相关businessType ***************/
+    case bt_ad_request = "adRequest" // 客户端请求广告
+    case bt_ad_loaded = "adLoaded" // 客户端广告加载成功
+    case bt_ad_view = "adView" // 广告展示到屏幕
+    case bt_ad_click = "adClick" // 广告被点击
+    case bt_ad_loadedError = "adLoadedError" // 客户端广告加载失败-非需求
+    case bt_ad_close = "adClose" // 广告被关闭-非需求
+    case bt_ad_configRequest = "adConfigRequest" // 客户端请求广告配置
+    case bt_ad_configLoaded = "adConfigLoaded" // 客户端请求广告配置成功
+}
+
+// MARK: - autoType 自动动作的类型
+
+/// autoType 自动动作的类型
+public  enum  autoType: Int {
+    case AUTO_TYPE_SCROLL_VERTICAL_DOWN = 11 // 用户手动下划触发播放(播上一个视频)
+    case AUTO_TYPE_SCROLL_VERTICAL_UP = 12 // 用户手动上划触发播放(播下一个视频)
+    case AUTO_TYPE_SCROLL_HORIZON_RIGHT = 13 // 用户手动右划触发播放
+    case AUTO_TYPE_SCROLL_HORIZON_LEFT = 14 // 用户手动左划触发播放
+    case AUTO_TYPE_ENTER_DETAIL = 15 // 用户手动点击(进详情页)触发播放
+    case AUTO_TYPE_ENTER_APP = 21 // 打开APP自动播放的第一个
+    case AUTO_TYPE_NEXT_AFTER_COMPLETE = 22 // 用户播放完一个视频后自动触发播放相关推荐视频
+    case AUTO_TYPE_BACKEND_RESUME = 23 // 后台唤醒app,如果播放器挂了,重新播放、在别的页面播放过视频再回来之前的页面播放
+    case AUTO_TYPE_REFRESH_LIST = 24 // 首页下拉刷新,自动播的第一个
+    case AUTO_TYPE_CLICK_RECOMMEND = 25 // 点击推荐视频播放
+    case AUTO_TYPE_CLICK_DOUBLE = 26 // 双列点击视频进入单列播放
+}
+
+// MARK: - 消息动作:表示该条日志属于某条消息生命周期的哪个漏斗环节
+
+/// 消息动作:表示该条日志属于某条消息生命周期的哪个漏斗环节
+public  enum  actionType: String {
+    case at_msg_backendReturn = "backendReturn" // 后端将消息返回给客户端
+    case at_msg_frontendPull = "frontendPull" // 客户端获取到
+    case at_msg_view = "view" // 在客户端消息被滑动展示在屏幕上
+    case at_msg_click = "click" // 用户点击消息
+}
+
+// MARK: - 消息类型
+
+/// 消息类型
+public  enum  messageType: Int {
+    case mt_nomal = 0 // 未知消息
+    case mt_fans = 1 // 粉丝消息
+    case mt_like = 2 // 喜欢/赞
+    case mt_share = 3 // 分享消息
+    case mt_comment = 4 // 评论消息
+    case mt_notification = 5 // 通知消息
+    case mt_share_dynamics = 6 // 分享动态消息
+    /*** 自定义添加不涉及正式消息类型 ***/
+    case mt_badgeNumber = 1001 // 显示消息个数cell
+}
+
+/// 分享空间二级界面数据请求类型  1,观看 2,分享,3,喜欢 4, 评论
+public  enum  sharePageType: Int {
+    case share_page_play = 1 // 分享空间-播放列表
+    case share_page_share = 2 // 分享空间-分享列表
+    case share_page_favorite = 3 // 分享空间-收藏列表
+    case share_page_commnet = 4 // 分享空间-评论列表
+}
+
+// MARK: - 消息子类型
+
+/// 消息子类型
+public  enum  messageSubType: Int {
+    case mtsub_nomal = 0 // 未知消息
+    case mtsub_fansAtt = 101 // 粉丝-关注
+    case mtsub_fansSbs = 102 // 粉丝-订阅
+    case mtsub_likeColl = 201 // 喜欢-收藏视频
+    case mtsube_likePrai = 202 // 喜欢-评论点赞
+    case mtsub_shareWechat = 301 // 分享-微信会话
+    case mtsub_shareFriend = 302 // 分享-微信朋友圈
+    case mtsub_comment = 401 // 评论一级评论
+    case mtsub_commentSub = 402 // 评论二级评论
+    case mtsub_pushSucc = 501 // 通知-发布成功
+    case mtsub_pushNoPass = 502 // 通知-审核不通过
+    case mtsub_pushFixing = 503 // 通知-审核待修改
+    case mtsub_pushTransFail = 504 // 通知-转码失败
+    case mtsub_pushVideoRecom = 505 // 通知视频推荐
+    case mtsub_pushAccForbi = 506 // 通知-账号封禁
+    case mtsub_pushAccUnban = 507 // 通知-账号解封
+    case mtsub_pushIncStart = 508 // 通知-激励开始
+    case mtsub_pushIncEnd = 509 // 通知-激励结束
+    case mtsub_pushIncRupt = 510 // 通知-激励中断
+    case mtsub_shareSpace = 601 // 分享空间-分享
+    case mtsub_shareSpLike = 602 // 分享空间-喜欢
+    case mtsub_shareSpComm = 603 // 分享空间-评论
+    case mtsub_shareSpPlay = 604 // 分享空间-播放
+    case mtsub_shareSpMerge = 605 // 分享空间-外层合并消息(与业务逻辑无关,埋点上报使用)
+}
+
+// MARK: - 埋点上报消息类型(暂未统一)
+
+/// 埋点上报消息类型(暂未统一)
+public  enum  messagePointType: Int {
+    case mt_point_nomal = 0 // 未知消息
+    case mt_point_fans = 1 // 粉丝消息
+    case mt_point_praise = 2 // 赞
+    case mt_point_barrage = 3 // 弹幕消息
+    case mt_point_comment = 4 // 评论消息
+    case mt_point_share = 5 // 分享消息
+    case mt_point_collect = 6 // 收藏消息
+    case mt_point_notification = 7 // 通知消息
+    case mt_point_operative = 8 // 运营消息
+    case mt_point_dynamics = 1000 // 分享动态消息
+}
+
+// MARK: - 日志库类型
+
+/// 日志库类型
+public  enum  statisticsLogType: Int {
+    case st_log_type_abtestinfo = 10 // abtestinfo-Log ABTEST的 dlog Store
+    case st_log_type_operation = 20 // operation-Log 漏斗模型的相关上报
+    case st_log_type_simpleevent = 30 // simpleevent-Log 独立事件的相关上报
+    case st_log_type_frontend = 40 // frontend-Log 前端调试日志
+    case st_log_type_pLayaction = 50 // pLayaction-Log 播放行为日志上报
+    case st_log_type_useractive = 60 // useractive-Log 用户活跃日志上报
+    case st_log_type_videoPlayTracking = 70 // video-play-tracking 视频播放行为跟踪
+    case st_log_type_proauceSearch = 80 // proauce-search-log 素材搜索日志上报
+    case st_log_type_videoCompose = 90 // video-compose-log 创作工具素材搜索日志上报
+    case st_log_type_location = 100 // user-location-log 位置信息日志上报
+    case st_log_type_videoProduction = 110 // video-production-log 创作工具埋点日志上报
+    case st_log_type_adAction = 120 // ad-action-log 广告埋点日志上报
+}
+
+// MARK: - 冷启动方式
+
+/// coldLaunchType // 冷启动方式。若为热启动,则不用上报该字段
+public  enum  coldLaunchType: String {
+    case coldLaunchType_userActiveOpen = "userActiveOpen" // 用户主动打开
+    case coldLaunchType_appRecall = "appRecall" // 其它APP唤起,包括H5应用宝唤起
+    case coldLaunchType_pushRecall = "pushRecall" // 推送唤起
+}
+
+// MARK: - 上报日志类型
+
+/// 上报日志类型
+public  enum  reportLogType {
+    case reportLogType_view // 显示页面
+    case reportLogType_realPlay // 真实播放 视频播放到20s或播放到总时长30%,哪个先到为准
+    case reportLogType_play // 记录播放的视频
+    case reportLogType_Action // 上报视频动作记录
+    case reportLogType_Frontend // 通用上报
+}
+
+// MARK: - 底部tab
+
+/// 底部tab
+public  enum  TAB_PAGETYPE: String {
+    case TAB_PAGETYPE_NORMAL = "nomalTab" // 默认
+    case TAB_PAGETYPE_RECOMM = "categoryTab" // 推荐
+    case TAB_PAGETYPE_ATTEN = "followTab" // 关注
+    case TAB_PAGETYPE_PUBLISH = "publishTab" // 发布
+    case TAB_PAGETYPE_MESSAGE = "messageTab" // 消息
+    case TAB_PAGETYPE_MINE = "mineTab" // 我的
+}
+
+// MARK: - 刷新控件类型
+
+/// 刷新控件类型
+public  enum  REFRESH_TYPE {
+    case REFRESH_TYPE_ALL // 推荐
+    case REFRESH_TYPE_HEADER // 头部
+    case REFRESH_TYPE_FOOTER // 尾部
+}
+
+// MARK: - 刷新控件类型
+
+/// 刷新控件类型
+public  enum  moveDirection {
+    case moveDirectionNormal
+    case moveDirectionUp
+    case moveDirectionDown
+    case moveDirectionRight
+    case moveDirectionLeft
+}
+
+// MARK: - 关注跟粉丝cell类型
+
+/// 关注跟粉丝cell类型
+public  enum  atttendAndFansCellType {
+    case cellType_attend // 关注
+    case cellType_fans // 粉丝
+    case cellType_banned // 黑名单
+}
+
+// MARK: - 视频全屏播放操作类型
+
+/// 视频全屏播放操作类型
+public  enum  fullScreenActionType {
+    case volume // 声音
+    case brightness // 亮度
+    case progress // 进度
+}
+
+// MARK: - 活动提示页类型
+
+/// 活动提示页类型
+public  enum  activityRemindType {
+    case nomal //
+    case newUser // 新用户专享
+    case yesterDay_finish // 昨日已完成
+    case yesterDay_unfinish // 昨日未完成
+    case today_finish // 今日任务已完成
+}
+
+// MARK: - 制作工具素材搜索msgType
+
+/// 制作工具素材搜索msgType
+public  enum  material_msgType: String {
+    case all = "ALL_SEARCH_EVENT" // 搜索所有
+    case image = "IMAGE_SEARCH_EVENT" // 图片搜索
+    case video = "VIDEO_SEARCH_EVENT" // 视频搜索
+    case gif = "GIF_SEARCH_EVENT" // 动图搜索
+    case recommend_image = "IMAGE_RECOMMEND_EVENT" // 图片推荐搜索
+    case recommend_video = "VIDEO_RECOMMEND_EVENT" // 视频推荐搜索
+    case recommend_gif = "GIF_RECOMMEND_EVENT" // 动图推荐搜索
+}
+
+// 画布类型
+public  enum  videoCanvasType: Int {
+    case origin = 1 // 原始
+    case nineToSixteen = 2 // 9:16
+    case oneToOne = 3 // 1:1
+    case sixteenToNine = 4 // 16:9
+}
+
+/// 贴纸类型
+public  enum  StickerType: String {
+    case UNKONW = "unknow"
+    case IMAGE = "image" // 图片
+    case VIDEO = "video" // 视频
+    case GIF = "gif" // gif动图
+    case VOICE = "voice" // 声音
+    case FILE = "file" // 文件
+    case SUBTITLE = "subtitle" // 字幕
+    /// 获取index
+    /// - Returns: description
+    public func index() -> Int {
+        var fileType: Int = 0
+        switch self {
+        case .IMAGE:
+            fileType = 1
+        case .VIDEO:
+            fileType = 2
+        case .VOICE:
+            fileType = 3
+        case .FILE:
+            fileType = 4
+        case .GIF:
+            fileType = 5
+        case .SUBTITLE:
+            fileType = 6
+        default:
+            fileType = 0
+        }
+        return fileType
+    }
+
+    /// 媒体类型
+    /// - Returns: <#description#>
+   public func mimeType() -> String {
+        var mimeType: String = "application/octet-stream"
+        switch self {
+        case .IMAGE:
+            mimeType = "image/jpeg"
+        case .VIDEO:
+            mimeType = "video/mpeg"
+        case .VOICE:
+            mimeType = "audio/mpeg"
+        case .FILE:
+            mimeType = "application/octet-stream"
+        case .GIF:
+            mimeType = "image/gif"
+        default:
+            mimeType = "application/octet-stream"
+        }
+        return mimeType
+    }
+
+   public func pathExtension() -> String {
+        var pathExtension: String = ""
+        switch self {
+        case .IMAGE:
+            pathExtension = "png"
+        case .VIDEO:
+            pathExtension = "mp4"
+        case .VOICE:
+            pathExtension = "mp3"
+        case .FILE:
+            pathExtension = ""
+        case .GIF:
+            pathExtension = "gif"
+        default:
+            break
+        }
+        return pathExtension
+    }
+}
+
+// MARK: - 贴纸裁剪方式
+
+/// 贴纸裁剪方式
+public  enum  stickerContentModeDef: Int {
+    case aspectFit = 0 // 自适应
+    case aspectFill = 1 // 铺满
+}
+
+/// 贴纸裁剪方式 add by ak v2
+public  enum  stickerContentMode: String {
+    case aspectFitStr = "complete" // 完整显示(有黑边)
+    case aspectFillStr = "full" // 铺满
+}
+
+// MARK: - 适配模式
+
+/// 适配模式
+public  enum  adapterModeDef: Int {
+    case speedyAuto = 0 // 快速自适应
+    case loopAuto = 1 // 自动循环
+    case crop = 2 // 定帧/裁剪
+}
+
+/// 适配模式 add by ak  v2
+public  enum  adapterMode: String {
+    case multiple // 快速自适应
+    case loopAuto = "loop" // 自动循环
+    case staticFrame // 定帧/裁剪
+}
+
+// MARK: - 上传视频类型
+
+/// 上传视频类型
+public  enum  videoUploadSourceType: String {
+    case videoUpload // 上传
+    case videoCompose // 合成制作
+    case videoUploadToCompose // 上传转合成制作
+}
+
+// MARK: - 进入创作工具入口
+
+/// 素材上传、保存、收藏相关
+public  enum  videoMakeEntranceType: String {
+    case entranceMineTabDraft = "draft_metab" // 我的Tab -> 草稿箱列表 -> 创作工具
+    case entrancePublicTabDraft = "draft_uploadpopup" // 发布Tab -> 草稿箱列表 -> 创作工具
+    case entrancePublicTabCompose = "composeVideo" // 发布Tab -> 视频合成 -> 创作工具
+    case entrancePublicTabImageText = "imageTextGenerate" // 发布Tab -> 图文生成视频 -> 创作工具
+    case entrancePublicTabAlbum = "electronicAlbum" // 发布Tab -> 电子相册 -> 创作工具
+    case entranceReproduction = "reproduction" // 视频详情 -> 做同款 -> 创作工具
+    case entrancePublicTabUpload = "upload" // 发布Tab -> 上传视频 -> 直接发布
+    // add by ak
+    case entranceUpload2compose = "upload2compose" // 发布Tab -> 上传视频 -> 加工视频 -> 创作工具
+    case entranceStuckPointPublic = "syncedUpVideo" // 卡点视频发布
+
+    // * 新添加-自定义
+    case entrancePublicTabAddMusic = "addMusic" // 发布Tab -> 上传视频 -> 加音乐 -> 创作工具
+    case entrancePublicTabAddSubtitle = "addSubtitle" // 发布Tab -> 上传视频 -> 加字幕 -> 创作工具
+    case entrancePublicTabAddVoice = "addVoice" // 发布Tab -> 上传视频 -> 加语音 -> 创作工具
+    case entrancePublicTabAddSection = "addSection" // 发布Tab -> 上传视频 -> 多段拼接 -> 创作工具
+}
+
+// MARK: - 段落类型
+
+/// 段落类型
+public  enum  sectionType: String {
+    case normal // 普通段
+    case global // 全局段
+}
+
+// MARK: - 音乐类型
+
+/// 音乐类型
+public  enum  VOICETYPT: String {
+    case PRODUCE = "produce" // 合成语音
+    case BGM = "bgm" // 背景音乐
+    case SPEECH = "speech" // 录音
+    case LOCAL = "local" // 导入文件
+}
+
+// MARK: - 输入框状态B
+
+/// 输入框状态
+public  enum  inputStatus {
+    case normal // 写故事,可智能配音,自动生成字幕
+    case inputing // 输入中
+    case recording // 语音识别成文字中…
+    case recordEmpty // 录音未识别到文字,点此输入
+    case recordError // 获取录音文字失败,请重试
+    case recordSuccess // 获取录音文字成功
+}
+
+// MARK: - 画面比例
+
+/// 画面比例
+public  enum  aspectRatio {
+    case origin(width: CGFloat, height: CGFloat) // 原始
+    case oneToOne // 1:1
+    case sixteenToNine // 16:9
+    case nineToSixteen // 9:16
+}
+
+// MARK: - 卡点视频音乐页面类型
+
+/// 卡点视频音乐页面类型
+public  enum  stuckPointMusicContentType {
+    case catagery
+    case serach
+    case page
+}
+
+// 视频发布来源场景 1:普通上传 2:创作工具,3:普通上传转创作工具,4:后台转换加工,5:卡点视频制作
+public  enum  videoFromScene: Int {
+    case UploadNormal = 1 // 普通上传
+    case UploadMakeVideo = 2 // 创作工具
+    case UploadNormalToMakeVideo = 3 // 普通上传转创作工具
+    case server = 4 // 后台转换加工
+    case stuckPoint = 5 // 卡点视频制作
+}
+
+// MARK: - 广告来源渠道
+
+/// 广告来源渠道
+public  enum  adChannel: String {
+    case wechat // 微信广告
+    case byteDouce = "pangle" // 穿山甲广告(字节跳动)
+    case pdd // 多多进宝广告(拼多多)
+    case gdt = "qq" // 腾讯优量汇
+}
+
+// MARK: - 广告类型
+
+/// 广告类型
+public  enum  adType: Int {
+    case banner = 1 // Banner 广告(即:固定位展示广告)
+    case drawVideo = 2 // 信息流视频广告
+    case preMovie = 3 // 贴片广告(场景举例:暂停贴片广告)
+    case tableScreen = 4 // 插屏广告(场景举例:退出全屏,弹出的插屏广告)
+    case splash = 5 // 开屏广告
+}
+
+// MARK: - 广告投放人群
+
+/// 广告投放人群
+public  enum  adCrowd: Int {
+    case highCrowd = 1 // 高分享人群
+    case mediumCrowd = 2 // 中分享人群
+    case lowCrowd = 3 // 低分享人群
+}
+
+// MARK: - 广告内部位置值
+
+/// 广告内部位置值
+public  enum  adPosition: String {
+    case splashAd // 票圈视频 App 闪屏页广告位
+    case videoFlowAd // 票圈视频 App 视频流广告位
+}
+
+
+// MARK: - 风格
+
+/// 风格
+public  enum  styleColor: String {
+    case nomal = "#F1034D"
+    case green = "#3DC1C1"
+    case red = "#EE0051"
+}
+
+// MARK: - statusBar
+
+/// statusBar
+public enum  statusBarStyle {
+    case light
+    case dark
+}

+ 25 - 6
Example/BFCommonKit.xcodeproj/project.pbxproj

@@ -28,8 +28,8 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
-		400C2BEA97548291160AA293 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; name = README.md; path = ../README.md; sourceTree = "<group>"; };
-		438AEA15A4D1B4ECCB1A454D /* BFCommonKit.podspec */ = {isa = PBXFileReference; includeInIndex = 1; name = BFCommonKit.podspec; path = ../BFCommonKit.podspec; sourceTree = "<group>"; };
+		400C2BEA97548291160AA293 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
+		438AEA15A4D1B4ECCB1A454D /* BFCommonKit.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = BFCommonKit.podspec; path = ../BFCommonKit.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
 		52DC1407124E2559B706ED34 /* Pods-BFCommonKit_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BFCommonKit_Tests.release.xcconfig"; path = "Target Support Files/Pods-BFCommonKit_Tests/Pods-BFCommonKit_Tests.release.xcconfig"; sourceTree = "<group>"; };
 		607FACD01AFB9204008FA782 /* BFCommonKit_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BFCommonKit_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -45,7 +45,7 @@
 		A36C6CCAB85F8CFAA805ABA6 /* Pods-BFCommonKit_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BFCommonKit_Example.release.xcconfig"; path = "Target Support Files/Pods-BFCommonKit_Example/Pods-BFCommonKit_Example.release.xcconfig"; sourceTree = "<group>"; };
 		B583C13AE3F150E0346FFAB4 /* Pods_BFCommonKit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BFCommonKit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		DEA0E3323A118BD64A7C448B /* Pods-BFCommonKit_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BFCommonKit_Example.debug.xcconfig"; path = "Target Support Files/Pods-BFCommonKit_Example/Pods-BFCommonKit_Example.debug.xcconfig"; sourceTree = "<group>"; };
-		E39C2F205C9AC6206F3FA21B /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
+		E39C2F205C9AC6206F3FA21B /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
 		E6CC6E3C71E4A59AEDF91214 /* Pods-BFCommonKit_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BFCommonKit_Tests.debug.xcconfig"; path = "Target Support Files/Pods-BFCommonKit_Tests/Pods-BFCommonKit_Tests.debug.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -77,7 +77,6 @@
 				E6CC6E3C71E4A59AEDF91214 /* Pods-BFCommonKit_Tests.debug.xcconfig */,
 				52DC1407124E2559B706ED34 /* Pods-BFCommonKit_Tests.release.xcconfig */,
 			);
-			name = Pods;
 			path = Pods;
 			sourceTree = "<group>";
 		};
@@ -213,6 +212,7 @@
 				TargetAttributes = {
 					607FACCF1AFB9204008FA782 = {
 						CreatedOnToolsVersion = 6.3.1;
+						DevelopmentTeam = UH52C8A7SN;
 						LastSwiftMigration = 0900;
 					};
 					607FACE41AFB9204008FA782 = {
@@ -227,6 +227,7 @@
 			developmentRegion = English;
 			hasScannedForEncodings = 0;
 			knownRegions = (
+				English,
 				en,
 				Base,
 			);
@@ -269,11 +270,27 @@
 			);
 			inputPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-BFCommonKit_Example/Pods-BFCommonKit_Example-frameworks.sh",
+				"${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework",
 				"${BUILT_PRODUCTS_DIR}/BFCommonKit/BFCommonKit.framework",
+				"${BUILT_PRODUCTS_DIR}/FDFullscreenPopGesture/FDFullscreenPopGesture.framework",
+				"${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework",
+				"${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework",
+				"${BUILT_PRODUCTS_DIR}/KingfisherWebP/KingfisherWebP.framework",
+				"${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework",
+				"${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework",
+				"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
 			);
 			name = "[CP] Embed Pods Frameworks";
 			outputPaths = (
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BFCommonKit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FDFullscreenPopGesture.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KingfisherWebP.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast_Swift.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
@@ -419,7 +436,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
@@ -465,7 +482,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
@@ -478,6 +495,7 @@
 			baseConfigurationReference = DEA0E3323A118BD64A7C448B /* Pods-BFCommonKit_Example.debug.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = UH52C8A7SN;
 				INFOPLIST_FILE = BFCommonKit/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MODULE_NAME = ExampleApp;
@@ -493,6 +511,7 @@
 			baseConfigurationReference = A36C6CCAB85F8CFAA805ABA6 /* Pods-BFCommonKit_Example.release.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = UH52C8A7SN;
 				INFOPLIST_FILE = BFCommonKit/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MODULE_NAME = ExampleApp;

+ 10 - 0
Example/BFCommonKit.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:BFCommonKit.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>

+ 8 - 0
Example/BFCommonKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 1 - 1
Example/Podfile

@@ -1,6 +1,6 @@
 use_frameworks!
 
-platform :ios, '9.0'
+platform :ios, '10.0'
 
 target 'BFCommonKit_Example' do
   pod 'BFCommonKit', :path => '../'

+ 60 - 0
Example/Podfile.lock

@@ -0,0 +1,60 @@
+PODS:
+  - Alamofire (4.9.1)
+  - BFCommonKit (0.1.0):
+    - Alamofire (= 4.9.1)
+    - FDFullscreenPopGesture
+    - KeychainAccess (= 4.2.2)
+    - Kingfisher (= 6.3.0)
+    - KingfisherWebP (= 1.3.0)
+    - SnapKit (= 5.0.1)
+    - Toast-Swift (= 5.0.1)
+  - FDFullscreenPopGesture (1.1)
+  - KeychainAccess (4.2.2)
+  - Kingfisher (6.3.0)
+  - KingfisherWebP (1.3.0):
+    - Kingfisher (~> 6.2)
+    - libwebp (>= 1.1.0)
+  - libwebp (1.2.0):
+    - libwebp/demux (= 1.2.0)
+    - libwebp/mux (= 1.2.0)
+    - libwebp/webp (= 1.2.0)
+  - libwebp/demux (1.2.0):
+    - libwebp/webp
+  - libwebp/mux (1.2.0):
+    - libwebp/demux
+  - libwebp/webp (1.2.0)
+  - SnapKit (5.0.1)
+  - Toast-Swift (5.0.1)
+
+DEPENDENCIES:
+  - BFCommonKit (from `../`)
+
+SPEC REPOS:
+  trunk:
+    - Alamofire
+    - FDFullscreenPopGesture
+    - KeychainAccess
+    - Kingfisher
+    - KingfisherWebP
+    - libwebp
+    - SnapKit
+    - Toast-Swift
+
+EXTERNAL SOURCES:
+  BFCommonKit:
+    :path: "../"
+
+SPEC CHECKSUMS:
+  Alamofire: 85e8a02c69d6020a0d734f6054870d7ecb75cf18
+  BFCommonKit: 0079ec32e485858b62132dd7bb66e87b13f56d71
+  FDFullscreenPopGesture: a8a620179e3d9c40e8e00256dcee1c1a27c6d0f0
+  KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
+  Kingfisher: 6c3df386db71d82c0817a429d2c9421a77396529
+  KingfisherWebP: dec17a5eb1af2658791bde1f93ae9a853678f826
+  libwebp: e90b9c01d99205d03b6bb8f2c8c415e5a4ef66f0
+  SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
+  Toast-Swift: 9b6a70f28b3bf0b96c40d46c0c4b9d6639846711
+
+PODFILE CHECKSUM: f633c46b12fef52fd5badf33e2bef9887c131b39
+
+COCOAPODS: 1.10.1

+ 19 - 0
Example/Pods/Alamofire/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 234 - 0
Example/Pods/Alamofire/README.md

@@ -0,0 +1,234 @@
+![Alamofire: Elegant Networking in Swift](https://raw.githubusercontent.com/Alamofire/Alamofire/master/alamofire.png)
+
+[![Build Status](https://travis-ci.org/Alamofire/Alamofire.svg?branch=master)](https://travis-ci.org/Alamofire/Alamofire)
+[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Alamofire.svg)](https://img.shields.io/cocoapods/v/Alamofire.svg)
+[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
+[![Platform](https://img.shields.io/cocoapods/p/Alamofire.svg?style=flat)](https://alamofire.github.io/Alamofire)
+[![Twitter](https://img.shields.io/badge/twitter-@AlamofireSF-blue.svg?style=flat)](https://twitter.com/AlamofireSF)
+[![Gitter](https://badges.gitter.im/Alamofire/Alamofire.svg)](https://gitter.im/Alamofire/Alamofire?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+
+Alamofire is an HTTP networking library written in Swift.
+
+- [Features](#features)
+- [Component Libraries](#component-libraries)
+- [Requirements](#requirements)
+- [Migration Guides](#migration-guides)
+- [Communication](#communication)
+- [Installation](#installation)
+- [Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md)
+    - **Intro -** [Making a Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#making-a-request), [Response Handling](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-handling), [Response Validation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-validation), [Response Caching](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-caching)
+	- **HTTP -** [HTTP Methods](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-methods), [Parameter Encoding](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#parameter-encoding), [HTTP Headers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-headers), [Authentication](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#authentication)
+	- **Large Data -** [Downloading Data to a File](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#downloading-data-to-a-file), [Uploading Data to a Server](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#uploading-data-to-a-server)
+	- **Tools -** [Statistical Metrics](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#statistical-metrics), [cURL Command Output](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#curl-command-output)
+- [Advanced Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md)
+	- **URL Session -** [Session Manager](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session-manager), [Session Delegate](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session-delegate), [Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#request)
+	- **Routing -** [Routing Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#routing-requests), [Adapting and Retrying Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests)
+	- **Model Objects -** [Custom Response Serialization](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#custom-response-serialization)
+	- **Connection -** [Security](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#security), [Network Reachability](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#network-reachability)
+- [Open Radars](#open-radars)
+- [FAQ](#faq)
+- [Credits](#credits)
+- [Donations](#donations)
+- [License](#license)
+
+## Features
+
+- [x] Chainable Request / Response Methods
+- [x] URL / JSON / plist Parameter Encoding
+- [x] Upload File / Data / Stream / MultipartFormData
+- [x] Download File using Request or Resume Data
+- [x] Authentication with URLCredential
+- [x] HTTP Response Validation
+- [x] Upload and Download Progress Closures with Progress
+- [x] cURL Command Output
+- [x] Dynamically Adapt and Retry Requests
+- [x] TLS Certificate and Public Key Pinning
+- [x] Network Reachability
+- [x] Comprehensive Unit and Integration Test Coverage
+- [x] [Complete Documentation](https://alamofire.github.io/Alamofire)
+
+## Component Libraries
+
+In order to keep Alamofire focused specifically on core networking implementations, additional component libraries have been created by the [Alamofire Software Foundation](https://github.com/Alamofire/Foundation) to bring additional functionality to the Alamofire ecosystem.
+
+- [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - An image library including image response serializers, `UIImage` and `UIImageView` extensions, custom image filters, an auto-purging in-memory cache and a priority-based image downloading system.
+- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - Controls the visibility of the network activity indicator on iOS using Alamofire. It contains configurable delay timers to help mitigate flicker and can support `URLSession` instances not managed by Alamofire.
+
+## Requirements
+
+- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
+- Xcode 9.3+
+- Swift 4.0+
+
+## Migration Guides
+
+- [Alamofire 4.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%204.0%20Migration%20Guide.md)
+- [Alamofire 3.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%203.0%20Migration%20Guide.md)
+- [Alamofire 2.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%202.0%20Migration%20Guide.md)
+
+## Communication
+- If you **need help with making network requests**, use [Stack Overflow](https://stackoverflow.com/questions/tagged/alamofire) and tag `alamofire`.
+- If you need to **find or understand an API**, check [our documentation](http://alamofire.github.io/Alamofire/) or [Apple's documentation for `URLSession`](https://developer.apple.com/documentation/foundation/url_loading_system), on top of which Alamofire is built.
+- If you need **help with an Alamofire feature**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire).
+- If you'd like to **discuss Alamofire best practices**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire).
+- If you'd like to **discuss a feature request**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). 
+- If you **found a bug**, open an issue and follow the guide. The more detail the better!
+- If you **want to contribute**, submit a pull request.
+
+## Installation
+
+### CocoaPods
+
+[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command:
+
+```bash
+$ gem install cocoapods
+```
+
+> CocoaPods 1.7+ is required to build Alamofire 4.9+.
+
+To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`:
+
+```ruby
+source 'https://github.com/CocoaPods/Specs.git'
+platform :ios, '10.0'
+use_frameworks!
+
+target '<Your Target Name>' do
+  pod 'Alamofire', '~> 4.9'
+end
+```
+
+Then, run the following command:
+
+```bash
+$ pod install
+```
+
+### Carthage
+
+[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
+
+You can install Carthage with [Homebrew](https://brew.sh/) using the following command:
+
+```bash
+$ brew install carthage
+```
+
+To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`:
+
+```ogdl
+github "Alamofire/Alamofire" ~> 4.9
+```
+
+Run `carthage update` to build the framework and drag the built `Alamofire.framework` into your Xcode project.
+
+### Swift Package Manager
+
+The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Alamofire does support its use on supported platforms. 
+
+Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
+
+#### Swift 4
+
+```swift
+dependencies: [
+    .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.9")
+]
+```
+
+### Manually
+
+If you prefer not to use any of the aforementioned dependency managers, you can integrate Alamofire into your project manually.
+
+#### Embedded Framework
+
+- Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository:
+
+  ```bash
+  $ git init
+  ```
+
+- Add Alamofire as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command:
+
+  ```bash
+  $ git submodule add https://github.com/Alamofire/Alamofire.git
+  ```
+
+- Open the new `Alamofire` folder, and drag the `Alamofire.xcodeproj` into the Project Navigator of your application's Xcode project.
+
+    > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.
+
+- Select the `Alamofire.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target.
+- Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar.
+- In the tab bar at the top of that window, open the "General" panel.
+- Click on the `+` button under the "Embedded Binaries" section.
+- You will see two different `Alamofire.xcodeproj` folders each with two different versions of the `Alamofire.framework` nested inside a `Products` folder.
+
+    > It does not matter which `Products` folder you choose from, but it does matter whether you choose the top or bottom `Alamofire.framework`.
+
+- Select the top `Alamofire.framework` for iOS and the bottom one for OS X.
+
+    > You can verify which one you selected by inspecting the build log for your project. The build target for `Alamofire` will be listed as either `Alamofire iOS`, `Alamofire macOS`, `Alamofire tvOS` or `Alamofire watchOS`.
+
+- And that's it!
+
+  > The `Alamofire.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.
+
+## Open Radars
+
+The following radars have some effect on the current implementation of Alamofire.
+
+- [`rdar://21349340`](http://www.openradar.me/radar?id=5517037090635776) - Compiler throwing warning due to toll-free bridging issue in test case
+- `rdar://26870455` - Background URL Session Configurations do not work in the simulator
+- `rdar://26849668` - Some URLProtocol APIs do not properly handle `URLRequest`
+- [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+
+
+## Resolved Radars
+
+The following radars have been resolved over time after being filed against the Alamofire project.
+
+- [`rdar://26761490`](http://www.openradar.me/radar?id=5010235949318144) - Swift string interpolation causing memory leak with common usage (Resolved on 9/1/17 in Xcode 9 beta 6).
+
+## FAQ
+
+### What's the origin of the name Alamofire?
+
+Alamofire is named after the [Alamo Fire flower](https://aggie-horticulture.tamu.edu/wildseed/alamofire.html), a hybrid variant of the Bluebonnet, the official state flower of Texas.
+
+### What logic belongs in a Router vs. a Request Adapter?
+
+Simple, static data such as paths, parameters and common headers belong in the `Router`. Dynamic data such as an `Authorization` header whose value can changed based on an authentication system belongs in a `RequestAdapter`.
+
+The reason the dynamic data MUST be placed into the `RequestAdapter` is to support retry operations. When a `Request` is retried, the original request is not rebuilt meaning the `Router` will not be called again. The `RequestAdapter` is called again allowing the dynamic data to be updated on the original request before retrying the `Request`.
+
+## Credits
+
+Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). You can follow them on Twitter at [@AlamofireSF](https://twitter.com/AlamofireSF) for project updates and releases.
+
+### Security Disclosure
+
+If you believe you have identified a security vulnerability with Alamofire, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker.
+
+## Donations
+
+The [ASF](https://github.com/Alamofire/Foundation#members) is looking to raise money to officially stay registered as a federal non-profit organization.
+Registering will allow us members to gain some legal protections and also allow us to put donations to use, tax free.
+Donating to the ASF will enable us to:
+
+- Pay our yearly legal fees to keep the non-profit in good status
+- Pay for our mail servers to help us stay on top of all questions and security issues
+- Potentially fund test servers to make it easier for us to test the edge cases
+- Potentially fund developers to work on one of our projects full-time
+
+The community adoption of the ASF libraries has been amazing.
+We are greatly humbled by your enthusiasm around the projects, and want to continue to do everything we can to move the needle forward.
+With your continued support, the ASF will be able to improve its reach and also provide better legal safety for the core members.
+If you use any of our libraries for work, see if your employers would be interested in donating.
+Any amount you can donate today to help us reach our goal would be greatly appreciated.
+
+[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=W34WPEE74APJQ)
+
+## License
+
+Alamofire is released under the MIT license. [See LICENSE](https://github.com/Alamofire/Alamofire/blob/master/LICENSE) for details.

+ 460 - 0
Example/Pods/Alamofire/Source/AFError.swift

@@ -0,0 +1,460 @@
+//
+//  AFError.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// `AFError` is the error type returned by Alamofire. It encompasses a few different types of errors, each with
+/// their own associated reasons.
+///
+/// - invalidURL:                  Returned when a `URLConvertible` type fails to create a valid `URL`.
+/// - parameterEncodingFailed:     Returned when a parameter encoding object throws an error during the encoding process.
+/// - multipartEncodingFailed:     Returned when some step in the multipart encoding process fails.
+/// - responseValidationFailed:    Returned when a `validate()` call fails.
+/// - responseSerializationFailed: Returned when a response serializer encounters an error in the serialization process.
+public enum AFError: Error {
+    /// The underlying reason the parameter encoding error occurred.
+    ///
+    /// - missingURL:                 The URL request did not have a URL to encode.
+    /// - jsonEncodingFailed:         JSON serialization failed with an underlying system error during the
+    ///                               encoding process.
+    /// - propertyListEncodingFailed: Property list serialization failed with an underlying system error during
+    ///                               encoding process.
+    public enum ParameterEncodingFailureReason {
+        case missingURL
+        case jsonEncodingFailed(error: Error)
+        case propertyListEncodingFailed(error: Error)
+    }
+
+    /// The underlying reason the multipart encoding error occurred.
+    ///
+    /// - bodyPartURLInvalid:                   The `fileURL` provided for reading an encodable body part isn't a
+    ///                                         file URL.
+    /// - bodyPartFilenameInvalid:              The filename of the `fileURL` provided has either an empty
+    ///                                         `lastPathComponent` or `pathExtension.
+    /// - bodyPartFileNotReachable:             The file at the `fileURL` provided was not reachable.
+    /// - bodyPartFileNotReachableWithError:    Attempting to check the reachability of the `fileURL` provided threw
+    ///                                         an error.
+    /// - bodyPartFileIsDirectory:              The file at the `fileURL` provided is actually a directory.
+    /// - bodyPartFileSizeNotAvailable:         The size of the file at the `fileURL` provided was not returned by
+    ///                                         the system.
+    /// - bodyPartFileSizeQueryFailedWithError: The attempt to find the size of the file at the `fileURL` provided
+    ///                                         threw an error.
+    /// - bodyPartInputStreamCreationFailed:    An `InputStream` could not be created for the provided `fileURL`.
+    /// - outputStreamCreationFailed:           An `OutputStream` could not be created when attempting to write the
+    ///                                         encoded data to disk.
+    /// - outputStreamFileAlreadyExists:        The encoded body data could not be writtent disk because a file
+    ///                                         already exists at the provided `fileURL`.
+    /// - outputStreamURLInvalid:               The `fileURL` provided for writing the encoded body data to disk is
+    ///                                         not a file URL.
+    /// - outputStreamWriteFailed:              The attempt to write the encoded body data to disk failed with an
+    ///                                         underlying error.
+    /// - inputStreamReadFailed:                The attempt to read an encoded body part `InputStream` failed with
+    ///                                         underlying system error.
+    public enum MultipartEncodingFailureReason {
+        case bodyPartURLInvalid(url: URL)
+        case bodyPartFilenameInvalid(in: URL)
+        case bodyPartFileNotReachable(at: URL)
+        case bodyPartFileNotReachableWithError(atURL: URL, error: Error)
+        case bodyPartFileIsDirectory(at: URL)
+        case bodyPartFileSizeNotAvailable(at: URL)
+        case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error)
+        case bodyPartInputStreamCreationFailed(for: URL)
+
+        case outputStreamCreationFailed(for: URL)
+        case outputStreamFileAlreadyExists(at: URL)
+        case outputStreamURLInvalid(url: URL)
+        case outputStreamWriteFailed(error: Error)
+
+        case inputStreamReadFailed(error: Error)
+    }
+
+    /// The underlying reason the response validation error occurred.
+    ///
+    /// - dataFileNil:             The data file containing the server response did not exist.
+    /// - dataFileReadFailed:      The data file containing the server response could not be read.
+    /// - missingContentType:      The response did not contain a `Content-Type` and the `acceptableContentTypes`
+    ///                            provided did not contain wildcard type.
+    /// - unacceptableContentType: The response `Content-Type` did not match any type in the provided
+    ///                            `acceptableContentTypes`.
+    /// - unacceptableStatusCode:  The response status code was not acceptable.
+    public enum ResponseValidationFailureReason {
+        case dataFileNil
+        case dataFileReadFailed(at: URL)
+        case missingContentType(acceptableContentTypes: [String])
+        case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String)
+        case unacceptableStatusCode(code: Int)
+    }
+
+    /// The underlying reason the response serialization error occurred.
+    ///
+    /// - inputDataNil:                    The server response contained no data.
+    /// - inputDataNilOrZeroLength:        The server response contained no data or the data was zero length.
+    /// - inputFileNil:                    The file containing the server response did not exist.
+    /// - inputFileReadFailed:             The file containing the server response could not be read.
+    /// - stringSerializationFailed:       String serialization failed using the provided `String.Encoding`.
+    /// - jsonSerializationFailed:         JSON serialization failed with an underlying system error.
+    /// - propertyListSerializationFailed: Property list serialization failed with an underlying system error.
+    public enum ResponseSerializationFailureReason {
+        case inputDataNil
+        case inputDataNilOrZeroLength
+        case inputFileNil
+        case inputFileReadFailed(at: URL)
+        case stringSerializationFailed(encoding: String.Encoding)
+        case jsonSerializationFailed(error: Error)
+        case propertyListSerializationFailed(error: Error)
+    }
+
+    case invalidURL(url: URLConvertible)
+    case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
+    case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
+    case responseValidationFailed(reason: ResponseValidationFailureReason)
+    case responseSerializationFailed(reason: ResponseSerializationFailureReason)
+}
+
+// MARK: - Adapt Error
+
+struct AdaptError: Error {
+    let error: Error
+}
+
+extension Error {
+    var underlyingAdaptError: Error? { return (self as? AdaptError)?.error }
+}
+
+// MARK: - Error Booleans
+
+extension AFError {
+    /// Returns whether the AFError is an invalid URL error.
+    public var isInvalidURLError: Bool {
+        if case .invalidURL = self { return true }
+        return false
+    }
+
+    /// Returns whether the AFError is a parameter encoding error. When `true`, the `underlyingError` property will
+    /// contain the associated value.
+    public var isParameterEncodingError: Bool {
+        if case .parameterEncodingFailed = self { return true }
+        return false
+    }
+
+    /// Returns whether the AFError is a multipart encoding error. When `true`, the `url` and `underlyingError` properties
+    /// will contain the associated values.
+    public var isMultipartEncodingError: Bool {
+        if case .multipartEncodingFailed = self { return true }
+        return false
+    }
+
+    /// Returns whether the `AFError` is a response validation error. When `true`, the `acceptableContentTypes`,
+    /// `responseContentType`, and `responseCode` properties will contain the associated values.
+    public var isResponseValidationError: Bool {
+        if case .responseValidationFailed = self { return true }
+        return false
+    }
+
+    /// Returns whether the `AFError` is a response serialization error. When `true`, the `failedStringEncoding` and
+    /// `underlyingError` properties will contain the associated values.
+    public var isResponseSerializationError: Bool {
+        if case .responseSerializationFailed = self { return true }
+        return false
+    }
+}
+
+// MARK: - Convenience Properties
+
+extension AFError {
+    /// The `URLConvertible` associated with the error.
+    public var urlConvertible: URLConvertible? {
+        switch self {
+        case .invalidURL(let url):
+            return url
+        default:
+            return nil
+        }
+    }
+
+    /// The `URL` associated with the error.
+    public var url: URL? {
+        switch self {
+        case .multipartEncodingFailed(let reason):
+            return reason.url
+        default:
+            return nil
+        }
+    }
+
+    /// The `Error` returned by a system framework associated with a `.parameterEncodingFailed`,
+    /// `.multipartEncodingFailed` or `.responseSerializationFailed` error.
+    public var underlyingError: Error? {
+        switch self {
+        case .parameterEncodingFailed(let reason):
+            return reason.underlyingError
+        case .multipartEncodingFailed(let reason):
+            return reason.underlyingError
+        case .responseSerializationFailed(let reason):
+            return reason.underlyingError
+        default:
+            return nil
+        }
+    }
+
+    /// The acceptable `Content-Type`s of a `.responseValidationFailed` error.
+    public var acceptableContentTypes: [String]? {
+        switch self {
+        case .responseValidationFailed(let reason):
+            return reason.acceptableContentTypes
+        default:
+            return nil
+        }
+    }
+
+    /// The response `Content-Type` of a `.responseValidationFailed` error.
+    public var responseContentType: String? {
+        switch self {
+        case .responseValidationFailed(let reason):
+            return reason.responseContentType
+        default:
+            return nil
+        }
+    }
+
+    /// The response code of a `.responseValidationFailed` error.
+    public var responseCode: Int? {
+        switch self {
+        case .responseValidationFailed(let reason):
+            return reason.responseCode
+        default:
+            return nil
+        }
+    }
+
+    /// The `String.Encoding` associated with a failed `.stringResponse()` call.
+    public var failedStringEncoding: String.Encoding? {
+        switch self {
+        case .responseSerializationFailed(let reason):
+            return reason.failedStringEncoding
+        default:
+            return nil
+        }
+    }
+}
+
+extension AFError.ParameterEncodingFailureReason {
+    var underlyingError: Error? {
+        switch self {
+        case .jsonEncodingFailed(let error), .propertyListEncodingFailed(let error):
+            return error
+        default:
+            return nil
+        }
+    }
+}
+
+extension AFError.MultipartEncodingFailureReason {
+    var url: URL? {
+        switch self {
+        case .bodyPartURLInvalid(let url), .bodyPartFilenameInvalid(let url), .bodyPartFileNotReachable(let url),
+             .bodyPartFileIsDirectory(let url), .bodyPartFileSizeNotAvailable(let url),
+             .bodyPartInputStreamCreationFailed(let url), .outputStreamCreationFailed(let url),
+             .outputStreamFileAlreadyExists(let url), .outputStreamURLInvalid(let url),
+             .bodyPartFileNotReachableWithError(let url, _), .bodyPartFileSizeQueryFailedWithError(let url, _):
+            return url
+        default:
+            return nil
+        }
+    }
+
+    var underlyingError: Error? {
+        switch self {
+        case .bodyPartFileNotReachableWithError(_, let error), .bodyPartFileSizeQueryFailedWithError(_, let error),
+             .outputStreamWriteFailed(let error), .inputStreamReadFailed(let error):
+            return error
+        default:
+            return nil
+        }
+    }
+}
+
+extension AFError.ResponseValidationFailureReason {
+    var acceptableContentTypes: [String]? {
+        switch self {
+        case .missingContentType(let types), .unacceptableContentType(let types, _):
+            return types
+        default:
+            return nil
+        }
+    }
+
+    var responseContentType: String? {
+        switch self {
+        case .unacceptableContentType(_, let responseType):
+            return responseType
+        default:
+            return nil
+        }
+    }
+
+    var responseCode: Int? {
+        switch self {
+        case .unacceptableStatusCode(let code):
+            return code
+        default:
+            return nil
+        }
+    }
+}
+
+extension AFError.ResponseSerializationFailureReason {
+    var failedStringEncoding: String.Encoding? {
+        switch self {
+        case .stringSerializationFailed(let encoding):
+            return encoding
+        default:
+            return nil
+        }
+    }
+
+    var underlyingError: Error? {
+        switch self {
+        case .jsonSerializationFailed(let error), .propertyListSerializationFailed(let error):
+            return error
+        default:
+            return nil
+        }
+    }
+}
+
+// MARK: - Error Descriptions
+
+extension AFError: LocalizedError {
+    public var errorDescription: String? {
+        switch self {
+        case .invalidURL(let url):
+            return "URL is not valid: \(url)"
+        case .parameterEncodingFailed(let reason):
+            return reason.localizedDescription
+        case .multipartEncodingFailed(let reason):
+            return reason.localizedDescription
+        case .responseValidationFailed(let reason):
+            return reason.localizedDescription
+        case .responseSerializationFailed(let reason):
+            return reason.localizedDescription
+        }
+    }
+}
+
+extension AFError.ParameterEncodingFailureReason {
+    var localizedDescription: String {
+        switch self {
+        case .missingURL:
+            return "URL request to encode was missing a URL"
+        case .jsonEncodingFailed(let error):
+            return "JSON could not be encoded because of error:\n\(error.localizedDescription)"
+        case .propertyListEncodingFailed(let error):
+            return "PropertyList could not be encoded because of error:\n\(error.localizedDescription)"
+        }
+    }
+}
+
+extension AFError.MultipartEncodingFailureReason {
+    var localizedDescription: String {
+        switch self {
+        case .bodyPartURLInvalid(let url):
+            return "The URL provided is not a file URL: \(url)"
+        case .bodyPartFilenameInvalid(let url):
+            return "The URL provided does not have a valid filename: \(url)"
+        case .bodyPartFileNotReachable(let url):
+            return "The URL provided is not reachable: \(url)"
+        case .bodyPartFileNotReachableWithError(let url, let error):
+            return (
+                "The system returned an error while checking the provided URL for " +
+                "reachability.\nURL: \(url)\nError: \(error)"
+            )
+        case .bodyPartFileIsDirectory(let url):
+            return "The URL provided is a directory: \(url)"
+        case .bodyPartFileSizeNotAvailable(let url):
+            return "Could not fetch the file size from the provided URL: \(url)"
+        case .bodyPartFileSizeQueryFailedWithError(let url, let error):
+            return (
+                "The system returned an error while attempting to fetch the file size from the " +
+                "provided URL.\nURL: \(url)\nError: \(error)"
+            )
+        case .bodyPartInputStreamCreationFailed(let url):
+            return "Failed to create an InputStream for the provided URL: \(url)"
+        case .outputStreamCreationFailed(let url):
+            return "Failed to create an OutputStream for URL: \(url)"
+        case .outputStreamFileAlreadyExists(let url):
+            return "A file already exists at the provided URL: \(url)"
+        case .outputStreamURLInvalid(let url):
+            return "The provided OutputStream URL is invalid: \(url)"
+        case .outputStreamWriteFailed(let error):
+            return "OutputStream write failed with error: \(error)"
+        case .inputStreamReadFailed(let error):
+            return "InputStream read failed with error: \(error)"
+        }
+    }
+}
+
+extension AFError.ResponseSerializationFailureReason {
+    var localizedDescription: String {
+        switch self {
+        case .inputDataNil:
+            return "Response could not be serialized, input data was nil."
+        case .inputDataNilOrZeroLength:
+            return "Response could not be serialized, input data was nil or zero length."
+        case .inputFileNil:
+            return "Response could not be serialized, input file was nil."
+        case .inputFileReadFailed(let url):
+            return "Response could not be serialized, input file could not be read: \(url)."
+        case .stringSerializationFailed(let encoding):
+            return "String could not be serialized with encoding: \(encoding)."
+        case .jsonSerializationFailed(let error):
+            return "JSON could not be serialized because of error:\n\(error.localizedDescription)"
+        case .propertyListSerializationFailed(let error):
+            return "PropertyList could not be serialized because of error:\n\(error.localizedDescription)"
+        }
+    }
+}
+
+extension AFError.ResponseValidationFailureReason {
+    var localizedDescription: String {
+        switch self {
+        case .dataFileNil:
+            return "Response could not be validated, data file was nil."
+        case .dataFileReadFailed(let url):
+            return "Response could not be validated, data file could not be read: \(url)."
+        case .missingContentType(let types):
+            return (
+                "Response Content-Type was missing and acceptable content types " +
+                "(\(types.joined(separator: ","))) do not match \"*/*\"."
+            )
+        case .unacceptableContentType(let acceptableTypes, let responseType):
+            return (
+                "Response Content-Type \"\(responseType)\" does not match any acceptable types: " +
+                "\(acceptableTypes.joined(separator: ","))."
+            )
+        case .unacceptableStatusCode(let code):
+            return "Response status code was unacceptable: \(code)."
+        }
+    }
+}

+ 465 - 0
Example/Pods/Alamofire/Source/Alamofire.swift

@@ -0,0 +1,465 @@
+//
+//  Alamofire.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// Types adopting the `URLConvertible` protocol can be used to construct URLs, which are then used to construct
+/// URL requests.
+public protocol URLConvertible {
+    /// Returns a URL that conforms to RFC 2396 or throws an `Error`.
+    ///
+    /// - throws: An `Error` if the type cannot be converted to a `URL`.
+    ///
+    /// - returns: A URL or throws an `Error`.
+    func asURL() throws -> URL
+}
+
+extension String: URLConvertible {
+    /// Returns a URL if `self` represents a valid URL string that conforms to RFC 2396 or throws an `AFError`.
+    ///
+    /// - throws: An `AFError.invalidURL` if `self` is not a valid URL string.
+    ///
+    /// - returns: A URL or throws an `AFError`.
+    public func asURL() throws -> URL {
+        guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) }
+        return url
+    }
+}
+
+extension URL: URLConvertible {
+    /// Returns self.
+    public func asURL() throws -> URL { return self }
+}
+
+extension URLComponents: URLConvertible {
+    /// Returns a URL if `url` is not nil, otherwise throws an `Error`.
+    ///
+    /// - throws: An `AFError.invalidURL` if `url` is `nil`.
+    ///
+    /// - returns: A URL or throws an `AFError`.
+    public func asURL() throws -> URL {
+        guard let url = url else { throw AFError.invalidURL(url: self) }
+        return url
+    }
+}
+
+// MARK: -
+
+/// Types adopting the `URLRequestConvertible` protocol can be used to construct URL requests.
+public protocol URLRequestConvertible {
+    /// Returns a URL request or throws if an `Error` was encountered.
+    ///
+    /// - throws: An `Error` if the underlying `URLRequest` is `nil`.
+    ///
+    /// - returns: A URL request.
+    func asURLRequest() throws -> URLRequest
+}
+
+extension URLRequestConvertible {
+    /// The URL request.
+    public var urlRequest: URLRequest? { return try? asURLRequest() }
+}
+
+extension URLRequest: URLRequestConvertible {
+    /// Returns a URL request or throws if an `Error` was encountered.
+    public func asURLRequest() throws -> URLRequest { return self }
+}
+
+// MARK: -
+
+extension URLRequest {
+    /// Creates an instance with the specified `method`, `urlString` and `headers`.
+    ///
+    /// - parameter url:     The URL.
+    /// - parameter method:  The HTTP method.
+    /// - parameter headers: The HTTP headers. `nil` by default.
+    ///
+    /// - returns: The new `URLRequest` instance.
+    public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws {
+        let url = try url.asURL()
+
+        self.init(url: url)
+
+        httpMethod = method.rawValue
+
+        if let headers = headers {
+            for (headerField, headerValue) in headers {
+                setValue(headerValue, forHTTPHeaderField: headerField)
+            }
+        }
+    }
+
+    func adapt(using adapter: RequestAdapter?) throws -> URLRequest {
+        guard let adapter = adapter else { return self }
+        return try adapter.adapt(self)
+    }
+}
+
+// MARK: - Data Request
+
+/// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of the specified `url`,
+/// `method`, `parameters`, `encoding` and `headers`.
+///
+/// - parameter url:        The URL.
+/// - parameter method:     The HTTP method. `.get` by default.
+/// - parameter parameters: The parameters. `nil` by default.
+/// - parameter encoding:   The parameter encoding. `URLEncoding.default` by default.
+/// - parameter headers:    The HTTP headers. `nil` by default.
+///
+/// - returns: The created `DataRequest`.
+@discardableResult
+public func request(
+    _ url: URLConvertible,
+    method: HTTPMethod = .get,
+    parameters: Parameters? = nil,
+    encoding: ParameterEncoding = URLEncoding.default,
+    headers: HTTPHeaders? = nil)
+    -> DataRequest
+{
+    return SessionManager.default.request(
+        url,
+        method: method,
+        parameters: parameters,
+        encoding: encoding,
+        headers: headers
+    )
+}
+
+/// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of a URL based on the
+/// specified `urlRequest`.
+///
+/// - parameter urlRequest: The URL request
+///
+/// - returns: The created `DataRequest`.
+@discardableResult
+public func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
+    return SessionManager.default.request(urlRequest)
+}
+
+// MARK: - Download Request
+
+// MARK: URL Request
+
+/// Creates a `DownloadRequest` using the default `SessionManager` to retrieve the contents of the specified `url`,
+/// `method`, `parameters`, `encoding`, `headers` and save them to the `destination`.
+///
+/// If `destination` is not specified, the contents will remain in the temporary location determined by the
+/// underlying URL session.
+///
+/// - parameter url:         The URL.
+/// - parameter method:      The HTTP method. `.get` by default.
+/// - parameter parameters:  The parameters. `nil` by default.
+/// - parameter encoding:    The parameter encoding. `URLEncoding.default` by default.
+/// - parameter headers:     The HTTP headers. `nil` by default.
+/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
+///
+/// - returns: The created `DownloadRequest`.
+@discardableResult
+public func download(
+    _ url: URLConvertible,
+    method: HTTPMethod = .get,
+    parameters: Parameters? = nil,
+    encoding: ParameterEncoding = URLEncoding.default,
+    headers: HTTPHeaders? = nil,
+    to destination: DownloadRequest.DownloadFileDestination? = nil)
+    -> DownloadRequest
+{
+    return SessionManager.default.download(
+        url,
+        method: method,
+        parameters: parameters,
+        encoding: encoding,
+        headers: headers,
+        to: destination
+    )
+}
+
+/// Creates a `DownloadRequest` using the default `SessionManager` to retrieve the contents of a URL based on the
+/// specified `urlRequest` and save them to the `destination`.
+///
+/// If `destination` is not specified, the contents will remain in the temporary location determined by the
+/// underlying URL session.
+///
+/// - parameter urlRequest:  The URL request.
+/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
+///
+/// - returns: The created `DownloadRequest`.
+@discardableResult
+public func download(
+    _ urlRequest: URLRequestConvertible,
+    to destination: DownloadRequest.DownloadFileDestination? = nil)
+    -> DownloadRequest
+{
+    return SessionManager.default.download(urlRequest, to: destination)
+}
+
+// MARK: Resume Data
+
+/// Creates a `DownloadRequest` using the default `SessionManager` from the `resumeData` produced from a
+/// previous request cancellation to retrieve the contents of the original request and save them to the `destination`.
+///
+/// If `destination` is not specified, the contents will remain in the temporary location determined by the
+/// underlying URL session.
+///
+/// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken
+/// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the
+/// data is written incorrectly and will always fail to resume the download. For more information about the bug and
+/// possible workarounds, please refer to the following Stack Overflow post:
+///
+///    - http://stackoverflow.com/a/39347461/1342462
+///
+/// - parameter resumeData:  The resume data. This is an opaque data blob produced by `URLSessionDownloadTask`
+///                          when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for additional
+///                          information.
+/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
+///
+/// - returns: The created `DownloadRequest`.
+@discardableResult
+public func download(
+    resumingWith resumeData: Data,
+    to destination: DownloadRequest.DownloadFileDestination? = nil)
+    -> DownloadRequest
+{
+    return SessionManager.default.download(resumingWith: resumeData, to: destination)
+}
+
+// MARK: - Upload Request
+
+// MARK: File
+
+/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
+/// for uploading the `file`.
+///
+/// - parameter file:    The file to upload.
+/// - parameter url:     The URL.
+/// - parameter method:  The HTTP method. `.post` by default.
+/// - parameter headers: The HTTP headers. `nil` by default.
+///
+/// - returns: The created `UploadRequest`.
+@discardableResult
+public func upload(
+    _ fileURL: URL,
+    to url: URLConvertible,
+    method: HTTPMethod = .post,
+    headers: HTTPHeaders? = nil)
+    -> UploadRequest
+{
+    return SessionManager.default.upload(fileURL, to: url, method: method, headers: headers)
+}
+
+/// Creates a `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
+/// uploading the `file`.
+///
+/// - parameter file:       The file to upload.
+/// - parameter urlRequest: The URL request.
+///
+/// - returns: The created `UploadRequest`.
+@discardableResult
+public func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest {
+    return SessionManager.default.upload(fileURL, with: urlRequest)
+}
+
+// MARK: Data
+
+/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
+/// for uploading the `data`.
+///
+/// - parameter data:    The data to upload.
+/// - parameter url:     The URL.
+/// - parameter method:  The HTTP method. `.post` by default.
+/// - parameter headers: The HTTP headers. `nil` by default.
+///
+/// - returns: The created `UploadRequest`.
+@discardableResult
+public func upload(
+    _ data: Data,
+    to url: URLConvertible,
+    method: HTTPMethod = .post,
+    headers: HTTPHeaders? = nil)
+    -> UploadRequest
+{
+    return SessionManager.default.upload(data, to: url, method: method, headers: headers)
+}
+
+/// Creates an `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
+/// uploading the `data`.
+///
+/// - parameter data:       The data to upload.
+/// - parameter urlRequest: The URL request.
+///
+/// - returns: The created `UploadRequest`.
+@discardableResult
+public func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest {
+    return SessionManager.default.upload(data, with: urlRequest)
+}
+
+// MARK: InputStream
+
+/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
+/// for uploading the `stream`.
+///
+/// - parameter stream:  The stream to upload.
+/// - parameter url:     The URL.
+/// - parameter method:  The HTTP method. `.post` by default.
+/// - parameter headers: The HTTP headers. `nil` by default.
+///
+/// - returns: The created `UploadRequest`.
+@discardableResult
+public func upload(
+    _ stream: InputStream,
+    to url: URLConvertible,
+    method: HTTPMethod = .post,
+    headers: HTTPHeaders? = nil)
+    -> UploadRequest
+{
+    return SessionManager.default.upload(stream, to: url, method: method, headers: headers)
+}
+
+/// Creates an `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
+/// uploading the `stream`.
+///
+/// - parameter urlRequest: The URL request.
+/// - parameter stream:     The stream to upload.
+///
+/// - returns: The created `UploadRequest`.
+@discardableResult
+public func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest {
+    return SessionManager.default.upload(stream, with: urlRequest)
+}
+
+// MARK: MultipartFormData
+
+/// Encodes `multipartFormData` using `encodingMemoryThreshold` with the default `SessionManager` and calls
+/// `encodingCompletion` with new `UploadRequest` using the `url`, `method` and `headers`.
+///
+/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
+/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
+/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
+/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
+/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
+/// used for larger payloads such as video content.
+///
+/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
+/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
+/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
+/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
+/// technique was used.
+///
+/// - parameter multipartFormData:       The closure used to append body parts to the `MultipartFormData`.
+/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
+///                                      `multipartFormDataEncodingMemoryThreshold` by default.
+/// - parameter url:                     The URL.
+/// - parameter method:                  The HTTP method. `.post` by default.
+/// - parameter headers:                 The HTTP headers. `nil` by default.
+/// - parameter encodingCompletion:      The closure called when the `MultipartFormData` encoding is complete.
+public func upload(
+    multipartFormData: @escaping (MultipartFormData) -> Void,
+    usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
+    to url: URLConvertible,
+    method: HTTPMethod = .post,
+    headers: HTTPHeaders? = nil,
+    encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?)
+{
+    return SessionManager.default.upload(
+        multipartFormData: multipartFormData,
+        usingThreshold: encodingMemoryThreshold,
+        to: url,
+        method: method,
+        headers: headers,
+        encodingCompletion: encodingCompletion
+    )
+}
+
+/// Encodes `multipartFormData` using `encodingMemoryThreshold` and the default `SessionManager` and
+/// calls `encodingCompletion` with new `UploadRequest` using the `urlRequest`.
+///
+/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
+/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
+/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
+/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
+/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
+/// used for larger payloads such as video content.
+///
+/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
+/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
+/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
+/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
+/// technique was used.
+///
+/// - parameter multipartFormData:       The closure used to append body parts to the `MultipartFormData`.
+/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
+///                                      `multipartFormDataEncodingMemoryThreshold` by default.
+/// - parameter urlRequest:              The URL request.
+/// - parameter encodingCompletion:      The closure called when the `MultipartFormData` encoding is complete.
+public func upload(
+    multipartFormData: @escaping (MultipartFormData) -> Void,
+    usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
+    with urlRequest: URLRequestConvertible,
+    encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?)
+{
+    return SessionManager.default.upload(
+        multipartFormData: multipartFormData,
+        usingThreshold: encodingMemoryThreshold,
+        with: urlRequest,
+        encodingCompletion: encodingCompletion
+    )
+}
+
+#if !os(watchOS)
+
+// MARK: - Stream Request
+
+// MARK: Hostname and Port
+
+/// Creates a `StreamRequest` using the default `SessionManager` for bidirectional streaming with the `hostname`
+/// and `port`.
+///
+/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+///
+/// - parameter hostName: The hostname of the server to connect to.
+/// - parameter port:     The port of the server to connect to.
+///
+/// - returns: The created `StreamRequest`.
+@discardableResult
+@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+public func stream(withHostName hostName: String, port: Int) -> StreamRequest {
+    return SessionManager.default.stream(withHostName: hostName, port: port)
+}
+
+// MARK: NetService
+
+/// Creates a `StreamRequest` using the default `SessionManager` for bidirectional streaming with the `netService`.
+///
+/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+///
+/// - parameter netService: The net service used to identify the endpoint.
+///
+/// - returns: The created `StreamRequest`.
+@discardableResult
+@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+public func stream(with netService: NetService) -> StreamRequest {
+    return SessionManager.default.stream(with: netService)
+}
+
+#endif

+ 37 - 0
Example/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift

@@ -0,0 +1,37 @@
+//
+//  DispatchQueue+Alamofire.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Dispatch
+import Foundation
+
+extension DispatchQueue {
+    static var userInteractive: DispatchQueue { return DispatchQueue.global(qos: .userInteractive) }
+    static var userInitiated: DispatchQueue { return DispatchQueue.global(qos: .userInitiated) }
+    static var utility: DispatchQueue { return DispatchQueue.global(qos: .utility) }
+    static var background: DispatchQueue { return DispatchQueue.global(qos: .background) }
+
+    func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) {
+        asyncAfter(deadline: .now() + delay, execute: closure)
+    }
+}

+ 580 - 0
Example/Pods/Alamofire/Source/MultipartFormData.swift

@@ -0,0 +1,580 @@
+//
+//  MultipartFormData.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+#if os(iOS) || os(watchOS) || os(tvOS)
+import MobileCoreServices
+#elseif os(macOS)
+import CoreServices
+#endif
+
+/// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode
+/// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead
+/// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the
+/// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for
+/// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset.
+///
+/// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well
+/// and the w3 form documentation.
+///
+/// - https://www.ietf.org/rfc/rfc2388.txt
+/// - https://www.ietf.org/rfc/rfc2045.txt
+/// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13
+open class MultipartFormData {
+
+    // MARK: - Helper Types
+
+    struct EncodingCharacters {
+        static let crlf = "\r\n"
+    }
+
+    struct BoundaryGenerator {
+        enum BoundaryType {
+            case initial, encapsulated, final
+        }
+
+        static func randomBoundary() -> String {
+            return String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random())
+        }
+
+        static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
+            let boundaryText: String
+
+            switch boundaryType {
+            case .initial:
+                boundaryText = "--\(boundary)\(EncodingCharacters.crlf)"
+            case .encapsulated:
+                boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
+            case .final:
+                boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
+            }
+
+            return boundaryText.data(using: String.Encoding.utf8, allowLossyConversion: false)!
+        }
+    }
+
+    class BodyPart {
+        let headers: HTTPHeaders
+        let bodyStream: InputStream
+        let bodyContentLength: UInt64
+        var hasInitialBoundary = false
+        var hasFinalBoundary = false
+
+        init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) {
+            self.headers = headers
+            self.bodyStream = bodyStream
+            self.bodyContentLength = bodyContentLength
+        }
+    }
+
+    // MARK: - Properties
+
+    /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
+    open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
+
+    /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries.
+    public var contentLength: UInt64 { return bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
+
+    /// The boundary used to separate the body parts in the encoded form data.
+    public var boundary: String
+
+    private var bodyParts: [BodyPart]
+    private var bodyPartError: AFError?
+    private let streamBufferSize: Int
+
+    // MARK: - Lifecycle
+
+    /// Creates a multipart form data object.
+    ///
+    /// - returns: The multipart form data object.
+    public init() {
+        self.boundary = BoundaryGenerator.randomBoundary()
+        self.bodyParts = []
+
+        ///
+        /// The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more
+        /// information, please refer to the following article:
+        ///   - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html
+        ///
+
+        self.streamBufferSize = 1024
+    }
+
+    // MARK: - Body Parts
+
+    /// Creates a body part from the data and appends it to the multipart form data object.
+    ///
+    /// The body part data will be encoded using the following format:
+    ///
+    /// - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
+    /// - Encoded data
+    /// - Multipart form boundary
+    ///
+    /// - parameter data: The data to encode into the multipart form data.
+    /// - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header.
+    public func append(_ data: Data, withName name: String) {
+        let headers = contentHeaders(withName: name)
+        let stream = InputStream(data: data)
+        let length = UInt64(data.count)
+
+        append(stream, withLength: length, headers: headers)
+    }
+
+    /// Creates a body part from the data and appends it to the multipart form data object.
+    ///
+    /// The body part data will be encoded using the following format:
+    ///
+    /// - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
+    /// - `Content-Type: #{generated mimeType}` (HTTP Header)
+    /// - Encoded data
+    /// - Multipart form boundary
+    ///
+    /// - parameter data:     The data to encode into the multipart form data.
+    /// - parameter name:     The name to associate with the data in the `Content-Disposition` HTTP header.
+    /// - parameter mimeType: The MIME type to associate with the data content type in the `Content-Type` HTTP header.
+    public func append(_ data: Data, withName name: String, mimeType: String) {
+        let headers = contentHeaders(withName: name, mimeType: mimeType)
+        let stream = InputStream(data: data)
+        let length = UInt64(data.count)
+
+        append(stream, withLength: length, headers: headers)
+    }
+
+    /// Creates a body part from the data and appends it to the multipart form data object.
+    ///
+    /// The body part data will be encoded using the following format:
+    ///
+    /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
+    /// - `Content-Type: #{mimeType}` (HTTP Header)
+    /// - Encoded file data
+    /// - Multipart form boundary
+    ///
+    /// - parameter data:     The data to encode into the multipart form data.
+    /// - parameter name:     The name to associate with the data in the `Content-Disposition` HTTP header.
+    /// - parameter fileName: The filename to associate with the data in the `Content-Disposition` HTTP header.
+    /// - parameter mimeType: The MIME type to associate with the data in the `Content-Type` HTTP header.
+    public func append(_ data: Data, withName name: String, fileName: String, mimeType: String) {
+        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
+        let stream = InputStream(data: data)
+        let length = UInt64(data.count)
+
+        append(stream, withLength: length, headers: headers)
+    }
+
+    /// Creates a body part from the file and appends it to the multipart form data object.
+    ///
+    /// The body part data will be encoded using the following format:
+    ///
+    /// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header)
+    /// - `Content-Type: #{generated mimeType}` (HTTP Header)
+    /// - Encoded file data
+    /// - Multipart form boundary
+    ///
+    /// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the
+    /// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the
+    /// system associated MIME type.
+    ///
+    /// - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data.
+    /// - parameter name:    The name to associate with the file content in the `Content-Disposition` HTTP header.
+    public func append(_ fileURL: URL, withName name: String) {
+        let fileName = fileURL.lastPathComponent
+        let pathExtension = fileURL.pathExtension
+
+        if !fileName.isEmpty && !pathExtension.isEmpty {
+            let mime = mimeType(forPathExtension: pathExtension)
+            append(fileURL, withName: name, fileName: fileName, mimeType: mime)
+        } else {
+            setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL))
+        }
+    }
+
+    /// Creates a body part from the file and appends it to the multipart form data object.
+    ///
+    /// The body part data will be encoded using the following format:
+    ///
+    /// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header)
+    /// - Content-Type: #{mimeType} (HTTP Header)
+    /// - Encoded file data
+    /// - Multipart form boundary
+    ///
+    /// - parameter fileURL:  The URL of the file whose content will be encoded into the multipart form data.
+    /// - parameter name:     The name to associate with the file content in the `Content-Disposition` HTTP header.
+    /// - parameter fileName: The filename to associate with the file content in the `Content-Disposition` HTTP header.
+    /// - parameter mimeType: The MIME type to associate with the file content in the `Content-Type` HTTP header.
+    public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) {
+        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
+
+        //============================================================
+        //                 Check 1 - is file URL?
+        //============================================================
+
+        guard fileURL.isFileURL else {
+            setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL))
+            return
+        }
+
+        //============================================================
+        //              Check 2 - is file URL reachable?
+        //============================================================
+
+        do {
+            let isReachable = try fileURL.checkPromisedItemIsReachable()
+            guard isReachable else {
+                setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL))
+                return
+            }
+        } catch {
+            setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
+            return
+        }
+
+        //============================================================
+        //            Check 3 - is file URL a directory?
+        //============================================================
+
+        var isDirectory: ObjCBool = false
+        let path = fileURL.path
+
+        guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else {
+            setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL))
+            return
+        }
+
+        //============================================================
+        //          Check 4 - can the file size be extracted?
+        //============================================================
+
+        let bodyContentLength: UInt64
+
+        do {
+            guard let fileSize = try FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber else {
+                setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL))
+                return
+            }
+
+            bodyContentLength = fileSize.uint64Value
+        }
+        catch {
+            setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error))
+            return
+        }
+
+        //============================================================
+        //       Check 5 - can a stream be created from file URL?
+        //============================================================
+
+        guard let stream = InputStream(url: fileURL) else {
+            setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL))
+            return
+        }
+
+        append(stream, withLength: bodyContentLength, headers: headers)
+    }
+
+    /// Creates a body part from the stream and appends it to the multipart form data object.
+    ///
+    /// The body part data will be encoded using the following format:
+    ///
+    /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
+    /// - `Content-Type: #{mimeType}` (HTTP Header)
+    /// - Encoded stream data
+    /// - Multipart form boundary
+    ///
+    /// - parameter stream:   The input stream to encode in the multipart form data.
+    /// - parameter length:   The content length of the stream.
+    /// - parameter name:     The name to associate with the stream content in the `Content-Disposition` HTTP header.
+    /// - parameter fileName: The filename to associate with the stream content in the `Content-Disposition` HTTP header.
+    /// - parameter mimeType: The MIME type to associate with the stream content in the `Content-Type` HTTP header.
+    public func append(
+        _ stream: InputStream,
+        withLength length: UInt64,
+        name: String,
+        fileName: String,
+        mimeType: String)
+    {
+        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
+        append(stream, withLength: length, headers: headers)
+    }
+
+    /// Creates a body part with the headers, stream and length and appends it to the multipart form data object.
+    ///
+    /// The body part data will be encoded using the following format:
+    ///
+    /// - HTTP headers
+    /// - Encoded stream data
+    /// - Multipart form boundary
+    ///
+    /// - parameter stream:  The input stream to encode in the multipart form data.
+    /// - parameter length:  The content length of the stream.
+    /// - parameter headers: The HTTP headers for the body part.
+    public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) {
+        let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
+        bodyParts.append(bodyPart)
+    }
+
+    // MARK: - Data Encoding
+
+    /// Encodes all the appended body parts into a single `Data` value.
+    ///
+    /// It is important to note that this method will load all the appended body parts into memory all at the same
+    /// time. This method should only be used when the encoded data will have a small memory footprint. For large data
+    /// cases, please use the `writeEncodedDataToDisk(fileURL:completionHandler:)` method.
+    ///
+    /// - throws: An `AFError` if encoding encounters an error.
+    ///
+    /// - returns: The encoded `Data` if encoding is successful.
+    public func encode() throws -> Data {
+        if let bodyPartError = bodyPartError {
+            throw bodyPartError
+        }
+
+        var encoded = Data()
+
+        bodyParts.first?.hasInitialBoundary = true
+        bodyParts.last?.hasFinalBoundary = true
+
+        for bodyPart in bodyParts {
+            let encodedData = try encode(bodyPart)
+            encoded.append(encodedData)
+        }
+
+        return encoded
+    }
+
+    /// Writes the appended body parts into the given file URL.
+    ///
+    /// This process is facilitated by reading and writing with input and output streams, respectively. Thus,
+    /// this approach is very memory efficient and should be used for large body part data.
+    ///
+    /// - parameter fileURL: The file URL to write the multipart form data into.
+    ///
+    /// - throws: An `AFError` if encoding encounters an error.
+    public func writeEncodedData(to fileURL: URL) throws {
+        if let bodyPartError = bodyPartError {
+            throw bodyPartError
+        }
+
+        if FileManager.default.fileExists(atPath: fileURL.path) {
+            throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
+        } else if !fileURL.isFileURL {
+            throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
+        }
+
+        guard let outputStream = OutputStream(url: fileURL, append: false) else {
+            throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
+        }
+
+        outputStream.open()
+        defer { outputStream.close() }
+
+        self.bodyParts.first?.hasInitialBoundary = true
+        self.bodyParts.last?.hasFinalBoundary = true
+
+        for bodyPart in self.bodyParts {
+            try write(bodyPart, to: outputStream)
+        }
+    }
+
+    // MARK: - Private - Body Part Encoding
+
+    private func encode(_ bodyPart: BodyPart) throws -> Data {
+        var encoded = Data()
+
+        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
+        encoded.append(initialData)
+
+        let headerData = encodeHeaders(for: bodyPart)
+        encoded.append(headerData)
+
+        let bodyStreamData = try encodeBodyStream(for: bodyPart)
+        encoded.append(bodyStreamData)
+
+        if bodyPart.hasFinalBoundary {
+            encoded.append(finalBoundaryData())
+        }
+
+        return encoded
+    }
+
+    private func encodeHeaders(for bodyPart: BodyPart) -> Data {
+        var headerText = ""
+
+        for (key, value) in bodyPart.headers {
+            headerText += "\(key): \(value)\(EncodingCharacters.crlf)"
+        }
+        headerText += EncodingCharacters.crlf
+
+        return headerText.data(using: String.Encoding.utf8, allowLossyConversion: false)!
+    }
+
+    private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data {
+        let inputStream = bodyPart.bodyStream
+        inputStream.open()
+        defer { inputStream.close() }
+
+        var encoded = Data()
+
+        while inputStream.hasBytesAvailable {
+            var buffer = [UInt8](repeating: 0, count: streamBufferSize)
+            let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
+
+            if let error = inputStream.streamError {
+                throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error))
+            }
+
+            if bytesRead > 0 {
+                encoded.append(buffer, count: bytesRead)
+            } else {
+                break
+            }
+        }
+
+        return encoded
+    }
+
+    // MARK: - Private - Writing Body Part to Output Stream
+
+    private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws {
+        try writeInitialBoundaryData(for: bodyPart, to: outputStream)
+        try writeHeaderData(for: bodyPart, to: outputStream)
+        try writeBodyStream(for: bodyPart, to: outputStream)
+        try writeFinalBoundaryData(for: bodyPart, to: outputStream)
+    }
+
+    private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
+        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
+        return try write(initialData, to: outputStream)
+    }
+
+    private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
+        let headerData = encodeHeaders(for: bodyPart)
+        return try write(headerData, to: outputStream)
+    }
+
+    private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
+        let inputStream = bodyPart.bodyStream
+
+        inputStream.open()
+        defer { inputStream.close() }
+
+        while inputStream.hasBytesAvailable {
+            var buffer = [UInt8](repeating: 0, count: streamBufferSize)
+            let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
+
+            if let streamError = inputStream.streamError {
+                throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError))
+            }
+
+            if bytesRead > 0 {
+                if buffer.count != bytesRead {
+                    buffer = Array(buffer[0..<bytesRead])
+                }
+
+                try write(&buffer, to: outputStream)
+            } else {
+                break
+            }
+        }
+    }
+
+    private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
+        if bodyPart.hasFinalBoundary {
+            return try write(finalBoundaryData(), to: outputStream)
+        }
+    }
+
+    // MARK: - Private - Writing Buffered Data to Output Stream
+
+    private func write(_ data: Data, to outputStream: OutputStream) throws {
+        var buffer = [UInt8](repeating: 0, count: data.count)
+        data.copyBytes(to: &buffer, count: data.count)
+
+        return try write(&buffer, to: outputStream)
+    }
+
+    private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws {
+        var bytesToWrite = buffer.count
+
+        while bytesToWrite > 0, outputStream.hasSpaceAvailable {
+            let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)
+
+            if let error = outputStream.streamError {
+                throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error))
+            }
+
+            bytesToWrite -= bytesWritten
+
+            if bytesToWrite > 0 {
+                buffer = Array(buffer[bytesWritten..<buffer.count])
+            }
+        }
+    }
+
+    // MARK: - Private - Mime Type
+
+    private func mimeType(forPathExtension pathExtension: String) -> String {
+        if
+            let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
+            let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue()
+        {
+            return contentType as String
+        }
+
+        return "application/octet-stream"
+    }
+
+    // MARK: - Private - Content Headers
+
+    private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> [String: String] {
+        var disposition = "form-data; name=\"\(name)\""
+        if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" }
+
+        var headers = ["Content-Disposition": disposition]
+        if let mimeType = mimeType { headers["Content-Type"] = mimeType }
+
+        return headers
+    }
+
+    // MARK: - Private - Boundary Encoding
+
+    private func initialBoundaryData() -> Data {
+        return BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary)
+    }
+
+    private func encapsulatedBoundaryData() -> Data {
+        return BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary)
+    }
+
+    private func finalBoundaryData() -> Data {
+        return BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary)
+    }
+
+    // MARK: - Private - Errors
+
+    private func setBodyPartError(withReason reason: AFError.MultipartEncodingFailureReason) {
+        guard bodyPartError == nil else { return }
+        bodyPartError = AFError.multipartEncodingFailed(reason: reason)
+    }
+}

+ 238 - 0
Example/Pods/Alamofire/Source/NetworkReachabilityManager.swift

@@ -0,0 +1,238 @@
+//
+//  NetworkReachabilityManager.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+#if !os(watchOS)
+
+import Foundation
+import SystemConfiguration
+
+/// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both WWAN and
+/// WiFi network interfaces.
+///
+/// Reachability can be used to determine background information about why a network operation failed, or to retry
+/// network requests when a connection is established. It should not be used to prevent a user from initiating a network
+/// request, as it's possible that an initial request may be required to establish reachability.
+open class NetworkReachabilityManager {
+    /// Defines the various states of network reachability.
+    ///
+    /// - unknown:      It is unknown whether the network is reachable.
+    /// - notReachable: The network is not reachable.
+    /// - reachable:    The network is reachable.
+    public enum NetworkReachabilityStatus {
+        case unknown
+        case notReachable
+        case reachable(ConnectionType)
+    }
+
+    /// Defines the various connection types detected by reachability flags.
+    ///
+    /// - ethernetOrWiFi: The connection type is either over Ethernet or WiFi.
+    /// - wwan:           The connection type is a WWAN connection.
+    public enum ConnectionType {
+        case ethernetOrWiFi
+        case wwan
+    }
+
+    /// A closure executed when the network reachability status changes. The closure takes a single argument: the
+    /// network reachability status.
+    public typealias Listener = (NetworkReachabilityStatus) -> Void
+
+    // MARK: - Properties
+
+    /// Whether the network is currently reachable.
+    open var isReachable: Bool { return isReachableOnWWAN || isReachableOnEthernetOrWiFi }
+
+    /// Whether the network is currently reachable over the WWAN interface.
+    open var isReachableOnWWAN: Bool { return networkReachabilityStatus == .reachable(.wwan) }
+
+    /// Whether the network is currently reachable over Ethernet or WiFi interface.
+    open var isReachableOnEthernetOrWiFi: Bool { return networkReachabilityStatus == .reachable(.ethernetOrWiFi) }
+
+    /// The current network reachability status.
+    open var networkReachabilityStatus: NetworkReachabilityStatus {
+        guard let flags = self.flags else { return .unknown }
+        return networkReachabilityStatusForFlags(flags)
+    }
+
+    /// The dispatch queue to execute the `listener` closure on.
+    open var listenerQueue: DispatchQueue = DispatchQueue.main
+
+    /// A closure executed when the network reachability status changes.
+    open var listener: Listener?
+
+    open var flags: SCNetworkReachabilityFlags? {
+        var flags = SCNetworkReachabilityFlags()
+
+        if SCNetworkReachabilityGetFlags(reachability, &flags) {
+            return flags
+        }
+
+        return nil
+    }
+
+    private let reachability: SCNetworkReachability
+    open var previousFlags: SCNetworkReachabilityFlags
+
+    // MARK: - Initialization
+
+    /// Creates a `NetworkReachabilityManager` instance with the specified host.
+    ///
+    /// - parameter host: The host used to evaluate network reachability.
+    ///
+    /// - returns: The new `NetworkReachabilityManager` instance.
+    public convenience init?(host: String) {
+        guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
+        self.init(reachability: reachability)
+    }
+
+    /// Creates a `NetworkReachabilityManager` instance that monitors the address 0.0.0.0.
+    ///
+    /// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing
+    /// status of the device, both IPv4 and IPv6.
+    ///
+    /// - returns: The new `NetworkReachabilityManager` instance.
+    public convenience init?() {
+        var address = sockaddr_in()
+        address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
+        address.sin_family = sa_family_t(AF_INET)
+
+        guard let reachability = withUnsafePointer(to: &address, { pointer in
+            return pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size) {
+                return SCNetworkReachabilityCreateWithAddress(nil, $0)
+            }
+        }) else { return nil }
+
+        self.init(reachability: reachability)
+    }
+
+    private init(reachability: SCNetworkReachability) {
+        self.reachability = reachability
+
+        // Set the previous flags to an unreserved value to represent unknown status
+        self.previousFlags = SCNetworkReachabilityFlags(rawValue: 1 << 30)
+    }
+
+    deinit {
+        stopListening()
+    }
+
+    // MARK: - Listening
+
+    /// Starts listening for changes in network reachability status.
+    ///
+    /// - returns: `true` if listening was started successfully, `false` otherwise.
+    @discardableResult
+    open func startListening() -> Bool {
+        var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
+        context.info = Unmanaged.passUnretained(self).toOpaque()
+
+        let callbackEnabled = SCNetworkReachabilitySetCallback(
+            reachability,
+            { (_, flags, info) in
+                let reachability = Unmanaged<NetworkReachabilityManager>.fromOpaque(info!).takeUnretainedValue()
+                reachability.notifyListener(flags)
+            },
+            &context
+        )
+
+        let queueEnabled = SCNetworkReachabilitySetDispatchQueue(reachability, listenerQueue)
+
+        listenerQueue.async {
+            self.previousFlags = SCNetworkReachabilityFlags(rawValue: 1 << 30)
+
+            guard let flags = self.flags else { return }
+
+            self.notifyListener(flags)
+        }
+
+        return callbackEnabled && queueEnabled
+    }
+
+    /// Stops listening for changes in network reachability status.
+    open func stopListening() {
+        SCNetworkReachabilitySetCallback(reachability, nil, nil)
+        SCNetworkReachabilitySetDispatchQueue(reachability, nil)
+    }
+
+    // MARK: - Internal - Listener Notification
+
+    func notifyListener(_ flags: SCNetworkReachabilityFlags) {
+        guard previousFlags != flags else { return }
+        previousFlags = flags
+
+        listener?(networkReachabilityStatusForFlags(flags))
+    }
+
+    // MARK: - Internal - Network Reachability Status
+
+    func networkReachabilityStatusForFlags(_ flags: SCNetworkReachabilityFlags) -> NetworkReachabilityStatus {
+        guard isNetworkReachable(with: flags) else { return .notReachable }
+
+        var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)
+
+    #if os(iOS)
+        if flags.contains(.isWWAN) { networkStatus = .reachable(.wwan) }
+    #endif
+
+        return networkStatus
+    }
+
+    func isNetworkReachable(with flags: SCNetworkReachabilityFlags) -> Bool {
+        let isReachable = flags.contains(.reachable)
+        let needsConnection = flags.contains(.connectionRequired)
+        let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic)
+        let canConnectWithoutUserInteraction = canConnectAutomatically && !flags.contains(.interventionRequired)
+
+        return isReachable && (!needsConnection || canConnectWithoutUserInteraction)
+    }
+}
+
+// MARK: -
+
+extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {}
+
+/// Returns whether the two network reachability status values are equal.
+///
+/// - parameter lhs: The left-hand side value to compare.
+/// - parameter rhs: The right-hand side value to compare.
+///
+/// - returns: `true` if the two values are equal, `false` otherwise.
+public func ==(
+    lhs: NetworkReachabilityManager.NetworkReachabilityStatus,
+    rhs: NetworkReachabilityManager.NetworkReachabilityStatus)
+    -> Bool
+{
+    switch (lhs, rhs) {
+    case (.unknown, .unknown):
+        return true
+    case (.notReachable, .notReachable):
+        return true
+    case let (.reachable(lhsConnectionType), .reachable(rhsConnectionType)):
+        return lhsConnectionType == rhsConnectionType
+    default:
+        return false
+    }
+}
+
+#endif

+ 55 - 0
Example/Pods/Alamofire/Source/Notifications.swift

@@ -0,0 +1,55 @@
+//
+//  Notifications.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension Notification.Name {
+    /// Used as a namespace for all `URLSessionTask` related notifications.
+    public struct Task {
+        /// Posted when a `URLSessionTask` is resumed. The notification `object` contains the resumed `URLSessionTask`.
+        public static let DidResume = Notification.Name(rawValue: "org.alamofire.notification.name.task.didResume")
+
+        /// Posted when a `URLSessionTask` is suspended. The notification `object` contains the suspended `URLSessionTask`.
+        public static let DidSuspend = Notification.Name(rawValue: "org.alamofire.notification.name.task.didSuspend")
+
+        /// Posted when a `URLSessionTask` is cancelled. The notification `object` contains the cancelled `URLSessionTask`.
+        public static let DidCancel = Notification.Name(rawValue: "org.alamofire.notification.name.task.didCancel")
+
+        /// Posted when a `URLSessionTask` is completed. The notification `object` contains the completed `URLSessionTask`.
+        public static let DidComplete = Notification.Name(rawValue: "org.alamofire.notification.name.task.didComplete")
+    }
+}
+
+// MARK: -
+
+extension Notification {
+    /// Used as a namespace for all `Notification` user info dictionary keys.
+    public struct Key {
+        /// User info dictionary key representing the `URLSessionTask` associated with the notification.
+        public static let Task = "org.alamofire.notification.key.task"
+
+        /// User info dictionary key representing the responseData associated with the notification.
+        public static let ResponseData = "org.alamofire.notification.key.responseData"
+    }
+}

+ 483 - 0
Example/Pods/Alamofire/Source/ParameterEncoding.swift

@@ -0,0 +1,483 @@
+//
+//  ParameterEncoding.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// HTTP method definitions.
+///
+/// See https://tools.ietf.org/html/rfc7231#section-4.3
+public enum HTTPMethod: String {
+    case options = "OPTIONS"
+    case get     = "GET"
+    case head    = "HEAD"
+    case post    = "POST"
+    case put     = "PUT"
+    case patch   = "PATCH"
+    case delete  = "DELETE"
+    case trace   = "TRACE"
+    case connect = "CONNECT"
+}
+
+// MARK: -
+
+/// A dictionary of parameters to apply to a `URLRequest`.
+public typealias Parameters = [String: Any]
+
+/// A type used to define how a set of parameters are applied to a `URLRequest`.
+public protocol ParameterEncoding {
+    /// Creates a URL request by encoding parameters and applying them onto an existing request.
+    ///
+    /// - parameter urlRequest: The request to have parameters applied.
+    /// - parameter parameters: The parameters to apply.
+    ///
+    /// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
+    ///
+    /// - returns: The encoded request.
+    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
+}
+
+// MARK: -
+
+/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
+/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
+/// the HTTP body depends on the destination of the encoding.
+///
+/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
+/// `application/x-www-form-urlencoded; charset=utf-8`.
+///
+/// There is no published specification for how to encode collection types. By default the convention of appending
+/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for
+/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the
+/// square brackets appended to array keys.
+///
+/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode
+/// `true` as 1 and `false` as 0.
+public struct URLEncoding: ParameterEncoding {
+
+    // MARK: Helper Types
+
+    /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the
+    /// resulting URL request.
+    ///
+    /// - methodDependent: Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE`
+    ///                    requests and sets as the HTTP body for requests with any other HTTP method.
+    /// - queryString:     Sets or appends encoded query string result to existing query string.
+    /// - httpBody:        Sets encoded query string result as the HTTP body of the URL request.
+    public enum Destination {
+        case methodDependent, queryString, httpBody
+    }
+
+    /// Configures how `Array` parameters are encoded.
+    ///
+    /// - brackets:        An empty set of square brackets is appended to the key for every value.
+    ///                    This is the default behavior.
+    /// - noBrackets:      No brackets are appended. The key is encoded as is.
+    public enum ArrayEncoding {
+        case brackets, noBrackets
+
+        func encode(key: String) -> String {
+            switch self {
+            case .brackets:
+                return "\(key)[]"
+            case .noBrackets:
+                return key
+            }
+        }
+    }
+
+    /// Configures how `Bool` parameters are encoded.
+    ///
+    /// - numeric:         Encode `true` as `1` and `false` as `0`. This is the default behavior.
+    /// - literal:         Encode `true` and `false` as string literals.
+    public enum BoolEncoding {
+        case numeric, literal
+
+        func encode(value: Bool) -> String {
+            switch self {
+            case .numeric:
+                return value ? "1" : "0"
+            case .literal:
+                return value ? "true" : "false"
+            }
+        }
+    }
+
+    // MARK: Properties
+
+    /// Returns a default `URLEncoding` instance.
+    public static var `default`: URLEncoding { return URLEncoding() }
+
+    /// Returns a `URLEncoding` instance with a `.methodDependent` destination.
+    public static var methodDependent: URLEncoding { return URLEncoding() }
+
+    /// Returns a `URLEncoding` instance with a `.queryString` destination.
+    public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }
+
+    /// Returns a `URLEncoding` instance with an `.httpBody` destination.
+    public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }
+
+    /// The destination defining where the encoded query string is to be applied to the URL request.
+    public let destination: Destination
+
+    /// The encoding to use for `Array` parameters.
+    public let arrayEncoding: ArrayEncoding
+
+    /// The encoding to use for `Bool` parameters.
+    public let boolEncoding: BoolEncoding
+
+    // MARK: Initialization
+
+    /// Creates a `URLEncoding` instance using the specified destination.
+    ///
+    /// - parameter destination: The destination defining where the encoded query string is to be applied.
+    /// - parameter arrayEncoding: The encoding to use for `Array` parameters.
+    /// - parameter boolEncoding: The encoding to use for `Bool` parameters.
+    ///
+    /// - returns: The new `URLEncoding` instance.
+    public init(destination: Destination = .methodDependent, arrayEncoding: ArrayEncoding = .brackets, boolEncoding: BoolEncoding = .numeric) {
+        self.destination = destination
+        self.arrayEncoding = arrayEncoding
+        self.boolEncoding = boolEncoding
+    }
+
+    // MARK: Encoding
+
+    /// Creates a URL request by encoding parameters and applying them onto an existing request.
+    ///
+    /// - parameter urlRequest: The request to have parameters applied.
+    /// - parameter parameters: The parameters to apply.
+    ///
+    /// - throws: An `Error` if the encoding process encounters an error.
+    ///
+    /// - returns: The encoded request.
+    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
+        var urlRequest = try urlRequest.asURLRequest()
+
+        guard let parameters = parameters else { return urlRequest }
+
+        if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: method) {
+            guard let url = urlRequest.url else {
+                throw AFError.parameterEncodingFailed(reason: .missingURL)
+            }
+
+            if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
+                let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
+                urlComponents.percentEncodedQuery = percentEncodedQuery
+                urlRequest.url = urlComponents.url
+            }
+        } else {
+            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
+                urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
+            }
+
+            urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false)
+        }
+
+        return urlRequest
+    }
+
+    /// Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion.
+    ///
+    /// - parameter key:   The key of the query component.
+    /// - parameter value: The value of the query component.
+    ///
+    /// - returns: The percent-escaped, URL encoded query string components.
+    public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
+        var components: [(String, String)] = []
+
+        if let dictionary = value as? [String: Any] {
+            for (nestedKey, value) in dictionary {
+                components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
+            }
+        } else if let array = value as? [Any] {
+            for value in array {
+                components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
+            }
+        } else if let value = value as? NSNumber {
+            if value.isBool {
+                components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
+            } else {
+                components.append((escape(key), escape("\(value)")))
+            }
+        } else if let bool = value as? Bool {
+            components.append((escape(key), escape(boolEncoding.encode(value: bool))))
+        } else {
+            components.append((escape(key), escape("\(value)")))
+        }
+
+        return components
+    }
+
+    /// Returns a percent-escaped string following RFC 3986 for a query string key or value.
+    ///
+    /// RFC 3986 states that the following characters are "reserved" characters.
+    ///
+    /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
+    /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
+    ///
+    /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
+    /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
+    /// should be percent-escaped in the query string.
+    ///
+    /// - parameter string: The string to be percent-escaped.
+    ///
+    /// - returns: The percent-escaped string.
+    public func escape(_ string: String) -> String {
+        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
+        let subDelimitersToEncode = "!$&'()*+,;="
+
+        var allowedCharacterSet = CharacterSet.urlQueryAllowed
+        allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
+
+        var escaped = ""
+
+        //==========================================================================================================
+        //
+        //  Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
+        //  hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
+        //  longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
+        //  info, please refer to:
+        //
+        //      - https://github.com/Alamofire/Alamofire/issues/206
+        //
+        //==========================================================================================================
+
+        if #available(iOS 8.3, *) {
+            escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
+        } else {
+            let batchSize = 50
+            var index = string.startIndex
+
+            while index != string.endIndex {
+                let startIndex = index
+                let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
+                let range = startIndex..<endIndex
+
+                let substring = string[range]
+
+                escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? String(substring)
+
+                index = endIndex
+            }
+        }
+
+        return escaped
+    }
+
+    private func query(_ parameters: [String: Any]) -> String {
+        var components: [(String, String)] = []
+
+        for key in parameters.keys.sorted(by: <) {
+            let value = parameters[key]!
+            components += queryComponents(fromKey: key, value: value)
+        }
+        return components.map { "\($0)=\($1)" }.joined(separator: "&")
+    }
+
+    private func encodesParametersInURL(with method: HTTPMethod) -> Bool {
+        switch destination {
+        case .queryString:
+            return true
+        case .httpBody:
+            return false
+        default:
+            break
+        }
+
+        switch method {
+        case .get, .head, .delete:
+            return true
+        default:
+            return false
+        }
+    }
+}
+
+// MARK: -
+
+/// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the
+/// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
+public struct JSONEncoding: ParameterEncoding {
+
+    // MARK: Properties
+
+    /// Returns a `JSONEncoding` instance with default writing options.
+    public static var `default`: JSONEncoding { return JSONEncoding() }
+
+    /// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options.
+    public static var prettyPrinted: JSONEncoding { return JSONEncoding(options: .prettyPrinted) }
+
+    /// The options for writing the parameters as JSON data.
+    public let options: JSONSerialization.WritingOptions
+
+    // MARK: Initialization
+
+    /// Creates a `JSONEncoding` instance using the specified options.
+    ///
+    /// - parameter options: The options for writing the parameters as JSON data.
+    ///
+    /// - returns: The new `JSONEncoding` instance.
+    public init(options: JSONSerialization.WritingOptions = []) {
+        self.options = options
+    }
+
+    // MARK: Encoding
+
+    /// Creates a URL request by encoding parameters and applying them onto an existing request.
+    ///
+    /// - parameter urlRequest: The request to have parameters applied.
+    /// - parameter parameters: The parameters to apply.
+    ///
+    /// - throws: An `Error` if the encoding process encounters an error.
+    ///
+    /// - returns: The encoded request.
+    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
+        var urlRequest = try urlRequest.asURLRequest()
+
+        guard let parameters = parameters else { return urlRequest }
+
+        do {
+            let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
+
+            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
+                urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
+            }
+
+            urlRequest.httpBody = data
+        } catch {
+            throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
+        }
+
+        return urlRequest
+    }
+
+    /// Creates a URL request by encoding the JSON object and setting the resulting data on the HTTP body.
+    ///
+    /// - parameter urlRequest: The request to apply the JSON object to.
+    /// - parameter jsonObject: The JSON object to apply to the request.
+    ///
+    /// - throws: An `Error` if the encoding process encounters an error.
+    ///
+    /// - returns: The encoded request.
+    public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest {
+        var urlRequest = try urlRequest.asURLRequest()
+
+        guard let jsonObject = jsonObject else { return urlRequest }
+
+        do {
+            let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options)
+
+            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
+                urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
+            }
+
+            urlRequest.httpBody = data
+        } catch {
+            throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
+        }
+
+        return urlRequest
+    }
+}
+
+// MARK: -
+
+/// Uses `PropertyListSerialization` to create a plist representation of the parameters object, according to the
+/// associated format and write options values, which is set as the body of the request. The `Content-Type` HTTP header
+/// field of an encoded request is set to `application/x-plist`.
+public struct PropertyListEncoding: ParameterEncoding {
+
+    // MARK: Properties
+
+    /// Returns a default `PropertyListEncoding` instance.
+    public static var `default`: PropertyListEncoding { return PropertyListEncoding() }
+
+    /// Returns a `PropertyListEncoding` instance with xml formatting and default writing options.
+    public static var xml: PropertyListEncoding { return PropertyListEncoding(format: .xml) }
+
+    /// Returns a `PropertyListEncoding` instance with binary formatting and default writing options.
+    public static var binary: PropertyListEncoding { return PropertyListEncoding(format: .binary) }
+
+    /// The property list serialization format.
+    public let format: PropertyListSerialization.PropertyListFormat
+
+    /// The options for writing the parameters as plist data.
+    public let options: PropertyListSerialization.WriteOptions
+
+    // MARK: Initialization
+
+    /// Creates a `PropertyListEncoding` instance using the specified format and options.
+    ///
+    /// - parameter format:  The property list serialization format.
+    /// - parameter options: The options for writing the parameters as plist data.
+    ///
+    /// - returns: The new `PropertyListEncoding` instance.
+    public init(
+        format: PropertyListSerialization.PropertyListFormat = .xml,
+        options: PropertyListSerialization.WriteOptions = 0)
+    {
+        self.format = format
+        self.options = options
+    }
+
+    // MARK: Encoding
+
+    /// Creates a URL request by encoding parameters and applying them onto an existing request.
+    ///
+    /// - parameter urlRequest: The request to have parameters applied.
+    /// - parameter parameters: The parameters to apply.
+    ///
+    /// - throws: An `Error` if the encoding process encounters an error.
+    ///
+    /// - returns: The encoded request.
+    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
+        var urlRequest = try urlRequest.asURLRequest()
+
+        guard let parameters = parameters else { return urlRequest }
+
+        do {
+            let data = try PropertyListSerialization.data(
+                fromPropertyList: parameters,
+                format: format,
+                options: options
+            )
+
+            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
+                urlRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
+            }
+
+            urlRequest.httpBody = data
+        } catch {
+            throw AFError.parameterEncodingFailed(reason: .propertyListEncodingFailed(error: error))
+        }
+
+        return urlRequest
+    }
+}
+
+// MARK: -
+
+extension NSNumber {
+    fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) }
+}

+ 660 - 0
Example/Pods/Alamofire/Source/Request.swift

@@ -0,0 +1,660 @@
+//
+//  Request.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary.
+public protocol RequestAdapter {
+    /// Inspects and adapts the specified `URLRequest` in some manner if necessary and returns the result.
+    ///
+    /// - parameter urlRequest: The URL request to adapt.
+    ///
+    /// - throws: An `Error` if the adaptation encounters an error.
+    ///
+    /// - returns: The adapted `URLRequest`.
+    func adapt(_ urlRequest: URLRequest) throws -> URLRequest
+}
+
+// MARK: -
+
+/// A closure executed when the `RequestRetrier` determines whether a `Request` should be retried or not.
+public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void
+
+/// A type that determines whether a request should be retried after being executed by the specified session manager
+/// and encountering an error.
+public protocol RequestRetrier {
+    /// Determines whether the `Request` should be retried by calling the `completion` closure.
+    ///
+    /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs
+    /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly
+    /// cleaned up after.
+    ///
+    /// - parameter manager:    The session manager the request was executed on.
+    /// - parameter request:    The request that failed due to the encountered error.
+    /// - parameter error:      The error encountered when executing the request.
+    /// - parameter completion: The completion closure to be executed when retry decision has been determined.
+    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion)
+}
+
+// MARK: -
+
+protocol TaskConvertible {
+    func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask
+}
+
+/// A dictionary of headers to apply to a `URLRequest`.
+public typealias HTTPHeaders = [String: String]
+
+// MARK: -
+
+/// Responsible for sending a request and receiving the response and associated data from the server, as well as
+/// managing its underlying `URLSessionTask`.
+open class Request {
+
+    // MARK: Helper Types
+
+    /// A closure executed when monitoring upload or download progress of a request.
+    public typealias ProgressHandler = (Progress) -> Void
+
+    enum RequestTask {
+        case data(TaskConvertible?, URLSessionTask?)
+        case download(TaskConvertible?, URLSessionTask?)
+        case upload(TaskConvertible?, URLSessionTask?)
+        case stream(TaskConvertible?, URLSessionTask?)
+    }
+
+    // MARK: Properties
+
+    /// The delegate for the underlying task.
+    open internal(set) var delegate: TaskDelegate {
+        get {
+            taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
+            return taskDelegate
+        }
+        set {
+            taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
+            taskDelegate = newValue
+        }
+    }
+
+    /// The underlying task.
+    open var task: URLSessionTask? { return delegate.task }
+
+    /// The session belonging to the underlying task.
+    public let session: URLSession
+
+    /// The request sent or to be sent to the server.
+    open var request: URLRequest? { return task?.originalRequest }
+
+    /// The response received from the server, if any.
+    open var response: HTTPURLResponse? { return task?.response as? HTTPURLResponse }
+
+    /// The number of times the request has been retried.
+    open internal(set) var retryCount: UInt = 0
+
+    let originalTask: TaskConvertible?
+
+    var startTime: CFAbsoluteTime?
+    var endTime: CFAbsoluteTime?
+
+    var validations: [() -> Void] = []
+
+    private var taskDelegate: TaskDelegate
+    private var taskDelegateLock = NSLock()
+
+    // MARK: Lifecycle
+
+    init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
+        self.session = session
+
+        switch requestTask {
+        case .data(let originalTask, let task):
+            taskDelegate = DataTaskDelegate(task: task)
+            self.originalTask = originalTask
+        case .download(let originalTask, let task):
+            taskDelegate = DownloadTaskDelegate(task: task)
+            self.originalTask = originalTask
+        case .upload(let originalTask, let task):
+            taskDelegate = UploadTaskDelegate(task: task)
+            self.originalTask = originalTask
+        case .stream(let originalTask, let task):
+            taskDelegate = TaskDelegate(task: task)
+            self.originalTask = originalTask
+        }
+
+        delegate.error = error
+        delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }
+    }
+
+    // MARK: Authentication
+
+    /// Associates an HTTP Basic credential with the request.
+    ///
+    /// - parameter user:        The user.
+    /// - parameter password:    The password.
+    /// - parameter persistence: The URL credential persistence. `.ForSession` by default.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    open func authenticate(
+        user: String,
+        password: String,
+        persistence: URLCredential.Persistence = .forSession)
+        -> Self
+    {
+        let credential = URLCredential(user: user, password: password, persistence: persistence)
+        return authenticate(usingCredential: credential)
+    }
+
+    /// Associates a specified credential with the request.
+    ///
+    /// - parameter credential: The credential.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    open func authenticate(usingCredential credential: URLCredential) -> Self {
+        delegate.credential = credential
+        return self
+    }
+
+    /// Returns a base64 encoded basic authentication credential as an authorization header tuple.
+    ///
+    /// - parameter user:     The user.
+    /// - parameter password: The password.
+    ///
+    /// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise.
+    open class func authorizationHeader(user: String, password: String) -> (key: String, value: String)? {
+        guard let data = "\(user):\(password)".data(using: .utf8) else { return nil }
+
+        let credential = data.base64EncodedString(options: [])
+
+        return (key: "Authorization", value: "Basic \(credential)")
+    }
+
+    // MARK: State
+
+    /// Resumes the request.
+    open func resume() {
+        guard let task = task else { delegate.queue.isSuspended = false ; return }
+
+        if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }
+
+        task.resume()
+
+        NotificationCenter.default.post(
+            name: Notification.Name.Task.DidResume,
+            object: self,
+            userInfo: [Notification.Key.Task: task]
+        )
+    }
+
+    /// Suspends the request.
+    open func suspend() {
+        guard let task = task else { return }
+
+        task.suspend()
+
+        NotificationCenter.default.post(
+            name: Notification.Name.Task.DidSuspend,
+            object: self,
+            userInfo: [Notification.Key.Task: task]
+        )
+    }
+
+    /// Cancels the request.
+    open func cancel() {
+        guard let task = task else { return }
+
+        task.cancel()
+
+        NotificationCenter.default.post(
+            name: Notification.Name.Task.DidCancel,
+            object: self,
+            userInfo: [Notification.Key.Task: task]
+        )
+    }
+}
+
+// MARK: - CustomStringConvertible
+
+extension Request: CustomStringConvertible {
+    /// The textual representation used when written to an output stream, which includes the HTTP method and URL, as
+    /// well as the response status code if a response has been received.
+    open var description: String {
+        var components: [String] = []
+
+        if let HTTPMethod = request?.httpMethod {
+            components.append(HTTPMethod)
+        }
+
+        if let urlString = request?.url?.absoluteString {
+            components.append(urlString)
+        }
+
+        if let response = response {
+            components.append("(\(response.statusCode))")
+        }
+
+        return components.joined(separator: " ")
+    }
+}
+
+// MARK: - CustomDebugStringConvertible
+
+extension Request: CustomDebugStringConvertible {
+    /// The textual representation used when written to an output stream, in the form of a cURL command.
+    open var debugDescription: String {
+        return cURLRepresentation()
+    }
+
+    func cURLRepresentation() -> String {
+        var components = ["$ curl -v"]
+
+        guard let request = self.request,
+              let url = request.url,
+              let host = url.host
+        else {
+            return "$ curl command could not be created"
+        }
+
+        if let httpMethod = request.httpMethod, httpMethod != "GET" {
+            components.append("-X \(httpMethod)")
+        }
+
+        if let credentialStorage = self.session.configuration.urlCredentialStorage {
+            let protectionSpace = URLProtectionSpace(
+                host: host,
+                port: url.port ?? 0,
+                protocol: url.scheme,
+                realm: host,
+                authenticationMethod: NSURLAuthenticationMethodHTTPBasic
+            )
+
+            if let credentials = credentialStorage.credentials(for: protectionSpace)?.values {
+                for credential in credentials {
+                    guard let user = credential.user, let password = credential.password else { continue }
+                    components.append("-u \(user):\(password)")
+                }
+            } else {
+                if let credential = delegate.credential, let user = credential.user, let password = credential.password {
+                    components.append("-u \(user):\(password)")
+                }
+            }
+        }
+
+        if session.configuration.httpShouldSetCookies {
+            if
+                let cookieStorage = session.configuration.httpCookieStorage,
+                let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty
+            {
+                let string = cookies.reduce("") { $0 + "\($1.name)=\($1.value);" }
+
+            #if swift(>=3.2)
+                components.append("-b \"\(string[..<string.index(before: string.endIndex)])\"")
+            #else
+                components.append("-b \"\(string.substring(to: string.characters.index(before: string.endIndex)))\"")
+            #endif
+            }
+        }
+
+        var headers: [AnyHashable: Any] = [:]
+
+        session.configuration.httpAdditionalHeaders?.filter {  $0.0 != AnyHashable("Cookie") }
+                                                    .forEach { headers[$0.0] = $0.1 }
+
+        request.allHTTPHeaderFields?.filter { $0.0 != "Cookie" }
+                                    .forEach { headers[$0.0] = $0.1 }
+
+        components += headers.map {
+            let escapedValue = String(describing: $0.value).replacingOccurrences(of: "\"", with: "\\\"")
+
+            return "-H \"\($0.key): \(escapedValue)\""
+        }
+
+        if let httpBodyData = request.httpBody, let httpBody = String(data: httpBodyData, encoding: .utf8) {
+            var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"")
+            escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"")
+
+            components.append("-d \"\(escapedBody)\"")
+        }
+
+        components.append("\"\(url.absoluteString)\"")
+
+        return components.joined(separator: " \\\n\t")
+    }
+}
+
+// MARK: -
+
+/// Specific type of `Request` that manages an underlying `URLSessionDataTask`.
+open class DataRequest: Request {
+
+    // MARK: Helper Types
+
+    struct Requestable: TaskConvertible {
+        let urlRequest: URLRequest
+
+        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
+            do {
+                let urlRequest = try self.urlRequest.adapt(using: adapter)
+                return queue.sync { session.dataTask(with: urlRequest) }
+            } catch {
+                throw AdaptError(error: error)
+            }
+        }
+    }
+
+    // MARK: Properties
+
+    /// The request sent or to be sent to the server.
+    open override var request: URLRequest? {
+        if let request = super.request { return request }
+        if let requestable = originalTask as? Requestable { return requestable.urlRequest }
+
+        return nil
+    }
+
+    /// The progress of fetching the response data from the server for the request.
+    open var progress: Progress { return dataDelegate.progress }
+
+    var dataDelegate: DataTaskDelegate { return delegate as! DataTaskDelegate }
+
+    // MARK: Stream
+
+    /// Sets a closure to be called periodically during the lifecycle of the request as data is read from the server.
+    ///
+    /// This closure returns the bytes most recently received from the server, not including data from previous calls.
+    /// If this closure is set, data will only be available within this closure, and will not be saved elsewhere. It is
+    /// also important to note that the server data in any `Response` object will be `nil`.
+    ///
+    /// - parameter closure: The code to be executed periodically during the lifecycle of the request.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    open func stream(closure: ((Data) -> Void)? = nil) -> Self {
+        dataDelegate.dataStream = closure
+        return self
+    }
+
+    // MARK: Progress
+
+    /// Sets a closure to be called periodically during the lifecycle of the `Request` as data is read from the server.
+    ///
+    /// - parameter queue:   The dispatch queue to execute the closure on.
+    /// - parameter closure: The code to be executed periodically as data is read from the server.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
+        dataDelegate.progressHandler = (closure, queue)
+        return self
+    }
+}
+
+// MARK: -
+
+/// Specific type of `Request` that manages an underlying `URLSessionDownloadTask`.
+open class DownloadRequest: Request {
+
+    // MARK: Helper Types
+
+    /// A collection of options to be executed prior to moving a downloaded file from the temporary URL to the
+    /// destination URL.
+    public struct DownloadOptions: OptionSet {
+        /// Returns the raw bitmask value of the option and satisfies the `RawRepresentable` protocol.
+        public let rawValue: UInt
+
+        /// A `DownloadOptions` flag that creates intermediate directories for the destination URL if specified.
+        public static let createIntermediateDirectories = DownloadOptions(rawValue: 1 << 0)
+
+        /// A `DownloadOptions` flag that removes a previous file from the destination URL if specified.
+        public static let removePreviousFile = DownloadOptions(rawValue: 1 << 1)
+
+        /// Creates a `DownloadFileDestinationOptions` instance with the specified raw value.
+        ///
+        /// - parameter rawValue: The raw bitmask value for the option.
+        ///
+        /// - returns: A new log level instance.
+        public init(rawValue: UInt) {
+            self.rawValue = rawValue
+        }
+    }
+
+    /// A closure executed once a download request has successfully completed in order to determine where to move the
+    /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL
+    /// and the URL response, and returns a two arguments: the file URL where the temporary file should be moved and
+    /// the options defining how the file should be moved.
+    public typealias DownloadFileDestination = (
+        _ temporaryURL: URL,
+        _ response: HTTPURLResponse)
+        -> (destinationURL: URL, options: DownloadOptions)
+
+    enum Downloadable: TaskConvertible {
+        case request(URLRequest)
+        case resumeData(Data)
+
+        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
+            do {
+                let task: URLSessionTask
+
+                switch self {
+                case let .request(urlRequest):
+                    let urlRequest = try urlRequest.adapt(using: adapter)
+                    task = queue.sync { session.downloadTask(with: urlRequest) }
+                case let .resumeData(resumeData):
+                    task = queue.sync { session.downloadTask(withResumeData: resumeData) }
+                }
+
+                return task
+            } catch {
+                throw AdaptError(error: error)
+            }
+        }
+    }
+
+    // MARK: Properties
+
+    /// The request sent or to be sent to the server.
+    open override var request: URLRequest? {
+        if let request = super.request { return request }
+
+        if let downloadable = originalTask as? Downloadable, case let .request(urlRequest) = downloadable {
+            return urlRequest
+        }
+
+        return nil
+    }
+
+    /// The resume data of the underlying download task if available after a failure.
+    open var resumeData: Data? { return downloadDelegate.resumeData }
+
+    /// The progress of downloading the response data from the server for the request.
+    open var progress: Progress { return downloadDelegate.progress }
+
+    var downloadDelegate: DownloadTaskDelegate { return delegate as! DownloadTaskDelegate }
+
+    // MARK: State
+
+    /// Cancels the request.
+    override open func cancel() {
+        cancel(createResumeData: true)
+    }
+
+    /// Cancels the request.
+    ///
+    /// - parameter createResumeData: Determines whether resume data is created via the underlying download task or not.
+    open func cancel(createResumeData: Bool) {
+        if createResumeData {
+            downloadDelegate.downloadTask.cancel { self.downloadDelegate.resumeData = $0 }
+        } else {
+            downloadDelegate.downloadTask.cancel()
+        }
+
+        NotificationCenter.default.post(
+            name: Notification.Name.Task.DidCancel,
+            object: self,
+            userInfo: [Notification.Key.Task: task as Any]
+        )
+    }
+
+    // MARK: Progress
+
+    /// Sets a closure to be called periodically during the lifecycle of the `Request` as data is read from the server.
+    ///
+    /// - parameter queue:   The dispatch queue to execute the closure on.
+    /// - parameter closure: The code to be executed periodically as data is read from the server.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
+        downloadDelegate.progressHandler = (closure, queue)
+        return self
+    }
+
+    // MARK: Destination
+
+    /// Creates a download file destination closure which uses the default file manager to move the temporary file to a
+    /// file URL in the first available directory with the specified search path directory and search path domain mask.
+    ///
+    /// - parameter directory: The search path directory. `.DocumentDirectory` by default.
+    /// - parameter domain:    The search path domain mask. `.UserDomainMask` by default.
+    ///
+    /// - returns: A download file destination closure.
+    open class func suggestedDownloadDestination(
+        for directory: FileManager.SearchPathDirectory = .documentDirectory,
+        in domain: FileManager.SearchPathDomainMask = .userDomainMask)
+        -> DownloadFileDestination
+    {
+        return { temporaryURL, response in
+            let directoryURLs = FileManager.default.urls(for: directory, in: domain)
+
+            if !directoryURLs.isEmpty {
+                return (directoryURLs[0].appendingPathComponent(response.suggestedFilename!), [])
+            }
+
+            return (temporaryURL, [])
+        }
+    }
+}
+
+// MARK: -
+
+/// Specific type of `Request` that manages an underlying `URLSessionUploadTask`.
+open class UploadRequest: DataRequest {
+
+    // MARK: Helper Types
+
+    enum Uploadable: TaskConvertible {
+        case data(Data, URLRequest)
+        case file(URL, URLRequest)
+        case stream(InputStream, URLRequest)
+
+        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
+            do {
+                let task: URLSessionTask
+
+                switch self {
+                case let .data(data, urlRequest):
+                    let urlRequest = try urlRequest.adapt(using: adapter)
+                    task = queue.sync { session.uploadTask(with: urlRequest, from: data) }
+                case let .file(url, urlRequest):
+                    let urlRequest = try urlRequest.adapt(using: adapter)
+                    task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) }
+                case let .stream(_, urlRequest):
+                    let urlRequest = try urlRequest.adapt(using: adapter)
+                    task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) }
+                }
+
+                return task
+            } catch {
+                throw AdaptError(error: error)
+            }
+        }
+    }
+
+    // MARK: Properties
+
+    /// The request sent or to be sent to the server.
+    open override var request: URLRequest? {
+        if let request = super.request { return request }
+
+        guard let uploadable = originalTask as? Uploadable else { return nil }
+
+        switch uploadable {
+        case .data(_, let urlRequest), .file(_, let urlRequest), .stream(_, let urlRequest):
+            return urlRequest
+        }
+    }
+
+    /// The progress of uploading the payload to the server for the upload request.
+    open var uploadProgress: Progress { return uploadDelegate.uploadProgress }
+
+    var uploadDelegate: UploadTaskDelegate { return delegate as! UploadTaskDelegate }
+
+    // MARK: Upload Progress
+
+    /// Sets a closure to be called periodically during the lifecycle of the `UploadRequest` as data is sent to
+    /// the server.
+    ///
+    /// After the data is sent to the server, the `progress(queue:closure:)` APIs can be used to monitor the progress
+    /// of data being read from the server.
+    ///
+    /// - parameter queue:   The dispatch queue to execute the closure on.
+    /// - parameter closure: The code to be executed periodically as data is sent to the server.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    open func uploadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
+        uploadDelegate.uploadProgressHandler = (closure, queue)
+        return self
+    }
+}
+
+// MARK: -
+
+#if !os(watchOS)
+
+/// Specific type of `Request` that manages an underlying `URLSessionStreamTask`.
+@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+open class StreamRequest: Request {
+    enum Streamable: TaskConvertible {
+        case stream(hostName: String, port: Int)
+        case netService(NetService)
+
+        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
+            let task: URLSessionTask
+
+            switch self {
+            case let .stream(hostName, port):
+                task = queue.sync { session.streamTask(withHostName: hostName, port: port) }
+            case let .netService(netService):
+                task = queue.sync { session.streamTask(with: netService) }
+            }
+
+            return task
+        }
+    }
+}
+
+#endif

+ 574 - 0
Example/Pods/Alamofire/Source/Response.swift

@@ -0,0 +1,574 @@
+//
+//  Response.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// Used to store all data associated with an non-serialized response of a data or upload request.
+public struct DefaultDataResponse {
+    /// The URL request sent to the server.
+    public let request: URLRequest?
+
+    /// The server's response to the URL request.
+    public let response: HTTPURLResponse?
+
+    /// The data returned by the server.
+    public let data: Data?
+
+    /// The error encountered while executing or validating the request.
+    public let error: Error?
+
+    /// The timeline of the complete lifecycle of the request.
+    public let timeline: Timeline
+
+    var _metrics: AnyObject?
+
+    /// Creates a `DefaultDataResponse` instance from the specified parameters.
+    ///
+    /// - Parameters:
+    ///   - request:  The URL request sent to the server.
+    ///   - response: The server's response to the URL request.
+    ///   - data:     The data returned by the server.
+    ///   - error:    The error encountered while executing or validating the request.
+    ///   - timeline: The timeline of the complete lifecycle of the request. `Timeline()` by default.
+    ///   - metrics:  The task metrics containing the request / response statistics. `nil` by default.
+    public init(
+        request: URLRequest?,
+        response: HTTPURLResponse?,
+        data: Data?,
+        error: Error?,
+        timeline: Timeline = Timeline(),
+        metrics: AnyObject? = nil)
+    {
+        self.request = request
+        self.response = response
+        self.data = data
+        self.error = error
+        self.timeline = timeline
+    }
+}
+
+// MARK: -
+
+/// Used to store all data associated with a serialized response of a data or upload request.
+public struct DataResponse<Value> {
+    /// The URL request sent to the server.
+    public let request: URLRequest?
+
+    /// The server's response to the URL request.
+    public let response: HTTPURLResponse?
+
+    /// The data returned by the server.
+    public let data: Data?
+
+    /// The result of response serialization.
+    public let result: Result<Value>
+
+    /// The timeline of the complete lifecycle of the request.
+    public let timeline: Timeline
+
+    /// Returns the associated value of the result if it is a success, `nil` otherwise.
+    public var value: Value? { return result.value }
+
+    /// Returns the associated error value if the result if it is a failure, `nil` otherwise.
+    public var error: Error? { return result.error }
+
+    var _metrics: AnyObject?
+
+    /// Creates a `DataResponse` instance with the specified parameters derived from response serialization.
+    ///
+    /// - parameter request:  The URL request sent to the server.
+    /// - parameter response: The server's response to the URL request.
+    /// - parameter data:     The data returned by the server.
+    /// - parameter result:   The result of response serialization.
+    /// - parameter timeline: The timeline of the complete lifecycle of the `Request`. Defaults to `Timeline()`.
+    ///
+    /// - returns: The new `DataResponse` instance.
+    public init(
+        request: URLRequest?,
+        response: HTTPURLResponse?,
+        data: Data?,
+        result: Result<Value>,
+        timeline: Timeline = Timeline())
+    {
+        self.request = request
+        self.response = response
+        self.data = data
+        self.result = result
+        self.timeline = timeline
+    }
+}
+
+// MARK: -
+
+extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible {
+    /// The textual representation used when written to an output stream, which includes whether the result was a
+    /// success or failure.
+    public var description: String {
+        return result.debugDescription
+    }
+
+    /// The debug textual representation used when written to an output stream, which includes the URL request, the URL
+    /// response, the server data, the response serialization result and the timeline.
+    public var debugDescription: String {
+        let requestDescription = request.map { "\($0.httpMethod ?? "GET") \($0)"} ?? "nil"
+        let requestBody = request?.httpBody.map { String(decoding: $0, as: UTF8.self) } ?? "None"
+        let responseDescription = response.map { "\($0)" } ?? "nil"
+        let responseBody = data.map { String(decoding: $0, as: UTF8.self) } ?? "None"
+
+        return """
+        [Request]: \(requestDescription)
+        [Request Body]: \n\(requestBody)
+        [Response]: \(responseDescription)
+        [Response Body]: \n\(responseBody)
+        [Result]: \(result)
+        [Timeline]: \(timeline.debugDescription)
+        """
+    }
+}
+
+// MARK: -
+
+extension DataResponse {
+    /// Evaluates the specified closure when the result of this `DataResponse` is a success, passing the unwrapped
+    /// result value as a parameter.
+    ///
+    /// Use the `map` method with a closure that does not throw. For example:
+    ///
+    ///     let possibleData: DataResponse<Data> = ...
+    ///     let possibleInt = possibleData.map { $0.count }
+    ///
+    /// - parameter transform: A closure that takes the success value of the instance's result.
+    ///
+    /// - returns: A `DataResponse` whose result wraps the value returned by the given closure. If this instance's
+    ///            result is a failure, returns a response wrapping the same failure.
+    public func map<T>(_ transform: (Value) -> T) -> DataResponse<T> {
+        var response = DataResponse<T>(
+            request: request,
+            response: self.response,
+            data: data,
+            result: result.map(transform),
+            timeline: timeline
+        )
+
+        response._metrics = _metrics
+
+        return response
+    }
+
+    /// Evaluates the given closure when the result of this `DataResponse` is a success, passing the unwrapped result
+    /// value as a parameter.
+    ///
+    /// Use the `flatMap` method with a closure that may throw an error. For example:
+    ///
+    ///     let possibleData: DataResponse<Data> = ...
+    ///     let possibleObject = possibleData.flatMap {
+    ///         try JSONSerialization.jsonObject(with: $0)
+    ///     }
+    ///
+    /// - parameter transform: A closure that takes the success value of the instance's result.
+    ///
+    /// - returns: A success or failure `DataResponse` depending on the result of the given closure. If this instance's
+    ///            result is a failure, returns the same failure.
+    public func flatMap<T>(_ transform: (Value) throws -> T) -> DataResponse<T> {
+        var response = DataResponse<T>(
+            request: request,
+            response: self.response,
+            data: data,
+            result: result.flatMap(transform),
+            timeline: timeline
+        )
+
+        response._metrics = _metrics
+
+        return response
+    }
+
+    /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `mapError` function with a closure that does not throw. For example:
+    ///
+    ///     let possibleData: DataResponse<Data> = ...
+    ///     let withMyError = possibleData.mapError { MyError.error($0) }
+    ///
+    /// - Parameter transform: A closure that takes the error of the instance.
+    /// - Returns: A `DataResponse` instance containing the result of the transform.
+    public func mapError<E: Error>(_ transform: (Error) -> E) -> DataResponse {
+        var response = DataResponse(
+            request: request,
+            response: self.response,
+            data: data,
+            result: result.mapError(transform),
+            timeline: timeline
+        )
+
+        response._metrics = _metrics
+
+        return response
+    }
+
+    /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `flatMapError` function with a closure that may throw an error. For example:
+    ///
+    ///     let possibleData: DataResponse<Data> = ...
+    ///     let possibleObject = possibleData.flatMapError {
+    ///         try someFailableFunction(taking: $0)
+    ///     }
+    ///
+    /// - Parameter transform: A throwing closure that takes the error of the instance.
+    ///
+    /// - Returns: A `DataResponse` instance containing the result of the transform.
+    public func flatMapError<E: Error>(_ transform: (Error) throws -> E) -> DataResponse {
+        var response = DataResponse(
+            request: request,
+            response: self.response,
+            data: data,
+            result: result.flatMapError(transform),
+            timeline: timeline
+        )
+
+        response._metrics = _metrics
+
+        return response
+    }
+}
+
+// MARK: -
+
+/// Used to store all data associated with an non-serialized response of a download request.
+public struct DefaultDownloadResponse {
+    /// The URL request sent to the server.
+    public let request: URLRequest?
+
+    /// The server's response to the URL request.
+    public let response: HTTPURLResponse?
+
+    /// The temporary destination URL of the data returned from the server.
+    public let temporaryURL: URL?
+
+    /// The final destination URL of the data returned from the server if it was moved.
+    public let destinationURL: URL?
+
+    /// The resume data generated if the request was cancelled.
+    public let resumeData: Data?
+
+    /// The error encountered while executing or validating the request.
+    public let error: Error?
+
+    /// The timeline of the complete lifecycle of the request.
+    public let timeline: Timeline
+
+    var _metrics: AnyObject?
+
+    /// Creates a `DefaultDownloadResponse` instance from the specified parameters.
+    ///
+    /// - Parameters:
+    ///   - request:        The URL request sent to the server.
+    ///   - response:       The server's response to the URL request.
+    ///   - temporaryURL:   The temporary destination URL of the data returned from the server.
+    ///   - destinationURL: The final destination URL of the data returned from the server if it was moved.
+    ///   - resumeData:     The resume data generated if the request was cancelled.
+    ///   - error:          The error encountered while executing or validating the request.
+    ///   - timeline:       The timeline of the complete lifecycle of the request. `Timeline()` by default.
+    ///   - metrics:        The task metrics containing the request / response statistics. `nil` by default.
+    public init(
+        request: URLRequest?,
+        response: HTTPURLResponse?,
+        temporaryURL: URL?,
+        destinationURL: URL?,
+        resumeData: Data?,
+        error: Error?,
+        timeline: Timeline = Timeline(),
+        metrics: AnyObject? = nil)
+    {
+        self.request = request
+        self.response = response
+        self.temporaryURL = temporaryURL
+        self.destinationURL = destinationURL
+        self.resumeData = resumeData
+        self.error = error
+        self.timeline = timeline
+    }
+}
+
+// MARK: -
+
+/// Used to store all data associated with a serialized response of a download request.
+public struct DownloadResponse<Value> {
+    /// The URL request sent to the server.
+    public let request: URLRequest?
+
+    /// The server's response to the URL request.
+    public let response: HTTPURLResponse?
+
+    /// The temporary destination URL of the data returned from the server.
+    public let temporaryURL: URL?
+
+    /// The final destination URL of the data returned from the server if it was moved.
+    public let destinationURL: URL?
+
+    /// The resume data generated if the request was cancelled.
+    public let resumeData: Data?
+
+    /// The result of response serialization.
+    public let result: Result<Value>
+
+    /// The timeline of the complete lifecycle of the request.
+    public let timeline: Timeline
+
+    /// Returns the associated value of the result if it is a success, `nil` otherwise.
+    public var value: Value? { return result.value }
+
+    /// Returns the associated error value if the result if it is a failure, `nil` otherwise.
+    public var error: Error? { return result.error }
+
+    var _metrics: AnyObject?
+
+    /// Creates a `DownloadResponse` instance with the specified parameters derived from response serialization.
+    ///
+    /// - parameter request:        The URL request sent to the server.
+    /// - parameter response:       The server's response to the URL request.
+    /// - parameter temporaryURL:   The temporary destination URL of the data returned from the server.
+    /// - parameter destinationURL: The final destination URL of the data returned from the server if it was moved.
+    /// - parameter resumeData:     The resume data generated if the request was cancelled.
+    /// - parameter result:         The result of response serialization.
+    /// - parameter timeline:       The timeline of the complete lifecycle of the `Request`. Defaults to `Timeline()`.
+    ///
+    /// - returns: The new `DownloadResponse` instance.
+    public init(
+        request: URLRequest?,
+        response: HTTPURLResponse?,
+        temporaryURL: URL?,
+        destinationURL: URL?,
+        resumeData: Data?,
+        result: Result<Value>,
+        timeline: Timeline = Timeline())
+    {
+        self.request = request
+        self.response = response
+        self.temporaryURL = temporaryURL
+        self.destinationURL = destinationURL
+        self.resumeData = resumeData
+        self.result = result
+        self.timeline = timeline
+    }
+}
+
+// MARK: -
+
+extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertible {
+    /// The textual representation used when written to an output stream, which includes whether the result was a
+    /// success or failure.
+    public var description: String {
+        return result.debugDescription
+    }
+
+    /// The debug textual representation used when written to an output stream, which includes the URL request, the URL
+    /// response, the temporary and destination URLs, the resume data, the response serialization result and the
+    /// timeline.
+    public var debugDescription: String {
+        let requestDescription = request.map { "\($0.httpMethod ?? "GET") \($0)"} ?? "nil"
+        let requestBody = request?.httpBody.map { String(decoding: $0, as: UTF8.self) } ?? "None"
+        let responseDescription = response.map { "\($0)" } ?? "nil"
+
+        return """
+        [Request]: \(requestDescription)
+        [Request Body]: \n\(requestBody)
+        [Response]: \(responseDescription)
+        [TemporaryURL]: \(temporaryURL?.path ?? "nil")
+        [DestinationURL]: \(destinationURL?.path ?? "nil")
+        [ResumeData]: \(resumeData?.count ?? 0) bytes
+        [Result]: \(result)
+        [Timeline]: \(timeline.debugDescription)
+        """
+    }
+}
+
+// MARK: -
+
+extension DownloadResponse {
+    /// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped
+    /// result value as a parameter.
+    ///
+    /// Use the `map` method with a closure that does not throw. For example:
+    ///
+    ///     let possibleData: DownloadResponse<Data> = ...
+    ///     let possibleInt = possibleData.map { $0.count }
+    ///
+    /// - parameter transform: A closure that takes the success value of the instance's result.
+    ///
+    /// - returns: A `DownloadResponse` whose result wraps the value returned by the given closure. If this instance's
+    ///            result is a failure, returns a response wrapping the same failure.
+    public func map<T>(_ transform: (Value) -> T) -> DownloadResponse<T> {
+        var response = DownloadResponse<T>(
+            request: request,
+            response: self.response,
+            temporaryURL: temporaryURL,
+            destinationURL: destinationURL,
+            resumeData: resumeData,
+            result: result.map(transform),
+            timeline: timeline
+        )
+
+        response._metrics = _metrics
+
+        return response
+    }
+
+    /// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped
+    /// result value as a parameter.
+    ///
+    /// Use the `flatMap` method with a closure that may throw an error. For example:
+    ///
+    ///     let possibleData: DownloadResponse<Data> = ...
+    ///     let possibleObject = possibleData.flatMap {
+    ///         try JSONSerialization.jsonObject(with: $0)
+    ///     }
+    ///
+    /// - parameter transform: A closure that takes the success value of the instance's result.
+    ///
+    /// - returns: A success or failure `DownloadResponse` depending on the result of the given closure. If this
+    /// instance's result is a failure, returns the same failure.
+    public func flatMap<T>(_ transform: (Value) throws -> T) -> DownloadResponse<T> {
+        var response = DownloadResponse<T>(
+            request: request,
+            response: self.response,
+            temporaryURL: temporaryURL,
+            destinationURL: destinationURL,
+            resumeData: resumeData,
+            result: result.flatMap(transform),
+            timeline: timeline
+        )
+
+        response._metrics = _metrics
+
+        return response
+    }
+
+    /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `mapError` function with a closure that does not throw. For example:
+    ///
+    ///     let possibleData: DownloadResponse<Data> = ...
+    ///     let withMyError = possibleData.mapError { MyError.error($0) }
+    ///
+    /// - Parameter transform: A closure that takes the error of the instance.
+    /// - Returns: A `DownloadResponse` instance containing the result of the transform.
+    public func mapError<E: Error>(_ transform: (Error) -> E) -> DownloadResponse {
+        var response = DownloadResponse(
+            request: request,
+            response: self.response,
+            temporaryURL: temporaryURL,
+            destinationURL: destinationURL,
+            resumeData: resumeData,
+            result: result.mapError(transform),
+            timeline: timeline
+        )
+
+        response._metrics = _metrics
+
+        return response
+    }
+
+    /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `flatMapError` function with a closure that may throw an error. For example:
+    ///
+    ///     let possibleData: DownloadResponse<Data> = ...
+    ///     let possibleObject = possibleData.flatMapError {
+    ///         try someFailableFunction(taking: $0)
+    ///     }
+    ///
+    /// - Parameter transform: A throwing closure that takes the error of the instance.
+    ///
+    /// - Returns: A `DownloadResponse` instance containing the result of the transform.
+    public func flatMapError<E: Error>(_ transform: (Error) throws -> E) -> DownloadResponse {
+        var response = DownloadResponse(
+            request: request,
+            response: self.response,
+            temporaryURL: temporaryURL,
+            destinationURL: destinationURL,
+            resumeData: resumeData,
+            result: result.flatMapError(transform),
+            timeline: timeline
+        )
+
+        response._metrics = _metrics
+
+        return response
+    }
+}
+
+// MARK: -
+
+protocol Response {
+    /// The task metrics containing the request / response statistics.
+    var _metrics: AnyObject? { get set }
+    mutating func add(_ metrics: AnyObject?)
+}
+
+extension Response {
+    mutating func add(_ metrics: AnyObject?) {
+        #if !os(watchOS)
+            guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) else { return }
+            guard let metrics = metrics as? URLSessionTaskMetrics else { return }
+
+            _metrics = metrics
+        #endif
+    }
+}
+
+// MARK: -
+
+@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
+extension DefaultDataResponse: Response {
+#if !os(watchOS)
+    /// The task metrics containing the request / response statistics.
+    public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
+#endif
+}
+
+@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
+extension DataResponse: Response {
+#if !os(watchOS)
+    /// The task metrics containing the request / response statistics.
+    public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
+#endif
+}
+
+@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
+extension DefaultDownloadResponse: Response {
+#if !os(watchOS)
+    /// The task metrics containing the request / response statistics.
+    public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
+#endif
+}
+
+@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
+extension DownloadResponse: Response {
+#if !os(watchOS)
+    /// The task metrics containing the request / response statistics.
+    public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
+#endif
+}

+ 715 - 0
Example/Pods/Alamofire/Source/ResponseSerialization.swift

@@ -0,0 +1,715 @@
+//
+//  ResponseSerialization.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// The type in which all data response serializers must conform to in order to serialize a response.
+public protocol DataResponseSerializerProtocol {
+    /// The type of serialized object to be created by this `DataResponseSerializerType`.
+    associatedtype SerializedObject
+
+    /// A closure used by response handlers that takes a request, response, data and error and returns a result.
+    var serializeResponse: (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result<SerializedObject> { get }
+}
+
+// MARK: -
+
+/// A generic `DataResponseSerializerType` used to serialize a request, response, and data into a serialized object.
+public struct DataResponseSerializer<Value>: DataResponseSerializerProtocol {
+    /// The type of serialized object to be created by this `DataResponseSerializer`.
+    public typealias SerializedObject = Value
+
+    /// A closure used by response handlers that takes a request, response, data and error and returns a result.
+    public var serializeResponse: (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result<Value>
+
+    /// Initializes the `ResponseSerializer` instance with the given serialize response closure.
+    ///
+    /// - parameter serializeResponse: The closure used to serialize the response.
+    ///
+    /// - returns: The new generic response serializer instance.
+    public init(serializeResponse: @escaping (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result<Value>) {
+        self.serializeResponse = serializeResponse
+    }
+}
+
+// MARK: -
+
+/// The type in which all download response serializers must conform to in order to serialize a response.
+public protocol DownloadResponseSerializerProtocol {
+    /// The type of serialized object to be created by this `DownloadResponseSerializerType`.
+    associatedtype SerializedObject
+
+    /// A closure used by response handlers that takes a request, response, url and error and returns a result.
+    var serializeResponse: (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result<SerializedObject> { get }
+}
+
+// MARK: -
+
+/// A generic `DownloadResponseSerializerType` used to serialize a request, response, and data into a serialized object.
+public struct DownloadResponseSerializer<Value>: DownloadResponseSerializerProtocol {
+    /// The type of serialized object to be created by this `DownloadResponseSerializer`.
+    public typealias SerializedObject = Value
+
+    /// A closure used by response handlers that takes a request, response, url and error and returns a result.
+    public var serializeResponse: (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result<Value>
+
+    /// Initializes the `ResponseSerializer` instance with the given serialize response closure.
+    ///
+    /// - parameter serializeResponse: The closure used to serialize the response.
+    ///
+    /// - returns: The new generic response serializer instance.
+    public init(serializeResponse: @escaping (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result<Value>) {
+        self.serializeResponse = serializeResponse
+    }
+}
+
+// MARK: - Timeline
+
+extension Request {
+    var timeline: Timeline {
+        let requestStartTime = self.startTime ?? CFAbsoluteTimeGetCurrent()
+        let requestCompletedTime = self.endTime ?? CFAbsoluteTimeGetCurrent()
+        let initialResponseTime = self.delegate.initialResponseTime ?? requestCompletedTime
+
+        return Timeline(
+            requestStartTime: requestStartTime,
+            initialResponseTime: initialResponseTime,
+            requestCompletedTime: requestCompletedTime,
+            serializationCompletedTime: CFAbsoluteTimeGetCurrent()
+        )
+    }
+}
+
+// MARK: - Default
+
+extension DataRequest {
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter queue:             The queue on which the completion handler is dispatched.
+    /// - parameter completionHandler: The code to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func response(queue: DispatchQueue? = nil, completionHandler: @escaping (DefaultDataResponse) -> Void) -> Self {
+        delegate.queue.addOperation {
+            (queue ?? DispatchQueue.main).async {
+                var dataResponse = DefaultDataResponse(
+                    request: self.request,
+                    response: self.response,
+                    data: self.delegate.data,
+                    error: self.delegate.error,
+                    timeline: self.timeline
+                )
+
+                dataResponse.add(self.delegate.metrics)
+
+                completionHandler(dataResponse)
+            }
+        }
+
+        return self
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter queue:              The queue on which the completion handler is dispatched.
+    /// - parameter responseSerializer: The response serializer responsible for serializing the request, response,
+    ///                                 and data.
+    /// - parameter completionHandler:  The code to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func response<T: DataResponseSerializerProtocol>(
+        queue: DispatchQueue? = nil,
+        responseSerializer: T,
+        completionHandler: @escaping (DataResponse<T.SerializedObject>) -> Void)
+        -> Self
+    {
+        delegate.queue.addOperation {
+            let result = responseSerializer.serializeResponse(
+                self.request,
+                self.response,
+                self.delegate.data,
+                self.delegate.error
+            )
+
+            var dataResponse = DataResponse<T.SerializedObject>(
+                request: self.request,
+                response: self.response,
+                data: self.delegate.data,
+                result: result,
+                timeline: self.timeline
+            )
+
+            dataResponse.add(self.delegate.metrics)
+
+            (queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }
+        }
+
+        return self
+    }
+}
+
+extension DownloadRequest {
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter queue:             The queue on which the completion handler is dispatched.
+    /// - parameter completionHandler: The code to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func response(
+        queue: DispatchQueue? = nil,
+        completionHandler: @escaping (DefaultDownloadResponse) -> Void)
+        -> Self
+    {
+        delegate.queue.addOperation {
+            (queue ?? DispatchQueue.main).async {
+                var downloadResponse = DefaultDownloadResponse(
+                    request: self.request,
+                    response: self.response,
+                    temporaryURL: self.downloadDelegate.temporaryURL,
+                    destinationURL: self.downloadDelegate.destinationURL,
+                    resumeData: self.downloadDelegate.resumeData,
+                    error: self.downloadDelegate.error,
+                    timeline: self.timeline
+                )
+
+                downloadResponse.add(self.delegate.metrics)
+
+                completionHandler(downloadResponse)
+            }
+        }
+
+        return self
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter queue:              The queue on which the completion handler is dispatched.
+    /// - parameter responseSerializer: The response serializer responsible for serializing the request, response,
+    ///                                 and data contained in the destination url.
+    /// - parameter completionHandler:  The code to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func response<T: DownloadResponseSerializerProtocol>(
+        queue: DispatchQueue? = nil,
+        responseSerializer: T,
+        completionHandler: @escaping (DownloadResponse<T.SerializedObject>) -> Void)
+        -> Self
+    {
+        delegate.queue.addOperation {
+            let result = responseSerializer.serializeResponse(
+                self.request,
+                self.response,
+                self.downloadDelegate.fileURL,
+                self.downloadDelegate.error
+            )
+
+            var downloadResponse = DownloadResponse<T.SerializedObject>(
+                request: self.request,
+                response: self.response,
+                temporaryURL: self.downloadDelegate.temporaryURL,
+                destinationURL: self.downloadDelegate.destinationURL,
+                resumeData: self.downloadDelegate.resumeData,
+                result: result,
+                timeline: self.timeline
+            )
+
+            downloadResponse.add(self.delegate.metrics)
+
+            (queue ?? DispatchQueue.main).async { completionHandler(downloadResponse) }
+        }
+
+        return self
+    }
+}
+
+// MARK: - Data
+
+extension Request {
+    /// Returns a result data type that contains the response data as-is.
+    ///
+    /// - parameter response: The response from the server.
+    /// - parameter data:     The data returned from the server.
+    /// - parameter error:    The error already encountered if it exists.
+    ///
+    /// - returns: The result data type.
+    public static func serializeResponseData(response: HTTPURLResponse?, data: Data?, error: Error?) -> Result<Data> {
+        guard error == nil else { return .failure(error!) }
+
+        if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(Data()) }
+
+        guard let validData = data else {
+            return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
+        }
+
+        return .success(validData)
+    }
+}
+
+extension DataRequest {
+    /// Creates a response serializer that returns the associated data as-is.
+    ///
+    /// - returns: A data response serializer.
+    public static func dataResponseSerializer() -> DataResponseSerializer<Data> {
+        return DataResponseSerializer { _, response, data, error in
+            return Request.serializeResponseData(response: response, data: data, error: error)
+        }
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter completionHandler: The code to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func responseData(
+        queue: DispatchQueue? = nil,
+        completionHandler: @escaping (DataResponse<Data>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: DataRequest.dataResponseSerializer(),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+extension DownloadRequest {
+    /// Creates a response serializer that returns the associated data as-is.
+    ///
+    /// - returns: A data response serializer.
+    public static func dataResponseSerializer() -> DownloadResponseSerializer<Data> {
+        return DownloadResponseSerializer { _, response, fileURL, error in
+            guard error == nil else { return .failure(error!) }
+
+            guard let fileURL = fileURL else {
+                return .failure(AFError.responseSerializationFailed(reason: .inputFileNil))
+            }
+
+            do {
+                let data = try Data(contentsOf: fileURL)
+                return Request.serializeResponseData(response: response, data: data, error: error)
+            } catch {
+                return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)))
+            }
+        }
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter completionHandler: The code to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func responseData(
+        queue: DispatchQueue? = nil,
+        completionHandler: @escaping (DownloadResponse<Data>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: DownloadRequest.dataResponseSerializer(),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+// MARK: - String
+
+extension Request {
+    /// Returns a result string type initialized from the response data with the specified string encoding.
+    ///
+    /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server
+    ///                       response, falling back to the default HTTP default character set, ISO-8859-1.
+    /// - parameter response: The response from the server.
+    /// - parameter data:     The data returned from the server.
+    /// - parameter error:    The error already encountered if it exists.
+    ///
+    /// - returns: The result data type.
+    public static func serializeResponseString(
+        encoding: String.Encoding?,
+        response: HTTPURLResponse?,
+        data: Data?,
+        error: Error?)
+        -> Result<String>
+    {
+        guard error == nil else { return .failure(error!) }
+
+        if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success("") }
+
+        guard let validData = data else {
+            return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
+        }
+
+        var convertedEncoding = encoding
+
+        if let encodingName = response?.textEncodingName as CFString?, convertedEncoding == nil {
+            convertedEncoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(
+                CFStringConvertIANACharSetNameToEncoding(encodingName))
+            )
+        }
+
+        let actualEncoding = convertedEncoding ?? .isoLatin1
+
+        if let string = String(data: validData, encoding: actualEncoding) {
+            return .success(string)
+        } else {
+            return .failure(AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding)))
+        }
+    }
+}
+
+extension DataRequest {
+    /// Creates a response serializer that returns a result string type initialized from the response data with
+    /// the specified string encoding.
+    ///
+    /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server
+    ///                       response, falling back to the default HTTP default character set, ISO-8859-1.
+    ///
+    /// - returns: A string response serializer.
+    public static func stringResponseSerializer(encoding: String.Encoding? = nil) -> DataResponseSerializer<String> {
+        return DataResponseSerializer { _, response, data, error in
+            return Request.serializeResponseString(encoding: encoding, response: response, data: data, error: error)
+        }
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter encoding:          The string encoding. If `nil`, the string encoding will be determined from the
+    ///                                server response, falling back to the default HTTP default character set,
+    ///                                ISO-8859-1.
+    /// - parameter completionHandler: A closure to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func responseString(
+        queue: DispatchQueue? = nil,
+        encoding: String.Encoding? = nil,
+        completionHandler: @escaping (DataResponse<String>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: DataRequest.stringResponseSerializer(encoding: encoding),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+extension DownloadRequest {
+    /// Creates a response serializer that returns a result string type initialized from the response data with
+    /// the specified string encoding.
+    ///
+    /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server
+    ///                       response, falling back to the default HTTP default character set, ISO-8859-1.
+    ///
+    /// - returns: A string response serializer.
+    public static func stringResponseSerializer(encoding: String.Encoding? = nil) -> DownloadResponseSerializer<String> {
+        return DownloadResponseSerializer { _, response, fileURL, error in
+            guard error == nil else { return .failure(error!) }
+
+            guard let fileURL = fileURL else {
+                return .failure(AFError.responseSerializationFailed(reason: .inputFileNil))
+            }
+
+            do {
+                let data = try Data(contentsOf: fileURL)
+                return Request.serializeResponseString(encoding: encoding, response: response, data: data, error: error)
+            } catch {
+                return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)))
+            }
+        }
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter encoding:          The string encoding. If `nil`, the string encoding will be determined from the
+    ///                                server response, falling back to the default HTTP default character set,
+    ///                                ISO-8859-1.
+    /// - parameter completionHandler: A closure to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func responseString(
+        queue: DispatchQueue? = nil,
+        encoding: String.Encoding? = nil,
+        completionHandler: @escaping (DownloadResponse<String>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: DownloadRequest.stringResponseSerializer(encoding: encoding),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+// MARK: - JSON
+
+extension Request {
+    /// Returns a JSON object contained in a result type constructed from the response data using `JSONSerialization`
+    /// with the specified reading options.
+    ///
+    /// - parameter options:  The JSON serialization reading options. Defaults to `.allowFragments`.
+    /// - parameter response: The response from the server.
+    /// - parameter data:     The data returned from the server.
+    /// - parameter error:    The error already encountered if it exists.
+    ///
+    /// - returns: The result data type.
+    public static func serializeResponseJSON(
+        options: JSONSerialization.ReadingOptions,
+        response: HTTPURLResponse?,
+        data: Data?,
+        error: Error?)
+        -> Result<Any>
+    {
+        guard error == nil else { return .failure(error!) }
+
+        if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()) }
+
+        guard let validData = data, validData.count > 0 else {
+            return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength))
+        }
+
+        do {
+            let json = try JSONSerialization.jsonObject(with: validData, options: options)
+            return .success(json)
+        } catch {
+            return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
+        }
+    }
+}
+
+extension DataRequest {
+    /// Creates a response serializer that returns a JSON object result type constructed from the response data using
+    /// `JSONSerialization` with the specified reading options.
+    ///
+    /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`.
+    ///
+    /// - returns: A JSON object response serializer.
+    public static func jsonResponseSerializer(
+        options: JSONSerialization.ReadingOptions = .allowFragments)
+        -> DataResponseSerializer<Any>
+    {
+        return DataResponseSerializer { _, response, data, error in
+            return Request.serializeResponseJSON(options: options, response: response, data: data, error: error)
+        }
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter options:           The JSON serialization reading options. Defaults to `.allowFragments`.
+    /// - parameter completionHandler: A closure to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func responseJSON(
+        queue: DispatchQueue? = nil,
+        options: JSONSerialization.ReadingOptions = .allowFragments,
+        completionHandler: @escaping (DataResponse<Any>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: DataRequest.jsonResponseSerializer(options: options),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+extension DownloadRequest {
+    /// Creates a response serializer that returns a JSON object result type constructed from the response data using
+    /// `JSONSerialization` with the specified reading options.
+    ///
+    /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`.
+    ///
+    /// - returns: A JSON object response serializer.
+    public static func jsonResponseSerializer(
+        options: JSONSerialization.ReadingOptions = .allowFragments)
+        -> DownloadResponseSerializer<Any>
+    {
+        return DownloadResponseSerializer { _, response, fileURL, error in
+            guard error == nil else { return .failure(error!) }
+
+            guard let fileURL = fileURL else {
+                return .failure(AFError.responseSerializationFailed(reason: .inputFileNil))
+            }
+
+            do {
+                let data = try Data(contentsOf: fileURL)
+                return Request.serializeResponseJSON(options: options, response: response, data: data, error: error)
+            } catch {
+                return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)))
+            }
+        }
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter options:           The JSON serialization reading options. Defaults to `.allowFragments`.
+    /// - parameter completionHandler: A closure to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func responseJSON(
+        queue: DispatchQueue? = nil,
+        options: JSONSerialization.ReadingOptions = .allowFragments,
+        completionHandler: @escaping (DownloadResponse<Any>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: DownloadRequest.jsonResponseSerializer(options: options),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+// MARK: - Property List
+
+extension Request {
+    /// Returns a plist object contained in a result type constructed from the response data using
+    /// `PropertyListSerialization` with the specified reading options.
+    ///
+    /// - parameter options:  The property list reading options. Defaults to `[]`.
+    /// - parameter response: The response from the server.
+    /// - parameter data:     The data returned from the server.
+    /// - parameter error:    The error already encountered if it exists.
+    ///
+    /// - returns: The result data type.
+    public static func serializeResponsePropertyList(
+        options: PropertyListSerialization.ReadOptions,
+        response: HTTPURLResponse?,
+        data: Data?,
+        error: Error?)
+        -> Result<Any>
+    {
+        guard error == nil else { return .failure(error!) }
+
+        if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()) }
+
+        guard let validData = data, validData.count > 0 else {
+            return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength))
+        }
+
+        do {
+            let plist = try PropertyListSerialization.propertyList(from: validData, options: options, format: nil)
+            return .success(plist)
+        } catch {
+            return .failure(AFError.responseSerializationFailed(reason: .propertyListSerializationFailed(error: error)))
+        }
+    }
+}
+
+extension DataRequest {
+    /// Creates a response serializer that returns an object constructed from the response data using
+    /// `PropertyListSerialization` with the specified reading options.
+    ///
+    /// - parameter options: The property list reading options. Defaults to `[]`.
+    ///
+    /// - returns: A property list object response serializer.
+    public static func propertyListResponseSerializer(
+        options: PropertyListSerialization.ReadOptions = [])
+        -> DataResponseSerializer<Any>
+    {
+        return DataResponseSerializer { _, response, data, error in
+            return Request.serializeResponsePropertyList(options: options, response: response, data: data, error: error)
+        }
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter options:           The property list reading options. Defaults to `[]`.
+    /// - parameter completionHandler: A closure to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func responsePropertyList(
+        queue: DispatchQueue? = nil,
+        options: PropertyListSerialization.ReadOptions = [],
+        completionHandler: @escaping (DataResponse<Any>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: DataRequest.propertyListResponseSerializer(options: options),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+extension DownloadRequest {
+    /// Creates a response serializer that returns an object constructed from the response data using
+    /// `PropertyListSerialization` with the specified reading options.
+    ///
+    /// - parameter options: The property list reading options. Defaults to `[]`.
+    ///
+    /// - returns: A property list object response serializer.
+    public static func propertyListResponseSerializer(
+        options: PropertyListSerialization.ReadOptions = [])
+        -> DownloadResponseSerializer<Any>
+    {
+        return DownloadResponseSerializer { _, response, fileURL, error in
+            guard error == nil else { return .failure(error!) }
+
+            guard let fileURL = fileURL else {
+                return .failure(AFError.responseSerializationFailed(reason: .inputFileNil))
+            }
+
+            do {
+                let data = try Data(contentsOf: fileURL)
+                return Request.serializeResponsePropertyList(options: options, response: response, data: data, error: error)
+            } catch {
+                return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)))
+            }
+        }
+    }
+
+    /// Adds a handler to be called once the request has finished.
+    ///
+    /// - parameter options:           The property list reading options. Defaults to `[]`.
+    /// - parameter completionHandler: A closure to be executed once the request has finished.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func responsePropertyList(
+        queue: DispatchQueue? = nil,
+        options: PropertyListSerialization.ReadOptions = [],
+        completionHandler: @escaping (DownloadResponse<Any>) -> Void)
+        -> Self
+    {
+        return response(
+            queue: queue,
+            responseSerializer: DownloadRequest.propertyListResponseSerializer(options: options),
+            completionHandler: completionHandler
+        )
+    }
+}
+
+/// A set of HTTP response status code that do not contain response data.
+private let emptyDataStatusCodes: Set<Int> = [204, 205]

+ 300 - 0
Example/Pods/Alamofire/Source/Result.swift

@@ -0,0 +1,300 @@
+//
+//  Result.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// Used to represent whether a request was successful or encountered an error.
+///
+/// - success: The request and all post processing operations were successful resulting in the serialization of the
+///            provided associated value.
+///
+/// - failure: The request encountered an error resulting in a failure. The associated values are the original data
+///            provided by the server as well as the error that caused the failure.
+public enum Result<Value> {
+    case success(Value)
+    case failure(Error)
+
+    /// Returns `true` if the result is a success, `false` otherwise.
+    public var isSuccess: Bool {
+        switch self {
+        case .success:
+            return true
+        case .failure:
+            return false
+        }
+    }
+
+    /// Returns `true` if the result is a failure, `false` otherwise.
+    public var isFailure: Bool {
+        return !isSuccess
+    }
+
+    /// Returns the associated value if the result is a success, `nil` otherwise.
+    public var value: Value? {
+        switch self {
+        case .success(let value):
+            return value
+        case .failure:
+            return nil
+        }
+    }
+
+    /// Returns the associated error value if the result is a failure, `nil` otherwise.
+    public var error: Error? {
+        switch self {
+        case .success:
+            return nil
+        case .failure(let error):
+            return error
+        }
+    }
+}
+
+// MARK: - CustomStringConvertible
+
+extension Result: CustomStringConvertible {
+    /// The textual representation used when written to an output stream, which includes whether the result was a
+    /// success or failure.
+    public var description: String {
+        switch self {
+        case .success:
+            return "SUCCESS"
+        case .failure:
+            return "FAILURE"
+        }
+    }
+}
+
+// MARK: - CustomDebugStringConvertible
+
+extension Result: CustomDebugStringConvertible {
+    /// The debug textual representation used when written to an output stream, which includes whether the result was a
+    /// success or failure in addition to the value or error.
+    public var debugDescription: String {
+        switch self {
+        case .success(let value):
+            return "SUCCESS: \(value)"
+        case .failure(let error):
+            return "FAILURE: \(error)"
+        }
+    }
+}
+
+// MARK: - Functional APIs
+
+extension Result {
+    /// Creates a `Result` instance from the result of a closure.
+    ///
+    /// A failure result is created when the closure throws, and a success result is created when the closure
+    /// succeeds without throwing an error.
+    ///
+    ///     func someString() throws -> String { ... }
+    ///
+    ///     let result = Result(value: {
+    ///         return try someString()
+    ///     })
+    ///
+    ///     // The type of result is Result<String>
+    ///
+    /// The trailing closure syntax is also supported:
+    ///
+    ///     let result = Result { try someString() }
+    ///
+    /// - parameter value: The closure to execute and create the result for.
+    public init(value: () throws -> Value) {
+        do {
+            self = try .success(value())
+        } catch {
+            self = .failure(error)
+        }
+    }
+
+    /// Returns the success value, or throws the failure error.
+    ///
+    ///     let possibleString: Result<String> = .success("success")
+    ///     try print(possibleString.unwrap())
+    ///     // Prints "success"
+    ///
+    ///     let noString: Result<String> = .failure(error)
+    ///     try print(noString.unwrap())
+    ///     // Throws error
+    public func unwrap() throws -> Value {
+        switch self {
+        case .success(let value):
+            return value
+        case .failure(let error):
+            throw error
+        }
+    }
+
+    /// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a parameter.
+    ///
+    /// Use the `map` method with a closure that does not throw. For example:
+    ///
+    ///     let possibleData: Result<Data> = .success(Data())
+    ///     let possibleInt = possibleData.map { $0.count }
+    ///     try print(possibleInt.unwrap())
+    ///     // Prints "0"
+    ///
+    ///     let noData: Result<Data> = .failure(error)
+    ///     let noInt = noData.map { $0.count }
+    ///     try print(noInt.unwrap())
+    ///     // Throws error
+    ///
+    /// - parameter transform: A closure that takes the success value of the `Result` instance.
+    ///
+    /// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the
+    ///            same failure.
+    public func map<T>(_ transform: (Value) -> T) -> Result<T> {
+        switch self {
+        case .success(let value):
+            return .success(transform(value))
+        case .failure(let error):
+            return .failure(error)
+        }
+    }
+
+    /// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a parameter.
+    ///
+    /// Use the `flatMap` method with a closure that may throw an error. For example:
+    ///
+    ///     let possibleData: Result<Data> = .success(Data(...))
+    ///     let possibleObject = possibleData.flatMap {
+    ///         try JSONSerialization.jsonObject(with: $0)
+    ///     }
+    ///
+    /// - parameter transform: A closure that takes the success value of the instance.
+    ///
+    /// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the
+    ///            same failure.
+    public func flatMap<T>(_ transform: (Value) throws -> T) -> Result<T> {
+        switch self {
+        case .success(let value):
+            do {
+                return try .success(transform(value))
+            } catch {
+                return .failure(error)
+            }
+        case .failure(let error):
+            return .failure(error)
+        }
+    }
+
+    /// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `mapError` function with a closure that does not throw. For example:
+    ///
+    ///     let possibleData: Result<Data> = .failure(someError)
+    ///     let withMyError: Result<Data> = possibleData.mapError { MyError.error($0) }
+    ///
+    /// - Parameter transform: A closure that takes the error of the instance.
+    /// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns
+    ///            the same instance.
+    public func mapError<T: Error>(_ transform: (Error) -> T) -> Result {
+        switch self {
+        case .failure(let error):
+            return .failure(transform(error))
+        case .success:
+            return self
+        }
+    }
+
+    /// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `flatMapError` function with a closure that may throw an error. For example:
+    ///
+    ///     let possibleData: Result<Data> = .success(Data(...))
+    ///     let possibleObject = possibleData.flatMapError {
+    ///         try someFailableFunction(taking: $0)
+    ///     }
+    ///
+    /// - Parameter transform: A throwing closure that takes the error of the instance.
+    ///
+    /// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns
+    ///            the same instance.
+    public func flatMapError<T: Error>(_ transform: (Error) throws -> T) -> Result {
+        switch self {
+        case .failure(let error):
+            do {
+                return try .failure(transform(error))
+            } catch {
+                return .failure(error)
+            }
+        case .success:
+            return self
+        }
+    }
+
+    /// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a parameter.
+    ///
+    /// Use the `withValue` function to evaluate the passed closure without modifying the `Result` instance.
+    ///
+    /// - Parameter closure: A closure that takes the success value of this instance.
+    /// - Returns: This `Result` instance, unmodified.
+    @discardableResult
+    public func withValue(_ closure: (Value) throws -> Void) rethrows -> Result {
+        if case let .success(value) = self { try closure(value) }
+
+        return self
+    }
+
+    /// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
+    ///
+    /// Use the `withError` function to evaluate the passed closure without modifying the `Result` instance.
+    ///
+    /// - Parameter closure: A closure that takes the success value of this instance.
+    /// - Returns: This `Result` instance, unmodified.
+    @discardableResult
+    public func withError(_ closure: (Error) throws -> Void) rethrows -> Result {
+        if case let .failure(error) = self { try closure(error) }
+
+        return self
+    }
+
+    /// Evaluates the specified closure when the `Result` is a success.
+    ///
+    /// Use the `ifSuccess` function to evaluate the passed closure without modifying the `Result` instance.
+    ///
+    /// - Parameter closure: A `Void` closure.
+    /// - Returns: This `Result` instance, unmodified.
+    @discardableResult
+    public func ifSuccess(_ closure: () throws -> Void) rethrows -> Result {
+        if isSuccess { try closure() }
+
+        return self
+    }
+
+    /// Evaluates the specified closure when the `Result` is a failure.
+    ///
+    /// Use the `ifFailure` function to evaluate the passed closure without modifying the `Result` instance.
+    ///
+    /// - Parameter closure: A `Void` closure.
+    /// - Returns: This `Result` instance, unmodified.
+    @discardableResult
+    public func ifFailure(_ closure: () throws -> Void) rethrows -> Result {
+        if isFailure { try closure() }
+
+        return self
+    }
+}

+ 310 - 0
Example/Pods/Alamofire/Source/ServerTrustPolicy.swift

@@ -0,0 +1,310 @@
+//
+//  ServerTrustPolicy.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host.
+open class ServerTrustPolicyManager {
+    /// The dictionary of policies mapped to a particular host.
+    public let policies: [String: ServerTrustPolicy]
+
+    /// Initializes the `ServerTrustPolicyManager` instance with the given policies.
+    ///
+    /// Since different servers and web services can have different leaf certificates, intermediate and even root
+    /// certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This
+    /// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key
+    /// pinning for host3 and disabling evaluation for host4.
+    ///
+    /// - parameter policies: A dictionary of all policies mapped to a particular host.
+    ///
+    /// - returns: The new `ServerTrustPolicyManager` instance.
+    public init(policies: [String: ServerTrustPolicy]) {
+        self.policies = policies
+    }
+
+    /// Returns the `ServerTrustPolicy` for the given host if applicable.
+    ///
+    /// By default, this method will return the policy that perfectly matches the given host. Subclasses could override
+    /// this method and implement more complex mapping implementations such as wildcards.
+    ///
+    /// - parameter host: The host to use when searching for a matching policy.
+    ///
+    /// - returns: The server trust policy for the given host if found.
+    open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
+        return policies[host]
+    }
+}
+
+// MARK: -
+
+extension URLSession {
+    private struct AssociatedKeys {
+        static var managerKey = "URLSession.ServerTrustPolicyManager"
+    }
+
+    var serverTrustPolicyManager: ServerTrustPolicyManager? {
+        get {
+            return objc_getAssociatedObject(self, &AssociatedKeys.managerKey) as? ServerTrustPolicyManager
+        }
+        set (manager) {
+            objc_setAssociatedObject(self, &AssociatedKeys.managerKey, manager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+}
+
+// MARK: - ServerTrustPolicy
+
+/// The `ServerTrustPolicy` evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when
+/// connecting to a server over a secure HTTPS connection. The policy configuration then evaluates the server trust
+/// with a given set of criteria to determine whether the server trust is valid and the connection should be made.
+///
+/// Using pinned certificates or public keys for evaluation helps prevent man-in-the-middle (MITM) attacks and other
+/// vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged
+/// to route all communication over an HTTPS connection with pinning enabled.
+///
+/// - performDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to
+///                             validate the host provided by the challenge. Applications are encouraged to always
+///                             validate the host in production environments to guarantee the validity of the server's
+///                             certificate chain.
+///
+/// - performRevokedEvaluation: Uses the default and revoked server trust evaluations allowing you to control whether to
+///                             validate the host provided by the challenge as well as specify the revocation flags for
+///                             testing for revoked certificates. Apple platforms did not start testing for revoked
+///                             certificates automatically until iOS 10.1, macOS 10.12 and tvOS 10.1 which is
+///                             demonstrated in our TLS tests. Applications are encouraged to always validate the host
+///                             in production environments to guarantee the validity of the server's certificate chain.
+///
+/// - pinCertificates:          Uses the pinned certificates to validate the server trust. The server trust is
+///                             considered valid if one of the pinned certificates match one of the server certificates.
+///                             By validating both the certificate chain and host, certificate pinning provides a very
+///                             secure form of server trust validation mitigating most, if not all, MITM attacks.
+///                             Applications are encouraged to always validate the host and require a valid certificate
+///                             chain in production environments.
+///
+/// - pinPublicKeys:            Uses the pinned public keys to validate the server trust. The server trust is considered
+///                             valid if one of the pinned public keys match one of the server certificate public keys.
+///                             By validating both the certificate chain and host, public key pinning provides a very
+///                             secure form of server trust validation mitigating most, if not all, MITM attacks.
+///                             Applications are encouraged to always validate the host and require a valid certificate
+///                             chain in production environments.
+///
+/// - disableEvaluation:        Disables all evaluation which in turn will always consider any server trust as valid.
+///
+/// - customEvaluation:         Uses the associated closure to evaluate the validity of the server trust.
+public enum ServerTrustPolicy {
+    case performDefaultEvaluation(validateHost: Bool)
+    case performRevokedEvaluation(validateHost: Bool, revocationFlags: CFOptionFlags)
+    case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool)
+    case pinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool)
+    case disableEvaluation
+    case customEvaluation((_ serverTrust: SecTrust, _ host: String) -> Bool)
+
+    // MARK: - Bundle Location
+
+    /// Returns all certificates within the given bundle with a `.cer` file extension.
+    ///
+    /// - parameter bundle: The bundle to search for all `.cer` files.
+    ///
+    /// - returns: All certificates within the given bundle.
+    public static func certificates(in bundle: Bundle = Bundle.main) -> [SecCertificate] {
+        var certificates: [SecCertificate] = []
+
+        let paths = Set([".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { fileExtension in
+            bundle.paths(forResourcesOfType: fileExtension, inDirectory: nil)
+        }.joined())
+
+        for path in paths {
+            if
+                let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData,
+                let certificate = SecCertificateCreateWithData(nil, certificateData)
+            {
+                certificates.append(certificate)
+            }
+        }
+
+        return certificates
+    }
+
+    /// Returns all public keys within the given bundle with a `.cer` file extension.
+    ///
+    /// - parameter bundle: The bundle to search for all `*.cer` files.
+    ///
+    /// - returns: All public keys within the given bundle.
+    public static func publicKeys(in bundle: Bundle = Bundle.main) -> [SecKey] {
+        var publicKeys: [SecKey] = []
+
+        for certificate in certificates(in: bundle) {
+            if let publicKey = publicKey(for: certificate) {
+                publicKeys.append(publicKey)
+            }
+        }
+
+        return publicKeys
+    }
+
+    // MARK: - Evaluation
+
+    /// Evaluates whether the server trust is valid for the given host.
+    ///
+    /// - parameter serverTrust: The server trust to evaluate.
+    /// - parameter host:        The host of the challenge protection space.
+    ///
+    /// - returns: Whether the server trust is valid.
+    public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool {
+        var serverTrustIsValid = false
+
+        switch self {
+        case let .performDefaultEvaluation(validateHost):
+            let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
+            SecTrustSetPolicies(serverTrust, policy)
+
+            serverTrustIsValid = trustIsValid(serverTrust)
+        case let .performRevokedEvaluation(validateHost, revocationFlags):
+            let defaultPolicy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
+            let revokedPolicy = SecPolicyCreateRevocation(revocationFlags)
+            SecTrustSetPolicies(serverTrust, [defaultPolicy, revokedPolicy] as CFTypeRef)
+
+            serverTrustIsValid = trustIsValid(serverTrust)
+        case let .pinCertificates(pinnedCertificates, validateCertificateChain, validateHost):
+            if validateCertificateChain {
+                let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
+                SecTrustSetPolicies(serverTrust, policy)
+
+                SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates as CFArray)
+                SecTrustSetAnchorCertificatesOnly(serverTrust, true)
+
+                serverTrustIsValid = trustIsValid(serverTrust)
+            } else {
+                let serverCertificatesDataArray = certificateData(for: serverTrust)
+                let pinnedCertificatesDataArray = certificateData(for: pinnedCertificates)
+
+                outerLoop: for serverCertificateData in serverCertificatesDataArray {
+                    for pinnedCertificateData in pinnedCertificatesDataArray {
+                        if serverCertificateData == pinnedCertificateData {
+                            serverTrustIsValid = true
+                            break outerLoop
+                        }
+                    }
+                }
+            }
+        case let .pinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost):
+            var certificateChainEvaluationPassed = true
+
+            if validateCertificateChain {
+                let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
+                SecTrustSetPolicies(serverTrust, policy)
+
+                certificateChainEvaluationPassed = trustIsValid(serverTrust)
+            }
+
+            if certificateChainEvaluationPassed {
+                outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
+                    for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
+                        if serverPublicKey.isEqual(pinnedPublicKey) {
+                            serverTrustIsValid = true
+                            break outerLoop
+                        }
+                    }
+                }
+            }
+        case .disableEvaluation:
+            serverTrustIsValid = true
+        case let .customEvaluation(closure):
+            serverTrustIsValid = closure(serverTrust, host)
+        }
+
+        return serverTrustIsValid
+    }
+
+    // MARK: - Private - Trust Validation
+
+    private func trustIsValid(_ trust: SecTrust) -> Bool {
+        var isValid = false
+
+        if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
+            isValid = SecTrustEvaluateWithError(trust, nil)
+        } else {
+            var result = SecTrustResultType.invalid
+            let status = SecTrustEvaluate(trust, &result)
+
+            if status == errSecSuccess {
+                let unspecified = SecTrustResultType.unspecified
+                let proceed = SecTrustResultType.proceed
+
+                isValid = result == unspecified || result == proceed
+            }
+        }
+
+        return isValid
+    }
+
+    // MARK: - Private - Certificate Data
+
+    private func certificateData(for trust: SecTrust) -> [Data] {
+        var certificates: [SecCertificate] = []
+
+        for index in 0..<SecTrustGetCertificateCount(trust) {
+            if let certificate = SecTrustGetCertificateAtIndex(trust, index) {
+                certificates.append(certificate)
+            }
+        }
+
+        return certificateData(for: certificates)
+    }
+
+    private func certificateData(for certificates: [SecCertificate]) -> [Data] {
+        return certificates.map { SecCertificateCopyData($0) as Data }
+    }
+
+    // MARK: - Private - Public Key Extraction
+
+    private static func publicKeys(for trust: SecTrust) -> [SecKey] {
+        var publicKeys: [SecKey] = []
+
+        for index in 0..<SecTrustGetCertificateCount(trust) {
+            if
+                let certificate = SecTrustGetCertificateAtIndex(trust, index),
+                let publicKey = publicKey(for: certificate)
+            {
+                publicKeys.append(publicKey)
+            }
+        }
+
+        return publicKeys
+    }
+
+    private static func publicKey(for certificate: SecCertificate) -> SecKey? {
+        var publicKey: SecKey?
+
+        let policy = SecPolicyCreateBasicX509()
+        var trust: SecTrust?
+        let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust)
+
+        if let trust = trust, trustCreationStatus == errSecSuccess {
+            publicKey = SecTrustCopyPublicKey(trust)
+        }
+
+        return publicKey
+    }
+}

+ 725 - 0
Example/Pods/Alamofire/Source/SessionDelegate.swift

@@ -0,0 +1,725 @@
+//
+//  SessionDelegate.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// Responsible for handling all delegate callbacks for the underlying session.
+open class SessionDelegate: NSObject {
+
+    // MARK: URLSessionDelegate Overrides
+
+    /// Overrides default behavior for URLSessionDelegate method `urlSession(_:didBecomeInvalidWithError:)`.
+    open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)?
+
+    /// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`.
+    open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
+
+    /// Overrides all behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)` and requires the caller to call the `completionHandler`.
+    open var sessionDidReceiveChallengeWithCompletion: ((URLSession, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)?
+
+    /// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`.
+    open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?
+
+    // MARK: URLSessionTaskDelegate Overrides
+
+    /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`.
+    open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
+
+    /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` and
+    /// requires the caller to call the `completionHandler`.
+    open var taskWillPerformHTTPRedirectionWithCompletion: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void)?
+
+    /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)`.
+    open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
+
+    /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)` and
+    /// requires the caller to call the `completionHandler`.
+    open var taskDidReceiveChallengeWithCompletion: ((URLSession, URLSessionTask, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)?
+
+    /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)`.
+    open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
+
+    /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)` and
+    /// requires the caller to call the `completionHandler`.
+    open var taskNeedNewBodyStreamWithCompletion: ((URLSession, URLSessionTask, @escaping (InputStream?) -> Void) -> Void)?
+
+    /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)`.
+    open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
+
+    /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didCompleteWithError:)`.
+    open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)?
+
+    // MARK: URLSessionDataDelegate Overrides
+
+    /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)`.
+    open var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
+
+    /// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)` and
+    /// requires caller to call the `completionHandler`.
+    open var dataTaskDidReceiveResponseWithCompletion: ((URLSession, URLSessionDataTask, URLResponse, @escaping (URLSession.ResponseDisposition) -> Void) -> Void)?
+
+    /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didBecome:)`.
+    open var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
+
+    /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:)`.
+    open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
+
+    /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`.
+    open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
+
+    /// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)` and
+    /// requires caller to call the `completionHandler`.
+    open var dataTaskWillCacheResponseWithCompletion: ((URLSession, URLSessionDataTask, CachedURLResponse, @escaping (CachedURLResponse?) -> Void) -> Void)?
+
+    // MARK: URLSessionDownloadDelegate Overrides
+
+    /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didFinishDownloadingTo:)`.
+    open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)?
+
+    /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)`.
+    open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
+
+    /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)`.
+    open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
+
+    // MARK: URLSessionStreamDelegate Overrides
+
+#if !os(watchOS)
+
+    /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:readClosedFor:)`.
+    @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+    open var streamTaskReadClosed: ((URLSession, URLSessionStreamTask) -> Void)? {
+        get {
+            return _streamTaskReadClosed as? (URLSession, URLSessionStreamTask) -> Void
+        }
+        set {
+            _streamTaskReadClosed = newValue
+        }
+    }
+
+    /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:writeClosedFor:)`.
+    @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+    open var streamTaskWriteClosed: ((URLSession, URLSessionStreamTask) -> Void)? {
+        get {
+            return _streamTaskWriteClosed as? (URLSession, URLSessionStreamTask) -> Void
+        }
+        set {
+            _streamTaskWriteClosed = newValue
+        }
+    }
+
+    /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:betterRouteDiscoveredFor:)`.
+    @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+    open var streamTaskBetterRouteDiscovered: ((URLSession, URLSessionStreamTask) -> Void)? {
+        get {
+            return _streamTaskBetterRouteDiscovered as? (URLSession, URLSessionStreamTask) -> Void
+        }
+        set {
+            _streamTaskBetterRouteDiscovered = newValue
+        }
+    }
+
+    /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:streamTask:didBecome:outputStream:)`.
+    @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+    open var streamTaskDidBecomeInputAndOutputStreams: ((URLSession, URLSessionStreamTask, InputStream, OutputStream) -> Void)? {
+        get {
+            return _streamTaskDidBecomeInputStream as? (URLSession, URLSessionStreamTask, InputStream, OutputStream) -> Void
+        }
+        set {
+            _streamTaskDidBecomeInputStream = newValue
+        }
+    }
+
+    var _streamTaskReadClosed: Any?
+    var _streamTaskWriteClosed: Any?
+    var _streamTaskBetterRouteDiscovered: Any?
+    var _streamTaskDidBecomeInputStream: Any?
+
+#endif
+
+    // MARK: Properties
+
+    var retrier: RequestRetrier?
+    weak var sessionManager: SessionManager?
+
+    var requests: [Int: Request] = [:]
+    private let lock = NSLock()
+
+    /// Access the task delegate for the specified task in a thread-safe manner.
+    open subscript(task: URLSessionTask) -> Request? {
+        get {
+            lock.lock() ; defer { lock.unlock() }
+            return requests[task.taskIdentifier]
+        }
+        set {
+            lock.lock() ; defer { lock.unlock() }
+            requests[task.taskIdentifier] = newValue
+        }
+    }
+
+    // MARK: Lifecycle
+
+    /// Initializes the `SessionDelegate` instance.
+    ///
+    /// - returns: The new `SessionDelegate` instance.
+    public override init() {
+        super.init()
+    }
+
+    // MARK: NSObject Overrides
+
+    /// Returns a `Bool` indicating whether the `SessionDelegate` implements or inherits a method that can respond
+    /// to a specified message.
+    ///
+    /// - parameter selector: A selector that identifies a message.
+    ///
+    /// - returns: `true` if the receiver implements or inherits a method that can respond to selector, otherwise `false`.
+    open override func responds(to selector: Selector) -> Bool {
+        #if !os(macOS)
+            if selector == #selector(URLSessionDelegate.urlSessionDidFinishEvents(forBackgroundURLSession:)) {
+                return sessionDidFinishEventsForBackgroundURLSession != nil
+            }
+        #endif
+
+        #if !os(watchOS)
+            if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
+                switch selector {
+                case #selector(URLSessionStreamDelegate.urlSession(_:readClosedFor:)):
+                    return streamTaskReadClosed != nil
+                case #selector(URLSessionStreamDelegate.urlSession(_:writeClosedFor:)):
+                    return streamTaskWriteClosed != nil
+                case #selector(URLSessionStreamDelegate.urlSession(_:betterRouteDiscoveredFor:)):
+                    return streamTaskBetterRouteDiscovered != nil
+                case #selector(URLSessionStreamDelegate.urlSession(_:streamTask:didBecome:outputStream:)):
+                    return streamTaskDidBecomeInputAndOutputStreams != nil
+                default:
+                    break
+                }
+            }
+        #endif
+
+        switch selector {
+        case #selector(URLSessionDelegate.urlSession(_:didBecomeInvalidWithError:)):
+            return sessionDidBecomeInvalidWithError != nil
+        case #selector(URLSessionDelegate.urlSession(_:didReceive:completionHandler:)):
+            return (sessionDidReceiveChallenge != nil  || sessionDidReceiveChallengeWithCompletion != nil)
+        case #selector(URLSessionTaskDelegate.urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)):
+            return (taskWillPerformHTTPRedirection != nil || taskWillPerformHTTPRedirectionWithCompletion != nil)
+        case #selector(URLSessionDataDelegate.urlSession(_:dataTask:didReceive:completionHandler:)):
+            return (dataTaskDidReceiveResponse != nil || dataTaskDidReceiveResponseWithCompletion != nil)
+        default:
+            return type(of: self).instancesRespond(to: selector)
+        }
+    }
+}
+
+// MARK: - URLSessionDelegate
+
+extension SessionDelegate: URLSessionDelegate {
+    /// Tells the delegate that the session has been invalidated.
+    ///
+    /// - parameter session: The session object that was invalidated.
+    /// - parameter error:   The error that caused invalidation, or nil if the invalidation was explicit.
+    open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
+        sessionDidBecomeInvalidWithError?(session, error)
+    }
+
+    /// Requests credentials from the delegate in response to a session-level authentication request from the
+    /// remote server.
+    ///
+    /// - parameter session:           The session containing the task that requested authentication.
+    /// - parameter challenge:         An object that contains the request for authentication.
+    /// - parameter completionHandler: A handler that your delegate method must call providing the disposition
+    ///                                and credential.
+    open func urlSession(
+        _ session: URLSession,
+        didReceive challenge: URLAuthenticationChallenge,
+        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
+    {
+        guard sessionDidReceiveChallengeWithCompletion == nil else {
+            sessionDidReceiveChallengeWithCompletion?(session, challenge, completionHandler)
+            return
+        }
+
+        var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
+        var credential: URLCredential?
+
+        if let sessionDidReceiveChallenge = sessionDidReceiveChallenge {
+            (disposition, credential) = sessionDidReceiveChallenge(session, challenge)
+        } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
+            let host = challenge.protectionSpace.host
+
+            if
+                let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
+                let serverTrust = challenge.protectionSpace.serverTrust
+            {
+                if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
+                    disposition = .useCredential
+                    credential = URLCredential(trust: serverTrust)
+                } else {
+                    disposition = .cancelAuthenticationChallenge
+                }
+            }
+        }
+
+        completionHandler(disposition, credential)
+    }
+
+#if !os(macOS)
+
+    /// Tells the delegate that all messages enqueued for a session have been delivered.
+    ///
+    /// - parameter session: The session that no longer has any outstanding requests.
+    open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
+        sessionDidFinishEventsForBackgroundURLSession?(session)
+    }
+
+#endif
+}
+
+// MARK: - URLSessionTaskDelegate
+
+extension SessionDelegate: URLSessionTaskDelegate {
+    /// Tells the delegate that the remote server requested an HTTP redirect.
+    ///
+    /// - parameter session:           The session containing the task whose request resulted in a redirect.
+    /// - parameter task:              The task whose request resulted in a redirect.
+    /// - parameter response:          An object containing the server’s response to the original request.
+    /// - parameter request:           A URL request object filled out with the new location.
+    /// - parameter completionHandler: A closure that your handler should call with either the value of the request
+    ///                                parameter, a modified URL request object, or NULL to refuse the redirect and
+    ///                                return the body of the redirect response.
+    open func urlSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        willPerformHTTPRedirection response: HTTPURLResponse,
+        newRequest request: URLRequest,
+        completionHandler: @escaping (URLRequest?) -> Void)
+    {
+        guard taskWillPerformHTTPRedirectionWithCompletion == nil else {
+            taskWillPerformHTTPRedirectionWithCompletion?(session, task, response, request, completionHandler)
+            return
+        }
+
+        var redirectRequest: URLRequest? = request
+
+        if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
+            redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
+        }
+
+        completionHandler(redirectRequest)
+    }
+
+    /// Requests credentials from the delegate in response to an authentication request from the remote server.
+    ///
+    /// - parameter session:           The session containing the task whose request requires authentication.
+    /// - parameter task:              The task whose request requires authentication.
+    /// - parameter challenge:         An object that contains the request for authentication.
+    /// - parameter completionHandler: A handler that your delegate method must call providing the disposition
+    ///                                and credential.
+    open func urlSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        didReceive challenge: URLAuthenticationChallenge,
+        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
+    {
+        guard taskDidReceiveChallengeWithCompletion == nil else {
+            taskDidReceiveChallengeWithCompletion?(session, task, challenge, completionHandler)
+            return
+        }
+
+        if let taskDidReceiveChallenge = taskDidReceiveChallenge {
+            let result = taskDidReceiveChallenge(session, task, challenge)
+            completionHandler(result.0, result.1)
+        } else if let delegate = self[task]?.delegate {
+            delegate.urlSession(
+                session,
+                task: task,
+                didReceive: challenge,
+                completionHandler: completionHandler
+            )
+        } else {
+            urlSession(session, didReceive: challenge, completionHandler: completionHandler)
+        }
+    }
+
+    /// Tells the delegate when a task requires a new request body stream to send to the remote server.
+    ///
+    /// - parameter session:           The session containing the task that needs a new body stream.
+    /// - parameter task:              The task that needs a new body stream.
+    /// - parameter completionHandler: A completion handler that your delegate method should call with the new body stream.
+    open func urlSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
+    {
+        guard taskNeedNewBodyStreamWithCompletion == nil else {
+            taskNeedNewBodyStreamWithCompletion?(session, task, completionHandler)
+            return
+        }
+
+        if let taskNeedNewBodyStream = taskNeedNewBodyStream {
+            completionHandler(taskNeedNewBodyStream(session, task))
+        } else if let delegate = self[task]?.delegate {
+            delegate.urlSession(session, task: task, needNewBodyStream: completionHandler)
+        }
+    }
+
+    /// Periodically informs the delegate of the progress of sending body content to the server.
+    ///
+    /// - parameter session:                  The session containing the data task.
+    /// - parameter task:                     The data task.
+    /// - parameter bytesSent:                The number of bytes sent since the last time this delegate method was called.
+    /// - parameter totalBytesSent:           The total number of bytes sent so far.
+    /// - parameter totalBytesExpectedToSend: The expected length of the body data.
+    open func urlSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        didSendBodyData bytesSent: Int64,
+        totalBytesSent: Int64,
+        totalBytesExpectedToSend: Int64)
+    {
+        if let taskDidSendBodyData = taskDidSendBodyData {
+            taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
+        } else if let delegate = self[task]?.delegate as? UploadTaskDelegate {
+            delegate.URLSession(
+                session,
+                task: task,
+                didSendBodyData: bytesSent,
+                totalBytesSent: totalBytesSent,
+                totalBytesExpectedToSend: totalBytesExpectedToSend
+            )
+        }
+    }
+
+#if !os(watchOS)
+
+    /// Tells the delegate that the session finished collecting metrics for the task.
+    ///
+    /// - parameter session: The session collecting the metrics.
+    /// - parameter task:    The task whose metrics have been collected.
+    /// - parameter metrics: The collected metrics.
+    @available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
+    @objc(URLSession:task:didFinishCollectingMetrics:)
+    open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
+        self[task]?.delegate.metrics = metrics
+    }
+
+#endif
+
+    /// Tells the delegate that the task finished transferring data.
+    ///
+    /// - parameter session: The session containing the task whose request finished transferring data.
+    /// - parameter task:    The task whose request finished transferring data.
+    /// - parameter error:   If an error occurred, an error object indicating how the transfer failed, otherwise nil.
+    open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+        /// Executed after it is determined that the request is not going to be retried
+        let completeTask: (URLSession, URLSessionTask, Error?) -> Void = { [weak self] session, task, error in
+            guard let strongSelf = self else { return }
+
+            strongSelf.taskDidComplete?(session, task, error)
+
+            strongSelf[task]?.delegate.urlSession(session, task: task, didCompleteWithError: error)
+
+            var userInfo: [String: Any] = [Notification.Key.Task: task]
+
+            if let data = (strongSelf[task]?.delegate as? DataTaskDelegate)?.data {
+                userInfo[Notification.Key.ResponseData] = data
+            }
+
+            NotificationCenter.default.post(
+                name: Notification.Name.Task.DidComplete,
+                object: strongSelf,
+                userInfo: userInfo
+            )
+
+            strongSelf[task] = nil
+        }
+
+        guard let request = self[task], let sessionManager = sessionManager else {
+            completeTask(session, task, error)
+            return
+        }
+
+        // Run all validations on the request before checking if an error occurred
+        request.validations.forEach { $0() }
+
+        // Determine whether an error has occurred
+        var error: Error? = error
+
+        if request.delegate.error != nil {
+            error = request.delegate.error
+        }
+
+        /// If an error occurred and the retrier is set, asynchronously ask the retrier if the request
+        /// should be retried. Otherwise, complete the task by notifying the task delegate.
+        if let retrier = retrier, let error = error {
+            retrier.should(sessionManager, retry: request, with: error) { [weak self] shouldRetry, timeDelay in
+                guard shouldRetry else { completeTask(session, task, error) ; return }
+
+                DispatchQueue.utility.after(timeDelay) { [weak self] in
+                    guard let strongSelf = self else { return }
+
+                    let retrySucceeded = strongSelf.sessionManager?.retry(request) ?? false
+
+                    if retrySucceeded, let task = request.task {
+                        strongSelf[task] = request
+                        return
+                    } else {
+                        completeTask(session, task, error)
+                    }
+                }
+            }
+        } else {
+            completeTask(session, task, error)
+        }
+    }
+}
+
+// MARK: - URLSessionDataDelegate
+
+extension SessionDelegate: URLSessionDataDelegate {
+    /// Tells the delegate that the data task received the initial reply (headers) from the server.
+    ///
+    /// - parameter session:           The session containing the data task that received an initial reply.
+    /// - parameter dataTask:          The data task that received an initial reply.
+    /// - parameter response:          A URL response object populated with headers.
+    /// - parameter completionHandler: A completion handler that your code calls to continue the transfer, passing a
+    ///                                constant to indicate whether the transfer should continue as a data task or
+    ///                                should become a download task.
+    open func urlSession(
+        _ session: URLSession,
+        dataTask: URLSessionDataTask,
+        didReceive response: URLResponse,
+        completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
+    {
+        guard dataTaskDidReceiveResponseWithCompletion == nil else {
+            dataTaskDidReceiveResponseWithCompletion?(session, dataTask, response, completionHandler)
+            return
+        }
+
+        var disposition: URLSession.ResponseDisposition = .allow
+
+        if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
+            disposition = dataTaskDidReceiveResponse(session, dataTask, response)
+        }
+
+        completionHandler(disposition)
+    }
+
+    /// Tells the delegate that the data task was changed to a download task.
+    ///
+    /// - parameter session:      The session containing the task that was replaced by a download task.
+    /// - parameter dataTask:     The data task that was replaced by a download task.
+    /// - parameter downloadTask: The new download task that replaced the data task.
+    open func urlSession(
+        _ session: URLSession,
+        dataTask: URLSessionDataTask,
+        didBecome downloadTask: URLSessionDownloadTask)
+    {
+        if let dataTaskDidBecomeDownloadTask = dataTaskDidBecomeDownloadTask {
+            dataTaskDidBecomeDownloadTask(session, dataTask, downloadTask)
+        } else {
+            self[downloadTask]?.delegate = DownloadTaskDelegate(task: downloadTask)
+        }
+    }
+
+    /// Tells the delegate that the data task has received some of the expected data.
+    ///
+    /// - parameter session:  The session containing the data task that provided data.
+    /// - parameter dataTask: The data task that provided data.
+    /// - parameter data:     A data object containing the transferred data.
+    open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
+        if let dataTaskDidReceiveData = dataTaskDidReceiveData {
+            dataTaskDidReceiveData(session, dataTask, data)
+        } else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate {
+            delegate.urlSession(session, dataTask: dataTask, didReceive: data)
+        }
+    }
+
+    /// Asks the delegate whether the data (or upload) task should store the response in the cache.
+    ///
+    /// - parameter session:           The session containing the data (or upload) task.
+    /// - parameter dataTask:          The data (or upload) task.
+    /// - parameter proposedResponse:  The default caching behavior. This behavior is determined based on the current
+    ///                                caching policy and the values of certain received headers, such as the Pragma
+    ///                                and Cache-Control headers.
+    /// - parameter completionHandler: A block that your handler must call, providing either the original proposed
+    ///                                response, a modified version of that response, or NULL to prevent caching the
+    ///                                response. If your delegate implements this method, it must call this completion
+    ///                                handler; otherwise, your app leaks memory.
+    open func urlSession(
+        _ session: URLSession,
+        dataTask: URLSessionDataTask,
+        willCacheResponse proposedResponse: CachedURLResponse,
+        completionHandler: @escaping (CachedURLResponse?) -> Void)
+    {
+        guard dataTaskWillCacheResponseWithCompletion == nil else {
+            dataTaskWillCacheResponseWithCompletion?(session, dataTask, proposedResponse, completionHandler)
+            return
+        }
+
+        if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
+            completionHandler(dataTaskWillCacheResponse(session, dataTask, proposedResponse))
+        } else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate {
+            delegate.urlSession(
+                session,
+                dataTask: dataTask,
+                willCacheResponse: proposedResponse,
+                completionHandler: completionHandler
+            )
+        } else {
+            completionHandler(proposedResponse)
+        }
+    }
+}
+
+// MARK: - URLSessionDownloadDelegate
+
+extension SessionDelegate: URLSessionDownloadDelegate {
+    /// Tells the delegate that a download task has finished downloading.
+    ///
+    /// - parameter session:      The session containing the download task that finished.
+    /// - parameter downloadTask: The download task that finished.
+    /// - parameter location:     A file URL for the temporary file. Because the file is temporary, you must either
+    ///                           open the file for reading or move it to a permanent location in your app’s sandbox
+    ///                           container directory before returning from this delegate method.
+    open func urlSession(
+        _ session: URLSession,
+        downloadTask: URLSessionDownloadTask,
+        didFinishDownloadingTo location: URL)
+    {
+        if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL {
+            downloadTaskDidFinishDownloadingToURL(session, downloadTask, location)
+        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
+            delegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
+        }
+    }
+
+    /// Periodically informs the delegate about the download’s progress.
+    ///
+    /// - parameter session:                   The session containing the download task.
+    /// - parameter downloadTask:              The download task.
+    /// - parameter bytesWritten:              The number of bytes transferred since the last time this delegate
+    ///                                        method was called.
+    /// - parameter totalBytesWritten:         The total number of bytes transferred so far.
+    /// - parameter totalBytesExpectedToWrite: The expected length of the file, as provided by the Content-Length
+    ///                                        header. If this header was not provided, the value is
+    ///                                        `NSURLSessionTransferSizeUnknown`.
+    open func urlSession(
+        _ session: URLSession,
+        downloadTask: URLSessionDownloadTask,
+        didWriteData bytesWritten: Int64,
+        totalBytesWritten: Int64,
+        totalBytesExpectedToWrite: Int64)
+    {
+        if let downloadTaskDidWriteData = downloadTaskDidWriteData {
+            downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
+        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
+            delegate.urlSession(
+                session,
+                downloadTask: downloadTask,
+                didWriteData: bytesWritten,
+                totalBytesWritten: totalBytesWritten,
+                totalBytesExpectedToWrite: totalBytesExpectedToWrite
+            )
+        }
+    }
+
+    /// Tells the delegate that the download task has resumed downloading.
+    ///
+    /// - parameter session:            The session containing the download task that finished.
+    /// - parameter downloadTask:       The download task that resumed. See explanation in the discussion.
+    /// - parameter fileOffset:         If the file's cache policy or last modified date prevents reuse of the
+    ///                                 existing content, then this value is zero. Otherwise, this value is an
+    ///                                 integer representing the number of bytes on disk that do not need to be
+    ///                                 retrieved again.
+    /// - parameter expectedTotalBytes: The expected length of the file, as provided by the Content-Length header.
+    ///                                 If this header was not provided, the value is NSURLSessionTransferSizeUnknown.
+    open func urlSession(
+        _ session: URLSession,
+        downloadTask: URLSessionDownloadTask,
+        didResumeAtOffset fileOffset: Int64,
+        expectedTotalBytes: Int64)
+    {
+        if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
+            downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
+        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
+            delegate.urlSession(
+                session,
+                downloadTask: downloadTask,
+                didResumeAtOffset: fileOffset,
+                expectedTotalBytes: expectedTotalBytes
+            )
+        }
+    }
+}
+
+// MARK: - URLSessionStreamDelegate
+
+#if !os(watchOS)
+
+@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+extension SessionDelegate: URLSessionStreamDelegate {
+    /// Tells the delegate that the read side of the connection has been closed.
+    ///
+    /// - parameter session:    The session.
+    /// - parameter streamTask: The stream task.
+    open func urlSession(_ session: URLSession, readClosedFor streamTask: URLSessionStreamTask) {
+        streamTaskReadClosed?(session, streamTask)
+    }
+
+    /// Tells the delegate that the write side of the connection has been closed.
+    ///
+    /// - parameter session:    The session.
+    /// - parameter streamTask: The stream task.
+    open func urlSession(_ session: URLSession, writeClosedFor streamTask: URLSessionStreamTask) {
+        streamTaskWriteClosed?(session, streamTask)
+    }
+
+    /// Tells the delegate that the system has determined that a better route to the host is available.
+    ///
+    /// - parameter session:    The session.
+    /// - parameter streamTask: The stream task.
+    open func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: URLSessionStreamTask) {
+        streamTaskBetterRouteDiscovered?(session, streamTask)
+    }
+
+    /// Tells the delegate that the stream task has been completed and provides the unopened stream objects.
+    ///
+    /// - parameter session:      The session.
+    /// - parameter streamTask:   The stream task.
+    /// - parameter inputStream:  The new input stream.
+    /// - parameter outputStream: The new output stream.
+    open func urlSession(
+        _ session: URLSession,
+        streamTask: URLSessionStreamTask,
+        didBecome inputStream: InputStream,
+        outputStream: OutputStream)
+    {
+        streamTaskDidBecomeInputAndOutputStreams?(session, streamTask, inputStream, outputStream)
+    }
+}
+
+#endif

+ 899 - 0
Example/Pods/Alamofire/Source/SessionManager.swift

@@ -0,0 +1,899 @@
+//
+//  SessionManager.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// Responsible for creating and managing `Request` objects, as well as their underlying `NSURLSession`.
+open class SessionManager {
+
+    // MARK: - Helper Types
+
+    /// Defines whether the `MultipartFormData` encoding was successful and contains result of the encoding as
+    /// associated values.
+    ///
+    /// - Success: Represents a successful `MultipartFormData` encoding and contains the new `UploadRequest` along with
+    ///            streaming information.
+    /// - Failure: Used to represent a failure in the `MultipartFormData` encoding and also contains the encoding
+    ///            error.
+    public enum MultipartFormDataEncodingResult {
+        case success(request: UploadRequest, streamingFromDisk: Bool, streamFileURL: URL?)
+        case failure(Error)
+    }
+
+    // MARK: - Properties
+
+    /// A default instance of `SessionManager`, used by top-level Alamofire request methods, and suitable for use
+    /// directly for any ad hoc requests.
+    public static let `default`: SessionManager = {
+        let configuration = URLSessionConfiguration.default
+        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
+
+        return SessionManager(configuration: configuration)
+    }()
+
+    /// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers.
+    public static let defaultHTTPHeaders: HTTPHeaders = {
+        // Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3
+        let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5"
+
+        // Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5
+        let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in
+            let quality = 1.0 - (Double(index) * 0.1)
+            return "\(languageCode);q=\(quality)"
+        }.joined(separator: ", ")
+
+        // User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3
+        // Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0`
+        let userAgent: String = {
+            if let info = Bundle.main.infoDictionary {
+                let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown"
+                let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown"
+                let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown"
+                let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown"
+
+                let osNameVersion: String = {
+                    let version = ProcessInfo.processInfo.operatingSystemVersion
+                    let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
+
+                    let osName: String = {
+                        #if os(iOS)
+                            return "iOS"
+                        #elseif os(watchOS)
+                            return "watchOS"
+                        #elseif os(tvOS)
+                            return "tvOS"
+                        #elseif os(macOS)
+                            return "OS X"
+                        #elseif os(Linux)
+                            return "Linux"
+                        #else
+                            return "Unknown"
+                        #endif
+                    }()
+
+                    return "\(osName) \(versionString)"
+                }()
+
+                let alamofireVersion: String = {
+                    guard
+                        let afInfo = Bundle(for: SessionManager.self).infoDictionary,
+                        let build = afInfo["CFBundleShortVersionString"]
+                    else { return "Unknown" }
+
+                    return "Alamofire/\(build)"
+                }()
+
+                return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)"
+            }
+
+            return "Alamofire"
+        }()
+
+        return [
+            "Accept-Encoding": acceptEncoding,
+            "Accept-Language": acceptLanguage,
+            "User-Agent": userAgent
+        ]
+    }()
+
+    /// Default memory threshold used when encoding `MultipartFormData` in bytes.
+    public static let multipartFormDataEncodingMemoryThreshold: UInt64 = 10_000_000
+
+    /// The underlying session.
+    public let session: URLSession
+
+    /// The session delegate handling all the task and session delegate callbacks.
+    public let delegate: SessionDelegate
+
+    /// Whether to start requests immediately after being constructed. `true` by default.
+    open var startRequestsImmediately: Bool = true
+
+    /// The request adapter called each time a new request is created.
+    open var adapter: RequestAdapter?
+
+    /// The request retrier called each time a request encounters an error to determine whether to retry the request.
+    open var retrier: RequestRetrier? {
+        get { return delegate.retrier }
+        set { delegate.retrier = newValue }
+    }
+
+    /// The background completion handler closure provided by the UIApplicationDelegate
+    /// `application:handleEventsForBackgroundURLSession:completionHandler:` method. By setting the background
+    /// completion handler, the SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` closure implementation
+    /// will automatically call the handler.
+    ///
+    /// If you need to handle your own events before the handler is called, then you need to override the
+    /// SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` and manually call the handler when finished.
+    ///
+    /// `nil` by default.
+    open var backgroundCompletionHandler: (() -> Void)?
+
+    let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString)
+
+    // MARK: - Lifecycle
+
+    /// Creates an instance with the specified `configuration`, `delegate` and `serverTrustPolicyManager`.
+    ///
+    /// - parameter configuration:            The configuration used to construct the managed session.
+    ///                                       `URLSessionConfiguration.default` by default.
+    /// - parameter delegate:                 The delegate used when initializing the session. `SessionDelegate()` by
+    ///                                       default.
+    /// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust
+    ///                                       challenges. `nil` by default.
+    ///
+    /// - returns: The new `SessionManager` instance.
+    public init(
+        configuration: URLSessionConfiguration = URLSessionConfiguration.default,
+        delegate: SessionDelegate = SessionDelegate(),
+        serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
+    {
+        self.delegate = delegate
+        self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
+
+        commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
+    }
+
+    /// Creates an instance with the specified `session`, `delegate` and `serverTrustPolicyManager`.
+    ///
+    /// - parameter session:                  The URL session.
+    /// - parameter delegate:                 The delegate of the URL session. Must equal the URL session's delegate.
+    /// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust
+    ///                                       challenges. `nil` by default.
+    ///
+    /// - returns: The new `SessionManager` instance if the URL session's delegate matches; `nil` otherwise.
+    public init?(
+        session: URLSession,
+        delegate: SessionDelegate,
+        serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
+    {
+        guard delegate === session.delegate else { return nil }
+
+        self.delegate = delegate
+        self.session = session
+
+        commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
+    }
+
+    private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?) {
+        session.serverTrustPolicyManager = serverTrustPolicyManager
+
+        delegate.sessionManager = self
+
+        delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
+            guard let strongSelf = self else { return }
+            DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
+        }
+    }
+
+    deinit {
+        session.invalidateAndCancel()
+    }
+
+    // MARK: - Data Request
+
+    /// Creates a `DataRequest` to retrieve the contents of the specified `url`, `method`, `parameters`, `encoding`
+    /// and `headers`.
+    ///
+    /// - parameter url:        The URL.
+    /// - parameter method:     The HTTP method. `.get` by default.
+    /// - parameter parameters: The parameters. `nil` by default.
+    /// - parameter encoding:   The parameter encoding. `URLEncoding.default` by default.
+    /// - parameter headers:    The HTTP headers. `nil` by default.
+    ///
+    /// - returns: The created `DataRequest`.
+    @discardableResult
+    open func request(
+        _ url: URLConvertible,
+        method: HTTPMethod = .get,
+        parameters: Parameters? = nil,
+        encoding: ParameterEncoding = URLEncoding.default,
+        headers: HTTPHeaders? = nil)
+        -> DataRequest
+    {
+        var originalRequest: URLRequest?
+
+        do {
+            originalRequest = try URLRequest(url: url, method: method, headers: headers)
+            let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
+            return request(encodedURLRequest)
+        } catch {
+            return request(originalRequest, failedWith: error)
+        }
+    }
+
+    /// Creates a `DataRequest` to retrieve the contents of a URL based on the specified `urlRequest`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter urlRequest: The URL request.
+    ///
+    /// - returns: The created `DataRequest`.
+    @discardableResult
+    open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
+        var originalRequest: URLRequest?
+
+        do {
+            originalRequest = try urlRequest.asURLRequest()
+            let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)
+
+            let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
+            let request = DataRequest(session: session, requestTask: .data(originalTask, task))
+
+            delegate[task] = request
+
+            if startRequestsImmediately { request.resume() }
+
+            return request
+        } catch {
+            return request(originalRequest, failedWith: error)
+        }
+    }
+
+    // MARK: Private - Request Implementation
+
+    private func request(_ urlRequest: URLRequest?, failedWith error: Error) -> DataRequest {
+        var requestTask: Request.RequestTask = .data(nil, nil)
+
+        if let urlRequest = urlRequest {
+            let originalTask = DataRequest.Requestable(urlRequest: urlRequest)
+            requestTask = .data(originalTask, nil)
+        }
+
+        let underlyingError = error.underlyingAdaptError ?? error
+        let request = DataRequest(session: session, requestTask: requestTask, error: underlyingError)
+
+        if let retrier = retrier, error is AdaptError {
+            allowRetrier(retrier, toRetry: request, with: underlyingError)
+        } else {
+            if startRequestsImmediately { request.resume() }
+        }
+
+        return request
+    }
+
+    // MARK: - Download Request
+
+    // MARK: URL Request
+
+    /// Creates a `DownloadRequest` to retrieve the contents the specified `url`, `method`, `parameters`, `encoding`,
+    /// `headers` and save them to the `destination`.
+    ///
+    /// If `destination` is not specified, the contents will remain in the temporary location determined by the
+    /// underlying URL session.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter url:         The URL.
+    /// - parameter method:      The HTTP method. `.get` by default.
+    /// - parameter parameters:  The parameters. `nil` by default.
+    /// - parameter encoding:    The parameter encoding. `URLEncoding.default` by default.
+    /// - parameter headers:     The HTTP headers. `nil` by default.
+    /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
+    ///
+    /// - returns: The created `DownloadRequest`.
+    @discardableResult
+    open func download(
+        _ url: URLConvertible,
+        method: HTTPMethod = .get,
+        parameters: Parameters? = nil,
+        encoding: ParameterEncoding = URLEncoding.default,
+        headers: HTTPHeaders? = nil,
+        to destination: DownloadRequest.DownloadFileDestination? = nil)
+        -> DownloadRequest
+    {
+        do {
+            let urlRequest = try URLRequest(url: url, method: method, headers: headers)
+            let encodedURLRequest = try encoding.encode(urlRequest, with: parameters)
+            return download(encodedURLRequest, to: destination)
+        } catch {
+            return download(nil, to: destination, failedWith: error)
+        }
+    }
+
+    /// Creates a `DownloadRequest` to retrieve the contents of a URL based on the specified `urlRequest` and save
+    /// them to the `destination`.
+    ///
+    /// If `destination` is not specified, the contents will remain in the temporary location determined by the
+    /// underlying URL session.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter urlRequest:  The URL request
+    /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
+    ///
+    /// - returns: The created `DownloadRequest`.
+    @discardableResult
+    open func download(
+        _ urlRequest: URLRequestConvertible,
+        to destination: DownloadRequest.DownloadFileDestination? = nil)
+        -> DownloadRequest
+    {
+        do {
+            let urlRequest = try urlRequest.asURLRequest()
+            return download(.request(urlRequest), to: destination)
+        } catch {
+            return download(nil, to: destination, failedWith: error)
+        }
+    }
+
+    // MARK: Resume Data
+
+    /// Creates a `DownloadRequest` from the `resumeData` produced from a previous request cancellation to retrieve
+    /// the contents of the original request and save them to the `destination`.
+    ///
+    /// If `destination` is not specified, the contents will remain in the temporary location determined by the
+    /// underlying URL session.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken
+    /// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the
+    /// data is written incorrectly and will always fail to resume the download. For more information about the bug and
+    /// possible workarounds, please refer to the following Stack Overflow post:
+    ///
+    ///    - http://stackoverflow.com/a/39347461/1342462
+    ///
+    /// - parameter resumeData:  The resume data. This is an opaque data blob produced by `URLSessionDownloadTask`
+    ///                          when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for
+    ///                          additional information.
+    /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
+    ///
+    /// - returns: The created `DownloadRequest`.
+    @discardableResult
+    open func download(
+        resumingWith resumeData: Data,
+        to destination: DownloadRequest.DownloadFileDestination? = nil)
+        -> DownloadRequest
+    {
+        return download(.resumeData(resumeData), to: destination)
+    }
+
+    // MARK: Private - Download Implementation
+
+    private func download(
+        _ downloadable: DownloadRequest.Downloadable,
+        to destination: DownloadRequest.DownloadFileDestination?)
+        -> DownloadRequest
+    {
+        do {
+            let task = try downloadable.task(session: session, adapter: adapter, queue: queue)
+            let download = DownloadRequest(session: session, requestTask: .download(downloadable, task))
+
+            download.downloadDelegate.destination = destination
+
+            delegate[task] = download
+
+            if startRequestsImmediately { download.resume() }
+
+            return download
+        } catch {
+            return download(downloadable, to: destination, failedWith: error)
+        }
+    }
+
+    private func download(
+        _ downloadable: DownloadRequest.Downloadable?,
+        to destination: DownloadRequest.DownloadFileDestination?,
+        failedWith error: Error)
+        -> DownloadRequest
+    {
+        var downloadTask: Request.RequestTask = .download(nil, nil)
+
+        if let downloadable = downloadable {
+            downloadTask = .download(downloadable, nil)
+        }
+
+        let underlyingError = error.underlyingAdaptError ?? error
+
+        let download = DownloadRequest(session: session, requestTask: downloadTask, error: underlyingError)
+        download.downloadDelegate.destination = destination
+
+        if let retrier = retrier, error is AdaptError {
+            allowRetrier(retrier, toRetry: download, with: underlyingError)
+        } else {
+            if startRequestsImmediately { download.resume() }
+        }
+
+        return download
+    }
+
+    // MARK: - Upload Request
+
+    // MARK: File
+
+    /// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `file`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter file:    The file to upload.
+    /// - parameter url:     The URL.
+    /// - parameter method:  The HTTP method. `.post` by default.
+    /// - parameter headers: The HTTP headers. `nil` by default.
+    ///
+    /// - returns: The created `UploadRequest`.
+    @discardableResult
+    open func upload(
+        _ fileURL: URL,
+        to url: URLConvertible,
+        method: HTTPMethod = .post,
+        headers: HTTPHeaders? = nil)
+        -> UploadRequest
+    {
+        do {
+            let urlRequest = try URLRequest(url: url, method: method, headers: headers)
+            return upload(fileURL, with: urlRequest)
+        } catch {
+            return upload(nil, failedWith: error)
+        }
+    }
+
+    /// Creates a `UploadRequest` from the specified `urlRequest` for uploading the `file`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter file:       The file to upload.
+    /// - parameter urlRequest: The URL request.
+    ///
+    /// - returns: The created `UploadRequest`.
+    @discardableResult
+    open func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest {
+        do {
+            let urlRequest = try urlRequest.asURLRequest()
+            return upload(.file(fileURL, urlRequest))
+        } catch {
+            return upload(nil, failedWith: error)
+        }
+    }
+
+    // MARK: Data
+
+    /// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `data`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter data:    The data to upload.
+    /// - parameter url:     The URL.
+    /// - parameter method:  The HTTP method. `.post` by default.
+    /// - parameter headers: The HTTP headers. `nil` by default.
+    ///
+    /// - returns: The created `UploadRequest`.
+    @discardableResult
+    open func upload(
+        _ data: Data,
+        to url: URLConvertible,
+        method: HTTPMethod = .post,
+        headers: HTTPHeaders? = nil)
+        -> UploadRequest
+    {
+        do {
+            let urlRequest = try URLRequest(url: url, method: method, headers: headers)
+            return upload(data, with: urlRequest)
+        } catch {
+            return upload(nil, failedWith: error)
+        }
+    }
+
+    /// Creates an `UploadRequest` from the specified `urlRequest` for uploading the `data`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter data:       The data to upload.
+    /// - parameter urlRequest: The URL request.
+    ///
+    /// - returns: The created `UploadRequest`.
+    @discardableResult
+    open func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest {
+        do {
+            let urlRequest = try urlRequest.asURLRequest()
+            return upload(.data(data, urlRequest))
+        } catch {
+            return upload(nil, failedWith: error)
+        }
+    }
+
+    // MARK: InputStream
+
+    /// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `stream`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter stream:  The stream to upload.
+    /// - parameter url:     The URL.
+    /// - parameter method:  The HTTP method. `.post` by default.
+    /// - parameter headers: The HTTP headers. `nil` by default.
+    ///
+    /// - returns: The created `UploadRequest`.
+    @discardableResult
+    open func upload(
+        _ stream: InputStream,
+        to url: URLConvertible,
+        method: HTTPMethod = .post,
+        headers: HTTPHeaders? = nil)
+        -> UploadRequest
+    {
+        do {
+            let urlRequest = try URLRequest(url: url, method: method, headers: headers)
+            return upload(stream, with: urlRequest)
+        } catch {
+            return upload(nil, failedWith: error)
+        }
+    }
+
+    /// Creates an `UploadRequest` from the specified `urlRequest` for uploading the `stream`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter stream:     The stream to upload.
+    /// - parameter urlRequest: The URL request.
+    ///
+    /// - returns: The created `UploadRequest`.
+    @discardableResult
+    open func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest {
+        do {
+            let urlRequest = try urlRequest.asURLRequest()
+            return upload(.stream(stream, urlRequest))
+        } catch {
+            return upload(nil, failedWith: error)
+        }
+    }
+
+    // MARK: MultipartFormData
+
+    /// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new
+    /// `UploadRequest` using the `url`, `method` and `headers`.
+    ///
+    /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
+    /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
+    /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
+    /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
+    /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
+    /// used for larger payloads such as video content.
+    ///
+    /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
+    /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
+    /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
+    /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
+    /// technique was used.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter multipartFormData:       The closure used to append body parts to the `MultipartFormData`.
+    /// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
+    ///                                      `multipartFormDataEncodingMemoryThreshold` by default.
+    /// - parameter url:                     The URL.
+    /// - parameter method:                  The HTTP method. `.post` by default.
+    /// - parameter headers:                 The HTTP headers. `nil` by default.
+    /// - parameter encodingCompletion:      The closure called when the `MultipartFormData` encoding is complete.
+    open func upload(
+        multipartFormData: @escaping (MultipartFormData) -> Void,
+        usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
+        to url: URLConvertible,
+        method: HTTPMethod = .post,
+        headers: HTTPHeaders? = nil,
+        queue: DispatchQueue? = nil,
+        encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?)
+    {
+        do {
+            let urlRequest = try URLRequest(url: url, method: method, headers: headers)
+
+            return upload(
+                multipartFormData: multipartFormData,
+                usingThreshold: encodingMemoryThreshold,
+                with: urlRequest,
+                queue: queue,
+                encodingCompletion: encodingCompletion
+            )
+        } catch {
+            (queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) }
+        }
+    }
+
+    /// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new
+    /// `UploadRequest` using the `urlRequest`.
+    ///
+    /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
+    /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
+    /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
+    /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
+    /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
+    /// used for larger payloads such as video content.
+    ///
+    /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
+    /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
+    /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
+    /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
+    /// technique was used.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter multipartFormData:       The closure used to append body parts to the `MultipartFormData`.
+    /// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
+    ///                                      `multipartFormDataEncodingMemoryThreshold` by default.
+    /// - parameter urlRequest:              The URL request.
+    /// - parameter encodingCompletion:      The closure called when the `MultipartFormData` encoding is complete.
+    open func upload(
+        multipartFormData: @escaping (MultipartFormData) -> Void,
+        usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
+        with urlRequest: URLRequestConvertible,
+        queue: DispatchQueue? = nil,
+        encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?)
+    {
+        DispatchQueue.global(qos: .utility).async {
+            let formData = MultipartFormData()
+            multipartFormData(formData)
+
+            var tempFileURL: URL?
+
+            do {
+                var urlRequestWithContentType = try urlRequest.asURLRequest()
+                urlRequestWithContentType.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
+
+                let isBackgroundSession = self.session.configuration.identifier != nil
+
+                if formData.contentLength < encodingMemoryThreshold && !isBackgroundSession {
+                    let data = try formData.encode()
+
+                    let encodingResult = MultipartFormDataEncodingResult.success(
+                        request: self.upload(data, with: urlRequestWithContentType),
+                        streamingFromDisk: false,
+                        streamFileURL: nil
+                    )
+
+                    (queue ?? DispatchQueue.main).async { encodingCompletion?(encodingResult) }
+                } else {
+                    let fileManager = FileManager.default
+                    let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory())
+                    let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data")
+                    let fileName = UUID().uuidString
+                    let fileURL = directoryURL.appendingPathComponent(fileName)
+
+                    tempFileURL = fileURL
+
+                    var directoryError: Error?
+
+                    // Create directory inside serial queue to ensure two threads don't do this in parallel
+                    self.queue.sync {
+                        do {
+                            try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
+                        } catch {
+                            directoryError = error
+                        }
+                    }
+
+                    if let directoryError = directoryError { throw directoryError }
+
+                    try formData.writeEncodedData(to: fileURL)
+
+                    let upload = self.upload(fileURL, with: urlRequestWithContentType)
+
+                    // Cleanup the temp file once the upload is complete
+                    upload.delegate.queue.addOperation {
+                        do {
+                            try FileManager.default.removeItem(at: fileURL)
+                        } catch {
+                            // No-op
+                        }
+                    }
+
+                    (queue ?? DispatchQueue.main).async {
+                        let encodingResult = MultipartFormDataEncodingResult.success(
+                            request: upload,
+                            streamingFromDisk: true,
+                            streamFileURL: fileURL
+                        )
+
+                        encodingCompletion?(encodingResult)
+                    }
+                }
+            } catch {
+                // Cleanup the temp file in the event that the multipart form data encoding failed
+                if let tempFileURL = tempFileURL {
+                    do {
+                        try FileManager.default.removeItem(at: tempFileURL)
+                    } catch {
+                        // No-op
+                    }
+                }
+
+                (queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) }
+            }
+        }
+    }
+
+    // MARK: Private - Upload Implementation
+
+    private func upload(_ uploadable: UploadRequest.Uploadable) -> UploadRequest {
+        do {
+            let task = try uploadable.task(session: session, adapter: adapter, queue: queue)
+            let upload = UploadRequest(session: session, requestTask: .upload(uploadable, task))
+
+            if case let .stream(inputStream, _) = uploadable {
+                upload.delegate.taskNeedNewBodyStream = { _, _ in inputStream }
+            }
+
+            delegate[task] = upload
+
+            if startRequestsImmediately { upload.resume() }
+
+            return upload
+        } catch {
+            return upload(uploadable, failedWith: error)
+        }
+    }
+
+    private func upload(_ uploadable: UploadRequest.Uploadable?, failedWith error: Error) -> UploadRequest {
+        var uploadTask: Request.RequestTask = .upload(nil, nil)
+
+        if let uploadable = uploadable {
+            uploadTask = .upload(uploadable, nil)
+        }
+
+        let underlyingError = error.underlyingAdaptError ?? error
+        let upload = UploadRequest(session: session, requestTask: uploadTask, error: underlyingError)
+
+        if let retrier = retrier, error is AdaptError {
+            allowRetrier(retrier, toRetry: upload, with: underlyingError)
+        } else {
+            if startRequestsImmediately { upload.resume() }
+        }
+
+        return upload
+    }
+
+#if !os(watchOS)
+
+    // MARK: - Stream Request
+
+    // MARK: Hostname and Port
+
+    /// Creates a `StreamRequest` for bidirectional streaming using the `hostname` and `port`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter hostName: The hostname of the server to connect to.
+    /// - parameter port:     The port of the server to connect to.
+    ///
+    /// - returns: The created `StreamRequest`.
+    @discardableResult
+    @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+    open func stream(withHostName hostName: String, port: Int) -> StreamRequest {
+        return stream(.stream(hostName: hostName, port: port))
+    }
+
+    // MARK: NetService
+
+    /// Creates a `StreamRequest` for bidirectional streaming using the `netService`.
+    ///
+    /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
+    ///
+    /// - parameter netService: The net service used to identify the endpoint.
+    ///
+    /// - returns: The created `StreamRequest`.
+    @discardableResult
+    @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+    open func stream(with netService: NetService) -> StreamRequest {
+        return stream(.netService(netService))
+    }
+
+    // MARK: Private - Stream Implementation
+
+    @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+    private func stream(_ streamable: StreamRequest.Streamable) -> StreamRequest {
+        do {
+            let task = try streamable.task(session: session, adapter: adapter, queue: queue)
+            let request = StreamRequest(session: session, requestTask: .stream(streamable, task))
+
+            delegate[task] = request
+
+            if startRequestsImmediately { request.resume() }
+
+            return request
+        } catch {
+            return stream(failedWith: error)
+        }
+    }
+
+    @available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
+    private func stream(failedWith error: Error) -> StreamRequest {
+        let stream = StreamRequest(session: session, requestTask: .stream(nil, nil), error: error)
+        if startRequestsImmediately { stream.resume() }
+        return stream
+    }
+
+#endif
+
+    // MARK: - Internal - Retry Request
+
+    func retry(_ request: Request) -> Bool {
+        guard let originalTask = request.originalTask else { return false }
+
+        do {
+            let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
+
+            if let originalTask = request.task {
+                delegate[originalTask] = nil // removes the old request to avoid endless growth
+            }
+
+            request.delegate.task = task // resets all task delegate data
+
+            request.retryCount += 1
+            request.startTime = CFAbsoluteTimeGetCurrent()
+            request.endTime = nil
+
+            task.resume()
+
+            return true
+        } catch {
+            request.delegate.error = error.underlyingAdaptError ?? error
+            return false
+        }
+    }
+
+    private func allowRetrier(_ retrier: RequestRetrier, toRetry request: Request, with error: Error) {
+        DispatchQueue.utility.async { [weak self] in
+            guard let strongSelf = self else { return }
+
+            retrier.should(strongSelf, retry: request, with: error) { shouldRetry, timeDelay in
+                guard let strongSelf = self else { return }
+
+                guard shouldRetry else {
+                    if strongSelf.startRequestsImmediately { request.resume() }
+                    return
+                }
+
+                DispatchQueue.utility.after(timeDelay) {
+                    guard let strongSelf = self else { return }
+
+                    let retrySucceeded = strongSelf.retry(request)
+
+                    if retrySucceeded, let task = request.task {
+                        strongSelf.delegate[task] = request
+                    } else {
+                        if strongSelf.startRequestsImmediately { request.resume() }
+                    }
+                }
+            }
+        }
+    }
+}

+ 466 - 0
Example/Pods/Alamofire/Source/TaskDelegate.swift

@@ -0,0 +1,466 @@
+//
+//  TaskDelegate.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// The task delegate is responsible for handling all delegate callbacks for the underlying task as well as
+/// executing all operations attached to the serial operation queue upon task completion.
+open class TaskDelegate: NSObject {
+
+    // MARK: Properties
+
+    /// The serial operation queue used to execute all operations after the task completes.
+    public let queue: OperationQueue
+
+    /// The data returned by the server.
+    public var data: Data? { return nil }
+
+    /// The error generated throughout the lifecyle of the task.
+    public var error: Error?
+
+    var task: URLSessionTask? {
+        set {
+            taskLock.lock(); defer { taskLock.unlock() }
+            _task = newValue
+        }
+        get {
+            taskLock.lock(); defer { taskLock.unlock() }
+            return _task
+        }
+    }
+
+    var initialResponseTime: CFAbsoluteTime?
+    var credential: URLCredential?
+    var metrics: AnyObject? // URLSessionTaskMetrics
+
+    private var _task: URLSessionTask? {
+        didSet { reset() }
+    }
+
+    private let taskLock = NSLock()
+
+    // MARK: Lifecycle
+
+    init(task: URLSessionTask?) {
+        _task = task
+
+        self.queue = {
+            let operationQueue = OperationQueue()
+
+            operationQueue.maxConcurrentOperationCount = 1
+            operationQueue.isSuspended = true
+            operationQueue.qualityOfService = .utility
+
+            return operationQueue
+        }()
+    }
+
+    func reset() {
+        error = nil
+        initialResponseTime = nil
+    }
+
+    // MARK: URLSessionTaskDelegate
+
+    var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
+    var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
+    var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
+    var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)?
+
+    @objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)
+    func urlSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        willPerformHTTPRedirection response: HTTPURLResponse,
+        newRequest request: URLRequest,
+        completionHandler: @escaping (URLRequest?) -> Void)
+    {
+        var redirectRequest: URLRequest? = request
+
+        if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
+            redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
+        }
+
+        completionHandler(redirectRequest)
+    }
+
+    @objc(URLSession:task:didReceiveChallenge:completionHandler:)
+    func urlSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        didReceive challenge: URLAuthenticationChallenge,
+        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
+    {
+        var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
+        var credential: URLCredential?
+
+        if let taskDidReceiveChallenge = taskDidReceiveChallenge {
+            (disposition, credential) = taskDidReceiveChallenge(session, task, challenge)
+        } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
+            let host = challenge.protectionSpace.host
+
+            if
+                let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
+                let serverTrust = challenge.protectionSpace.serverTrust
+            {
+                if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
+                    disposition = .useCredential
+                    credential = URLCredential(trust: serverTrust)
+                } else {
+                    disposition = .cancelAuthenticationChallenge
+                }
+            }
+        } else {
+            if challenge.previousFailureCount > 0 {
+                disposition = .rejectProtectionSpace
+            } else {
+                credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace)
+
+                if credential != nil {
+                    disposition = .useCredential
+                }
+            }
+        }
+
+        completionHandler(disposition, credential)
+    }
+
+    @objc(URLSession:task:needNewBodyStream:)
+    func urlSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
+    {
+        var bodyStream: InputStream?
+
+        if let taskNeedNewBodyStream = taskNeedNewBodyStream {
+            bodyStream = taskNeedNewBodyStream(session, task)
+        }
+
+        completionHandler(bodyStream)
+    }
+
+    @objc(URLSession:task:didCompleteWithError:)
+    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+        if let taskDidCompleteWithError = taskDidCompleteWithError {
+            taskDidCompleteWithError(session, task, error)
+        } else {
+            if let error = error {
+                if self.error == nil { self.error = error }
+
+                if
+                    let downloadDelegate = self as? DownloadTaskDelegate,
+                    let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
+                {
+                    downloadDelegate.resumeData = resumeData
+                }
+            }
+
+            queue.isSuspended = false
+        }
+    }
+}
+
+// MARK: -
+
+class DataTaskDelegate: TaskDelegate, URLSessionDataDelegate {
+
+    // MARK: Properties
+
+    var dataTask: URLSessionDataTask { return task as! URLSessionDataTask }
+
+    override var data: Data? {
+        if dataStream != nil {
+            return nil
+        } else {
+            return mutableData
+        }
+    }
+
+    var progress: Progress
+    var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
+
+    var dataStream: ((_ data: Data) -> Void)?
+
+    private var totalBytesReceived: Int64 = 0
+    private var mutableData: Data
+
+    private var expectedContentLength: Int64?
+
+    // MARK: Lifecycle
+
+    override init(task: URLSessionTask?) {
+        mutableData = Data()
+        progress = Progress(totalUnitCount: 0)
+
+        super.init(task: task)
+    }
+
+    override func reset() {
+        super.reset()
+
+        progress = Progress(totalUnitCount: 0)
+        totalBytesReceived = 0
+        mutableData = Data()
+        expectedContentLength = nil
+    }
+
+    // MARK: URLSessionDataDelegate
+
+    var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
+    var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
+    var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
+    var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
+
+    func urlSession(
+        _ session: URLSession,
+        dataTask: URLSessionDataTask,
+        didReceive response: URLResponse,
+        completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
+    {
+        var disposition: URLSession.ResponseDisposition = .allow
+
+        expectedContentLength = response.expectedContentLength
+
+        if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
+            disposition = dataTaskDidReceiveResponse(session, dataTask, response)
+        }
+
+        completionHandler(disposition)
+    }
+
+    func urlSession(
+        _ session: URLSession,
+        dataTask: URLSessionDataTask,
+        didBecome downloadTask: URLSessionDownloadTask)
+    {
+        dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask)
+    }
+
+    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
+        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
+
+        if let dataTaskDidReceiveData = dataTaskDidReceiveData {
+            dataTaskDidReceiveData(session, dataTask, data)
+        } else {
+            if let dataStream = dataStream {
+                dataStream(data)
+            } else {
+                mutableData.append(data)
+            }
+
+            let bytesReceived = Int64(data.count)
+            totalBytesReceived += bytesReceived
+            let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
+
+            progress.totalUnitCount = totalBytesExpected
+            progress.completedUnitCount = totalBytesReceived
+
+            if let progressHandler = progressHandler {
+                progressHandler.queue.async { progressHandler.closure(self.progress) }
+            }
+        }
+    }
+
+    func urlSession(
+        _ session: URLSession,
+        dataTask: URLSessionDataTask,
+        willCacheResponse proposedResponse: CachedURLResponse,
+        completionHandler: @escaping (CachedURLResponse?) -> Void)
+    {
+        var cachedResponse: CachedURLResponse? = proposedResponse
+
+        if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
+            cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse)
+        }
+
+        completionHandler(cachedResponse)
+    }
+}
+
+// MARK: -
+
+class DownloadTaskDelegate: TaskDelegate, URLSessionDownloadDelegate {
+
+    // MARK: Properties
+
+    var downloadTask: URLSessionDownloadTask { return task as! URLSessionDownloadTask }
+
+    var progress: Progress
+    var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
+
+    var resumeData: Data?
+    override var data: Data? { return resumeData }
+
+    var destination: DownloadRequest.DownloadFileDestination?
+
+    var temporaryURL: URL?
+    var destinationURL: URL?
+
+    var fileURL: URL? { return destination != nil ? destinationURL : temporaryURL }
+
+    // MARK: Lifecycle
+
+    override init(task: URLSessionTask?) {
+        progress = Progress(totalUnitCount: 0)
+        super.init(task: task)
+    }
+
+    override func reset() {
+        super.reset()
+
+        progress = Progress(totalUnitCount: 0)
+        resumeData = nil
+    }
+
+    // MARK: URLSessionDownloadDelegate
+
+    var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)?
+    var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
+    var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
+
+    func urlSession(
+        _ session: URLSession,
+        downloadTask: URLSessionDownloadTask,
+        didFinishDownloadingTo location: URL)
+    {
+        temporaryURL = location
+
+        guard
+            let destination = destination,
+            let response = downloadTask.response as? HTTPURLResponse
+        else { return }
+
+        let result = destination(location, response)
+        let destinationURL = result.destinationURL
+        let options = result.options
+
+        self.destinationURL = destinationURL
+
+        do {
+            if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) {
+                try FileManager.default.removeItem(at: destinationURL)
+            }
+
+            if options.contains(.createIntermediateDirectories) {
+                let directory = destinationURL.deletingLastPathComponent()
+                try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+            }
+
+            try FileManager.default.moveItem(at: location, to: destinationURL)
+        } catch {
+            self.error = error
+        }
+    }
+
+    func urlSession(
+        _ session: URLSession,
+        downloadTask: URLSessionDownloadTask,
+        didWriteData bytesWritten: Int64,
+        totalBytesWritten: Int64,
+        totalBytesExpectedToWrite: Int64)
+    {
+        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
+
+        if let downloadTaskDidWriteData = downloadTaskDidWriteData {
+            downloadTaskDidWriteData(
+                session,
+                downloadTask,
+                bytesWritten,
+                totalBytesWritten,
+                totalBytesExpectedToWrite
+            )
+        } else {
+            progress.totalUnitCount = totalBytesExpectedToWrite
+            progress.completedUnitCount = totalBytesWritten
+
+            if let progressHandler = progressHandler {
+                progressHandler.queue.async { progressHandler.closure(self.progress) }
+            }
+        }
+    }
+
+    func urlSession(
+        _ session: URLSession,
+        downloadTask: URLSessionDownloadTask,
+        didResumeAtOffset fileOffset: Int64,
+        expectedTotalBytes: Int64)
+    {
+        if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
+            downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
+        } else {
+            progress.totalUnitCount = expectedTotalBytes
+            progress.completedUnitCount = fileOffset
+        }
+    }
+}
+
+// MARK: -
+
+class UploadTaskDelegate: DataTaskDelegate {
+
+    // MARK: Properties
+
+    var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask }
+
+    var uploadProgress: Progress
+    var uploadProgressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
+
+    // MARK: Lifecycle
+
+    override init(task: URLSessionTask?) {
+        uploadProgress = Progress(totalUnitCount: 0)
+        super.init(task: task)
+    }
+
+    override func reset() {
+        super.reset()
+        uploadProgress = Progress(totalUnitCount: 0)
+    }
+
+    // MARK: URLSessionTaskDelegate
+
+    var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
+
+    func URLSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        didSendBodyData bytesSent: Int64,
+        totalBytesSent: Int64,
+        totalBytesExpectedToSend: Int64)
+    {
+        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
+
+        if let taskDidSendBodyData = taskDidSendBodyData {
+            taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
+        } else {
+            uploadProgress.totalUnitCount = totalBytesExpectedToSend
+            uploadProgress.completedUnitCount = totalBytesSent
+
+            if let uploadProgressHandler = uploadProgressHandler {
+                uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) }
+            }
+        }
+    }
+}

+ 136 - 0
Example/Pods/Alamofire/Source/Timeline.swift

@@ -0,0 +1,136 @@
+//
+//  Timeline.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+/// Responsible for computing the timing metrics for the complete lifecycle of a `Request`.
+public struct Timeline {
+    /// The time the request was initialized.
+    public let requestStartTime: CFAbsoluteTime
+
+    /// The time the first bytes were received from or sent to the server.
+    public let initialResponseTime: CFAbsoluteTime
+
+    /// The time when the request was completed.
+    public let requestCompletedTime: CFAbsoluteTime
+
+    /// The time when the response serialization was completed.
+    public let serializationCompletedTime: CFAbsoluteTime
+
+    /// The time interval in seconds from the time the request started to the initial response from the server.
+    public let latency: TimeInterval
+
+    /// The time interval in seconds from the time the request started to the time the request completed.
+    public let requestDuration: TimeInterval
+
+    /// The time interval in seconds from the time the request completed to the time response serialization completed.
+    public let serializationDuration: TimeInterval
+
+    /// The time interval in seconds from the time the request started to the time response serialization completed.
+    public let totalDuration: TimeInterval
+
+    /// Creates a new `Timeline` instance with the specified request times.
+    ///
+    /// - parameter requestStartTime:           The time the request was initialized. Defaults to `0.0`.
+    /// - parameter initialResponseTime:        The time the first bytes were received from or sent to the server.
+    ///                                         Defaults to `0.0`.
+    /// - parameter requestCompletedTime:       The time when the request was completed. Defaults to `0.0`.
+    /// - parameter serializationCompletedTime: The time when the response serialization was completed. Defaults
+    ///                                         to `0.0`.
+    ///
+    /// - returns: The new `Timeline` instance.
+    public init(
+        requestStartTime: CFAbsoluteTime = 0.0,
+        initialResponseTime: CFAbsoluteTime = 0.0,
+        requestCompletedTime: CFAbsoluteTime = 0.0,
+        serializationCompletedTime: CFAbsoluteTime = 0.0)
+    {
+        self.requestStartTime = requestStartTime
+        self.initialResponseTime = initialResponseTime
+        self.requestCompletedTime = requestCompletedTime
+        self.serializationCompletedTime = serializationCompletedTime
+
+        self.latency = initialResponseTime - requestStartTime
+        self.requestDuration = requestCompletedTime - requestStartTime
+        self.serializationDuration = serializationCompletedTime - requestCompletedTime
+        self.totalDuration = serializationCompletedTime - requestStartTime
+    }
+}
+
+// MARK: - CustomStringConvertible
+
+extension Timeline: CustomStringConvertible {
+    /// The textual representation used when written to an output stream, which includes the latency, the request
+    /// duration and the total duration.
+    public var description: String {
+        let latency = String(format: "%.3f", self.latency)
+        let requestDuration = String(format: "%.3f", self.requestDuration)
+        let serializationDuration = String(format: "%.3f", self.serializationDuration)
+        let totalDuration = String(format: "%.3f", self.totalDuration)
+
+        // NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is
+        // fixed, we should move back to string interpolation by reverting commit 7d4a43b1.
+        let timings = [
+            "\"Latency\": " + latency + " secs",
+            "\"Request Duration\": " + requestDuration + " secs",
+            "\"Serialization Duration\": " + serializationDuration + " secs",
+            "\"Total Duration\": " + totalDuration + " secs"
+        ]
+
+        return "Timeline: { " + timings.joined(separator: ", ") + " }"
+    }
+}
+
+// MARK: - CustomDebugStringConvertible
+
+extension Timeline: CustomDebugStringConvertible {
+    /// The textual representation used when written to an output stream, which includes the request start time, the
+    /// initial response time, the request completed time, the serialization completed time, the latency, the request
+    /// duration and the total duration.
+    public var debugDescription: String {
+        let requestStartTime = String(format: "%.3f", self.requestStartTime)
+        let initialResponseTime = String(format: "%.3f", self.initialResponseTime)
+        let requestCompletedTime = String(format: "%.3f", self.requestCompletedTime)
+        let serializationCompletedTime = String(format: "%.3f", self.serializationCompletedTime)
+        let latency = String(format: "%.3f", self.latency)
+        let requestDuration = String(format: "%.3f", self.requestDuration)
+        let serializationDuration = String(format: "%.3f", self.serializationDuration)
+        let totalDuration = String(format: "%.3f", self.totalDuration)
+
+        // NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is
+        // fixed, we should move back to string interpolation by reverting commit 7d4a43b1.
+        let timings = [
+            "\"Request Start Time\": " + requestStartTime,
+            "\"Initial Response Time\": " + initialResponseTime,
+            "\"Request Completed Time\": " + requestCompletedTime,
+            "\"Serialization Completed Time\": " + serializationCompletedTime,
+            "\"Latency\": " + latency + " secs",
+            "\"Request Duration\": " + requestDuration + " secs",
+            "\"Serialization Duration\": " + serializationDuration + " secs",
+            "\"Total Duration\": " + totalDuration + " secs"
+        ]
+
+        return "Timeline: { " + timings.joined(separator: ", ") + " }"
+    }
+}

+ 321 - 0
Example/Pods/Alamofire/Source/Validation.swift

@@ -0,0 +1,321 @@
+//
+//  Validation.swift
+//
+//  Copyright (c) 2014 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension Request {
+
+    // MARK: Helper Types
+
+    fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason
+
+    /// Used to represent whether validation was successful or encountered an error resulting in a failure.
+    ///
+    /// - success: The validation was successful.
+    /// - failure: The validation failed encountering the provided error.
+    public enum ValidationResult {
+        case success
+        case failure(Error)
+    }
+
+    fileprivate struct MIMEType {
+        let type: String
+        let subtype: String
+
+        var isWildcard: Bool { return type == "*" && subtype == "*" }
+
+        init?(_ string: String) {
+            let components: [String] = {
+                let stripped = string.trimmingCharacters(in: .whitespacesAndNewlines)
+
+            #if swift(>=3.2)
+                let split = stripped[..<(stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)]
+            #else
+                let split = stripped.substring(to: stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)
+            #endif
+
+                return split.components(separatedBy: "/")
+            }()
+
+            if let type = components.first, let subtype = components.last {
+                self.type = type
+                self.subtype = subtype
+            } else {
+                return nil
+            }
+        }
+
+        func matches(_ mime: MIMEType) -> Bool {
+            switch (type, subtype) {
+            case (mime.type, mime.subtype), (mime.type, "*"), ("*", mime.subtype), ("*", "*"):
+                return true
+            default:
+                return false
+            }
+        }
+    }
+
+    // MARK: Properties
+
+    fileprivate var acceptableStatusCodes: [Int] { return Array(200..<300) }
+
+    fileprivate var acceptableContentTypes: [String] {
+        if let accept = request?.value(forHTTPHeaderField: "Accept") {
+            return accept.components(separatedBy: ",")
+        }
+
+        return ["*/*"]
+    }
+
+    // MARK: Status Code
+
+    fileprivate func validate<S: Sequence>(
+        statusCode acceptableStatusCodes: S,
+        response: HTTPURLResponse)
+        -> ValidationResult
+        where S.Iterator.Element == Int
+    {
+        if acceptableStatusCodes.contains(response.statusCode) {
+            return .success
+        } else {
+            let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode)
+            return .failure(AFError.responseValidationFailed(reason: reason))
+        }
+    }
+
+    // MARK: Content Type
+
+    fileprivate func validate<S: Sequence>(
+        contentType acceptableContentTypes: S,
+        response: HTTPURLResponse,
+        data: Data?)
+        -> ValidationResult
+        where S.Iterator.Element == String
+    {
+        guard let data = data, data.count > 0 else { return .success }
+
+        guard
+            let responseContentType = response.mimeType,
+            let responseMIMEType = MIMEType(responseContentType)
+        else {
+            for contentType in acceptableContentTypes {
+                if let mimeType = MIMEType(contentType), mimeType.isWildcard {
+                    return .success
+                }
+            }
+
+            let error: AFError = {
+                let reason: ErrorReason = .missingContentType(acceptableContentTypes: Array(acceptableContentTypes))
+                return AFError.responseValidationFailed(reason: reason)
+            }()
+
+            return .failure(error)
+        }
+
+        for contentType in acceptableContentTypes {
+            if let acceptableMIMEType = MIMEType(contentType), acceptableMIMEType.matches(responseMIMEType) {
+                return .success
+            }
+        }
+
+        let error: AFError = {
+            let reason: ErrorReason = .unacceptableContentType(
+                acceptableContentTypes: Array(acceptableContentTypes),
+                responseContentType: responseContentType
+            )
+
+            return AFError.responseValidationFailed(reason: reason)
+        }()
+
+        return .failure(error)
+    }
+}
+
+// MARK: -
+
+extension DataRequest {
+    /// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the
+    /// request was valid.
+    public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult
+
+    /// Validates the request, using the specified closure.
+    ///
+    /// If validation fails, subsequent calls to response handlers will have an associated error.
+    ///
+    /// - parameter validation: A closure to validate the request.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func validate(_ validation: @escaping Validation) -> Self {
+        let validationExecution: () -> Void = { [unowned self] in
+            if
+                let response = self.response,
+                self.delegate.error == nil,
+                case let .failure(error) = validation(self.request, response, self.delegate.data)
+            {
+                self.delegate.error = error
+            }
+        }
+
+        validations.append(validationExecution)
+
+        return self
+    }
+
+    /// Validates that the response has a status code in the specified sequence.
+    ///
+    /// If validation fails, subsequent calls to response handlers will have an associated error.
+    ///
+    /// - parameter range: The range of acceptable status codes.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
+        return validate { [unowned self] _, response, _ in
+            return self.validate(statusCode: acceptableStatusCodes, response: response)
+        }
+    }
+
+    /// Validates that the response has a content type in the specified sequence.
+    ///
+    /// If validation fails, subsequent calls to response handlers will have an associated error.
+    ///
+    /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func validate<S: Sequence>(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String {
+        return validate { [unowned self] _, response, data in
+            return self.validate(contentType: acceptableContentTypes, response: response, data: data)
+        }
+    }
+
+    /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
+    /// type matches any specified in the Accept HTTP header field.
+    ///
+    /// If validation fails, subsequent calls to response handlers will have an associated error.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func validate() -> Self {
+        let contentTypes = { [unowned self] in
+            self.acceptableContentTypes
+        }
+        return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())
+    }
+}
+
+// MARK: -
+
+extension DownloadRequest {
+    /// A closure used to validate a request that takes a URL request, a URL response, a temporary URL and a
+    /// destination URL, and returns whether the request was valid.
+    public typealias Validation = (
+        _ request: URLRequest?,
+        _ response: HTTPURLResponse,
+        _ temporaryURL: URL?,
+        _ destinationURL: URL?)
+        -> ValidationResult
+
+    /// Validates the request, using the specified closure.
+    ///
+    /// If validation fails, subsequent calls to response handlers will have an associated error.
+    ///
+    /// - parameter validation: A closure to validate the request.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func validate(_ validation: @escaping Validation) -> Self {
+        let validationExecution: () -> Void = { [unowned self] in
+            let request = self.request
+            let temporaryURL = self.downloadDelegate.temporaryURL
+            let destinationURL = self.downloadDelegate.destinationURL
+
+            if
+                let response = self.response,
+                self.delegate.error == nil,
+                case let .failure(error) = validation(request, response, temporaryURL, destinationURL)
+            {
+                self.delegate.error = error
+            }
+        }
+
+        validations.append(validationExecution)
+
+        return self
+    }
+
+    /// Validates that the response has a status code in the specified sequence.
+    ///
+    /// If validation fails, subsequent calls to response handlers will have an associated error.
+    ///
+    /// - parameter range: The range of acceptable status codes.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
+        return validate { [unowned self] _, response, _, _ in
+            return self.validate(statusCode: acceptableStatusCodes, response: response)
+        }
+    }
+
+    /// Validates that the response has a content type in the specified sequence.
+    ///
+    /// If validation fails, subsequent calls to response handlers will have an associated error.
+    ///
+    /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func validate<S: Sequence>(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String {
+        return validate { [unowned self] _, response, _, _ in
+            let fileURL = self.downloadDelegate.fileURL
+
+            guard let validFileURL = fileURL else {
+                return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
+            }
+
+            do {
+                let data = try Data(contentsOf: validFileURL)
+                return self.validate(contentType: acceptableContentTypes, response: response, data: data)
+            } catch {
+                return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: validFileURL)))
+            }
+        }
+    }
+
+    /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
+    /// type matches any specified in the Accept HTTP header field.
+    ///
+    /// If validation fails, subsequent calls to response handlers will have an associated error.
+    ///
+    /// - returns: The request.
+    @discardableResult
+    public func validate() -> Self {
+        let contentTypes = { [unowned self] in
+            self.acceptableContentTypes
+        }
+        return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())
+    }
+}

+ 58 - 0
Example/Pods/FDFullscreenPopGesture/FDFullscreenPopGesture/UINavigationController+FDFullscreenPopGesture.h

@@ -0,0 +1,58 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2015-2016 forkingdog ( https://github.com/forkingdog )
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#import <UIKit/UIKit.h>
+
+/// "UINavigation+FDFullscreenPopGesture" extends UINavigationController's swipe-
+/// to-pop behavior in iOS 7+ by supporting fullscreen pan gesture. Instead of
+/// screen edge, you can now swipe from any place on the screen and the onboard
+/// interactive pop transition works seamlessly.
+///
+/// Adding the implementation file of this category to your target will
+/// automatically patch UINavigationController with this feature.
+@interface UINavigationController (FDFullscreenPopGesture)
+
+/// The gesture recognizer that actually handles interactive pop.
+@property (nonatomic, strong, readonly) UIPanGestureRecognizer *fd_fullscreenPopGestureRecognizer;
+
+/// A view controller is able to control navigation bar's appearance by itself,
+/// rather than a global way, checking "fd_prefersNavigationBarHidden" property.
+/// Default to YES, disable it if you don't want so.
+@property (nonatomic, assign) BOOL fd_viewControllerBasedNavigationBarAppearanceEnabled;
+
+@end
+
+/// Allows any view controller to disable interactive pop gesture, which might
+/// be necessary when the view controller itself handles pan gesture in some
+/// cases.
+@interface UIViewController (FDFullscreenPopGesture)
+
+/// Whether the interactive pop gesture is disabled when contained in a navigation
+/// stack.
+@property (nonatomic, assign) BOOL fd_interactivePopDisabled;
+
+/// Indicate this view controller prefers its navigation bar hidden or not,
+/// checked when view controller based navigation bar's appearance is enabled.
+/// Default to NO, bars are more likely to show.
+@property (nonatomic, assign) BOOL fd_prefersNavigationBarHidden;
+
+@end

+ 227 - 0
Example/Pods/FDFullscreenPopGesture/FDFullscreenPopGesture/UINavigationController+FDFullscreenPopGesture.m

@@ -0,0 +1,227 @@
+// The MIT License (MIT)
+//
+// Copyright (c) 2015-2016 forkingdog ( https://github.com/forkingdog )
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#import "UINavigationController+FDFullscreenPopGesture.h"
+#import <objc/runtime.h>
+
+@interface _FDFullscreenPopGestureRecognizerDelegate : NSObject <UIGestureRecognizerDelegate>
+
+@property (nonatomic, weak) UINavigationController *navigationController;
+
+@end
+
+@implementation _FDFullscreenPopGestureRecognizerDelegate
+
+- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
+{
+    // Ignore when no view controller is pushed into the navigation stack.
+    if (self.navigationController.viewControllers.count <= 1) {
+        return NO;
+    }
+    
+    // Disable when the active view controller doesn't allow interactive pop.
+    UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
+    if (topViewController.fd_interactivePopDisabled) {
+        return NO;
+    }
+
+    // Ignore pan gesture when the navigation controller is currently in transition.
+    if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
+        return NO;
+    }
+    
+    // Prevent calling the handler when the gesture begins in an opposite direction.
+    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
+    if (translation.x <= 0) {
+        return NO;
+    }
+    
+    return YES;
+}
+
+@end
+
+typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);
+
+@interface UIViewController (FDFullscreenPopGesturePrivate)
+
+@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;
+
+@end
+
+@implementation UIViewController (FDFullscreenPopGesturePrivate)
+
++ (void)load
+{
+    Method originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
+    Method swizzledMethod = class_getInstanceMethod(self, @selector(fd_viewWillAppear:));
+    method_exchangeImplementations(originalMethod, swizzledMethod);
+}
+
+- (void)fd_viewWillAppear:(BOOL)animated
+{
+    // Forward to primary implementation.
+    [self fd_viewWillAppear:animated];
+    
+    if (self.fd_willAppearInjectBlock) {
+        self.fd_willAppearInjectBlock(self, animated);
+    }
+}
+
+- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
+{
+    return objc_getAssociatedObject(self, _cmd);
+}
+
+- (void)setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block
+{
+    objc_setAssociatedObject(self, @selector(fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);
+}
+
+@end
+
+@implementation UINavigationController (FDFullscreenPopGesture)
+
++ (void)load
+{
+    // Inject "-pushViewController:animated:"
+    Method originalMethod = class_getInstanceMethod(self, @selector(pushViewController:animated:));
+    Method swizzledMethod = class_getInstanceMethod(self, @selector(fd_pushViewController:animated:));
+    method_exchangeImplementations(originalMethod, swizzledMethod);
+}
+
+- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
+{
+    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
+        
+        // Add our own gesture recognizer to where the onboard screen edge pan gesture recognizer is attached to.
+        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];
+
+        // Forward the gesture events to the private handler of the onboard gesture recognizer.
+        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
+        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
+        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
+        self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
+        [self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];
+
+        // Disable the onboard gesture recognizer.
+        self.interactivePopGestureRecognizer.enabled = NO;
+    }
+    
+    // Handle perferred navigation bar appearance.
+    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
+    
+    // Forward to primary implementation.
+    [self fd_pushViewController:viewController animated:animated];
+}
+
+- (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
+{
+    if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) {
+        return;
+    }
+    
+    __weak typeof(self) weakSelf = self;
+    _FDViewControllerWillAppearInjectBlock block = ^(UIViewController *viewController, BOOL animated) {
+        __strong typeof(weakSelf) strongSelf = weakSelf;
+        if (strongSelf) {
+            [strongSelf setNavigationBarHidden:viewController.fd_prefersNavigationBarHidden animated:animated];
+        }
+    };
+    
+    // Setup will appear inject block to appearing view controller.
+    // Setup disappearing view controller as well, because not every view controller is added into
+    // stack by pushing, maybe by "-setViewControllers:".
+    appearingViewController.fd_willAppearInjectBlock = block;
+    UIViewController *disappearingViewController = self.viewControllers.lastObject;
+    if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) {
+        disappearingViewController.fd_willAppearInjectBlock = block;
+    }
+}
+
+- (_FDFullscreenPopGestureRecognizerDelegate *)fd_popGestureRecognizerDelegate
+{
+    _FDFullscreenPopGestureRecognizerDelegate *delegate = objc_getAssociatedObject(self, _cmd);
+
+    if (!delegate) {
+        delegate = [[_FDFullscreenPopGestureRecognizerDelegate alloc] init];
+        delegate.navigationController = self;
+        
+        objc_setAssociatedObject(self, _cmd, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+    }
+    return delegate;
+}
+
+- (UIPanGestureRecognizer *)fd_fullscreenPopGestureRecognizer
+{
+    UIPanGestureRecognizer *panGestureRecognizer = objc_getAssociatedObject(self, _cmd);
+
+    if (!panGestureRecognizer) {
+        panGestureRecognizer = [[UIPanGestureRecognizer alloc] init];
+        panGestureRecognizer.maximumNumberOfTouches = 1;
+        
+        objc_setAssociatedObject(self, _cmd, panGestureRecognizer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+    }
+    return panGestureRecognizer;
+}
+
+- (BOOL)fd_viewControllerBasedNavigationBarAppearanceEnabled
+{
+    NSNumber *number = objc_getAssociatedObject(self, _cmd);
+    if (number) {
+        return number.boolValue;
+    }
+    self.fd_viewControllerBasedNavigationBarAppearanceEnabled = YES;
+    return YES;
+}
+
+- (void)setFd_viewControllerBasedNavigationBarAppearanceEnabled:(BOOL)enabled
+{
+    SEL key = @selector(fd_viewControllerBasedNavigationBarAppearanceEnabled);
+    objc_setAssociatedObject(self, key, @(enabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+@end
+
+@implementation UIViewController (FDFullscreenPopGesture)
+
+- (BOOL)fd_interactivePopDisabled
+{
+    return [objc_getAssociatedObject(self, _cmd) boolValue];
+}
+
+- (void)setFd_interactivePopDisabled:(BOOL)disabled
+{
+    objc_setAssociatedObject(self, @selector(fd_interactivePopDisabled), @(disabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (BOOL)fd_prefersNavigationBarHidden
+{
+    return [objc_getAssociatedObject(self, _cmd) boolValue];
+}
+
+- (void)setFd_prefersNavigationBarHidden:(BOOL)hidden
+{
+    objc_setAssociatedObject(self, @selector(fd_prefersNavigationBarHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+@end

+ 22 - 0
Example/Pods/FDFullscreenPopGesture/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

+ 62 - 0
Example/Pods/FDFullscreenPopGesture/README.md

@@ -0,0 +1,62 @@
+# FDFullscreenPopGesture
+An UINavigationController's category to enable fullscreen pop gesture in an iOS7+ system style with AOP.
+
+# Overview
+
+![snapshot](https://raw.githubusercontent.com/forkingdog/FDFullscreenPopGesture/master/Snapshots/snapshot0.gif)
+
+这个扩展来自 @J_雨 同学的这个很天才的思路,他的文章地址:[http://www.jianshu.com/p/d39f7d22db6c](http://www.jianshu.com/p/d39f7d22db6c)
+
+# Usage
+
+**AOP**, just add 2 files and **no need** for any setups, all navigation controllers will be able to use fullscreen pop gesture automatically.  
+
+To disable this pop gesture of a navigation controller:  
+
+``` objc
+navigationController.fd_fullscreenPopGestureRecognizer.enabled = NO;
+```
+
+To disable this pop gesture of a view controller:  
+
+``` objc
+viewController.fd_interactivePopDisabled = YES;
+```
+
+Require at least iOS **7.0**.
+
+# View Controller Based Navigation Bar Appearance
+
+It handles navigation bar transition properly when using fullscreen gesture to push or pop a view controller:  
+
+- with bar -> without bar
+- without bar -> with bar
+- without bar -> without bar
+
+![snapshot with bar states](https://raw.githubusercontent.com/forkingdog/FDFullscreenPopGesture/master/Snapshots/snapshot1.gif)
+
+This opmiziation is enabled by default, from now on you don't need to call **UINavigationController**'s `-setNavigationBarHidden:animated:` method, instead, use view controller's specific API to hide its bar:  
+
+``` objc
+- (void)viewDidLoad {
+    [super viewDidLoad];
+    self.fd_prefersNavigationBarHidden = NO;
+}
+```
+
+And this property is **YES** by default.
+
+# Installation
+
+Use cocoapods  
+
+``` ruby
+pod 'FDFullscreenPopGesture', '1.1'
+```
+# Release Notes
+
+**1.1** - View controller based navigation bar appearance and transition.  
+**1.0** - Fullscreen pop gesture.  
+
+# License  
+MIT

+ 22 - 0
Example/Pods/KeychainAccess/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 kishikawa katsumi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

+ 3074 - 0
Example/Pods/KeychainAccess/Lib/KeychainAccess/Keychain.swift

@@ -0,0 +1,3074 @@
+//
+//  Keychain.swift
+//  KeychainAccess
+//
+//  Created by kishikawa katsumi on 2014/12/24.
+//  Copyright (c) 2014 kishikawa katsumi. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Foundation
+import Security
+#if os(iOS) || os(OSX)
+import LocalAuthentication
+#endif
+
+public let KeychainAccessErrorDomain = "com.kishikawakatsumi.KeychainAccess.error"
+
+public enum ItemClass {
+    case genericPassword
+    case internetPassword
+}
+
+public enum ProtocolType {
+    case ftp
+    case ftpAccount
+    case http
+    case irc
+    case nntp
+    case pop3
+    case smtp
+    case socks
+    case imap
+    case ldap
+    case appleTalk
+    case afp
+    case telnet
+    case ssh
+    case ftps
+    case https
+    case httpProxy
+    case httpsProxy
+    case ftpProxy
+    case smb
+    case rtsp
+    case rtspProxy
+    case daap
+    case eppc
+    case ipp
+    case nntps
+    case ldaps
+    case telnetS
+    case imaps
+    case ircs
+    case pop3S
+}
+
+public enum AuthenticationType {
+    case ntlm
+    case msn
+    case dpa
+    case rpa
+    case httpBasic
+    case httpDigest
+    case htmlForm
+    case `default`
+}
+
+public enum Accessibility {
+    /**
+     Item data can only be accessed
+     while the device is unlocked. This is recommended for items that only
+     need be accesible while the application is in the foreground. Items
+     with this attribute will migrate to a new device when using encrypted
+     backups.
+     */
+    case whenUnlocked
+
+    /**
+     Item data can only be
+     accessed once the device has been unlocked after a restart. This is
+     recommended for items that need to be accesible by background
+     applications. Items with this attribute will migrate to a new device
+     when using encrypted backups.
+     */
+    case afterFirstUnlock
+
+    /**
+     Item data can always be accessed
+     regardless of the lock state of the device. This is not recommended
+     for anything except system use. Items with this attribute will migrate
+     to a new device when using encrypted backups.
+     */
+    @available(macCatalyst, unavailable)
+    case always
+
+    /**
+     Item data can
+     only be accessed while the device is unlocked. This class is only
+     available if a passcode is set on the device. This is recommended for
+     items that only need to be accessible while the application is in the
+     foreground. Items with this attribute will never migrate to a new
+     device, so after a backup is restored to a new device, these items
+     will be missing. No items can be stored in this class on devices
+     without a passcode. Disabling the device passcode will cause all
+     items in this class to be deleted.
+     */
+    @available(iOS 8.0, OSX 10.10, *)
+    case whenPasscodeSetThisDeviceOnly
+
+    /**
+     Item data can only
+     be accessed while the device is unlocked. This is recommended for items
+     that only need be accesible while the application is in the foreground.
+     Items with this attribute will never migrate to a new device, so after
+     a backup is restored to a new device, these items will be missing.
+     */
+    case whenUnlockedThisDeviceOnly
+
+    /**
+     Item data can
+     only be accessed once the device has been unlocked after a restart.
+     This is recommended for items that need to be accessible by background
+     applications. Items with this attribute will never migrate to a new
+     device, so after a backup is restored to a new device these items will
+     be missing.
+     */
+    case afterFirstUnlockThisDeviceOnly
+
+    /**
+     Item data can always
+     be accessed regardless of the lock state of the device. This option
+     is not recommended for anything except system use. Items with this
+     attribute will never migrate to a new device, so after a backup is
+     restored to a new device, these items will be missing.
+     */
+    @available(macCatalyst, unavailable)
+    case alwaysThisDeviceOnly
+}
+
+/**
+ Predefined item attribute constants used to get or set values
+ in a dictionary. The kSecUseAuthenticationUI constant is the key and its
+ value is one of the constants defined here.
+ If the key kSecUseAuthenticationUI not provided then kSecUseAuthenticationUIAllow
+ is used as default.
+ */
+public enum AuthenticationUI {
+    /**
+     Specifies that authenticate UI can appear.
+     */
+    case allow
+
+    /**
+     Specifies that the error
+     errSecInteractionNotAllowed will be returned if an item needs
+     to authenticate with UI
+     */
+    case fail
+
+    /**
+     Specifies that all items which need
+     to authenticate with UI will be silently skipped. This value can be used
+     only with SecItemCopyMatching.
+     */
+    case skip
+}
+
+@available(iOS 9.0, OSX 10.11, *)
+extension AuthenticationUI {
+    public var rawValue: String {
+        switch self {
+        case .allow:
+            return UseAuthenticationUIAllow
+        case .fail:
+            return UseAuthenticationUIFail
+        case .skip:
+            return UseAuthenticationUISkip
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .allow:
+            return "allow"
+        case .fail:
+            return "fail"
+        case .skip:
+            return "skip"
+        }
+    }
+}
+
+public struct AuthenticationPolicy: OptionSet {
+    /**
+     User presence policy using Touch ID or Passcode. Touch ID does not
+     have to be available or enrolled. Item is still accessible by Touch ID
+     even if fingers are added or removed.
+     */
+    @available(iOS 8.0, OSX 10.10, watchOS 2.0, tvOS 8.0, *)
+    public static let userPresence = AuthenticationPolicy(rawValue: 1 << 0)
+
+    /**
+     Constraint: Touch ID (any finger) or Face ID. Touch ID or Face ID must be available. With Touch ID
+     at least one finger must be enrolled. With Face ID user has to be enrolled. Item is still accessible by Touch ID even
+     if fingers are added or removed. Item is still accessible by Face ID if user is re-enrolled.
+     */
+    @available(iOS 11.3, OSX 10.13.4, watchOS 4.3, tvOS 11.3, *)
+    public static let biometryAny = AuthenticationPolicy(rawValue: 1 << 1)
+
+    /**
+     Deprecated, please use biometryAny instead.
+     */
+    @available(iOS, introduced: 9.0, deprecated: 11.3, renamed: "biometryAny")
+    @available(OSX, introduced: 10.12.1, deprecated: 10.13.4, renamed: "biometryAny")
+    @available(watchOS, introduced: 2.0, deprecated: 4.3, renamed: "biometryAny")
+    @available(tvOS, introduced: 9.0, deprecated: 11.3, renamed: "biometryAny")
+    public static let touchIDAny = AuthenticationPolicy(rawValue: 1 << 1)
+
+    /**
+     Constraint: Touch ID from the set of currently enrolled fingers. Touch ID must be available and at least one finger must
+     be enrolled. When fingers are added or removed, the item is invalidated. When Face ID is re-enrolled this item is invalidated.
+     */
+    @available(iOS 11.3, OSX 10.13, watchOS 4.3, tvOS 11.3, *)
+    public static let biometryCurrentSet = AuthenticationPolicy(rawValue: 1 << 3)
+
+    /**
+     Deprecated, please use biometryCurrentSet instead.
+     */
+    @available(iOS, introduced: 9.0, deprecated: 11.3, renamed: "biometryCurrentSet")
+    @available(OSX, introduced: 10.12.1, deprecated: 10.13.4, renamed: "biometryCurrentSet")
+    @available(watchOS, introduced: 2.0, deprecated: 4.3, renamed: "biometryCurrentSet")
+    @available(tvOS, introduced: 9.0, deprecated: 11.3, renamed: "biometryCurrentSet")
+    public static let touchIDCurrentSet = AuthenticationPolicy(rawValue: 1 << 3)
+
+    /**
+     Constraint: Device passcode
+     */
+    @available(iOS 9.0, OSX 10.11, watchOS 2.0, tvOS 9.0, *)
+    public static let devicePasscode = AuthenticationPolicy(rawValue: 1 << 4)
+
+    /**
+     Constraint: Watch
+     */
+    @available(iOS, unavailable)
+    @available(OSX 10.15, *)
+    @available(watchOS, unavailable)
+    @available(tvOS, unavailable)
+    public static let watch = AuthenticationPolicy(rawValue: 1 << 5)
+
+    /**
+     Constraint logic operation: when using more than one constraint,
+     at least one of them must be satisfied.
+     */
+    @available(iOS 9.0, OSX 10.12.1, watchOS 2.0, tvOS 9.0, *)
+    public static let or = AuthenticationPolicy(rawValue: 1 << 14)
+
+    /**
+     Constraint logic operation: when using more than one constraint,
+     all must be satisfied.
+     */
+    @available(iOS 9.0, OSX 10.12.1, watchOS 2.0, tvOS 9.0, *)
+    public static let and = AuthenticationPolicy(rawValue: 1 << 15)
+
+    /**
+     Create access control for private key operations (i.e. sign operation)
+     */
+    @available(iOS 9.0, OSX 10.12.1, watchOS 2.0, tvOS 9.0, *)
+    public static let privateKeyUsage = AuthenticationPolicy(rawValue: 1 << 30)
+
+    /**
+     Security: Application provided password for data encryption key generation.
+     This is not a constraint but additional item encryption mechanism.
+     */
+    @available(iOS 9.0, OSX 10.12.1, watchOS 2.0, tvOS 9.0, *)
+    public static let applicationPassword = AuthenticationPolicy(rawValue: 1 << 31)
+
+    #if swift(>=2.3)
+    public let rawValue: UInt
+
+    public init(rawValue: UInt) {
+        self.rawValue = rawValue
+    }
+    #else
+    public let rawValue: Int
+
+    public init(rawValue: Int) {
+        self.rawValue = rawValue
+    }
+    #endif
+}
+
+public struct Attributes {
+    public var `class`: String? {
+        return attributes[Class] as? String
+    }
+    public var data: Data? {
+        return attributes[ValueData] as? Data
+    }
+    public var ref: Data? {
+        return attributes[ValueRef] as? Data
+    }
+    public var persistentRef: Data? {
+        return attributes[ValuePersistentRef] as? Data
+    }
+
+    public var accessible: String? {
+        return attributes[AttributeAccessible] as? String
+    }
+    public var accessControl: SecAccessControl? {
+        if #available(OSX 10.10, *) {
+            if let accessControl = attributes[AttributeAccessControl] {
+                return (accessControl as! SecAccessControl)
+            }
+            return nil
+        } else {
+            return nil
+        }
+    }
+    public var accessGroup: String? {
+        return attributes[AttributeAccessGroup] as? String
+    }
+    public var synchronizable: Bool? {
+        return attributes[AttributeSynchronizable] as? Bool
+    }
+    public var creationDate: Date? {
+        return attributes[AttributeCreationDate] as? Date
+    }
+    public var modificationDate: Date? {
+        return attributes[AttributeModificationDate] as? Date
+    }
+    public var attributeDescription: String? {
+        return attributes[AttributeDescription] as? String
+    }
+    public var comment: String? {
+        return attributes[AttributeComment] as? String
+    }
+    public var creator: String? {
+        return attributes[AttributeCreator] as? String
+    }
+    public var type: String? {
+        return attributes[AttributeType] as? String
+    }
+    public var label: String? {
+        return attributes[AttributeLabel] as? String
+    }
+    public var isInvisible: Bool? {
+        return attributes[AttributeIsInvisible] as? Bool
+    }
+    public var isNegative: Bool? {
+        return attributes[AttributeIsNegative] as? Bool
+    }
+    public var account: String? {
+        return attributes[AttributeAccount] as? String
+    }
+    public var service: String? {
+        return attributes[AttributeService] as? String
+    }
+    public var generic: Data? {
+        return attributes[AttributeGeneric] as? Data
+    }
+    public var securityDomain: String? {
+        return attributes[AttributeSecurityDomain] as? String
+    }
+    public var server: String? {
+        return attributes[AttributeServer] as? String
+    }
+    public var `protocol`: String? {
+        return attributes[AttributeProtocol] as? String
+    }
+    public var authenticationType: String? {
+        return attributes[AttributeAuthenticationType] as? String
+    }
+    public var port: Int? {
+        return attributes[AttributePort] as? Int
+    }
+    public var path: String? {
+        return attributes[AttributePath] as? String
+    }
+
+    fileprivate let attributes: [String: Any]
+
+    init(attributes: [String: Any]) {
+        self.attributes = attributes
+    }
+
+    public subscript(key: String) -> Any? {
+        get {
+            return attributes[key]
+        }
+    }
+}
+
+public final class Keychain {
+    public var itemClass: ItemClass {
+        return options.itemClass
+    }
+
+    public var service: String {
+        return options.service
+    }
+
+    // This attribute (kSecAttrAccessGroup) applies to macOS keychain items only if you also set a value of true for the
+    // kSecUseDataProtectionKeychain key, the kSecAttrSynchronizable key, or both.
+    public var accessGroup: String? {
+        return options.accessGroup
+    }
+
+    public var server: URL {
+        return options.server
+    }
+
+    public var protocolType: ProtocolType {
+        return options.protocolType
+    }
+
+    public var authenticationType: AuthenticationType {
+        return options.authenticationType
+    }
+
+    public var accessibility: Accessibility {
+        return options.accessibility
+    }
+
+    @available(iOS 8.0, OSX 10.10, *)
+    @available(watchOS, unavailable)
+    public var authenticationPolicy: AuthenticationPolicy? {
+        return options.authenticationPolicy
+    }
+
+    public var synchronizable: Bool {
+        return options.synchronizable
+    }
+
+    public var label: String? {
+        return options.label
+    }
+
+    public var comment: String? {
+        return options.comment
+    }
+
+    @available(iOS 8.0, OSX 10.10, *)
+    @available(watchOS, unavailable)
+    public var authenticationPrompt: String? {
+        return options.authenticationPrompt
+    }
+
+    @available(iOS 9.0, OSX 10.11, *)
+    public var authenticationUI: AuthenticationUI {
+        return options.authenticationUI ?? .allow
+    }
+
+    #if os(iOS) || os(OSX)
+    @available(iOS 9.0, OSX 10.11, *)
+    public var authenticationContext: LAContext? {
+        return options.authenticationContext as? LAContext
+    }
+    #endif
+
+    fileprivate let options: Options
+
+    // MARK:
+
+    public convenience init() {
+        var options = Options()
+        if let bundleIdentifier = Bundle.main.bundleIdentifier {
+            options.service = bundleIdentifier
+        }
+        self.init(options)
+    }
+
+    public convenience init(service: String) {
+        var options = Options()
+        options.service = service
+        self.init(options)
+    }
+
+    public convenience init(accessGroup: String) {
+        var options = Options()
+        if let bundleIdentifier = Bundle.main.bundleIdentifier {
+            options.service = bundleIdentifier
+        }
+        options.accessGroup = accessGroup
+        self.init(options)
+    }
+
+    public convenience init(service: String, accessGroup: String) {
+        var options = Options()
+        options.service = service
+        options.accessGroup = accessGroup
+        self.init(options)
+    }
+
+    public convenience init(server: String, protocolType: ProtocolType, accessGroup: String? = nil, authenticationType: AuthenticationType = .default) {
+        self.init(server: URL(string: server)!, protocolType: protocolType, accessGroup: accessGroup, authenticationType: authenticationType)
+    }
+
+    public convenience init(server: URL, protocolType: ProtocolType, accessGroup: String? = nil, authenticationType: AuthenticationType = .default) {
+        var options = Options()
+        options.itemClass = .internetPassword
+        options.server = server
+        options.protocolType = protocolType
+        options.accessGroup = accessGroup
+        options.authenticationType = authenticationType
+        self.init(options)
+    }
+
+    fileprivate init(_ opts: Options) {
+        options = opts
+    }
+
+    // MARK:
+
+    public func accessibility(_ accessibility: Accessibility) -> Keychain {
+        var options = self.options
+        options.accessibility = accessibility
+        return Keychain(options)
+    }
+
+    @available(iOS 8.0, OSX 10.10, *)
+    @available(watchOS, unavailable)
+    public func accessibility(_ accessibility: Accessibility, authenticationPolicy: AuthenticationPolicy) -> Keychain {
+        var options = self.options
+        options.accessibility = accessibility
+        options.authenticationPolicy = authenticationPolicy
+        return Keychain(options)
+    }
+
+    public func synchronizable(_ synchronizable: Bool) -> Keychain {
+        var options = self.options
+        options.synchronizable = synchronizable
+        return Keychain(options)
+    }
+
+    public func label(_ label: String) -> Keychain {
+        var options = self.options
+        options.label = label
+        return Keychain(options)
+    }
+
+    public func comment(_ comment: String) -> Keychain {
+        var options = self.options
+        options.comment = comment
+        return Keychain(options)
+    }
+
+    public func attributes(_ attributes: [String: Any]) -> Keychain {
+        var options = self.options
+        attributes.forEach { options.attributes.updateValue($1, forKey: $0) }
+        return Keychain(options)
+    }
+
+    @available(iOS 8.0, OSX 10.10, *)
+    @available(watchOS, unavailable)
+    public func authenticationPrompt(_ authenticationPrompt: String) -> Keychain {
+        var options = self.options
+        options.authenticationPrompt = authenticationPrompt
+        return Keychain(options)
+    }
+
+    @available(iOS 9.0, OSX 10.11, *)
+    public func authenticationUI(_ authenticationUI: AuthenticationUI) -> Keychain {
+        var options = self.options
+        options.authenticationUI = authenticationUI
+        return Keychain(options)
+    }
+
+    #if os(iOS) || os(OSX)
+    @available(iOS 9.0, OSX 10.11, *)
+    public func authenticationContext(_ authenticationContext: LAContext) -> Keychain {
+        var options = self.options
+        options.authenticationContext = authenticationContext
+        return Keychain(options)
+    }
+    #endif
+
+    // MARK:
+
+    public func get(_ key: String, ignoringAttributeSynchronizable: Bool = true) throws -> String? {
+        return try getString(key, ignoringAttributeSynchronizable: ignoringAttributeSynchronizable)
+    }
+
+    public func getString(_ key: String, ignoringAttributeSynchronizable: Bool = true) throws -> String? {
+        guard let data = try getData(key, ignoringAttributeSynchronizable: ignoringAttributeSynchronizable) else  {
+            return nil
+        }
+        guard let string = String(data: data, encoding: .utf8) else {
+            print("failed to convert data to string")
+            throw Status.conversionError
+        }
+        return string
+    }
+
+    public func getData(_ key: String, ignoringAttributeSynchronizable: Bool = true) throws -> Data? {
+        var query = options.query(ignoringAttributeSynchronizable: ignoringAttributeSynchronizable)
+
+        query[MatchLimit] = MatchLimitOne
+        query[ReturnData] = kCFBooleanTrue
+
+        query[AttributeAccount] = key
+
+        var result: AnyObject?
+        let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+        switch status {
+        case errSecSuccess:
+            guard let data = result as? Data else {
+                throw Status.unexpectedError
+            }
+            return data
+        case errSecItemNotFound:
+            return nil
+        default:
+            throw securityError(status: status)
+        }
+    }
+
+    public func get<T>(_ key: String, ignoringAttributeSynchronizable: Bool = true, handler: (Attributes?) -> T) throws -> T {
+        var query = options.query(ignoringAttributeSynchronizable: ignoringAttributeSynchronizable)
+
+        query[MatchLimit] = MatchLimitOne
+
+        query[ReturnData] = kCFBooleanTrue
+        query[ReturnAttributes] = kCFBooleanTrue
+        query[ReturnRef] = kCFBooleanTrue
+        query[ReturnPersistentRef] = kCFBooleanTrue
+
+        query[AttributeAccount] = key
+
+        var result: AnyObject?
+        let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+        switch status {
+        case errSecSuccess:
+            guard let attributes = result as? [String: Any] else {
+                throw Status.unexpectedError
+            }
+            return handler(Attributes(attributes: attributes))
+        case errSecItemNotFound:
+            return handler(nil)
+        default:
+            throw securityError(status: status)
+        }
+    }
+
+    // MARK:
+
+    public func set(_ value: String, key: String, ignoringAttributeSynchronizable: Bool = true) throws {
+        guard let data = value.data(using: .utf8, allowLossyConversion: false) else {
+            print("failed to convert string to data")
+            throw Status.conversionError
+        }
+        try set(data, key: key, ignoringAttributeSynchronizable: ignoringAttributeSynchronizable)
+    }
+
+    public func set(_ value: Data, key: String, ignoringAttributeSynchronizable: Bool = true) throws {
+        var query = options.query(ignoringAttributeSynchronizable: ignoringAttributeSynchronizable)
+        query[AttributeAccount] = key
+        #if os(iOS)
+        if #available(iOS 9.0, *) {
+            if let authenticationUI = options.authenticationUI {
+                query[UseAuthenticationUI] = authenticationUI.rawValue
+            } else {
+                query[UseAuthenticationUI] = UseAuthenticationUIFail
+            }
+        } else {
+            query[UseNoAuthenticationUI] = kCFBooleanTrue
+        }
+        #elseif os(OSX)
+        query[ReturnData] = kCFBooleanTrue
+        if #available(OSX 10.11, *) {
+            if let authenticationUI = options.authenticationUI {
+                query[UseAuthenticationUI] = authenticationUI.rawValue
+            } else {
+                query[UseAuthenticationUI] = UseAuthenticationUIFail
+            }
+        }
+        #else
+        if let authenticationUI = options.authenticationUI {
+            query[UseAuthenticationUI] = authenticationUI.rawValue
+        }
+        #endif
+
+        var status = SecItemCopyMatching(query as CFDictionary, nil)
+        switch status {
+        case errSecSuccess, errSecInteractionNotAllowed:
+            var query = options.query()
+            query[AttributeAccount] = key
+
+            var (attributes, error) = options.attributes(key: nil, value: value)
+            if let error = error {
+                print(error.localizedDescription)
+                throw error
+            }
+
+            options.attributes.forEach { attributes.updateValue($1, forKey: $0) }
+
+            #if os(iOS)
+            if status == errSecInteractionNotAllowed && floor(NSFoundationVersionNumber) <= floor(NSFoundationVersionNumber_iOS_8_0) {
+                try remove(key)
+                try set(value, key: key)
+            } else {
+                status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
+                if status != errSecSuccess {
+                    throw securityError(status: status)
+                }
+            }
+            #else
+            status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
+            if status != errSecSuccess {
+                throw securityError(status: status)
+            }
+            #endif
+        case errSecItemNotFound:
+            var (attributes, error) = options.attributes(key: key, value: value)
+            if let error = error {
+                print(error.localizedDescription)
+                throw error
+            }
+
+            options.attributes.forEach { attributes.updateValue($1, forKey: $0) }
+
+            status = SecItemAdd(attributes as CFDictionary, nil)
+            if status != errSecSuccess {
+                throw securityError(status: status)
+            }
+        default:
+            throw securityError(status: status)
+        }
+    }
+
+    public subscript(key: String) -> String? {
+        get {
+            #if swift(>=5.0)
+            return try? get(key)
+            #else
+            return (try? get(key)).flatMap { $0 }
+            #endif
+        }
+
+        set {
+            if let value = newValue {
+                do {
+                    try set(value, key: key)
+                } catch {}
+            } else {
+                do {
+                    try remove(key)
+                } catch {}
+            }
+        }
+    }
+
+    public subscript(string key: String) -> String? {
+        get {
+            return self[key]
+        }
+
+        set {
+            self[key] = newValue
+        }
+    }
+
+    public subscript(data key: String) -> Data? {
+        get {
+            #if swift(>=5.0)
+            return try? getData(key)
+            #else
+            return (try? getData(key)).flatMap { $0 }
+            #endif
+        }
+
+        set {
+            if let value = newValue {
+                do {
+                    try set(value, key: key)
+                } catch {}
+            } else {
+                do {
+                    try remove(key)
+                } catch {}
+            }
+        }
+    }
+
+    public subscript(attributes key: String) -> Attributes? {
+        get {
+            #if swift(>=5.0)
+            return try? get(key) { $0 }
+            #else
+            return (try? get(key) { $0 }).flatMap { $0 }
+            #endif
+        }
+    }
+
+    // MARK:
+
+    public func remove(_ key: String, ignoringAttributeSynchronizable: Bool = true) throws {
+        var query = options.query(ignoringAttributeSynchronizable: ignoringAttributeSynchronizable)
+        query[AttributeAccount] = key
+
+        let status = SecItemDelete(query as CFDictionary)
+        if status != errSecSuccess && status != errSecItemNotFound {
+            throw securityError(status: status)
+        }
+    }
+
+    public func removeAll() throws {
+        var query = options.query()
+        #if !os(iOS) && !os(watchOS) && !os(tvOS)
+        query[MatchLimit] = MatchLimitAll
+        #endif
+
+        let status = SecItemDelete(query as CFDictionary)
+        if status != errSecSuccess && status != errSecItemNotFound {
+            throw securityError(status: status)
+        }
+    }
+
+    // MARK:
+
+    public func contains(_ key: String, withoutAuthenticationUI: Bool = false) throws -> Bool {
+        var query = options.query()
+        query[AttributeAccount] = key
+
+        if withoutAuthenticationUI {
+            #if os(iOS) || os(watchOS) || os(tvOS)
+            if #available(iOS 9.0, *) {
+                if let authenticationUI = options.authenticationUI {
+                    query[UseAuthenticationUI] = authenticationUI.rawValue
+                } else {
+                    query[UseAuthenticationUI] = UseAuthenticationUIFail
+                }
+            } else {
+                query[UseNoAuthenticationUI] = kCFBooleanTrue
+            }
+            #else
+            if #available(OSX 10.11, *) {
+                if let authenticationUI = options.authenticationUI {
+                    query[UseAuthenticationUI] = authenticationUI.rawValue
+                } else {
+                    query[UseAuthenticationUI] = UseAuthenticationUIFail
+                }
+            } else if #available(OSX 10.10, *) {
+                query[UseNoAuthenticationUI] = kCFBooleanTrue
+            }
+            #endif
+        } else {
+            if #available(iOS 9.0, OSX 10.11, *) {
+                if let authenticationUI = options.authenticationUI {
+                    query[UseAuthenticationUI] = authenticationUI.rawValue
+                }
+            }
+        }
+        
+        let status = SecItemCopyMatching(query as CFDictionary, nil)
+        switch status {
+        case errSecSuccess:
+                return true
+        case errSecInteractionNotAllowed:
+            if withoutAuthenticationUI {
+                return true
+            }
+            return false
+        case errSecItemNotFound:
+            return false
+        default:
+            throw securityError(status: status)
+        }
+    }
+
+    // MARK:
+
+    public class func allKeys(_ itemClass: ItemClass) -> [(String, String)] {
+        var query = [String: Any]()
+        query[Class] = itemClass.rawValue
+        query[AttributeSynchronizable] = SynchronizableAny
+        query[MatchLimit] = MatchLimitAll
+        query[ReturnAttributes] = kCFBooleanTrue
+
+        var result: AnyObject?
+        let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+        switch status {
+        case errSecSuccess:
+            if let items = result as? [[String: Any]] {
+                return prettify(itemClass: itemClass, items: items).map {
+                    switch itemClass {
+                    case .genericPassword:
+                        return (($0["service"] ?? "") as! String, ($0["key"] ?? "") as! String)
+                    case .internetPassword:
+                        return (($0["server"] ?? "") as! String, ($0["key"] ?? "") as! String)
+                    }
+                }
+            }
+        case errSecItemNotFound:
+            return []
+        default: ()
+        }
+
+        securityError(status: status)
+        return []
+    }
+
+    public func allKeys() -> [String] {
+        let allItems = type(of: self).prettify(itemClass: itemClass, items: items())
+        let filter: ([String: Any]) -> String? = { $0["key"] as? String }
+
+        #if swift(>=4.1)
+            return allItems.compactMap(filter)
+        #else
+            return allItems.flatMap(filter)
+        #endif
+    }
+
+    public class func allItems(_ itemClass: ItemClass) -> [[String: Any]] {
+        var query = [String: Any]()
+        query[Class] = itemClass.rawValue
+        query[MatchLimit] = MatchLimitAll
+        query[ReturnAttributes] = kCFBooleanTrue
+        #if os(iOS) || os(watchOS) || os(tvOS)
+        query[ReturnData] = kCFBooleanTrue
+        #endif
+
+        var result: AnyObject?
+        let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+        switch status {
+        case errSecSuccess:
+            if let items = result as? [[String: Any]] {
+                return prettify(itemClass: itemClass, items: items)
+            }
+        case errSecItemNotFound:
+            return []
+        default: ()
+        }
+
+        securityError(status: status)
+        return []
+    }
+
+    public func allItems() -> [[String: Any]] {
+        return type(of: self).prettify(itemClass: itemClass, items: items())
+    }
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    public func getSharedPassword(_ completion: @escaping (_ account: String?, _ password: String?, _ error: Error?) -> () = { account, password, error -> () in }) {
+        if let domain = server.host {
+            type(of: self).requestSharedWebCredential(domain: domain, account: nil) { (credentials, error) -> () in
+                if let credential = credentials.first {
+                    let account = credential["account"]
+                    let password = credential["password"]
+                    completion(account, password, error)
+                } else {
+                    completion(nil, nil, error)
+                }
+            }
+        } else {
+            let error = securityError(status: Status.param.rawValue)
+            completion(nil, nil, error)
+        }
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    public func getSharedPassword(_ account: String, completion: @escaping (_ password: String?, _ error: Error?) -> () = { password, error -> () in }) {
+        if let domain = server.host {
+            type(of: self).requestSharedWebCredential(domain: domain, account: account) { (credentials, error) -> () in
+                if let credential = credentials.first {
+                    if let password = credential["password"] {
+                        completion(password, error)
+                    } else {
+                        completion(nil, error)
+                    }
+                } else {
+                    completion(nil, error)
+                }
+            }
+        } else {
+            let error = securityError(status: Status.param.rawValue)
+            completion(nil, error)
+        }
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    public func setSharedPassword(_ password: String, account: String, completion: @escaping (_ error: Error?) -> () = { e -> () in }) {
+        setSharedPassword(password as String?, account: account, completion: completion)
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    fileprivate func setSharedPassword(_ password: String?, account: String, completion: @escaping (_ error: Error?) -> () = { e -> () in }) {
+        if let domain = server.host {
+            SecAddSharedWebCredential(domain as CFString, account as CFString, password as CFString?) { error -> () in
+                if let error = error {
+                    completion(error.error)
+                } else {
+                    completion(nil)
+                }
+            }
+        } else {
+            let error = securityError(status: Status.param.rawValue)
+            completion(error)
+        }
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    public func removeSharedPassword(_ account: String, completion: @escaping (_ error: Error?) -> () = { e -> () in }) {
+        setSharedPassword(nil, account: account, completion: completion)
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    public class func requestSharedWebCredential(_ completion: @escaping (_ credentials: [[String: String]], _ error: Error?) -> () = { credentials, error -> () in }) {
+        requestSharedWebCredential(domain: nil, account: nil, completion: completion)
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    public class func requestSharedWebCredential(domain: String, completion: @escaping (_ credentials: [[String: String]], _ error: Error?) -> () = { credentials, error -> () in }) {
+        requestSharedWebCredential(domain: domain, account: nil, completion: completion)
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    public class func requestSharedWebCredential(domain: String, account: String, completion: @escaping (_ credentials: [[String: String]], _ error: Error?) -> () = { credentials, error -> () in }) {
+        requestSharedWebCredential(domain: Optional(domain), account: Optional(account)!, completion: completion)
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    @available(iOS 8.0, *)
+    fileprivate class func requestSharedWebCredential(domain: String?, account: String?, completion: @escaping (_ credentials: [[String: String]], _ error: Error?) -> ()) {
+        SecRequestSharedWebCredential(domain as CFString?, account as CFString?) { (credentials, error) -> () in
+            var remoteError: NSError?
+            if let error = error {
+                remoteError = error.error
+                if remoteError?.code != Int(errSecItemNotFound) {
+                    print("error:[\(remoteError!.code)] \(remoteError!.localizedDescription)")
+                }
+            }
+            if let credentials = credentials {
+                let credentials = (credentials as NSArray).map { credentials -> [String: String] in
+                    var credential = [String: String]()
+                    if let credentials = credentials as? [String: String] {
+                        if let server = credentials[AttributeServer] {
+                            credential["server"] = server
+                        }
+                        if let account = credentials[AttributeAccount] {
+                            credential["account"] = account
+                        }
+                        if let password = credentials[SharedPassword] {
+                            credential["password"] = password
+                        }
+                    }
+                    return credential
+                }
+                completion(credentials, remoteError)
+            } else {
+                completion([], remoteError)
+            }
+        }
+    }
+    #endif
+
+    #if os(iOS) && !targetEnvironment(macCatalyst)
+    /**
+     @abstract Returns a randomly generated password.
+     @return String password in the form xxx-xxx-xxx-xxx where x is taken from the sets "abcdefghkmnopqrstuvwxy", "ABCDEFGHJKLMNPQRSTUVWXYZ", "3456789" with at least one character from each set being present.
+     */
+    @available(iOS 8.0, *)
+    public class func generatePassword() -> String {
+        return SecCreateSharedWebCredentialPassword()! as String
+    }
+    #endif
+
+    // MARK:
+
+    fileprivate func items() -> [[String: Any]] {
+        var query = options.query()
+        query[MatchLimit] = MatchLimitAll
+        query[ReturnAttributes] = kCFBooleanTrue
+        #if os(iOS) || os(watchOS) || os(tvOS)
+        query[ReturnData] = kCFBooleanTrue
+        #endif
+
+        var result: AnyObject?
+        let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+        switch status {
+        case errSecSuccess:
+            if let items = result as? [[String: Any]] {
+                return items
+            }
+        case errSecItemNotFound:
+            return []
+        default: ()
+        }
+
+        securityError(status: status)
+        return []
+    }
+
+    fileprivate class func prettify(itemClass: ItemClass, items: [[String: Any]]) -> [[String: Any]] {
+        let items = items.map { attributes -> [String: Any] in
+            var item = [String: Any]()
+
+            item["class"] = itemClass.description
+            
+            if let accessGroup = attributes[AttributeAccessGroup] as? String {
+                item["accessGroup"] = accessGroup
+            }
+
+            switch itemClass {
+            case .genericPassword:
+                if let service = attributes[AttributeService] as? String {
+                    item["service"] = service
+                }
+            case .internetPassword:
+                if let server = attributes[AttributeServer] as? String {
+                    item["server"] = server
+                }
+                if let proto = attributes[AttributeProtocol] as? String {
+                    if let protocolType = ProtocolType(rawValue: proto) {
+                        item["protocol"] = protocolType.description
+                    }
+                }
+                if let auth = attributes[AttributeAuthenticationType] as? String {
+                    if let authenticationType = AuthenticationType(rawValue: auth) {
+                        item["authenticationType"] = authenticationType.description
+                    }
+                }
+            }
+
+            if let key = attributes[AttributeAccount] as? String {
+                item["key"] = key
+            }
+            if let data = attributes[ValueData] as? Data {
+                if let text = String(data: data, encoding: .utf8) {
+                    item["value"] = text
+                } else  {
+                    item["value"] = data
+                }
+            }
+
+            if let accessible = attributes[AttributeAccessible] as? String {
+                if let accessibility = Accessibility(rawValue: accessible) {
+                    item["accessibility"] = accessibility.description
+                }
+            }
+            if let synchronizable = attributes[AttributeSynchronizable] as? Bool {
+                item["synchronizable"] = synchronizable ? "true" : "false"
+            }
+
+            return item
+        }
+        return items
+    }
+
+    // MARK:
+
+    @discardableResult
+    fileprivate class func securityError(status: OSStatus) -> Error {
+        let error = Status(status: status)
+        if error != .userCanceled {
+            print("OSStatus error:[\(error.errorCode)] \(error.description)")
+        }
+
+        return error
+    }
+
+    @discardableResult
+    fileprivate func securityError(status: OSStatus) -> Error {
+        return type(of: self).securityError(status: status)
+    }
+}
+
+struct Options {
+    var itemClass: ItemClass = .genericPassword
+
+    var service: String = ""
+    var accessGroup: String? = nil
+
+    var server: URL!
+    var protocolType: ProtocolType!
+    var authenticationType: AuthenticationType = .default
+
+    var accessibility: Accessibility = .afterFirstUnlock
+    var authenticationPolicy: AuthenticationPolicy?
+
+    var synchronizable: Bool = false
+
+    var label: String?
+    var comment: String?
+
+    var authenticationPrompt: String?
+    var authenticationUI: AuthenticationUI?
+    var authenticationContext: AnyObject?
+
+    var attributes = [String: Any]()
+}
+
+/** Class Key Constant */
+private let Class = String(kSecClass)
+
+/** Attribute Key Constants */
+private let AttributeAccessible = String(kSecAttrAccessible)
+
+@available(iOS 8.0, OSX 10.10, *)
+private let AttributeAccessControl = String(kSecAttrAccessControl)
+
+private let AttributeAccessGroup = String(kSecAttrAccessGroup)
+private let AttributeSynchronizable = String(kSecAttrSynchronizable)
+private let AttributeCreationDate = String(kSecAttrCreationDate)
+private let AttributeModificationDate = String(kSecAttrModificationDate)
+private let AttributeDescription = String(kSecAttrDescription)
+private let AttributeComment = String(kSecAttrComment)
+private let AttributeCreator = String(kSecAttrCreator)
+private let AttributeType = String(kSecAttrType)
+private let AttributeLabel = String(kSecAttrLabel)
+private let AttributeIsInvisible = String(kSecAttrIsInvisible)
+private let AttributeIsNegative = String(kSecAttrIsNegative)
+private let AttributeAccount = String(kSecAttrAccount)
+private let AttributeService = String(kSecAttrService)
+private let AttributeGeneric = String(kSecAttrGeneric)
+private let AttributeSecurityDomain = String(kSecAttrSecurityDomain)
+private let AttributeServer = String(kSecAttrServer)
+private let AttributeProtocol = String(kSecAttrProtocol)
+private let AttributeAuthenticationType = String(kSecAttrAuthenticationType)
+private let AttributePort = String(kSecAttrPort)
+private let AttributePath = String(kSecAttrPath)
+
+private let SynchronizableAny = kSecAttrSynchronizableAny
+
+/** Search Constants */
+private let MatchLimit = String(kSecMatchLimit)
+private let MatchLimitOne = kSecMatchLimitOne
+private let MatchLimitAll = kSecMatchLimitAll
+
+/** Return Type Key Constants */
+private let ReturnData = String(kSecReturnData)
+private let ReturnAttributes = String(kSecReturnAttributes)
+private let ReturnRef = String(kSecReturnRef)
+private let ReturnPersistentRef = String(kSecReturnPersistentRef)
+
+/** Value Type Key Constants */
+private let ValueData = String(kSecValueData)
+private let ValueRef = String(kSecValueRef)
+private let ValuePersistentRef = String(kSecValuePersistentRef)
+
+/** Other Constants */
+@available(iOS 8.0, OSX 10.10, tvOS 8.0, *)
+private let UseOperationPrompt = String(kSecUseOperationPrompt)
+
+@available(iOS, introduced: 8.0, deprecated: 9.0, message: "Use a UseAuthenticationUI instead.")
+@available(OSX, introduced: 10.10, deprecated: 10.11, message: "Use UseAuthenticationUI instead.")
+@available(watchOS, introduced: 2.0, deprecated: 2.0, message: "Use UseAuthenticationUI instead.")
+@available(tvOS, introduced: 8.0, deprecated: 9.0, message: "Use UseAuthenticationUI instead.")
+private let UseNoAuthenticationUI = String(kSecUseNoAuthenticationUI)
+
+@available(iOS 9.0, OSX 10.11, watchOS 2.0, tvOS 9.0, *)
+private let UseAuthenticationUI = String(kSecUseAuthenticationUI)
+
+@available(iOS 9.0, OSX 10.11, watchOS 2.0, tvOS 9.0, *)
+private let UseAuthenticationContext = String(kSecUseAuthenticationContext)
+
+@available(iOS 9.0, OSX 10.11, watchOS 2.0, tvOS 9.0, *)
+private let UseAuthenticationUIAllow = String(kSecUseAuthenticationUIAllow)
+
+@available(iOS 9.0, OSX 10.11, watchOS 2.0, tvOS 9.0, *)
+private let UseAuthenticationUIFail = String(kSecUseAuthenticationUIFail)
+
+@available(iOS 9.0, OSX 10.11, watchOS 2.0, tvOS 9.0, *)
+private let UseAuthenticationUISkip = String(kSecUseAuthenticationUISkip)
+
+#if os(iOS) && !targetEnvironment(macCatalyst)
+/** Credential Key Constants */
+private let SharedPassword = String(kSecSharedPassword)
+#endif
+
+extension Keychain: CustomStringConvertible, CustomDebugStringConvertible {
+    public var description: String {
+        let items = allItems()
+        if items.isEmpty {
+            return "[]"
+        }
+        var description = "[\n"
+        for item in items {
+            description += "  "
+            description += "\(item)\n"
+        }
+        description += "]"
+        return description
+    }
+
+    public var debugDescription: String {
+        return "\(items())"
+    }
+}
+
+extension Options {
+    func query(ignoringAttributeSynchronizable: Bool = true) -> [String: Any] {
+        var query = [String: Any]()
+
+        query[Class] = itemClass.rawValue
+        if let accessGroup = self.accessGroup {
+            query[AttributeAccessGroup] = accessGroup
+        }
+        if ignoringAttributeSynchronizable {
+            query[AttributeSynchronizable] = SynchronizableAny
+        } else {
+            query[AttributeSynchronizable] = synchronizable ? kCFBooleanTrue : kCFBooleanFalse
+        }
+
+        switch itemClass {
+        case .genericPassword:
+            query[AttributeService] = service
+        case .internetPassword:
+            query[AttributeServer] = server.host
+            query[AttributePort] = server.port
+            query[AttributeProtocol] = protocolType.rawValue
+            query[AttributeAuthenticationType] = authenticationType.rawValue
+        }
+
+        if #available(OSX 10.10, *) {
+            if authenticationPrompt != nil {
+                query[UseOperationPrompt] = authenticationPrompt
+            }
+        }
+
+        #if !os(watchOS)
+        if #available(iOS 9.0, OSX 10.11, *) {
+            if authenticationContext != nil {
+                query[UseAuthenticationContext] = authenticationContext
+            }
+        }
+        #endif
+
+        return query
+    }
+
+    func attributes(key: String?, value: Data) -> ([String: Any], Error?) {
+        var attributes: [String: Any]
+
+        if key != nil {
+            attributes = query()
+            attributes[AttributeAccount] = key
+        } else {
+            attributes = [String: Any]()
+        }
+
+        attributes[ValueData] = value
+
+        if label != nil {
+            attributes[AttributeLabel] = label
+        }
+        if comment != nil {
+            attributes[AttributeComment] = comment
+        }
+
+        if let policy = authenticationPolicy {
+            if #available(OSX 10.10, *) {
+                var error: Unmanaged<CFError>?
+                guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility.rawValue as CFTypeRef, SecAccessControlCreateFlags(rawValue: CFOptionFlags(policy.rawValue)), &error) else {
+                    if let error = error?.takeUnretainedValue() {
+                        return (attributes, error.error)
+                    }
+
+                    return (attributes, Status.unexpectedError)
+                }
+                attributes[AttributeAccessControl] = accessControl
+            } else {
+                print("Unavailable 'Touch ID integration' on OS X versions prior to 10.10.")
+            }
+        } else {
+            attributes[AttributeAccessible] = accessibility.rawValue
+        }
+
+        attributes[AttributeSynchronizable] = synchronizable ? kCFBooleanTrue : kCFBooleanFalse
+
+        return (attributes, nil)
+    }
+}
+
+// MARK:
+
+extension Attributes: CustomStringConvertible, CustomDebugStringConvertible {
+    public var description: String {
+        return "\(attributes)"
+    }
+
+    public var debugDescription: String {
+        return description
+    }
+}
+
+extension ItemClass: RawRepresentable, CustomStringConvertible {
+    public init?(rawValue: String) {
+        switch rawValue {
+        case String(kSecClassGenericPassword):
+            self = .genericPassword
+        case String(kSecClassInternetPassword):
+            self = .internetPassword
+        default:
+            return nil
+        }
+    }
+
+    public var rawValue: String {
+        switch self {
+        case .genericPassword:
+            return String(kSecClassGenericPassword)
+        case .internetPassword:
+            return String(kSecClassInternetPassword)
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .genericPassword:
+            return "GenericPassword"
+        case .internetPassword:
+            return "InternetPassword"
+        }
+    }
+}
+
+extension ProtocolType: RawRepresentable, CustomStringConvertible {
+    public init?(rawValue: String) {
+        switch rawValue {
+        case String(kSecAttrProtocolFTP):
+            self = .ftp
+        case String(kSecAttrProtocolFTPAccount):
+            self = .ftpAccount
+        case String(kSecAttrProtocolHTTP):
+            self = .http
+        case String(kSecAttrProtocolIRC):
+            self = .irc
+        case String(kSecAttrProtocolNNTP):
+            self = .nntp
+        case String(kSecAttrProtocolPOP3):
+            self = .pop3
+        case String(kSecAttrProtocolSMTP):
+            self = .smtp
+        case String(kSecAttrProtocolSOCKS):
+            self = .socks
+        case String(kSecAttrProtocolIMAP):
+            self = .imap
+        case String(kSecAttrProtocolLDAP):
+            self = .ldap
+        case String(kSecAttrProtocolAppleTalk):
+            self = .appleTalk
+        case String(kSecAttrProtocolAFP):
+            self = .afp
+        case String(kSecAttrProtocolTelnet):
+            self = .telnet
+        case String(kSecAttrProtocolSSH):
+            self = .ssh
+        case String(kSecAttrProtocolFTPS):
+            self = .ftps
+        case String(kSecAttrProtocolHTTPS):
+            self = .https
+        case String(kSecAttrProtocolHTTPProxy):
+            self = .httpProxy
+        case String(kSecAttrProtocolHTTPSProxy):
+            self = .httpsProxy
+        case String(kSecAttrProtocolFTPProxy):
+            self = .ftpProxy
+        case String(kSecAttrProtocolSMB):
+            self = .smb
+        case String(kSecAttrProtocolRTSP):
+            self = .rtsp
+        case String(kSecAttrProtocolRTSPProxy):
+            self = .rtspProxy
+        case String(kSecAttrProtocolDAAP):
+            self = .daap
+        case String(kSecAttrProtocolEPPC):
+            self = .eppc
+        case String(kSecAttrProtocolIPP):
+            self = .ipp
+        case String(kSecAttrProtocolNNTPS):
+            self = .nntps
+        case String(kSecAttrProtocolLDAPS):
+            self = .ldaps
+        case String(kSecAttrProtocolTelnetS):
+            self = .telnetS
+        case String(kSecAttrProtocolIMAPS):
+            self = .imaps
+        case String(kSecAttrProtocolIRCS):
+            self = .ircs
+        case String(kSecAttrProtocolPOP3S):
+            self = .pop3S
+        default:
+            return nil
+        }
+    }
+
+    public var rawValue: String {
+        switch self {
+        case .ftp:
+            return String(kSecAttrProtocolFTP)
+        case .ftpAccount:
+            return String(kSecAttrProtocolFTPAccount)
+        case .http:
+            return String(kSecAttrProtocolHTTP)
+        case .irc:
+            return String(kSecAttrProtocolIRC)
+        case .nntp:
+            return String(kSecAttrProtocolNNTP)
+        case .pop3:
+            return String(kSecAttrProtocolPOP3)
+        case .smtp:
+            return String(kSecAttrProtocolSMTP)
+        case .socks:
+            return String(kSecAttrProtocolSOCKS)
+        case .imap:
+            return String(kSecAttrProtocolIMAP)
+        case .ldap:
+            return String(kSecAttrProtocolLDAP)
+        case .appleTalk:
+            return String(kSecAttrProtocolAppleTalk)
+        case .afp:
+            return String(kSecAttrProtocolAFP)
+        case .telnet:
+            return String(kSecAttrProtocolTelnet)
+        case .ssh:
+            return String(kSecAttrProtocolSSH)
+        case .ftps:
+            return String(kSecAttrProtocolFTPS)
+        case .https:
+            return String(kSecAttrProtocolHTTPS)
+        case .httpProxy:
+            return String(kSecAttrProtocolHTTPProxy)
+        case .httpsProxy:
+            return String(kSecAttrProtocolHTTPSProxy)
+        case .ftpProxy:
+            return String(kSecAttrProtocolFTPProxy)
+        case .smb:
+            return String(kSecAttrProtocolSMB)
+        case .rtsp:
+            return String(kSecAttrProtocolRTSP)
+        case .rtspProxy:
+            return String(kSecAttrProtocolRTSPProxy)
+        case .daap:
+            return String(kSecAttrProtocolDAAP)
+        case .eppc:
+            return String(kSecAttrProtocolEPPC)
+        case .ipp:
+            return String(kSecAttrProtocolIPP)
+        case .nntps:
+            return String(kSecAttrProtocolNNTPS)
+        case .ldaps:
+            return String(kSecAttrProtocolLDAPS)
+        case .telnetS:
+            return String(kSecAttrProtocolTelnetS)
+        case .imaps:
+            return String(kSecAttrProtocolIMAPS)
+        case .ircs:
+            return String(kSecAttrProtocolIRCS)
+        case .pop3S:
+            return String(kSecAttrProtocolPOP3S)
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .ftp:
+            return "FTP"
+        case .ftpAccount:
+            return "FTPAccount"
+        case .http:
+            return "HTTP"
+        case .irc:
+            return "IRC"
+        case .nntp:
+            return "NNTP"
+        case .pop3:
+            return "POP3"
+        case .smtp:
+            return "SMTP"
+        case .socks:
+            return "SOCKS"
+        case .imap:
+            return "IMAP"
+        case .ldap:
+            return "LDAP"
+        case .appleTalk:
+            return "AppleTalk"
+        case .afp:
+            return "AFP"
+        case .telnet:
+            return "Telnet"
+        case .ssh:
+            return "SSH"
+        case .ftps:
+            return "FTPS"
+        case .https:
+            return "HTTPS"
+        case .httpProxy:
+            return "HTTPProxy"
+        case .httpsProxy:
+            return "HTTPSProxy"
+        case .ftpProxy:
+            return "FTPProxy"
+        case .smb:
+            return "SMB"
+        case .rtsp:
+            return "RTSP"
+        case .rtspProxy:
+            return "RTSPProxy"
+        case .daap:
+            return "DAAP"
+        case .eppc:
+            return "EPPC"
+        case .ipp:
+            return "IPP"
+        case .nntps:
+            return "NNTPS"
+        case .ldaps:
+            return "LDAPS"
+        case .telnetS:
+            return "TelnetS"
+        case .imaps:
+            return "IMAPS"
+        case .ircs:
+            return "IRCS"
+        case .pop3S:
+            return "POP3S"
+        }
+    }
+}
+
+extension AuthenticationType: RawRepresentable, CustomStringConvertible {
+    public init?(rawValue: String) {
+        switch rawValue {
+        case String(kSecAttrAuthenticationTypeNTLM):
+            self = .ntlm
+        case String(kSecAttrAuthenticationTypeMSN):
+            self = .msn
+        case String(kSecAttrAuthenticationTypeDPA):
+            self = .dpa
+        case String(kSecAttrAuthenticationTypeRPA):
+            self = .rpa
+        case String(kSecAttrAuthenticationTypeHTTPBasic):
+            self = .httpBasic
+        case String(kSecAttrAuthenticationTypeHTTPDigest):
+            self = .httpDigest
+        case String(kSecAttrAuthenticationTypeHTMLForm):
+            self = .htmlForm
+        case String(kSecAttrAuthenticationTypeDefault):
+            self = .`default`
+        default:
+            return nil
+        }
+    }
+
+    public var rawValue: String {
+        switch self {
+        case .ntlm:
+            return String(kSecAttrAuthenticationTypeNTLM)
+        case .msn:
+            return String(kSecAttrAuthenticationTypeMSN)
+        case .dpa:
+            return String(kSecAttrAuthenticationTypeDPA)
+        case .rpa:
+            return String(kSecAttrAuthenticationTypeRPA)
+        case .httpBasic:
+            return String(kSecAttrAuthenticationTypeHTTPBasic)
+        case .httpDigest:
+            return String(kSecAttrAuthenticationTypeHTTPDigest)
+        case .htmlForm:
+            return String(kSecAttrAuthenticationTypeHTMLForm)
+        case .`default`:
+            return String(kSecAttrAuthenticationTypeDefault)
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .ntlm:
+            return "NTLM"
+        case .msn:
+            return "MSN"
+        case .dpa:
+            return "DPA"
+        case .rpa:
+            return "RPA"
+        case .httpBasic:
+            return "HTTPBasic"
+        case .httpDigest:
+            return "HTTPDigest"
+        case .htmlForm:
+            return "HTMLForm"
+        case .`default`:
+            return "Default"
+        }
+    }
+}
+
+extension Accessibility: RawRepresentable, CustomStringConvertible {
+    public init?(rawValue: String) {
+        if #available(OSX 10.10, *) {
+            switch rawValue {
+            case String(kSecAttrAccessibleWhenUnlocked):
+                self = .whenUnlocked
+            case String(kSecAttrAccessibleAfterFirstUnlock):
+                self = .afterFirstUnlock
+            #if !targetEnvironment(macCatalyst)
+            case String(kSecAttrAccessibleAlways):
+                self = .always
+            #endif
+            case String(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly):
+                self = .whenPasscodeSetThisDeviceOnly
+            case String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly):
+                self = .whenUnlockedThisDeviceOnly
+            case String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly):
+                self = .afterFirstUnlockThisDeviceOnly
+            #if !targetEnvironment(macCatalyst)
+            case String(kSecAttrAccessibleAlwaysThisDeviceOnly):
+                self = .alwaysThisDeviceOnly
+            #endif
+            default:
+                return nil
+            }
+        } else {
+            switch rawValue {
+            case String(kSecAttrAccessibleWhenUnlocked):
+                self = .whenUnlocked
+            case String(kSecAttrAccessibleAfterFirstUnlock):
+                self = .afterFirstUnlock
+            #if !targetEnvironment(macCatalyst)
+            case String(kSecAttrAccessibleAlways):
+                self = .always
+            #endif
+            case String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly):
+                self = .whenUnlockedThisDeviceOnly
+            case String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly):
+                self = .afterFirstUnlockThisDeviceOnly
+            #if !targetEnvironment(macCatalyst)
+            case String(kSecAttrAccessibleAlwaysThisDeviceOnly):
+                self = .alwaysThisDeviceOnly
+            #endif
+            default:
+                return nil
+            }
+        }
+    }
+
+    public var rawValue: String {
+        switch self {
+        case .whenUnlocked:
+            return String(kSecAttrAccessibleWhenUnlocked)
+        case .afterFirstUnlock:
+            return String(kSecAttrAccessibleAfterFirstUnlock)
+        #if !targetEnvironment(macCatalyst)
+        case .always:
+            return String(kSecAttrAccessibleAlways)
+        #endif
+        case .whenPasscodeSetThisDeviceOnly:
+            if #available(OSX 10.10, *) {
+                return String(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly)
+            } else {
+                fatalError("'Accessibility.WhenPasscodeSetThisDeviceOnly' is not available on this version of OS.")
+            }
+        case .whenUnlockedThisDeviceOnly:
+            return String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly)
+        case .afterFirstUnlockThisDeviceOnly:
+            return String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
+        #if !targetEnvironment(macCatalyst)
+        case .alwaysThisDeviceOnly:
+            return String(kSecAttrAccessibleAlwaysThisDeviceOnly)
+        #endif
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .whenUnlocked:
+            return "WhenUnlocked"
+        case .afterFirstUnlock:
+            return "AfterFirstUnlock"
+        #if !targetEnvironment(macCatalyst)
+        case .always:
+            return "Always"
+        #endif
+        case .whenPasscodeSetThisDeviceOnly:
+            return "WhenPasscodeSetThisDeviceOnly"
+        case .whenUnlockedThisDeviceOnly:
+            return "WhenUnlockedThisDeviceOnly"
+        case .afterFirstUnlockThisDeviceOnly:
+            return "AfterFirstUnlockThisDeviceOnly"
+        #if !targetEnvironment(macCatalyst)
+        case .alwaysThisDeviceOnly:
+            return "AlwaysThisDeviceOnly"
+        #endif
+        }
+    }
+}
+
+extension CFError {
+    var error: NSError {
+        let domain = CFErrorGetDomain(self) as String
+        let code = CFErrorGetCode(self)
+        let userInfo = CFErrorCopyUserInfo(self) as! [String: Any]
+
+        return NSError(domain: domain, code: code, userInfo: userInfo)
+    }
+}
+
+public enum Status: OSStatus, Error {
+    case success                            = 0
+    case unimplemented                      = -4
+    case diskFull                           = -34
+    case io                                 = -36
+    case opWr                               = -49
+    case param                              = -50
+    case wrPerm                             = -61
+    case allocate                           = -108
+    case userCanceled                       = -128
+    case badReq                             = -909
+    case internalComponent                  = -2070
+    case notAvailable                       = -25291
+    case readOnly                           = -25292
+    case authFailed                         = -25293
+    case noSuchKeychain                     = -25294
+    case invalidKeychain                    = -25295
+    case duplicateKeychain                  = -25296
+    case duplicateCallback                  = -25297
+    case invalidCallback                    = -25298
+    case duplicateItem                      = -25299
+    case itemNotFound                       = -25300
+    case bufferTooSmall                     = -25301
+    case dataTooLarge                       = -25302
+    case noSuchAttr                         = -25303
+    case invalidItemRef                     = -25304
+    case invalidSearchRef                   = -25305
+    case noSuchClass                        = -25306
+    case noDefaultKeychain                  = -25307
+    case interactionNotAllowed              = -25308
+    case readOnlyAttr                       = -25309
+    case wrongSecVersion                    = -25310
+    case keySizeNotAllowed                  = -25311
+    case noStorageModule                    = -25312
+    case noCertificateModule                = -25313
+    case noPolicyModule                     = -25314
+    case interactionRequired                = -25315
+    case dataNotAvailable                   = -25316
+    case dataNotModifiable                  = -25317
+    case createChainFailed                  = -25318
+    case invalidPrefsDomain                 = -25319
+    case inDarkWake                         = -25320
+    case aclNotSimple                       = -25240
+    case policyNotFound                     = -25241
+    case invalidTrustSetting                = -25242
+    case noAccessForItem                    = -25243
+    case invalidOwnerEdit                   = -25244
+    case trustNotAvailable                  = -25245
+    case unsupportedFormat                  = -25256
+    case unknownFormat                      = -25257
+    case keyIsSensitive                     = -25258
+    case multiplePrivKeys                   = -25259
+    case passphraseRequired                 = -25260
+    case invalidPasswordRef                 = -25261
+    case invalidTrustSettings               = -25262
+    case noTrustSettings                    = -25263
+    case pkcs12VerifyFailure                = -25264
+    case invalidCertificate                 = -26265
+    case notSigner                          = -26267
+    case policyDenied                       = -26270
+    case invalidKey                         = -26274
+    case decode                             = -26275
+    case `internal`                         = -26276
+    case unsupportedAlgorithm               = -26268
+    case unsupportedOperation               = -26271
+    case unsupportedPadding                 = -26273
+    case itemInvalidKey                     = -34000
+    case itemInvalidKeyType                 = -34001
+    case itemInvalidValue                   = -34002
+    case itemClassMissing                   = -34003
+    case itemMatchUnsupported               = -34004
+    case useItemListUnsupported             = -34005
+    case useKeychainUnsupported             = -34006
+    case useKeychainListUnsupported         = -34007
+    case returnDataUnsupported              = -34008
+    case returnAttributesUnsupported        = -34009
+    case returnRefUnsupported               = -34010
+    case returnPersitentRefUnsupported      = -34011
+    case valueRefUnsupported                = -34012
+    case valuePersistentRefUnsupported      = -34013
+    case returnMissingPointer               = -34014
+    case matchLimitUnsupported              = -34015
+    case itemIllegalQuery                   = -34016
+    case waitForCallback                    = -34017
+    case missingEntitlement                 = -34018
+    case upgradePending                     = -34019
+    case mpSignatureInvalid                 = -25327
+    case otrTooOld                          = -25328
+    case otrIDTooNew                        = -25329
+    case serviceNotAvailable                = -67585
+    case insufficientClientID               = -67586
+    case deviceReset                        = -67587
+    case deviceFailed                       = -67588
+    case appleAddAppACLSubject              = -67589
+    case applePublicKeyIncomplete           = -67590
+    case appleSignatureMismatch             = -67591
+    case appleInvalidKeyStartDate           = -67592
+    case appleInvalidKeyEndDate             = -67593
+    case conversionError                    = -67594
+    case appleSSLv2Rollback                 = -67595
+    case quotaExceeded                      = -67596
+    case fileTooBig                         = -67597
+    case invalidDatabaseBlob                = -67598
+    case invalidKeyBlob                     = -67599
+    case incompatibleDatabaseBlob           = -67600
+    case incompatibleKeyBlob                = -67601
+    case hostNameMismatch                   = -67602
+    case unknownCriticalExtensionFlag       = -67603
+    case noBasicConstraints                 = -67604
+    case noBasicConstraintsCA               = -67605
+    case invalidAuthorityKeyID              = -67606
+    case invalidSubjectKeyID                = -67607
+    case invalidKeyUsageForPolicy           = -67608
+    case invalidExtendedKeyUsage            = -67609
+    case invalidIDLinkage                   = -67610
+    case pathLengthConstraintExceeded       = -67611
+    case invalidRoot                        = -67612
+    case crlExpired                         = -67613
+    case crlNotValidYet                     = -67614
+    case crlNotFound                        = -67615
+    case crlServerDown                      = -67616
+    case crlBadURI                          = -67617
+    case unknownCertExtension               = -67618
+    case unknownCRLExtension                = -67619
+    case crlNotTrusted                      = -67620
+    case crlPolicyFailed                    = -67621
+    case idpFailure                         = -67622
+    case smimeEmailAddressesNotFound        = -67623
+    case smimeBadExtendedKeyUsage           = -67624
+    case smimeBadKeyUsage                   = -67625
+    case smimeKeyUsageNotCritical           = -67626
+    case smimeNoEmailAddress                = -67627
+    case smimeSubjAltNameNotCritical        = -67628
+    case sslBadExtendedKeyUsage             = -67629
+    case ocspBadResponse                    = -67630
+    case ocspBadRequest                     = -67631
+    case ocspUnavailable                    = -67632
+    case ocspStatusUnrecognized             = -67633
+    case endOfData                          = -67634
+    case incompleteCertRevocationCheck      = -67635
+    case networkFailure                     = -67636
+    case ocspNotTrustedToAnchor             = -67637
+    case recordModified                     = -67638
+    case ocspSignatureError                 = -67639
+    case ocspNoSigner                       = -67640
+    case ocspResponderMalformedReq          = -67641
+    case ocspResponderInternalError         = -67642
+    case ocspResponderTryLater              = -67643
+    case ocspResponderSignatureRequired     = -67644
+    case ocspResponderUnauthorized          = -67645
+    case ocspResponseNonceMismatch          = -67646
+    case codeSigningBadCertChainLength      = -67647
+    case codeSigningNoBasicConstraints      = -67648
+    case codeSigningBadPathLengthConstraint = -67649
+    case codeSigningNoExtendedKeyUsage      = -67650
+    case codeSigningDevelopment             = -67651
+    case resourceSignBadCertChainLength     = -67652
+    case resourceSignBadExtKeyUsage         = -67653
+    case trustSettingDeny                   = -67654
+    case invalidSubjectName                 = -67655
+    case unknownQualifiedCertStatement      = -67656
+    case mobileMeRequestQueued              = -67657
+    case mobileMeRequestRedirected          = -67658
+    case mobileMeServerError                = -67659
+    case mobileMeServerNotAvailable         = -67660
+    case mobileMeServerAlreadyExists        = -67661
+    case mobileMeServerServiceErr           = -67662
+    case mobileMeRequestAlreadyPending      = -67663
+    case mobileMeNoRequestPending           = -67664
+    case mobileMeCSRVerifyFailure           = -67665
+    case mobileMeFailedConsistencyCheck     = -67666
+    case notInitialized                     = -67667
+    case invalidHandleUsage                 = -67668
+    case pvcReferentNotFound                = -67669
+    case functionIntegrityFail              = -67670
+    case internalError                      = -67671
+    case memoryError                        = -67672
+    case invalidData                        = -67673
+    case mdsError                           = -67674
+    case invalidPointer                     = -67675
+    case selfCheckFailed                    = -67676
+    case functionFailed                     = -67677
+    case moduleManifestVerifyFailed         = -67678
+    case invalidGUID                        = -67679
+    case invalidHandle                      = -67680
+    case invalidDBList                      = -67681
+    case invalidPassthroughID               = -67682
+    case invalidNetworkAddress              = -67683
+    case crlAlreadySigned                   = -67684
+    case invalidNumberOfFields              = -67685
+    case verificationFailure                = -67686
+    case unknownTag                         = -67687
+    case invalidSignature                   = -67688
+    case invalidName                        = -67689
+    case invalidCertificateRef              = -67690
+    case invalidCertificateGroup            = -67691
+    case tagNotFound                        = -67692
+    case invalidQuery                       = -67693
+    case invalidValue                       = -67694
+    case callbackFailed                     = -67695
+    case aclDeleteFailed                    = -67696
+    case aclReplaceFailed                   = -67697
+    case aclAddFailed                       = -67698
+    case aclChangeFailed                    = -67699
+    case invalidAccessCredentials           = -67700
+    case invalidRecord                      = -67701
+    case invalidACL                         = -67702
+    case invalidSampleValue                 = -67703
+    case incompatibleVersion                = -67704
+    case privilegeNotGranted                = -67705
+    case invalidScope                       = -67706
+    case pvcAlreadyConfigured               = -67707
+    case invalidPVC                         = -67708
+    case emmLoadFailed                      = -67709
+    case emmUnloadFailed                    = -67710
+    case addinLoadFailed                    = -67711
+    case invalidKeyRef                      = -67712
+    case invalidKeyHierarchy                = -67713
+    case addinUnloadFailed                  = -67714
+    case libraryReferenceNotFound           = -67715
+    case invalidAddinFunctionTable          = -67716
+    case invalidServiceMask                 = -67717
+    case moduleNotLoaded                    = -67718
+    case invalidSubServiceID                = -67719
+    case attributeNotInContext              = -67720
+    case moduleManagerInitializeFailed      = -67721
+    case moduleManagerNotFound              = -67722
+    case eventNotificationCallbackNotFound  = -67723
+    case inputLengthError                   = -67724
+    case outputLengthError                  = -67725
+    case privilegeNotSupported              = -67726
+    case deviceError                        = -67727
+    case attachHandleBusy                   = -67728
+    case notLoggedIn                        = -67729
+    case algorithmMismatch                  = -67730
+    case keyUsageIncorrect                  = -67731
+    case keyBlobTypeIncorrect               = -67732
+    case keyHeaderInconsistent              = -67733
+    case unsupportedKeyFormat               = -67734
+    case unsupportedKeySize                 = -67735
+    case invalidKeyUsageMask                = -67736
+    case unsupportedKeyUsageMask            = -67737
+    case invalidKeyAttributeMask            = -67738
+    case unsupportedKeyAttributeMask        = -67739
+    case invalidKeyLabel                    = -67740
+    case unsupportedKeyLabel                = -67741
+    case invalidKeyFormat                   = -67742
+    case unsupportedVectorOfBuffers         = -67743
+    case invalidInputVector                 = -67744
+    case invalidOutputVector                = -67745
+    case invalidContext                     = -67746
+    case invalidAlgorithm                   = -67747
+    case invalidAttributeKey                = -67748
+    case missingAttributeKey                = -67749
+    case invalidAttributeInitVector         = -67750
+    case missingAttributeInitVector         = -67751
+    case invalidAttributeSalt               = -67752
+    case missingAttributeSalt               = -67753
+    case invalidAttributePadding            = -67754
+    case missingAttributePadding            = -67755
+    case invalidAttributeRandom             = -67756
+    case missingAttributeRandom             = -67757
+    case invalidAttributeSeed               = -67758
+    case missingAttributeSeed               = -67759
+    case invalidAttributePassphrase         = -67760
+    case missingAttributePassphrase         = -67761
+    case invalidAttributeKeyLength          = -67762
+    case missingAttributeKeyLength          = -67763
+    case invalidAttributeBlockSize          = -67764
+    case missingAttributeBlockSize          = -67765
+    case invalidAttributeOutputSize         = -67766
+    case missingAttributeOutputSize         = -67767
+    case invalidAttributeRounds             = -67768
+    case missingAttributeRounds             = -67769
+    case invalidAlgorithmParms              = -67770
+    case missingAlgorithmParms              = -67771
+    case invalidAttributeLabel              = -67772
+    case missingAttributeLabel              = -67773
+    case invalidAttributeKeyType            = -67774
+    case missingAttributeKeyType            = -67775
+    case invalidAttributeMode               = -67776
+    case missingAttributeMode               = -67777
+    case invalidAttributeEffectiveBits      = -67778
+    case missingAttributeEffectiveBits      = -67779
+    case invalidAttributeStartDate          = -67780
+    case missingAttributeStartDate          = -67781
+    case invalidAttributeEndDate            = -67782
+    case missingAttributeEndDate            = -67783
+    case invalidAttributeVersion            = -67784
+    case missingAttributeVersion            = -67785
+    case invalidAttributePrime              = -67786
+    case missingAttributePrime              = -67787
+    case invalidAttributeBase               = -67788
+    case missingAttributeBase               = -67789
+    case invalidAttributeSubprime           = -67790
+    case missingAttributeSubprime           = -67791
+    case invalidAttributeIterationCount     = -67792
+    case missingAttributeIterationCount     = -67793
+    case invalidAttributeDLDBHandle         = -67794
+    case missingAttributeDLDBHandle         = -67795
+    case invalidAttributeAccessCredentials  = -67796
+    case missingAttributeAccessCredentials  = -67797
+    case invalidAttributePublicKeyFormat    = -67798
+    case missingAttributePublicKeyFormat    = -67799
+    case invalidAttributePrivateKeyFormat   = -67800
+    case missingAttributePrivateKeyFormat   = -67801
+    case invalidAttributeSymmetricKeyFormat = -67802
+    case missingAttributeSymmetricKeyFormat = -67803
+    case invalidAttributeWrappedKeyFormat   = -67804
+    case missingAttributeWrappedKeyFormat   = -67805
+    case stagedOperationInProgress          = -67806
+    case stagedOperationNotStarted          = -67807
+    case verifyFailed                       = -67808
+    case querySizeUnknown                   = -67809
+    case blockSizeMismatch                  = -67810
+    case publicKeyInconsistent              = -67811
+    case deviceVerifyFailed                 = -67812
+    case invalidLoginName                   = -67813
+    case alreadyLoggedIn                    = -67814
+    case invalidDigestAlgorithm             = -67815
+    case invalidCRLGroup                    = -67816
+    case certificateCannotOperate           = -67817
+    case certificateExpired                 = -67818
+    case certificateNotValidYet             = -67819
+    case certificateRevoked                 = -67820
+    case certificateSuspended               = -67821
+    case insufficientCredentials            = -67822
+    case invalidAction                      = -67823
+    case invalidAuthority                   = -67824
+    case verifyActionFailed                 = -67825
+    case invalidCertAuthority               = -67826
+    case invaldCRLAuthority                 = -67827
+    case invalidCRLEncoding                 = -67828
+    case invalidCRLType                     = -67829
+    case invalidCRL                         = -67830
+    case invalidFormType                    = -67831
+    case invalidID                          = -67832
+    case invalidIdentifier                  = -67833
+    case invalidIndex                       = -67834
+    case invalidPolicyIdentifiers           = -67835
+    case invalidTimeString                  = -67836
+    case invalidReason                      = -67837
+    case invalidRequestInputs               = -67838
+    case invalidResponseVector              = -67839
+    case invalidStopOnPolicy                = -67840
+    case invalidTuple                       = -67841
+    case multipleValuesUnsupported          = -67842
+    case notTrusted                         = -67843
+    case noDefaultAuthority                 = -67844
+    case rejectedForm                       = -67845
+    case requestLost                        = -67846
+    case requestRejected                    = -67847
+    case unsupportedAddressType             = -67848
+    case unsupportedService                 = -67849
+    case invalidTupleGroup                  = -67850
+    case invalidBaseACLs                    = -67851
+    case invalidTupleCredendtials           = -67852
+    case invalidEncoding                    = -67853
+    case invalidValidityPeriod              = -67854
+    case invalidRequestor                   = -67855
+    case requestDescriptor                  = -67856
+    case invalidBundleInfo                  = -67857
+    case invalidCRLIndex                    = -67858
+    case noFieldValues                      = -67859
+    case unsupportedFieldFormat             = -67860
+    case unsupportedIndexInfo               = -67861
+    case unsupportedLocality                = -67862
+    case unsupportedNumAttributes           = -67863
+    case unsupportedNumIndexes              = -67864
+    case unsupportedNumRecordTypes          = -67865
+    case fieldSpecifiedMultiple             = -67866
+    case incompatibleFieldFormat            = -67867
+    case invalidParsingModule               = -67868
+    case databaseLocked                     = -67869
+    case datastoreIsOpen                    = -67870
+    case missingValue                       = -67871
+    case unsupportedQueryLimits             = -67872
+    case unsupportedNumSelectionPreds       = -67873
+    case unsupportedOperator                = -67874
+    case invalidDBLocation                  = -67875
+    case invalidAccessRequest               = -67876
+    case invalidIndexInfo                   = -67877
+    case invalidNewOwner                    = -67878
+    case invalidModifyMode                  = -67879
+    case missingRequiredExtension           = -67880
+    case extendedKeyUsageNotCritical        = -67881
+    case timestampMissing                   = -67882
+    case timestampInvalid                   = -67883
+    case timestampNotTrusted                = -67884
+    case timestampServiceNotAvailable       = -67885
+    case timestampBadAlg                    = -67886
+    case timestampBadRequest                = -67887
+    case timestampBadDataFormat             = -67888
+    case timestampTimeNotAvailable          = -67889
+    case timestampUnacceptedPolicy          = -67890
+    case timestampUnacceptedExtension       = -67891
+    case timestampAddInfoNotAvailable       = -67892
+    case timestampSystemFailure             = -67893
+    case signingTimeMissing                 = -67894
+    case timestampRejection                 = -67895
+    case timestampWaiting                   = -67896
+    case timestampRevocationWarning         = -67897
+    case timestampRevocationNotification    = -67898
+    case unexpectedError                    = -99999
+}
+
+extension Status: RawRepresentable, CustomStringConvertible {
+
+    public init(status: OSStatus) {
+        if let mappedStatus = Status(rawValue: status) {
+            self = mappedStatus
+        } else {
+            self = .unexpectedError
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .success:
+            return "No error."
+        case .unimplemented:
+            return "Function or operation not implemented."
+        case .diskFull:
+            return "The disk is full."
+        case .io:
+            return "I/O error (bummers)"
+        case .opWr:
+            return "file already open with with write permission"
+        case .param:
+            return "One or more parameters passed to a function were not valid."
+        case .wrPerm:
+            return "write permissions error"
+        case .allocate:
+            return "Failed to allocate memory."
+        case .userCanceled:
+            return "User canceled the operation."
+        case .badReq:
+            return "Bad parameter or invalid state for operation."
+        case .internalComponent:
+            return ""
+        case .notAvailable:
+            return "No keychain is available. You may need to restart your computer."
+        case .readOnly:
+            return "This keychain cannot be modified."
+        case .authFailed:
+            return "The user name or passphrase you entered is not correct."
+        case .noSuchKeychain:
+            return "The specified keychain could not be found."
+        case .invalidKeychain:
+            return "The specified keychain is not a valid keychain file."
+        case .duplicateKeychain:
+            return "A keychain with the same name already exists."
+        case .duplicateCallback:
+            return "The specified callback function is already installed."
+        case .invalidCallback:
+            return "The specified callback function is not valid."
+        case .duplicateItem:
+            return "The specified item already exists in the keychain."
+        case .itemNotFound:
+            return "The specified item could not be found in the keychain."
+        case .bufferTooSmall:
+            return "There is not enough memory available to use the specified item."
+        case .dataTooLarge:
+            return "This item contains information which is too large or in a format that cannot be displayed."
+        case .noSuchAttr:
+            return "The specified attribute does not exist."
+        case .invalidItemRef:
+            return "The specified item is no longer valid. It may have been deleted from the keychain."
+        case .invalidSearchRef:
+            return "Unable to search the current keychain."
+        case .noSuchClass:
+            return "The specified item does not appear to be a valid keychain item."
+        case .noDefaultKeychain:
+            return "A default keychain could not be found."
+        case .interactionNotAllowed:
+            return "User interaction is not allowed."
+        case .readOnlyAttr:
+            return "The specified attribute could not be modified."
+        case .wrongSecVersion:
+            return "This keychain was created by a different version of the system software and cannot be opened."
+        case .keySizeNotAllowed:
+            return "This item specifies a key size which is too large."
+        case .noStorageModule:
+            return "A required component (data storage module) could not be loaded. You may need to restart your computer."
+        case .noCertificateModule:
+            return "A required component (certificate module) could not be loaded. You may need to restart your computer."
+        case .noPolicyModule:
+            return "A required component (policy module) could not be loaded. You may need to restart your computer."
+        case .interactionRequired:
+            return "User interaction is required, but is currently not allowed."
+        case .dataNotAvailable:
+            return "The contents of this item cannot be retrieved."
+        case .dataNotModifiable:
+            return "The contents of this item cannot be modified."
+        case .createChainFailed:
+            return "One or more certificates required to validate this certificate cannot be found."
+        case .invalidPrefsDomain:
+            return "The specified preferences domain is not valid."
+        case .inDarkWake:
+            return "In dark wake, no UI possible"
+        case .aclNotSimple:
+            return "The specified access control list is not in standard (simple) form."
+        case .policyNotFound:
+            return "The specified policy cannot be found."
+        case .invalidTrustSetting:
+            return "The specified trust setting is invalid."
+        case .noAccessForItem:
+            return "The specified item has no access control."
+        case .invalidOwnerEdit:
+            return "Invalid attempt to change the owner of this item."
+        case .trustNotAvailable:
+            return "No trust results are available."
+        case .unsupportedFormat:
+            return "Import/Export format unsupported."
+        case .unknownFormat:
+            return "Unknown format in import."
+        case .keyIsSensitive:
+            return "Key material must be wrapped for export."
+        case .multiplePrivKeys:
+            return "An attempt was made to import multiple private keys."
+        case .passphraseRequired:
+            return "Passphrase is required for import/export."
+        case .invalidPasswordRef:
+            return "The password reference was invalid."
+        case .invalidTrustSettings:
+            return "The Trust Settings Record was corrupted."
+        case .noTrustSettings:
+            return "No Trust Settings were found."
+        case .pkcs12VerifyFailure:
+            return "MAC verification failed during PKCS12 import (wrong password?)"
+        case .invalidCertificate:
+            return "This certificate could not be decoded."
+        case .notSigner:
+            return "A certificate was not signed by its proposed parent."
+        case .policyDenied:
+            return "The certificate chain was not trusted due to a policy not accepting it."
+        case .invalidKey:
+            return "The provided key material was not valid."
+        case .decode:
+            return "Unable to decode the provided data."
+        case .`internal`:
+            return "An internal error occurred in the Security framework."
+        case .unsupportedAlgorithm:
+            return "An unsupported algorithm was encountered."
+        case .unsupportedOperation:
+            return "The operation you requested is not supported by this key."
+        case .unsupportedPadding:
+            return "The padding you requested is not supported."
+        case .itemInvalidKey:
+            return "A string key in dictionary is not one of the supported keys."
+        case .itemInvalidKeyType:
+            return "A key in a dictionary is neither a CFStringRef nor a CFNumberRef."
+        case .itemInvalidValue:
+            return "A value in a dictionary is an invalid (or unsupported) CF type."
+        case .itemClassMissing:
+            return "No kSecItemClass key was specified in a dictionary."
+        case .itemMatchUnsupported:
+            return "The caller passed one or more kSecMatch keys to a function which does not support matches."
+        case .useItemListUnsupported:
+            return "The caller passed in a kSecUseItemList key to a function which does not support it."
+        case .useKeychainUnsupported:
+            return "The caller passed in a kSecUseKeychain key to a function which does not support it."
+        case .useKeychainListUnsupported:
+            return "The caller passed in a kSecUseKeychainList key to a function which does not support it."
+        case .returnDataUnsupported:
+            return "The caller passed in a kSecReturnData key to a function which does not support it."
+        case .returnAttributesUnsupported:
+            return "The caller passed in a kSecReturnAttributes key to a function which does not support it."
+        case .returnRefUnsupported:
+            return "The caller passed in a kSecReturnRef key to a function which does not support it."
+        case .returnPersitentRefUnsupported:
+            return "The caller passed in a kSecReturnPersistentRef key to a function which does not support it."
+        case .valueRefUnsupported:
+            return "The caller passed in a kSecValueRef key to a function which does not support it."
+        case .valuePersistentRefUnsupported:
+            return "The caller passed in a kSecValuePersistentRef key to a function which does not support it."
+        case .returnMissingPointer:
+            return "The caller passed asked for something to be returned but did not pass in a result pointer."
+        case .matchLimitUnsupported:
+            return "The caller passed in a kSecMatchLimit key to a call which does not support limits."
+        case .itemIllegalQuery:
+            return "The caller passed in a query which contained too many keys."
+        case .waitForCallback:
+            return "This operation is incomplete, until the callback is invoked (not an error)."
+        case .missingEntitlement:
+            return "Internal error when a required entitlement isn't present, client has neither application-identifier nor keychain-access-groups entitlements."
+        case .upgradePending:
+            return "Error returned if keychain database needs a schema migration but the device is locked, clients should wait for a device unlock notification and retry the command."
+        case .mpSignatureInvalid:
+            return "Signature invalid on MP message"
+        case .otrTooOld:
+            return "Message is too old to use"
+        case .otrIDTooNew:
+            return "Key ID is too new to use! Message from the future?"
+        case .serviceNotAvailable:
+            return "The required service is not available."
+        case .insufficientClientID:
+            return "The client ID is not correct."
+        case .deviceReset:
+            return "A device reset has occurred."
+        case .deviceFailed:
+            return "A device failure has occurred."
+        case .appleAddAppACLSubject:
+            return "Adding an application ACL subject failed."
+        case .applePublicKeyIncomplete:
+            return "The public key is incomplete."
+        case .appleSignatureMismatch:
+            return "A signature mismatch has occurred."
+        case .appleInvalidKeyStartDate:
+            return "The specified key has an invalid start date."
+        case .appleInvalidKeyEndDate:
+            return "The specified key has an invalid end date."
+        case .conversionError:
+            return "A conversion error has occurred."
+        case .appleSSLv2Rollback:
+            return "A SSLv2 rollback error has occurred."
+        case .quotaExceeded:
+            return "The quota was exceeded."
+        case .fileTooBig:
+            return "The file is too big."
+        case .invalidDatabaseBlob:
+            return "The specified database has an invalid blob."
+        case .invalidKeyBlob:
+            return "The specified database has an invalid key blob."
+        case .incompatibleDatabaseBlob:
+            return "The specified database has an incompatible blob."
+        case .incompatibleKeyBlob:
+            return "The specified database has an incompatible key blob."
+        case .hostNameMismatch:
+            return "A host name mismatch has occurred."
+        case .unknownCriticalExtensionFlag:
+            return "There is an unknown critical extension flag."
+        case .noBasicConstraints:
+            return "No basic constraints were found."
+        case .noBasicConstraintsCA:
+            return "No basic CA constraints were found."
+        case .invalidAuthorityKeyID:
+            return "The authority key ID is not valid."
+        case .invalidSubjectKeyID:
+            return "The subject key ID is not valid."
+        case .invalidKeyUsageForPolicy:
+            return "The key usage is not valid for the specified policy."
+        case .invalidExtendedKeyUsage:
+            return "The extended key usage is not valid."
+        case .invalidIDLinkage:
+            return "The ID linkage is not valid."
+        case .pathLengthConstraintExceeded:
+            return "The path length constraint was exceeded."
+        case .invalidRoot:
+            return "The root or anchor certificate is not valid."
+        case .crlExpired:
+            return "The CRL has expired."
+        case .crlNotValidYet:
+            return "The CRL is not yet valid."
+        case .crlNotFound:
+            return "The CRL was not found."
+        case .crlServerDown:
+            return "The CRL server is down."
+        case .crlBadURI:
+            return "The CRL has a bad Uniform Resource Identifier."
+        case .unknownCertExtension:
+            return "An unknown certificate extension was encountered."
+        case .unknownCRLExtension:
+            return "An unknown CRL extension was encountered."
+        case .crlNotTrusted:
+            return "The CRL is not trusted."
+        case .crlPolicyFailed:
+            return "The CRL policy failed."
+        case .idpFailure:
+            return "The issuing distribution point was not valid."
+        case .smimeEmailAddressesNotFound:
+            return "An email address mismatch was encountered."
+        case .smimeBadExtendedKeyUsage:
+            return "The appropriate extended key usage for SMIME was not found."
+        case .smimeBadKeyUsage:
+            return "The key usage is not compatible with SMIME."
+        case .smimeKeyUsageNotCritical:
+            return "The key usage extension is not marked as critical."
+        case .smimeNoEmailAddress:
+            return "No email address was found in the certificate."
+        case .smimeSubjAltNameNotCritical:
+            return "The subject alternative name extension is not marked as critical."
+        case .sslBadExtendedKeyUsage:
+            return "The appropriate extended key usage for SSL was not found."
+        case .ocspBadResponse:
+            return "The OCSP response was incorrect or could not be parsed."
+        case .ocspBadRequest:
+            return "The OCSP request was incorrect or could not be parsed."
+        case .ocspUnavailable:
+            return "OCSP service is unavailable."
+        case .ocspStatusUnrecognized:
+            return "The OCSP server did not recognize this certificate."
+        case .endOfData:
+            return "An end-of-data was detected."
+        case .incompleteCertRevocationCheck:
+            return "An incomplete certificate revocation check occurred."
+        case .networkFailure:
+            return "A network failure occurred."
+        case .ocspNotTrustedToAnchor:
+            return "The OCSP response was not trusted to a root or anchor certificate."
+        case .recordModified:
+            return "The record was modified."
+        case .ocspSignatureError:
+            return "The OCSP response had an invalid signature."
+        case .ocspNoSigner:
+            return "The OCSP response had no signer."
+        case .ocspResponderMalformedReq:
+            return "The OCSP responder was given a malformed request."
+        case .ocspResponderInternalError:
+            return "The OCSP responder encountered an internal error."
+        case .ocspResponderTryLater:
+            return "The OCSP responder is busy, try again later."
+        case .ocspResponderSignatureRequired:
+            return "The OCSP responder requires a signature."
+        case .ocspResponderUnauthorized:
+            return "The OCSP responder rejected this request as unauthorized."
+        case .ocspResponseNonceMismatch:
+            return "The OCSP response nonce did not match the request."
+        case .codeSigningBadCertChainLength:
+            return "Code signing encountered an incorrect certificate chain length."
+        case .codeSigningNoBasicConstraints:
+            return "Code signing found no basic constraints."
+        case .codeSigningBadPathLengthConstraint:
+            return "Code signing encountered an incorrect path length constraint."
+        case .codeSigningNoExtendedKeyUsage:
+            return "Code signing found no extended key usage."
+        case .codeSigningDevelopment:
+            return "Code signing indicated use of a development-only certificate."
+        case .resourceSignBadCertChainLength:
+            return "Resource signing has encountered an incorrect certificate chain length."
+        case .resourceSignBadExtKeyUsage:
+            return "Resource signing has encountered an error in the extended key usage."
+        case .trustSettingDeny:
+            return "The trust setting for this policy was set to Deny."
+        case .invalidSubjectName:
+            return "An invalid certificate subject name was encountered."
+        case .unknownQualifiedCertStatement:
+            return "An unknown qualified certificate statement was encountered."
+        case .mobileMeRequestQueued:
+            return "The MobileMe request will be sent during the next connection."
+        case .mobileMeRequestRedirected:
+            return "The MobileMe request was redirected."
+        case .mobileMeServerError:
+            return "A MobileMe server error occurred."
+        case .mobileMeServerNotAvailable:
+            return "The MobileMe server is not available."
+        case .mobileMeServerAlreadyExists:
+            return "The MobileMe server reported that the item already exists."
+        case .mobileMeServerServiceErr:
+            return "A MobileMe service error has occurred."
+        case .mobileMeRequestAlreadyPending:
+            return "A MobileMe request is already pending."
+        case .mobileMeNoRequestPending:
+            return "MobileMe has no request pending."
+        case .mobileMeCSRVerifyFailure:
+            return "A MobileMe CSR verification failure has occurred."
+        case .mobileMeFailedConsistencyCheck:
+            return "MobileMe has found a failed consistency check."
+        case .notInitialized:
+            return "A function was called without initializing CSSM."
+        case .invalidHandleUsage:
+            return "The CSSM handle does not match with the service type."
+        case .pvcReferentNotFound:
+            return "A reference to the calling module was not found in the list of authorized callers."
+        case .functionIntegrityFail:
+            return "A function address was not within the verified module."
+        case .internalError:
+            return "An internal error has occurred."
+        case .memoryError:
+            return "A memory error has occurred."
+        case .invalidData:
+            return "Invalid data was encountered."
+        case .mdsError:
+            return "A Module Directory Service error has occurred."
+        case .invalidPointer:
+            return "An invalid pointer was encountered."
+        case .selfCheckFailed:
+            return "Self-check has failed."
+        case .functionFailed:
+            return "A function has failed."
+        case .moduleManifestVerifyFailed:
+            return "A module manifest verification failure has occurred."
+        case .invalidGUID:
+            return "An invalid GUID was encountered."
+        case .invalidHandle:
+            return "An invalid handle was encountered."
+        case .invalidDBList:
+            return "An invalid DB list was encountered."
+        case .invalidPassthroughID:
+            return "An invalid passthrough ID was encountered."
+        case .invalidNetworkAddress:
+            return "An invalid network address was encountered."
+        case .crlAlreadySigned:
+            return "The certificate revocation list is already signed."
+        case .invalidNumberOfFields:
+            return "An invalid number of fields were encountered."
+        case .verificationFailure:
+            return "A verification failure occurred."
+        case .unknownTag:
+            return "An unknown tag was encountered."
+        case .invalidSignature:
+            return "An invalid signature was encountered."
+        case .invalidName:
+            return "An invalid name was encountered."
+        case .invalidCertificateRef:
+            return "An invalid certificate reference was encountered."
+        case .invalidCertificateGroup:
+            return "An invalid certificate group was encountered."
+        case .tagNotFound:
+            return "The specified tag was not found."
+        case .invalidQuery:
+            return "The specified query was not valid."
+        case .invalidValue:
+            return "An invalid value was detected."
+        case .callbackFailed:
+            return "A callback has failed."
+        case .aclDeleteFailed:
+            return "An ACL delete operation has failed."
+        case .aclReplaceFailed:
+            return "An ACL replace operation has failed."
+        case .aclAddFailed:
+            return "An ACL add operation has failed."
+        case .aclChangeFailed:
+            return "An ACL change operation has failed."
+        case .invalidAccessCredentials:
+            return "Invalid access credentials were encountered."
+        case .invalidRecord:
+            return "An invalid record was encountered."
+        case .invalidACL:
+            return "An invalid ACL was encountered."
+        case .invalidSampleValue:
+            return "An invalid sample value was encountered."
+        case .incompatibleVersion:
+            return "An incompatible version was encountered."
+        case .privilegeNotGranted:
+            return "The privilege was not granted."
+        case .invalidScope:
+            return "An invalid scope was encountered."
+        case .pvcAlreadyConfigured:
+            return "The PVC is already configured."
+        case .invalidPVC:
+            return "An invalid PVC was encountered."
+        case .emmLoadFailed:
+            return "The EMM load has failed."
+        case .emmUnloadFailed:
+            return "The EMM unload has failed."
+        case .addinLoadFailed:
+            return "The add-in load operation has failed."
+        case .invalidKeyRef:
+            return "An invalid key was encountered."
+        case .invalidKeyHierarchy:
+            return "An invalid key hierarchy was encountered."
+        case .addinUnloadFailed:
+            return "The add-in unload operation has failed."
+        case .libraryReferenceNotFound:
+            return "A library reference was not found."
+        case .invalidAddinFunctionTable:
+            return "An invalid add-in function table was encountered."
+        case .invalidServiceMask:
+            return "An invalid service mask was encountered."
+        case .moduleNotLoaded:
+            return "A module was not loaded."
+        case .invalidSubServiceID:
+            return "An invalid subservice ID was encountered."
+        case .attributeNotInContext:
+            return "An attribute was not in the context."
+        case .moduleManagerInitializeFailed:
+            return "A module failed to initialize."
+        case .moduleManagerNotFound:
+            return "A module was not found."
+        case .eventNotificationCallbackNotFound:
+            return "An event notification callback was not found."
+        case .inputLengthError:
+            return "An input length error was encountered."
+        case .outputLengthError:
+            return "An output length error was encountered."
+        case .privilegeNotSupported:
+            return "The privilege is not supported."
+        case .deviceError:
+            return "A device error was encountered."
+        case .attachHandleBusy:
+            return "The CSP handle was busy."
+        case .notLoggedIn:
+            return "You are not logged in."
+        case .algorithmMismatch:
+            return "An algorithm mismatch was encountered."
+        case .keyUsageIncorrect:
+            return "The key usage is incorrect."
+        case .keyBlobTypeIncorrect:
+            return "The key blob type is incorrect."
+        case .keyHeaderInconsistent:
+            return "The key header is inconsistent."
+        case .unsupportedKeyFormat:
+            return "The key header format is not supported."
+        case .unsupportedKeySize:
+            return "The key size is not supported."
+        case .invalidKeyUsageMask:
+            return "The key usage mask is not valid."
+        case .unsupportedKeyUsageMask:
+            return "The key usage mask is not supported."
+        case .invalidKeyAttributeMask:
+            return "The key attribute mask is not valid."
+        case .unsupportedKeyAttributeMask:
+            return "The key attribute mask is not supported."
+        case .invalidKeyLabel:
+            return "The key label is not valid."
+        case .unsupportedKeyLabel:
+            return "The key label is not supported."
+        case .invalidKeyFormat:
+            return "The key format is not valid."
+        case .unsupportedVectorOfBuffers:
+            return "The vector of buffers is not supported."
+        case .invalidInputVector:
+            return "The input vector is not valid."
+        case .invalidOutputVector:
+            return "The output vector is not valid."
+        case .invalidContext:
+            return "An invalid context was encountered."
+        case .invalidAlgorithm:
+            return "An invalid algorithm was encountered."
+        case .invalidAttributeKey:
+            return "A key attribute was not valid."
+        case .missingAttributeKey:
+            return "A key attribute was missing."
+        case .invalidAttributeInitVector:
+            return "An init vector attribute was not valid."
+        case .missingAttributeInitVector:
+            return "An init vector attribute was missing."
+        case .invalidAttributeSalt:
+            return "A salt attribute was not valid."
+        case .missingAttributeSalt:
+            return "A salt attribute was missing."
+        case .invalidAttributePadding:
+            return "A padding attribute was not valid."
+        case .missingAttributePadding:
+            return "A padding attribute was missing."
+        case .invalidAttributeRandom:
+            return "A random number attribute was not valid."
+        case .missingAttributeRandom:
+            return "A random number attribute was missing."
+        case .invalidAttributeSeed:
+            return "A seed attribute was not valid."
+        case .missingAttributeSeed:
+            return "A seed attribute was missing."
+        case .invalidAttributePassphrase:
+            return "A passphrase attribute was not valid."
+        case .missingAttributePassphrase:
+            return "A passphrase attribute was missing."
+        case .invalidAttributeKeyLength:
+            return "A key length attribute was not valid."
+        case .missingAttributeKeyLength:
+            return "A key length attribute was missing."
+        case .invalidAttributeBlockSize:
+            return "A block size attribute was not valid."
+        case .missingAttributeBlockSize:
+            return "A block size attribute was missing."
+        case .invalidAttributeOutputSize:
+            return "An output size attribute was not valid."
+        case .missingAttributeOutputSize:
+            return "An output size attribute was missing."
+        case .invalidAttributeRounds:
+            return "The number of rounds attribute was not valid."
+        case .missingAttributeRounds:
+            return "The number of rounds attribute was missing."
+        case .invalidAlgorithmParms:
+            return "An algorithm parameters attribute was not valid."
+        case .missingAlgorithmParms:
+            return "An algorithm parameters attribute was missing."
+        case .invalidAttributeLabel:
+            return "A label attribute was not valid."
+        case .missingAttributeLabel:
+            return "A label attribute was missing."
+        case .invalidAttributeKeyType:
+            return "A key type attribute was not valid."
+        case .missingAttributeKeyType:
+            return "A key type attribute was missing."
+        case .invalidAttributeMode:
+            return "A mode attribute was not valid."
+        case .missingAttributeMode:
+            return "A mode attribute was missing."
+        case .invalidAttributeEffectiveBits:
+            return "An effective bits attribute was not valid."
+        case .missingAttributeEffectiveBits:
+            return "An effective bits attribute was missing."
+        case .invalidAttributeStartDate:
+            return "A start date attribute was not valid."
+        case .missingAttributeStartDate:
+            return "A start date attribute was missing."
+        case .invalidAttributeEndDate:
+            return "An end date attribute was not valid."
+        case .missingAttributeEndDate:
+            return "An end date attribute was missing."
+        case .invalidAttributeVersion:
+            return "A version attribute was not valid."
+        case .missingAttributeVersion:
+            return "A version attribute was missing."
+        case .invalidAttributePrime:
+            return "A prime attribute was not valid."
+        case .missingAttributePrime:
+            return "A prime attribute was missing."
+        case .invalidAttributeBase:
+            return "A base attribute was not valid."
+        case .missingAttributeBase:
+            return "A base attribute was missing."
+        case .invalidAttributeSubprime:
+            return "A subprime attribute was not valid."
+        case .missingAttributeSubprime:
+            return "A subprime attribute was missing."
+        case .invalidAttributeIterationCount:
+            return "An iteration count attribute was not valid."
+        case .missingAttributeIterationCount:
+            return "An iteration count attribute was missing."
+        case .invalidAttributeDLDBHandle:
+            return "A database handle attribute was not valid."
+        case .missingAttributeDLDBHandle:
+            return "A database handle attribute was missing."
+        case .invalidAttributeAccessCredentials:
+            return "An access credentials attribute was not valid."
+        case .missingAttributeAccessCredentials:
+            return "An access credentials attribute was missing."
+        case .invalidAttributePublicKeyFormat:
+            return "A public key format attribute was not valid."
+        case .missingAttributePublicKeyFormat:
+            return "A public key format attribute was missing."
+        case .invalidAttributePrivateKeyFormat:
+            return "A private key format attribute was not valid."
+        case .missingAttributePrivateKeyFormat:
+            return "A private key format attribute was missing."
+        case .invalidAttributeSymmetricKeyFormat:
+            return "A symmetric key format attribute was not valid."
+        case .missingAttributeSymmetricKeyFormat:
+            return "A symmetric key format attribute was missing."
+        case .invalidAttributeWrappedKeyFormat:
+            return "A wrapped key format attribute was not valid."
+        case .missingAttributeWrappedKeyFormat:
+            return "A wrapped key format attribute was missing."
+        case .stagedOperationInProgress:
+            return "A staged operation is in progress."
+        case .stagedOperationNotStarted:
+            return "A staged operation was not started."
+        case .verifyFailed:
+            return "A cryptographic verification failure has occurred."
+        case .querySizeUnknown:
+            return "The query size is unknown."
+        case .blockSizeMismatch:
+            return "A block size mismatch occurred."
+        case .publicKeyInconsistent:
+            return "The public key was inconsistent."
+        case .deviceVerifyFailed:
+            return "A device verification failure has occurred."
+        case .invalidLoginName:
+            return "An invalid login name was detected."
+        case .alreadyLoggedIn:
+            return "The user is already logged in."
+        case .invalidDigestAlgorithm:
+            return "An invalid digest algorithm was detected."
+        case .invalidCRLGroup:
+            return "An invalid CRL group was detected."
+        case .certificateCannotOperate:
+            return "The certificate cannot operate."
+        case .certificateExpired:
+            return "An expired certificate was detected."
+        case .certificateNotValidYet:
+            return "The certificate is not yet valid."
+        case .certificateRevoked:
+            return "The certificate was revoked."
+        case .certificateSuspended:
+            return "The certificate was suspended."
+        case .insufficientCredentials:
+            return "Insufficient credentials were detected."
+        case .invalidAction:
+            return "The action was not valid."
+        case .invalidAuthority:
+            return "The authority was not valid."
+        case .verifyActionFailed:
+            return "A verify action has failed."
+        case .invalidCertAuthority:
+            return "The certificate authority was not valid."
+        case .invaldCRLAuthority:
+            return "The CRL authority was not valid."
+        case .invalidCRLEncoding:
+            return "The CRL encoding was not valid."
+        case .invalidCRLType:
+            return "The CRL type was not valid."
+        case .invalidCRL:
+            return "The CRL was not valid."
+        case .invalidFormType:
+            return "The form type was not valid."
+        case .invalidID:
+            return "The ID was not valid."
+        case .invalidIdentifier:
+            return "The identifier was not valid."
+        case .invalidIndex:
+            return "The index was not valid."
+        case .invalidPolicyIdentifiers:
+            return "The policy identifiers are not valid."
+        case .invalidTimeString:
+            return "The time specified was not valid."
+        case .invalidReason:
+            return "The trust policy reason was not valid."
+        case .invalidRequestInputs:
+            return "The request inputs are not valid."
+        case .invalidResponseVector:
+            return "The response vector was not valid."
+        case .invalidStopOnPolicy:
+            return "The stop-on policy was not valid."
+        case .invalidTuple:
+            return "The tuple was not valid."
+        case .multipleValuesUnsupported:
+            return "Multiple values are not supported."
+        case .notTrusted:
+            return "The trust policy was not trusted."
+        case .noDefaultAuthority:
+            return "No default authority was detected."
+        case .rejectedForm:
+            return "The trust policy had a rejected form."
+        case .requestLost:
+            return "The request was lost."
+        case .requestRejected:
+            return "The request was rejected."
+        case .unsupportedAddressType:
+            return "The address type is not supported."
+        case .unsupportedService:
+            return "The service is not supported."
+        case .invalidTupleGroup:
+            return "The tuple group was not valid."
+        case .invalidBaseACLs:
+            return "The base ACLs are not valid."
+        case .invalidTupleCredendtials:
+            return "The tuple credentials are not valid."
+        case .invalidEncoding:
+            return "The encoding was not valid."
+        case .invalidValidityPeriod:
+            return "The validity period was not valid."
+        case .invalidRequestor:
+            return "The requestor was not valid."
+        case .requestDescriptor:
+            return "The request descriptor was not valid."
+        case .invalidBundleInfo:
+            return "The bundle information was not valid."
+        case .invalidCRLIndex:
+            return "The CRL index was not valid."
+        case .noFieldValues:
+            return "No field values were detected."
+        case .unsupportedFieldFormat:
+            return "The field format is not supported."
+        case .unsupportedIndexInfo:
+            return "The index information is not supported."
+        case .unsupportedLocality:
+            return "The locality is not supported."
+        case .unsupportedNumAttributes:
+            return "The number of attributes is not supported."
+        case .unsupportedNumIndexes:
+            return "The number of indexes is not supported."
+        case .unsupportedNumRecordTypes:
+            return "The number of record types is not supported."
+        case .fieldSpecifiedMultiple:
+            return "Too many fields were specified."
+        case .incompatibleFieldFormat:
+            return "The field format was incompatible."
+        case .invalidParsingModule:
+            return "The parsing module was not valid."
+        case .databaseLocked:
+            return "The database is locked."
+        case .datastoreIsOpen:
+            return "The data store is open."
+        case .missingValue:
+            return "A missing value was detected."
+        case .unsupportedQueryLimits:
+            return "The query limits are not supported."
+        case .unsupportedNumSelectionPreds:
+            return "The number of selection predicates is not supported."
+        case .unsupportedOperator:
+            return "The operator is not supported."
+        case .invalidDBLocation:
+            return "The database location is not valid."
+        case .invalidAccessRequest:
+            return "The access request is not valid."
+        case .invalidIndexInfo:
+            return "The index information is not valid."
+        case .invalidNewOwner:
+            return "The new owner is not valid."
+        case .invalidModifyMode:
+            return "The modify mode is not valid."
+        case .missingRequiredExtension:
+            return "A required certificate extension is missing."
+        case .extendedKeyUsageNotCritical:
+            return "The extended key usage extension was not marked critical."
+        case .timestampMissing:
+            return "A timestamp was expected but was not found."
+        case .timestampInvalid:
+            return "The timestamp was not valid."
+        case .timestampNotTrusted:
+            return "The timestamp was not trusted."
+        case .timestampServiceNotAvailable:
+            return "The timestamp service is not available."
+        case .timestampBadAlg:
+            return "An unrecognized or unsupported Algorithm Identifier in timestamp."
+        case .timestampBadRequest:
+            return "The timestamp transaction is not permitted or supported."
+        case .timestampBadDataFormat:
+            return "The timestamp data submitted has the wrong format."
+        case .timestampTimeNotAvailable:
+            return "The time source for the Timestamp Authority is not available."
+        case .timestampUnacceptedPolicy:
+            return "The requested policy is not supported by the Timestamp Authority."
+        case .timestampUnacceptedExtension:
+            return "The requested extension is not supported by the Timestamp Authority."
+        case .timestampAddInfoNotAvailable:
+            return "The additional information requested is not available."
+        case .timestampSystemFailure:
+            return "The timestamp request cannot be handled due to system failure."
+        case .signingTimeMissing:
+            return "A signing time was expected but was not found."
+        case .timestampRejection:
+            return "A timestamp transaction was rejected."
+        case .timestampWaiting:
+            return "A timestamp transaction is waiting."
+        case .timestampRevocationWarning:
+            return "A timestamp authority revocation warning was issued."
+        case .timestampRevocationNotification:
+            return "A timestamp authority revocation notification was issued."
+        case .unexpectedError:
+            return "Unexpected error has occurred."
+        }
+    }
+}
+
+extension Status: CustomNSError {
+    public static let errorDomain = KeychainAccessErrorDomain
+
+    public var errorCode: Int {
+        return Int(rawValue)
+    }
+
+    public var errorUserInfo: [String : Any] {
+        return [NSLocalizedDescriptionKey: description]
+    }
+}

+ 635 - 0
Example/Pods/KeychainAccess/README.md

@@ -0,0 +1,635 @@
+# KeychainAccess
+[![CI Status](http://img.shields.io/travis/kishikawakatsumi/KeychainAccess.svg)](https://travis-ci.org/kishikawakatsumi/KeychainAccess)
+[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
+[![SPM supported](https://img.shields.io/badge/SPM-supported-DE5C43.svg?style=flat)](https://swift.org/package-manager)
+[![Version](https://img.shields.io/cocoapods/v/KeychainAccess.svg)](http://cocoadocs.org/docsets/KeychainAccess)
+[![Platform](https://img.shields.io/cocoapods/p/KeychainAccess.svg)](http://cocoadocs.org/docsets/KeychainAccess)
+
+KeychainAccess is a simple Swift wrapper for Keychain that works on iOS and OS X. Makes using Keychain APIs extremely easy and much more palatable to use in Swift.
+
+<img src="https://raw.githubusercontent.com/kishikawakatsumi/KeychainAccess/master/Screenshots/01.png" width="320px" />
+<img src="https://raw.githubusercontent.com/kishikawakatsumi/KeychainAccess/master/Screenshots/02.png" width="320px" />
+<img src="https://raw.githubusercontent.com/kishikawakatsumi/KeychainAccess/master/Screenshots/03.png" width="320px" />
+
+## :bulb: Features
+
+- Simple interface
+- Support access group
+- [Support accessibility](#accessibility)
+- [Support iCloud sharing](#icloud_sharing)
+- **[Support TouchID and Keychain integration (iOS 8+)](#touch_id_integration)**
+- **[Support Shared Web Credentials (iOS 8+)](#shared_web_credentials)**
+- [Works on both iOS & macOS](#requirements)
+- [watchOS and tvOS are supported](#requirements)
+- **[Mac Catalyst is supported](#requirements)**
+- **[Swift 3, 4 and 5 compatible](#requirements)**
+
+## :book: Usage
+
+##### :eyes: See also:  
+- [:link: iOS Example Project](https://github.com/kishikawakatsumi/KeychainAccess/tree/master/Examples/Example-iOS)
+
+### :key: Basics
+
+#### Saving Application Password
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
+```
+
+#### Saving Internet Password
+
+```swift
+let keychain = Keychain(server: "https://github.com", protocolType: .https)
+keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
+```
+
+### :key: Instantiation
+
+#### Create Keychain for Application Password
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+```
+
+```swift
+let keychain = Keychain(service: "com.example.github-token", accessGroup: "12ABCD3E4F.shared")
+```
+
+#### Create Keychain for Internet Password
+
+```swift
+let keychain = Keychain(server: "https://github.com", protocolType: .https)
+```
+
+```swift
+let keychain = Keychain(server: "https://github.com", protocolType: .https, authenticationType: .htmlForm)
+```
+
+### :key: Adding an item
+
+#### subscripting
+
+##### for String
+
+```swift
+keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
+```
+
+```swift
+keychain[string: "kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
+```
+
+##### for NSData
+
+```swift
+keychain[data: "secret"] = NSData(contentsOfFile: "secret.bin")
+```
+
+#### set method
+
+```swift
+keychain.set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+```
+
+#### error handling
+
+```swift
+do {
+    try keychain.set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+}
+catch let error {
+    print(error)
+}
+```
+
+### :key: Obtaining an item
+
+#### subscripting
+
+##### for String (If the value is NSData, attempt to convert to String)
+
+```swift
+let token = keychain["kishikawakatsumi"]
+```
+
+```swift
+let token = keychain[string: "kishikawakatsumi"]
+```
+
+##### for NSData
+
+```swift
+let secretData = keychain[data: "secret"]
+```
+
+#### get methods
+
+##### as String
+
+```swift
+let token = try? keychain.get("kishikawakatsumi")
+```
+
+```swift
+let token = try? keychain.getString("kishikawakatsumi")
+```
+
+##### as NSData
+
+```swift
+let data = try? keychain.getData("kishikawakatsumi")
+```
+
+### :key: Removing an item
+
+#### subscripting
+
+```swift
+keychain["kishikawakatsumi"] = nil
+```
+
+#### remove method
+
+```swift
+do {
+    try keychain.remove("kishikawakatsumi")
+} catch let error {
+    print("error: \(error)")
+}
+```
+
+### :key: Set Label and Comment
+
+```swift
+let keychain = Keychain(server: "https://github.com", protocolType: .https)
+do {
+    try keychain
+        .label("github.com (kishikawakatsumi)")
+        .comment("github access token")
+        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+} catch let error {
+    print("error: \(error)")
+}
+```
+
+### :key: Obtaining Other Attributes
+
+#### PersistentRef
+
+```swift
+let keychain = Keychain()
+let persistentRef = keychain[attributes: "kishikawakatsumi"]?.persistentRef
+...
+```
+
+#### Creation Date
+
+```swift
+let keychain = Keychain()
+let creationDate = keychain[attributes: "kishikawakatsumi"]?.creationDate
+...
+```
+
+#### All Attributes
+
+```swift
+let keychain = Keychain()
+do {
+    let attributes = try keychain.get("kishikawakatsumi") { $0 }
+    print(attributes?.comment)
+    print(attributes?.label)
+    print(attributes?.creator)
+    ...
+} catch let error {
+    print("error: \(error)")
+}
+```
+
+##### subscripting
+
+```swift
+let keychain = Keychain()
+if let attributes = keychain[attributes: "kishikawakatsumi"] {
+    print(attributes.comment)
+    print(attributes.label)
+    print(attributes.creator)
+}
+```
+
+### :key: Configuration (Accessibility, Sharing, iCloud Sync)
+
+**Provides fluent interfaces**
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+    .label("github.com (kishikawakatsumi)")
+    .synchronizable(true)
+    .accessibility(.afterFirstUnlock)
+```
+
+#### <a name="accessibility"> Accessibility
+
+##### Default accessibility matches background application (=kSecAttrAccessibleAfterFirstUnlock)
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+```
+
+##### For background application
+
+###### Creating instance
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+    .accessibility(.afterFirstUnlock)
+
+keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
+```
+
+###### One-shot
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+do {
+    try keychain
+        .accessibility(.afterFirstUnlock)
+        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+} catch let error {
+    print("error: \(error)")
+}
+```
+
+##### For foreground application
+
+###### Creating instance
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+    .accessibility(.whenUnlocked)
+
+keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
+```
+
+###### One-shot
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+do {
+    try keychain
+        .accessibility(.whenUnlocked)
+        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+} catch let error {
+    print("error: \(error)")
+}
+```
+
+#### :couple: Sharing Keychain items
+
+```swift
+let keychain = Keychain(service: "com.example.github-token", accessGroup: "12ABCD3E4F.shared")
+```
+
+#### <a name="icloud_sharing"> :arrows_counterclockwise: Synchronizing Keychain items with iCloud
+
+###### Creating instance
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+    .synchronizable(true)
+
+keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
+```
+
+###### One-shot
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+do {
+    try keychain
+        .synchronizable(true)
+        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+} catch let error {
+    print("error: \(error)")
+}
+```
+
+### <a name="touch_id_integration"> :cyclone: Touch ID (Face ID) integration
+
+**Any Operation that require authentication must be run in the background thread.**  
+**If you run in the main thread, UI thread will lock for the system to try to display the authentication dialog.**
+
+
+**To use Face ID, add `NSFaceIDUsageDescription` key to your `Info.plist`**
+
+#### :closed_lock_with_key: Adding a Touch ID (Face ID) protected item
+
+If you want to store the Touch ID protected Keychain item, specify `accessibility` and `authenticationPolicy` attributes.  
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+DispatchQueue.global().async {
+    do {
+        // Should be the secret invalidated when passcode is removed? If not then use `.WhenUnlocked`
+        try keychain
+            .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .userPresence)
+            .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+    } catch let error {
+        // Error handling if needed...
+    }
+}
+```
+
+#### :closed_lock_with_key: Updating a Touch ID (Face ID) protected item
+
+The same way as when adding.  
+
+**Do not run in the main thread if there is a possibility that the item you are trying to add already exists, and protected.**
+**Because updating protected items requires authentication.**
+
+Additionally, you want to show custom authentication prompt message when updating, specify an `authenticationPrompt` attribute.
+If the item not protected, the `authenticationPrompt` parameter just be ignored.
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+DispatchQueue.global().async {
+    do {
+        // Should be the secret invalidated when passcode is removed? If not then use `.WhenUnlocked`
+        try keychain
+            .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .userPresence)
+            .authenticationPrompt("Authenticate to update your access token")
+            .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
+    } catch let error {
+        // Error handling if needed...
+    }
+}
+```
+
+#### :closed_lock_with_key: Obtaining a Touch ID (Face ID) protected item
+
+The same way as when you get a normal item. It will be displayed automatically Touch ID or passcode authentication If the item you try to get is protected.  
+If you want to show custom authentication prompt message, specify an `authenticationPrompt` attribute.
+If the item not protected, the `authenticationPrompt` parameter just be ignored.
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+DispatchQueue.global().async {
+    do {
+        let password = try keychain
+            .authenticationPrompt("Authenticate to login to server")
+            .get("kishikawakatsumi")
+
+        print("password: \(password)")
+    } catch let error {
+        // Error handling if needed...
+    }
+}
+```
+
+#### :closed_lock_with_key: Removing a Touch ID (Face ID) protected item
+
+The same way as when you remove a normal item.
+There is no way to show Touch ID or passcode authentication when removing Keychain items.
+
+```swift
+let keychain = Keychain(service: "com.example.github-token")
+
+do {
+    try keychain.remove("kishikawakatsumi")
+} catch let error {
+    // Error handling if needed...
+}
+```
+
+### <a name="shared_web_credentials"> :key: Shared Web Credentials
+
+> Shared web credentials is a programming interface that enables native iOS apps to share credentials with their website counterparts. For example, a user may log in to a website in Safari, entering a user name and password, and save those credentials using the iCloud Keychain. Later, the user may run a native app from the same developer, and instead of the app requiring the user to reenter a user name and password, shared web credentials gives it access to the credentials that were entered earlier in Safari. The user can also create new accounts, update passwords, or delete her account from within the app. These changes are then saved and used by Safari.  
+<https://developer.apple.com/library/ios/documentation/Security/Reference/SharedWebCredentialsRef/>
+
+
+```swift
+let keychain = Keychain(server: "https://www.kishikawakatsumi.com", protocolType: .HTTPS)
+
+let username = "kishikawakatsumi@mac.com"
+
+// First, check the credential in the app's Keychain
+if let password = try? keychain.get(username) {
+    // If found password in the Keychain,
+    // then log into the server
+} else {
+    // If not found password in the Keychain,
+    // try to read from Shared Web Credentials
+    keychain.getSharedPassword(username) { (password, error) -> () in
+        if password != nil {
+            // If found password in the Shared Web Credentials,
+            // then log into the server
+            // and save the password to the Keychain
+
+            keychain[username] = password
+        } else {
+            // If not found password either in the Keychain also Shared Web Credentials,
+            // prompt for username and password
+
+            // Log into server
+
+            // If the login is successful,
+            // save the credentials to both the Keychain and the Shared Web Credentials.
+
+            keychain[username] = inputPassword
+            keychain.setSharedPassword(inputPassword, account: username)
+        }
+    }
+}
+```
+
+#### Request all associated domain's credentials
+
+```swift
+Keychain.requestSharedWebCredential { (credentials, error) -> () in
+
+}
+```
+
+#### Generate strong random password
+
+Generate strong random password that is in the same format used by Safari autofill (xxx-xxx-xxx-xxx).
+
+```swift
+let password = Keychain.generatePassword() // => Nhu-GKm-s3n-pMx
+```
+
+#### How to set up Shared Web Credentials
+
+> 1. Add a com.apple.developer.associated-domains entitlement to your app. This entitlement must include all the domains with which you want to share credentials.
+>
+> 2. Add an apple-app-site-association file to your website. This file must include application identifiers for all the apps with which the site wants to share credentials, and it must be properly signed.
+>
+> 3. When the app is installed, the system downloads and verifies the site association file for each of its associated domains. If the verification is successful, the app is associated with the domain.
+
+**More details:**  
+<https://developer.apple.com/library/ios/documentation/Security/Reference/SharedWebCredentialsRef/>
+
+### :mag: Debugging
+
+#### Display all stored items if print keychain object
+
+```swift
+let keychain = Keychain(server: "https://github.com", protocolType: .https)
+print("\(keychain)")
+```
+
+```
+=>
+[
+  [authenticationType: default, key: kishikawakatsumi, server: github.com, class: internetPassword, protocol: https]
+  [authenticationType: default, key: hirohamada, server: github.com, class: internetPassword, protocol: https]
+  [authenticationType: default, key: honeylemon, server: github.com, class: internetPassword, protocol: https]
+]
+```
+
+#### Obtaining all stored keys
+
+```swift
+let keychain = Keychain(server: "https://github.com", protocolType: .https)
+
+let keys = keychain.allKeys()
+for key in keys {
+  print("key: \(key)")
+}
+```
+
+```
+=>
+key: kishikawakatsumi
+key: hirohamada
+key: honeylemon
+```
+
+#### Obtaining all stored items
+
+```swift
+let keychain = Keychain(server: "https://github.com", protocolType: .https)
+
+let items = keychain.allItems()
+for item in items {
+  print("item: \(item)")
+}
+```
+
+```
+=>
+item: [authenticationType: Default, key: kishikawakatsumi, server: github.com, class: InternetPassword, protocol: https]
+item: [authenticationType: Default, key: hirohamada, server: github.com, class: InternetPassword, protocol: https]
+item: [authenticationType: Default, key: honeylemon, server: github.com, class: InternetPassword, protocol: https]
+```
+
+## Keychain sharing capability
+
+If you encounter the error below, you need to add an `Keychain.entitlements`.
+
+```
+OSStatus error:[-34018] Internal error when a required entitlement isn't present, client has neither application-identifier nor keychain-access-groups entitlements.
+```
+
+<img alt="Screen Shot 2019-10-27 at 8 08 50" src="https://user-images.githubusercontent.com/40610/67627108-1a7f2f80-f891-11e9-97bc-7f7313cb63d1.png" width="500">
+
+<img src="https://user-images.githubusercontent.com/40610/67627072-333b1580-f890-11e9-9feb-bf507abc2724.png" width="500" />
+
+## Requirements
+
+|            | OS                                                         | Swift              |
+|------------|------------------------------------------------------------|--------------------|
+| **v1.1.x** | iOS 7+, macOS 10.9+                                        | 1.1                |
+| **v1.2.x** | iOS 7+, macOS 10.9+                                        | 1.2                |
+| **v2.0.x** | iOS 7+, macOS 10.9+, watchOS 2+                            | 2.0                |
+| **v2.1.x** | iOS 7+, macOS 10.9+, watchOS 2+                            | 2.0                |
+| **v2.2.x** | iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+                   | 2.0, 2.1           |
+| **v2.3.x** | iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+                   | 2.0, 2.1, 2.2      |
+| **v2.4.x** | iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+                   | 2.2, 2.3           |
+| **v3.0.x** | iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+                   | 3.x                |
+| **v3.1.x** | iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+                   | 4.0, 4.1, 4.2      |
+| **v3.2.x** | iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+                   | 4.0, 4.1, 4.2, 5.0 |
+| **v4.0.x** | iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+                   | 4.0, 4.1, 4.2, 5.1 |
+| **v4.1.x** | iOS 8+, macOS 10.9+, watchOS 3+, tvOS 9+, Mac Catalyst 13+ | 4.0, 4.1, 4.2, 5.1 |
+
+## Installation
+
+### CocoaPods
+
+KeychainAccess is available through [CocoaPods](http://cocoapods.org). To install
+it, simply add the following lines to your Podfile:
+
+```ruby
+use_frameworks!
+pod 'KeychainAccess'
+```
+
+### Carthage
+
+KeychainAccess is available through [Carthage](https://github.com/Carthage/Carthage). To install
+it, simply add the following line to your Cartfile:
+
+`github "kishikawakatsumi/KeychainAccess"`
+
+### Swift Package Manager
+
+KeychainAccess is also available through [Swift Package Manager](https://github.com/apple/swift-package-manager/).
+
+#### Xcode
+
+Select `File > Swift Packages > Add Package Dependency...`,  
+
+<img src="https://user-images.githubusercontent.com/40610/67627000-2833b580-f88f-11e9-89ef-18819b1a6c67.png" width="800px" />
+
+#### CLI
+
+First, create `Package.swift` that its package declaration includes:
+
+```swift
+// swift-tools-version:5.0
+import PackageDescription
+
+let package = Package(
+    name: "MyLibrary",
+    products: [
+        .library(name: "MyLibrary", targets: ["MyLibrary"]),
+    ],
+    dependencies: [
+        .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "3.0.0"),
+    ],
+    targets: [
+        .target(name: "MyLibrary", dependencies: ["KeychainAccess"]),
+    ]
+)
+```
+
+Then, type
+
+```shell
+$ swift build
+```
+
+### To manually add to your project
+
+1. Add `Lib/KeychainAccess.xcodeproj` to your project
+2. Link `KeychainAccess.framework` with your target
+3. Add `Copy Files Build Phase` to include the framework to your application bundle
+
+_See [iOS Example Project](https://github.com/kishikawakatsumi/KeychainAccess/tree/master/Examples/Example-iOS) as reference._
+
+<img src="https://raw.githubusercontent.com/kishikawakatsumi/KeychainAccess/master/Screenshots/Installation.png" width="800px" />
+
+## Author
+
+kishikawa katsumi, kishikawakatsumi@mac.com
+
+## License
+
+KeychainAccess is available under the MIT license. See the LICENSE file for more info.

+ 22 - 0
Example/Pods/Kingfisher/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Wei Wang
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

+ 240 - 0
Example/Pods/Kingfisher/README.md

@@ -0,0 +1,240 @@
+<p align="center">
+<img src="https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png" alt="Kingfisher" title="Kingfisher" width="557"/>
+</p>
+
+<p align="center">
+<a href="https://github.com/onevcat/Kingfisher/actions?query=workflow%3Abuild"><img src="https://github.com/onevcat/kingfisher/workflows/build/badge.svg?branch=master"></a>
+<a href="https://kingfisher.onevcat.com/"><img src="https://img.shields.io/badge/Swift-Doc-DE5C43.svg?style=flat"></a>
+<a href="https://cocoapods.org/pods/Kingfisher"><img src="https://img.shields.io/cocoapods/v/Kingfisher.svg?style=flat"></a>
+<a href="https://github.com/Carthage/Carthage/"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"></a>
+<a href="https://swift.org/package-manager/"><img src="https://img.shields.io/badge/SPM-supported-DE5C43.svg?style=flat"></a>
+<br />
+<a href="https://raw.githubusercontent.com/onevcat/Kingfisher/master/LICENSE"><img src="https://img.shields.io/cocoapods/l/Kingfisher.svg?style=flat"></a>
+<a href="https://kingfisher.onevcat.com/"><img src="https://img.shields.io/cocoapods/p/Kingfisher.svg?style=flat"></a>
+</p>
+
+Kingfisher is a powerful, pure-Swift library for downloading and caching images from the web. It provides you a chance to use a pure-Swift way to work with remote images in your next app.
+
+## Features
+
+- [x] Asynchronous image downloading and caching.
+- [x] Loading image from either `URLSession`-based networking or local provided data.
+- [x] Useful image processors and filters provided.
+- [x] Multiple-layer hybrid cache for both memory and disk.
+- [x] Fine control on cache behavior. Customizable expiration date and size limit.
+- [x] Cancelable downloading and auto-reusing previous downloaded content to improve performance.
+- [x] Independent components. Use the downloader, caching system, and image processors separately as you need.
+- [x] Prefetching images and showing them from the cache to boost your app.
+- [x] View extensions for `UIImageView`, `NSImageView`, `NSButton` and `UIButton` to directly set an image from a URL.
+- [x] Built-in transition animation when setting images.
+- [x] Customizable placeholder and indicator while loading images.
+- [x] Extensible image processing and image format easily.
+- [x] Low Data Mode support.
+- [x] SwiftUI support.
+
+### Kingfisher 101
+
+The simplest use-case is setting an image to an image view with the `UIImageView` extension:
+
+```swift
+import Kingfisher
+
+let url = URL(string: "https://example.com/image.png")
+imageView.kf.setImage(with: url)
+```
+
+Kingfisher will download the image from `url`, send it to both memory cache and disk cache, and display it in `imageView`. 
+When you set with the same URL later, the image will be retrieved from the cache and shown immediately.
+
+It also works if you use SwiftUI:
+
+```swift
+var body: some View {
+    KFImage(URL(string: "https://example.com/image.png")!)
+}
+```
+
+### A More Advanced Example
+
+With the powerful options, you can do hard tasks with Kingfisher in a simple way. For example, the code below: 
+
+1. Downloads a high-resolution image.
+2. Downsamples it to match the image view size.
+3. Makes it round cornered with a given radius.
+4. Shows a system indicator and a placeholder image while downloading.
+5. When prepared, it animates the small thumbnail image with a "fade in" effect. 
+6. The original large image is also cached to disk for later use, to get rid of downloading it again in a detail view.
+7. A console log is printed when the task finishes, either for success or failure.
+
+```swift
+let url = URL(string: "https://example.com/high_resolution_image.png")
+let processor = DownsamplingImageProcessor(size: imageView.bounds.size)
+             |> RoundCornerImageProcessor(cornerRadius: 20)
+imageView.kf.indicatorType = .activity
+imageView.kf.setImage(
+    with: url,
+    placeholder: UIImage(named: "placeholderImage"),
+    options: [
+        .processor(processor),
+        .scaleFactor(UIScreen.main.scale),
+        .transition(.fade(1)),
+        .cacheOriginalImage
+    ])
+{
+    result in
+    switch result {
+    case .success(let value):
+        print("Task done for: \(value.source.url?.absoluteString ?? "")")
+    case .failure(let error):
+        print("Job failed: \(error.localizedDescription)")
+    }
+}
+```
+
+It is a common situation I can meet in my daily work. Think about how many lines you need to write without
+Kingfisher!
+
+### Method Chaining
+
+If you are not a fan of the `kf` extension, you can also prefer to use the `KF` builder and chained the method 
+invocations. The code below is doing the same thing:
+
+```swift
+// Use `kf` extension
+imageView.kf.setImage(
+    with: url,
+    placeholder: placeholderImage,
+    options: [
+        .processor(processor),
+        .loadDiskFileSynchronously,
+        .cacheOriginalImage,
+        .transition(.fade(0.25)),
+        .lowDataMode(.network(lowResolutionURL))
+    ],
+    progressBlock: { receivedSize, totalSize in
+        // Progress updated
+    },
+    completionHandler: { result in
+        // Done
+    }
+)
+
+// Use `KF` builder
+KF.url(url)
+  .placeholder(placeholderImage)
+  .setProcessor(processor)
+  .loadDiskFileSynchronously()
+  .cacheMemoryOnly()
+  .fade(duration: 0.25)
+  .lowDataModeSource(.network(lowResolutionURL))
+  .onProgress { receivedSize, totalSize in  }
+  .onSuccess { result in  }
+  .onFailure { error in }
+  .set(to: imageView)
+```
+
+And even better, if later you want to switch to SwiftUI, just make some trivial changes and you've done.
+
+```swift
+struct ContentView: View {
+    var body: some View {
+        KFImage.url(url)
+          .placeholder(placeholderImage)
+          .setProcessor(processor)
+          .loadDiskFileSynchronously()
+          .cacheMemoryOnly()
+          .fade(duration: 0.25)
+          .lowDataModeSource(.network(lowResolutionURL))
+          .onProgress { receivedSize, totalSize in  }
+          .onSuccess { result in  }
+          .onFailure { error in }
+    }
+}
+```
+
+### Learn More
+
+To learn the use of Kingfisher by more examples, take a look at the well-prepared [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet). T
+here we summarized the most common tasks in Kingfisher, you can get a better idea of what this framework can do. 
+There are also some performance tips, remember to check them too.
+
+## Requirements
+
+- iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
+- Swift 4.0+
+
+### Installation
+
+A detailed guide for installation can be found in [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide).
+
+#### Swift Package Manager
+
+- File > Swift Packages > Add Package Dependency
+- Add `https://github.com/onevcat/Kingfisher.git`
+- Select "Up to Next Major" with "6.0.0"
+
+#### CocoaPods
+
+```ruby
+source 'https://github.com/CocoaPods/Specs.git'
+platform :ios, '10.0'
+use_frameworks!
+
+target 'MyApp' do
+  pod 'Kingfisher', '~> 6.0'
+end
+```
+
+#### Carthage
+
+```
+github "onevcat/Kingfisher" ~> 6.0
+```
+
+
+### Migrating
+
+[Kingfisher 6.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-6.0-Migration-Guide) - Kingfisher 6.x is NOT fully compatible with the previous version. However, the migration is not difficult. Depending on your use cases, it may take no effect or several minutes to modify your existing code for the new version. Please follow the [migration guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-6.0-Migration-Guide) when you prepare to upgrade Kingfisher in your project.
+
+If you are using an even earlier version, see the guides below to know the steps for migrating.
+
+> - [Kingfisher 5.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-5.0-Migration-Guide) - If you are upgrading to Kingfisher 5.x from 4.x, please read this for more information.
+> - Kingfisher 4.0 Migration - Kingfisher 3.x should be source compatible to Kingfisher 4. The reason for a major update is that we need to specify the Swift version explicitly for Xcode. All deprecated methods in Kingfisher 3 were removed, so please ensure you have no warning left before you migrate from Kingfisher 3 to Kingfisher 4. If you have any trouble when migrating, please open an issue to discuss.
+> - [Kingfisher 3.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-3.0-Migration-Guide) - If you are upgrading to Kingfisher 3.x from an earlier version, please read this for more information.
+
+## Next Steps
+
+We prepared a [wiki page](https://github.com/onevcat/Kingfisher/wiki). You can find tons of useful things there.
+
+* [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide) - Follow it to integrate Kingfisher into your project.
+* [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet)- Curious about what Kingfisher could do and how would it look like when used in your project? See this page for useful code snippets. If you are already familiar with Kingfisher, you could also learn new tricks to improve the way you use Kingfisher!
+* [API Reference](https://kingfisher.onevcat.com/) - Lastly, please remember to read the full whenever you may need a more detailed reference.
+
+## Other
+
+### Future of Kingfisher
+
+I want to keep Kingfisher lightweight. This framework focuses on providing a simple solution for downloading and caching images. This doesn’t mean the framework can’t be improved. Kingfisher is far from perfect, so necessary and useful updates will be made to make it better.
+
+### Developments and Tests
+
+Any contributing and pull requests are warmly welcome. However, before you plan to implement some features or try to fix an uncertain issue, it is recommended to open a discussion first. It would be appreciated if your pull requests could build and with all tests green. :)
+
+### About the logo
+
+The logo of Kingfisher is inspired by [Tangram (七巧板)](http://en.wikipedia.org/wiki/Tangram), a dissection puzzle consisting of seven flat shapes from China. I believe she's a kingfisher bird instead of a swift, but someone insists that she is a pigeon. I guess I should give her a name. Hi, guys, do you have any suggestions?
+
+### Contact
+
+Follow and contact me on [Twitter](http://twitter.com/onevcat) or [Sina Weibo](http://weibo.com/onevcat). If you find an issue, [open a ticket](https://github.com/onevcat/Kingfisher/issues/new). Pull requests are warmly welcome as well.
+
+## Backers & Sponsors
+
+Open-source projects cannot live long without your help. If you find Kingfisher is useful, please consider supporting this 
+project by becoming a sponsor. Your user icon or company logo shows up [on my blog](https://onevcat.com/tabs/about/) with a link to your home page. 
+
+Become a sponsor through [GitHub Sponsors](https://github.com/sponsors/onevcat) or [Open Collective](https://opencollective.com/kingfisher#sponsor). :heart:
+
+### License
+
+Kingfisher is released under the MIT license. See LICENSE for details.

+ 117 - 0
Example/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift

@@ -0,0 +1,117 @@
+//
+//  CacheSerializer.swift
+//  Kingfisher
+//
+//  Created by Wei Wang on 2016/09/02.
+//
+//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+import Foundation
+import CoreGraphics
+
+/// An `CacheSerializer` is used to convert some data to an image object after
+/// retrieving it from disk storage, and vice versa, to convert an image to data object
+/// for storing to the disk storage.
+public protocol CacheSerializer {
+    
+    /// Gets the serialized data from a provided image
+    /// and optional original data for caching to disk.
+    ///
+    /// - Parameters:
+    ///   - image: The image needed to be serialized.
+    ///   - original: The original data which is just downloaded.
+    ///               If the image is retrieved from cache instead of
+    ///               downloaded, it will be `nil`.
+    /// - Returns: The data object for storing to disk, or `nil` when no valid
+    ///            data could be serialized.
+    func data(with image: KFCrossPlatformImage, original: Data?) -> Data?
+
+    /// Gets an image from provided serialized data.
+    ///
+    /// - Parameters:
+    ///   - data: The data from which an image should be deserialized.
+    ///   - options: The parsed options for deserialization.
+    /// - Returns: An image deserialized or `nil` when no valid image
+    ///            could be deserialized.
+    func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
+}
+
+/// Represents a basic and default `CacheSerializer` used in Kingfisher disk cache system.
+/// It could serialize and deserialize images in PNG, JPEG and GIF format. For
+/// image other than these formats, a normalized `pngRepresentation` will be used.
+public struct DefaultCacheSerializer: CacheSerializer {
+    
+    /// The default general cache serializer used across Kingfisher's cache.
+    public static let `default` = DefaultCacheSerializer()
+
+    /// The compression quality when converting image to a lossy format data. Default is 1.0.
+    public var compressionQuality: CGFloat = 1.0
+
+    /// Whether the original data should be preferred when serializing the image.
+    /// If `true`, the input original data will be checked first and used unless the data is `nil`.
+    /// In that case, the serialization will fall back to creating data from image.
+    public var preferCacheOriginalData: Bool = false
+
+    /// Creates a cache serializer that serialize and deserialize images in PNG, JPEG and GIF format.
+    ///
+    /// - Note:
+    /// Use `DefaultCacheSerializer.default` unless you need to specify your own properties.
+    ///
+    public init() { }
+
+    /// - Parameters:
+    ///   - image: The image needed to be serialized.
+    ///   - original: The original data which is just downloaded.
+    ///               If the image is retrieved from cache instead of
+    ///               downloaded, it will be `nil`.
+    /// - Returns: The data object for storing to disk, or `nil` when no valid
+    ///            data could be serialized.
+    ///
+    /// - Note:
+    /// Only when `original` contains valid PNG, JPEG and GIF format data, the `image` will be
+    /// converted to the corresponding data type. Otherwise, if the `original` is provided but it is not
+    /// If `original` is `nil`, the input `image` will be encoded as PNG data.
+    public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {
+        if preferCacheOriginalData {
+            return original ??
+                image.kf.data(
+                    format: original?.kf.imageFormat ?? .unknown,
+                    compressionQuality: compressionQuality
+                )
+        } else {
+            return image.kf.data(
+                format: original?.kf.imageFormat ?? .unknown,
+                compressionQuality: compressionQuality
+            )
+        }
+    }
+    
+    /// Gets an image deserialized from provided data.
+    ///
+    /// - Parameters:
+    ///   - data: The data from which an image should be deserialized.
+    ///   - options: Options for deserialization.
+    /// - Returns: An image deserialized or `nil` when no valid image
+    ///            could be deserialized.
+    public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
+        return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)
+    }
+}

+ 584 - 0
Example/Pods/Kingfisher/Sources/Cache/DiskStorage.swift

@@ -0,0 +1,584 @@
+//
+//  DiskStorage.swift
+//  Kingfisher
+//
+//  Created by Wei Wang on 2018/10/15.
+//
+//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+import Foundation
+
+
+/// Represents a set of conception related to storage which stores a certain type of value in disk.
+/// This is a namespace for the disk storage types. A `Backend` with a certain `Config` will be used to describe the
+/// storage. See these composed types for more information.
+public enum DiskStorage {
+
+    /// Represents a storage back-end for the `DiskStorage`. The value is serialized to data
+    /// and stored as file in the file system under a specified location.
+    ///
+    /// You can config a `DiskStorage.Backend` in its initializer by passing a `DiskStorage.Config` value.
+    /// or modifying the `config` property after it being created. `DiskStorage` will use file's attributes to keep
+    /// track of a file for its expiration or size limitation.
+    public class Backend<T: DataTransformable> {
+        /// The config used for this disk storage.
+        public var config: Config
+
+        // The final storage URL on disk, with `name` and `cachePathBlock` considered.
+        public let directoryURL: URL
+
+        let metaChangingQueue: DispatchQueue
+
+        var maybeCached : Set<String>?
+        let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue")
+
+        // `false` if the storage initialized with an error. This prevents unexpected forcibly crash when creating
+        // storage in the default cache.
+        private var storageReady: Bool = true
+
+        /// Creates a disk storage with the given `DiskStorage.Config`.
+        ///
+        /// - Parameter config: The config used for this disk storage.
+        /// - Throws: An error if the folder for storage cannot be got or created.
+        public convenience init(config: Config) throws {
+            self.init(noThrowConfig: config, creatingDirectory: false)
+            try prepareDirectory()
+        }
+
+        // If `creatingDirectory` is `false`, the directory preparation will be skipped.
+        // We need to call `prepareDirectory` manually after this returns.
+        init(noThrowConfig config: Config, creatingDirectory: Bool) {
+            var config = config
+
+            let creation = Creation(config)
+            self.directoryURL = creation.directoryURL
+
+            // Break any possible retain cycle set by outside.
+            config.cachePathBlock = nil
+            self.config = config
+
+            metaChangingQueue = DispatchQueue(label: creation.cacheName)
+            setupCacheChecking()
+
+            if creatingDirectory {
+                try? prepareDirectory()
+            }
+        }
+
+        private func setupCacheChecking() {
+            maybeCachedCheckingQueue.async {
+                do {
+                    self.maybeCached = Set()
+                    try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path).forEach { fileName in
+                        self.maybeCached?.insert(fileName)
+                    }
+                } catch {
+                    // Just disable the functionality if we fail to initialize it properly. This will just revert to
+                    // the behavior which is to check file existence on disk directly.
+                    self.maybeCached = nil
+                }
+            }
+        }
+
+        // Creates the storage folder.
+        private func prepareDirectory() throws {
+            let fileManager = config.fileManager
+            let path = directoryURL.path
+
+            guard !fileManager.fileExists(atPath: path) else { return }
+
+            do {
+                try fileManager.createDirectory(
+                    atPath: path,
+                    withIntermediateDirectories: true,
+                    attributes: nil)
+            } catch {
+                self.storageReady = false
+                throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error))
+            }
+        }
+
+        /// Stores a value to the storage under the specified key and expiration policy.
+        /// - Parameters:
+        ///   - value: The value to be stored.
+        ///   - key: The key to which the `value` will be stored. If there is already a value under the key,
+        ///          the old value will be overwritten by `value`.
+        ///   - expiration: The expiration policy used by this store action.
+        /// - Throws: An error during converting the value to a data format or during writing it to disk.
+        public func store(
+            value: T,
+            forKey key: String,
+            expiration: StorageExpiration? = nil) throws
+        {
+            guard storageReady else {
+                throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))
+            }
+
+            let expiration = expiration ?? config.expiration
+            // The expiration indicates that already expired, no need to store.
+            guard !expiration.isExpired else { return }
+            
+            let data: Data
+            do {
+                data = try value.toData()
+            } catch {
+                throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error))
+            }
+
+            let fileURL = cacheFileURL(forKey: key)
+            do {
+                try data.write(to: fileURL)
+            } catch {
+                throw KingfisherError.cacheError(
+                    reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error)
+                )
+            }
+
+            let now = Date()
+            let attributes: [FileAttributeKey : Any] = [
+                // The last access date.
+                .creationDate: now.fileAttributeDate,
+                // The estimated expiration date.
+                .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
+            ]
+            do {
+                try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path)
+            } catch {
+                try? config.fileManager.removeItem(at: fileURL)
+                throw KingfisherError.cacheError(
+                    reason: .cannotSetCacheFileAttribute(
+                        filePath: fileURL.path,
+                        attributes: attributes,
+                        error: error
+                    )
+                )
+            }
+
+            maybeCachedCheckingQueue.async {
+                self.maybeCached?.insert(fileURL.lastPathComponent)
+            }
+        }
+
+        /// Gets a value from the storage.
+        /// - Parameters:
+        ///   - key: The cache key of value.
+        ///   - extendingExpiration: The expiration policy used by this getting action.
+        /// - Throws: An error during converting the data to a value or during operation of disk files.
+        /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`.
+        public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) throws -> T? {
+            return try value(forKey: key, referenceDate: Date(), actuallyLoad: true, extendingExpiration: extendingExpiration)
+        }
+
+        func value(
+            forKey key: String,
+            referenceDate: Date,
+            actuallyLoad: Bool,
+            extendingExpiration: ExpirationExtending) throws -> T?
+        {
+            guard storageReady else {
+                throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))
+            }
+
+            let fileManager = config.fileManager
+            let fileURL = cacheFileURL(forKey: key)
+            let filePath = fileURL.path
+
+            let fileMaybeCached = maybeCachedCheckingQueue.sync {
+                return maybeCached?.contains(fileURL.lastPathComponent) ?? true
+            }
+            guard fileMaybeCached else {
+                return nil
+            }
+            guard fileManager.fileExists(atPath: filePath) else {
+                return nil
+            }
+
+            let meta: FileMeta
+            do {
+                let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey]
+                meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys)
+            } catch {
+                throw KingfisherError.cacheError(
+                    reason: .invalidURLResource(error: error, key: key, url: fileURL))
+            }
+
+            if meta.expired(referenceDate: referenceDate) {
+                return nil
+            }
+            if !actuallyLoad { return T.empty }
+
+            do {
+                let data = try Data(contentsOf: fileURL)
+                let obj = try T.fromData(data)
+                metaChangingQueue.async {
+                    meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration)
+                }
+                return obj
+            } catch {
+                throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error))
+            }
+        }
+
+        /// Whether there is valid cached data under a given key.
+        /// - Parameter key: The cache key of value.
+        /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`.
+        ///
+        /// - Note:
+        /// This method does not actually load the data from disk, so it is faster than directly loading the cached value
+        /// by checking the nullability of `value(forKey:extendingExpiration:)` method.
+        ///
+        public func isCached(forKey key: String) -> Bool {
+            return isCached(forKey: key, referenceDate: Date())
+        }
+
+        /// Whether there is valid cached data under a given key and a reference date.
+        /// - Parameters:
+        ///   - key: The cache key of value.
+        ///   - referenceDate: A reference date to check whether the cache is still valid.
+        /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`.
+        ///
+        /// - Note:
+        /// If you pass `Date()` to `referenceDate`, this method is identical to `isCached(forKey:)`. Use the
+        /// `referenceDate` to determine whether the cache is still valid for a future date.
+        public func isCached(forKey key: String, referenceDate: Date) -> Bool {
+            do {
+                let result = try value(
+                    forKey: key,
+                    referenceDate: referenceDate,
+                    actuallyLoad: false,
+                    extendingExpiration: .none
+                )
+                return result != nil
+            } catch {
+                return false
+            }
+        }
+
+        /// Removes a value from a specified key.
+        /// - Parameter key: The cache key of value.
+        /// - Throws: An error during removing the value.
+        public func remove(forKey key: String) throws {
+            let fileURL = cacheFileURL(forKey: key)
+            try removeFile(at: fileURL)
+        }
+
+        func removeFile(at url: URL) throws {
+            try config.fileManager.removeItem(at: url)
+        }
+
+        /// Removes all values in this storage.
+        /// - Throws: An error during removing the values.
+        public func removeAll() throws {
+            try removeAll(skipCreatingDirectory: false)
+        }
+
+        func removeAll(skipCreatingDirectory: Bool) throws {
+            try config.fileManager.removeItem(at: directoryURL)
+            if !skipCreatingDirectory {
+                try prepareDirectory()
+            }
+        }
+
+        /// The URL of the cached file with a given computed `key`.
+        ///
+        /// - Parameter key: The final computed key used when caching the image. Please note that usually this is not
+        /// the `cacheKey` of an image `Source`. It is the computed key with processor identifier considered.
+        ///
+        /// - Note:
+        /// This method does not guarantee there is an image already cached in the returned URL. It just gives your
+        /// the URL that the image should be if it exists in disk storage, with the give key.
+        ///
+        public func cacheFileURL(forKey key: String) -> URL {
+            let fileName = cacheFileName(forKey: key)
+            return directoryURL.appendingPathComponent(fileName, isDirectory: false)
+        }
+
+        func cacheFileName(forKey key: String) -> String {
+            if config.usesHashedFileName {
+                let hashedKey = key.kf.md5
+                if let ext = config.pathExtension {
+                    return "\(hashedKey).\(ext)"
+                } else if config.autoExtAfterHashedFileName,
+                          let ext = key.kf.ext {
+                    return "\(hashedKey).\(ext)"
+                }
+                return hashedKey
+            } else {
+                if let ext = config.pathExtension {
+                    return "\(key).\(ext)"
+                }
+                return key
+            }
+        }
+
+        func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] {
+            let fileManager = config.fileManager
+
+            guard let directoryEnumerator = fileManager.enumerator(
+                at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else
+            {
+                throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL))
+            }
+
+            guard let urls = directoryEnumerator.allObjects as? [URL] else {
+                throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL))
+            }
+            return urls
+        }
+
+        /// Removes all expired values from this storage.
+        /// - Throws: A file manager error during removing the file.
+        /// - Returns: The URLs for removed files.
+        public func removeExpiredValues() throws -> [URL] {
+            return try removeExpiredValues(referenceDate: Date())
+        }
+
+        func removeExpiredValues(referenceDate: Date) throws -> [URL] {
+            let propertyKeys: [URLResourceKey] = [
+                .isDirectoryKey,
+                .contentModificationDateKey
+            ]
+
+            let urls = try allFileURLs(for: propertyKeys)
+            let keys = Set(propertyKeys)
+            let expiredFiles = urls.filter { fileURL in
+                do {
+                    let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys)
+                    if meta.isDirectory {
+                        return false
+                    }
+                    return meta.expired(referenceDate: referenceDate)
+                } catch {
+                    return true
+                }
+            }
+            try expiredFiles.forEach { url in
+                try removeFile(at: url)
+            }
+            return expiredFiles
+        }
+
+        /// Removes all size exceeded values from this storage.
+        /// - Throws: A file manager error during removing the file.
+        /// - Returns: The URLs for removed files.
+        ///
+        /// - Note: This method checks `config.sizeLimit` and remove cached files in an LRU (Least Recently Used) way.
+        func removeSizeExceededValues() throws -> [URL] {
+
+            if config.sizeLimit == 0 { return [] } // Back compatible. 0 means no limit.
+
+            var size = try totalSize()
+            if size < config.sizeLimit { return [] }
+
+            let propertyKeys: [URLResourceKey] = [
+                .isDirectoryKey,
+                .creationDateKey,
+                .fileSizeKey
+            ]
+            let keys = Set(propertyKeys)
+
+            let urls = try allFileURLs(for: propertyKeys)
+            var pendings: [FileMeta] = urls.compactMap { fileURL in
+                guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else {
+                    return nil
+                }
+                return meta
+            }
+            // Sort by last access date. Most recent file first.
+            pendings.sort(by: FileMeta.lastAccessDate)
+
+            var removed: [URL] = []
+            let target = config.sizeLimit / 2
+            while size > target, let meta = pendings.popLast() {
+                size -= UInt(meta.fileSize)
+                try removeFile(at: meta.url)
+                removed.append(meta.url)
+            }
+            return removed
+        }
+
+        /// Gets the total file size of the folder in bytes.
+        public func totalSize() throws -> UInt {
+            let propertyKeys: [URLResourceKey] = [.fileSizeKey]
+            let urls = try allFileURLs(for: propertyKeys)
+            let keys = Set(propertyKeys)
+            let totalSize: UInt = urls.reduce(0) { size, fileURL in
+                do {
+                    let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys)
+                    return size + UInt(meta.fileSize)
+                } catch {
+                    return size
+                }
+            }
+            return totalSize
+        }
+    }
+}
+
+extension DiskStorage {
+    /// Represents the config used in a `DiskStorage`.
+    public struct Config {
+
+        /// The file size limit on disk of the storage in bytes. 0 means no limit.
+        public var sizeLimit: UInt
+
+        /// The `StorageExpiration` used in this disk storage. Default is `.days(7)`,
+        /// means that the disk cache would expire in one week.
+        public var expiration: StorageExpiration = .days(7)
+
+        /// The preferred extension of cache item. It will be appended to the file name as its extension.
+        /// Default is `nil`, means that the cache file does not contain a file extension.
+        public var pathExtension: String? = nil
+
+        /// Default is `true`, means that the cache file name will be hashed before storing.
+        public var usesHashedFileName = true
+
+        /// Default is `false`
+        /// If set to `true`, image extension will be extracted from original file name and append to
+        /// the hased file name and used as the cache key on disk.
+        public var autoExtAfterHashedFileName = false
+
+        let name: String
+        let fileManager: FileManager
+        let directory: URL?
+
+        var cachePathBlock: ((_ directory: URL, _ cacheName: String) -> URL)! = {
+            (directory, cacheName) in
+            return directory.appendingPathComponent(cacheName, isDirectory: true)
+        }
+
+        /// Creates a config value based on given parameters.
+        ///
+        /// - Parameters:
+        ///   - name: The name of cache. It is used as a part of storage folder. It is used to identify the disk
+        ///           storage. Two storages with the same `name` would share the same folder in disk, and it should
+        ///           be prevented.
+        ///   - sizeLimit: The size limit in bytes for all existing files in the disk storage.
+        ///   - fileManager: The `FileManager` used to manipulate files on disk. Default is `FileManager.default`.
+        ///   - directory: The URL where the disk storage should live. The storage will use this as the root folder,
+        ///                and append a path which is constructed by input `name`. Default is `nil`, indicates that
+        ///                the cache directory under user domain mask will be used.
+        public init(
+            name: String,
+            sizeLimit: UInt,
+            fileManager: FileManager = .default,
+            directory: URL? = nil)
+        {
+            self.name = name
+            self.fileManager = fileManager
+            self.directory = directory
+            self.sizeLimit = sizeLimit
+        }
+    }
+}
+
+extension DiskStorage {
+    struct FileMeta {
+    
+        let url: URL
+        
+        let lastAccessDate: Date?
+        let estimatedExpirationDate: Date?
+        let isDirectory: Bool
+        let fileSize: Int
+        
+        static func lastAccessDate(lhs: FileMeta, rhs: FileMeta) -> Bool {
+            return lhs.lastAccessDate ?? .distantPast > rhs.lastAccessDate ?? .distantPast
+        }
+        
+        init(fileURL: URL, resourceKeys: Set<URLResourceKey>) throws {
+            let meta = try fileURL.resourceValues(forKeys: resourceKeys)
+            self.init(
+                fileURL: fileURL,
+                lastAccessDate: meta.creationDate,
+                estimatedExpirationDate: meta.contentModificationDate,
+                isDirectory: meta.isDirectory ?? false,
+                fileSize: meta.fileSize ?? 0)
+        }
+        
+        init(
+            fileURL: URL,
+            lastAccessDate: Date?,
+            estimatedExpirationDate: Date?,
+            isDirectory: Bool,
+            fileSize: Int)
+        {
+            self.url = fileURL
+            self.lastAccessDate = lastAccessDate
+            self.estimatedExpirationDate = estimatedExpirationDate
+            self.isDirectory = isDirectory
+            self.fileSize = fileSize
+        }
+
+        func expired(referenceDate: Date) -> Bool {
+            return estimatedExpirationDate?.isPast(referenceDate: referenceDate) ?? true
+        }
+        
+        func extendExpiration(with fileManager: FileManager, extendingExpiration: ExpirationExtending) {
+            guard let lastAccessDate = lastAccessDate,
+                  let lastEstimatedExpiration = estimatedExpirationDate else
+            {
+                return
+            }
+
+            let attributes: [FileAttributeKey : Any]
+
+            switch extendingExpiration {
+            case .none:
+                // not extending expiration time here
+                return
+            case .cacheTime:
+                let originalExpiration: StorageExpiration =
+                    .seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate))
+                attributes = [
+                    .creationDate: Date().fileAttributeDate,
+                    .modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate
+                ]
+            case .expirationTime(let expirationTime):
+                attributes = [
+                    .creationDate: Date().fileAttributeDate,
+                    .modificationDate: expirationTime.estimatedExpirationSinceNow.fileAttributeDate
+                ]
+            }
+
+            try? fileManager.setAttributes(attributes, ofItemAtPath: url.path)
+        }
+    }
+}
+
+extension DiskStorage {
+    struct Creation {
+        let directoryURL: URL
+        let cacheName: String
+
+        init(_ config: Config) {
+            let url: URL
+            if let directory = config.directory {
+                url = directory
+            } else {
+                url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
+            }
+
+            cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)"
+            directoryURL = config.cachePathBlock(url, cacheName)
+        }
+    }
+}

+ 118 - 0
Example/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift

@@ -0,0 +1,118 @@
+//
+//  RequestModifier.swift
+//  Kingfisher
+//
+//  Created by Junyu Kuang on 5/28/17.
+//
+//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+import Foundation
+import CoreGraphics
+
+/// `FormatIndicatedCacheSerializer` lets you indicate an image format for serialized caches.
+///
+/// It could serialize and deserialize PNG, JPEG and GIF images. For
+/// image other than these formats, a normalized `pngRepresentation` will be used.
+///
+/// Example:
+/// ````
+/// let profileImageSize = CGSize(width: 44, height: 44)
+///
+/// // A round corner image.
+/// let imageProcessor = RoundCornerImageProcessor(
+///     cornerRadius: profileImageSize.width / 2, targetSize: profileImageSize)
+///
+/// let optionsInfo: KingfisherOptionsInfo = [
+///     .cacheSerializer(FormatIndicatedCacheSerializer.png), 
+///     .processor(imageProcessor)]
+///
+/// A URL pointing to a JPEG image.
+/// let url = URL(string: "https://example.com/image.jpg")!
+///
+/// // Image will be always cached as PNG format to preserve alpha channel for round rectangle.
+/// // So when you load it from cache again later, it will be still round cornered.
+/// // Otherwise, the corner part would be filled by white color (since JPEG does not contain an alpha channel).
+/// imageView.kf.setImage(with: url, options: optionsInfo)
+/// ````
+public struct FormatIndicatedCacheSerializer: CacheSerializer {
+    
+    /// A `FormatIndicatedCacheSerializer` which converts image from and to PNG format. If the image cannot be
+    /// represented by PNG format, it will fallback to its real format which is determined by `original` data.
+    public static let png = FormatIndicatedCacheSerializer(imageFormat: .PNG, jpegCompressionQuality: nil)
+    
+    /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format. If the image cannot be
+    /// represented by JPEG format, it will fallback to its real format which is determined by `original` data.
+    /// The compression quality is 1.0 when using this serializer. If you need to set a customized compression quality,
+    /// use `jpeg(compressionQuality:)`.
+    public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: 1.0)
+
+    /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format with a settable compression
+    /// quality. If the image cannot be represented by JPEG format, it will fallback to its real format which is
+    /// determined by `original` data.
+    /// - Parameter compressionQuality: The compression quality when converting image to JPEG data.
+    public static func jpeg(compressionQuality: CGFloat) -> FormatIndicatedCacheSerializer {
+        return FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: compressionQuality)
+    }
+    
+    /// A `FormatIndicatedCacheSerializer` which converts image from and to GIF format. If the image cannot be
+    /// represented by GIF format, it will fallback to its real format which is determined by `original` data.
+    public static let gif = FormatIndicatedCacheSerializer(imageFormat: .GIF, jpegCompressionQuality: nil)
+    
+    /// The indicated image format.
+    private let imageFormat: ImageFormat
+
+    /// The compression quality used for loss image format (like JPEG).
+    private let jpegCompressionQuality: CGFloat?
+    
+    /// Creates data which represents the given `image` under a format.
+    public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {
+        
+        func imageData(withFormat imageFormat: ImageFormat) -> Data? {
+            return autoreleasepool { () -> Data? in
+                switch imageFormat {
+                case .PNG: return image.kf.pngRepresentation()
+                case .JPEG: return image.kf.jpegRepresentation(compressionQuality: jpegCompressionQuality ?? 1.0)
+                case .GIF: return image.kf.gifRepresentation()
+                case .unknown: return nil
+                }
+            }
+        }
+        
+        // generate data with indicated image format
+        if let data = imageData(withFormat: imageFormat) {
+            return data
+        }
+        
+        let originalFormat = original?.kf.imageFormat ?? .unknown
+        
+        // generate data with original image's format
+        if originalFormat != imageFormat, let data = imageData(withFormat: originalFormat) {
+            return data
+        }
+        
+        return original ?? image.kf.normalized.kf.pngRepresentation()
+    }
+    
+    /// Same implementation as `DefaultCacheSerializer`.
+    public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
+        return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)
+    }
+}

+ 849 - 0
Example/Pods/Kingfisher/Sources/Cache/ImageCache.swift

@@ -0,0 +1,849 @@
+//
+//  ImageCache.swift
+//  Kingfisher
+//
+//  Created by Wei Wang on 15/4/6.
+//
+//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+#if os(macOS)
+import AppKit
+#else
+import UIKit
+#endif
+
+extension Notification.Name {
+    /// This notification will be sent when the disk cache got cleaned either there are cached files expired or the
+    /// total size exceeding the max allowed size. The manually invoking of `clearDiskCache` method will not trigger
+    /// this notification.
+    ///
+    /// The `object` of this notification is the `ImageCache` object which sends the notification.
+    /// A list of removed hashes (files) could be retrieved by accessing the array under
+    /// `KingfisherDiskCacheCleanedHashKey` key in `userInfo` of the notification object you received.
+    /// By checking the array, you could know the hash codes of files are removed.
+    public static let KingfisherDidCleanDiskCache =
+        Notification.Name("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache")
+}
+
+/// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`.
+public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash"
+
+/// Cache type of a cached image.
+/// - none: The image is not cached yet when retrieving it.
+/// - memory: The image is cached in memory.
+/// - disk: The image is cached in disk.
+public enum CacheType {
+    /// The image is not cached yet when retrieving it.
+    case none
+    /// The image is cached in memory.
+    case memory
+    /// The image is cached in disk.
+    case disk
+    
+    /// Whether the cache type represents the image is already cached or not.
+    public var cached: Bool {
+        switch self {
+        case .memory, .disk: return true
+        case .none: return false
+        }
+    }
+}
+
+/// Represents the caching operation result.
+public struct CacheStoreResult {
+    
+    /// The cache result for memory cache. Caching an image to memory will never fail.
+    public let memoryCacheResult: Result<(), Never>
+    
+    /// The cache result for disk cache. If an error happens during caching operation,
+    /// you can get it from `.failure` case of this `diskCacheResult`.
+    public let diskCacheResult: Result<(), KingfisherError>
+}
+
+extension KFCrossPlatformImage: CacheCostCalculable {
+    /// Cost of an image
+    public var cacheCost: Int { return kf.cost }
+}
+
+extension Data: DataTransformable {
+    public func toData() throws -> Data {
+        return self
+    }
+
+    public static func fromData(_ data: Data) throws -> Data {
+        return data
+    }
+
+    public static let empty = Data()
+}
+
+
+/// Represents the getting image operation from the cache.
+///
+/// - disk: The image can be retrieved from disk cache.
+/// - memory: The image can be retrieved memory cache.
+/// - none: The image does not exist in the cache.
+public enum ImageCacheResult {
+    
+    /// The image can be retrieved from disk cache.
+    case disk(KFCrossPlatformImage)
+    
+    /// The image can be retrieved memory cache.
+    case memory(KFCrossPlatformImage)
+    
+    /// The image does not exist in the cache.
+    case none
+    
+    /// Extracts the image from cache result. It returns the associated `Image` value for
+    /// `.disk` and `.memory` case. For `.none` case, `nil` is returned.
+    public var image: KFCrossPlatformImage? {
+        switch self {
+        case .disk(let image): return image
+        case .memory(let image): return image
+        case .none: return nil
+        }
+    }
+    
+    /// Returns the corresponding `CacheType` value based on the result type of `self`.
+    public var cacheType: CacheType {
+        switch self {
+        case .disk: return .disk
+        case .memory: return .memory
+        case .none: return .none
+        }
+    }
+}
+
+/// Represents a hybrid caching system which is composed by a `MemoryStorage.Backend` and a `DiskStorage.Backend`.
+/// `ImageCache` is a high level abstract for storing an image as well as its data to disk memory and disk, and
+/// retrieving them back.
+///
+/// While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create
+/// your own cache object and configure its storages as your need. This class also provide an interface for you to set
+/// the memory and disk storage config.
+open class ImageCache {
+
+    // MARK: Singleton
+    /// The default `ImageCache` object. Kingfisher will use this cache for its related methods if there is no
+    /// other cache specified. The `name` of this default cache is "default", and you should not use this name
+    /// for any of your customize cache.
+    public static let `default` = ImageCache(name: "default")
+
+
+    // MARK: Public Properties
+    /// The `MemoryStorage.Backend` object used in this cache. This storage holds loaded images in memory with a
+    /// reasonable expire duration and a maximum memory usage. To modify the configuration of a storage, just set
+    /// the storage `config` and its properties.
+    public let memoryStorage: MemoryStorage.Backend<KFCrossPlatformImage>
+    
+    /// The `DiskStorage.Backend` object used in this cache. This storage stores loaded images in disk with a
+    /// reasonable expire duration and a maximum disk usage. To modify the configuration of a storage, just set
+    /// the storage `config` and its properties.
+    public let diskStorage: DiskStorage.Backend<Data>
+    
+    private let ioQueue: DispatchQueue
+    
+    /// Closure that defines the disk cache path from a given path and cacheName.
+    public typealias DiskCachePathClosure = (URL, String) -> URL
+
+    // MARK: Initializers
+
+    /// Creates an `ImageCache` from a customized `MemoryStorage` and `DiskStorage`.
+    ///
+    /// - Parameters:
+    ///   - memoryStorage: The `MemoryStorage.Backend` object to use in the image cache.
+    ///   - diskStorage: The `DiskStorage.Backend` object to use in the image cache.
+    public init(
+        memoryStorage: MemoryStorage.Backend<KFCrossPlatformImage>,
+        diskStorage: DiskStorage.Backend<Data>)
+    {
+        self.memoryStorage = memoryStorage
+        self.diskStorage = diskStorage
+        let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(UUID().uuidString)"
+        ioQueue = DispatchQueue(label: ioQueueName)
+
+        let notifications: [(Notification.Name, Selector)]
+        #if !os(macOS) && !os(watchOS)
+        notifications = [
+            (UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)),
+            (UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)),
+            (UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache))
+        ]
+        #elseif os(macOS)
+        notifications = [
+            (NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)),
+        ]
+        #else
+        notifications = []
+        #endif
+        notifications.forEach {
+            NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil)
+        }
+    }
+    
+    /// Creates an `ImageCache` with a given `name`. Both `MemoryStorage` and `DiskStorage` will be created
+    /// with a default config based on the `name`.
+    ///
+    /// - Parameter name: The name of cache object. It is used to setup disk cache directories and IO queue.
+    ///                   You should not use the same `name` for different caches, otherwise, the disk storage would
+    ///                   be conflicting to each other. The `name` should not be an empty string.
+    public convenience init(name: String) {
+        self.init(noThrowName: name, cacheDirectoryURL: nil, diskCachePathClosure: nil)
+    }
+
+    /// Creates an `ImageCache` with a given `name`, cache directory `path`
+    /// and a closure to modify the cache directory.
+    ///
+    /// - Parameters:
+    ///   - name: The name of cache object. It is used to setup disk cache directories and IO queue.
+    ///           You should not use the same `name` for different caches, otherwise, the disk storage would
+    ///           be conflicting to each other.
+    ///   - cacheDirectoryURL: Location of cache directory URL on disk. It will be internally pass to the
+    ///                        initializer of `DiskStorage` as the disk cache directory. If `nil`, the cache
+    ///                        directory under user domain mask will be used.
+    ///   - diskCachePathClosure: Closure that takes in an optional initial path string and generates
+    ///                           the final disk cache path. You could use it to fully customize your cache path.
+    /// - Throws: An error that happens during image cache creating, such as unable to create a directory at the given
+    ///           path.
+    public convenience init(
+        name: String,
+        cacheDirectoryURL: URL?,
+        diskCachePathClosure: DiskCachePathClosure? = nil
+    ) throws
+    {
+        if name.isEmpty {
+            fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
+        }
+
+        let memoryStorage = ImageCache.createMemoryStorage()
+
+        let config = ImageCache.createConfig(
+            name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure
+        )
+        let diskStorage = try DiskStorage.Backend<Data>(config: config)
+        self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)
+    }
+
+    convenience init(
+        noThrowName name: String,
+        cacheDirectoryURL: URL?,
+        diskCachePathClosure: DiskCachePathClosure?
+    )
+    {
+        if name.isEmpty {
+            fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
+        }
+
+        let memoryStorage = ImageCache.createMemoryStorage()
+
+        let config = ImageCache.createConfig(
+            name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure
+        )
+        let diskStorage = DiskStorage.Backend<Data>(noThrowConfig: config, creatingDirectory: true)
+        self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)
+    }
+
+    private static func createMemoryStorage() -> MemoryStorage.Backend<KFCrossPlatformImage> {
+        let totalMemory = ProcessInfo.processInfo.physicalMemory
+        let costLimit = totalMemory / 4
+        let memoryStorage = MemoryStorage.Backend<KFCrossPlatformImage>(config:
+            .init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit)))
+        return memoryStorage
+    }
+
+    private static func createConfig(
+        name: String,
+        cacheDirectoryURL: URL?,
+        diskCachePathClosure: DiskCachePathClosure? = nil
+    ) -> DiskStorage.Config
+    {
+        var diskConfig = DiskStorage.Config(
+            name: name,
+            sizeLimit: 0,
+            directory: cacheDirectoryURL
+        )
+        if let closure = diskCachePathClosure {
+            diskConfig.cachePathBlock = closure
+        }
+        return diskConfig
+    }
+    
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+    }
+
+    // MARK: Storing Images
+
+    open func store(_ image: KFCrossPlatformImage,
+                    original: Data? = nil,
+                    forKey key: String,
+                    options: KingfisherParsedOptionsInfo,
+                    toDisk: Bool = true,
+                    completionHandler: ((CacheStoreResult) -> Void)? = nil)
+    {
+        let identifier = options.processor.identifier
+        let callbackQueue = options.callbackQueue
+        
+        let computedKey = key.computedKey(with: identifier)
+        // Memory storage should not throw.
+        memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)
+        
+        guard toDisk else {
+            if let completionHandler = completionHandler {
+                let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
+                callbackQueue.execute { completionHandler(result) }
+            }
+            return
+        }
+        
+        ioQueue.async {
+            let serializer = options.cacheSerializer
+            if let data = serializer.data(with: image, original: original) {
+                self.syncStoreToDisk(
+                    data,
+                    forKey: key,
+                    processorIdentifier: identifier,
+                    callbackQueue: callbackQueue,
+                    expiration: options.diskCacheExpiration,
+                    completionHandler: completionHandler)
+            } else {
+                guard let completionHandler = completionHandler else { return }
+                
+                let diskError = KingfisherError.cacheError(
+                    reason: .cannotSerializeImage(image: image, original: original, serializer: serializer))
+                let result = CacheStoreResult(
+                    memoryCacheResult: .success(()),
+                    diskCacheResult: .failure(diskError))
+                callbackQueue.execute { completionHandler(result) }
+            }
+        }
+    }
+
+    /// Stores an image to the cache.
+    ///
+    /// - Parameters:
+    ///   - image: The image to be stored.
+    ///   - original: The original data of the image. This value will be forwarded to the provided `serializer` for
+    ///               further use. By default, Kingfisher uses a `DefaultCacheSerializer` to serialize the image to
+    ///               data for caching in disk, it checks the image format based on `original` data to determine in
+    ///               which image format should be used. For other types of `serializer`, it depends on their
+    ///               implementation detail on how to use this original data.
+    ///   - key: The key used for caching the image.
+    ///   - identifier: The identifier of processor being used for caching. If you are using a processor for the
+    ///                 image, pass the identifier of processor to this parameter.
+    ///   - serializer: The `CacheSerializer`
+    ///   - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory.
+    ///             Otherwise, it is cached in both memory storage and disk storage. Default is `true`.
+    ///   - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. For case
+    ///                    that `toDisk` is `false`, a `.untouch` queue means `callbackQueue` will be invoked from the
+    ///                    caller queue of this method. If `toDisk` is `true`, the `completionHandler` will be called
+    ///                    from an internal file IO queue. To change this behavior, specify another `CallbackQueue`
+    ///                    value.
+    ///   - completionHandler: A closure which is invoked when the cache operation finishes.
+    open func store(_ image: KFCrossPlatformImage,
+                      original: Data? = nil,
+                      forKey key: String,
+                      processorIdentifier identifier: String = "",
+                      cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
+                      toDisk: Bool = true,
+                      callbackQueue: CallbackQueue = .untouch,
+                      completionHandler: ((CacheStoreResult) -> Void)? = nil)
+    {
+        struct TempProcessor: ImageProcessor {
+            let identifier: String
+            func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
+                return nil
+            }
+        }
+        
+        let options = KingfisherParsedOptionsInfo([
+            .processor(TempProcessor(identifier: identifier)),
+            .cacheSerializer(serializer),
+            .callbackQueue(callbackQueue)
+        ])
+        store(image, original: original, forKey: key, options: options,
+              toDisk: toDisk, completionHandler: completionHandler)
+    }
+    
+    open func storeToDisk(
+        _ data: Data,
+        forKey key: String,
+        processorIdentifier identifier: String = "",
+        expiration: StorageExpiration? = nil,
+        callbackQueue: CallbackQueue = .untouch,
+        completionHandler: ((CacheStoreResult) -> Void)? = nil)
+    {
+        ioQueue.async {
+            self.syncStoreToDisk(
+                data,
+                forKey: key,
+                processorIdentifier: identifier,
+                callbackQueue: callbackQueue,
+                expiration: expiration,
+                completionHandler: completionHandler)
+        }
+    }
+    
+    private func syncStoreToDisk(
+        _ data: Data,
+        forKey key: String,
+        processorIdentifier identifier: String = "",
+        callbackQueue: CallbackQueue = .untouch,
+        expiration: StorageExpiration? = nil,
+        completionHandler: ((CacheStoreResult) -> Void)? = nil)
+    {
+        let computedKey = key.computedKey(with: identifier)
+        let result: CacheStoreResult
+        do {
+            try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration)
+            result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
+        } catch {
+            let diskError: KingfisherError
+            if let error = error as? KingfisherError {
+                diskError = error
+            } else {
+                diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error))
+            }
+            
+            result = CacheStoreResult(
+                memoryCacheResult: .success(()),
+                diskCacheResult: .failure(diskError)
+            )
+        }
+        if let completionHandler = completionHandler {
+            callbackQueue.execute { completionHandler(result) }
+        }
+    }
+
+    // MARK: Removing Images
+
+    /// Removes the image for the given key from the cache.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the image.
+    ///   - identifier: The identifier of processor being used for caching. If you are using a processor for the
+    ///                 image, pass the identifier of processor to this parameter.
+    ///   - fromMemory: Whether this image should be removed from memory storage or not.
+    ///                 If `false`, the image won't be removed from the memory storage. Default is `true`.
+    ///   - fromDisk: Whether this image should be removed from disk storage or not.
+    ///               If `false`, the image won't be removed from the disk storage. Default is `true`.
+    ///   - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`.
+    ///   - completionHandler: A closure which is invoked when the cache removing operation finishes.
+    open func removeImage(forKey key: String,
+                          processorIdentifier identifier: String = "",
+                          fromMemory: Bool = true,
+                          fromDisk: Bool = true,
+                          callbackQueue: CallbackQueue = .untouch,
+                          completionHandler: (() -> Void)? = nil)
+    {
+        let computedKey = key.computedKey(with: identifier)
+
+        if fromMemory {
+            memoryStorage.remove(forKey: computedKey)
+        }
+        
+        if fromDisk {
+            ioQueue.async{
+                try? self.diskStorage.remove(forKey: computedKey)
+                if let completionHandler = completionHandler {
+                    callbackQueue.execute { completionHandler() }
+                }
+            }
+        } else {
+            if let completionHandler = completionHandler {
+                callbackQueue.execute { completionHandler() }
+            }
+        }
+    }
+
+    func retrieveImage(forKey key: String,
+                       options: KingfisherParsedOptionsInfo,
+                       callbackQueue: CallbackQueue = .mainCurrentOrAsync,
+                       completionHandler: ((Result<ImageCacheResult, KingfisherError>) -> Void)?)
+    {
+        // No completion handler. No need to start working and early return.
+        guard let completionHandler = completionHandler else { return }
+
+        // Try to check the image from memory cache first.
+        if let image = retrieveImageInMemoryCache(forKey: key, options: options) {
+            callbackQueue.execute { completionHandler(.success(.memory(image))) }
+        } else if options.fromMemoryCacheOrRefresh {
+            callbackQueue.execute { completionHandler(.success(.none)) }
+        } else {
+
+            // Begin to disk search.
+            self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) {
+                result in
+                switch result {
+                case .success(let image):
+
+                    guard let image = image else {
+                        // No image found in disk storage.
+                        callbackQueue.execute { completionHandler(.success(.none)) }
+                        return
+                    }
+
+                    // Cache the disk image to memory.
+                    // We are passing `false` to `toDisk`, the memory cache does not change
+                    // callback queue, we can call `completionHandler` without another dispatch.
+                    var cacheOptions = options
+                    cacheOptions.callbackQueue = .untouch
+                    self.store(
+                        image,
+                        forKey: key,
+                        options: cacheOptions,
+                        toDisk: false)
+                    {
+                        _ in
+                        callbackQueue.execute { completionHandler(.success(.disk(image))) }
+                    }
+                case .failure(let error):
+                    callbackQueue.execute { completionHandler(.failure(error)) }
+                }
+            }
+        }
+    }
+
+    // MARK: Getting Images
+
+    /// Gets an image for a given key from the cache, either from memory storage or disk storage.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the image.
+    ///   - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
+    ///   - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.mainCurrentOrAsync`.
+    ///   - completionHandler: A closure which is invoked when the image getting operation finishes. If the
+    ///                        image retrieving operation finishes without problem, an `ImageCacheResult` value
+    ///                        will be sent to this closure as result. Otherwise, a `KingfisherError` result
+    ///                        with detail failing reason will be sent.
+    open func retrieveImage(forKey key: String,
+                               options: KingfisherOptionsInfo? = nil,
+                        callbackQueue: CallbackQueue = .mainCurrentOrAsync,
+                     completionHandler: ((Result<ImageCacheResult, KingfisherError>) -> Void)?)
+    {
+        retrieveImage(
+            forKey: key,
+            options: KingfisherParsedOptionsInfo(options),
+            callbackQueue: callbackQueue,
+            completionHandler: completionHandler)
+    }
+
+    func retrieveImageInMemoryCache(
+        forKey key: String,
+        options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
+    {
+        let computedKey = key.computedKey(with: options.processor.identifier)
+        return memoryStorage.value(forKey: computedKey, extendingExpiration: options.memoryCacheAccessExtendingExpiration)
+    }
+
+    /// Gets an image for a given key from the memory storage.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the image.
+    ///   - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
+    /// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or
+    ///            has already expired, `nil` is returned.
+    open func retrieveImageInMemoryCache(
+        forKey key: String,
+        options: KingfisherOptionsInfo? = nil) -> KFCrossPlatformImage?
+    {
+        return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options))
+    }
+
+    func retrieveImageInDiskCache(
+        forKey key: String,
+        options: KingfisherParsedOptionsInfo,
+        callbackQueue: CallbackQueue = .untouch,
+        completionHandler: @escaping (Result<KFCrossPlatformImage?, KingfisherError>) -> Void)
+    {
+        let computedKey = key.computedKey(with: options.processor.identifier)
+        let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue)
+        loadingQueue.execute {
+            do {
+                var image: KFCrossPlatformImage? = nil
+                if let data = try self.diskStorage.value(forKey: computedKey, extendingExpiration: options.diskCacheAccessExtendingExpiration) {
+                    image = options.cacheSerializer.image(with: data, options: options)
+                }
+                callbackQueue.execute { completionHandler(.success(image)) }
+            } catch {
+                if let error = error as? KingfisherError {
+                    callbackQueue.execute { completionHandler(.failure(error)) }
+                } else {
+                    assertionFailure("The internal thrown error should be a `KingfisherError`.")
+                }
+            }
+        }
+    }
+    
+    /// Gets an image for a given key from the disk storage.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the image.
+    ///   - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
+    ///   - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`.
+    ///   - completionHandler: A closure which is invoked when the operation finishes.
+    open func retrieveImageInDiskCache(
+        forKey key: String,
+        options: KingfisherOptionsInfo? = nil,
+        callbackQueue: CallbackQueue = .untouch,
+        completionHandler: @escaping (Result<KFCrossPlatformImage?, KingfisherError>) -> Void)
+    {
+        retrieveImageInDiskCache(
+            forKey: key,
+            options: KingfisherParsedOptionsInfo(options),
+            callbackQueue: callbackQueue,
+            completionHandler: completionHandler)
+    }
+
+    // MARK: Cleaning
+    /// Clears the memory & disk storage of this cache. This is an async operation.
+    ///
+    /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.
+    ///                      This `handler` will be called from the main queue.
+    public func clearCache(completion handler: (() -> Void)? = nil) {
+        clearMemoryCache()
+        clearDiskCache(completion: handler)
+    }
+    
+    /// Clears the memory storage of this cache.
+    @objc public func clearMemoryCache() {
+        memoryStorage.removeAll()
+    }
+    
+    /// Clears the disk storage of this cache. This is an async operation.
+    ///
+    /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.
+    ///                      This `handler` will be called from the main queue.
+    open func clearDiskCache(completion handler: (() -> Void)? = nil) {
+        ioQueue.async {
+            do {
+                try self.diskStorage.removeAll()
+            } catch _ { }
+            if let handler = handler {
+                DispatchQueue.main.async { handler() }
+            }
+        }
+    }
+    
+    /// Clears the expired images from memory & disk storage. This is an async operation.
+    open func cleanExpiredCache(completion handler: (() -> Void)? = nil) {
+        cleanExpiredMemoryCache()
+        cleanExpiredDiskCache(completion: handler)
+    }
+
+    /// Clears the expired images from disk storage.
+    open func cleanExpiredMemoryCache() {
+        memoryStorage.removeExpired()
+    }
+    
+    /// Clears the expired images from disk storage. This is an async operation.
+    @objc func cleanExpiredDiskCache() {
+        cleanExpiredDiskCache(completion: nil)
+    }
+
+    /// Clears the expired images from disk storage. This is an async operation.
+    ///
+    /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.
+    ///                      This `handler` will be called from the main queue.
+    open func cleanExpiredDiskCache(completion handler: (() -> Void)? = nil) {
+        ioQueue.async {
+            do {
+                var removed: [URL] = []
+                let removedExpired = try self.diskStorage.removeExpiredValues()
+                removed.append(contentsOf: removedExpired)
+
+                let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues()
+                removed.append(contentsOf: removedSizeExceeded)
+
+                if !removed.isEmpty {
+                    DispatchQueue.main.async {
+                        let cleanedHashes = removed.map { $0.lastPathComponent }
+                        NotificationCenter.default.post(
+                            name: .KingfisherDidCleanDiskCache,
+                            object: self,
+                            userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
+                    }
+                }
+
+                if let handler = handler {
+                    DispatchQueue.main.async { handler() }
+                }
+            } catch {}
+        }
+    }
+
+#if !os(macOS) && !os(watchOS)
+    /// Clears the expired images from disk storage when app is in background. This is an async operation.
+    /// In most cases, you should not call this method explicitly.
+    /// It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received.
+    @objc public func backgroundCleanExpiredDiskCache() {
+        // if 'sharedApplication()' is unavailable, then return
+        guard let sharedApplication = KingfisherWrapper<UIApplication>.shared else { return }
+
+        func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) {
+            sharedApplication.endBackgroundTask(task)
+            task = UIBackgroundTaskIdentifier.invalid
+        }
+        
+        var backgroundTask: UIBackgroundTaskIdentifier!
+        backgroundTask = sharedApplication.beginBackgroundTask {
+            endBackgroundTask(&backgroundTask!)
+        }
+        
+        cleanExpiredDiskCache {
+            endBackgroundTask(&backgroundTask!)
+        }
+    }
+#endif
+
+    // MARK: Image Cache State
+
+    /// Returns the cache type for a given `key` and `identifier` combination.
+    /// This method is used for checking whether an image is cached in current cache.
+    /// It also provides information on which kind of cache can it be found in the return value.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the image.
+    ///   - identifier: Processor identifier which used for this image. Default is the `identifier` of
+    ///                 `DefaultImageProcessor.default`.
+    /// - Returns: A `CacheType` instance which indicates the cache status.
+    ///            `.none` means the image is not in cache or it is already expired.
+    open func imageCachedType(
+        forKey key: String,
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> CacheType
+    {
+        let computedKey = key.computedKey(with: identifier)
+        if memoryStorage.isCached(forKey: computedKey) { return .memory }
+        if diskStorage.isCached(forKey: computedKey) { return .disk }
+        return .none
+    }
+    
+    /// Returns whether the file exists in cache for a given `key` and `identifier` combination.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the image.
+    ///   - identifier: Processor identifier which used for this image. Default is the `identifier` of
+    ///                 `DefaultImageProcessor.default`.
+    /// - Returns: A `Bool` which indicates whether a cache could match the given `key` and `identifier` combination.
+    ///
+    /// - Note:
+    /// The return value does not contain information about from which kind of storage the cache matches.
+    /// To get the information about cache type according `CacheType`,
+    /// use `imageCachedType(forKey:processorIdentifier:)` instead.
+    public func isCached(
+        forKey key: String,
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> Bool
+    {
+        return imageCachedType(forKey: key, processorIdentifier: identifier).cached
+    }
+    
+    /// Gets the hash used as cache file name for the key.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the image.
+    ///   - identifier: Processor identifier which used for this image. Default is the `identifier` of
+    ///                 `DefaultImageProcessor.default`.
+    /// - Returns: The hash which is used as the cache file name.
+    ///
+    /// - Note:
+    /// By default, for a given combination of `key` and `identifier`, `ImageCache` will use the value
+    /// returned by this method as the cache file name. You can use this value to check and match cache file
+    /// if you need.
+    open func hash(
+        forKey key: String,
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String
+    {
+        let computedKey = key.computedKey(with: identifier)
+        return diskStorage.cacheFileName(forKey: computedKey)
+    }
+    
+    /// Calculates the size taken by the disk storage.
+    /// It is the total file size of all cached files in the `diskStorage` on disk in bytes.
+    ///
+    /// - Parameter handler: Called with the size calculating finishes. This closure is invoked from the main queue.
+    open func calculateDiskStorageSize(completion handler: @escaping ((Result<UInt, KingfisherError>) -> Void)) {
+        ioQueue.async {
+            do {
+                let size = try self.diskStorage.totalSize()
+                DispatchQueue.main.async { handler(.success(size)) }
+            } catch {
+                if let error = error as? KingfisherError {
+                    DispatchQueue.main.async { handler(.failure(error)) }
+                } else {
+                    assertionFailure("The internal thrown error should be a `KingfisherError`.")
+                }
+                
+            }
+        }
+    }
+    
+    /// Gets the cache path for the key.
+    /// It is useful for projects with web view or anyone that needs access to the local file path.
+    ///
+    /// i.e. Replacing the `<img src='path_for_key'>` tag in your HTML.
+    ///
+    /// - Parameters:
+    ///   - key: The key used for caching the image.
+    ///   - identifier: Processor identifier which used for this image. Default is the `identifier` of
+    ///                 `DefaultImageProcessor.default`.
+    /// - Returns: The disk path of cached image under the given `key` and `identifier`.
+    ///
+    /// - Note:
+    /// This method does not guarantee there is an image already cached in the returned path. It just gives your
+    /// the path that the image should be, if it exists in disk storage.
+    ///
+    /// You could use `isCached(forKey:)` method to check whether the image is cached under that key in disk.
+    open func cachePath(
+        forKey key: String,
+        processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String
+    {
+        let computedKey = key.computedKey(with: identifier)
+        return diskStorage.cacheFileURL(forKey: computedKey).path
+    }
+}
+
+extension Dictionary {
+    func keysSortedByValue(_ isOrderedBefore: (Value, Value) -> Bool) -> [Key] {
+        return Array(self).sorted{ isOrderedBefore($0.1, $1.1) }.map{ $0.0 }
+    }
+}
+
+#if !os(macOS) && !os(watchOS)
+// MARK: - For App Extensions
+extension UIApplication: KingfisherCompatible { }
+extension KingfisherWrapper where Base: UIApplication {
+    public static var shared: UIApplication? {
+        let selector = NSSelectorFromString("sharedApplication")
+        guard Base.responds(to: selector) else { return nil }
+        return Base.perform(selector).takeUnretainedValue() as? UIApplication
+    }
+}
+#endif
+
+extension String {
+    func computedKey(with identifier: String) -> String {
+        if identifier.isEmpty {
+            return self
+        } else {
+            return appending("@\(identifier)")
+        }
+    }
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików