100Years 3 luni în urmă
părinte
comite
76e4fcf692
100 a modificat fișierele cu 24235 adăugiri și 1615 ștergeri
  1. 3 2
      Podfile
  2. 17 5
      Podfile.lock
  3. 1589 0
      Pods/BSText/BSText/BSLabel.swift
  4. 29 0
      Pods/BSText/BSText/BSText.h
  5. 2668 0
      Pods/BSText/BSText/BSTextView.swift
  6. 196 0
      Pods/BSText/BSText/Component/TextContainerView.swift
  7. 225 0
      Pods/BSText/BSText/Component/TextDebugOption.swift
  8. 529 0
      Pods/BSText/BSText/Component/TextEffectWindow.swift
  9. 287 0
      Pods/BSText/BSText/Component/TextInput.swift
  10. 586 0
      Pods/BSText/BSText/Component/TextKeyboardManager.swift
  11. 4283 0
      Pods/BSText/BSText/Component/TextLayout.swift
  12. 267 0
      Pods/BSText/BSText/Component/TextLine.swift
  13. 376 0
      Pods/BSText/BSText/Component/TextMagnifier.swift
  14. 388 0
      Pods/BSText/BSText/Component/TextSelectionView.swift
  15. 305 0
      Pods/BSText/BSText/String/TextArchiver.swift
  16. 542 0
      Pods/BSText/BSText/String/TextParser.swift
  17. 143 0
      Pods/BSText/BSText/String/TextRubyAnnotation.swift
  18. 84 0
      Pods/BSText/BSText/String/TextRunDelegate.swift
  19. 2650 0
      Pods/BSText/BSText/Utility/NSAttributedStringExtension.swift
  20. 180 0
      Pods/BSText/BSText/Utility/ParagraphStyleExtension.swift
  21. 98 0
      Pods/BSText/BSText/Utility/StringExtension.swift
  22. 286 0
      Pods/BSText/BSText/Utility/TextAsyncLayer.swift
  23. 927 0
      Pods/BSText/BSText/Utility/TextAttribute.swift
  24. 81 0
      Pods/BSText/BSText/Utility/TextTransaction.swift
  25. 873 0
      Pods/BSText/BSText/Utility/TextUtilities.swift
  26. 160 0
      Pods/BSText/BSText/Utility/UIPasteboardExtension.swift
  27. 134 0
      Pods/BSText/BSText/Utility/UIViewExtension.swift
  28. 87 0
      Pods/BSText/BSText/Utility/WeakTimerProxy.swift
  29. 21 0
      Pods/BSText/LICENSE
  30. 1159 0
      Pods/BSText/README.md
  31. 17 5
      Pods/Manifest.lock
  32. 659 527
      Pods/Pods.xcodeproj/project.pbxproj
  33. 3 3
      Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/BSText.xcscheme
  34. 3 3
      Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/SVProgressHUD.xcscheme
  35. 58 0
      Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/YYImage.xcscheme
  36. 10 27
      Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/xcschememanagement.plist
  37. 21 0
      Pods/SVProgressHUD/LICENSE
  38. 221 0
      Pods/SVProgressHUD/README.md
  39. 1 1
      Pods/SVProgressHUD/SVProgressHUD/PrivacyInfo.xcprivacy
  40. 17 0
      Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.h
  41. 142 0
      Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.m
  42. 17 0
      Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.h
  43. 96 0
      Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.m
  44. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask.png
  45. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@2x.png
  46. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@3x.png
  47. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error.png
  48. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@2x.png
  49. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@3x.png
  50. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info.png
  51. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@2x.png
  52. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@3x.png
  53. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success.png
  54. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@2x.png
  55. BIN
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@3x.png
  56. 392 0
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.h
  57. 1524 0
      Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.m
  58. 14 0
      Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.h
  59. 25 0
      Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.m
  60. 1 1
      Pods/Target Support Files/BSText/BSText-Info.plist
  61. 5 0
      Pods/Target Support Files/BSText/BSText-dummy.m
  62. 0 0
      Pods/Target Support Files/BSText/BSText-prefix.pch
  63. 17 0
      Pods/Target Support Files/BSText/BSText-umbrella.h
  64. 16 0
      Pods/Target Support Files/BSText/BSText.debug.xcconfig
  65. 6 0
      Pods/Target Support Files/BSText/BSText.modulemap
  66. 16 0
      Pods/Target Support Files/BSText/BSText.release.xcconfig
  67. 70 18
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-acknowledgements.markdown
  68. 85 21
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-acknowledgements.plist
  69. 3 1
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks-Debug-input-files.xcfilelist
  70. 3 1
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks-Debug-output-files.xcfilelist
  71. 3 1
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks-Release-input-files.xcfilelist
  72. 3 1
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks-Release-output-files.xcfilelist
  73. 6 2
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks.sh
  74. 5 5
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper.debug.xcconfig
  75. 5 5
      Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper.release.xcconfig
  76. 5 3
      Pods/Target Support Files/SVProgressHUD/SVProgressHUD-Info.plist
  77. 5 0
      Pods/Target Support Files/SVProgressHUD/SVProgressHUD-dummy.m
  78. 0 4
      Pods/Target Support Files/SVProgressHUD/SVProgressHUD-prefix.pch
  79. 20 0
      Pods/Target Support Files/SVProgressHUD/SVProgressHUD-umbrella.h
  80. 2 4
      Pods/Target Support Files/SVProgressHUD/SVProgressHUD.debug.xcconfig
  81. 6 0
      Pods/Target Support Files/SVProgressHUD/SVProgressHUD.modulemap
  82. 2 4
      Pods/Target Support Files/SVProgressHUD/SVProgressHUD.release.xcconfig
  83. 0 5
      Pods/Target Support Files/Toast-Swift/Toast-Swift-dummy.m
  84. 0 6
      Pods/Target Support Files/Toast-Swift/Toast-Swift.modulemap
  85. 26 0
      Pods/Target Support Files/YYImage/YYImage-Info.plist
  86. 5 0
      Pods/Target Support Files/YYImage/YYImage-dummy.m
  87. 12 0
      Pods/Target Support Files/YYImage/YYImage-prefix.pch
  88. 21 0
      Pods/Target Support Files/YYImage/YYImage-umbrella.h
  89. 13 0
      Pods/Target Support Files/YYImage/YYImage.debug.xcconfig
  90. 6 0
      Pods/Target Support Files/YYImage/YYImage.modulemap
  91. 13 0
      Pods/Target Support Files/YYImage/YYImage.release.xcconfig
  92. 0 20
      Pods/Toast-Swift/LICENSE
  93. 0 143
      Pods/Toast-Swift/README.md
  94. 0 797
      Pods/Toast-Swift/Toast/Toast.swift
  95. 22 0
      Pods/YYImage/LICENSE
  96. 384 0
      Pods/YYImage/README.md
  97. 125 0
      Pods/YYImage/YYImage/YYAnimatedImageView.h
  98. 672 0
      Pods/YYImage/YYImage/YYAnimatedImageView.m
  99. 109 0
      Pods/YYImage/YYImage/YYFrameImage.h
  100. 150 0
      Pods/YYImage/YYImage/YYFrameImage.m

+ 3 - 2
Podfile

@@ -12,14 +12,15 @@ target 'TSLiveWallpaper' do
   pod 'ObjectMapper', '4.2'
   pod 'SnapKit'
 
-  pod 'Toast-Swift'
+#  pod 'Toast-Swift'
+  pod 'SVProgressHUD'
   pod 'Kingfisher', '7.10.0'
 #  pod 'Alamofire', '5.6.4'
 
   pod 'MJRefresh', '3.7.5'
   pod 'IQKeyboardManagerSwift', '6.5.12'
   pod 'TYCyclePagerView'
-
+  pod 'BSText'
 end
 
 

+ 17 - 5
Podfile.lock

@@ -1,40 +1,52 @@
 PODS:
+  - BSText (1.1.3):
+    - YYImage
   - IQKeyboardManagerSwift (6.5.12)
   - Kingfisher (7.10.0)
   - MJRefresh (3.7.5)
   - ObjectMapper (4.2.0)
   - SnapKit (5.7.1)
-  - Toast-Swift (5.1.1)
+  - SVProgressHUD (2.3.1):
+    - SVProgressHUD/Core (= 2.3.1)
+  - SVProgressHUD/Core (2.3.1)
   - TYCyclePagerView (1.2.0)
+  - YYImage (1.0.4):
+    - YYImage/Core (= 1.0.4)
+  - YYImage/Core (1.0.4)
 
 DEPENDENCIES:
+  - BSText
   - IQKeyboardManagerSwift (= 6.5.12)
   - Kingfisher (= 7.10.0)
   - MJRefresh (= 3.7.5)
   - ObjectMapper (= 4.2)
   - SnapKit
-  - Toast-Swift
+  - SVProgressHUD
   - TYCyclePagerView
 
 SPEC REPOS:
   trunk:
+    - BSText
     - IQKeyboardManagerSwift
     - Kingfisher
     - MJRefresh
     - ObjectMapper
     - SnapKit
-    - Toast-Swift
+    - SVProgressHUD
     - TYCyclePagerView
+    - YYImage
 
 SPEC CHECKSUMS:
+  BSText: fde17ab2d7b591745f73a408de0eeed063009b6a
   IQKeyboardManagerSwift: 371b08cb39664fb56030f5345c815a4ffc74bbc0
   Kingfisher: a18f05d3b6d37d8650ee4a3e61d57a28fc6207f6
   MJRefresh: fdf5e979eb406a0341468932d1dfc8b7f9fce961
   ObjectMapper: 1eb41f610210777375fa806bf161dc39fb832b81
   SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
-  Toast-Swift: 7a03a532afe3a560d4044bc7c237e2864d295173
+  SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22
   TYCyclePagerView: 2b051dade0615c70784aa34f40c646feeddb7344
+  YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
 
-PODFILE CHECKSUM: d845390dcb5d2a1807fc43839de70d8677485883
+PODFILE CHECKSUM: ffe07205f591638b00c11baae131cf7d7b5a44f5
 
 COCOAPODS: 1.16.2

+ 1589 - 0
Pods/BSText/BSText/BSLabel.swift

@@ -0,0 +1,1589 @@
+//
+//  BSLabel.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/12/19.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+private let BSLabelGetReleaseQueue = DispatchQueue.global(qos: .default)
+
+/// Time in seconds the fingers must be held down for long press gesture.
+fileprivate let kLongPressMinimumDuration = 0.5
+/// Maximum movement in points allowed before the long press fails.
+fileprivate let kLongPressAllowableMovement: Float = 9
+/// Time in seconds for highlight fadeout animation.
+fileprivate let kHighlightFadeDuration = 0.15
+/// Time in seconds for async display fadeout animation.
+fileprivate let kAsyncFadeDuration = 0.08
+
+
+/**
+ The BSLabel class implements a read-only text view.
+ 
+ @discussion The API and behavior is similar to UILabel, but provides more features:
+ 
+ * It supports asynchronous layout and rendering (to avoid blocking UI thread).
+ * It extends the CoreText attributes to support more text effects.
+ * It allows to add UIImage, UIView and CALayer as text attachments.
+ * It allows to add 'highlight' link to some range of text to allow user interact with.
+ * It allows to add container path and exclusion paths to control text container's shape.
+ * It supports vertical form layout to display CJK text.
+ 
+ See NSAttributedStringExtension.swift for more convenience methods to set the attributes.
+ See TextAttribute.swift and TextLayout.swift for more information.
+ */
+open class BSLabel: UIView, TextDebugTarget, TextAsyncLayerDelegate, NSSecureCoding {
+    
+    // MARK: - Accessing the Text Attributes
+    
+    ///=============================================================================
+    /// @name Accessing the Text Attributes
+    ///=============================================================================
+    
+    private var _text: String?
+    /**
+     The text displayed by the label. Default is nil.
+     Set a new value to this property also replaces the text in `attributedText`.
+     Get the value returns the plain text in `attributedText`.
+     */
+    @objc open var text: String? {
+        set {
+            if (_text == newValue) {
+                return
+            }
+            _text = newValue
+            let needAddAttributes = (innerText.length == 0 && (text?.length ?? 0) > 0)
+            innerText.replaceCharacters(in: NSRange(location: 0, length: innerText.length), with: text != nil ? text! : "")
+            innerText.bs_removeDiscontinuousAttributes(in: NSRange(location: 0, length: innerText.length))
+            if needAddAttributes {
+                innerText.bs_font = font
+                innerText.bs_color = textColor
+                innerText.bs_shadow = _shadowFromProperties()
+                innerText.bs_alignment = textAlignment
+                switch lineBreakMode {
+                case NSLineBreakMode.byWordWrapping, NSLineBreakMode.byCharWrapping, NSLineBreakMode.byClipping:
+                    innerText.bs_lineBreakMode = lineBreakMode
+                case NSLineBreakMode.byTruncatingHead, NSLineBreakMode.byTruncatingTail, NSLineBreakMode.byTruncatingMiddle:
+                    innerText.bs_lineBreakMode = NSLineBreakMode.byWordWrapping
+                default:
+                    break
+                }
+            }
+            if let t = textParser, t.parseText(innerText, selectedRange: nil) {
+                _updateOuterTextProperties()
+            }
+            if !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _text
+        }
+    }
+    
+    private lazy var _font = BSLabel._defaultFont
+    /**
+     The font of the text. Default is 17-point system font.
+     Set a new value to this property also causes the new font to be applied to the entire `attributedText`.
+     Get the value returns the font at the head of `attributedText`.
+     */
+    @objc open var font: UIFont? {
+        set {
+            let f = newValue ?? BSLabel._defaultFont
+            if _font == f { return }
+            _font = f
+            innerText.bs_font = _font
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _font
+        }
+    }
+    
+    private var _textColor = UIColor.black
+    
+    /**
+     The color of the text. Default is black.
+     Set a new value to this property also causes the new color to be applied to the entire `attributedText`.
+     Get the value returns the color at the head of `attributedText`.
+     */
+    @objc open var textColor: UIColor {
+        set {
+            if _textColor == newValue {
+                return
+            }
+            _textColor = newValue
+            innerText.bs_color = _textColor
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+            }
+        }
+        get {
+            return _textColor
+        }
+    }
+    
+    private var _shadowColor: UIColor?
+    
+    /**
+     The shadow color of the text. Default is nil.
+     Set a new value to this property also causes the shadow color to be applied to the entire `attributedText`.
+     Get the value returns the shadow color at the head of `attributedText`.
+     */
+    @objc open var shadowColor: UIColor? {
+        set {
+            if (_shadowColor == newValue) {
+                return
+            }
+            _shadowColor = newValue
+            innerText.bs_shadow = _shadowFromProperties()
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+            }
+        }
+        get {
+            return _shadowColor
+        }
+    }
+    
+    private var _shadowOffset = CGSize.zero
+    /**
+     The shadow offset of the text. Default is CGSizeZero.
+     Set a new value to this property also causes the shadow offset to be applied to the entire `attributedText`.
+     Get the value returns the shadow offset at the head of `attributedText`.
+     */
+    @objc open var shadowOffset: CGSize {
+        set {
+            if _shadowOffset == newValue {
+                return
+            }
+            _shadowOffset = newValue
+            innerText.bs_shadow = _shadowFromProperties()
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+            }
+        }
+        get {
+            return _shadowOffset
+        }
+    }
+    
+    private var _shadowBlurRadius: CGFloat = 0
+    /**
+     The shadow blur of the text. Default is 0.
+     Set a new value to this property also causes the shadow blur to be applied to the entire `attributedText`.
+     Get the value returns the shadow blur at the head of `attributedText`.
+     */
+    @objc open var shadowBlurRadius: CGFloat {
+        set {
+            if _shadowBlurRadius == newValue {
+                return
+            }
+            _shadowBlurRadius = newValue
+            innerText.bs_shadow = _shadowFromProperties()
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+            }
+        }
+        get {
+            return _shadowBlurRadius
+        }
+    }
+    
+    private var _textAlignment = NSTextAlignment.natural
+    /**
+     The technique to use for aligning the text. Default is NSTextAlignmentNatural.
+     Set a new value to this property also causes the new alignment to be applied to the entire `attributedText`.
+     Get the value returns the alignment at the head of `attributedText`.
+     */
+    @objc open var textAlignment: NSTextAlignment {
+        set {
+            if _textAlignment == newValue {
+                return
+            }
+            _textAlignment = newValue
+            innerText.bs_alignment = _textAlignment
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _textAlignment
+        }
+    }
+    /**
+     The text vertical aligmnent in container. Default is TextVerticalAlignment.center.
+     */
+    @objc open var textVerticalAlignment = TextVerticalAlignment.center {
+        didSet {
+            if self.textVerticalAlignment == oldValue {
+                return
+            }
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+    }
+    
+    private var _attributedText: NSAttributedString?
+    
+    /**
+     The styled text displayed by the label.
+     Set a new value to this property also replaces the value of the `text`, `font`, `textColor`,
+     `textAlignment` and other properties in label.
+     
+     @discussion It only support the attributes declared in CoreText and TextAttribute.
+     See `NSAttributedStringExtension.swift` for more convenience methods to set the attributes.
+     */
+    @objc open var attributedText: NSAttributedString? {
+        set {
+            if _attributedText == newValue {
+                return
+            }
+            if let n = newValue, n.length > 0 {
+                innerText = NSMutableAttributedString(attributedString: n)
+                switch lineBreakMode {
+                case NSLineBreakMode.byWordWrapping, NSLineBreakMode.byCharWrapping, NSLineBreakMode.byClipping:
+                    innerText.bs_lineBreakMode = lineBreakMode
+                case NSLineBreakMode.byTruncatingHead, NSLineBreakMode.byTruncatingTail, NSLineBreakMode.byTruncatingMiddle:
+                    innerText.bs_lineBreakMode = NSLineBreakMode.byWordWrapping
+                default:
+                    break
+                }
+            } else {
+                innerText = NSMutableAttributedString()
+            }
+            textParser?.parseText(innerText, selectedRange: nil)
+            if !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _updateOuterTextProperties()
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _attributedText
+        }
+    }
+    
+    private var _lineBreakMode = NSLineBreakMode.byTruncatingTail
+    /**
+     The technique to use for wrapping and truncating the label's text.
+     Default is NSLineBreakByTruncatingTail.
+     */
+    @objc open var lineBreakMode: NSLineBreakMode {
+        set {
+            if _lineBreakMode == newValue {
+                return
+            }
+            _lineBreakMode = newValue
+            innerText.bs_lineBreakMode = _lineBreakMode
+            // allow multi-line break
+            switch _lineBreakMode {
+            case .byWordWrapping, .byCharWrapping, .byClipping:
+                innerContainer.truncationType = .none
+                innerText.bs_lineBreakMode = _lineBreakMode
+            case .byTruncatingHead:
+                innerContainer.truncationType = .start
+                innerText.bs_lineBreakMode = NSLineBreakMode.byWordWrapping
+            case .byTruncatingTail:
+                innerContainer.truncationType = .end
+                innerText.bs_lineBreakMode = NSLineBreakMode.byWordWrapping
+            case .byTruncatingMiddle:
+                innerContainer.truncationType = .middle
+                innerText.bs_lineBreakMode = NSLineBreakMode.byWordWrapping
+            default:
+                break
+            }
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _lineBreakMode
+        }
+    }
+    
+    private var _truncationToken: NSAttributedString?
+    /**
+     The truncation token string used when text is truncated. Default is nil.
+     When the value is nil, the label use "…" as default truncation token.
+     */
+    @objc open var truncationToken: NSAttributedString? {
+        set {
+            if _truncationToken == newValue {
+                return
+            }
+            _truncationToken = newValue
+            innerContainer.truncationToken = _truncationToken
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _truncationToken
+        }
+    }
+    
+    private var _numberOfLines: Int = 1
+    /**
+     The maximum number of lines to use for rendering text. Default value is 1.
+     0 means no limit.
+     */
+    @objc open var numberOfLines: Int {
+        set {
+            if _numberOfLines == newValue {
+                return
+            }
+            _numberOfLines = newValue
+            innerContainer.maximumNumberOfRows = _numberOfLines
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _numberOfLines
+        }
+    }
+    /**
+     When `text` or `attributedText` is changed, the parser will be called to modify the text.
+     It can be used to add code highlighting or emoticon replacement to text view.
+     The default value is nil.
+     
+     See `TextParser` protocol for more information.
+     */
+    @objc open var textParser: TextParser? {
+        didSet {
+            if self.textParser === oldValue {
+                return
+            }
+            if self.textParser?.parseText(innerText, selectedRange: nil) ?? false {
+                _updateOuterTextProperties()
+                if !ignoreCommonProperties {
+                    if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                        _clearContents()
+                    }
+                    _setLayoutNeedUpdate()
+                    _endTouch()
+                    invalidateIntrinsicContentSize()
+                }
+            }
+        }
+    }
+    /**
+     The current text layout in text view. It can be used to query the text layout information.
+     Set a new value to this property also replaces most properties in this label, such as `text`,
+     `color`, `attributedText`, `lineBreakMode`, `textContainerPath`, `exclusionPaths` and so on.
+     */
+    @objc open var textLayout: TextLayout? {
+        set {
+            innerLayout = newValue
+            shrinkInnerLayout = nil
+            
+            if ignoreCommonProperties {
+                innerText = newValue!.text as? NSMutableAttributedString ?? NSMutableAttributedString()
+                innerContainer = newValue!.container.copy() as! TextContainer
+            } else {
+                innerText = (newValue?.text != nil) ? NSMutableAttributedString(attributedString: newValue!.text!) : NSMutableAttributedString()
+                
+                _updateOuterTextProperties()
+                
+                if let t = newValue?.container.copy() as! TextContainer? {
+                    innerContainer = t
+                } else {
+                    innerContainer = TextContainer()
+                    innerContainer.size = bounds.size
+                    innerContainer.insets = textContainerInset
+                }
+                _updateOuterContainerProperties()
+            }
+            
+            if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                _clearContents()
+            }
+            state.layoutNeedUpdate = false
+            _setLayoutNeedRedraw()
+            _endTouch()
+            invalidateIntrinsicContentSize()
+        }
+        get {
+            _updateIfNeeded()
+            return innerLayout
+        }
+    }
+    // MARK: - Configuring the Text Container
+    
+    ///=============================================================================
+    /// @name Configuring the Text Container
+    ///=============================================================================
+    
+    private var _textContainerPath: UIBezierPath?
+    /**
+     A UIBezierPath object that specifies the shape of the text frame. Default value is nil.
+     */
+    @objc open var textContainerPath: UIBezierPath? {
+        set {
+            if _textContainerPath == newValue { return }
+            
+            _textContainerPath = newValue
+            innerContainer.path = _textContainerPath
+            if textContainerPath == nil {
+                innerContainer.size = bounds.size
+                innerContainer.insets = textContainerInset
+            }
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _textContainerPath
+        }
+    }
+    
+    private var _exclusionPaths: [UIBezierPath]?
+    /**
+     An array of UIBezierPath objects representing the exclusion paths inside the
+     receiver's bounding rectangle. Default value is nil.
+     */
+    @objc open var exclusionPaths: [UIBezierPath]? {
+        set {
+            if _exclusionPaths == newValue { return }
+            
+            _exclusionPaths = newValue
+            if let aPaths = _exclusionPaths {
+                innerContainer.exclusionPaths = aPaths
+            }
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _exclusionPaths
+        }
+    }
+    
+    private var _textContainerInset = UIEdgeInsets.zero
+    /**
+     The inset of the text container's layout area within the text view's content area.
+     Default value is UIEdgeInsetsZero.
+     */
+    @objc open var textContainerInset: UIEdgeInsets {
+        set {
+            if _textContainerInset == newValue { return }
+            
+            _textContainerInset = newValue
+            innerContainer.insets = _textContainerInset
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _textContainerInset
+        }
+    }
+    
+    private var _verticalForm = false
+    /**
+     Whether the receiver's layout orientation is vertical form. Default is false.
+     It may used to display CJK text.
+     */
+    @objc open var verticalForm: Bool {
+        set {
+            if _verticalForm == newValue { return }
+            
+            _verticalForm = newValue
+            innerContainer.isVerticalForm = _verticalForm
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _verticalForm
+        }
+    }
+    
+    private var _linePositionModifier: TextLinePositionModifier?
+    /**
+     The text line position modifier used to modify the lines' position in layout.
+     Default value is nil.
+     See `TextLinePositionModifier` protocol for more information.
+     */
+    @objc open weak var linePositionModifier: TextLinePositionModifier? {
+        set {
+            if _linePositionModifier === newValue { return }
+            
+            _linePositionModifier = newValue
+            innerContainer.linePositionModifier = _linePositionModifier
+            if innerText.length != 0 && !ignoreCommonProperties {
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedUpdate()
+                _endTouch()
+                invalidateIntrinsicContentSize()
+            }
+        }
+        get {
+            return _linePositionModifier
+        }
+    }
+    
+    private var _debugOption: TextDebugOption? = TextDebugOption.shared
+    
+    /**
+     The debug option to display CoreText layout result.
+     The default value is TextDebugOption.shared.
+     */
+    @objc open var debugOption: TextDebugOption? {
+        set {
+            let needDraw = _debugOption?.needDrawDebug
+            _debugOption = newValue?.copy() as? TextDebugOption
+            if _debugOption?.needDrawDebug != needDraw {
+                _setLayoutNeedRedraw()
+            }
+        }
+        get {
+            return _debugOption
+        }
+    }
+    // MARK: - Getting the Layout Constraints
+    
+    ///=============================================================================
+    /// @name Getting the Layout Constraints
+    ///=============================================================================
+    
+    /**
+     The preferred maximum width (in points) for a multiline label.
+     
+     @discussion This property affects the size of the label when layout constraints
+     are applied to it. During layout, if the text extends beyond the width
+     specified by this property, the additional text is flowed to one or more new
+     lines, thereby increasing the height of the label. If the text is vertical
+     form, this value will match to text height.
+     */
+    @objc open var preferredMaxLayoutWidth: CGFloat = 0 {
+        didSet {
+            if self.preferredMaxLayoutWidth == oldValue {
+                return
+            }
+            invalidateIntrinsicContentSize()
+        }
+    }
+    // MARK: - Interacting with Text Data
+    
+    ///=============================================================================
+    /// @name Interacting with Text Data
+    ///=============================================================================
+    
+    /**
+     When user tap the label, this action will be called (similar to tap gesture).
+     The default value is nil.
+     */
+    @objc open var textTapAction: TextAction?
+    /**
+     When user long press the label, this action will be called (similar to long press gesture).
+     The default value is nil.
+     */
+    @objc open var textLongPressAction: TextAction?
+    /**
+     When user tap the highlight range of text, this action will be called.
+     The default value is nil.
+     */
+    @objc open var highlightTapAction: TextAction?
+    /**
+     When user long press the highlight range of text, this action will be called.
+     The default value is nil.
+     */
+    @objc open var highlightLongPressAction: TextAction?
+    // MARK: - Configuring the Display Mode
+    
+    ///=============================================================================
+    /// @name Configuring the Display Mode
+    ///=============================================================================
+    
+    /**
+     A Boolean value indicating whether the layout and rendering codes are running
+     asynchronously on background threads.
+     
+     The default value is `false`.
+     */
+    @objc open var displaysAsynchronously = false {
+        didSet {
+            (layer as? TextAsyncLayer)?.displaysAsynchronously = displaysAsynchronously
+        }
+    }
+    
+    /**
+     If the value is true, and the layer is rendered asynchronously, then it will
+     set label.layer.contents to nil before display.
+     
+     The default value is `true`.
+     
+     @discussion When the asynchronously display is enabled, the layer's content will
+     be updated after the background render process finished. If the render process
+     can not finished in a vsync time (1/60 second), the old content will be still kept
+     for display. You may manually clear the content by set the layer.contents to nil
+     after you update the label's properties, or you can just set this property to true.
+     */
+    @objc open var clearContentsBeforeAsynchronouslyDisplay = true
+    
+    /**
+     If the value is true, and the layer is rendered asynchronously, then it will add
+     a fade animation on layer when the contents of layer changed.
+     
+     The default value is `true`.
+     */
+    @objc open var fadeOnAsynchronouslyDisplay = true
+    
+    /**
+     If the value is true, then it will add a fade animation on layer when some range
+     of text become highlighted.
+     
+     The default value is `true`.
+     */
+    @objc open var fadeOnHighlight = true
+    
+    /**
+     Ignore common properties (such as text, font, textColor, attributedText...) and
+     only use "textLayout" to display content.
+     
+     The default value is `false`.
+     
+     @discussion If you control the label content only through "textLayout", then
+     you may set this value to true for higher performance.
+     */
+    @objc open var ignoreCommonProperties = false
+    
+    /*
+     Tips:
+     
+     1. If you only need a UILabel alternative to display rich text and receive link touch event,
+     you do not need to adjust the display mode properties.
+     
+     2. If you have performance issues, you may enable the asynchronous display mode
+     by setting the `displaysAsynchronously` to true.
+     
+     3. If you want to get the highest performance, you should do text layout with
+     `TextLayout` class in background thread. Here's an example:
+     
+     let label = BSLabel()
+     label.displaysAsynchronously = true
+     label.ignoreCommonProperties = true
+     
+     DispatchQueue.global().async(execute: {
+     
+         // Create attributed string.
+         let text = NSMutableAttributedString.init(string: "Some Text")
+         text.bs_font = UIFont.systemFont(ofSize: 16)
+         text.bs_color = .gray
+         text.bs_set(color: .red, range: NSRange(location: 0, length: 4))
+     
+         // Create text container
+         let container = TextContainer()
+         container.size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude)
+         container.maximumNumberOfRows = 0
+     
+         // Generate a text layout.
+         let layout = TextLayout(container: container, text: text)
+     
+         DispatchQueue.main.async(execute: {
+     
+             label.size = layout.textBoundingSize
+             label.textLayout = layout
+         })
+     })
+     
+     */
+
+    private lazy var innerText = NSMutableAttributedString() ///< nonnull
+    private var innerLayout: TextLayout?
+    private lazy var innerContainer = TextContainer() ///< nonnull
+    private lazy var attachmentViews = [UIView]()
+    private lazy var attachmentLayers = [CALayer]()
+    private lazy var highlightRange = NSRange(location: 0, length: 0) ///< current highlight range
+    private var highlight: TextHighlight? ///< highlight attribute in `_highlightRange`
+    private var highlightLayout: TextLayout? ///< when _state.showingHighlight=YES, this layout should be displayed
+    private var shrinkInnerLayout: TextLayout?
+    private var shrinkHighlightLayout: TextLayout?
+    private var longPressTimer: Timer?
+    private var touchBeganPoint = CGPoint.zero
+    
+    private lazy var state = State()
+    
+    private struct State {
+        var layoutNeedUpdate : Bool = false
+        var showingHighlight : Bool = false
+        
+        var trackingTouch : Bool = false
+        var swallowTouch : Bool = false
+        var touchMoved : Bool = false
+        
+        var hasTapAction : Bool = false
+        var hasLongPressAction : Bool = false
+        
+        var contentsNeedFade : Bool = false
+    }
+    
+    // MARK: - Private
+    private func _updateIfNeeded() {
+        if state.layoutNeedUpdate {
+            state.layoutNeedUpdate = false
+            _updateLayout()
+            layer.setNeedsDisplay()
+        }
+    }
+    
+    private func _updateLayout() {
+        innerLayout = TextLayout(container: innerContainer, text: innerText)
+        shrinkInnerLayout = BSLabel._shrinkLayout(with: innerLayout)
+    }
+    
+    private func _setLayoutNeedUpdate() {
+        state.layoutNeedUpdate = true
+        _clearInnerLayout()
+        _setLayoutNeedRedraw()
+    }
+    
+    private func _setLayoutNeedRedraw() {
+        layer.setNeedsDisplay()
+    }
+    
+    private func _clearInnerLayout() {
+        if innerLayout == nil {
+            return
+        }
+        let layout: TextLayout? = innerLayout
+        innerLayout = nil
+        shrinkInnerLayout = nil
+        BSLabelGetReleaseQueue.async(execute: {
+            let text: NSAttributedString? = layout?.text // capture to block and release in background
+            if let c = layout?.attachments?.count, c != 0 {
+                DispatchQueue.main.async(execute: {
+                    let _ = text?.length // capture to block and release in main thread (maybe there's UIView/CALayer attachments).
+                })
+            }
+        })
+    }
+    
+    private func _innerLayout() -> TextLayout? {
+        return (shrinkInnerLayout != nil) ? shrinkInnerLayout : innerLayout
+    }
+    
+    private func _highlightLayout() -> TextLayout? {
+        return (shrinkHighlightLayout != nil) ? shrinkHighlightLayout : highlightLayout
+    }
+    
+    private class func _shrinkLayout(with layout: TextLayout?) -> TextLayout? {
+        guard let layout = layout else {
+            return nil
+        }
+        guard let t = layout.text, t.length > 0, layout.lines.count == 0 else {
+            return nil
+        }
+        
+        let container = layout.container.copy() as! TextContainer
+        container.maximumNumberOfRows = 1
+        var containerSize = container.size
+        if container.isVerticalForm == false {
+            containerSize.height = TextContainer.textContainerMaxSize.height
+        } else {
+            containerSize.width = TextContainer.textContainerMaxSize.width
+        }
+        container.size = containerSize
+        return TextLayout(container: container, text: layout.text)
+    }
+    
+    private func _startLongPressTimer() {
+        longPressTimer?.invalidate()
+        
+        longPressTimer = Timer.bs_scheduledTimer(with: kLongPressMinimumDuration, target: self, selector: #selector(self._trackDidLongPress), userInfo: nil, repeats: false)
+        RunLoop.current.add(longPressTimer!, forMode: .common)
+    }
+    
+    private func _endLongPressTimer() {
+        longPressTimer?.invalidate()
+        longPressTimer = nil
+    }
+    
+    @objc private func _trackDidLongPress() {
+        _endLongPressTimer()
+        if state.hasLongPressAction && (textLongPressAction != nil) {
+            var range = NSRange(location: NSNotFound, length: 0)
+            var rect = CGRect.null
+            let point: CGPoint = _convertPoint(toLayout: touchBeganPoint)
+            let textRange: TextRange? = innerLayout?.textRange(at: point)
+            var textRect: CGRect = innerLayout?.rect(for: textRange) ?? CGRect.zero
+            textRect = _convertRect(fromLayout: textRect)
+            if textRange != nil {
+                if let aRange = textRange?.asRange {
+                    range = aRange
+                }
+                rect = textRect
+            }
+            textLongPressAction?(self, innerText, range, rect)
+        }
+        if (highlight != nil) {
+            let longPressAction: TextAction? = (highlight!.longPressAction != nil) ? highlight!.longPressAction : highlightLongPressAction
+            if longPressAction != nil {
+                let start = TextPosition.position(with: highlightRange.location)
+                let end = TextPosition.position(with: highlightRange.location + highlightRange.length, affinity: TextAffinity.backward)
+                let range = TextRange.range(with: start, end: end)
+                var rect: CGRect = innerLayout!.rect(for: range)
+                rect = _convertRect(fromLayout: rect)
+                longPressAction!(self, innerText, highlightRange, rect)
+                _removeHighlight(animated: true)
+                state.trackingTouch = false
+            }
+        }
+    }
+    
+    private func _getHighlight(at point: CGPoint, range: NSRangePointer?) -> TextHighlight? {
+        var point = point
+        
+        guard let c = innerLayout?.containsHighlight, c else {
+            return nil
+        }
+        point = _convertPoint(toLayout: point)
+        
+        guard let textRange = innerLayout?.textRange(at: point) else {
+            return nil
+        }
+        
+        var startIndex = textRange.start.offset
+        if startIndex == innerText.length {
+            if startIndex > 0 {
+                startIndex = startIndex - 1
+            }
+        }
+        let highlightRange = NSRangePointer.allocate(capacity: 1)
+        defer {
+            highlightRange.deallocate()
+        }
+        
+        guard let highlight = innerText.attribute(NSAttributedString.Key(rawValue: TextAttribute.textHighlightAttributeName), at: startIndex, longestEffectiveRange: highlightRange, in: NSRange(location: 0, length: innerText.length)) as? TextHighlight else {
+            return nil
+        }
+        
+        range?.pointee = highlightRange.pointee
+        
+        return highlight
+    }
+    
+    private func _showHighlight(animated: Bool) {
+        if highlight == nil { return }
+        if highlightLayout == nil {
+            let hiText = innerText.mutableCopy() as! NSMutableAttributedString
+            let newAttrs = highlight?.attributes
+            for (key, value) in newAttrs ?? [:] {
+                hiText.bs_set(attribute: key, value: value, range: highlightRange)
+            }
+            highlightLayout = TextLayout(container: innerContainer, text: hiText)
+            shrinkHighlightLayout = BSLabel._shrinkLayout(with: highlightLayout)
+            if highlightLayout == nil {
+                highlight = nil
+            }
+        }
+        
+        if (highlightLayout != nil) && !state.showingHighlight {
+            state.showingHighlight = true
+            state.contentsNeedFade = animated
+            _setLayoutNeedRedraw()
+        }
+    }
+    
+    private func _hideHighlight(animated: Bool) {
+        if state.showingHighlight {
+            state.showingHighlight = false
+            state.contentsNeedFade = animated
+            _setLayoutNeedRedraw()
+        }
+    }
+    
+    private func _removeHighlight(animated: Bool) {
+        _hideHighlight(animated: animated)
+        highlight = nil
+        highlightLayout = nil
+        shrinkHighlightLayout = nil
+    }
+    
+    private func _endTouch() {
+        _endLongPressTimer()
+        _removeHighlight(animated: true)
+        state.trackingTouch = false
+    }
+    
+    private func _convertPoint(toLayout point: CGPoint) -> CGPoint {
+        var point = point
+        let boundingSize: CGSize = innerLayout!.textBoundingSize
+        if let v = innerLayout?.container.isVerticalForm, v {
+            var w = innerLayout!.textBoundingSize.width
+            if w < bounds.size.width {
+                w = bounds.size.width
+            }
+            point.x += innerLayout!.container.size.width - w
+            if textVerticalAlignment == TextVerticalAlignment.center {
+                point.x += (bounds.size.width - boundingSize.width) * 0.5
+            } else if textVerticalAlignment == TextVerticalAlignment.bottom {
+                point.x += bounds.size.width - boundingSize.width
+            }
+            return point
+        } else {
+            if textVerticalAlignment == TextVerticalAlignment.center {
+                point.y -= (bounds.size.height - boundingSize.height) * 0.5
+            } else if textVerticalAlignment == TextVerticalAlignment.bottom {
+                point.y -= bounds.size.height - boundingSize.height
+            }
+            return point
+        }
+    }
+    
+    private func _convertPoint(fromLayout point: CGPoint) -> CGPoint {
+        var point = point
+        let boundingSize: CGSize = innerLayout!.textBoundingSize
+        if let v = innerLayout?.container.isVerticalForm, v {
+            var w = innerLayout!.textBoundingSize.width
+            if w < bounds.size.width {
+                w = bounds.size.width
+            }
+            point.x -= innerLayout!.container.size.width - w
+            if boundingSize.width < bounds.size.width {
+                if textVerticalAlignment == TextVerticalAlignment.center {
+                    point.x -= (bounds.size.width - boundingSize.width) * 0.5
+                } else if textVerticalAlignment == TextVerticalAlignment.bottom {
+                    point.x -= bounds.size.width - boundingSize.width
+                }
+            }
+            return point
+        } else {
+            if boundingSize.height < bounds.size.height {
+                if textVerticalAlignment == TextVerticalAlignment.center {
+                    point.y += (bounds.size.height - boundingSize.height) * 0.5
+                } else if textVerticalAlignment == TextVerticalAlignment.bottom {
+                    point.y += bounds.size.height - boundingSize.height
+                }
+            }
+            return point
+        }
+    }
+    
+    private func _convertRect(toLayout rect: CGRect) -> CGRect {
+        var rect = rect
+        rect.origin = _convertPoint(toLayout: rect.origin)
+        return rect
+    }
+    
+    private func _convertRect(fromLayout rect: CGRect) -> CGRect {
+        var rect = rect
+        rect.origin = _convertPoint(fromLayout: rect.origin)
+        return rect
+    }
+    
+    private static let _defaultFont = UIFont.systemFont(ofSize: 17)
+    
+    private func _shadowFromProperties() -> NSShadow? {
+        if !(shadowColor != nil) || shadowBlurRadius < 0 {
+            return nil
+        }
+        let shadow = NSShadow()
+        shadow.shadowColor = shadowColor
+        #if !TARGET_INTERFACE_BUILDER
+        shadow.shadowOffset = shadowOffset
+        #else
+        shadow.shadowOffset = CGSize(width: shadowOffset.x, height: shadowOffset.y)
+        #endif
+        shadow.shadowBlurRadius = shadowBlurRadius
+        return shadow
+    }
+    
+    private func _updateOuterLineBreakMode() {
+        if innerContainer.truncationType != .none {
+            switch innerContainer.truncationType {
+            case .start:
+                _lineBreakMode = NSLineBreakMode.byTruncatingHead
+            case .end:
+                _lineBreakMode = NSLineBreakMode.byTruncatingTail
+            case .middle:
+                _lineBreakMode = NSLineBreakMode.byTruncatingMiddle
+            default:
+                break
+            }
+        } else {
+            _lineBreakMode = innerText.bs_lineBreakMode
+        }
+    }
+    
+    private func _updateOuterTextProperties() {
+        
+        _text = innerText.bs_plainText(for: NSRange(location: 0, length: innerText.length))
+        _font = innerText.bs_font ?? BSLabel._defaultFont
+        _textColor = innerText.bs_color ?? UIColor.black
+        
+        _textAlignment = innerText.bs_alignment
+        _lineBreakMode = innerText.bs_lineBreakMode
+        let shadow: NSShadow? = innerText.bs_shadow
+        _shadowColor = shadow?.shadowColor as! UIColor?
+        // TARGET_INTERFACE_BUILDER
+        _shadowOffset = shadow?.shadowOffset ?? .zero
+        
+        _shadowBlurRadius = shadow?.shadowBlurRadius ?? 0
+        _attributedText = innerText
+        _updateOuterLineBreakMode()
+    }
+    
+    private func _updateOuterContainerProperties() {
+        _truncationToken = innerContainer.truncationToken
+        _numberOfLines = innerContainer.maximumNumberOfRows
+        _textContainerPath = innerContainer.path
+        _exclusionPaths = innerContainer.exclusionPaths
+        _textContainerInset = innerContainer.insets
+        _verticalForm = innerContainer.isVerticalForm
+        _linePositionModifier = innerContainer.linePositionModifier
+        _updateOuterLineBreakMode()
+    }
+    
+    private func _clearContents() {
+        let image = layer.contents as! CGImage?
+        layer.contents = nil
+        if image != nil {
+            BSLabelGetReleaseQueue.async(execute: {
+                let _ = image
+            })
+        }
+    }
+    
+    private func _initLabel() {
+        (layer as? TextAsyncLayer)?.displaysAsynchronously = false
+        layer.contentsScale = UIScreen.main.scale
+        contentMode = .redraw
+        
+        TextDebugOption.add(self)
+        
+        innerContainer.truncationType = .end
+        innerContainer.maximumNumberOfRows = numberOfLines
+        
+        isAccessibilityElement = true
+    }
+
+    // MARK: - Override
+    
+    override public init(frame: CGRect) {
+        super.init(frame: CGRect.zero)
+        backgroundColor = UIColor.clear
+        isOpaque = false
+        _initLabel()
+        self.frame = frame
+    }
+    
+    deinit {
+        TextDebugOption.remove(self)
+        longPressTimer?.invalidate()
+    }
+    
+    override open class var layerClass: AnyClass {
+        return TextAsyncLayer.self
+    }
+    
+    open override var frame: CGRect {
+        set {
+            let oldSize: CGSize = bounds.size
+            super.frame = newValue
+            let newSize: CGSize = bounds.size
+            if oldSize != newSize {
+                innerContainer.size = bounds.size
+                if !ignoreCommonProperties {
+                    state.layoutNeedUpdate = true
+                }
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedRedraw()
+            }
+        }
+        get {
+            return super.frame
+        }
+    }
+    
+    open override var bounds: CGRect {
+        set {
+            let oldSize: CGSize = self.bounds.size
+            super.bounds = newValue
+            let newSize: CGSize = self.bounds.size
+            if oldSize != newSize {
+                innerContainer.size = self.bounds.size
+                if !ignoreCommonProperties {
+                    state.layoutNeedUpdate = true
+                }
+                if displaysAsynchronously && clearContentsBeforeAsynchronouslyDisplay {
+                    _clearContents()
+                }
+                _setLayoutNeedRedraw()
+            }
+        }
+        get {
+            return super.bounds
+        }
+    }
+    
+    override open func sizeThatFits(_ size: CGSize) -> CGSize {
+        var size = size
+        if ignoreCommonProperties {
+            return innerLayout!.textBoundingSize
+        }
+        
+        if !verticalForm && size.width <= 0 {
+            size.width = TextContainer.textContainerMaxSize.width
+        } else if verticalForm && size.height <= 0 {
+            size.height = TextContainer.textContainerMaxSize.height
+        }
+        
+        if (!verticalForm && size.width == bounds.size.width) || (verticalForm && size.height == bounds.size.height) {
+            _updateIfNeeded()
+            let layout: TextLayout? = innerLayout
+            var contains = false
+            if layout?.container.maximumNumberOfRows == 0 {
+                if layout?.truncatedLine == nil {
+                    contains = true
+                }
+            } else {
+                if layout?.rowCount ?? 0 <= (layout?.container.maximumNumberOfRows ?? 0) {
+                    contains = true
+                }
+            }
+            if contains {
+                return layout?.textBoundingSize ?? CGSize.zero
+            }
+        }
+        
+        if !verticalForm {
+            size.height = TextContainer.textContainerMaxSize.height
+        } else {
+            size.width = TextContainer.textContainerMaxSize.width
+        }
+        
+        let container = innerContainer.copy() as? TextContainer
+        container?.size = size
+        
+        let layout = TextLayout(container: container, text: innerText)
+        return layout?.textBoundingSize ?? .zero
+    }
+    
+    func accessibilityLabel() -> String? {
+        return innerLayout?.text?.bs_plainText(for: innerLayout?.text?.bs_rangeOfAll ?? NSRange(location: 0, length: 0))
+    }
+    
+    // MARK: - NSCoding
+    
+    override open func encode(with aCoder: NSCoder) {
+        super.encode(with: aCoder)
+        aCoder.encode(_attributedText, forKey: "attributedText")
+        aCoder.encode(innerContainer, forKey: "innerContainer")
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _initLabel()
+        
+        if let innerContainer = aDecoder.decodeObject(forKey: "innerContainer") as? TextContainer {
+            self.innerContainer = innerContainer
+        } else {
+            self.innerContainer.size = bounds.size
+        }
+        _updateOuterContainerProperties()
+        self.attributedText = aDecoder.decodeObject(forKey: "attributedText") as? NSAttributedString
+        _setLayoutNeedUpdate()
+    }
+    
+    // MARK: - NSSecureCoding
+    public static var supportsSecureCoding: Bool {
+        return true
+    }
+    
+    // MARK: - Touches
+    open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
+        _updateIfNeeded()
+        let touch = touches.first!
+        let point = touch.location(in: self)
+        
+        highlight = _getHighlight(at: point, range: &highlightRange)
+        highlightLayout = nil
+        shrinkHighlightLayout = nil
+        state.hasTapAction = (textTapAction != nil)
+        state.hasLongPressAction = (textLongPressAction != nil)
+        
+        if (highlight != nil) || (textTapAction != nil) || (textLongPressAction != nil) {
+            touchBeganPoint = point
+            state.trackingTouch = true
+            state.swallowTouch = true
+            state.touchMoved = false
+            _startLongPressTimer()
+            if (highlight != nil) {
+                _showHighlight(animated: false)
+            }
+        } else {
+            state.trackingTouch = false
+            state.swallowTouch = false
+            state.touchMoved = false
+        }
+        if !state.swallowTouch {
+            super.touchesBegan(touches, with: event)
+        }
+    }
+    
+    open override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
+        _updateIfNeeded()
+        
+        let touch = touches.first!
+        let point = touch.location(in: self)
+        
+        if state.trackingTouch {
+            if !state.touchMoved {
+                let moveH = Float(point.x - touchBeganPoint.x)
+                let moveV = Float(point.y - touchBeganPoint.y)
+                if abs(moveH) > abs(moveV) {
+                    if abs(moveH) > kLongPressAllowableMovement {
+                        state.touchMoved = true
+                    }
+                } else {
+                    if abs(moveV) > kLongPressAllowableMovement {
+                        state.touchMoved = true
+                    }
+                }
+                if state.touchMoved {
+                    _endLongPressTimer()
+                }
+            }
+            if state.touchMoved && self.highlight != nil {
+                let highlight = _getHighlight(at: point, range: nil)
+                if highlight == self.highlight {
+                    _showHighlight(animated: fadeOnHighlight)
+                } else {
+                    _hideHighlight(animated: fadeOnHighlight)
+                }
+            }
+        }
+        
+        if !state.swallowTouch {
+            super.touchesMoved(touches, with: event)
+        }
+    }
+    
+    open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
+        let touch = touches.first!
+        let point = touch.location(in: self)
+        
+        if state.trackingTouch {
+            _endLongPressTimer()
+            if !state.touchMoved && (textTapAction != nil) {
+                var range = NSRange(location: NSNotFound, length: 0)
+                var rect = CGRect.null
+                let point: CGPoint = _convertPoint(toLayout: touchBeganPoint)
+                let textRange: TextRange? = innerLayout?.textRange(at: point)
+                var textRect: CGRect = innerLayout!.rect(for: textRange)
+                textRect = _convertRect(fromLayout: textRect)
+                if textRange != nil {
+                    if let aRange = textRange?.asRange {
+                        range = aRange
+                    }
+                    rect = textRect
+                }
+                textTapAction?(self, innerText, range, rect)
+            }
+            
+            if (highlight != nil) {
+                if !state.touchMoved || _getHighlight(at: point, range: nil) == highlight {
+                    if let tapAction = highlight?.tapAction != nil ? highlight!.tapAction : highlightTapAction {
+                        let start = TextPosition.position(with: highlightRange.location)
+                        let end = TextPosition.position(with: highlightRange.location + highlightRange.length, affinity: .backward)
+                        let range = TextRange.range(with: start, end: end)
+                        var rect: CGRect = innerLayout!.rect(for: range)
+                        rect = _convertRect(fromLayout: rect)
+                        tapAction(self, innerText, highlightRange, rect)
+                    }
+                }
+                _removeHighlight(animated: fadeOnHighlight)
+            }
+        }
+        
+        if !state.swallowTouch {
+            super.touchesEnded(touches, with: event)
+        }
+    }
+    
+    open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
+        _endTouch()
+        if !state.swallowTouch {
+            super.touchesCancelled(touches, with: event)
+        }
+    }
+    
+    // MARK: - AutoLayout
+    
+    override open var intrinsicContentSize: CGSize {
+        if preferredMaxLayoutWidth == 0 {
+            let container = innerContainer.copy() as! TextContainer
+            container.size = TextContainer.textContainerMaxSize
+            
+            let layout = TextLayout(container: container, text: innerText)
+            return layout!.textBoundingSize
+        }
+        
+        var containerSize: CGSize = innerContainer.size
+        if !verticalForm {
+            containerSize.height = TextContainer.textContainerMaxSize.height
+            containerSize.width = preferredMaxLayoutWidth
+            if containerSize.width == 0 {
+                containerSize.width = bounds.size.width
+            }
+        } else {
+            containerSize.width = TextContainer.textContainerMaxSize.width
+            containerSize.height = preferredMaxLayoutWidth
+            if containerSize.height == 0 {
+                containerSize.height = bounds.size.height
+            }
+        }
+        
+        let container = innerContainer.copy() as! TextContainer
+        container.size = containerSize
+        
+        let layout = TextLayout(container: container, text: innerText)
+        return layout!.textBoundingSize
+    }
+    
+    // MARK: - TextAsyncLayerDelegate
+    
+    public func newAsyncDisplayTask() -> TextAsyncLayerDisplayTask? {
+        
+        // capture current context
+        let contentsNeedFade_ = state.contentsNeedFade
+        var text_: NSAttributedString = innerText
+        var container_: TextContainer? = innerContainer
+        let verticalAlignment_: TextVerticalAlignment = textVerticalAlignment
+        let debug_: TextDebugOption? = debugOption
+        
+        let layoutNeedUpdate_ = state.layoutNeedUpdate
+        let fadeForAsync_: Bool = displaysAsynchronously && fadeOnAsynchronouslyDisplay
+        var layout_: TextLayout? = (state.showingHighlight && (highlightLayout != nil)) ? _highlightLayout() : _innerLayout()
+        var shrinkLayout_: TextLayout? = nil
+        var layoutUpdated_ = false
+        if layoutNeedUpdate_ {
+            text_ = text_.copy() as! NSAttributedString
+            container_ = container_?.copy() as! TextContainer?
+        }
+        
+//        weak var weakSelf = self
+        
+        // create display task
+        let task = TextAsyncLayerDisplayTask()
+        
+        task.willDisplay = { layer in
+            layer?.removeAnimation(forKey: "contents")
+            
+            // If the attachment is not in new layout, or we don't know the new layout currently,
+            // the attachment should be removed.
+            for view: UIView in self.attachmentViews {
+                if layoutNeedUpdate_ || !(layout_?.attachmentContentsSet!.contains(view) ?? false) {
+                    if view.superview == self {
+                        view.removeFromSuperview()
+                    }
+                }
+            }
+            for layer: CALayer in self.attachmentLayers {
+                if layoutNeedUpdate_ || !(layout_?.attachmentContentsSet!.contains(layer) ?? false) {
+                    if layer.superlayer == self.layer {
+                        layer.removeFromSuperlayer()
+                    }
+                }
+            }
+            self.attachmentViews.removeAll()
+            self.attachmentLayers.removeAll()
+        }
+        
+        task.display = { context, size, isCancelled in
+            if isCancelled() {
+                return
+            }
+            guard text_.length > 0 else {
+                return
+            }
+            
+            var drawLayout: TextLayout? = layout_
+            if layoutNeedUpdate_ {
+                layout_ = TextLayout(container: container_, text: text_)
+                shrinkLayout_ = BSLabel._shrinkLayout(with: layout_)
+                if isCancelled() {
+                    return
+                }
+                layoutUpdated_ = true
+                drawLayout = (shrinkLayout_ != nil) ? shrinkLayout_ : layout_
+            }
+            
+            let boundingSize: CGSize = drawLayout?.textBoundingSize ?? .zero
+            var point = CGPoint.zero
+            if verticalAlignment_ == TextVerticalAlignment.center {
+                if let v = drawLayout?.container.isVerticalForm, v {
+                    point.x = -(size.width - boundingSize.width) * 0.5
+                } else {
+                    point.y = (size.height - boundingSize.height) * 0.5
+                }
+            } else if verticalAlignment_ == TextVerticalAlignment.bottom {
+                if let v = drawLayout?.container.isVerticalForm, v {
+                    point.x = -(size.width - boundingSize.width)
+                } else {
+                    point.y = size.height - boundingSize.height
+                }
+            }
+            point = TextUtilities.TextCGPoint(pixelRound: point)
+            drawLayout?.draw(in: context, size: size, point: point, view: nil, layer: nil, debug: debug_, cancel: isCancelled)
+        }
+        
+        task.didDisplay = { layer, finished in
+            var drawLayout = layout_
+            if layoutUpdated_ && (shrinkLayout_ != nil) {
+                drawLayout = shrinkLayout_
+            }
+            if !finished {
+                // If the display task is cancelled, we should clear the attachments.
+                for a: TextAttachment in drawLayout?.attachments ?? [] {
+                    if (a.content is UIView) {
+                        if (a.content as? UIView)?.superview === layer.delegate {
+                            (a.content as? UIView)?.removeFromSuperview()
+                        }
+                    } else if (a.content is CALayer) {
+                        if (a.content as? CALayer)?.superlayer == layer {
+                            (a.content as? CALayer)?.removeFromSuperlayer()
+                        }
+                    }
+                }
+                return
+            }
+            layer.removeAnimation(forKey: "contents")
+            
+            guard let view = layer.delegate as? BSLabel else {
+                return
+            }
+            if view.state.layoutNeedUpdate && layoutUpdated_ {
+                view.innerLayout = layout_
+                view.shrinkInnerLayout = shrinkLayout_
+                view.state.layoutNeedUpdate = false
+            }
+            
+            let size = layer.bounds.size
+            let boundingSize: CGSize = drawLayout?.textBoundingSize ?? .zero
+            var point = CGPoint.zero
+            if verticalAlignment_ == TextVerticalAlignment.center {
+                if let v = drawLayout?.container.isVerticalForm, v {
+                    point.x = -(size.width - boundingSize.width) * 0.5
+                } else {
+                    point.y = (size.height - boundingSize.height) * 0.5
+                }
+            } else if verticalAlignment_ == TextVerticalAlignment.bottom {
+                if let v = drawLayout?.container.isVerticalForm, v {
+                    point.x = -(size.width - boundingSize.width)
+                } else {
+                    point.y = size.height - boundingSize.height
+                }
+            }
+            point = TextUtilities.TextCGPoint(pixelRound: point)
+            drawLayout?.draw(in: nil, size: size, point: point, view: view, layer: layer, debug: nil, cancel: nil)
+            for a in drawLayout?.attachments ?? [] {
+                if (a.content is UIView) {
+                    self.attachmentViews.append(a.content as! UIView)
+                } else if (a.content is CALayer) {
+                    self.attachmentLayers.append(a.content as! CALayer)
+                }
+            }
+            
+            if contentsNeedFade_ {
+                let transition = CATransition()
+                transition.duration = kHighlightFadeDuration
+                transition.timingFunction = CAMediaTimingFunction(name: .easeOut)
+                transition.type = .fade
+                layer.add(transition, forKey: "contents")
+            } else if fadeForAsync_ {
+                let transition = CATransition()
+                transition.duration = kAsyncFadeDuration
+                transition.timingFunction = CAMediaTimingFunction(name: .easeOut)
+                transition.type = .fade
+                layer.add(transition, forKey: "contents")
+            }
+        }
+        
+        return task
+    }
+}

+ 29 - 0
Pods/BSText/BSText/BSText.h

@@ -0,0 +1,29 @@
+//
+//  BSText.h
+//  BSText
+//
+//  Created by BlueSky on 2018/10/22.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+//! Project version number for BSText.
+FOUNDATION_EXPORT double BSTextVersionNumber;
+
+//! Project version string for BSText.
+FOUNDATION_EXPORT const unsigned char BSTextVersionString[];
+
+// To Bridge Objective-C
+
+/**
+ The tap/long press action callback defined in BSText.
+ 
+ @param containerView The text container view (such as BSLabel/BSTextView).
+ @param text          The whole text.
+ @param range         The text range in `text` (if no range, the range.location is NSNotFound).
+ @param rect          The text frame in `containerView` (if no data, the rect is CGRectNull).
+ */
+typedef void(^TextAction)(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect);
+
+#import <CoreText/CoreText.h>

Fișier diff suprimat deoarece este prea mare
+ 2668 - 0
Pods/BSText/BSText/BSTextView.swift


+ 196 - 0
Pods/BSText/BSText/Component/TextContainerView.swift

@@ -0,0 +1,196 @@
+//
+//  TextContainerView.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/12/19.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+/**
+ A simple view to diaplay `TextLayout`.
+ 
+ @discussion This view can become first responder. If this view is first responder,
+ all the action (such as UIMenu's action) would forward to the `hostView` property.
+ Typically, you should not use this class directly.
+ 
+ @warning All the methods in this class should be called on main thread.
+ */
+public class TextContainerView: UIView {
+    
+    /// First responder's aciton will forward to this view.
+    @objc public weak var hostView: UIView?
+    
+    private var _debugOption: TextDebugOption?
+    /// Debug option for layout debug. Set this property will let the view redraw it's contents.
+    @objc public var debugOption: TextDebugOption? {
+        set {
+            let needDraw = _debugOption?.needDrawDebug ?? false
+            _debugOption = newValue?.copy() as! TextDebugOption?
+            if _debugOption?.needDrawDebug ?? false != needDraw {
+                setNeedsDisplay()
+            }
+        }
+        get {
+            return _debugOption
+        }
+    }
+    
+    /// Text vertical alignment.
+    @objc public var textVerticalAlignment: TextVerticalAlignment = .top {
+        didSet {
+            if textVerticalAlignment == oldValue {
+                return
+            }
+            setNeedsDisplay()
+        }
+    }
+    
+    /// Text layout. Set this property will let the view redraw it's contents.
+    @objc public var layout: TextLayout? {
+        willSet {
+            if self.layout == newValue {
+                return
+            }
+            attachmentChanged = true
+            setNeedsDisplay()
+        }
+    }
+    
+    /// The contents fade animation duration when the layout's contents changed. Default is 0 (no animation).
+    @objc public var contentsFadeDuration: TimeInterval = 0 {
+        didSet {
+            if contentsFadeDuration == oldValue {
+                return
+            }
+            if contentsFadeDuration <= 0 {
+                layer.removeAnimation(forKey: "contents")
+            }
+        }
+    }
+    
+    private var attachmentChanged = false
+    private lazy var attachmentViews: [UIView] = []
+    private lazy var attachmentLayers: [CALayer] = []
+    
+    /// Convenience method to set `layout` and `contentsFadeDuration`.
+    /// @param layout  Same as `layout` property.
+    /// @param fadeDuration  Same as `contentsFadeDuration` property.
+    @objc(setLayout:withFadeDuration:)
+    public func set(layout: TextLayout?, with fadeDuration: TimeInterval) {
+        contentsFadeDuration = fadeDuration
+        self.layout = layout
+    }
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        backgroundColor = UIColor.clear
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    // MARK: - override function
+    public override func draw(_ rect: CGRect) {
+        
+        // fade content
+        layer.removeAnimation(forKey: "contents")
+        if contentsFadeDuration > 0 {
+            let transition = CATransition()
+            transition.duration = contentsFadeDuration
+            transition.timingFunction = CAMediaTimingFunction(name: .easeOut)
+            transition.type = .fade
+            layer.add(transition, forKey: "contents")
+        }
+
+        // update attachment
+        if attachmentChanged {
+            for view in attachmentViews {
+                if view.superview == self {
+                    view.removeFromSuperview()
+                }
+            }
+            for layer in attachmentLayers {
+                if layer.superlayer == layer {
+                    layer.removeFromSuperlayer()
+                }
+            }
+            attachmentViews.removeAll()
+            attachmentLayers.removeAll()
+        }
+
+        // draw layout
+        let boundingSize: CGSize = layout?.textBoundingSize ?? CGSize.zero
+        var point = CGPoint.zero
+        if textVerticalAlignment == TextVerticalAlignment.center {
+            if layout?.container.isVerticalForm ?? false {
+                point.x = -(bounds.size.width - boundingSize.width) * 0.5
+            } else {
+                point.y = (bounds.size.height - boundingSize.height) * 0.5
+            }
+        } else if textVerticalAlignment == TextVerticalAlignment.bottom {
+            if layout?.container.isVerticalForm ?? false {
+                point.x = -(bounds.size.width - boundingSize.width)
+            } else {
+                point.y = bounds.size.height - boundingSize.height
+            }
+        }
+        layout?.draw(in: UIGraphicsGetCurrentContext(), size: bounds.size, point: point, view: self, layer: layer, debug: _debugOption, cancel: nil)
+        
+        // update attachment
+        if attachmentChanged {
+            attachmentChanged = false
+            for a: TextAttachment in layout?.attachments ?? [] {
+                if let aContent = a.content as? UIView {
+                    attachmentViews.append(aContent)
+                }
+                if let aContent = a.content as? CALayer {
+                    attachmentLayers.append(aContent)
+                }
+            }
+        }
+    }
+    
+    override public var frame: CGRect {
+        set {
+            let oldSize: CGSize = bounds.size
+            super.frame = newValue
+            if !oldSize.equalTo(bounds.size) {
+                setNeedsLayout()
+            }
+        }
+        get {
+            return super.frame
+        }
+    }
+    
+    override public var bounds: CGRect {
+        set {
+            let oldSize: CGSize = self.bounds.size
+            super.bounds = newValue
+            if !oldSize.equalTo(self.bounds.size) {
+                setNeedsLayout()
+            }
+        }
+        get {
+            return super.bounds
+        }
+    }
+
+    // MARK: - UIResponder forward
+    
+    override public var canBecomeFirstResponder: Bool {
+        return true
+    }
+
+    override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
+        return hostView?.canPerformAction(action, withSender: sender) ?? false
+    }
+
+    override public func forwardingTarget(for aSelector: Selector!) -> Any? {
+        return hostView
+    }
+}

+ 225 - 0
Pods/BSText/BSText/Component/TextDebugOption.swift

@@ -0,0 +1,225 @@
+//
+//  TextDebugOption.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/12/10.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+/**
+ The TextDebugTarget protocol defines the method a debug target should implement.
+ A debug target can be add to the global container to receive the shared debug
+ option changed notification.
+ */
+@objc public protocol TextDebugTarget: NSObjectProtocol {
+    /**
+     When the shared debug option changed, this method would be called on main thread.
+     It should return as quickly as possible. The option's property should not be changed
+     in this method.
+     
+     Setter: The shared debug option.
+     */
+    var debugOption: TextDebugOption? { get set }
+}
+
+
+fileprivate var sharedDebugLock = DispatchSemaphore(value: 1)
+
+/// A List Of TextDebugOption (Unsafe Unretain)
+fileprivate var sharedDebugTargets = NSPointerArray()
+
+@objc public class TextDebugOption: NSObject, NSCopying {
+    
+    private static let _shared = TextDebugOption()
+    private static var sharedOption: TextDebugOption?
+    
+    @objc public static var shared: TextDebugOption {
+        get {
+            if let s = sharedOption {
+                return s
+            }
+            return _shared
+        }
+        set {
+            sharedOption = newValue
+        }
+    }
+    
+    /*/< baseline color */
+    @objc public var baselineColor: UIColor?
+    /*/< CTFrame path border color */
+    @objc public var ctFrameBorderColor: UIColor?
+    /*/< CTFrame path fill color */
+    @objc public var ctFrameFillColor: UIColor?
+    /*/< CTLine bounds border color */
+    @objc public var ctLineBorderColor: UIColor?
+    /*/< CTLine bounds fill color */
+    @objc public var ctLineFillColor: UIColor?
+    /*/< CTLine line number color */
+    @objc public var ctLineNumberColor: UIColor?
+    /*/< CTRun bounds border color */
+    @objc public var ctRunBorderColor: UIColor?
+    /*/< CTRun bounds fill color */
+    @objc public var ctRunFillColor: UIColor?
+    /*/< CTRun number color */
+    @objc public var ctRunNumberColor: UIColor?
+    /*/< CGGlyph bounds border color */
+    @objc public var cgGlyphBorderColor: UIColor?
+    ///< CGGlyph bounds fill color
+    @objc public var cgGlyphFillColor: UIColor?
+    
+    public override init() {
+        super.init()
+    }
+    
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let op = TextDebugOption()
+        op.baselineColor = baselineColor
+        op.ctFrameBorderColor = ctFrameBorderColor
+        op.ctFrameFillColor = ctFrameFillColor
+        op.ctLineBorderColor = ctLineBorderColor
+        op.ctLineFillColor = ctLineFillColor
+        op.ctLineNumberColor = ctLineNumberColor
+        op.ctRunBorderColor = ctRunBorderColor
+        op.ctRunFillColor = ctRunFillColor
+        op.ctRunNumberColor = ctRunNumberColor
+        op.cgGlyphBorderColor = cgGlyphBorderColor
+        op.cgGlyphFillColor = cgGlyphFillColor
+        return op
+    }
+    
+    ///< `YES`: at least one debug color is visible. `NO`: all debug color is invisible/nil.
+    @objc public var needDrawDebug: Bool {
+        
+        if ((self.baselineColor != nil) ||
+            (self.ctFrameBorderColor != nil) ||
+            (self.ctFrameFillColor != nil) ||
+            (self.ctLineBorderColor != nil) ||
+            (self.ctLineFillColor != nil) ||
+            (self.ctLineNumberColor != nil) ||
+            (self.ctRunBorderColor != nil) ||
+            (self.ctRunFillColor != nil) ||
+            (self.ctRunNumberColor != nil) ||
+            (self.cgGlyphBorderColor != nil) ||
+            (self.cgGlyphFillColor != nil)) {
+            
+            return true
+        }
+        
+        return false
+    }
+
+    ///< Set all debug color to nil.
+    @objc public func clear() {
+        self.baselineColor = nil;
+        self.ctFrameBorderColor = nil;
+        self.ctFrameFillColor = nil;
+        self.ctLineBorderColor = nil;
+        self.ctLineFillColor = nil;
+        self.ctLineNumberColor = nil;
+        self.ctRunBorderColor = nil;
+        self.ctRunFillColor = nil;
+        self.ctRunNumberColor = nil;
+        self.cgGlyphBorderColor = nil;
+        self.cgGlyphFillColor = nil;
+    }
+    
+    /**
+     Add a debug target.
+     
+     @discussion When `setSharedDebugOption:` is called, all added debug target will
+     receive `setDebugOption:` in main thread. It maintains an unsafe_unretained
+     reference to this target. The target must to removed before dealloc.
+     
+     @param target A debug target.
+     */
+    @objc(addDebugTarget:)
+    public class func add(_ target: TextDebugTarget?) {
+        
+        sharedDebugLock.wait()
+        sharedDebugTargets.addObject(target)
+        sharedDebugLock.signal()
+    }
+    
+    /**
+     Remove a debug target which is added by `addDebugTarget:`.
+     
+     @param target A debug target.
+     */
+    @objc(removeDebugTarget:)
+    public class func remove(_ target: TextDebugTarget?) {
+        
+        sharedDebugLock.wait()
+        sharedDebugTargets.addObject(target)
+        sharedDebugLock.signal()
+    }
+    
+    /**
+     Returns the shared debug option.
+     
+     @return The shared debug option, default is nil.
+     */
+    @objc public class func sharedDebugOption() -> TextDebugOption? {
+        
+        sharedDebugLock.wait()
+        let op = TextDebugOption.shared
+        sharedDebugLock.signal()
+        
+        return op
+    }
+    
+    /**
+     Set a debug option as shared debug option.
+     This method must be called on main thread.
+     
+     @discussion When call this method, the new option will set to all debug target
+     which is added by `addDebugTarget:`.
+     
+     @param option  A new debug option (nil is valid).
+     */
+    @objc public class func setSharedDebugOption(_ option: TextDebugOption) {
+        assert(Thread.isMainThread, "This method must be called on the main thread")
+        
+        sharedDebugLock.wait()
+        TextDebugOption.shared = option
+        for target in sharedDebugTargets.allObjects {
+            (target as? TextDebugTarget)?.debugOption = TextDebugOption.shared
+        }
+        sharedDebugLock.signal()
+    }
+}
+
+extension NSPointerArray {
+    
+    func addObject(_ object: AnyObject?) {
+        guard let strongObject = object else { return }
+        let pointer = Unmanaged.passUnretained(strongObject).toOpaque()
+        addPointer(pointer)
+    }
+    
+    func insertObject(_ object: AnyObject?, at index: Int) {
+        guard index < count, let strongObject = object else { return }
+        let pointer = Unmanaged.passUnretained(strongObject).toOpaque()
+        insertPointer(pointer, at: index)
+    }
+    
+    func replaceObject(at index: Int, withObject object: AnyObject?) {
+        guard index < count, let strongObject = object else { return }
+        let pointer = Unmanaged.passUnretained(strongObject).toOpaque()
+        replacePointer(at: index, withPointer: pointer)
+    }
+    
+    func object(at index: Int) -> AnyObject? {
+        guard index < count, let pointer = self.pointer(at: index) else { return nil }
+        return Unmanaged<AnyObject>.fromOpaque(pointer).takeUnretainedValue()
+    }
+    
+    func removeObject(at index: Int) {
+        guard index < count else { return }
+        removePointer(at: index)
+    }
+    
+//    如果想清理这个数组,把其中的对象都置为 nil ,你可以调用 compact() 方法:
+}

+ 529 - 0
Pods/BSText/BSText/Component/TextEffectWindow.swift

@@ -0,0 +1,529 @@
+//
+//  TextEffectWindow.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/12/4.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+/**
+ A window to display magnifier and extra contents for text view.
+ 
+ @discussion Use `sharedWindow` to get the instance, don't create your own instance.
+ Typically, you should not use this class directly.
+ */
+@objc public class TextEffectWindow: UIWindow {
+    
+    static var sharedWindowOne: TextEffectWindow? = nil
+    
+    /// Returns the shared instance (returns nil in App Extension).
+    @objc(sharedWindow)
+    public class var shared: TextEffectWindow? {
+    
+        if sharedWindowOne == nil {
+            // iOS 9 compatible
+            let mode = RunLoop.current.currentMode
+            if mode?.rawValue.count == 27 && mode?.rawValue.hasPrefix("UI") ?? false && mode?.rawValue.hasSuffix("InitializationRunLoopMode") ?? false {
+                return nil
+            }
+        }
+        
+        if let _ = sharedWindowOne {
+            return sharedWindowOne
+        }
+        
+        if !TextUtilities.isAppExtension {
+            let one = self.init()
+            one.rootViewController = UIViewController()
+            var rect = CGRect.zero
+            rect.size = TextUtilities.textScreenSize
+            one.frame = rect
+            one.isUserInteractionEnabled = false
+            one.windowLevel = UIWindow.Level(UIWindow.Level.statusBar.rawValue + 1)
+            one.isHidden = false
+            // for iOS9:
+            one.isOpaque = false
+            one.backgroundColor = UIColor.clear
+            one.layer.backgroundColor = UIColor.clear.cgColor
+            
+            sharedWindowOne = one
+        }
+        
+        return sharedWindowOne
+    }
+    
+    /// Show the magnifier in this window with a 'popup' animation. @param magnifier A magnifier.
+    @objc(showMagnifier:)
+    public func show(_ magnifier: TextMagnifier?) {
+        guard let mag = magnifier else {
+            return
+        }
+        if mag.superview != self {
+            addSubview(mag)
+        }
+        _updateWindowLevel()
+        let rotation = _update(magnifier: mag)
+        let center: CGPoint = bs_convertPoint(mag.hostPopoverCenter, fromViewOrWindow: mag.hostView)
+        var trans = CGAffineTransform(rotationAngle: rotation)
+        trans = trans.scaledBy(x: 0.3, y: 0.3)
+        mag.transform = trans
+        mag.center = center
+        if mag.type == TextMagnifierType.ranged {
+            mag.alpha = 0
+        }
+        let time: TimeInterval = mag.type == TextMagnifierType.caret ? 0.08 : 0.1
+        UIView.animate(withDuration: time, delay: 0, options: [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState], animations: {
+            if mag.type == TextMagnifierType.caret {
+                var newCenter = CGPoint(x: 0, y: -mag.fitSize.height / 2)
+                newCenter = newCenter.applying(CGAffineTransform(rotationAngle: rotation))
+                newCenter.x += center.x
+                newCenter.y += center.y
+                mag.center = self._corrected(center: newCenter, for: mag, rotation: rotation)
+            } else {
+                mag.center = self._corrected(center: center, for: mag, rotation: rotation)
+            }
+            mag.transform = CGAffineTransform(rotationAngle: rotation)
+            mag.alpha = 1
+        }) { finished in
+        }
+    }
+    
+    /// Update the magnifier content and position. @param magnifier A magnifier.
+    @objc(moveMagnifier:)
+    public func move(_ magnifier: TextMagnifier?) {
+        guard let mag = magnifier else {
+            return
+        }
+        _updateWindowLevel()
+        let rotation = _update(magnifier: mag)
+        let center: CGPoint = bs_convertPoint(mag.hostPopoverCenter, fromViewOrWindow: mag.hostView)
+        if mag.type == TextMagnifierType.caret {
+            var newCenter = CGPoint(x: 0, y: -mag.fitSize.height / 2)
+            newCenter = newCenter.applying(CGAffineTransform(rotationAngle: rotation))
+            newCenter.x += center.x
+            newCenter.y += center.y
+            mag.center = _corrected(center: newCenter, for: mag, rotation: rotation)
+        } else {
+            mag.center = _corrected(center: center, for: mag, rotation: rotation)
+        }
+        mag.transform = CGAffineTransform(rotationAngle: rotation)
+    }
+    
+    /// Remove the magnifier from this window with a 'shrink' animation. @param magnifier A magnifier.
+    @objc(hideMagnifier:)
+    public func hide(_ magnifier: TextMagnifier?) {
+        guard let mag = magnifier else {
+            return
+        }
+        if mag.superview != self {
+            return
+        }
+        let rotation = _update(magnifier: mag)
+        let center: CGPoint = bs_convertPoint(mag.hostPopoverCenter, fromViewOrWindow: mag.hostView)
+        let time: TimeInterval = mag.type == TextMagnifierType.caret ? 0.20 : 0.15
+        UIView.animate(withDuration: time, delay: 0, options: [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState], animations: {
+            var trans = CGAffineTransform(rotationAngle: rotation)
+            trans = trans.scaledBy(x: 0.01, y: 0.01)
+            mag.transform = trans
+            if mag.type == TextMagnifierType.caret {
+                var newCenter = CGPoint(x: 0, y: -mag.fitSize.height / 2)
+                newCenter = newCenter.applying(CGAffineTransform(rotationAngle: rotation))
+                newCenter.x += center.x
+                newCenter.y += center.y
+                mag.center = self._corrected(center: newCenter, for: mag, rotation: rotation)
+            } else {
+                mag.center = self._corrected(center: center, for: mag, rotation: rotation)
+                mag.alpha = 0
+            }
+        }) { finished in
+            if finished {
+                mag.removeFromSuperview()
+                mag.transform = CGAffineTransform.identity
+                mag.alpha = 1
+            }
+        }
+    }
+    
+    /// Show the selection dot in this window if the dot is clipped by the selection view.
+    /// @param selectionDot A selection view.
+    @objc(showSelectionDot:)
+    public func show(selectionDot: TextSelectionView?) {
+        guard let selection = selectionDot else {
+            return
+        }
+        _updateWindowLevel()
+        let aMirror = selection.startGrabber.dot.mirror
+        insertSubview(aMirror, at: 0)
+        
+        let eMirror = selection.endGrabber.dot.mirror
+        insertSubview(eMirror, at: 0)
+        
+        _update(dot: selection.startGrabber.dot, selection: selection)
+        _update(dot: selection.endGrabber.dot, selection: selection)
+    }
+    
+    /// Remove the selection dot from this window.
+    /// @param selectionDot A selection view.
+    @objc(hideSelectionDot:)
+    public func hide(selectionDot: TextSelectionView?) {
+        guard let selection = selectionDot else {
+            return
+        }
+        selection.startGrabber.dot.mirror.removeFromSuperview()
+        selection.endGrabber.dot.mirror.removeFromSuperview()
+    }
+    
+    // stop self from becoming the KeyWindow
+    override public func becomeKey() {
+        TextUtilities.sharedApplication?.delegate?.window??.makeKey()
+    }
+    
+    override public var rootViewController: UIViewController? {
+        set {
+            super.rootViewController = newValue
+        }
+        get {
+            guard let ws = TextUtilities.sharedApplication?.windows else {
+                return nil
+            }
+            if #available(iOS 13, *) {
+                return super.rootViewController
+            }
+            for window in ws {
+                if self == window {
+                    continue
+                }
+                if window.isHidden {
+                    continue
+                }
+                
+                if let topViewController = window.rootViewController {
+                    return topViewController
+                }
+            }
+            
+            return super.rootViewController
+        }
+    }
+    
+    // Bring self to front
+    func _updateWindowLevel() {
+        
+        guard let app = TextUtilities.sharedApplication else {
+            return
+        }
+        var top = app.windows.last
+        let key = app.keyWindow
+        if let aLevel = key?.windowLevel, let aLevel1 = top?.windowLevel {
+            if key != nil && aLevel > aLevel1 {
+                top = key
+            }
+        }
+        if top == self {
+            return
+        }
+        windowLevel = UIWindow.Level((top?.windowLevel.rawValue ?? 0) + 1)
+    }
+    
+    func _keyboardDirection() -> TextDirection {
+        var keyboardFrame: CGRect = TextKeyboardManager.default.keyboardFrame
+        keyboardFrame = TextKeyboardManager.default.convert(keyboardFrame, to: self)
+        if keyboardFrame.isNull || keyboardFrame.isEmpty {
+            return TextDirection.none
+        }
+        if keyboardFrame.minY == 0 && keyboardFrame.minX == 0 && keyboardFrame.maxX == frame.width {
+            return TextDirection.top
+        }
+        if keyboardFrame.maxX == frame.width && keyboardFrame.minY == 0 && keyboardFrame.maxY == frame.height {
+            return TextDirection.right
+        }
+        if keyboardFrame.maxY == frame.height && keyboardFrame.minX == 0 && keyboardFrame.maxX == frame.width {
+            return TextDirection.bottom
+        }
+        if keyboardFrame.minX == 0 && keyboardFrame.minY == 0 && keyboardFrame.maxY == frame.height {
+            return TextDirection.left
+        }
+        return TextDirection.none
+    }
+    
+    func _corrected(captureCenter center: CGPoint) -> CGPoint {
+        var center = center
+        var keyboardFrame: CGRect = TextKeyboardManager.default.keyboardFrame
+        keyboardFrame = TextKeyboardManager.default.convert(keyboardFrame, to: self)
+        if !keyboardFrame.isNull && !keyboardFrame.isEmpty {
+            let direction: TextDirection = _keyboardDirection()
+            switch direction {
+            case TextDirection.top:
+                if center.y < keyboardFrame.maxY {
+                    center.y = keyboardFrame.maxY
+                }
+            case TextDirection.right:
+                if center.x > keyboardFrame.minX {
+                    center.x = keyboardFrame.minX
+                }
+            case TextDirection.bottom:
+                if center.y > keyboardFrame.minY {
+                    center.y = keyboardFrame.minY
+                }
+            case TextDirection.left:
+                if center.x < keyboardFrame.maxX {
+                    center.x = keyboardFrame.maxX
+                }
+            default:
+                break
+            }
+        }
+        return center
+    }
+    
+    func _corrected(center: CGPoint, for mag: TextMagnifier, rotation: CGFloat) -> CGPoint {
+        var center = center
+        var degree = TextUtilities.textDegrees(from: rotation)
+        degree /= 45.0
+        if degree < 0 {
+            degree += CGFloat(Int(-degree / 8.0 + 1) * 8)
+        }
+        if degree > 8 {
+            degree -= CGFloat(Int(degree / 8.0) * 8)
+        }
+        let caretExt: CGFloat = 10
+        if degree <= 1 || degree >= 7 {
+            //top
+            if mag.type == TextMagnifierType.caret {
+                if center.y < caretExt {
+                    center.y = caretExt
+                }
+            } else if mag.type == TextMagnifierType.ranged {
+                if center.y < mag.bounds.size.height {
+                    center.y = mag.bounds.size.height
+                }
+            }
+        } else if 1 < degree && degree < 3 {
+            // right
+            if mag.type == TextMagnifierType.caret {
+                if center.x > bounds.size.width - caretExt {
+                    center.x = bounds.size.width - caretExt
+                }
+            } else if mag.type == TextMagnifierType.ranged {
+                if center.x > bounds.size.width - mag.bounds.size.height {
+                    center.x = bounds.size.width - mag.bounds.size.height
+                }
+            }
+        } else if 3 <= degree && degree <= 5 {
+            // bottom
+            if mag.type == TextMagnifierType.caret {
+                if center.y > bounds.size.height - caretExt {
+                    center.y = bounds.size.height - caretExt
+                }
+            } else if mag.type == TextMagnifierType.ranged {
+                if center.y > mag.bounds.size.height {
+                    center.y = mag.bounds.size.height
+                }
+            }
+        } else if 5 < degree && degree < 7 {
+            // left
+            if mag.type == TextMagnifierType.caret {
+                if center.x < caretExt {
+                    center.x = caretExt
+                }
+            } else if mag.type == TextMagnifierType.ranged {
+                if center.x < mag.bounds.size.height {
+                    center.x = mag.bounds.size.height
+                }
+            }
+        }
+        
+        var keyboardFrame: CGRect = TextKeyboardManager.default.keyboardFrame
+        keyboardFrame = TextKeyboardManager.default.convert(keyboardFrame, to: self)
+        if !keyboardFrame.isNull && !keyboardFrame.isEmpty {
+            let direction: TextDirection = _keyboardDirection()
+            switch direction {
+            case TextDirection.top:
+                if mag.type == TextMagnifierType.caret {
+                    if center.y - mag.bounds.size.height / 2 < keyboardFrame.maxY {
+                        center.y = keyboardFrame.maxY + mag.bounds.size.height / 2
+                    }
+                } else if mag.type == TextMagnifierType.ranged {
+                    if center.y < keyboardFrame.maxY {
+                        center.y = keyboardFrame.maxY
+                    }
+                }
+            case TextDirection.right:
+                if mag.type == TextMagnifierType.caret {
+                    if center.x + mag.bounds.size.height / 2 > keyboardFrame.minX {
+                        center.x = keyboardFrame.minX - mag.bounds.size.width / 2
+                    }
+                } else if mag.type == TextMagnifierType.ranged {
+                    if center.x > keyboardFrame.minX {
+                        center.x = keyboardFrame.minX
+                    }
+                }
+            case TextDirection.bottom:
+                if mag.type == TextMagnifierType.caret {
+                    if center.y + mag.bounds.size.height / 2 > keyboardFrame.minY {
+                        center.y = keyboardFrame.minY - mag.bounds.size.height / 2
+                    }
+                } else if mag.type == TextMagnifierType.ranged {
+                    if center.y > keyboardFrame.minY {
+                        center.y = keyboardFrame.minY
+                    }
+                }
+            case TextDirection.left:
+                if mag.type == TextMagnifierType.caret {
+                    if center.x - mag.bounds.size.height / 2 < keyboardFrame.maxX {
+                        center.x = keyboardFrame.maxX + mag.bounds.size.width / 2
+                    }
+                } else if mag.type == TextMagnifierType.ranged {
+                    if center.x < keyboardFrame.maxX {
+                        center.x = keyboardFrame.maxX
+                    }
+                }
+            default:
+                break
+            }
+        }
+        
+        return center;
+    }
+    
+    private static var placeholderRect = CGRect.zero
+    private static var placeholder: UIImage = {
+        
+        placeholderRect.origin = CGPoint.zero
+        UIGraphicsBeginImageContextWithOptions(placeholderRect.size, false, 0)
+        let context = UIGraphicsGetCurrentContext()
+        UIColor(white: 1, alpha: 0.8).set()
+        context?.fill(placeholderRect)
+        let img = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
+        UIGraphicsEndImageContext()
+        
+        return img
+    }()
+    
+    /**
+     Capture screen snapshot and set it to magnifier.
+     @return Magnifier rotation radius.
+     */
+    private func _update(magnifier mag: TextMagnifier) -> CGFloat {
+        
+        guard let app = TextUtilities.sharedApplication else {
+            return 0
+        }
+        let hostView: UIView? = mag.hostView
+        let hostWindow = (hostView is UIWindow) ? (hostView as? UIWindow) : hostView?.window
+        if hostView == nil || hostWindow == nil {
+            return 0
+        }
+        var captureCenter: CGPoint = bs_convertPoint(mag.hostCaptureCenter, fromViewOrWindow: hostView)
+        captureCenter = _corrected(captureCenter: captureCenter)
+        var captureRect = CGRect()
+        captureRect.size = mag.snapshotSize
+        captureRect.origin.x = captureCenter.x - captureRect.size.width / 2
+        captureRect.origin.y = captureCenter.y - captureRect.size.height / 2
+        let trans: CGAffineTransform = TextUtilities.textCGAffineTransformGet(from: hostView, to: self)
+        let rotation: CGFloat = TextUtilities.textCGAffineTransformGetRotation((trans))
+        if mag.captureDisabled {
+            if mag.snapshot == nil || mag.snapshot!.size.width > 1 {
+                
+                TextEffectWindow.placeholderRect = mag.bounds
+                
+                mag.captureFadeAnimation = true
+                mag.snapshot = TextEffectWindow.placeholder
+                mag.captureFadeAnimation = false
+            }
+            return rotation
+        }
+
+        UIGraphicsBeginImageContextWithOptions(captureRect.size, _: false, _: 0)
+        let context = UIGraphicsGetCurrentContext()
+        if context == nil {
+            return rotation
+        }
+        var tp = CGPoint(x: captureRect.size.width / 2, y: captureRect.size.height / 2)
+        tp = tp.applying(CGAffineTransform(rotationAngle: rotation))
+        context?.rotate(by: -rotation)
+        context?.translateBy(x: tp.x - captureCenter.x, y: tp.y - captureCenter.y)
+        var windows = app.windows
+        let keyWindow = app.keyWindow
+        if let aWindow = keyWindow {
+            if !windows.contains(aWindow) {
+                windows.append(aWindow)
+            }
+        }
+        windows = (windows as NSArray).sortedArray(comparator: { w1, w2 in
+            let aLevel = (w1 as! UIWindow).windowLevel
+            let aLevel1 = (w2 as! UIWindow).windowLevel
+            
+            if aLevel < aLevel1 {
+                return .orderedAscending
+            } else if aLevel > aLevel {
+                return .orderedDescending
+            }
+            
+            return .orderedSame
+        }) as? [UIWindow] ?? windows
+        
+        let mainScreen = UIScreen.main
+        for window in windows {
+            if window.isHidden || window.alpha <= 0.01 {
+                continue
+            }
+            if window.screen != mainScreen {
+                continue
+            }
+            if (window.isKind(of: type(of: self))) {
+                break //don't capture window above self
+            }
+            context?.saveGState()
+            context?.concatenate(TextUtilities.textCGAffineTransformGet(from: window, to: self))
+            if let aContext = context {
+                window.layer.render(in: aContext)
+            } //render
+            //[window drawViewHierarchyInRect:window.bounds afterScreenUpdates:NO]; //slower when capture whole window
+            context?.restoreGState()
+        }
+        let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        
+        if mag.snapshot!.size.width == 1 {
+            mag.captureFadeAnimation = true
+        }
+        mag.snapshot = image
+        mag.captureFadeAnimation = false
+        return rotation
+    }
+    
+    func _update(dot: SelectionGrabberDot, selection: TextSelectionView) {
+        dot.mirror.isHidden = true
+        if selection.hostView?.clipsToBounds == true && dot.bs_visibleAlpha > 0.1 {
+            let dotRect = dot.bs_convertRect(dot.bounds, toViewOrWindow: self)
+            var dotInKeyboard = false
+            var keyboardFrame: CGRect = TextKeyboardManager.default.keyboardFrame
+            keyboardFrame = TextKeyboardManager.default.convert(keyboardFrame, to: self)
+            if !keyboardFrame.isNull && !keyboardFrame.isEmpty {
+                let inter: CGRect = dotRect.intersection(keyboardFrame)
+                if !inter.isNull && (inter.size.width > 1 || inter.size.height > 1) {
+                    dotInKeyboard = true
+                }
+            }
+            if !dotInKeyboard {
+                let hostRect = selection.hostView!.convert(selection.hostView!.bounds, to: self)
+                let intersection: CGRect = dotRect.intersection(hostRect)
+                if TextUtilities.textCGRectGetArea(intersection) < TextUtilities.textCGRectGetArea(dotRect) {
+                    let dist = TextUtilities.textCGPointGetDistance(to: TextUtilities.textCGRectGetCenter(dotRect), r: hostRect)
+                    if dist < dot.frame.width * 0.55 {
+                        dot.mirror.isHidden = false
+                    }
+                }
+            }
+        }
+        let center = dot.bs_convertPoint(CGPoint(x: dot.frame.width / 2, y: dot.frame.height / 2), toViewOrWindow: self)
+        if center.x.isNaN || center.y.isNaN || center.x.isInfinite || center.y.isInfinite {
+            dot.mirror.isHidden = true
+        } else {
+            dot.mirror.center = center
+        }
+    }
+}

+ 287 - 0
Pods/BSText/BSText/Component/TextInput.swift

@@ -0,0 +1,287 @@
+//
+//  TextInput.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/10/31.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+/**
+ Text position affinity. For example, the offset appears after the last
+ character on a line is backward affinity, before the first character on
+ the following line is forward affinity.
+ */
+@objc public enum TextAffinity : Int {
+    ///< offset appears before the character
+    case forward = 0
+    ///< offset appears after the character
+    case backward = 1
+}
+
+/**
+ A TextPosition object represents a position in a text container; in other words,
+ it is an index into the backing string in a text-displaying view.
+ 
+ TextPosition has the same API as Apple's implementation in UITextView/UITextField,
+ so you can alse use it to interact with UITextView/UITextField.
+ */
+public class TextPosition: UITextPosition, NSCopying {
+    
+    @objc public private(set) var offset: Int = 0
+    @objc public private(set) var affinity: TextAffinity = .forward
+    
+    @objc override init() {
+        super.init()
+    }
+    
+    @objc(positionWithOffset:)
+    public class func position(with offset: Int) -> TextPosition {
+        return TextPosition.position(with: offset, affinity: TextAffinity.forward)
+    }
+    
+    public convenience init(offset: Int) {
+        self.init()
+        self.offset = offset
+    }
+    
+    @objc(positionWithOffset:affinity:)
+    public class func position(with offset: Int, affinity: TextAffinity) -> TextPosition {
+        let e = TextPosition()
+        e.offset = offset
+        e.affinity = affinity
+        return e
+    }
+    
+    public convenience init(offset: Int, affinity: TextAffinity) {
+        self.init()
+        self.offset = offset
+        self.affinity = affinity
+    }
+    
+    public func copy(with zone: NSZone? = nil) -> Any {
+        return TextPosition.position(with: self.offset, affinity: self.affinity)
+    }
+    
+    public override var description: String {
+        return "<\(type(of: self)): \(String(format: "%p", self))> (\(offset)\(affinity == TextAffinity.forward ? "F" : "B"))"
+    }
+    
+    public func hash() -> Int {
+        return offset * 2 + (affinity == TextAffinity.forward ? 1 : 0)
+    }
+    
+    public func isEqual(_ object: TextPosition?) -> Bool {
+        guard let o = object else {
+            return false
+        }
+        return offset == o.offset && affinity == o.affinity
+    }
+    
+    @objc public func compare(_ otherPosition: TextPosition?) -> ComparisonResult {
+        if otherPosition == nil {
+            return .orderedAscending
+        }
+        if offset < otherPosition?.offset ?? 0 {
+            return .orderedAscending
+        }
+        if offset > otherPosition?.offset ?? 0 {
+            return .orderedDescending
+        }
+        if affinity == TextAffinity.backward && otherPosition?.affinity == TextAffinity.forward {
+            return .orderedAscending
+        }
+        if affinity == TextAffinity.forward && otherPosition?.affinity == TextAffinity.backward {
+            return .orderedDescending
+        }
+        return .orderedSame
+    }
+}
+
+/**
+ A TextRange object represents a range of characters in a text container; in other words,
+ it identifies a starting index and an ending index in string backing a text-displaying view.
+ 
+ TextRange has the same API as Apple's implementation in UITextView/UITextField,
+ so you can alse use it to interact with UITextView/UITextField.
+ */
+public class TextRange: UITextRange, NSCopying {
+    
+    private var _start = TextPosition(offset: 0)
+    @objc override public var start: TextPosition {
+        set {
+            _start = newValue
+        }
+        get {
+            return _start
+        }
+    }
+    
+    private var _end = TextPosition(offset: 0)
+    override public var end: TextPosition {
+        set {
+            _end = newValue
+        }
+        get {
+            return _end
+        }
+    }
+    
+    override public var isEmpty: Bool {
+        get {
+            return _start.offset == _end.offset
+        }
+    }
+    
+    @objc(rangeWithRange:)
+    public class func range(with range: NSRange) -> TextRange {
+        return TextRange.range(with: range, affinity: .forward)
+    }
+    
+    @objc(rangeWithRange:affinity:)
+    public class func range(with range: NSRange, affinity: TextAffinity) -> TextRange {
+        let start = TextPosition.position(with: range.location, affinity: affinity)
+        let end = TextPosition.position(with: range.location + range.length, affinity: affinity)
+        return TextRange.range(with: start, end: end)
+    }
+    
+    @objc(rangeWithStart:end:)
+    public class func range(with start: TextPosition, end: TextPosition) -> TextRange {
+        
+        let range = TextRange()
+        if start.compare(end) == .orderedDescending {
+            range._start = end
+            range._end = start
+        } else {
+            range._start = start
+            range._end = end
+        }
+        return range
+    }
+    
+    override init() {
+        super.init()
+    }
+    
+    public convenience init(range: NSRange) {
+        self.init(range: range, affinity: .forward)
+    }
+    
+    public convenience init(range: NSRange, affinity: TextAffinity) {
+        let start = TextPosition.position(with: range.location, affinity: affinity)
+        let end = TextPosition.position(with: range.location + range.length, affinity: affinity)
+        self.init(start: start, end: end)
+    }
+    
+    public convenience init(start: TextPosition, end: TextPosition) {
+        self.init()
+        if start.compare(end) == .orderedDescending {
+            self._start = end
+            self._end = start
+        } else {
+            self._start = start
+            self._end = end
+        }
+    }
+    
+    @objc public var asRange: NSRange {
+        return NSRange(location: _start.offset, length: _end.offset - _start.offset)
+    }
+    
+    @objc(defaultRange)
+    public class func `default`() -> TextRange {
+        return TextRange.init()
+    }
+    
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let e = TextRange.range(with: self.start, end: self.end)
+        return e
+    }
+    
+    public override var description: String {
+        return "<\(type(of: self)): \(String(format: "%p", self))> (\(_start.offset), \(end.offset - start.offset))\(end.affinity == TextAffinity.forward ? "F" : "B")"
+    }
+    
+    func hash() -> Int {
+        return (MemoryLayout<Int>.size == 8 ? Int(CFSwapInt64(UInt64(start.hash()))) : Int(CFSwapInt32(UInt32(start.hash()))) + end.hash())
+    }
+    
+    override public func isEqual(_ object: Any?) -> Bool {
+        guard let o = object as! TextRange? else {
+            return false
+        }
+        return start.isEqual(o.start) && end.isEqual(o.end)
+    }
+}
+
+
+/**
+ A TextSelectionRect object encapsulates information about a selected range of
+ text in a text-displaying view.
+ 
+ TextSelectionRect has the same API as Apple's implementation in UITextView/UITextField,
+ so you can alse use it to interact with UITextView/UITextField.
+ */
+public class TextSelectionRect: UITextSelectionRect, NSCopying {
+    
+    private var _rect = CGRect.zero
+    @objc override public var rect: CGRect {
+        set {
+            _rect = newValue
+        }
+        get {
+            return _rect
+        }
+    }
+    
+    private var _writingDirection: UITextWritingDirection = .natural
+    @objc override public var writingDirection: UITextWritingDirection {
+        set {
+            _writingDirection = newValue
+        }
+        get {
+            return _writingDirection
+        }
+    }
+    
+    private var _containsStart = false
+    @objc override public var containsStart: Bool {
+        set {
+            _containsStart = newValue
+        }
+        get {
+            return _containsStart
+        }
+    }
+    
+    private var _containsEnd = false
+    @objc override public var containsEnd: Bool {
+        set {
+            _containsEnd = newValue
+        }
+        get {
+            return _containsEnd
+        }
+    }
+    
+    private var _isVertical = false
+    @objc override public var isVertical: Bool {
+        set {
+            _isVertical = newValue
+        }
+        get {
+            return _isVertical
+        }
+    }
+    
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextSelectionRect()
+        one.rect = self.rect
+        one.writingDirection = self.writingDirection
+        one.containsStart = self.containsStart
+        one.containsEnd = self.containsEnd
+        one.isVertical = self.isVertical
+        return one
+    }
+}

+ 586 - 0
Pods/BSText/BSText/Component/TextKeyboardManager.swift

@@ -0,0 +1,586 @@
+//
+//  TextKeyboardManager.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/11/12.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+fileprivate var _TextKeyboardViewFrameObserverKey: Int = 0
+
+/// Observer for view's frame/bounds/center/transform
+fileprivate class TextKeyboardViewFrameObserver: NSObject {
+    
+    private var keyboardView: UIView?
+    @objc public var notifyBlock: ((_ keyboard: UIView?) -> Void)?
+    
+    @objc(addToKeyboardView:) public func addTo(keyboardView: UIView?) {
+        if self.keyboardView == keyboardView {
+            return
+        }
+        if let _ = self.keyboardView {
+            removeFrameObserver()
+            objc_setAssociatedObject(self.keyboardView!, &_TextKeyboardViewFrameObserverKey, nil, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+        self.keyboardView = keyboardView
+        if keyboardView != nil {
+            addFrameObserver()
+        }
+        objc_setAssociatedObject(keyboardView!, &_TextKeyboardViewFrameObserverKey, self, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+    }
+    
+    func removeFrameObserver() {
+        keyboardView?.removeObserver(self, forKeyPath: "frame")
+        keyboardView?.removeObserver(self, forKeyPath: "center")
+        keyboardView?.removeObserver(self, forKeyPath: "bounds")
+        keyboardView?.removeObserver(self, forKeyPath: "transform")
+        keyboardView = nil
+    }
+    
+    func addFrameObserver() {
+        if keyboardView == nil {
+            return
+        }
+        keyboardView?.addObserver(self, forKeyPath: "frame", options: [], context: nil)
+        keyboardView?.addObserver(self, forKeyPath: "center", options: [], context: nil)
+        keyboardView?.addObserver(self, forKeyPath: "bounds", options: [], context: nil)
+        keyboardView?.addObserver(self, forKeyPath: "transform", options: [], context: nil)
+    }
+    
+    public class func observerForView(_ keyboardView: UIView?) -> TextKeyboardViewFrameObserver? {
+        guard let k = keyboardView else {
+            return nil
+        }
+        return objc_getAssociatedObject(k, &_TextKeyboardViewFrameObserverKey) as? TextKeyboardViewFrameObserver;
+    }
+    
+    deinit {
+        removeFrameObserver()
+    }
+    
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        
+        let isPrior: Bool = ((change?[.notificationIsPriorKey] as? Int) != 0)
+        if isPrior {
+            return
+        }
+        let changeKind = NSKeyValueChange(rawValue: UInt((change?[.kindKey] as? Int) ?? 0))
+        if changeKind != .setting {
+            return
+        }
+        var newVal = change?[.newKey]
+        if (newVal as? NSNull) == NSNull() {
+            newVal = nil
+        }
+        if (notifyBlock != nil) {
+            notifyBlock!(keyboardView)
+        }
+    }
+}
+
+/**
+ The TextKeyboardObserver protocol defines the method you can use
+ to receive system keyboard change information.
+ */
+@objc public protocol TextKeyboardObserver: NSObjectProtocol {
+    @objc optional func keyboardChanged(with transition: TextKeyboardTransition)
+}
+
+// TODO: - here should use struct in pure Swift
+/**
+ System keyboard transition information.
+ Use -[TextKeyboardManager convertRect:toView:] to convert frame to specified view.
+ */
+public class TextKeyboardTransition: NSObject {
+    
+    ///< Keyboard visible before transition.
+    @objc public var fromVisible = false
+    
+    ///< Keyboard visible after transition.
+    @objc public var toVisible = false
+    
+    ///< Keyboard frame before transition.
+    @objc public var fromFrame = CGRect.zero
+    
+    ///< Keyboard frame after transition.
+    @objc public var toFrame = CGRect.zero
+    
+    ///< Keyboard transition animation duration.
+    @objc public var animationDuration: TimeInterval = 0
+    
+    ///< Keyboard transition animation curve.
+    @objc public var animationCurve = UIView.AnimationCurve.easeInOut
+    
+    ///< Keybaord transition animation option.
+    @objc public var animationOption = UIView.AnimationOptions.layoutSubviews
+}
+
+
+/**
+ A TextKeyboardManager object lets you get the system keyboard information,
+ and track the keyboard visible/frame/transition.
+ 
+ @discussion You should access this class in main thread.
+ */
+public class TextKeyboardManager: NSObject {
+    
+    /// Get the keyboard window. nil if there's no keyboard window.
+    @objc public var keyboardWindow: UIWindow? {
+        
+        guard let app = TextUtilities.sharedApplication else {
+            return nil
+        }
+        
+        for window in app.windows {
+            if (_getKeyboardView(from: window) != nil) {
+                return window
+            }
+        }
+        
+        let window: UIWindow? = app.keyWindow
+        if (_getKeyboardView(from: window) != nil) {
+            return window
+        }
+        var kbWindows = [UIWindow]()
+        for window in app.windows {
+            let windowName = NSStringFromClass(type(of: window))
+            if _systemVersion < 9 {
+                // UITextEffectsWindow
+                if windowName.length == 19 && windowName.hasPrefix("UI") && windowName.hasSuffix("TextEffectsWindow") {
+                    
+                    kbWindows.append(window)
+                }
+            } else {
+                // UIRemoteKeyboardWindow
+                if windowName.length == 22 && windowName.hasPrefix("UI") && windowName.hasSuffix("RemoteKeyboardWindow") {
+                    
+                    kbWindows.append(window)
+                }
+            }
+        }
+        if kbWindows.count == 1 {
+            return kbWindows.first
+        }
+        return nil
+    }
+    
+    /// Get the keyboard view. nil if there's no keyboard view.
+    @objc public var keyboardView: UIView? {
+        
+        let app: UIApplication? = TextUtilities.sharedApplication
+        if app == nil {
+            return nil
+        }
+        var window: UIWindow? = nil
+        var view: UIView? = nil
+        for window in app?.windows ?? [] {
+            view = _getKeyboardView(from: window)
+            if view != nil {
+                return view
+            }
+        }
+        window = app?.keyWindow
+        view = _getKeyboardView(from: window)
+        if view != nil {
+            return view
+        }
+        return nil
+    }
+    
+    /// Whether the keyboard is visible.
+    @objc public var keyboardVisible: Bool {
+        
+        guard let window = keyboardWindow else {
+            return false
+        }
+        
+        guard let view = keyboardView else {
+            return false
+        }
+        let rect: CGRect = window.bounds.intersection(view.frame)
+        if rect.isNull {
+            return false
+        }
+        if rect.isInfinite {
+            return false
+        }
+        return rect.size.width > 0 && rect.size.height > 0
+    }
+    
+    /// Get the keyboard frame. CGRectNull if there's no keyboard view.
+    /// Use convertRect:toView: to convert frame to specified view.
+    @objc public var keyboardFrame: CGRect {
+        
+        guard let keyboard = keyboardView else {
+            return CGRect.null
+        }
+        var frame = CGRect.null
+        
+        if let window = keyboard.window {
+            frame = window.convert(keyboard.frame, to: nil)
+        } else {
+            frame = keyboard.frame
+        }
+        return frame
+    }
+    
+    @objc public class func startManager() -> Void {
+        let _ = `default`
+    }
+    
+    /// Get the default manager (returns nil in App Extension).
+    @objc(defaultManager)
+    public static let `default` = TextKeyboardManager()
+    
+    override private init() {
+        observers = NSHashTable(options: [.weakMemory, .objectPointerPersonality], capacity: 0)
+        super.init()
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(self._keyboardFrameWillChange(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
+        // for iPad (iOS 9)
+        if _systemVersion >= 9 {
+            NotificationCenter.default.addObserver(self, selector: #selector(self._keyboardFrameDidChange(_:)), name: UIResponder.keyboardDidChangeFrameNotification, object: nil)
+        }
+    }
+    
+    func _initFrameObserver() {
+        
+        guard let keyboardView = self.keyboardView else {
+            return
+        }
+        weak var _self = self
+        var observer: TextKeyboardViewFrameObserver? = TextKeyboardViewFrameObserver.observerForView(keyboardView)
+        if observer == nil {
+            observer = TextKeyboardViewFrameObserver()
+            observer?.notifyBlock = { keyboard in
+                _self!._keyboardFrameChanged(keyboard)
+            }
+            observer?.addTo(keyboardView: keyboardView)
+        }
+    }
+    
+    /**
+     Add an observer to manager to get keyboard change information.
+     This method makes a weak reference to the observer.
+     
+     @param observer An observer.
+     This method will do nothing if the observer is nil, or already added.
+     */
+    @objc(addObserver:)
+    public func add(observer: TextKeyboardObserver?) {
+        guard let observer = observer else {
+            return
+        }
+        observers.add(observer)
+    }
+    
+    /**
+     Remove an observer from manager.
+     
+     @param observer An observer.
+     This method will do nothing if the observer is nil, or not in manager.
+     */
+    @objc(removeObserver:)
+    public func remove(observer: TextKeyboardObserver?) {
+        guard let observer = observer else {
+            return
+        }
+        observers.remove(observer)
+    }
+    
+    
+    private var observers: NSHashTable<TextKeyboardObserver>
+    private var fromFrame = CGRect.zero
+    private var fromVisible = false
+    private var notificationFromFrame = CGRect.zero
+    private var notificationToFrame = CGRect.zero
+    private var notificationDuration: TimeInterval = 0
+    private var notificationCurve = UIView.AnimationCurve.linear
+    private var hasNotification = false
+    private var observedToFrame = CGRect.zero
+    private var hasObservedChange = false
+    private var lastIsNotification = false
+    
+    // MARK: - private
+    
+    private let _systemVersion = Double(UIDevice.current.systemVersion) ?? 0
+    
+    private func _getKeyboardView(from window: UIWindow?) -> UIView? {
+        /*
+         iOS 8:
+         UITextEffectsWindow
+         UIInputSetContainerView
+         UIInputSetHostView << keyboard
+         
+         iOS 9:
+         UIRemoteKeyboardWindow
+         UIInputSetContainerView
+         UIInputSetHostView << keyboard
+         */
+        guard let window = window else {
+            return nil
+        }
+        // Get the window
+        let windowName = NSStringFromClass(type(of: window))
+        if _systemVersion < 9 {
+            // UITextEffectsWindow
+            if windowName.length != 19 {
+                return nil
+            }
+            if !windowName.hasPrefix("UI") {
+                return nil
+            }
+            if !windowName.hasSuffix("TextEffectsWindow") {
+                return nil
+            }
+        } else {
+            // UIRemoteKeyboardWindow
+            if windowName.length != 22 {
+                return nil
+            }
+            if !windowName.hasPrefix("UI") {
+                return nil
+            }
+            if !windowName.hasSuffix("RemoteKeyboardWindow") {
+                return nil
+            }
+        }
+        
+        // Get the view
+        // UIInputSetContainerView
+        for view: UIView in window.subviews {
+            let viewName = NSStringFromClass(type(of: view))
+            if viewName.length != 23 {
+                continue
+            }
+            if !viewName.hasPrefix("UI") {
+                continue
+            }
+            if !viewName.hasSuffix("InputSetContainerView") {
+                continue
+            }
+            // UIInputSetHostView
+            for subView: UIView in view.subviews {
+                let subViewName = NSStringFromClass(type(of: subView))
+                if subViewName.length != 18 {
+                    continue
+                }
+                if !subViewName.hasPrefix("UI") {
+                    continue
+                }
+                if !subViewName.hasSuffix("InputSetHostView") {
+                    continue
+                }
+                return subView
+            }
+        }
+        return nil
+    }
+    
+    @objc private func _keyboardFrameWillChange(_ notif: Notification?) {
+        guard let notif = notif else {
+            return
+        }
+        guard notif.name == UIResponder.keyboardWillChangeFrameNotification else {
+            return
+        }
+        guard let info = notif.userInfo else {
+            return
+        }
+        _initFrameObserver()
+        let beforeValue = info[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue
+        let afterValue = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
+        let curveNumber = info[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int
+        let durationNumber = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
+        let before: CGRect = beforeValue?.cgRectValue ?? .zero
+        let after: CGRect = afterValue?.cgRectValue ?? .zero
+        let curve: UIView.AnimationCurve = UIView.AnimationCurve(rawValue: curveNumber)!
+        let duration = durationNumber
+        // ignore zero end frame
+        if (after.size.width <= 0) && (after.size.height <= 0) {
+            return
+        }
+        notificationFromFrame = before
+        notificationToFrame = after
+        notificationCurve = curve
+        notificationDuration = duration
+        hasNotification = true
+        lastIsNotification = true
+        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self._notifyAllObservers), object: nil)
+        if duration == 0 {
+            perform(#selector(self._notifyAllObservers), with: nil, afterDelay: 0, inModes: [.common])
+        } else {
+            _notifyAllObservers()
+        }
+    }
+    
+    @objc private func _keyboardFrameDidChange(_ notif: Notification?) {
+        guard let notif = notif else {
+            return
+        }
+        guard notif.name == UIResponder.keyboardDidChangeFrameNotification else {
+            return
+        }
+        guard let info = notif.userInfo else {
+            return
+        }
+        _initFrameObserver()
+        let afterValue = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
+        let after: CGRect = afterValue?.cgRectValue ?? .zero
+        // ignore zero end frame
+        if (after.size.width <= 0) && (after.size.height <= 0) {
+            return
+        }
+        notificationToFrame = after
+        notificationCurve = UIView.AnimationCurve.easeInOut
+        notificationDuration = 0
+        hasNotification = true
+        lastIsNotification = true
+        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self._notifyAllObservers), object: nil)
+        perform(#selector(self._notifyAllObservers), with: nil, afterDelay: 0, inModes: [.common])
+    }
+    
+    private func _keyboardFrameChanged(_ keyboard: UIView?) {
+        if keyboard != keyboardView {
+            return
+        }
+        
+        if let window = keyboard?.window {
+            observedToFrame = window.convert(keyboard?.frame ?? CGRect.zero, to: nil)
+        } else {
+            observedToFrame = (keyboard?.frame)!
+        }
+        hasObservedChange = true
+        lastIsNotification = false
+        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self._notifyAllObservers), object: nil)
+        perform(#selector(self._notifyAllObservers), with: nil, afterDelay: 0, inModes: [.common])
+    }
+    
+    @objc private func _notifyAllObservers() {
+        
+        guard let app = TextUtilities.sharedApplication else {
+            return
+        }
+        let keyboard: UIView? = keyboardView
+        var window: UIWindow? = keyboard?.window
+        if window == nil {
+            window = app.keyWindow
+        }
+        if window == nil {
+            window = app.windows.first
+        }
+        guard let w = window else {
+            return
+        }
+        let trans = TextKeyboardTransition()
+        // from
+        if fromFrame.size.width == 0 && fromFrame.size.height == 0 {
+            // first notify
+            fromFrame.size.width = w.bounds.size.width
+            fromFrame.size.height = trans.toFrame.size.height
+            fromFrame.origin.x = trans.toFrame.origin.x
+            fromFrame.origin.y = w.bounds.size.height
+        }
+        trans.fromFrame = fromFrame
+        trans.fromVisible = fromVisible
+        // to
+        if lastIsNotification || (hasObservedChange && observedToFrame.equalTo(notificationToFrame)) {
+            trans.toFrame = notificationToFrame
+            trans.animationDuration = notificationDuration
+            trans.animationCurve = notificationCurve
+            trans.animationOption = UIView.AnimationOptions(rawValue: UInt(notificationCurve.rawValue << 16))
+        } else {
+            trans.toFrame = observedToFrame
+        }
+        if window != nil && trans.toFrame.size.width > 0 && trans.toFrame.size.height > 0 {
+            let rect: CGRect = w.bounds.intersection(trans.toFrame)
+            if !rect.isNull && !rect.isEmpty {
+                trans.toVisible = true
+            }
+        }
+        if !trans.toFrame.equalTo(fromFrame) {
+            
+            for (_, observer) in observers.objectEnumerator().enumerated() {
+                guard let o = observer as? TextKeyboardObserver else {
+                    return
+                }
+                if o.responds(to: #selector(TextKeyboardObserver.keyboardChanged(with:))) {
+                    o.keyboardChanged!(with: trans)
+                }
+            }
+        }
+        hasNotification = false
+        hasObservedChange = false
+        fromFrame = trans.toFrame
+        fromVisible = trans.toVisible
+    }
+    
+    /**
+     Convert rect to specified view or window.
+     
+     @param rect The frame rect.
+     @param view A specified view or window (pass nil to convert for main window).
+     @return The converted rect in specifeid view.
+     */
+    @objc(convertRect:toView:)
+    public func convert(_ rect: CGRect, to view: UIView?) -> CGRect {
+        var rect = rect
+        
+        guard let app = TextUtilities.sharedApplication else {
+            return CGRect.zero
+        }
+        if rect.isNull {
+            return rect
+        }
+        if rect.isInfinite {
+            return rect
+        }
+        var mainWindow: UIWindow? = app.keyWindow
+        if mainWindow == nil {
+            mainWindow = app.windows.first
+        }
+        if mainWindow == nil {
+            // no window ?!
+            if view != nil {
+                view?.convert(rect, from: nil)
+            } else {
+                return rect
+            }
+        }
+        rect = mainWindow?.convert(rect, from: nil) ?? CGRect.zero
+        if view == nil {
+            return mainWindow?.convert(rect, to: nil) ?? CGRect.zero
+        }
+        if view == mainWindow {
+            return rect
+        }
+        let toWindow = (view is UIWindow) ? (view as? UIWindow) : view?.window
+        if mainWindow == nil || toWindow == nil {
+            return mainWindow?.convert(rect, to: view) ?? CGRect.zero
+        }
+        if mainWindow == toWindow {
+            return mainWindow?.convert(rect, to: view) ?? CGRect.zero
+        }
+        // in different window
+        rect = mainWindow?.convert(rect, to: mainWindow) ?? CGRect.zero
+        rect = toWindow?.convert(rect, from: mainWindow) ?? CGRect.zero
+        rect = view?.convert(rect, from: toWindow) ?? CGRect.zero
+        return rect
+    }
+}
+
+
+extension UIApplication {
+    
+    private static let runOnce: Void = {
+        TextKeyboardManager.startManager()
+    }()
+    
+    override open var next: UIResponder? {
+        // Called before applicationDidFinishLaunching
+        UIApplication.runOnce
+        return super.next
+    }
+}

+ 4283 - 0
Pods/BSText/BSText/Component/TextLayout.swift

@@ -0,0 +1,4283 @@
+//
+//  TextLayout.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/12/5.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+fileprivate struct RowEdge {
+    var head: CGFloat = 0
+    var foot: CGFloat = 0
+}
+
+@inline(__always) private func textClipCGSize(_ size: CGSize) -> CGSize {
+    var size = size
+    if size.width > TextContainer.textContainerMaxSize.width {
+        size.width = TextContainer.textContainerMaxSize.width
+    }
+    if size.height > TextContainer.textContainerMaxSize.height {
+        size.height = TextContainer.textContainerMaxSize.height
+    }
+    return size
+}
+
+@inline(__always) private func UIEdgeInsetRotateVertical(insets: UIEdgeInsets) -> UIEdgeInsets {
+    var one = UIEdgeInsets.zero
+    one.top = insets.left
+    one.left = insets.bottom
+    one.bottom = insets.right
+    one.right = insets.top
+    return one
+}
+
+/**
+ The TextContainer class defines a region in which text is laid out.
+ TextLayout class uses one or more TextContainer objects to generate layouts.
+ 
+ A TextContainer defines rectangular regions (`size` and `insets`) or
+ nonrectangular shapes (`path`), and you can define exclusion paths inside the
+ text container's bounding rectangle so that text flows around the exclusion
+ path as it is laid out.
+ 
+ All methods in this class is thread-safe.
+ 
+ Example:
+ 
+ ┌─────────────────────────────┐  <------- container
+ │                             │
+ │    asdfasdfasdfasdfasdfa   <------------ container insets
+ │    asdfasdfa   asdfasdfa    │
+ │    asdfas         asdasd    │
+ │    asdfa        <----------------------- container exclusion path
+ │    asdfas         adfasd    │
+ │    asdfasdfa   asdfasdfa    │
+ │    asdfasdfasdfasdfasdfa    │
+ │                             │
+ └─────────────────────────────┘
+ */
+public class TextContainer: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    /**
+     The max text container size in layout.
+     */
+    @objc public static let textContainerMaxSize = CGSize(width: 0x100000, height: 0x100000)
+    
+    private var _size = CGSize.zero
+    /// The constrained size. (if the size is larger than TextContainerMaxSize, it will be clipped)
+    @objc public var size: CGSize {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            if _path == nil {
+                _size = textClipCGSize(newValue)
+            }
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let s = _size
+            lock.signal()
+            return s
+        }
+    }
+    
+    private var _insets = UIEdgeInsets.zero
+    /// The insets for constrained size. The inset value should not be negative. Default is UIEdgeInsetsZero.
+    @objc public var insets: UIEdgeInsets {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            if _path == nil {
+                var i = newValue
+                if i.top < 0 { i.top = 0 }
+                if i.left < 0 { i.left = 0 }
+                if i.bottom < 0 { i.bottom = 0 }
+                if i.right < 0 { i.right = 0 }
+                _insets = i
+            }
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let i = _insets
+            lock.signal()
+            return i
+        }
+    }
+    
+    private var _path: UIBezierPath?
+    /// Custom constrained path. Set this property to ignore `size` and `insets`. Default is nil.
+    @objc public var path: UIBezierPath? {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _path = newValue?.copy() as? UIBezierPath;
+            if (_path != nil) {
+                let bounds = _path!.bounds;
+                var size = bounds.size;
+                var insets = UIEdgeInsets.zero;
+                if (bounds.origin.x < 0) { size.width += bounds.origin.x; }
+                if (bounds.origin.x > 0) { insets.left = bounds.origin.x; }
+                if (bounds.origin.y < 0) { size.height += bounds.origin.y; }
+                if (bounds.origin.y > 0) { insets.top = bounds.origin.y; }
+                _size = size;
+                _insets = insets;
+            }
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let p = _path
+            lock.signal()
+            return p
+        }
+    }
+    
+    private var _exclusionPaths: [UIBezierPath]?
+    /// An array of `UIBezierPath` for path exclusion. Default is nil.
+    @objc public var exclusionPaths: [UIBezierPath]? {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _exclusionPaths = newValue
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let p = _exclusionPaths
+            lock.signal()
+            return p
+        }
+    }
+    
+    private var _pathLineWidth: CGFloat = 0
+    /// Path line width. Default is 0;
+    @objc public var pathLineWidth: CGFloat {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _pathLineWidth = newValue
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let p = _pathLineWidth
+            lock.signal()
+            return p
+        }
+    }
+    
+    private var _pathFillEvenOdd = true
+    /// YES:(PathFillEvenOdd) Text is filled in the area that would be painted if the path were given to CGContextEOFillPath.
+    /// NO: (PathFillWindingNumber) Text is fill in the area that would be painted if the path were given to CGContextFillPath.
+    /// Default is YES;
+    @objc public var pathFillEvenOdd: Bool {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _pathFillEvenOdd = newValue
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let p = _pathFillEvenOdd
+            lock.signal()
+            return p
+        }
+    }
+    
+    private var _isVerticalForm = false
+    /// Whether the text is vertical form (may used for CJK text layout). Default is NO.
+    @objc public var isVerticalForm: Bool {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _isVerticalForm = newValue
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let v = _isVerticalForm
+            lock.signal()
+            return v
+        }
+    }
+    
+    private var _maximumNumberOfRows: Int = 0
+    /// Maximum number of rows, 0 means no limit. Default is 0.
+    @objc public var maximumNumberOfRows: Int {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _maximumNumberOfRows = newValue
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let m = _maximumNumberOfRows
+            lock.signal()
+            return m
+        }
+    }
+    
+    private var _truncationType = TextTruncationType.none
+    /// The line truncation type, default is none.
+    @objc public var truncationType: TextTruncationType {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _truncationType = newValue
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let t = _truncationType
+            lock.signal()
+            return t
+        }
+    }
+    
+    private var _truncationToken: NSAttributedString?
+    /// The truncation token. If nil, the layout will use "…" instead. Default is nil.
+    @objc public var truncationToken: NSAttributedString? {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _truncationToken = newValue?.copy() as? NSAttributedString
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let t = _truncationToken
+            lock.signal()
+            return t
+        }
+    }
+    
+    private weak var _linePositionModifier: TextLinePositionModifier?
+    /// This modifier is applied to the lines before the layout is completed,
+    /// give you a chance to modify the line position. Default is nil.
+    @objc public weak var linePositionModifier: TextLinePositionModifier? {
+        set {
+            if readonly {
+                fatalError("Cannot change the property of the 'container' in 'TextLayout'.")
+            }
+            lock.wait()
+            _linePositionModifier = _linePositionModifier?.copy() as? TextLinePositionModifier
+            lock.signal()
+        }
+        get {
+            lock.wait()
+            let l = _linePositionModifier
+            lock.signal()
+            return l
+        }
+    }
+    
+    
+    ///< used only in TextLayout.implementation
+    fileprivate var readonly = false
+    fileprivate lazy var lock: DispatchSemaphore = DispatchSemaphore(value: 1)
+
+    
+    /// Creates a container with the specified size. @param size The size.
+    @objc(containerWithSize:)
+    public class func container(with size: CGSize) -> TextContainer {
+        return container(with: size, insets: UIEdgeInsets.zero)
+    }
+    
+    /// Creates a container with the specified size and insets. @param size The size. @param insets The text insets.
+    @objc(containerWithSize:insets:)
+    public class func container(with size: CGSize, insets: UIEdgeInsets) -> TextContainer {
+        let one = TextContainer.init()
+        one.size = textClipCGSize(size)
+        one.insets = insets
+        return one
+    }
+    
+    /// Creates a container with the specified path. @param path The path.
+    @objc(containerWithPath:)
+    public class func container(with path: UIBezierPath?) -> TextContainer {
+        let one = TextContainer.init()
+        one.path = path
+        return one
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextContainer.init()
+        lock.wait()
+        one._size = _size
+        one._insets = _insets
+        one._path = _path
+        one._exclusionPaths = _exclusionPaths
+        one._pathFillEvenOdd = _pathFillEvenOdd
+        one._pathLineWidth = _pathLineWidth
+        one._isVerticalForm = _isVerticalForm
+        one._maximumNumberOfRows = _maximumNumberOfRows
+        one._truncationType = _truncationType
+        one._truncationToken = _truncationToken?.copy() as? NSAttributedString
+        one._linePositionModifier = _linePositionModifier
+        lock.signal()
+        return one
+    }
+    
+    override public func mutableCopy() -> Any {
+        return self.copy()
+    }
+    
+    // MARK: - NSCoding
+    @objc public func encode(with aCoder: NSCoder) {
+        aCoder.encode(_size, forKey: "size")
+        aCoder.encode(_insets, forKey: "insets")
+        aCoder.encode(_path, forKey: "path")
+        aCoder.encode(_exclusionPaths, forKey: "exclusionPaths")
+        aCoder.encode(_pathFillEvenOdd, forKey: "pathFillEvenOdd")
+        aCoder.encode(Float(_pathLineWidth), forKey: "pathLineWidth")
+        aCoder.encode(_isVerticalForm, forKey: "isVerticalForm")
+        aCoder.encode(_maximumNumberOfRows, forKey: "maximumNumberOfRows")
+        aCoder.encode(_truncationType.rawValue, forKey: "truncationType")
+        aCoder.encode(_truncationToken, forKey: "truncationToken")
+        if (_linePositionModifier?.responds(to: #selector(self.encode(with:))) ?? false) {
+            aCoder.encode(linePositionModifier, forKey: "linePositionModifier")
+        }
+    }
+    
+    required convenience public init?(coder aDecoder: NSCoder) {
+        self.init()
+        _size = aDecoder.decodeCGSize(forKey: "size")
+        _insets = aDecoder.decodeUIEdgeInsets(forKey: "insets")
+        _path = aDecoder.decodeObject(forKey: "path") as? UIBezierPath
+        _exclusionPaths = aDecoder.decodeObject(forKey: "exclusionPaths") as? [UIBezierPath]
+        _pathFillEvenOdd = aDecoder.decodeBool(forKey: "pathFillEvenOdd")
+        _pathLineWidth = CGFloat(aDecoder.decodeFloat(forKey: "pathLineWidth"))
+        _isVerticalForm = aDecoder.decodeBool(forKey: "isVerticalForm")
+        _maximumNumberOfRows = aDecoder.decodeInteger(forKey: "maximumNumberOfRows")
+        _truncationType = TextTruncationType(rawValue: aDecoder.decodeInteger(forKey: "truncationType"))!
+        _truncationToken = aDecoder.decodeObject(forKey: "truncationToken") as? NSAttributedString
+        _linePositionModifier = aDecoder.decodeObject(forKey: "linePositionModifier") as? TextLinePositionModifier
+    }
+    
+    // MARK: - NSSecureCoding
+    public static var supportsSecureCoding: Bool {
+        return true
+    }
+}
+
+/**
+ The TextLinePositionModifier protocol declares the required method to modify
+ the line position in text layout progress. See `TextLinePositionSimpleModifier` for example.
+ */
+@objc public protocol TextLinePositionModifier: NSObjectProtocol, NSCopying {
+    /**
+     This method will called before layout is completed. The method should be thread-safe.
+     @param lines     An array of TextLine.
+     @param text      The full text.
+     @param container The layout container.
+     */
+    func modifyLines(_ lines: [TextLine]?, fromText text: NSAttributedString?, in container: TextContainer?)
+}
+
+/**
+ A simple implementation of `TextLinePositionModifier`. It can fix each line's position
+ to a specified value, lets each line of height be the same.
+ */
+public class TextLinePositionSimpleModifier: NSObject, TextLinePositionModifier {
+    
+    ///< The fixed line height (distance between two baseline).
+    @objc public var fixedLineHeight: CGFloat = 0
+    
+    public func modifyLines(_ lines: [TextLine]?, fromText text: NSAttributedString?, in container: TextContainer?) {
+        
+        guard let l = lines, let c = container else {
+            return
+        }
+        
+        let maxCount = l.count
+        
+        if c.isVerticalForm {
+            for i in 0..<maxCount {
+                let line = l[i]
+                var pos = line.position
+                pos.x = c.size.width - c.insets.right - CGFloat(integerLiteral: line.row) * fixedLineHeight - fixedLineHeight * 0.9
+                line.position = pos
+            }
+        } else {
+            for i in 0..<maxCount {
+                let line = l[i]
+                var pos = line.position
+                pos.y = CGFloat(integerLiteral: line.row) * fixedLineHeight + fixedLineHeight * 0.9 + c.insets.top
+                line.position = pos
+            }
+        }
+    }
+    
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextLinePositionSimpleModifier()
+        one.fixedLineHeight = fixedLineHeight
+        return one
+    }
+}
+
+// CoreText bug when draw joined emoji since iOS 8.3.
+// See -[NSMutableAttributedString setClearColorToJoinedEmoji] for more information.
+fileprivate var needFixJoinedEmojiBug = { () -> Bool in
+    let systemVersionDouble = Double(UIDevice.current.systemVersion) ?? 0
+    if (8.3 <= systemVersionDouble && systemVersionDouble < 9) {
+        return true
+    }
+    return false
+}()
+
+// It may use larger constraint size when create CTFrame with CTFramesetterCreateFrame in iOS 10.
+fileprivate var needFixLayoutSizeBug = { () -> Bool in
+    let systemVersionDouble = Double(UIDevice.current.systemVersion) ?? 0
+    if (systemVersionDouble >= 10) {
+        return true
+    }
+    return false
+}()
+
+
+/**
+ TextLayout class is a readonly class stores text layout result.
+ All the property in this class is readonly, and should not be changed.
+ The methods in this class is thread-safe (except some of the draw methods).
+ 
+ example: (layout with a circle exclusion path)
+ 
+ ┌──────────────────────────┐  <------ container
+ │ [--------Line0--------]  │  <- Row0
+ │ [--------Line1--------]  │  <- Row1
+ │ [-Line2-]     [-Line3-]  │  <- Row2
+ │ [-Line4]       [Line5-]  │  <- Row3
+ │ [-Line6-]     [-Line7-]  │  <- Row4
+ │ [--------Line8--------]  │  <- Row5
+ │ [--------Line9--------]  │  <- Row6
+ └──────────────────────────┘
+ */
+public class TextLayout: NSObject, NSCoding, NSCopying {
+    
+    // MARK: - Text layout attributes
+    
+    ///=============================================================================
+    /// @name Text layout attributes
+    ///=============================================================================
+    
+    ///< The text container
+    @objc public private(set) lazy var container = TextContainer()
+    ///< The full text
+    @objc public private(set) var text: NSAttributedString?
+    ///< The text range in full text
+    @objc public private(set) var range = NSRange(location: 0, length: 0)
+    ///< CTFrameSetter
+    @objc public private(set) lazy var frameSetter = CTFramesetterCreateWithAttributedString(NSAttributedString(string: ""))
+    ///< CTFrame
+    @objc public private(set) lazy var frame: CTFrame = {
+        let ctSetter = CTFramesetterCreateWithAttributedString(NSAttributedString(string: ""))
+        let ctFrame = CTFramesetterCreateFrame(ctSetter, TextUtilities.textCFRange(from: NSRange(location: 0, length: 0)), UIBezierPath().cgPath, [:] as CFDictionary)
+        return ctFrame
+    }()
+    ///< Array of `TextLine`, no truncated
+    @objc public private(set) lazy var lines: [TextLine] = []
+    ///< TextLine with truncated token, or nil
+    @objc public private(set) var truncatedLine: TextLine?
+    ///< Array of `TextAttachment`
+    @objc public private(set) var attachments: [TextAttachment]?
+    ///< Array of NSRange(wrapped by NSValue) in text
+    @objc public private(set) var attachmentRanges: [NSValue]?
+    ///< Array of CGRect(wrapped by NSValue) in container
+    @objc public private(set) var attachmentRects: [NSValue]?
+    ///< Set of Attachment (UIImage/UIView/CALayer)
+    @objc public private(set) var attachmentContentsSet: Set<AnyHashable>?
+    ///< Number of rows
+    @objc public private(set) var rowCount: Int = 0
+    ///< Visible text range
+    @objc public private(set) lazy var visibleRange = NSRange(location: 0, length: 0)
+    ///< Bounding rect (glyphs)
+    @objc public private(set) var textBoundingRect = CGRect.zero
+    ///< Bounding size (glyphs and insets, ceil to pixel)
+    @objc public private(set) var textBoundingSize = CGSize.zero
+    ///< Has highlight attribute
+    @objc public private(set) var containsHighlight = false
+    ///< Has block border attribute
+    @objc public private(set) var needDrawBlockBorder = false
+    
+    ///< Has background border attribute
+    @objc public private(set) var needDrawBackgroundBorder = false
+    ///< Has shadow attribute
+    @objc public private(set) var needDrawShadow = false
+    ///< Has underline attribute
+    @objc public private(set) var needDrawUnderline = false
+    ///< Has visible text
+    @objc public private(set) var needDrawText = false
+    ///< Has attachment attribute
+    @objc public private(set) var needDrawAttachment = false
+    ///< Has inner shadow attribute
+    @objc public private(set) var needDrawInnerShadow = false
+    ///< Has strickthrough attribute
+    @objc public private(set) var needDrawStrikethrough = false
+    ///< Has border attribute
+    @objc public private(set) var needDrawBorder = false
+    
+    private var lineRowsIndex: UnsafeMutablePointer<Int>?
+    ///< top-left origin
+    private var lineRowsEdge: UnsafeMutablePointer<RowEdge>?
+    
+    
+    private override init() {
+        super.init()
+    }
+    
+    private convenience init(container: TextContainer) {
+        self.init()
+        self.container = container
+    }
+    
+    deinit {
+        lineRowsEdge?.deallocate()
+        lineRowsIndex?.deallocate()
+    }
+    
+    // MARK: - Generate text layout
+    ///=============================================================================
+    /// @name Generate text layout
+    ///=============================================================================
+    /**
+     Generate a layout with the given container size and text.
+     
+     @param containerSize The text container's size
+     @param text The text (if nil, returns nil).
+     @return A new layout, or nil when an error occurs.
+     */
+    @objc(initWithContainerSize:text:)
+    public convenience init?(containerSize: CGSize, text: NSAttributedString?) {
+        let container = TextContainer.container(with: containerSize)
+        self.init(container: container, text: text)
+    }
+    
+    /**
+     Generate a layout with the given container and text.
+     
+     @param container The text container (if nil, returns nil).
+     @param text      The text (if nil, returns nil).
+     @return A new layout, or nil when an error occurs.
+     */
+    @objc(initWithContainer:text:)
+    public convenience init?(container: TextContainer?, text: NSAttributedString?) {
+        self.init(container: container, text: text, range: NSRange(location: 0, length: text?.length ?? 0))
+    }
+    
+    /**
+     Generate a layout with the given container and text.
+     
+     @param container The text container (if nil, returns nil).
+     @param text      The text (if nil, returns nil).
+     @param range     The text range (if out of range, returns nil). If the
+     length of the range is 0, it means the length is no limit.
+     @return A new layout, or nil when an error occurs.
+     */
+    @objc(initWithContainer:text:range:)
+    public convenience init?(container: TextContainer?, text: NSAttributedString?, range: NSRange) {
+        
+        guard let t = text?.mutableCopy() as? NSMutableAttributedString, let c = container?.copy() as? TextContainer else {
+            return nil
+        }
+        if range.location + range.length > t.length {
+            return nil
+        }
+        self.init(container: c)
+        
+        var cgPath: CGPath
+        var cgPathBox = CGRect.zero
+        var isVerticalForm = false
+        var rowMaySeparated = false
+        var frameAttrs = [AnyHashable : AnyObject]()
+        
+        
+        var ctLines: CFArray? = nil
+        var lineOrigins: UnsafeMutablePointer<CGPoint>? = nil
+        var lineCount: Int = 0
+        var lines_ = [TextLine]()
+        var attachments_: [TextAttachment]? = nil
+        var attachmentRanges_: [NSValue]? = nil
+        var attachmentRects_: [NSValue]? = nil
+        var attachmentContentsSet_: Set<AnyHashable>? = nil
+        var needTruncation = false
+        var truncationToken: NSAttributedString? = nil
+        var truncatedLine_: TextLine? = nil
+        var lineRowsEdge_: UnsafeMutablePointer<RowEdge>? = nil
+        var lineRowsIndex_: UnsafeMutablePointer<Int>? = nil
+        
+        var maximumNumberOfRows: Int = 0
+        var constraintSizeIsExtended = false
+        var constraintRectBeforeExtended = CGRect.zero
+        
+        c.readonly = true
+        maximumNumberOfRows = c.maximumNumberOfRows
+        
+        if needFixJoinedEmojiBug {
+            (text as? NSMutableAttributedString)?.bs_setClearColorToJoinedEmoji()
+        }
+        
+        self.text = text
+        self.container = c
+        self.range = range
+        isVerticalForm = c.isVerticalForm
+        // set cgPath and cgPathBox
+        if c.path == nil && (c.exclusionPaths?.count ?? 0) == 0 {
+            if c.size.width <= 0 || c.size.height <= 0 {
+                lineOrigins?.deallocate()
+                lineRowsEdge_?.deallocate()
+                lineRowsIndex_?.deallocate()
+                return nil
+            }
+            var rect = CGRect.zero
+            rect.size = c.size
+            if needFixLayoutSizeBug {
+                constraintSizeIsExtended = true
+                constraintRectBeforeExtended = rect.inset(by: c.insets)
+                constraintRectBeforeExtended = constraintRectBeforeExtended.standardized
+                if c.isVerticalForm {
+                    rect.size.width = TextContainer.textContainerMaxSize.width
+                } else {
+                    rect.size.height = TextContainer.textContainerMaxSize.height
+                }
+            }
+            rect = rect.inset(by: c.insets)
+            rect = rect.standardized
+            cgPathBox = rect
+            rect = rect.applying(CGAffineTransform(scaleX: 1, y: -1))
+            cgPath = CGPath(rect: rect, transform: nil) // let CGPathIsRect() returns true
+            
+        } else if (c.path != nil) && c.path!.cgPath.isRect(&cgPathBox) && c.exclusionPaths?.count ?? 0 == 0 {
+            
+            let rect: CGRect = cgPathBox.applying(CGAffineTransform(scaleX: 1, y: -1))
+            cgPath = CGPath(rect: rect, transform: nil) // let CGPathIsRect() returns true
+            
+        } else {
+            rowMaySeparated = true
+            var path: CGMutablePath
+            if c.path != nil {
+                path = c.path!.cgPath.mutableCopy()!
+            } else {
+                var rect = CGRect.zero
+                rect.size = c.size
+                rect = rect.inset(by: c.insets)
+                let rectPath = CGPath(rect: rect, transform: nil)
+                path = rectPath.mutableCopy()!
+            }
+            if true {   // path != nil
+                if let e = self.container.exclusionPaths {
+                    for onePath in e {
+                        path.addPath(onePath.cgPath, transform: .identity)
+                    }
+                }
+                cgPathBox = path.boundingBoxOfPath
+                var trans = CGAffineTransform(scaleX: 1, y: -1)
+                let transPath = path.mutableCopy(using: &trans)!
+                path = transPath
+            }
+            cgPath = path
+        }
+        
+        // frame setter config
+        if c.pathFillEvenOdd == false {
+            frameAttrs[kCTFramePathFillRuleAttributeName] = NSNumber(value: CTFramePathFillRule.windingNumber.rawValue)
+        }
+        if c.pathLineWidth > 0 {
+            frameAttrs[kCTFramePathWidthAttributeName] = NSNumber(value: Float(c.pathLineWidth))
+        }
+        if c.isVerticalForm == true {
+            frameAttrs[kCTFrameProgressionAttributeName] = NSNumber(value: CTFrameProgression.rightToLeft.rawValue)
+        }
+        // create CoreText objects
+        let ctSetter = CTFramesetterCreateWithAttributedString(t)
+        let ctFrame = CTFramesetterCreateFrame(ctSetter, TextUtilities.textCFRange(from: range), cgPath, frameAttrs as CFDictionary)
+        
+        ctLines = CTFrameGetLines(ctFrame)
+        lineCount = CFArrayGetCount(ctLines)
+        if lineCount > 0 {
+            lineOrigins = UnsafeMutablePointer<CGPoint>.allocate(capacity: lineCount)
+            CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, lineCount), lineOrigins!)
+        }
+        
+        var textBoundingRect_ = CGRect.zero
+        var textBoundingSize_ = CGSize.zero
+        var rowIdx: Int = -1
+        var rowCount_: Int = 0
+        var lastRect = CGRect(x: 0, y: CGFloat(-Float.greatestFiniteMagnitude), width: 0, height: 0)
+        var lastPosition = CGPoint(x: 0, y: CGFloat(-Float.greatestFiniteMagnitude))
+        if isVerticalForm {
+            lastRect = CGRect(x: CGFloat(Float.greatestFiniteMagnitude), y: 0, width: 0, height: 0)
+            lastPosition = CGPoint(x: CGFloat(Float.greatestFiniteMagnitude), y: 0)
+        }
+        
+        
+        // calculate line frame
+        var lineCurrentIdx: Int = 0;
+        for i in 0..<lineCount {
+            let ctLine = unsafeBitCast(CFArrayGetValueAtIndex(ctLines, i), to: CTLine.self)
+            let ctRuns = CTLineGetGlyphRuns(ctLine)
+            if CFArrayGetCount(ctRuns) == 0 {
+                continue
+            }
+            // CoreText coordinate system
+            let ctLineOrigin: CGPoint = lineOrigins![i]
+            // UIKit coordinate system
+            var position = CGPoint.zero
+            position.x = cgPathBox.origin.x + ctLineOrigin.x
+            position.y = cgPathBox.size.height + cgPathBox.origin.y - ctLineOrigin.y
+            let line = TextLine.lineWith(ctLine: ctLine, position: position, vertical: isVerticalForm)
+            let rect: CGRect = line.bounds
+            if constraintSizeIsExtended {
+                if isVerticalForm {
+                    if rect.origin.x + rect.size.width > constraintRectBeforeExtended.origin.x + constraintRectBeforeExtended.size.width {
+                        break
+                    }
+                } else {
+                    if rect.origin.y + rect.size.height > constraintRectBeforeExtended.origin.y + constraintRectBeforeExtended.size.height {
+                        break
+                    }
+                }
+            }
+            
+            var newRow = true
+            if rowMaySeparated && position.x != lastPosition.x {
+                if isVerticalForm {
+                    if rect.size.width > lastRect.size.width {
+                        if rect.origin.x > lastPosition.x && lastPosition.x > rect.origin.x - rect.size.width {
+                            newRow = false
+                        }
+                    } else {
+                        if lastRect.origin.x > position.x && position.x > lastRect.origin.x - lastRect.size.width {
+                            newRow = false
+                        }
+                    }
+                } else {
+                    if rect.size.height > lastRect.size.height {
+                        if rect.origin.y < lastPosition.y && lastPosition.y < rect.origin.y + rect.size.height {
+                            newRow = false
+                        }
+                    } else {
+                        if lastRect.origin.y < position.y && position.y < lastRect.origin.y + lastRect.size.height {
+                            newRow = false
+                        }
+                    }
+                }
+            }
+            if newRow {
+                rowIdx += 1
+            }
+            lastRect = rect
+            lastPosition = position
+            
+            line.index = lineCurrentIdx
+            line.row = rowIdx
+            lines_.append(line)
+            rowCount_ = rowIdx + 1
+            lineCurrentIdx += 1
+            if i == 0 {
+                textBoundingRect_ = rect
+            } else {
+                if maximumNumberOfRows == 0 || rowIdx < maximumNumberOfRows {
+                    textBoundingRect_ = textBoundingRect_.union(rect)
+                }
+            }
+        }
+        
+        if rowCount_ > 0 {
+            if maximumNumberOfRows > 0 {
+                if rowCount_ > maximumNumberOfRows {
+                    needTruncation = true
+                    rowCount_ = maximumNumberOfRows
+                    repeat {
+                        let line = lines_.last
+                        if line == nil {
+                            break
+                        }
+                        if line!.row < rowCount_ {
+                            break
+                        }
+                        lines_.removeLast()
+                    } while true
+                }
+            }
+            let lastLine = lines_.last
+            if !needTruncation && (lastLine?.range.location)! + (lastLine?.range.length)! < text!.length {
+                needTruncation = true
+            }
+            // Give user a chance to modify the line's position.
+            if (c.linePositionModifier != nil) {
+                c.linePositionModifier!.modifyLines(lines_, fromText: text, in: c)
+                textBoundingRect_ = CGRect.zero
+                var i = 0, maxCount = lines_.count
+                while i < maxCount {
+                    let line = lines_[i]
+                    if i == 0 {
+                        textBoundingRect_ = line.bounds
+                    } else {
+                        textBoundingRect_ = textBoundingRect_.union(line.bounds)
+                    }
+                    i += 1
+                }
+            }
+            lineRowsEdge_ = UnsafeMutablePointer<RowEdge>.allocate(capacity: rowCount_)
+            lineRowsIndex_ = UnsafeMutablePointer<Int>.allocate(capacity: rowCount_)
+            
+            var lastRowIdx: Int = -1
+            var lastHead: CGFloat = 0
+            var lastFoot: CGFloat = 0
+            
+            var i = 0, maxCount = lines_.count
+            while i < maxCount {
+                let line = lines_[i]
+                let rect = line.bounds
+                if line.row != lastRowIdx {
+                    if lastRowIdx >= 0 {
+                        lineRowsEdge_![lastRowIdx] = RowEdge(head: lastHead, foot: lastFoot)
+                    }
+                    lastRowIdx = line.row
+                    lineRowsIndex_![lastRowIdx] = i
+                    if isVerticalForm {
+                        lastHead = rect.origin.x + rect.size.width
+                        lastFoot = lastHead - rect.size.width
+                    } else {
+                        lastHead = rect.origin.y
+                        lastFoot = lastHead + rect.size.height
+                    }
+                } else {
+                    if isVerticalForm {
+                        lastHead = max(lastHead, rect.origin.x + rect.size.width)
+                        lastFoot = min(lastFoot, rect.origin.x)
+                    } else {
+                        lastHead = min(lastHead, rect.origin.y)
+                        lastFoot = max(lastFoot, rect.origin.y + rect.size.height)
+                    }
+                }
+                i += 1
+            }
+            
+            lineRowsEdge_![lastRowIdx] = RowEdge(head: lastHead, foot: lastFoot)
+            
+            for i in 1..<rowCount_ {
+                let v0: RowEdge = lineRowsEdge_![i - 1]
+                let v1: RowEdge = lineRowsEdge_![i]
+                let tmp = (v0.foot + v1.head) * 0.5
+                lineRowsEdge_![i].head = tmp
+                lineRowsEdge_![i - 1].foot = tmp
+            }
+        }
+        
+        if true {
+            // calculate bounding size
+            var rect: CGRect = textBoundingRect_
+            if (c.path != nil) {
+                if c.pathLineWidth > 0 {
+                    let inset: CGFloat = c.pathLineWidth / 2
+                    rect = rect.insetBy(dx: -inset, dy: -inset)
+                }
+            } else {
+                rect = rect.inset(by: TextUtilities.textUIEdgeInsetsInvert(c.insets))
+            }
+            rect = rect.standardized
+            var size: CGSize = rect.size
+            if c.isVerticalForm {
+                size.width += c.size.width - (rect.origin.x + rect.size.width)
+            } else {
+                size.width += rect.origin.x
+            }
+            size.height += rect.origin.y
+            if size.width < 0 {
+                size.width = 0
+            }
+            if size.height < 0 {
+                size.height = 0
+            }
+            size.width = ceil(size.width)
+            size.height = ceil(size.height)
+            textBoundingSize_ = size
+        }
+        
+        var visibleRange_ = TextUtilities.textNSRange(from: CTFrameGetVisibleStringRange(ctFrame))
+        
+        if needTruncation {
+            let lastLine = lines_.last!
+            let lastRange = lastLine.range
+            visibleRange_.length = lastRange.location + lastRange.length - visibleRange_.location
+            
+            // create truncated line
+            if c.truncationType != TextTruncationType.none {
+                var truncationTokenLine: CTLine? = nil
+                if (c.truncationToken != nil) {
+                    truncationToken = c.truncationToken
+                    truncationTokenLine = CTLineCreateWithAttributedString(truncationToken! as CFAttributedString)
+                } else {
+                    let runs = CTLineGetGlyphRuns(lastLine.ctLine!)
+                    let runCount: Int = CFArrayGetCount(runs)
+                    var attrs: [NSAttributedString.Key : Any]? = nil
+                    if runCount > 0 {
+                        let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, runCount - 1), to: CTRun.self)
+                        attrs = CTRunGetAttributes(run) as? [NSAttributedString.Key : Any]
+                        attrs = attrs != nil ? attrs : [NSAttributedString.Key : Any]()
+                        
+                        for k in NSMutableAttributedString.bs_allDiscontinuousAttributeKeys() {
+                            attrs!.removeValue(forKey: k)
+                        }
+                        
+                        var font = (attrs?[kCTFontAttributeName as NSAttributedString.Key] as! CTFont?)
+                        let fontSize: CGFloat = font != nil ? CTFontGetSize(font!) : 12.0
+                        let uiFont = UIFont.systemFont(ofSize: fontSize * 0.9)
+                        font = CTFontCreateWithName((uiFont.fontName as CFString?)!, uiFont.pointSize, nil)
+                        if font != nil {
+                            attrs![kCTFontAttributeName as NSAttributedString.Key] = font
+                        }
+                        let color = (attrs?[kCTForegroundColorAttributeName as NSAttributedString.Key] as! CGColor?)
+                        if let c = color, CFGetTypeID(c) == CGColor.typeID, c.alpha == 0 {
+                            // ignore clear color
+                            attrs?.removeValue(forKey: kCTForegroundColorAttributeName as NSAttributedString.Key)
+                        }
+                        if attrs == nil {
+                            attrs = [NSAttributedString.Key : Any]()
+                        }
+                    }
+                    truncationToken = NSAttributedString(string: TextAttribute.textTruncationToken, attributes: attrs)
+                    truncationTokenLine = CTLineCreateWithAttributedString(truncationToken! as CFAttributedString)
+                }
+                
+                if (truncationTokenLine != nil) {
+                    var type: CTLineTruncationType = .end
+                    if c.truncationType == TextTruncationType.start {
+                        type = .start
+                    } else if c.truncationType == TextTruncationType.middle {
+                        type = .middle
+                    }
+                    let lastLineText = t.attributedSubstring(from: lastLine.range) as? NSMutableAttributedString
+                    lastLineText?.append(truncationToken!)
+                    let ctLastLineExtend = CTLineCreateWithAttributedString(lastLineText! as CFAttributedString)
+                    
+                    var truncatedWidth: CGFloat = lastLine.width
+                    var cgPathRect = CGRect.zero
+                    if cgPath.isRect(&cgPathRect) {
+                        if isVerticalForm {
+                            truncatedWidth = cgPathRect.size.height
+                        } else {
+                            truncatedWidth = cgPathRect.size.width
+                        }
+                    }
+                    
+                    if let ctTruncatedLine = CTLineCreateTruncatedLine(ctLastLineExtend, Double(truncatedWidth), type, truncationTokenLine) {
+                        truncatedLine_ = TextLine.lineWith(ctLine: ctTruncatedLine, position: lastLine.position, vertical: isVerticalForm)
+                        truncatedLine_!.index = lastLine.index
+                        truncatedLine_!.row = lastLine.row
+                    }
+                }
+            }
+        }
+        
+        if (isVerticalForm) {
+            let rotateCharset = TextUtilities.textVerticalFormRotateCharacterSet
+            let rotateMoveCharset = TextUtilities.textVerticalFormRotateAndMoveCharacterSet
+            let lineBlock: ((TextLine?) -> Void) = { line in
+                guard let l = line, let ctl = l.ctLine else {
+                    return
+                }
+                let runs = CTLineGetGlyphRuns(ctl)
+                let runCount: Int = CFArrayGetCount(runs)
+                if runCount == 0 {
+                    return
+                }
+                line!.verticalRotateRange = [[TextRunGlyphRange]]()
+                for i in 0..<runCount {
+                    let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, i), to: CTRun.self)
+                    var runRanges = [TextRunGlyphRange]()
+                    let glyphCount: Int = CTRunGetGlyphCount(run)
+                    if glyphCount == 0 {
+                        continue
+                    }
+                    
+                    let runStrIdx = UnsafeMutablePointer<CFIndex>.allocate(capacity: (glyphCount + 1))
+                    CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx)
+                    let runStrRange: CFRange = CTRunGetStringRange(run)
+                    runStrIdx[glyphCount] = runStrRange.location + runStrRange.length
+                    
+                    let runAttrs = CTRunGetAttributes(run) as! [String: AnyObject]
+                    let font = runAttrs[kCTFontAttributeName as String] as! CTFont
+                    let isColorGlyph: Bool = TextUtilities.textCTFontContainsColorBitmapGlyphs(font)
+                    var prevIdx: Int = 0
+                    var prevMode = TextRunGlyphDrawMode.horizontal
+                    let layoutStr = self.text!.string
+                    
+                    for g in 0..<glyphCount {
+                        var glyphRotate = false
+                        var glyphRotateMove = false
+                        let runStrLen = CFIndex(runStrIdx[g + 1] - runStrIdx[g])
+                        if isColorGlyph {
+                            glyphRotate = true
+                        } else if runStrLen == 1 {
+                            let c = (layoutStr as NSString).character(at: runStrIdx[g])
+                            glyphRotate = rotateCharset.characterIsMember(c)
+                            if glyphRotate {
+                                glyphRotateMove = rotateMoveCharset.characterIsMember(c)
+                            }
+                        } else if runStrLen > 1 {
+                            let glyphStr = layoutStr[layoutStr.index(layoutStr.startIndex, offsetBy: runStrIdx[g])..<layoutStr.index(layoutStr.startIndex, offsetBy: (runStrIdx[g]+runStrLen))]
+                            let glyphRotate: Bool = (glyphStr as NSString).rangeOfCharacter(from: rotateCharset as CharacterSet).location != NSNotFound
+                            if glyphRotate {
+                                glyphRotateMove = (glyphStr as NSString).rangeOfCharacter(from: rotateMoveCharset as CharacterSet).location != NSNotFound
+                            }
+                        }
+                        let mode = glyphRotateMove ? TextRunGlyphDrawMode.verticalRotateMove : (glyphRotate ? TextRunGlyphDrawMode.verticalRotate : TextRunGlyphDrawMode.horizontal);
+                        if g == 0 {
+                            prevMode = mode
+                        } else if mode != prevMode {
+                            let aRange = TextRunGlyphRange.range(with: NSRange(location: prevIdx, length: g - prevIdx), drawMode: prevMode)
+                            runRanges.append(aRange)
+                            prevIdx = g
+                            prevMode = mode
+                        }
+                    }
+                    
+                    if prevIdx < glyphCount {
+                        let aRange = TextRunGlyphRange.range(with: NSRange(location: prevIdx, length: glyphCount - prevIdx), drawMode: prevMode)
+                        runRanges.append(aRange)
+                    }
+                    runStrIdx.deallocate()
+                    
+                    line!.verticalRotateRange!.append(runRanges)
+                }
+            }
+            
+            for line in lines_ {
+                lineBlock(line)
+            }
+            if (truncatedLine_ != nil) {
+                lineBlock(truncatedLine_)
+            }
+        }
+        
+        if visibleRange_.length > 0 {
+            self.needDrawText = true
+            let block: ((_ attrs: [AnyHashable : Any]?, _ range: NSRange, _ stop: UnsafeMutablePointer<ObjCBool>?) -> Void)? = { attrs, range, stop in
+                if attrs?[TextAttribute.textHighlightAttributeName] != nil {
+                    self.containsHighlight = true
+                }
+                if attrs?[TextAttribute.textBlockBorderAttributeName] != nil {
+                    self.needDrawBlockBorder = true
+                }
+                if attrs?[TextAttribute.textBackgroundBorderAttributeName] != nil {
+                    self.needDrawBackgroundBorder = true
+                }
+                if attrs?[TextAttribute.textShadowAttributeName] != nil || attrs?[NSAttributedString.Key.shadow] != nil {
+                    self.needDrawShadow = true
+                }
+                if attrs?[TextAttribute.textUnderlineAttributeName] != nil {
+                    self.needDrawUnderline = true
+                }
+                if attrs?[TextAttribute.textAttachmentAttributeName] != nil {
+                    self.needDrawAttachment = true
+                }
+                if attrs?[TextAttribute.textInnerShadowAttributeName] != nil {
+                    self.needDrawInnerShadow = true
+                }
+                if attrs?[TextAttribute.textStrikethroughAttributeName] != nil {
+                    self.needDrawStrikethrough = true
+                }
+                if attrs?[TextAttribute.textBorderAttributeName] != nil {
+                    self.needDrawBorder = true
+                }
+            }
+            if let aBlock = block {
+                self.text!.enumerateAttributes(in: visibleRange_, options: .longestEffectiveRangeNotRequired, using: aBlock)
+            }
+            if (truncatedLine_ != nil) {
+                if let aBlock = block {
+                    truncationToken!.enumerateAttributes(in: NSRange(location: 0, length: truncationToken!.length), options: .longestEffectiveRangeNotRequired, using: aBlock)
+                }
+            }
+        }
+        
+        attachments_ = [TextAttachment]()
+        attachmentRanges_ = [NSValue]()
+        attachmentRects_ = [NSValue]()
+        attachmentContentsSet_ = Set<AnyHashable>()
+        
+        let maxCount = lines_.count
+        for i in 0..<maxCount {
+            
+            var line = lines_[i]
+            if (truncatedLine_ != nil) && line.index == truncatedLine_!.index {
+                line = truncatedLine_!
+            }
+            if line.attachments?.count ?? 0 > 0 {
+                if let anAttachments = line.attachments {
+                    attachments_!.append(contentsOf: anAttachments)
+                }
+                if let aRanges = line.attachmentRanges {
+                    attachmentRanges_?.append(contentsOf: aRanges)
+                }
+                if let aRects = line.attachmentRects {
+                    attachmentRects_?.append(contentsOf: aRects)
+                }
+                for attachment in line.attachments! {
+                    if let aContent = attachment.content {
+                        attachmentContentsSet_!.insert(aContent as! AnyHashable)
+                    }
+                }
+            }
+        }
+        if attachments_!.count == 0 {
+            attachmentRects_ = nil
+            attachmentRanges_ = nil
+            attachments_ = nil
+        }
+        
+        self.frameSetter = ctSetter
+        self.frame = ctFrame
+        self.lines = lines_
+        self.truncatedLine = truncatedLine_
+        self.attachments = attachments_
+        self.attachmentRanges = attachmentRanges_
+        self.attachmentRects = attachmentRects_
+        self.attachmentContentsSet = attachmentContentsSet_
+        self.rowCount = rowCount_
+        self.visibleRange = visibleRange_
+        self.textBoundingRect = textBoundingRect_
+        self.textBoundingSize = textBoundingSize_
+        self.lineRowsEdge = lineRowsEdge_
+        self.lineRowsIndex = lineRowsIndex_
+        
+        lineOrigins?.deallocate()
+    }
+    
+    @objc(layoutWithContainers:text:)
+    public class func layout(with containers: [TextContainer]?, text: NSAttributedString?) -> [TextLayout]? {
+        return self.layout(with: containers, text: text, range: NSRange(location: 0, length: text?.length ?? 0))
+    }
+    
+    @objc(layoutWithContainers:text:range:)
+    public class func layout(with containers: [TextContainer]?, text: NSAttributedString?, range: NSRange) -> [TextLayout]? {
+        guard let c = containers, let t = text else {
+            return nil
+        }
+        if range.location + range.length > t.length {
+            return nil
+        }
+        var range = range
+        var layouts: [TextLayout] = []
+        let maxCount = c.count
+        for i in 0..<maxCount {
+            let container = c[i]
+            
+            guard let layout = TextLayout.init(container: container, text: text, range: range) else {
+                return nil
+            }
+            let length = range.length - layout.visibleRange.length
+            if length <= 0 {
+                range.length = 0
+                range.location = t.length
+            } else {
+                range.length = length
+                range.location += layout.visibleRange.length
+            }
+            layouts.append(layout)
+        }
+        return layouts
+    }
+    
+    
+    // MARK: - Coding
+    public func encode(with aCoder: NSCoder) {
+        var textData: Data? = nil
+        if let aText = text {
+            textData = TextArchiver.archivedData(withRootObject: aText)
+        }
+        aCoder.encode(textData, forKey: "text")
+        aCoder.encode(container, forKey: "container")
+        aCoder.encode(NSValue(range: range), forKey: "range")
+    }
+    
+    required convenience public init?(coder aDecoder: NSCoder) {
+        let textData = aDecoder.decodeObject(forKey: "text") as? Data
+        var text: NSAttributedString? = nil
+        if let aData = textData {
+            text = TextUnarchiver.unarchiveObject(with: aData) as? NSAttributedString
+        }
+        let container = aDecoder.decodeObject(forKey: "container") as? TextContainer
+        let range: NSRange = ((aDecoder.decodeObject(forKey: "range") as? NSValue)?.rangeValue)!
+        self.init(container: container, text: text, range: range)
+    }
+    
+    // MARK: - Copying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        return self // readonly object
+    }
+    
+    // MARK: - Query
+    /**
+     Get the row index with 'edge' distance.
+     
+     @param edge  The distance from edge to the point.
+     If vertical form, the edge is left edge, otherwise the edge is top edge.
+     
+     @return Returns NSNotFound if there's no row at the point.
+     */
+    private func _rowIndex(for edge: CGFloat) -> Int {
+        if rowCount == 0 {
+            return NSNotFound
+        }
+        let isVertical = container.isVerticalForm
+        var lo: Int = 0, hi: Int = rowCount - 1, mid: Int = 0
+        var rowIdx: Int = NSNotFound
+        while lo <= hi {
+            mid = (lo + hi) / 2
+            let oneEdge = lineRowsEdge![mid]
+            if (isVertical ? (oneEdge.foot <= edge && edge <= oneEdge.head) : (oneEdge.head <= edge && edge <= oneEdge.foot)) {
+                rowIdx = mid
+                break
+            }
+            if (isVertical ? (edge > oneEdge.head) : (edge < oneEdge.head)) {
+                if mid == 0 {
+                    break
+                }
+                hi = mid - 1
+            } else {
+                lo = mid + 1
+            }
+        }
+        return rowIdx
+    }
+    
+    /**
+     Get the closest row index with 'edge' distance.
+     
+     @param edge  The distance from edge to the point.
+     If vertical form, the edge is left edge, otherwise the edge is top edge.
+     
+     @return Returns NSNotFound if there's no line.
+     */
+    private func _closestRowIndex(forEdge edge: CGFloat) -> Int {
+        if rowCount == 0 {
+            return NSNotFound
+        }
+        var rowIdx = _rowIndex(for: edge)
+        if rowIdx == NSNotFound {
+            if container.isVerticalForm {
+                if edge > lineRowsEdge![0].head {
+                    rowIdx = 0
+                } else if edge < lineRowsEdge![rowCount - 1].foot {
+                    rowIdx = rowCount - 1
+                }
+            } else {
+                if edge < lineRowsEdge![0].head {
+                    rowIdx = 0
+                } else if edge > lineRowsEdge![rowCount - 1].foot {
+                    rowIdx = rowCount - 1
+                }
+            }
+        }
+        return rowIdx
+    }
+    
+    /**
+     Get a CTRun from a line position.
+     
+     @param line     The text line.
+     @param position The position in the whole text.
+     
+     @return Returns NULL if not found (no CTRun at the position).
+     */
+    private func _run(for line: TextLine?, position: TextPosition?) -> CTRun? {
+        if line == nil || position == nil {
+            return nil
+        }
+        let runs = CTLineGetGlyphRuns((line?.ctLine)!)
+        var i = 0, max = CFArrayGetCount(runs)
+        while i < max {
+            let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, i), to: CTRun.self)
+            let range: CFRange = CTRunGetStringRange(run)
+            if position?.affinity == TextAffinity.backward {
+                if range.location < position?.offset ?? 0 && position?.offset ?? 0 <= range.location + range.length {
+                    return run
+                }
+            } else {
+                if range.location <= position?.offset ?? 0 && position?.offset ?? 0 < range.location + range.length {
+                    return run
+                }
+            }
+            i += 1
+        }
+        return nil
+    }
+    
+    /**
+     Whether the position is inside a composed character sequence.
+     
+     @param line     The text line.
+     @param position Text text position in whole text.
+     @param block    The block to be executed before returns YES.
+     left:  left X offset
+     right: right X offset
+     prev:  left position
+     next:  right position
+     */
+    @discardableResult
+    private func _insideComposedCharacterSequences(_ line: TextLine?, position: Int, block: @escaping (_ `left`: CGFloat, _ `right`: CGFloat, _ prev: Int, _ next: Int) -> Void) -> Bool {
+        
+        guard let range = line?.range else {
+            return false
+        }
+        if range.length == 0 {
+            return false
+        }
+        var inside = false
+        var _prev: Int = 0
+        var _next: Int = 0
+        
+        guard let s = text?.string, let r = Range(range, in: s) else {
+            return false
+        }
+        s.enumerateSubstrings(in: r, options: NSString.EnumerationOptions.byComposedCharacterSequences, { substring, substringRange, enclosingRange, stop in
+            let tmpr = NSRange(substringRange, in: s)
+            let prev = tmpr.location
+            let next = tmpr.location + tmpr.length
+            if prev == position || next == position {
+                stop = true
+            }
+            if prev < position && position < next {
+                inside = true
+                _prev = prev
+                _next = next
+                stop = true
+            }
+        })
+        if inside {
+            let `left` = offset(for: _prev, lineIndex: line!.index)
+            let `right` = offset(for: _next, lineIndex: line!.index)
+            block(`left`, `right`, _prev, _next)
+        }
+        return inside
+    }
+    
+    /**
+     Whether the position is inside an emoji (such as National Flag Emoji).
+     
+     @param line     The text line.
+     @param position Text text position in whole text.
+     @param block    Yhe block to be executed before returns YES.
+     left:  emoji's left X offset
+     right: emoji's right X offset
+     prev:  emoji's left position
+     next:  emoji's right position
+     */
+    @discardableResult
+    private func _insideEmoji(_ line: TextLine?, position: Int, block: @escaping (_ `left`: CGFloat, _ `right`: CGFloat, _ prev: Int, _ next: Int) -> Void) -> Bool {
+        
+        if line == nil {
+            return false
+        }
+        let runs = CTLineGetGlyphRuns(line!.ctLine!)
+        let rMax = CFArrayGetCount(runs)
+        for r in 0..<rMax {
+            let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+            let glyphCount = CTRunGetGlyphCount(run)
+            if glyphCount == 0 {
+                continue
+            }
+            let range: CFRange = CTRunGetStringRange(run)
+            if range.length <= 1 {
+                continue
+            }
+            if position <= range.location || position >= range.location + range.length {
+                continue
+            }
+            let attrs = CTRunGetAttributes(run) as! [String: AnyObject]
+            
+            let font = attrs[kCTFontAttributeName as String] as! CTFont
+            if !TextUtilities.textCTFontContainsColorBitmapGlyphs(font) {
+                continue
+            }
+            // Here's Emoji runs (larger than 1 unichar), and position is inside the range.
+            let indices = UnsafeMutablePointer<CFIndex>.allocate(capacity: glyphCount)
+            CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), indices)
+            for g in 0..<glyphCount {
+                let prev: CFIndex = indices[g]
+                let next: CFIndex = g + 1 < glyphCount ? indices[g + 1] : range.location + range.length
+                if position == prev {
+                    break // Emoji edge
+                }
+                if prev < position && position < next {
+                    // inside an emoji (such as National Flag Emoji)
+                    var pos = CGPoint.zero
+                    var adv = CGSize.zero
+                    CTRunGetPositions(run, CFRangeMake(g, 1), &pos)
+                    CTRunGetAdvances(run, CFRangeMake(g, 1), &adv)
+                    //if block
+                    block(line?.position.x ?? 0 + pos.x, line?.position.x ?? 0 + pos.x + adv.width, prev, next)
+                    
+                    return true
+                }
+            }
+            indices.deallocate()
+        }
+        return false
+    }
+    
+    /**
+     Whether the write direction is RTL at the specified point
+     
+     @param line  The text line
+     @param point The point in layout.
+     
+     @return YES if RTL.
+     */
+    private func _isRightToLeft(in line: TextLine?, at point: CGPoint) -> Bool {
+        if line == nil {
+            return false
+        }
+        // get write direction
+        var RTL = false
+        let runs = CTLineGetGlyphRuns(line!.ctLine!)
+        var r = 0, max = CFArrayGetCount(runs)
+        while r < max {
+            let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+            var glyphPosition = CGPoint.zero
+            CTRunGetPositions(run, CFRangeMake(0, 1), &glyphPosition)
+            if container.isVerticalForm {
+                var runX: CGFloat = glyphPosition.x
+                runX += line?.position.y ?? 0
+                let runWidth = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nil, nil, nil))
+                if runX <= point.y && point.y <= runX + runWidth {
+                    if CTRunGetStatus(run).rawValue & CTRunStatus.rightToLeft.rawValue != 0 {
+                        RTL = true
+                    }
+                    break
+                }
+            } else {
+                var runX: CGFloat = glyphPosition.x
+                runX += line?.position.x ?? 0
+                let runWidth = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nil, nil, nil))
+                if runX <= point.x && point.x <= runX + runWidth {
+                    if CTRunGetStatus(run).rawValue & CTRunStatus.rightToLeft.rawValue != 0 {
+                        RTL = true
+                    }
+                    break
+                }
+            }
+            r += 1
+        }
+        return RTL
+    }
+    
+    /**
+     Correct the range's edge.
+     */
+    private func _correctedRange(withEdge range: TextRange) -> TextRange? {
+        var range = range
+        let visibleRange = self.visibleRange
+        var start = range.start
+        var end = range.end
+        if start.offset == visibleRange.location && start.affinity == TextAffinity.backward {
+            start = TextPosition.position(with: start.offset, affinity: TextAffinity.forward)
+        }
+        if end.offset == visibleRange.location + visibleRange.length && start.affinity == TextAffinity.forward {
+            end = TextPosition.position(with: end.offset, affinity: TextAffinity.backward)
+        }
+        if start != range.start || end != range.end {
+            range = TextRange.range(with: start, end: end)
+        }
+        return range
+    }
+
+    
+    // MARK: - Query information from text layout
+    
+    ///=============================================================================
+    /// @name Query information from text layout
+    ///=============================================================================
+    
+    /**
+     The first line index for row.
+     
+     @param row  A row index.
+     @return The line index, or NSNotFound if not found.
+     */
+    @objc(lineIndexForRow:)
+    public func lineIndex(for row: Int) -> Int {
+        if row >= rowCount {
+            return NSNotFound
+        }
+        return lineRowsIndex![row]
+    }
+    
+    /**
+     The number of lines for row.
+     
+     @param row  A row index.
+     @return The number of lines, or NSNotFound when an error occurs.
+     */
+    @objc(lineCountForRow:)
+    public func lineCount(for row: Int) -> Int {
+        if (row >= self.rowCount) { return NSNotFound }
+        if (row == self.rowCount - 1) {
+            return self.lines.count - lineRowsIndex![row]
+        } else {
+            return lineRowsIndex![row + 1] - lineRowsIndex![row]
+        }
+    }
+    
+    /**
+     The row index for line.
+     
+     @param line A row index.
+     
+     @return The row index, or NSNotFound if not found.
+     */
+    public func rowIndex(for line: Int) -> Int {
+        if line >= lines.count {
+            return NSNotFound
+        }
+        return lines[line].row
+    }
+    
+    /**
+     The line index for a specified point.
+     
+     @discussion It returns NSNotFound if there's no text at the point.
+     
+     @param point  A point in the container.
+     @return The line index, or NSNotFound if not found.
+     */
+    @objc(lineIndexForPoint:)
+    public func lineIndex(for point: CGPoint) -> Int {
+        if lines.count == 0 || rowCount == 0 {
+            return NSNotFound
+        }
+        let rowIdx: Int = _rowIndex(for: container.isVerticalForm ? point.x : point.y)
+        if rowIdx == NSNotFound {
+            return NSNotFound
+        }
+        let lineIdx0: Int = lineRowsIndex![rowIdx]
+        let lineIdx1: Int = (rowIdx == (rowCount - 1)) ? (lines.count - 1) : (lineRowsIndex![rowIdx + 1] - 1)
+        for i in lineIdx0...lineIdx1 {
+            let bounds = lines[i].bounds
+            if bounds.contains(point) {
+                return i
+            }
+        }
+        return NSNotFound
+    }
+    
+    /**
+     The line index closest to a specified point.
+     
+     @param point  A point in the container.
+     @return The line index, or NSNotFound if no line exist in layout.
+     */
+    @objc(closestLineIndexForPoint:)
+    public func closestLineIndex(for point: CGPoint) -> Int {
+        let isVertical = container.isVerticalForm
+        if lines.count == 0 || rowCount == 0 {
+            return NSNotFound
+        }
+        let rowIdx: Int = _closestRowIndex(forEdge: isVertical ? point.x : point.y)
+        if rowIdx == NSNotFound {
+            return NSNotFound
+        }
+        let lineIdx0: Int = lineRowsIndex![rowIdx]
+        let lineIdx1: Int = (rowIdx == rowCount - 1) ? (lines.count - 1) : (lineRowsIndex![rowIdx + 1] - 1)
+        if lineIdx0 == lineIdx1 {
+            return lineIdx0
+        }
+        var minDistance: CGFloat = CGFloat.greatestFiniteMagnitude
+        var minIndex: Int = lineIdx0
+        for i in lineIdx0...lineIdx1 {
+            let bounds = lines[i].bounds
+            if isVertical {
+                if bounds.origin.y <= point.y && point.y <= bounds.origin.y + bounds.size.height {
+                    return i
+                }
+                var distance: CGFloat = 0
+                if point.y < bounds.origin.y {
+                    distance = bounds.origin.y - point.y
+                } else {
+                    distance = point.y - (bounds.origin.y + bounds.size.height)
+                }
+                if distance < minDistance {
+                    minDistance = distance
+                    minIndex = i
+                }
+            } else {
+                if bounds.origin.x <= point.x && point.x <= bounds.origin.x + bounds.size.width {
+                    return i
+                }
+                var distance: CGFloat = 0
+                if point.x < bounds.origin.x {
+                    distance = bounds.origin.x - point.x
+                } else {
+                    distance = point.x - (bounds.origin.x + bounds.size.width)
+                }
+                if distance < minDistance {
+                    minDistance = distance
+                    minIndex = i
+                }
+            }
+        }
+        return minIndex
+    }
+    
+    /**
+     The offset in container for a text position in a specified line.
+     
+     @discussion The offset is the text position's baseline point.x.
+     If the container is vertical form, the offset is the baseline point.y;
+     
+     @param textPosition   The text position in string.
+     @param lineIndex  The line index.
+     @return The offset in container, or CGFloat.greatestFiniteMagnitude if not found.
+     */
+    @objc(offsetForTextPosition:lineIndex:)
+    public func offset(for textPosition: Int, lineIndex: Int) -> CGFloat {
+        if lineIndex >= lines.count {
+            return CGFloat.greatestFiniteMagnitude
+        }
+        let position = textPosition
+        let line = lines[lineIndex]
+        let range: CFRange = CTLineGetStringRange(line.ctLine!)
+        if position < range.location || position > range.location + range.length {
+            return CGFloat.greatestFiniteMagnitude
+        }
+        let offset: CGFloat = CTLineGetOffsetForStringIndex(line.ctLine!, position, nil)
+        return container.isVerticalForm ? (offset + line.position.y) : (offset + line.position.x)
+    }
+    
+    /**
+     The text position for a point in a specified line.
+     
+     @discussion This method just call CTLineGetStringIndexForPosition() and does
+     NOT consider the emoji, line break character, binding text...
+     
+     @param point      A point in the container.
+     @param lineIndex  The line index.
+     @return The text position, or NSNotFound if not found.
+     */
+    @objc(textPositionForPoint:lineIndex:)
+    public func textPosition(for point: CGPoint, lineIndex: Int) -> Int {
+        if lineIndex >= lines.count {
+            return NSNotFound
+        }
+        var point = point
+        let line = lines[lineIndex]
+        if container.isVerticalForm {
+            point.x = point.y - line.position.y
+            point.y = 0
+        } else {
+            point.x -= line.position.x
+            point.y = 0
+        }
+        var idx: CFIndex = CTLineGetStringIndexForPosition(line.ctLine!, point)
+        if idx == kCFNotFound {
+            return NSNotFound
+        }
+        
+        /*
+         If the emoji contains one or more variant form (such as ☔️ "\u2614\uFE0F")
+         and the font size is smaller than 379/15, then each variant form ("\uFE0F")
+         will rendered as a single blank glyph behind the emoji glyph. Maybe it's a
+         bug in CoreText? Seems iOS8.3 fixes this problem.
+         
+         If the point hit the blank glyph, the CTLineGetStringIndexForPosition()
+         returns the position before the emoji glyph, but it should returns the
+         position after the emoji and variant form.
+         
+         Here's a workaround.
+         */
+        let runs = CTLineGetGlyphRuns(line.ctLine!)
+        var r = 0, max = CFArrayGetCount(runs)
+        while r < max {
+            let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+            let range: CFRange = CTRunGetStringRange(run)
+            if range.location <= idx && idx < range.location + range.length {
+                let glyphCount: Int = CTRunGetGlyphCount(run)
+                if glyphCount == 0 {
+                    break
+                }
+                let attrs = CTRunGetAttributes(run) as! [String: AnyObject]
+                let font = attrs[kCTFontAttributeName as String] as! CTFont
+                if !TextUtilities.textCTFontContainsColorBitmapGlyphs(font) {
+                    break
+                }
+                let indices = UnsafeMutablePointer<CFIndex>.allocate(capacity: glyphCount)
+                let positions = UnsafeMutablePointer<CGPoint>.allocate(capacity: glyphCount)
+                CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), indices)
+                CTRunGetPositions(run, CFRangeMake(0, glyphCount), positions)
+                for g in 0..<glyphCount {
+                    let gIdx: Int = indices[g]
+                    if gIdx == idx && g + 1 < glyphCount {
+                        let `right`: CGFloat = positions[g + 1].x
+                        if point.x < `right` {
+                            break
+                        }
+                        var next: Int = indices[g + 1]
+                        repeat {
+                            if next == range.location + range.length {
+                                break
+                            }
+                            let c = (text!.string as NSString).character(at: next)
+                            if (c == 0xfe0e || c == 0xfe0f) {
+                                // unicode variant form for emoji style
+                                next += 1
+                            } else {
+                                break
+                            }
+                        } while true
+                        if next != indices[g + 1] {
+                            idx = next
+                        }
+                        break
+                    }
+                }
+                indices.deallocate()
+                positions.deallocate()
+                break
+            }
+            r += 1
+        }
+        return idx
+    }
+    
+    /**
+     The closest text position to a specified point.
+     
+     @discussion This method takes into account the restrict of emoji, line break
+     character, binding text and text affinity.
+     
+     @param point  A point in the container.
+     @return A text position, or nil if not found.
+     */
+    @objc(closestPositionToPoint:)
+    public func closestPosition(to point: CGPoint) -> TextPosition? {
+        let isVertical: Bool = container.isVerticalForm
+        var point = point
+        // When call CTLineGetStringIndexForPosition() on ligature such as 'fi',
+        // and the point `hit` the glyph's left edge, it may get the ligature inside offset.
+        // I don't know why, maybe it's a bug of CoreText. Try to avoid it.
+        if isVertical {
+            point.y += 0.00001234
+        } else {
+            point.x += 0.00001234
+        }
+        var lineIndex: Int = closestLineIndex(for: point)
+        if lineIndex == NSNotFound {
+            return nil
+        }
+        var line: TextLine? = lines[lineIndex]
+        var position: Int = textPosition(for: point, lineIndex: lineIndex)
+        if position == NSNotFound {
+            position = line?.range.location ?? 0
+        }
+        if position <= visibleRange.location {
+            return TextPosition.position(with: visibleRange.location, affinity: TextAffinity.forward)
+        } else if position >= visibleRange.location + visibleRange.length {
+            return TextPosition.position(with: visibleRange.location + visibleRange.length, affinity: TextAffinity.backward)
+        }
+        var finalAffinity = TextAffinity.forward
+        var finalAffinityDetected = false
+        // binding range
+        var bindingRange = NSRange(location: 0, length: 0)
+        let binding = text!.attribute(NSAttributedString.Key(rawValue: TextAttribute.textBindingAttributeName), at: position, longestEffectiveRange: &bindingRange, in: NSRange(location: 0, length: text!.length))
+        
+        if let _ = binding, bindingRange.length > 0 {
+            let headLineIdx: Int = self.lineIndex(for: TextPosition.position(with: bindingRange.location))
+            let tailLineIdx: Int = self.lineIndex(for: TextPosition.position(with: bindingRange.location + bindingRange.length, affinity: TextAffinity.backward))
+            if headLineIdx == lineIndex && lineIndex == tailLineIdx {
+                // all in same line
+                let `left` = offset(for: bindingRange.location, lineIndex: lineIndex)
+                let `right` = offset(for: bindingRange.location + bindingRange.length, lineIndex: lineIndex)
+                if `left` != CGFloat.greatestFiniteMagnitude && `right` != CGFloat.greatestFiniteMagnitude {
+                    if container.isVerticalForm {
+                        if abs(Float(point.y - `left`)) < abs(Float(point.y - `right`)) {
+                            position = bindingRange.location
+                            finalAffinity = TextAffinity.forward
+                        } else {
+                            position = bindingRange.location + bindingRange.length
+                            finalAffinity = TextAffinity.backward
+                        }
+                    } else {
+                        if abs(Float(point.x - `left`)) < abs(Float(point.x - `right`)) {
+                            position = bindingRange.location
+                            finalAffinity = TextAffinity.forward
+                        } else {
+                            position = bindingRange.location + bindingRange.length
+                            finalAffinity = TextAffinity.backward
+                        }
+                    }
+                } else if `left` != CGFloat.greatestFiniteMagnitude {
+                    position = Int(`left`)
+                    finalAffinity = TextAffinity.forward
+                } else if `right` != CGFloat.greatestFiniteMagnitude {
+                    position = Int(`right`)
+                    finalAffinity = TextAffinity.backward
+                }
+                finalAffinityDetected = true
+            } else if headLineIdx == lineIndex {
+                let `left`: CGFloat = offset(for: bindingRange.location, lineIndex: lineIndex)
+                if `left` != CGFloat.greatestFiniteMagnitude {
+                    position = bindingRange.location
+                    finalAffinity = TextAffinity.forward
+                    finalAffinityDetected = true
+                }
+            } else if tailLineIdx == lineIndex {
+                let `right`: CGFloat = offset(for: bindingRange.location + bindingRange.length, lineIndex: lineIndex)
+                if `right` != CGFloat.greatestFiniteMagnitude {
+                    position = bindingRange.location + bindingRange.length
+                    finalAffinity = TextAffinity.backward
+                    finalAffinityDetected = true
+                }
+            } else {
+                var onLeft = false
+                var onRight = false
+                if headLineIdx != NSNotFound && tailLineIdx != NSNotFound {
+                    if abs(headLineIdx - lineIndex) < abs(tailLineIdx - lineIndex) {
+                        onLeft = true
+                    } else {
+                        onRight = true
+                    }
+                } else if headLineIdx != NSNotFound {
+                    onLeft = true
+                } else if tailLineIdx != NSNotFound {
+                    onRight = true
+                }
+                if onLeft {
+                    let `left` = offset(for: bindingRange.location, lineIndex: headLineIdx)
+                    if `left` != CGFloat.greatestFiniteMagnitude {
+                        lineIndex = headLineIdx
+                        line = lines[headLineIdx]
+                        position = bindingRange.location
+                        finalAffinity = TextAffinity.forward
+                        finalAffinityDetected = true
+                    }
+                } else if onRight {
+                    let `right` = offset(for: bindingRange.location + bindingRange.length, lineIndex: tailLineIdx)
+                    if `right` != CGFloat.greatestFiniteMagnitude {
+                        lineIndex = tailLineIdx
+                        line = lines[tailLineIdx]
+                        position = bindingRange.location + bindingRange.length
+                        finalAffinity = TextAffinity.backward
+                        finalAffinityDetected = true
+                    }
+                }
+            }
+        }
+        
+        // empty line
+        if line!.range.length == 0 {
+            let behind: Bool = lines.count > 1 && lineIndex == lines.count - 1 //end line
+            return TextPosition.position(with: line!.range.location, affinity: behind ? TextAffinity.backward : TextAffinity.forward)
+        }
+        // detect weather the line is a linebreak token
+        if line!.range.length <= 2 {
+            let r = line!.range
+            let str = text!.string.subString(start: r.location, end: r.location + r.length)
+            if TextUtilities.textIsLinebreakString(str) {
+                // an empty line ("\r", "\n", "\r\n")
+                return TextPosition.position(with: line!.range.location)
+            }
+        }
+        // above whole text frame
+        if lineIndex == 0 && (isVertical ? (point.x > line!.right) : (point.y < line!.top)) {
+            position = 0
+            finalAffinity = TextAffinity.forward
+            finalAffinityDetected = true
+        }
+        // below whole text frame
+        if lineIndex == lines.count - 1 && (isVertical ? (point.x < line!.left) : (point.y > line!.bottom)) {
+            position = line!.range.location + line!.range.length
+            finalAffinity = TextAffinity.backward
+            finalAffinityDetected = true
+        }
+        
+        // There must be at least one non-linebreak char,
+        // ignore the linebreak characters at line end if exists.
+        // There must be at least one non-linebreak char,
+        // ignore the linebreak characters at line end if exists.
+        if position >= line!.range.location + line!.range.length - 1 {
+            if position > line!.range.location {
+                let c1 = (text!.string as NSString).character(at: position - 1)
+                if TextUtilities.textIsLinebreakChar(c1) {
+                    position -= 1
+                    if position > line!.range.location {
+                        let c0 = (text!.string as NSString).character(at: position - 1)
+                        if TextUtilities.textIsLinebreakChar(c0) {
+                            position -= 1
+                        }
+                    }
+                }
+            }
+        }
+        if position == line!.range.location {
+            return TextPosition.position(with: position)
+        }
+        if position == line!.range.location + line!.range.length {
+            return TextPosition.position(with: position, affinity: TextAffinity.backward)
+        }
+        
+        _insideComposedCharacterSequences(line, position: position) { `left`, `right`, prev, next in
+            if isVertical {
+                position = (abs(Float(`left` - point.y)) < abs(Float(`right` - point.y))) && (abs(Float(`right` - point.y)) < Float(`right` != 0 ? prev : next)) ? 1 : 0
+            } else {
+                position = (abs(Float(`left` - point.x)) < abs(Float(`right` - point.x))) && (abs(Float(`right` - point.x)) < Float(`right` != 0 ? prev : next)) ? 1 : 0
+            }
+        }
+        
+        _insideEmoji(line, position: position) { `left`, `right`, prev, next in
+            if isVertical {
+                position = (abs(Float(`left` - point.y)) < abs(Float(`right` - point.y))) && (abs(Float(`right` - point.y)) < Float(`right` != 0 ? prev : next)) ? 1 : 0
+            } else {
+                position = (abs(Float(`left` - point.x)) < abs(Float(`right` - point.x))) && (abs(Float(`right` - point.x)) < Float(`right` != 0 ? prev : next)) ? 1 : 0
+            }
+        }
+        
+        if position < visibleRange.location {
+            position = visibleRange.location
+        } else if position > visibleRange.location + visibleRange.length {
+            position = visibleRange.location + visibleRange.length
+        }
+        if !finalAffinityDetected {
+            let ofs: CGFloat = offset(for: position, lineIndex: lineIndex)
+            if ofs != CGFloat.greatestFiniteMagnitude {
+                let RTL: Bool = _isRightToLeft(in: line, at: point)
+                if position >= line!.range.location + line!.range.length {
+                    finalAffinity = RTL ? TextAffinity.forward : TextAffinity.backward
+                } else if position <= line!.range.location {
+                    finalAffinity = RTL ? TextAffinity.backward : TextAffinity.forward
+                } else {
+                    finalAffinity = (ofs < (isVertical ? point.y : point.x) && !RTL) ? TextAffinity.forward : TextAffinity.backward
+                }
+            }
+        }
+        return TextPosition.position(with: position, affinity: finalAffinity)
+    }
+    
+    /**
+     Returns the new position when moving selection grabber in text view.
+     
+     @discussion There are two grabber in the text selection period, user can only
+     move one grabber at the same time.
+     
+     @param point          A point in the container.
+     @param oldPosition    The old text position for the moving grabber.
+     @param otherPosition  The other position in text selection view.
+     
+     @return A text position, or nil if not found.
+     */
+    @objc(positionForPoint:oldPosition:otherPosition:)
+    public func position(for point: CGPoint, oldPosition: TextPosition?, otherPosition: TextPosition?) -> TextPosition? {
+        guard let old = oldPosition, let other = otherPosition else {
+            return oldPosition
+        }
+        var point = point
+        var newPos = closestPosition(to: point)
+        if newPos == nil {
+            return oldPosition
+        }
+        if newPos?.compare(otherPosition) == old.compare(otherPosition) && newPos?.offset != other.offset {
+            return newPos
+        }
+        let lineIndex: Int = self.lineIndex(for: otherPosition)
+        if lineIndex == NSNotFound {
+            return oldPosition
+        }
+        let line = lines[lineIndex]
+        let vertical = lineRowsEdge![line.row]
+        
+        if container.isVerticalForm {
+            point.x = (vertical.head + vertical.foot) * 0.5
+        } else {
+            point.y = (vertical.head + vertical.foot) * 0.5
+        }
+        newPos = closestPosition(to: point)
+        if newPos?.compare(otherPosition) == old.compare(otherPosition) && newPos?.offset != other.offset {
+            return newPos
+        }
+        
+        if container.isVerticalForm {
+            if old.compare(other) == .orderedAscending {
+                // search backward
+                let range: TextRange? = textRange(byExtending: otherPosition, in: UITextLayoutDirection.up, offset: 1)
+                if range != nil {
+                    return range?.start
+                }
+            } else {
+                // search forward
+                let range: TextRange? = textRange(byExtending: otherPosition, in: UITextLayoutDirection.down, offset: 1)
+                if range != nil {
+                    return range?.end
+                }
+            }
+        } else {
+            if old.compare(other) == .orderedAscending {
+                // search backward
+                let range: TextRange? = textRange(byExtending: otherPosition, in: UITextLayoutDirection.left, offset: 1)
+                if range != nil {
+                    return range?.start
+                }
+            } else {
+                // search forward
+                let range: TextRange? = textRange(byExtending: otherPosition, in: UITextLayoutDirection.right, offset: 1)
+                if range != nil {
+                    return range?.end
+                }
+            }
+        }
+        return oldPosition
+    }
+    
+    
+    /**
+     Returns the character or range of characters that is at a given point in the container.
+     If there is no text at the point, returns nil.
+     
+     @discussion This method takes into account the restrict of emoji, line break
+     character, binding text and text affinity.
+     
+     @param point  A point in the container.
+     @return An object representing a range that encloses a character (or characters)
+     at point. Or nil if not found.
+     */
+    @objc(textRangeAtPoint:)
+    public func textRange(at point: CGPoint) -> TextRange? {
+        let lineIndex: Int = self.lineIndex(for: point)
+        if lineIndex == NSNotFound {
+            return nil
+        }
+        let textPosition: Int = self.textPosition(for: point, lineIndex: lineIndex)
+        if textPosition == NSNotFound {
+            return nil
+        }
+        let pos = self.closestPosition(to: point)
+        if pos == nil {
+            return nil
+        }
+        // get write direction
+        let RTL: Bool = _isRightToLeft(in: lines[lineIndex], at: point)
+        let rect = caretRect(for: pos!)
+        
+        if rect.isNull {
+            return nil
+        }
+        if container.isVerticalForm {
+            let range: TextRange? = textRange(byExtending: pos, in: ((rect.origin.y ) >= point.y && !RTL) ? .up : .down, offset: 1)
+            return range
+        } else {
+            let range: TextRange? = textRange(byExtending: pos, in: ((rect.origin.x ) >= point.x && !RTL) ? .left : .right, offset: 1)
+            return range
+        }
+    }
+    
+    /**
+     Returns the closest character or range of characters that is at a given point in
+     the container.
+     
+     @discussion This method takes into account the restrict of emoji, line break
+     character, binding text and text affinity.
+     
+     @param point  A point in the container.
+     @return An object representing a range that encloses a character (or characters)
+     at point. Or nil if not found.
+     */
+    @objc(closestTextRangeAtPoint:)
+    public func closestTextRange(at point: CGPoint) -> TextRange? {
+        let pos = closestPosition(to: point)
+        if pos == nil {
+            return nil
+        }
+        let lineIndex: Int = self.lineIndex(for: pos)
+        if lineIndex == NSNotFound {
+            return nil
+        }
+        let line = lines[lineIndex]
+        let RTL: Bool = _isRightToLeft(in: line, at: point)
+        let rect = caretRect(for: pos!)
+        
+        if rect.isNull {
+            return nil
+        }
+        var direction: UITextLayoutDirection = .right
+        if pos!.offset >= line.range.location + line.range.length {
+            if direction.rawValue != (RTL ? 1 : 0) {
+                direction = container.isVerticalForm ? .up : .left
+            } else {
+                direction = container.isVerticalForm ? .down : .right
+            }
+        } else if pos!.offset <= line.range.location {
+            if direction.rawValue != (RTL ? 1 : 0) {
+                direction = container.isVerticalForm ? .down : .right
+            } else {
+                direction = container.isVerticalForm ? .up : .left
+            }
+        } else {
+            if container.isVerticalForm {
+                direction = ((rect.origin.y ) >= point.y && !RTL) ? .up : .down
+            } else {
+                direction = ((rect.origin.x ) >= point.x && !RTL) ? .left : .right
+            }
+        }
+        let range: TextRange? = textRange(byExtending: pos, in: direction, offset: 1)
+        return range
+    }
+    
+    /**
+     If the position is inside an emoji, composed character sequences, line break '\\r\\n'
+     or custom binding range, then returns the range by extend the position. Otherwise,
+     returns a zero length range from the position.
+     
+     @param position A text-position object that identifies a location in layout.
+     
+     @return A text-range object that extend the position. Or nil if an error occurs
+     */
+    @objc(textRangeByExtendingPosition:)
+    public func textRange(byExtending position: TextPosition?) -> TextRange? {
+        
+        let visibleStart: Int = visibleRange.location
+        let visibleEnd: Int = visibleRange.location + visibleRange.length
+        guard let p = position, p.offset >= visibleStart, p.offset <= visibleEnd else {
+            return nil
+        }
+        
+        // head or tail, returns immediately
+        if p.offset == visibleStart {
+            return TextRange.range(with: NSRange(location: p.offset, length: 0))
+        } else if p.offset == visibleEnd {
+            return TextRange.range(with: NSRange(location: p.offset, length: 0), affinity: TextAffinity.backward)
+        }
+        
+        // binding range
+        var tRange = NSRange(location: 0, length: 0)
+        let binding = text!.attribute(NSAttributedString.Key(rawValue: TextAttribute.textBindingAttributeName), at: p.offset, longestEffectiveRange: &tRange, in: visibleRange)
+        if binding != nil && tRange.length != 0 && tRange.location < p.offset {
+            return TextRange.range(with: tRange)
+        }
+        // inside emoji or composed character sequences
+        let lineIndex: Int = self.lineIndex(for: position)
+        
+        if lineIndex != NSNotFound {
+            var _prev: Int = 0
+            var _next: Int = 0
+            var emoji = false
+            var seq = false
+            let line = lines[lineIndex]
+            emoji = _insideEmoji(line, position: p.offset, block: { `left`, `right`, prev, next in
+                _prev = prev
+                _next = next
+            })
+            if !emoji {
+                seq = _insideComposedCharacterSequences(line, position: p.offset, block: { `left`, `right`, prev, next in
+                    _prev = prev
+                    _next = next
+                })
+            }
+            if emoji || seq {
+                return TextRange.range(with: NSRange(location: _prev, length:_next - _prev))
+            }
+        }
+        
+        // inside linebreak '\r\n'
+        if p.offset > visibleStart && p.offset < visibleEnd {
+            
+            let c0 = (text!.string as NSString).character(at: p.offset - 1)
+            if (c0 == ("\r" as NSString).character(at: 0)) && p.offset < visibleEnd {
+                let c1 = (text!.string as NSString).character(at: p.offset)
+                if c1 == ("\n" as NSString).character(at: 0) {
+                    return TextRange.range(with: TextPosition.position(with: p.offset - 1), end: TextPosition.position(with: p.offset + 1))
+                }
+            }
+            if TextUtilities.textIsLinebreakChar(c0) && p.affinity == TextAffinity.backward {
+                let str = (text!.string as NSString).substring(to: p.offset)
+                let len: Int = TextUtilities.textLinebreakTailLength(str)
+                return TextRange.range(with: TextPosition.position(with: p.offset - len), end: TextPosition.position(with: p.offset))
+            }
+        }
+        
+        return TextRange.range(with: NSRange(location: p.offset, length: 0), affinity: p.affinity)
+    }
+    
+    /**
+     Returns a text range at a given offset in a specified direction from another
+     text position to its farthest extent in a certain direction of layout.
+     
+     @param position  A text-position object that identifies a location in layout.
+     @param direction A constant that indicates a direction of layout (right, left, up, down).
+     @param offset    A character offset from position.
+     
+     @return A text-range object that represents the distance from position to the
+     farthest extent in direction. Or nil if an error occurs.
+     */
+    @objc(textRangeByExtendingPosition:inDirection:offset:)
+    public func textRange(byExtending position: TextPosition?, in direction: UITextLayoutDirection, offset: Int) -> TextRange? {
+        
+        let visibleStart: Int = visibleRange.location
+        let visibleEnd: Int = visibleRange.location + visibleRange.length
+        guard let p = position, p.offset >= visibleStart, p.offset <= visibleEnd else {
+            return nil
+        }
+        if offset == 0 {
+            return textRange(byExtending: position)
+        }
+        var offset = offset
+        
+        let isVerticalForm = container.isVerticalForm
+        var verticalMove = false
+        var forwardMove = false
+        if isVerticalForm {
+            verticalMove = direction == .left || direction == .right
+            forwardMove = direction == .left || direction == .down
+        } else {
+            verticalMove = direction == .up || direction == .down
+            forwardMove = direction == .down || direction == .right
+        }
+        if offset < 0 {
+            forwardMove = !forwardMove
+            offset = -offset
+        }
+        // head or tail, returns immediately
+        if !forwardMove && p.offset == visibleStart {
+            return TextRange.range(with: NSRange(location: visibleRange.location, length: 0))
+        } else if forwardMove && p.offset == visibleEnd {
+            return TextRange.range(with: NSRange(location: p.offset, length: 0), affinity: TextAffinity.backward)
+        }
+
+        // extend from position
+        guard let fromRange = textRange(byExtending: p) else {
+            return nil
+        }
+        let allForward = TextRange.range(with: fromRange.start, end: TextPosition.position(with: visibleEnd))
+        let allBackward = TextRange.range(with: TextPosition.position(with: visibleStart), end: fromRange.end)
+        
+        if (verticalMove) { // up/down in text layout
+            
+            let lineIndex: Int = self.lineIndex(for: position)
+            if lineIndex == NSNotFound {
+                return nil
+            }
+            let line = lines[lineIndex]
+            let moveToRowIndex = line.row + (forwardMove ? offset : -offset)
+            if moveToRowIndex < 0 {
+                return allBackward
+            } else if moveToRowIndex >= Int(rowCount) {
+                return allForward
+            }
+            let ofs: CGFloat = self.offset(for: p.offset, lineIndex: lineIndex)
+            if ofs == CGFloat.greatestFiniteMagnitude {
+                return nil
+            }
+            let moveToLineFirstIndex: Int = self.lineIndex(for: moveToRowIndex)
+            let moveToLineCount: Int = lineCount(for: moveToRowIndex)
+            if moveToLineFirstIndex == NSNotFound || moveToLineCount == NSNotFound || moveToLineCount == 0 {
+                return nil
+            }
+            var mostLeft: CGFloat = CGFloat.greatestFiniteMagnitude
+            var mostRight: CGFloat = -CGFloat.greatestFiniteMagnitude
+            var mostLeftLine = TextLine()
+            var mostRightLine = TextLine()
+            var insideIndex: Int = NSNotFound
+            
+            for i in 0..<moveToLineCount {
+                let lineIndex: Int = moveToLineFirstIndex + i
+                let line = lines[lineIndex]
+                if isVerticalForm {
+                    if line.top <= ofs && ofs <= line.bottom {
+                        insideIndex = line.index
+                        break
+                    }
+                    if line.top < mostLeft {
+                        mostLeft = line.top
+                        mostLeftLine = line
+                    }
+                    if line.bottom > mostRight {
+                        mostRight = line.bottom
+                        mostRightLine = line
+                    }
+                } else {
+                    if line.left <= ofs && ofs <= line.right {
+                        insideIndex = line.index
+                        break
+                    }
+                    if line.left < mostLeft {
+                        mostLeft = line.left
+                        mostLeftLine = line
+                    }
+                    if line.right > mostRight {
+                        mostRight = line.right
+                        mostRightLine = line
+                    }
+                }
+            }
+            
+            var afinityEdge = false
+            if insideIndex == NSNotFound {
+                if ofs <= mostLeft {
+                    insideIndex = mostLeftLine.index
+                } else {
+                    insideIndex = mostRightLine.index
+                }
+                afinityEdge = true
+            }
+            let insideLine = lines[insideIndex]
+            var pos: Int = 0
+            if isVerticalForm {
+                pos = textPosition(for: CGPoint(x: insideLine.position.x, y: ofs), lineIndex: insideIndex)
+            } else {
+                pos = textPosition(for: CGPoint(x: ofs, y: insideLine.position.y), lineIndex: insideIndex)
+            }
+            if pos == NSNotFound {
+                return nil
+            }
+            var extPos: TextPosition?
+            
+            if afinityEdge {
+                if pos == insideLine.range.location + insideLine.range.length {
+                    let subStr = text!.string.subString(start: insideLine.range.location, end:insideLine.range.location + insideLine.range.length)
+                    let lineBreakLen: Int = TextUtilities.textLinebreakTailLength(subStr)
+                    extPos = TextPosition.position(with: pos - lineBreakLen)
+                } else {
+                    extPos = TextPosition.position(with: pos)
+                }
+            } else {
+                extPos = TextPosition.position(with: pos)
+            }
+            
+            guard let ext = textRange(byExtending: extPos) else {
+                return nil
+            }
+            if forwardMove {
+                return TextRange.range(with: fromRange.start, end: ext.end)
+            } else {
+                return TextRange.range(with: ext.start, end: fromRange.end)
+            }
+        } else {
+            let toPosition = TextPosition.position(with: p.offset + (forwardMove ? offset : -offset))
+            if toPosition.offset <= visibleStart {
+                return allBackward
+            } else if toPosition.offset >= visibleEnd {
+                return allForward
+            }
+            
+            guard let toRange = textRange(byExtending: toPosition) else {
+                return nil
+            }
+            let start: Int = min(fromRange.start.offset, toRange.start.offset)
+            let end: Int = max(fromRange.end.offset, toRange.end.offset)
+            return TextRange.range(with: NSRange(location: start, length: end - start))
+        }
+    }
+
+    /**
+     Returns the line index for a given text position.
+     
+     @discussion This method takes into account the text affinity.
+     
+     @param position A text-position object that identifies a location in layout.
+     @return The line index, or NSNotFound if not found.
+     */
+    @objc(lineIndexForPosition:)
+    public func lineIndex(for position: TextPosition?) -> Int {
+        guard let p = position else {
+            return NSNotFound
+        }
+        if lines.count == 0 {
+            return NSNotFound
+        }
+        let location = p.offset
+        var lo: Int = 0
+        var hi: Int = lines.count - 1
+        var mid: Int = 0
+        if position?.affinity == TextAffinity.backward {
+            while lo <= hi {
+                mid = (lo + hi) / 2
+                let line = lines[mid]
+                let range = line.range
+                if (range.location < location) && (location <= (range.location + range.length)) {
+                    return mid
+                }
+                if location <= range.location {
+                    hi = mid - 1
+                } else {
+                    lo = mid + 1
+                }
+            }
+        } else {
+            while lo <= hi {
+                mid = (lo + hi) / 2
+                let line = lines[mid]
+                let range = line.range
+                if (range.location <= location) && (location < (range.location + range.length)) {
+                    return mid
+                }
+                if location < range.location {
+                    hi = mid - 1
+                } else {
+                    lo = mid + 1
+                }
+            }
+        }
+        return NSNotFound
+    }
+    
+    /**
+     Returns the baseline position for a given text position.
+     
+     @param position An object that identifies a location in the layout.
+     @return The baseline position for text, or CGPointZero if not found.
+     */
+    @objc(linePositionForPosition:)
+    public func linePosition(for position: TextPosition?) -> CGPoint {
+        let lineIndex = self.lineIndex(for: position)
+        if lineIndex == NSNotFound {
+            return CGPoint.zero
+        }
+        let line = lines[lineIndex]
+        let offset = self.offset(for: position!.offset, lineIndex: lineIndex)
+        if offset == CGFloat.greatestFiniteMagnitude {
+            return CGPoint.zero
+        }
+        if container.isVerticalForm {
+            return CGPoint(x: line.position.x, y: offset)
+        } else {
+            return CGPoint(x: offset, y: line.position.y)
+        }
+    }
+    
+    /**
+     Returns a rectangle used to draw the caret at a given insertion point.
+     
+     @param position An object that identifies a location in the layout.
+     @return A rectangle that defines the area for drawing the caret. The width is
+     always zero in normal container, the height is always zero in vertical form container.
+     If not found, it returns CGRectNull.
+     */
+    @objc(caretRectForPosition:)
+    public func caretRect(for position: TextPosition) -> CGRect {
+        let lineIndex = self.lineIndex(for: position)
+        if lineIndex == NSNotFound {
+            return CGRect.null
+        }
+        let line = lines[lineIndex]
+        let offset = self.offset(for: position.offset, lineIndex: lineIndex)
+        if offset == CGFloat.greatestFiniteMagnitude {
+            return CGRect.null
+        }
+        if container.isVerticalForm {
+            return CGRect(x: line.bounds.origin.x, y: offset, width: line.bounds.size.width, height: 0)
+        } else {
+            return CGRect(x: offset, y: line.bounds.origin.y, width: 0, height: line.bounds.size.height)
+        }
+    }
+    
+    /**
+     Returns the first rectangle that encloses a range of text in the layout.
+     
+     @param range An object that represents a range of text in layout.
+     
+     @return The first rectangle in a range of text. You might use this rectangle to
+     draw a correction rectangle. The "first" in the name refers the rectangle
+     enclosing the first line when the range encompasses multiple lines of text.
+     If not found, it returns CGRectNull.
+     */
+    @objc(firstRectForRange:)
+    public func firstRect(for range: TextRange) -> CGRect {
+        var range = range
+        range = _correctedRange(withEdge: range)!
+        let startLineIndex: Int = self.lineIndex(for: range.start)
+        let endLineIndex: Int = self.lineIndex(for: range.end)
+        if startLineIndex == NSNotFound || endLineIndex == NSNotFound {
+            return CGRect.null
+        }
+        if startLineIndex > endLineIndex {
+            return CGRect.null
+        }
+        let startLine = self.lines[startLineIndex]
+        let endLine = self.lines[endLineIndex]
+        var lines_ = [TextLine]()
+        for i in startLineIndex...startLineIndex {
+            let line = self.lines[i]
+            if line.row != startLine.row {
+                break
+            }
+            lines_.append(line)
+        }
+        if container.isVerticalForm {
+            if lines_.count == 1 {
+                var top: CGFloat = self.offset(for: range.start.offset, lineIndex: startLineIndex)
+                var bottom: CGFloat = 0
+                if startLine == endLine {
+                    bottom = self.offset(for: range.end.offset, lineIndex: startLineIndex)
+                } else {
+                    bottom = startLine.bottom
+                }
+                if top == CGFloat.greatestFiniteMagnitude || bottom == CGFloat.greatestFiniteMagnitude {
+                    return CGRect.null
+                }
+                if top > bottom {
+                    (top, bottom) = (bottom, top)
+                }
+                return CGRect(x: startLine.left, y: top, width: startLine.width, height: bottom - top)
+            } else {
+                var top: CGFloat = self.offset(for: range.start.offset, lineIndex: startLineIndex)
+                var bottom: CGFloat = startLine.bottom
+                if top == CGFloat.greatestFiniteMagnitude || bottom == CGFloat.greatestFiniteMagnitude {
+                    return CGRect.null
+                }
+                if top > bottom {
+                    (top, bottom) = (bottom, top)
+                }
+                var rect = CGRect(x: startLine.left, y: top, width: startLine.width, height: bottom - top)
+                for i in 1..<lines_.count {
+                    let line = lines_[i]
+                    rect = rect.union(line.bounds)
+                }
+                return rect
+            }
+        } else {
+            if lines_.count == 1 {
+                var `left`: CGFloat = offset(for: range.start.offset, lineIndex: startLineIndex)
+                var `right`: CGFloat = 0
+                if startLine == endLine {
+                    `right` = offset(for: range.end.offset, lineIndex: startLineIndex)
+                } else {
+                    `right` = startLine.right
+                }
+                if `left` == CGFloat.greatestFiniteMagnitude || `right` == CGFloat.greatestFiniteMagnitude {
+                    return CGRect.null
+                }
+                if `left` > `right` {
+                    (`left`, `right`) = (`right`, `left`)
+                }
+                return CGRect(x: `left`, y: startLine.top, width: `right` - `left`, height: startLine.height)
+            } else {
+                var `left`: CGFloat = offset(for: range.start.offset, lineIndex: startLineIndex)
+                var `right`: CGFloat = startLine.right
+                if `left` == CGFloat.greatestFiniteMagnitude || `right` == CGFloat.greatestFiniteMagnitude {
+                    return CGRect.null
+                }
+                if `left` > `right` {
+                    (`left`, `right`) = (`right`, `left`)
+                }
+                var rect = CGRect(x: `left`, y: startLine.top, width: `right` - `left`, height: startLine.height)
+                for i in 1..<lines_.count {
+                    let line = lines_[i]
+                    rect = rect.union(line.bounds)
+                }
+                return rect
+            }
+        }
+    }
+    
+    /**
+     Returns the rectangle union that encloses a range of text in the layout.
+     
+     @param range An object that represents a range of text in layout.
+     
+     @return A rectangle that defines the area than encloses the range.
+     If not found, it returns CGRectNull.
+     */
+    @objc(rectForRange:)
+    public func rect(for range: TextRange?) -> CGRect {
+        var rects: [UITextSelectionRect]? = nil
+        if let aRange = range {
+            rects = selectionRects(for: aRange)
+        }
+        guard let r = rects, r.count > 0 else {
+            return CGRect.null
+        }
+        var rectUnion = r.first!.rect
+        for rect in r {
+            rectUnion = rectUnion.union(rect.rect)
+        }
+        return rectUnion
+    }
+    
+    /**
+     Returns an array of selection rects corresponding to the range of text.
+     The start and end rect can be used to show grabber.
+     
+     @param range An object representing a range in text.
+     @return An array of `TextSelectionRect` objects that encompass the selection.
+     If not found, the array is empty.
+     */
+    @objc(selectionRectsForRange:)
+    public func selectionRects(for range: TextRange) -> [TextSelectionRect] {
+        
+        let range = _correctedRange(withEdge: range)!
+        let isVertical = container.isVerticalForm
+        var rects: [TextSelectionRect] = []
+        
+        var startLineIndex: Int = lineIndex(for: range.start)
+        var endLineIndex: Int = lineIndex(for: range.end)
+        if startLineIndex == NSNotFound || endLineIndex == NSNotFound {
+            return rects
+        }
+        if startLineIndex > endLineIndex {
+            TextUtilities.numberSwap(&startLineIndex, b: &endLineIndex)
+        }
+        let startLine = lines[startLineIndex]
+        let endLine = lines[endLineIndex]
+        var offsetStart: CGFloat = offset(for: range.start.offset, lineIndex: startLineIndex)
+        var offsetEnd: CGFloat = offset(for: range.end.offset, lineIndex: endLineIndex)
+        let start = TextSelectionRect()
+        if isVertical {
+            start.rect = CGRect(x: startLine.left, y: offsetStart, width: startLine.width, height: 0)
+        } else {
+            start.rect = CGRect(x: offsetStart, y: startLine.top, width: 0, height: startLine.height)
+        }
+        start.containsStart = true
+        start.isVertical = isVertical
+        rects.append(start)
+        let end = TextSelectionRect()
+        if isVertical {
+            end.rect = CGRect(x: endLine.left, y: offsetEnd, width: endLine.width, height: 0)
+        } else {
+            end.rect = CGRect(x: offsetEnd, y: endLine.top, width: 0, height: endLine.height)
+        }
+        end.containsEnd = true
+        end.isVertical = isVertical
+        rects.append(end)
+        
+        if startLine.row == endLine.row {
+            // same row
+            if offsetStart > offsetEnd {
+                TextUtilities.numberSwap(&offsetStart, b: &offsetEnd)
+            }
+            let rect = TextSelectionRect()
+            if isVertical {
+                rect.rect = CGRect(x: startLine.bounds.origin.x, y: offsetStart, width: max(startLine.width, endLine.width), height: offsetEnd - offsetStart)
+            } else {
+                rect.rect = CGRect(x: offsetStart, y: startLine.bounds.origin.y, width: offsetEnd - offsetStart, height: max(startLine.height, endLine.height))
+            }
+            rect.isVertical = isVertical
+            rects.append(rect)
+        } else { // more than one row
+            
+            // start line select rect
+            let topRect = TextSelectionRect()
+            topRect.isVertical = isVertical
+            let topOffset: CGFloat = offset(for: range.start.offset, lineIndex: startLineIndex)
+            let topRun = _run(for: startLine, position: range.start)
+            if topRun != nil && (CTRunGetStatus(topRun!).rawValue & CTRunStatus.rightToLeft.rawValue) != 0 {
+                if isVertical {
+                    topRect.rect = CGRect(x: startLine.left, y: (container.path != nil) ? startLine.top : container.insets.top, width: startLine.width, height: topOffset - startLine.top)
+                } else {
+                    topRect.rect = CGRect(x: (container.path != nil) ? startLine.left : container.insets.left, y: startLine.top, width: topOffset - startLine.left, height: startLine.height)
+                }
+                topRect.writingDirection = UITextWritingDirection.rightToLeft
+            } else {
+                if isVertical {
+                    topRect.rect = CGRect(x: startLine.left, y: topOffset, width: startLine.width, height: ((container.path != nil) ? startLine.bottom : container.size.height - container.insets.bottom) - topOffset)
+                } else {
+                    topRect.rect = CGRect(x: topOffset, y: startLine.top, width: ((container.path != nil) ? startLine.right : container.size.width - container.insets.right) - topOffset, height: startLine.height)
+                }
+            }
+            rects.append(topRect)
+            // end line select rect
+            let bottomRect = TextSelectionRect()
+            bottomRect.isVertical = isVertical
+            let bottomOffset: CGFloat = offset(for: range.end.offset, lineIndex: endLineIndex)
+            let bottomRun = _run(for: endLine, position: range.end)
+            
+            if (bottomRun != nil) && (CTRunGetStatus(bottomRun!).rawValue & CTRunStatus.rightToLeft.rawValue) != 0 {
+                if isVertical {
+                    bottomRect.rect = CGRect(x: endLine.left, y: bottomOffset, width: endLine.width, height: ((container.path != nil) ? endLine.bottom : container.size.height - container.insets.bottom) - bottomOffset)
+                } else {
+                    bottomRect.rect = CGRect(x: bottomOffset, y: endLine.top, width: ((container.path != nil) ? endLine.right : container.size.width - container.insets.right) - bottomOffset, height: endLine.height)
+                }
+                bottomRect.writingDirection = .rightToLeft
+            } else {
+                if isVertical {
+                    let top: CGFloat = (container.path != nil) ? endLine.top : container.insets.top
+                    bottomRect.rect = CGRect(x: endLine.left, y: top, width: endLine.width, height: bottomOffset - top)
+                } else {
+                    let `left`: CGFloat = (container.path != nil) ? endLine.left : container.insets.left
+                    bottomRect.rect = CGRect(x: `left`, y: endLine.top, width: bottomOffset - `left`, height: endLine.height)
+                }
+            }
+            rects.append(bottomRect)
+            
+            if endLineIndex - startLineIndex >= 2 {
+                var r = CGRect.zero
+                var startLineDetected = false
+                for l in startLineIndex + 1..<endLineIndex {
+                    let line = lines[l]
+                    if line.row == startLine.row || line.row == endLine.row {
+                        continue
+                    }
+                    if !startLineDetected {
+                        r = line.bounds
+                        startLineDetected = true
+                    } else {
+                        r = r.union(line.bounds)
+                    }
+                }
+                if startLineDetected {
+                    if isVertical {
+                        if container.path == nil {
+                            r.origin.y = container.insets.top
+                            r.size.height = container.size.height - container.insets.bottom - container.insets.top
+                        }
+                        r.size.width = topRect.rect.minX - bottomRect.rect.maxX
+                        r.origin.x = bottomRect.rect.maxX
+                    } else {
+                        if container.path == nil {
+                            r.origin.x = container.insets.left
+                            r.size.width = container.size.width - container.insets.right - container.insets.left
+                        }
+                        r.origin.y = topRect.rect.maxY
+                        r.size.height = bottomRect.rect.origin.y - r.origin.y
+                    }
+                    let rect = TextSelectionRect()
+                    rect.rect = r
+                    rect.isVertical = isVertical
+                    rects.append(rect)
+                }
+            } else {
+                if isVertical {
+                    var r0: CGRect = bottomRect.rect
+                    var r1: CGRect = topRect.rect
+                    let mid: CGFloat = (r0.maxX + r1.minX) * 0.5
+                    r0.size.width = mid - r0.origin.x
+                    let r1ofs: CGFloat = r1.origin.x - mid
+                    r1.origin.x -= r1ofs
+                    r1.size.width += r1ofs
+                    topRect.rect = r1
+                    bottomRect.rect = r0
+                } else {
+                    var r0: CGRect = topRect.rect
+                    var r1: CGRect = bottomRect.rect
+                    let mid: CGFloat = (r0.maxY + r1.minY) * 0.5
+                    r0.size.height = mid - r0.origin.y
+                    let r1ofs: CGFloat = r1.origin.y - mid
+                    r1.origin.y -= r1ofs
+                    r1.size.height += r1ofs
+                    topRect.rect = r0
+                    bottomRect.rect = r1
+                }
+            }
+        }
+        
+        return rects
+    }
+    
+    /**
+     Returns an array of selection rects corresponding to the range of text.
+     
+     @param range An object representing a range in text.
+     @return An array of `TextSelectionRect` objects that encompass the selection.
+     If not found, the array is empty.
+     */
+    @objc(selectionRectsWithoutStartAndEndForRange:)
+    public func selectionRectsWithoutStartAndEnd(for range: TextRange) -> [TextSelectionRect] {
+        
+        var rects = selectionRects(for: range)
+        var i = 0, max = rects.count
+        while i < max {
+            let rect = rects[i]
+            if rect.containsStart || rect.containsEnd {
+                rects.remove(at: i)
+                i -= 1
+                max -= 1
+            }
+            i += 1
+        }
+        return rects
+    }
+    
+    /**
+     Returns the start and end selection rects corresponding to the range of text.
+     The start and end rect can be used to show grabber.
+     
+     @param range An object representing a range in text.
+     @return An array of `TextSelectionRect` objects contains the start and end to
+     the selection. If not found, the array is empty.
+     */
+    @objc(selectionRectsWithOnlyStartAndEndForRange:)
+    public func selectionRectsWithOnlyStartAndEnd(for range: TextRange) -> [TextSelectionRect] {
+        
+        var rects = selectionRects(for: range)
+        var i = 0, max = rects.count
+        while i < max {
+            let rect = rects[i]
+            if rect.containsStart && rect.containsEnd {
+                rects.remove(at: i)
+                i -= 1
+                max -= 1
+            }
+            i += 1
+        }
+        return rects
+    }
+    
+    // MARK: - Draw text layout
+    ///=============================================================================
+    /// @name Draw text layout
+    ///=============================================================================
+    
+    /**
+     Draw the layout and show the attachments.
+     
+     @discussion If the `view` parameter is not nil, then the attachment views will
+     add to this `view`, and if the `layer` parameter is not nil, then the attachment
+     layers will add to this `layer`.
+     
+     @warning This method should be called on main thread if `view` or `layer` parameter
+     is not nil and there's UIView or CALayer attachments in layout.
+     Otherwise, it can be called on any thread.
+     
+     @param context The draw context. Pass nil to avoid text and image drawing.
+     @param size    The context size.
+     @param point   The point at which to draw the layout.
+     @param view    The attachment views will add to this view.
+     @param layer   The attachment layers will add to this layer.
+     @param debug   The debug option. Pass nil to avoid debug drawing.
+     @param cancel  The cancel checker block. It will be called in drawing progress.
+     If it returns YES, the further draw progress will be canceled.
+     Pass nil to ignore this feature.
+     */
+    @objc(drawInContext:size:point:view:layer:debug:cancel:)
+    public func draw(in context: CGContext?, size: CGSize, point: CGPoint, view: UIView?, layer: CALayer?, debug: TextDebugOption?, cancel: (() -> Bool)? = nil) {
+        
+        if needDrawBlockBorder, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawBlockBorder(self, context: c, size: size, point: point, cancel: cancel)
+        }
+        
+        if needDrawBackgroundBorder, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawBorder(self, context: c, size: size, point: point, type: TextBorderType.backgound, cancel: cancel)
+        }
+        
+        if needDrawShadow, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawShadow(self, context: c, size: size, point: point, cancel: cancel)
+        }
+        
+        if needDrawUnderline, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawDecoration(self, context: c, size: size, point: point, type: TextDecorationType.underline, cancel: cancel)
+        }
+        
+        if needDrawText, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawText(self, context: c, size: size, point: point, cancel: cancel)
+        }
+        
+        if needDrawAttachment && (context != nil || view != nil || layer != nil) {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawAttachment(self, context: context, size: size, point: point, targetView: view, targetLayer: layer, cancel: cancel)
+        }
+        
+        if needDrawInnerShadow, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawInnerShadow(self, context: c, size: size, point: point, cancel: cancel)
+        }
+        
+        if needDrawStrikethrough, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawDecoration(self, context: c, size: size, point: point, type: TextDecorationType.strikethrough, cancel: cancel)
+        }
+        
+        if needDrawBorder, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawBorder(self, context: c, size: size, point: point, type: TextBorderType.normal, cancel: cancel)
+        }
+        
+        if let d = debug?.needDrawDebug, d, let c = context {
+            if let _cancel = cancel, _cancel() { return }
+            TextDrawDebug(self, context: c, size: size, point: point, op: debug)
+        }
+    }
+    
+    /**
+     Draw the layout text and image (without view or layer attachments).
+     
+     @discussion This method is thread safe and can be called on any thread.
+     
+     @param context The draw context. Pass nil to avoid text and image drawing.
+     @param size    The context size.
+     @param debug   The debug option. Pass nil to avoid debug drawing.
+     */
+    @objc(drawInContext:size:debug:)
+    public func draw(in context: CGContext?, size: CGSize, debug: TextDebugOption?) {
+        self.draw(in: context, size: size, point: CGPoint.zero, view: nil, layer: nil, debug: debug, cancel: nil)
+    }
+    
+    /**
+     Show view and layer attachments.
+     
+     @warning This method must be called on main thread.
+     
+     @param view  The attachment views will add to this view.
+     @param layer The attachment layers will add to this layer.
+     */
+    @objc(addAttachmentToView:layer:)
+    public func addAttachment(to view: UIView?, layer: CALayer?) {
+        assert(Thread.isMainThread, "This method must be called on the main thread")
+        self.draw(in: nil, size: CGSize.zero, point: CGPoint.zero, view: view, layer: layer, debug: nil, cancel: nil)
+    }
+    
+    /**
+     Remove attachment views and layers from their super container.
+     
+     @warning This method must be called on main thread.
+     */
+    @objc(removeAttachmentFromViewAndLayer)
+    public func removeAttachmentFromViewAndLayer() {
+        assert(Thread.isMainThread, "This method must be called on the main thread")
+        guard let att = attachments else {
+            return
+        }
+        for a in att {
+            if (a.content is UIView) {
+                let v = a.content! as! UIView
+                v.removeFromSuperview()
+            } else if (a.content is CALayer) {
+                let l = a.content! as! CALayer
+                l.removeFromSuperlayer()
+            }
+        }
+    }
+}
+
+fileprivate struct TextDecorationType : OptionSet {
+    let rawValue: Int
+    static let underline = TextDecorationType(rawValue: 1 << 0)
+    static let strikethrough = TextDecorationType(rawValue: 1 << 1)
+}
+
+fileprivate struct TextBorderType : OptionSet {
+    let rawValue: Int
+    static let backgound = TextBorderType(rawValue: 1 << 0)
+    static let normal = TextBorderType(rawValue: 1 << 1)
+}
+
+private func TextMergeRectInSameLine(rect1: CGRect, rect2: CGRect, isVertical: Bool) -> CGRect {
+    if isVertical {
+        let top = min(rect1.origin.y, rect2.origin.y)
+        let bottom = max(rect1.origin.y + rect1.size.height, rect2.origin.y + rect2.size.height)
+        let width = max(rect1.size.width, rect2.size.width)
+        return CGRect(x: rect1.origin.x, y: top, width: width, height: bottom - top)
+    } else {
+        let `left` = min(rect1.origin.x, rect2.origin.x)
+        let `right` = max(rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width)
+        let height = max(rect1.size.height, rect2.size.height)
+        return CGRect(x: `left`, y: rect1.origin.y, width: `right` - `left`, height: height)
+    }
+}
+
+private func TextGetRunsMaxMetric(runs: CFArray, xHeight: UnsafeMutablePointer<CGFloat>, underlinePosition: UnsafeMutablePointer<CGFloat>?, lineThickness: UnsafeMutablePointer<CGFloat>) {
+    let xHeight = xHeight
+    let underlinePosition = underlinePosition
+    let lineThickness = lineThickness
+    var maxXHeight: CGFloat = 0
+    var maxUnderlinePos: CGFloat = 0
+    var maxLineThickness: CGFloat = 0
+    let max = CFArrayGetCount(runs)
+    for i in 0..<max {
+        let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, i), to: CTRun.self)
+        
+        if let attrs = CTRunGetAttributes(run) as? [String: AnyObject] {
+            if let font = attrs[kCTFontAttributeName as String] as! CTFont? {
+                
+                let xHeight = CTFontGetXHeight(font)
+                if xHeight > maxXHeight {
+                    maxXHeight = xHeight
+                }
+                let underlinePos = CTFontGetUnderlinePosition(font)
+                if underlinePos < maxUnderlinePos {
+                    maxUnderlinePos = underlinePos
+                }
+                let lineThickness = CTFontGetUnderlineThickness(font)
+                if lineThickness > maxLineThickness {
+                    maxLineThickness = lineThickness
+                }
+            }
+        }
+    }
+    if xHeight.pointee != 0 {
+        xHeight.pointee = maxXHeight
+    }
+    if underlinePosition != nil {
+        underlinePosition!.pointee = maxUnderlinePos
+    }
+    if lineThickness.pointee != 0 {
+        lineThickness.pointee = maxLineThickness
+    }
+}
+
+private func TextDrawRun(line: TextLine, run: CTRun, context: CGContext, size: CGSize, isVertical: Bool, runRanges: [TextRunGlyphRange]?, verticalOffset: CGFloat) {
+    
+    let runTextMatrix: CGAffineTransform = CTRunGetTextMatrix(run)
+    let runTextMatrixIsID = runTextMatrix.isIdentity
+    let runAttrs = CTRunGetAttributes(run) as! [String: AnyObject]
+    
+    let glyphTransformValue = runAttrs[TextAttribute.textGlyphTransformAttributeName] as? NSValue
+    
+    if !isVertical && glyphTransformValue == nil {
+        // draw run
+        if !runTextMatrixIsID {
+            context.saveGState()
+            let trans: CGAffineTransform = context.textMatrix
+            context.textMatrix = trans.concatenating(runTextMatrix)
+        }
+        CTRunDraw(run, context, CFRangeMake(0, 0))
+        if !runTextMatrixIsID {
+            context.restoreGState()
+        }
+    } else {
+        
+        guard let runFont = runAttrs[kCTFontAttributeName as String] as! CTFont? else {
+            return
+        }
+        
+        let glyphCount: Int = CTRunGetGlyphCount(run)
+        if glyphCount <= 0 {
+            return
+        }
+        let glyphs = UnsafeMutablePointer<CGGlyph>.allocate(capacity: glyphCount)
+        let glyphPositions = UnsafeMutablePointer<CGPoint>.allocate(capacity: glyphCount)
+        CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs)
+        CTRunGetPositions(run, CFRangeMake(0, 0), glyphPositions)
+        
+        let fillColor = (runAttrs[kCTForegroundColorAttributeName as String] as! CGColor?) ?? UIColor.black.cgColor
+        let strokeWidth = runAttrs[kCTStrokeWidthAttributeName as String] as? Int ?? 0
+        
+        context.saveGState()
+        do {
+            context.setFillColor(fillColor)
+            if strokeWidth == 0 {
+                context.setTextDrawingMode(.fill)
+            } else {
+                
+                var strokeColor = runAttrs[kCTStrokeColorAttributeName as String] as! CGColor?
+                if strokeColor == nil {
+                    strokeColor = fillColor
+                }
+                if let aColor = strokeColor {
+                    context.setStrokeColor(aColor)
+                }
+                context.setLineWidth(CTFontGetSize(runFont) * CGFloat(abs(Float(strokeWidth) * 0.01)))
+                if strokeWidth > 0 {
+                    context.setTextDrawingMode(.stroke)
+                } else {
+                    context.setTextDrawingMode(.fillStroke)
+                }
+            }
+            
+            if isVertical {
+                let runStrIdx = UnsafeMutablePointer<CFIndex>.allocate(capacity: glyphCount + 1)
+                CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx)
+                let runStrRange: CFRange = CTRunGetStringRange(run)
+                runStrIdx[glyphCount] = runStrRange.location + runStrRange.length
+                let glyphAdvances = UnsafeMutablePointer<CGSize>.allocate(capacity: glyphCount)
+                CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances)
+                let ascent: CGFloat = CTFontGetAscent(runFont)
+                let descent: CGFloat = CTFontGetDescent(runFont)
+                let glyphTransform = glyphTransformValue?.cgAffineTransformValue
+                let zeroPoint = UnsafeMutablePointer<CGPoint>.allocate(capacity: 1)
+                zeroPoint.pointee = CGPoint.zero
+                
+                for oneRange in runRanges ?? [] {
+                    let range = oneRange.glyphRangeInRun
+                    let rangeMax = range.location + range.length
+                    let mode: TextRunGlyphDrawMode = oneRange.drawMode
+                    
+                    for g in range.location..<rangeMax {
+                    
+                        context.saveGState()
+                        do {
+                            context.textMatrix = .identity
+                            if glyphTransformValue != nil {
+                                context.textMatrix = glyphTransform!
+                            }
+                            if mode != .horizontal {
+                                // CJK glyph, need rotated
+                                let ofs = (ascent - descent) * 0.5
+                                let w = glyphAdvances[g].width * 0.5
+                                var x = line.position.x + verticalOffset + (glyphPositions + g).pointee.y + (ofs - w)
+                                var y = -line.position.y + size.height - glyphPositions[g].x - (ofs + w)
+                                if mode == TextRunGlyphDrawMode.verticalRotateMove {
+                                    x += w
+                                    y += w
+                                }
+                                context.textPosition = CGPoint(x: x, y: y)
+                            } else {
+                                context.rotate(by: TextUtilities.textRadians(from: (-90)))
+                                context.textPosition = CGPoint(x: line.position.y - size.height + glyphPositions[g].x, y: line.position.x + verticalOffset + glyphPositions[g].y)
+                            }
+                            if TextUtilities.textCTFontContainsColorBitmapGlyphs((runFont)) {
+                                CTFontDrawGlyphs(runFont, glyphs + g, zeroPoint, 1, context)
+                            } else {
+                                let cgFont = CTFontCopyGraphicsFont(runFont, nil)
+                                context.setFont(cgFont)
+                                context.setFontSize(CTFontGetSize(runFont))
+                                context.showGlyphs(Array(UnsafeBufferPointer(start: glyphs + g, count: 1)), at: Array(UnsafeBufferPointer(start: zeroPoint, count: 1)))
+                            }
+                        }
+                        context.restoreGState()
+                    }
+                }
+                
+                runStrIdx.deallocate()
+                glyphAdvances.deallocate()
+                zeroPoint.deallocate()
+                
+            } else {
+                if glyphTransformValue != nil {
+                    let runStrIdx = UnsafeMutablePointer<CFIndex>.allocate(capacity: glyphCount + 1)
+                    CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx)
+                    let runStrRange: CFRange = CTRunGetStringRange(run)
+                    (runStrIdx + glyphCount).pointee = runStrRange.location + runStrRange.length
+                    let glyphAdvances = UnsafeMutablePointer<CGSize>.allocate(capacity: glyphCount)
+                    CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances)
+                    let glyphTransform: CGAffineTransform = glyphTransformValue!.cgAffineTransformValue
+                    let zeroPoint = UnsafeMutablePointer<CGPoint>.allocate(capacity: 1)
+                    zeroPoint.pointee = CGPoint.zero
+                
+                    for g in 0..<glyphCount {
+                        context.saveGState()
+                        do {
+                            context.textMatrix = .identity
+                            context.textMatrix = glyphTransform
+                            context.textPosition = CGPoint(x: line.position.x + glyphPositions[g].x, y: size.height - (line.position.y + glyphPositions[g].y))
+                            if TextUtilities.textCTFontContainsColorBitmapGlyphs((runFont)) {
+                                CTFontDrawGlyphs(runFont, glyphs + g, zeroPoint, 1, context)
+                            } else {
+                                let cgFont = CTFontCopyGraphicsFont(runFont, nil)
+                                context.setFont(cgFont)
+                                context.setFontSize(CTFontGetSize(runFont))
+                                context.showGlyphs(Array(UnsafeBufferPointer(start: glyphs + g, count: 1)), at: Array(UnsafeBufferPointer(start: zeroPoint, count: 1)))
+                            }
+                        }
+                        context.restoreGState()
+                    }
+                    
+                    runStrIdx.deallocate()
+                    glyphAdvances.deallocate()
+                    zeroPoint.deallocate()
+                    
+                } else {
+                    
+                    if TextUtilities.textCTFontContainsColorBitmapGlyphs((runFont)) {
+                        CTFontDrawGlyphs(runFont, glyphs, glyphPositions, glyphCount, context)
+                    } else {
+                        let cgFont = CTFontCopyGraphicsFont(runFont, nil)
+                        context.setFont(cgFont)
+                        context.setFontSize(CTFontGetSize(runFont))
+                        context.showGlyphs(Array(UnsafeBufferPointer(start: glyphs, count: glyphCount)), at: Array(UnsafeBufferPointer(start: glyphPositions, count: glyphCount)))
+                    }
+                }
+            }
+        }
+        context.restoreGState()
+        
+        glyphs.deallocate()
+        glyphPositions.deallocate()
+    }
+}
+
+private func TextSetLinePatternInContext(style: TextLineStyle, width: CGFloat, phase: CGFloat, context: CGContext) {
+    
+    context.setLineWidth(width)
+    context.setLineCap(CGLineCap.butt)
+    context.setLineJoin(CGLineJoin.miter)
+    let dash: CGFloat = 12
+    let dot: CGFloat = 5
+    let space: CGFloat = 3
+    let pattern = style.rawValue & 0xf00
+    if pattern == TextLineStyle.none.rawValue {
+        // TextLineStylePatternSolid
+        context.setLineDash(phase: phase, lengths: [])
+    } else if pattern == TextLineStyle.patternDot.rawValue {
+        let lengths = [width * dot, width * space]
+        context.setLineDash(phase: phase, lengths: lengths)
+    } else if pattern == TextLineStyle.patternDash.rawValue {
+        let lengths = [width * dash, width * space]
+        context.setLineDash(phase: phase, lengths: lengths)
+    } else if pattern == TextLineStyle.patternDashDot.rawValue {
+        let lengths = [width * dash, width * space, width * dot, width * space]
+        context.setLineDash(phase: phase, lengths: lengths)
+    } else if pattern == TextLineStyle.patternDashDotDot.rawValue {
+        let lengths = [width * dash, width * space, width * dot, width * space, width * dot, width * space]
+        context.setLineDash(phase: phase, lengths: lengths)
+    } else if pattern == TextLineStyle.patternCircleDot.rawValue {
+        let lengths = [width * 0, width * 3]
+        context.setLineDash(phase: phase, lengths: lengths)
+        context.setLineCap(CGLineCap.round)
+        context.setLineJoin(CGLineJoin.round)
+    }
+}
+
+private func TextDrawBorderRects(context: CGContext, size: CGSize, border: TextBorder, rects: [NSValue], isVertical: Bool) {
+    
+    if rects.count == 0 {
+        return
+    }
+    
+    if let shadow = border.shadow, let color = shadow.color {
+        context.saveGState()
+        context.setShadow(offset: shadow.offset, blur: shadow.radius, color: color.cgColor)
+        context.beginTransparencyLayer(auxiliaryInfo: nil)
+    }
+    var paths = [UIBezierPath]()
+    for value in rects {
+        var rect = value.cgRectValue
+        if isVertical {
+            rect = rect.inset(by: UIEdgeInsetRotateVertical(insets: border.insets))
+        } else {
+            rect = rect.inset(by: border.insets)
+        }
+        rect = TextUtilities.textCGRect(pixelRound: rect)
+        let path = UIBezierPath(roundedRect: rect, cornerRadius: border.cornerRadius)
+        path.close()
+        paths.append(path)
+    }
+    if let color = border.fillColor {
+        context.saveGState()
+        context.setFillColor(color.cgColor)
+        for path in paths {
+            context.addPath(path.cgPath)
+        }
+        context.fillPath()
+        context.restoreGState()
+    }
+    if border.strokeColor != nil && border.lineStyle.rawValue > 0 && border.strokeWidth > 0 {
+        //-------------------------- single line ------------------------------//
+        context.saveGState()
+        for path in paths {
+            
+            var bounds: CGRect = path.bounds.union(CGRect(origin: CGPoint.zero, size: size))
+            bounds = bounds.insetBy(dx: -2 * border.strokeWidth, dy: -2 * border.strokeWidth)
+            context.addRect(bounds)
+            context.addPath(path.cgPath)
+            context.clip(using: .evenOdd)
+        }
+        border.strokeColor!.setStroke()
+        TextSetLinePatternInContext(style: border.lineStyle, width: border.strokeWidth, phase: 0, context: context)
+        var inset: CGFloat = -border.strokeWidth * 0.5
+        if (border.lineStyle.rawValue & 0xff) == TextLineStyle.thick.rawValue {
+            inset *= 2
+            context.setLineWidth(border.strokeWidth * 2)
+        }
+        var radiusDelta: CGFloat = -inset
+        if border.cornerRadius <= 0 {
+            radiusDelta = 0
+        }
+        context.setLineJoin(border.lineJoin)
+        for value in rects {
+            var rect = value.cgRectValue
+            if isVertical {
+                rect = rect.inset(by: UIEdgeInsetRotateVertical(insets: border.insets))
+            } else {
+                rect = rect.inset(by: border.insets)
+            }
+            rect = rect.insetBy(dx: inset, dy: inset)
+            let path = UIBezierPath(roundedRect: rect, cornerRadius: border.cornerRadius + radiusDelta)
+            path.close()
+            context.addPath(path.cgPath)
+        }
+        context.strokePath()
+        context.restoreGState()
+        
+        //------------------------- second line ------------------------------//
+        if (border.lineStyle.rawValue & 0xff) == TextLineStyle.double.rawValue {
+            
+            context.saveGState()
+            var inset: CGFloat = -border.strokeWidth * 2
+            for value in rects {
+                var rect = value.cgRectValue
+                rect = rect.inset(by: border.insets)
+                rect = rect.insetBy(dx: inset, dy: inset)
+                let path = UIBezierPath(roundedRect: rect, cornerRadius: border.cornerRadius + 2 * border.strokeWidth)
+                path.close()
+                
+                var bounds: CGRect = path.bounds.union(CGRect(origin: CGPoint.zero, size: size))
+                bounds = bounds.insetBy(dx: -2 * border.strokeWidth, dy: -2 * border.strokeWidth)
+                context.addRect(bounds)
+                context.addPath(path.cgPath)
+                context.clip(using: .evenOdd)
+            }
+            if let aColor = border.strokeColor?.cgColor {
+                context.setStrokeColor(aColor)
+            }
+            TextSetLinePatternInContext(style: border.lineStyle, width: border.strokeWidth, phase: 0, context: context)
+            context.setLineJoin(border.lineJoin)
+            inset = -border.strokeWidth * 2.5
+            radiusDelta = border.strokeWidth * 2
+            if border.cornerRadius <= 0 {
+                radiusDelta = 0
+            }
+            for value in rects {
+                var rect = value.cgRectValue
+                rect = rect.inset(by: border.insets)
+                rect = rect.insetBy(dx: inset, dy: inset)
+                let path = UIBezierPath(roundedRect: rect, cornerRadius: border.cornerRadius + radiusDelta)
+                path.close()
+                context.addPath(path.cgPath)
+            }
+            context.strokePath()
+            context.restoreGState()
+        }
+    }
+    
+    if let _ = border.shadow?.color {
+        context.endTransparencyLayer()
+        context.restoreGState()
+    }
+}
+
+private func TextDrawLineStyle(context: CGContext, length: CGFloat, lineWidth: CGFloat, style: TextLineStyle, position: CGPoint, color: CGColor, isVertical: Bool) {
+    
+    let styleBase = style.rawValue & 0xff
+    if styleBase == 0 {
+        return
+    }
+    context.saveGState()
+    do {
+        if isVertical {
+            var x: CGFloat
+            var y1: CGFloat
+            var y2: CGFloat
+            var w: CGFloat
+            y1 = TextUtilities.textCGFloat(pixelRound: position.y)
+            y2 = TextUtilities.textCGFloat(pixelRound: (position.y + length))
+            w = styleBase == TextLineStyle.thick.rawValue ? lineWidth * 2 : lineWidth
+            let linePixel = TextUtilities.textCGFloat(toPixel: w)
+            if abs(Float(linePixel - floor(linePixel))) < 0.1 {
+                let iPixel = Int(linePixel)
+                if iPixel == 0 || (iPixel % 2) != 0 {
+                    // odd line pixel
+                    x = TextUtilities.textCGFloat(pixelHalf: position.x)
+                } else {
+                    x = TextUtilities.textCGFloat(pixelFloor: position.x)
+                }
+            } else {
+                x = position.x
+            }
+            
+            context.setStrokeColor(color)
+            
+            TextSetLinePatternInContext(style: style, width: lineWidth, phase: position.y, context: context)
+            context.setLineWidth(w)
+            if styleBase == TextLineStyle.single.rawValue {
+                context.move(to: CGPoint(x: x, y: y1))
+                context.addLine(to: CGPoint(x: x, y: y2))
+                context.strokePath()
+            } else if styleBase == TextLineStyle.thick.rawValue {
+                context.move(to: CGPoint(x: x, y: y1))
+                context.addLine(to: CGPoint(x: x, y: y2))
+                context.strokePath()
+            } else if styleBase == TextLineStyle.double.rawValue {
+                context.move(to: CGPoint(x: x - w, y: y1))
+                context.addLine(to: CGPoint(x: x - w, y: y2))
+                context.strokePath()
+                context.move(to: CGPoint(x: x + w, y: y1))
+                context.addLine(to: CGPoint(x: x + w, y: y2))
+                context.strokePath()
+            }
+        } else {
+            var x1: CGFloat = 0
+            var x2: CGFloat = 0
+            var y: CGFloat = 0
+            var w: CGFloat = 0
+            x1 = TextUtilities.textCGFloat(pixelRound: position.x)
+            x2 = TextUtilities.textCGFloat(pixelRound: position.x + length)
+            w = styleBase == TextLineStyle.thick.rawValue ? lineWidth * 2 : lineWidth
+            let linePixel = TextUtilities.textCGFloat(toPixel: w)
+            if abs(Float(linePixel - floor(linePixel))) < 0.1 {
+                let iPixel = Int(linePixel)
+                if iPixel == 0 || (iPixel % 2) != 0 {
+                    // odd line pixel
+                    y = TextUtilities.textCGFloat(pixelHalf: position.y)
+                } else {
+                    y = TextUtilities.textCGFloat(pixelFloor: position.y)
+                }
+            } else {
+                y = position.y
+            }
+            context.setStrokeColor(color)
+            TextSetLinePatternInContext(style: style, width: lineWidth, phase: position.x, context: context)
+            context.setLineWidth(w)
+            if styleBase == TextLineStyle.single.rawValue {
+                context.move(to: CGPoint(x: x1, y: y))
+                context.addLine(to: CGPoint(x: x2, y: y))
+                context.strokePath()
+            } else if styleBase == TextLineStyle.thick.rawValue {
+                context.move(to: CGPoint(x: x1, y: y))
+                context.addLine(to: CGPoint(x: x2, y: y))
+                context.strokePath()
+            } else if styleBase == TextLineStyle.double.rawValue {
+                context.move(to: CGPoint(x: x1, y: y - w))
+                context.addLine(to: CGPoint(x: x2, y: y - w))
+                context.strokePath()
+                context.move(to: CGPoint(x: x1, y: y + w))
+                context.addLine(to: CGPoint(x: x2, y: y + w))
+                context.strokePath()
+            }
+        }
+    }
+    
+    context.restoreGState()
+}
+
+private func TextDrawText(_ layout: TextLayout, context: CGContext, size: CGSize, point: CGPoint, cancel: (() -> Bool)? = nil) {
+    context.saveGState()
+    do {
+        context.translateBy(x: point.x, y: point.y)
+        context.translateBy(x: 0, y: size.height)
+        context.scaleBy(x: 1, y: -1)
+        let isVertical = layout.container.isVerticalForm
+        let verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0
+        let lines = layout.lines
+        
+        for l in lines {
+            var line = l
+            if let tmpL = layout.truncatedLine, tmpL.index == line.index {
+                line = tmpL
+            }
+            let lineRunRanges = line.verticalRotateRange
+            let posX: CGFloat = line.position.x
+            let posY: CGFloat = size.height - line.position.y
+            let runs = CTLineGetGlyphRuns(line.ctLine!)
+            let rMax = CFArrayGetCount(runs)
+            for r in 0..<rMax {
+                let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+                context.textMatrix = .identity
+                context.textPosition = CGPoint(x: posX, y: posY)
+                TextDrawRun(line: line, run: run, context: context, size: size, isVertical: isVertical, runRanges: lineRunRanges?[r], verticalOffset: verticalOffset)
+            }
+            if let _cancel = cancel, _cancel() {
+                break
+            }
+        }
+        // Use this to draw frame for test/debug.
+        // CGContextTranslateCTM(context, verticalOffset, size.height);
+        // CTFrameDraw(layout.frame, context);
+    }
+    context.restoreGState()
+}
+
+private func TextDrawBlockBorder(_ layout: TextLayout, context: CGContext, size: CGSize, point: CGPoint, cancel: (() -> Bool)? = nil) {
+    
+    context.saveGState()
+    context.translateBy(x: point.x, y: point.y)
+    let isVertical = layout.container.isVerticalForm
+    let verticalOffset: CGFloat = isVertical ? (size.width - layout.container.size.width) : 0
+    let lines = layout.lines
+    
+    var l = 0, lMax = lines.count
+    while l < lMax {
+        if let _cancel = cancel, _cancel() {
+            break
+        }
+        var line = lines[l]
+        if let tmpL = layout.truncatedLine, tmpL.index == line.index {
+            line = tmpL
+        }
+        let runs = CTLineGetGlyphRuns(line.ctLine!)
+        let rMax = CFArrayGetCount(runs)
+        for r in 0..<rMax {
+            let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+            let glyphCount = CTRunGetGlyphCount(run)
+            if glyphCount == 0 {
+                continue
+            }
+            let attrs = CTRunGetAttributes(run) as? [AnyHashable : Any]
+            
+            guard let border = attrs?[TextAttribute.textBlockBorderAttributeName] as? TextBorder else {
+                continue
+            }
+            var lineStartIndex = line.index
+            while lineStartIndex > 0 {
+                if (lines[lineStartIndex - 1]).row == line.row {
+                    lineStartIndex = (lineStartIndex - 1)
+                } else {
+                    break
+                }
+            }
+            var unionRect = CGRect.zero
+            let lineStartRow = (lines[lineStartIndex]).row
+            var lineContinueIndex = lineStartIndex
+            var lineContinueRow = lineStartRow
+            
+            repeat {
+                let one = lines[lineContinueIndex]
+                if lineContinueIndex == lineStartIndex {
+                    unionRect = one.bounds
+                } else {
+                    unionRect = unionRect.union(one.bounds)
+                }
+                if lineContinueIndex + 1 == lMax {
+                    break
+                }
+                let next = lines[lineContinueIndex + 1]
+                if next.row != lineContinueRow {
+                    let nextBorder = layout.text?.bs_attribute(NSAttributedString.Key(rawValue: TextAttribute.textBlockBorderAttributeName), at: next.range.location) as? TextBorder
+                    if nextBorder == border {
+                        lineContinueRow += 1
+                    } else {
+                        break
+                    }
+                }
+                lineContinueIndex += 1
+            } while true
+            
+            if isVertical {
+                let insets: UIEdgeInsets = layout.container.insets
+                unionRect.origin.y = insets.top
+                unionRect.size.height = layout.container.size.height - insets.top - insets.bottom
+            } else {
+                let insets: UIEdgeInsets = layout.container.insets
+                unionRect.origin.x = insets.left
+                unionRect.size.width = layout.container.size.width - insets.left - insets.right
+            }
+            unionRect.origin.x += verticalOffset
+            TextDrawBorderRects(context: context, size: size, border: border, rects: [NSValue(cgRect: unionRect)], isVertical: isVertical)
+            l = lineContinueIndex
+            break
+        }
+        l += 1
+    }
+    context.restoreGState()
+}
+
+private func TextDrawBorder(_ layout: TextLayout, context: CGContext, size: CGSize, point: CGPoint, type: TextBorderType, cancel: (() -> Bool)? = nil) {
+    
+    context.saveGState()
+    context.translateBy(x: point.x, y: point.y)
+    let isVertical = layout.container.isVerticalForm
+    let verticalOffset: CGFloat = isVertical ? (size.width - layout.container.size.width) : 0
+    let lines = layout.lines
+    let borderKey = (type == TextBorderType.normal ? TextAttribute.textBorderAttributeName : TextAttribute.textBackgroundBorderAttributeName)
+    var needJumpRun = false
+    var jumpRunIndex: Int = 0
+    
+    var l = 0, lMax = lines.count
+    while l < lMax {
+        if let _cancel = cancel, _cancel() {
+            break
+        }
+        var line = lines[l]
+        if let tmpL = layout.truncatedLine, tmpL.index == line.index {
+            line = tmpL
+        }
+        let runs = CTLineGetGlyphRuns(line.ctLine!)
+        var r = 0, rMax = CFArrayGetCount(runs)
+        while r < rMax {
+            
+            if needJumpRun {
+                needJumpRun = false
+                r = jumpRunIndex + 1
+                if r >= rMax {
+                    break
+                }
+            }
+            let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+            let glyphCount = CTRunGetGlyphCount(run)
+            if glyphCount == 0 {
+                r += 1
+                continue
+            }
+            let attrs = CTRunGetAttributes(run) as? [AnyHashable : AnyObject]
+            
+            guard let border = attrs?[borderKey] as? TextBorder else {
+                r += 1
+                continue
+            }
+            let runRange: CFRange = CTRunGetStringRange(run)
+            if runRange.location == kCFNotFound || runRange.length == 0 {
+                r += 1
+                continue
+            }
+            if runRange.location + runRange.length > layout.text?.length ?? 0 {
+                r += 1
+                continue
+            }
+            var runRects = [NSValue]()
+            var endLineIndex = l
+            var endRunIndex: Int = r
+            var endFound = false
+            for ll in l..<lMax {
+                if endFound {
+                    break
+                }
+                let iLine = lines[ll]
+                let iRuns = CTLineGetGlyphRuns(iLine.ctLine!)
+                var extLineRect = CGRect.null
+                
+                let rr_ = (ll == l) ? r : 0, rrMax = CFArrayGetCount(iRuns)
+                for rr in rr_..<rrMax {
+                    let iRun = unsafeBitCast(CFArrayGetValueAtIndex(iRuns, rr), to: CTRun.self)
+                    let iAttrs = CTRunGetAttributes(iRun) as? [AnyHashable : Any]
+                    let iBorder = iAttrs?[borderKey] as? TextBorder
+                    if !(border == iBorder) {
+                        endFound = true
+                        break
+                    }
+                    endLineIndex = ll
+                    endRunIndex = rr
+                    var iRunPosition = CGPoint.zero
+                    CTRunGetPositions(iRun, CFRangeMake(0, 1), &iRunPosition)
+                    var ascent: CGFloat = 0
+                    var descent: CGFloat = 0
+                    let iRunWidth: CGFloat = CGFloat(CTRunGetTypographicBounds(iRun, CFRangeMake(0, 0), &ascent, &descent, nil))
+                    if isVertical {
+                        TextUtilities.numberSwap(&iRunPosition.x, b: &iRunPosition.y)
+                        iRunPosition.y += iLine.position.y
+                        let iRect = CGRect(x: verticalOffset + line.position.x - descent, y: iRunPosition.y, width: ascent + descent, height: iRunWidth)
+                        if extLineRect.isNull {
+                            extLineRect = iRect
+                        } else {
+                            extLineRect = extLineRect.union(iRect)
+                        }
+                    } else {
+                        iRunPosition.x += iLine.position.x
+                        let iRect = CGRect(x: iRunPosition.x, y: iLine.position.y - ascent, width: iRunWidth, height: ascent + descent)
+                        if extLineRect.isNull {
+                            extLineRect = iRect
+                        } else {
+                            extLineRect = extLineRect.union(iRect)
+                        }
+                    }
+                }
+                if !extLineRect.isNull {
+                    runRects.append(NSValue(cgRect: extLineRect))
+                }
+            }
+            var drawRects = [NSValue]()
+            var curRect = runRects.first!.cgRectValue
+            let reMax = runRects.count
+            for re in 0..<reMax {
+                let rect = runRects[re].cgRectValue
+                if isVertical {
+                    if abs(Float((rect.origin.x) - (curRect.origin.x))) < 1 {
+                        curRect = TextMergeRectInSameLine(rect1: rect, rect2: curRect, isVertical: isVertical)
+                    } else {
+                        drawRects.append(NSValue(cgRect: curRect))
+                        curRect = rect
+                    }
+                } else {
+                    if abs(Float((rect.origin.y) - (curRect.origin.y))) < 1 {
+                        curRect = TextMergeRectInSameLine(rect1: rect, rect2: curRect, isVertical: isVertical)
+                    } else {
+                        drawRects.append(NSValue(cgRect: curRect))
+                        curRect = rect
+                    }
+                }
+            }
+            if !curRect.equalTo(CGRect.zero) {
+                drawRects.append(NSValue(cgRect: curRect))
+            }
+            TextDrawBorderRects(context: context, size: size, border: border, rects: drawRects, isVertical: isVertical)
+            if l == endLineIndex {
+                r = endRunIndex
+            } else {
+                l = endLineIndex - 1
+                needJumpRun = true
+                jumpRunIndex = endRunIndex
+                break
+            }
+            r += 1
+        }
+        l += 1
+    }
+    context.restoreGState()
+}
+
+private func TextDrawDecoration(_ layout: TextLayout, context: CGContext, size: CGSize, point: CGPoint, type: TextDecorationType, cancel: (() -> Bool)? = nil) {
+    
+    let lines = layout.lines
+    context.saveGState()
+    context.translateBy(x: point.x, y: point.y)
+    let isVertical = layout.container.isVerticalForm
+    let verticalOffset: CGFloat = isVertical ? (size.width - layout.container.size.width) : 0
+    context.translateBy(x: verticalOffset, y: 0)
+    
+    let lMax = layout.lines.count
+    for l in 0..<lMax {
+        if let _cancel = cancel, _cancel() {
+            break
+        }
+        var line = lines[l]
+        if let tmpL = layout.truncatedLine, tmpL.index == line.index {
+            line = tmpL
+        }
+        let runs = CTLineGetGlyphRuns(line.ctLine!)
+        let rMax = CFArrayGetCount(runs)
+        for r in 0..<rMax {
+            let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+            let glyphCount = CTRunGetGlyphCount(run)
+            if glyphCount == 0 {
+                continue
+            }
+            let attrs = CTRunGetAttributes(run) as? [AnyHashable : Any]
+            let underline = attrs?[TextAttribute.textUnderlineAttributeName] as? TextDecoration
+            let strikethrough = attrs?[TextAttribute.textStrikethroughAttributeName] as? TextDecoration
+            var needDrawUnderline = false
+            var needDrawStrikethrough = false
+            if (type.rawValue & TextDecorationType.underline.rawValue) != 0 && underline?.style.rawValue ?? 0 > 0 {
+                needDrawUnderline = true
+            }
+            if (type.rawValue & TextDecorationType.strikethrough.rawValue) != 0 && strikethrough?.style.rawValue ?? 0 > 0 {
+                needDrawStrikethrough = true
+            }
+            if !needDrawUnderline && !needDrawStrikethrough {
+                continue
+            }
+            let runRange: CFRange = CTRunGetStringRange(run)
+            if runRange.location == kCFNotFound || runRange.length == 0 { continue }
+            if runRange.location + runRange.length > layout.text!.length {
+                continue
+            }
+            let runStr = layout.text!.attributedSubstring(from: NSRange(location: runRange.location, length: runRange.length)).string
+            if TextUtilities.textIsLinebreakString((runStr)) {
+                continue // may need more checks...
+            }
+            var xHeight: CGFloat = 0
+            var underlinePosition: CGFloat = 0
+            var lineThickness: CGFloat = 0
+            TextGetRunsMaxMetric(runs: runs, xHeight: &xHeight, underlinePosition: &underlinePosition, lineThickness: &lineThickness)
+            var underlineStart = CGPoint.zero
+            var strikethroughStart = CGPoint.zero
+            var length: CGFloat = 0
+            if isVertical {
+                underlineStart.x = line.position.x + underlinePosition
+                strikethroughStart.x = line.position.x + xHeight / 2
+                var runPosition = CGPoint.zero
+                CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition)
+                strikethroughStart.y = runPosition.x + line.position.y
+                underlineStart.y = strikethroughStart.y
+                length = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nil, nil, nil))
+            } else {
+                underlineStart.y = line.position.y - underlinePosition
+                strikethroughStart.y = line.position.y - xHeight / 2
+                var runPosition = CGPoint.zero
+                CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition)
+                strikethroughStart.x = runPosition.x + line.position.x
+                underlineStart.x = strikethroughStart.x
+                length = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nil, nil, nil))
+            }
+            
+            if needDrawUnderline {
+                
+                var color = underline?.color?.cgColor
+                if color == nil {
+                    if let aName = attrs?[kCTForegroundColorAttributeName] as! CGColor? {
+                        color = aName
+                    }
+                }
+                
+                let thickness = (underline?.width != nil) ? CGFloat(underline!.width!.floatValue) : lineThickness
+                var shadow = underline?.shadow
+                while shadow != nil {
+                    if shadow!.color == nil {
+                        shadow = shadow?.subShadow
+                        continue
+                    }
+                    let offsetAlterX: CGFloat = size.width + 0xffff
+                    context.saveGState()
+                    do {
+                        var offset = shadow!.offset
+                        offset.width -= offsetAlterX
+                        context.saveGState()
+                        do {
+                            context.setShadow(offset: offset, blur: shadow!.radius, color: shadow!.color!.cgColor)
+                            context.setBlendMode(shadow!.blendMode)
+                            context.translateBy(x: offsetAlterX, y: 0)
+                            TextDrawLineStyle(context: context, length: length, lineWidth: thickness, style: underline!.style, position: underlineStart, color: color!, isVertical: isVertical)
+                        }
+                        context.restoreGState()
+                    }
+                    context.restoreGState()
+                    shadow = shadow?.subShadow
+                }
+                TextDrawLineStyle(context: context, length: length, lineWidth: thickness, style: underline!.style, position: underlineStart, color: color!, isVertical: isVertical)
+            }
+            
+            if needDrawStrikethrough {
+                var color = strikethrough?.color?.cgColor
+                
+                if color == nil {
+                    if let aName = (attrs?[kCTForegroundColorAttributeName]) as! CGColor? {
+                        color = aName
+                    }
+                }
+                
+                let thickness = (strikethrough?.width != nil) ? CGFloat((strikethrough!.width?.floatValue)!) : lineThickness
+                var shadow = underline?.shadow
+                while shadow != nil {
+                    if shadow?.color == nil {
+                        shadow = shadow?.subShadow
+                        continue
+                    }
+                    let offsetAlterX: CGFloat = size.width + 0xffff
+                    context.saveGState()
+                    do {
+                        var offset: CGSize? = shadow?.offset
+                        offset?.width -= offsetAlterX
+                        context.saveGState()
+                        do {
+                            context.setShadow(offset: offset!, blur: (shadow?.radius)!, color: shadow?.color?.cgColor)
+                            context.setBlendMode((shadow?.blendMode)!)
+                            context.translateBy(x: offsetAlterX, y: 0)
+                            TextDrawLineStyle(context: context, length: length, lineWidth: thickness, style: underline!.style, position: underlineStart, color: color!, isVertical: isVertical)
+                        }
+                        context.restoreGState()
+                    }
+                    context.restoreGState()
+                    shadow = shadow?.subShadow
+                }
+                TextDrawLineStyle(context: context, length: length, lineWidth: thickness, style: strikethrough!.style, position: strikethroughStart, color: color!, isVertical: isVertical)
+            }
+        }
+    }
+    context.restoreGState()
+}
+
+private func TextDrawAttachment(_ layout: TextLayout, context: CGContext?, size: CGSize, point: CGPoint, targetView: UIView?, targetLayer: CALayer?, cancel: (() -> Bool)? = nil) {
+    
+    let isVertical = layout.container.isVerticalForm
+    let verticalOffset: CGFloat = isVertical ? (size.width - layout.container.size.width) : 0
+    let max = layout.attachments?.count ?? 0
+    for i in 0..<max {
+        let a = layout.attachments![i]
+        if a.content == nil {
+            continue
+        }
+        var image: UIImage? = nil
+        var view: UIView? = nil
+        var layer: CALayer? = nil
+        if (a.content is UIImage) {
+            image = a.content as? UIImage
+        } else if (a.content is UIView) {
+            view = a.content as? UIView
+        } else if (a.content is CALayer) {
+            layer = a.content as? CALayer
+        }
+        if image == nil && view == nil && layer == nil {
+            continue
+        }
+        if image != nil && context == nil {
+            continue
+        }
+        if view != nil && targetView == nil {
+            continue
+        }
+        if layer != nil && targetLayer == nil {
+            continue
+        }
+        if let _cancel = cancel, _cancel() {
+            break
+        }
+        let asize = image != nil ? image!.size : view != nil ? view!.frame.size : layer!.frame.size
+        var rect = (layout.attachmentRects?[i])?.cgRectValue
+        if isVertical {
+            rect = rect?.inset(by: UIEdgeInsetRotateVertical(insets: a.contentInsets))
+        } else {
+            rect = rect?.inset(by: a.contentInsets)
+        }
+        rect = TextUtilities.textCGRectFit(with: a.contentMode, rect: rect!, size: asize)
+        rect = TextUtilities.textCGRect(pixelRound: rect!)
+        rect = rect?.standardized
+        rect?.origin.x += point.x + verticalOffset
+        rect?.origin.y += point.y
+        if image != nil {
+            if let ref = image!.cgImage {
+                context?.saveGState()
+                context?.translateBy(x: 0, y: rect!.maxY + rect!.minY)
+                context?.scaleBy(x: 1, y: -1)
+                context?.draw(ref, in: rect!)
+                context?.restoreGState()
+            }
+        } else if view != nil {
+            view!.frame = rect!
+            targetView?.addSubview(view!)
+        } else if layer != nil {
+            layer!.frame = rect!
+            targetLayer?.addSublayer(layer!)
+        }
+    }
+}
+
+private func TextDrawShadow(_ layout: TextLayout, context: CGContext, size: CGSize, point: CGPoint, cancel: (() -> Bool)? = nil) {
+    
+    // move out of context. (0xFFFF is just a random large number)
+    let offsetAlterX: CGFloat = size.width + 0xffff
+    let isVertical = layout.container.isVerticalForm
+    let verticalOffset: CGFloat = isVertical ? (size.width - layout.container.size.width) : 0
+    context.saveGState()
+    do {
+        context.translateBy(x: point.x, y: point.y)
+        context.translateBy(x: 0, y: size.height)
+        context.scaleBy(x: 1, y: -1)
+        let lines = layout.lines
+        let lMax = layout.lines.count
+        for l in 0..<lMax {
+            if let _cancel = cancel, _cancel() {
+                break
+            }
+            var line = lines[l]
+            if let tmp = layout.truncatedLine, tmp.index == line.index {
+                line = tmp
+            }
+            let lineRunRanges = line.verticalRotateRange
+            let linePosX = line.position.x
+            let linePosY: CGFloat = size.height - line.position.y
+            let runs = CTLineGetGlyphRuns(line.ctLine!)
+            let rMax = CFArrayGetCount(runs)
+            for r in 0..<rMax {
+                let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+                context.textMatrix = .identity
+                context.textPosition = CGPoint(x: linePosX, y: linePosY)
+                let attrs = CTRunGetAttributes(run) as? [AnyHashable : Any]
+                var shadow = attrs?[TextAttribute.textShadowAttributeName] as? TextShadow
+                let nsShadow = TextShadow.shadow(with: (attrs?[NSAttributedString.Key.shadow] as? NSShadow)) // NSShadow compatible
+                
+                if nsShadow != nil {
+                    nsShadow!.subShadow = shadow
+                    shadow = nsShadow
+                }
+            
+                while shadow != nil {
+                    if shadow?.color == nil {
+                        shadow = shadow?.subShadow
+                        continue
+                    }
+                    var offset: CGSize = shadow!.offset
+                    offset.width -= offsetAlterX
+                    context.saveGState()
+                    do {
+                        context.setShadow(offset: offset, blur: shadow!.radius, color: shadow!.color!.cgColor)
+                        context.setBlendMode(shadow!.blendMode)
+                        context.translateBy(x: offsetAlterX, y: 0)
+                        TextDrawRun(line: line, run: run, context: context, size: size, isVertical: isVertical, runRanges: lineRunRanges?[r], verticalOffset: verticalOffset)
+                    }
+                    context.restoreGState()
+                    shadow = shadow!.subShadow
+                }
+            }
+        }
+    }
+    context.restoreGState()
+}
+
+private func TextDrawInnerShadow(_ layout: TextLayout, context: CGContext, size: CGSize, point: CGPoint, cancel: (() -> Bool)? = nil) {
+    
+    context.saveGState()
+    context.translateBy(x: point.x, y: point.y)
+    context.translateBy(x: 0, y: size.height)
+    context.scaleBy(x: 1, y: -1)
+    context.textMatrix = .identity
+    let isVertical = layout.container.isVerticalForm
+    let verticalOffset: CGFloat = isVertical ? (size.width - layout.container.size.width) : 0
+    let lines = layout.lines
+    
+    let lMax = lines.count
+    for l in 0..<lMax {
+        if let _cancel = cancel, _cancel() {
+            break
+        }
+        var line = lines[l]
+        if let tmp = layout.truncatedLine, tmp.index == line.index {
+            line = tmp
+        }
+        let lineRunRanges = line.verticalRotateRange
+        let linePosX = line.position.x
+        let linePosY: CGFloat = size.height - line.position.y
+        let runs = CTLineGetGlyphRuns(line.ctLine!)
+        let rMax = CFArrayGetCount(runs)
+        for r in 0..<rMax {
+            let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+            if CTRunGetGlyphCount(run) == 0 {
+                continue
+            }
+            context.textMatrix = .identity
+            context.textPosition = CGPoint(x: linePosX, y: linePosY)
+            let attrs = CTRunGetAttributes(run) as? [AnyHashable : Any]
+            var shadow = attrs?[TextAttribute.textInnerShadowAttributeName] as? TextShadow
+            while shadow != nil {
+                if shadow?.color == nil {
+                    shadow = shadow?.subShadow
+                    continue
+                }
+                var runPosition = CGPoint.zero
+                CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition)
+                var runImageBounds: CGRect = CTRunGetImageBounds(run, context, CFRangeMake(0, 0))
+                runImageBounds.origin.x += runPosition.x
+                if runImageBounds.size.width < 0.1 || runImageBounds.size.height < 0.1 {
+                    continue
+                }
+                let runAttrs = CTRunGetAttributes(run) as! [String: AnyObject]
+                
+                if let _ = runAttrs[TextAttribute.textGlyphTransformAttributeName] as? NSValue {
+                    runImageBounds = CGRect(x: 0, y: 0, width: size.width, height: size.height)
+                }
+                // text inner shadow
+                context.saveGState()
+                do {
+                    context.setBlendMode(shadow!.blendMode)
+                    context.setShadow(offset: CGSize.zero, blur: 0, color: nil)
+                    context.setAlpha(shadow!.color!.cgColor.alpha)
+                    context.clip(to: runImageBounds)
+                    context.beginTransparencyLayer(auxiliaryInfo: nil)
+                    do {
+                        let opaqueShadowColor = shadow!.color!.withAlphaComponent(1)
+                        context.setShadow(offset: shadow!.offset, blur: shadow!.radius, color: opaqueShadowColor.cgColor)
+                        context.setFillColor(opaqueShadowColor.cgColor)
+                        context.setBlendMode(CGBlendMode.sourceOut)
+                        context.beginTransparencyLayer(auxiliaryInfo: nil)
+                        do {
+                            context.fill(runImageBounds)
+                            context.setBlendMode(CGBlendMode.destinationIn)
+                            context.beginTransparencyLayer(auxiliaryInfo: nil)
+                            do {
+                                TextDrawRun(line: line, run: run, context: context, size: size, isVertical: isVertical, runRanges: lineRunRanges?[r], verticalOffset: verticalOffset)
+                            }
+                            context.endTransparencyLayer()
+                        }
+                        context.endTransparencyLayer()
+                    }
+                    context.endTransparencyLayer()
+                }
+                context.restoreGState()
+                shadow = shadow!.subShadow
+            }
+        }
+    }
+    context.restoreGState()
+}
+
+private func TextDrawDebug(_ layout: TextLayout, context: CGContext, size: CGSize, point: CGPoint, op: TextDebugOption?) {
+    
+    UIGraphicsPushContext(context)
+    context.saveGState()
+    context.translateBy(x: point.x, y: point.y)
+    context.setLineWidth(1.0 / TextUtilities.textScreenScale)
+    context.setLineDash(phase: 0, lengths: [])
+    context.setLineJoin(CGLineJoin.miter)
+    context.setLineCap(CGLineCap.butt)
+    let isVertical = layout.container.isVerticalForm
+    let verticalOffset: CGFloat = (isVertical ? (size.width - layout.container.size.width) : 0)
+    context.translateBy(x: verticalOffset, y: 0)
+    
+    if op?.ctFrameBorderColor != nil || op?.ctFrameFillColor != nil {
+        var path = layout.container.path
+        if path == nil {
+            var rect = CGRect.zero
+            rect.size = layout.container.size
+            rect = rect.inset(by: layout.container.insets)
+            if op?.ctFrameBorderColor != nil {
+                rect = TextUtilities.textCGRect(pixelHalf: rect)
+            } else {
+                rect = TextUtilities.textCGRect(pixelRound: rect)
+            }
+            path = UIBezierPath(rect: rect)
+        }
+        path?.close()
+        for ex in layout.container.exclusionPaths ?? [] {
+            path?.append(ex)
+        }
+        if op?.ctFrameFillColor != nil {
+            op!.ctFrameFillColor!.setFill()
+            if layout.container.pathLineWidth > 0 {
+                context.saveGState()
+                do {
+                    context.beginTransparencyLayer(auxiliaryInfo: nil)
+                    do {
+                        context.addPath(path!.cgPath)
+                        if layout.container.pathFillEvenOdd {
+                            context.fillPath(using: .evenOdd)
+                        } else {
+                            context.fillPath()
+                        }
+                        context.setBlendMode(CGBlendMode.destinationOut)
+                        UIColor.black.setFill()
+                        let cgPath = path!.cgPath.copy(strokingWithWidth: layout.container.pathLineWidth, lineCap: .butt, lineJoin: .miter, miterLimit: 0, transform: .identity)
+                        //if cgPath
+                        context.addPath(cgPath)
+                        context.fillPath()
+                    }
+                    context.endTransparencyLayer()
+                }
+                context.restoreGState()
+            } else {
+                context.addPath(path!.cgPath)
+                if layout.container.pathFillEvenOdd {
+                    context.fillPath(using: .evenOdd)
+                } else {
+                    context.fillPath()
+                }
+            }
+        }
+        if ((op?.ctFrameBorderColor) != nil) {
+            context.saveGState()
+            do {
+                if layout.container.pathLineWidth > 0 {
+                    context.setLineWidth(layout.container.pathLineWidth)
+                }
+                op!.ctFrameBorderColor!.setStroke()
+                context.addPath(path!.cgPath)
+                context.strokePath()
+            }
+            context.restoreGState()
+        }
+    }
+    
+    let lines = layout.lines
+    let lMax = lines.count
+    for l in 0..<lMax {
+        var line = lines[l]
+        if let tmp = layout.truncatedLine, tmp.index == line.index {
+            line = tmp
+        }
+        let lineBounds = line.bounds
+        if op?.ctLineFillColor != nil {
+            op!.ctLineFillColor!.setFill()
+            context.addRect(TextUtilities.textCGRect(pixelRound: lineBounds))
+            context.fillPath()
+        }
+        if op?.ctLineBorderColor != nil {
+            op!.ctLineBorderColor!.setStroke()
+            context.addRect(TextUtilities.textCGRect(pixelHalf: lineBounds))
+            context.strokePath()
+        }
+        if op?.baselineColor != nil {
+            op!.baselineColor!.setStroke()
+            if isVertical {
+                let x: CGFloat = TextUtilities.textCGFloat(pixelHalf: line.position.x)
+                let y1: CGFloat = TextUtilities.textCGFloat(pixelHalf: line.top)
+                let y2: CGFloat = TextUtilities.textCGFloat(pixelHalf: line.bottom)
+                context.move(to: CGPoint(x: x, y: y1))
+                context.addLine(to: CGPoint(x: x, y: y2))
+                context.strokePath()
+            } else {
+                let x1: CGFloat = TextUtilities.textCGFloat(pixelHalf: lineBounds.origin.x)
+                let x2: CGFloat = TextUtilities.textCGFloat(pixelHalf: (lineBounds.origin.x + lineBounds.size.width))
+                let y: CGFloat = TextUtilities.textCGFloat(pixelHalf: line.position.y)
+                context.move(to: CGPoint(x: x1, y: y))
+                context.addLine(to: CGPoint(x: x2, y: y))
+                context.strokePath()
+            }
+        }
+        if op?.ctLineNumberColor != nil {
+            op!.ctLineNumberColor!.set()
+            let num = NSMutableAttributedString(string: l.description)
+            num.bs_color = op?.ctLineNumberColor
+            num.bs_font = UIFont.systemFont(ofSize: 6)
+            num.draw(at: CGPoint(x: line.position.x, y: line.position.y - (isVertical ? 1 : 6)))
+        }
+        if op?.ctRunFillColor != nil || op?.ctRunBorderColor != nil || op?.ctRunNumberColor != nil || op?.cgGlyphFillColor != nil || op?.cgGlyphBorderColor != nil {
+            let runs = CTLineGetGlyphRuns(line.ctLine!)
+            let rMax = CFArrayGetCount(runs)
+            for r in 0..<rMax {
+                let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, r), to: CTRun.self)
+                let glyphCount: CFIndex = CTRunGetGlyphCount(run)
+                if glyphCount == 0 {
+                    continue
+                }
+                let glyphPositions = UnsafeMutablePointer<CGPoint>.allocate(capacity: glyphCount)
+                CTRunGetPositions(run, CFRangeMake(0, glyphCount), glyphPositions)
+                let glyphAdvances = UnsafeMutablePointer<CGSize>.allocate(capacity: glyphCount)
+                CTRunGetAdvances(run, CFRangeMake(0, glyphCount), glyphAdvances)
+                var runPosition: CGPoint = glyphPositions[0]
+                
+                if isVertical {
+                    TextUtilities.numberSwap(&runPosition.x, b: &runPosition.y)
+                    runPosition.x = line.position.x
+                    runPosition.y += line.position.y
+                } else {
+                    runPosition.x += line.position.x
+                    runPosition.y = line.position.y - runPosition.y
+                }
+                var ascent: CGFloat = 0
+                var descent: CGFloat = 0
+                var leading: CGFloat = 0
+                let width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading))
+                var runTypoBounds = CGRect.zero
+                if isVertical {
+                    runTypoBounds = CGRect(x: runPosition.x - descent, y: runPosition.y, width: ascent + descent, height: width)
+                } else {
+                    runTypoBounds = CGRect(x: runPosition.x, y: line.position.y - ascent, width: width, height: ascent + descent)
+                }
+                if op?.ctRunFillColor != nil {
+                    op!.ctRunFillColor!.setFill()
+                    context.addRect(TextUtilities.textCGRect(pixelRound: runTypoBounds))
+                    context.fillPath()
+                }
+                if op?.ctRunBorderColor != nil {
+                    op!.ctRunBorderColor!.setStroke()
+                    context.addRect(TextUtilities.textCGRect(pixelHalf: runTypoBounds))
+                    context.strokePath()
+                }
+                if op?.ctRunNumberColor != nil {
+                    op!.ctRunNumberColor!.set()
+                    let num = NSMutableAttributedString(string: r.description)
+                    num.bs_color = op?.ctRunNumberColor
+                    num.bs_font = UIFont.systemFont(ofSize: 6)
+                    num.draw(at: CGPoint(x: runTypoBounds.origin.x, y: runTypoBounds.origin.y - 1))
+                }
+                if op?.cgGlyphBorderColor != nil || op?.cgGlyphFillColor != nil {
+                    for g in 0..<glyphCount {
+                        var pos: CGPoint = glyphPositions[g]
+                        let adv: CGSize = glyphAdvances[g]
+                        var rect = CGRect.zero
+                        if isVertical {
+                            TextUtilities.numberSwap(&pos.x, b: &pos.y)
+                            pos.x = runPosition.x
+                            pos.y += line.position.y
+                            rect = CGRect(x: pos.x - descent, y: pos.y, width: runTypoBounds.size.width, height: adv.width)
+                        } else {
+                            pos.x += line.position.x
+                            pos.y = runPosition.y
+                            rect = CGRect(x: pos.x, y: pos.y - ascent, width: adv.width, height: runTypoBounds.size.height)
+                        }
+                        if op?.cgGlyphFillColor != nil {
+                            op!.cgGlyphFillColor!.setFill()
+                            context.addRect(TextUtilities.textCGRect(pixelRound: rect))
+                            context.fillPath()
+                        }
+                        if op?.cgGlyphBorderColor != nil {
+                            op!.cgGlyphBorderColor!.setStroke()
+                            context.addRect(TextUtilities.textCGRect(pixelHalf: rect))
+                            context.strokePath()
+                        }
+                    }
+                }
+                glyphPositions.deallocate()
+                glyphAdvances.deallocate()
+            }
+        }
+    }
+    context.restoreGState()
+    UIGraphicsPopContext()
+}

+ 267 - 0
Pods/BSText/BSText/Component/TextLine.swift

@@ -0,0 +1,267 @@
+//
+//  TextLine.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/11/19.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+/**
+ A range in CTRun, used for vertical form.
+ */
+public class TextLine: NSObject {
+    
+    private var firstGlyphPos: CGFloat = 0
+    
+    /*/< line index */
+    @objc public var index: Int = 0
+    
+    /*/< line row */
+    @objc public var row: Int = 0
+    
+    /*/< Run rotate range */
+    @objc public var verticalRotateRange: [[TextRunGlyphRange]]?
+    
+    private var _ctLine: CTLine?
+    
+    /*/< CoreText line */
+    @objc public private(set) var ctLine: CTLine? {
+        set {
+            if _ctLine != newValue {
+                _ctLine = newValue
+                
+                if let line = newValue {
+                    lineWidth = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading))
+                    let range: CFRange = CTLineGetStringRange(line)
+                    self.range = NSRange(location: range.location, length: range.length)
+                    if CTLineGetGlyphCount(line) > 0 {
+                        let runs = CTLineGetGlyphRuns(line)
+                        let p = CFArrayGetValueAtIndex(runs, 0);
+                        // 获取 UnsafeRawPointer 指针中的内容,用 unsafeBitCast 方法
+                        let run = unsafeBitCast(p, to: CTRun.self)
+                        
+                        var pos = CGPoint.zero
+                        CTRunGetPositions(run, CFRangeMake(0, 1), &pos)
+                        
+                        firstGlyphPos = pos.x
+                    } else {
+                        firstGlyphPos = 0
+                    }
+                    trailingWhitespaceWidth = CGFloat(CTLineGetTrailingWhitespaceWidth(line))
+                } else {
+                    trailingWhitespaceWidth = 0
+                    firstGlyphPos = trailingWhitespaceWidth
+                    leading = firstGlyphPos
+                    descent = leading
+                    ascent = descent
+                    lineWidth = ascent
+                    self.range = NSRange(location: 0, length: 0)
+                }
+                reloadBounds()
+            }
+        }
+        get {
+            return _ctLine
+        }
+    }
+    
+    /*/< string range */
+    @objc public private(set) var range = NSRange(location: 0, length: 0)
+    
+    /*/< vertical form */
+    @objc public private(set) var vertical = false
+    
+    /*/< bounds (ascent + descent) */
+    @objc public private(set) var bounds = CGRect.zero
+    
+    /*/< bounds.size */
+    @objc public var size: CGSize {
+        return bounds.size
+    }
+    
+    /*/< bounds.size.width */
+    @objc public var width: CGFloat {
+        return bounds.size.width
+    }
+    
+    /*/< bounds.size.height */
+    @objc public var height: CGFloat {
+        return bounds.size.height
+    }
+    
+    /*/< bounds.origin.y */
+    @objc public var top: CGFloat {
+        return bounds.minY
+    }
+    
+    /*/< bounds.origin.y + bounds.size.height */
+    @objc public var bottom: CGFloat {
+        return bounds.maxY
+    }
+    
+    /*/< bounds.origin.x */
+    @objc public var left: CGFloat {
+        return bounds.minX
+    }
+    
+    /*/< bounds.origin.x + bounds.size.width */
+    @objc public var right: CGFloat {
+        return bounds.maxX
+    }
+    
+    private var _position = CGPoint.zero
+    
+    /*/< baseline position */
+    @objc public var position: CGPoint {
+        set {
+            _position = newValue
+            self.reloadBounds()
+        }
+        get {
+            return _position
+        }
+    }
+    
+    /*/< line ascent */
+    @objc public private(set) var ascent: CGFloat = 0
+    
+    /*/< line descent */
+    @objc public private(set) var descent: CGFloat = 0
+    
+    /*/< line leading */
+    @objc public private(set) var leading: CGFloat = 0
+    
+    /*/< line width */
+    @objc public private(set) var lineWidth: CGFloat = 0
+    
+    @objc public private(set) var trailingWhitespaceWidth: CGFloat = 0
+    
+    /*/< TextAttachment */
+    @objc public private(set) var attachments: [TextAttachment]?
+    
+    /*/< NSRange(NSValue) */
+    @objc public private(set) var attachmentRanges: [NSValue]?
+    
+    ///< CGRect(NSValue)
+    @objc public private(set) var attachmentRects: [NSValue]?
+    
+    
+    @objc(lineWithCTLine:position:vertical:)
+    public class func lineWith(ctLine: CTLine, position: CGPoint, vertical isVertical: Bool) -> TextLine {
+        
+        let line = TextLine()
+        line.position = position
+        line.vertical = isVertical
+        line.ctLine = ctLine
+        
+        return line
+    }
+    
+    public override init() {
+        super.init()
+    }
+    
+    private func reloadBounds() {
+        if vertical {
+            bounds = CGRect(x: position.x - descent, y: position.y, width: self.ascent + descent, height: lineWidth)
+            bounds.origin.y += firstGlyphPos
+        } else {
+            bounds = CGRect(x: position.x, y: position.y - self.ascent, width: lineWidth, height: self.ascent + descent)
+            bounds.origin.x += firstGlyphPos
+        }
+        self.attachments = nil
+        self.attachmentRanges = nil
+        self.attachmentRects = nil
+        if ctLine == nil {
+            return
+        }
+        let runs = CTLineGetGlyphRuns(ctLine!)
+        let runCount = CFArrayGetCount(runs)
+        if runCount == 0 {
+            return
+        }
+        var attachments_ = [TextAttachment]()
+        var attachmentRanges_ = [NSValue]()
+        var attachmentRects_ = [NSValue]()
+        for r in 0..<runCount {
+            let p = CFArrayGetValueAtIndex(runs, r);
+            // 获取 UnsafeRawPointer 指针中的内容,用 unsafeBitCast 方法
+            let run = unsafeBitCast(p, to: CTRun.self)
+            let glyphCount: CFIndex = CTRunGetGlyphCount(run)
+            if glyphCount == 0 {
+                continue
+            }
+            let attrs = CTRunGetAttributes(run) as? [AnyHashable : Any]
+            
+            if let attachment = attrs?[TextAttribute.textAttachmentAttributeName] as? TextAttachment {
+                var runPosition = CGPoint.zero
+                CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition)
+                var ascent: CGFloat = 0
+                var descent: CGFloat = 0
+                var leading: CGFloat = 0
+                var runWidth: CGFloat = 0
+                var runTypoBounds = CGRect.zero
+                runWidth = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading))
+                
+                if vertical {
+                    (runPosition.x, runPosition.y) = (runPosition.y, runPosition.x)
+                    runPosition.y = position.y + runPosition.y
+                    runTypoBounds = CGRect(x: position.x + runPosition.x - descent, y: runPosition.y, width: ascent + descent, height: runWidth)
+                } else {
+                    runPosition.x += position.x
+                    runPosition.y = position.y - runPosition.y
+                    runTypoBounds = CGRect(x: runPosition.x, y: runPosition.y - ascent, width: runWidth, height: ascent + descent)
+                }
+                let cfRange: CFRange = CTRunGetStringRange(run)
+                let runRange = NSRange(location: cfRange.location, length: cfRange.length)
+                
+                attachments_.append(attachment)
+                attachmentRanges_.append(NSValue(range: runRange))
+                attachmentRects_.append(NSValue(cgRect: runTypoBounds))
+            }
+        }
+        attachments = attachments_.count > 0 ? attachments_ : nil
+        attachmentRanges = attachmentRanges_.count > 0 ? attachmentRanges_ : nil
+        attachmentRects = attachmentRects_.count > 0 ? attachmentRects_ : nil
+    }
+    
+    public override var description: String {
+        var desc = ""
+        let range = self.range
+        desc += String(format: "<TextLine: %p> row: %zd range: %tu, %tu", self, row, range.location, range.length)
+        desc += " position:\(NSCoder.string(for: position))"
+        desc += " bounds:\(NSCoder.string(for: bounds))"
+        return desc
+    }
+}
+
+@objc public enum TextRunGlyphDrawMode : Int {
+    /// No rotate.
+    case horizontal = 0
+    /// Rotate vertical for single glyph.
+    case verticalRotate = 1
+    /// Rotate vertical for single glyph, and move the glyph to a better position,
+    /// such as fullwidth punctuation.
+    case verticalRotateMove = 2
+}
+
+/**
+ A range in CTRun, used for vertical form.
+ */
+public class TextRunGlyphRange: NSObject {
+    
+    @objc public var glyphRangeInRun = NSRange(location: 0, length: 0)
+    @objc public var drawMode = TextRunGlyphDrawMode.horizontal
+    
+    @objc(rangeWithRange:drawMode:)
+    public class func range(with range: NSRange, drawMode mode: TextRunGlyphDrawMode) -> TextRunGlyphRange {
+        
+        let one = TextRunGlyphRange()
+        one.glyphRangeInRun = range
+        one.drawMode = mode
+        
+        return one
+    }
+}

+ 376 - 0
Pods/BSText/BSText/Component/TextMagnifier.swift

@@ -0,0 +1,376 @@
+//
+//  TextMagnifier.swift
+//  BSText
+//
+//  Created by Bruce on 2018/12/1.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+/// Magnifier type
+@objc public enum TextMagnifierType : Int {
+    ///< Circular magnifier
+    case caret
+    ///< Round rectangle magnifier
+    case ranged
+}
+
+/**
+ A magnifier view which can be displayed in `TextEffectWindow`.
+ 
+ @discussion Use `magnifierWithType:` to create instance.
+ Typically, you should not use this class directly.
+ */
+@objc public class TextMagnifier: UIView {
+    
+    /*/< Type of magnifier */
+    @objc public private(set) var type = TextMagnifierType.caret
+    /*/< The 'best' size for magnifier view. */
+    @objc public private(set) var fitSize = CGSize.zero
+    /*/< The 'best' snapshot image size for magnifier. */
+    @objc public private(set) var snapshotSize = CGSize.zero
+    /*/< The image in magnifier (readwrite). */
+    @objc public var snapshot: UIImage?
+    /*/< The coordinate based view. */
+    @objc public weak var hostView: UIView?
+    /*/< The snapshot capture center in `hostView`. */
+    @objc public var hostCaptureCenter = CGPoint.zero
+    /*/< The popover center in `hostView`. */
+    @objc public var hostPopoverCenter = CGPoint.zero
+    /*/< The host view is vertical form. */
+    @objc public var hostVerticalForm = false
+    /*/< A hint for `TextEffectWindow` to disable capture. */
+    @objc public var captureDisabled = false
+    ///< Show fade animation when the snapshot image changed.
+    @objc public var captureFadeAnimation = false
+    
+    
+    /// Create a mangifier with the specified type. @param type The magnifier type.
+    @objc(magnifierWithType:)
+    public class func magnifier(with type: TextMagnifierType) -> TextMagnifier? {
+        switch type {
+        case .caret:
+            return TextMagnifierCaret()
+        case .ranged:
+            return TextMagnifierRanged()
+        default:
+            break
+        }
+        return nil
+    }
+    
+    fileprivate override init(frame: CGRect) {
+        super.init(frame: frame)
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+fileprivate let kCaptureDisableFadeTime = 0.1
+
+fileprivate class TextMagnifierCaret: TextMagnifier {
+    
+    var contentView: UIImageView
+    var coverView: UIImageView
+    
+    static let kMultiple: CGFloat = 1.2
+    static let kDiameter: CGFloat = 113
+    static let kPadding: CGFloat = 7
+    static let kSize = CGSize(width: kDiameter + kPadding * 2, height: kDiameter + kPadding * 2)
+
+    override init(frame: CGRect) {
+        contentView = UIImageView()
+        coverView = UIImageView()
+        super.init(frame: frame)
+        
+        contentView.frame = CGRect(x: CGFloat(TextMagnifierCaret.kPadding), y: CGFloat(TextMagnifierCaret.kPadding), width: CGFloat(TextMagnifierCaret.kDiameter), height: CGFloat(TextMagnifierCaret.kDiameter))
+        contentView.layer.cornerRadius = CGFloat(TextMagnifierCaret.kDiameter / 2)
+        contentView.clipsToBounds = true
+        addSubview(contentView)
+        coverView.frame = CGRect()
+        coverView.frame.origin = CGPoint.zero
+        coverView.frame.size = TextMagnifierCaret.kSize
+        coverView.image = TextMagnifierCaret.coverImage()
+        addSubview(coverView)
+    }
+    
+    convenience init() {
+        self.init(frame: CGRect.zero)
+        frame = CGRect.zero
+        frame.size = sizeThatFits(CGSize.zero)
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override var type: TextMagnifierType {
+        return .caret
+    }
+    
+    override func sizeThatFits(_ size: CGSize) -> CGSize {
+        return TextMagnifierCaret.kSize
+    }
+    
+    func setSnapshot(_ snapshot: UIImage?) {
+        if captureFadeAnimation {
+            contentView.layer.removeAnimation(forKey: "contents")
+            let animation = CABasicAnimation()
+            animation.duration = kCaptureDisableFadeTime
+            animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
+            contentView.layer.add(animation, forKey: "contents")
+        }
+        contentView.image = snapshot
+    }
+    
+    func snapshot() -> UIImage? {
+        return contentView.image
+    }
+    
+    func snapshotSize() -> CGSize {
+        let length = floor(TextMagnifierCaret.kDiameter / 1.2)
+        return CGSize(width: length, height: length)
+    }
+    
+    func fitSize() -> CGSize {
+        return sizeThatFits(CGSize.zero)
+    }
+    
+    static var image: UIImage?
+    class func coverImage() -> UIImage? {
+        if let i = image {
+            return i
+        }
+        let size: CGSize = kSize
+        var rect = CGRect()
+        rect.size = size
+        rect.origin = CGPoint.zero
+        rect = rect.insetBy(dx: kPadding, dy: kPadding)
+        UIGraphicsBeginImageContextWithOptions(size, _: false, _: 0)
+        let context = UIGraphicsGetCurrentContext()!
+        
+        let boxPath = CGPath(rect: CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: nil)
+        let fillPath = CGPath(ellipseIn: rect, transform: nil)
+        let strokePath = CGPath(ellipseIn: TextUtilities.textCGRect(pixelHalf: rect), transform: nil)
+        // inner shadow
+        context.saveGState()
+        do {
+            let blurRadius: CGFloat = 25
+            let offset = CGSize(width: 0, height: 15)
+            let shadowColor = UIColor(white: 0, alpha: 0.16).cgColor
+            let opaqueShadowColor = shadowColor.copy(alpha: 1)
+            context.addPath(fillPath)
+            context.clip()
+            context.setAlpha(shadowColor.alpha)
+            context.beginTransparencyLayer(auxiliaryInfo: nil)
+            do {
+                context.setShadow(offset: offset, blur: blurRadius, color: opaqueShadowColor)
+                context.setBlendMode(CGBlendMode.sourceOut)
+                context.setFillColor(opaqueShadowColor!)
+                context.addPath(fillPath)
+                context.fillPath()
+            }
+            context.endTransparencyLayer()
+        }
+        context.restoreGState()
+        
+        // outer shadow
+        context.saveGState()
+        do {
+            context.addPath(boxPath)
+            context.addPath(fillPath)
+            context.clip(using: .evenOdd)
+            let shadowColor = UIColor(white: 0, alpha: 0.32).cgColor
+            context.setShadow(offset: CGSize(width: 0, height: 1.5), blur: 3, color: shadowColor)
+            context.beginTransparencyLayer(auxiliaryInfo: nil)
+            do {
+                context.addPath(fillPath)
+                UIColor(white: 0.7, alpha: 1).setFill()
+                context.fillPath()
+            }
+            context.endTransparencyLayer()
+        }
+        context.restoreGState()
+        // stroke
+        context.saveGState()
+        do {
+            context.addPath(strokePath)
+            UIColor(white: 0.6, alpha: 1).setStroke()
+            context.setLineWidth(TextUtilities.textCGFloat(fromPixel: 1))
+            context.strokePath()
+        }
+        context.restoreGState()
+        image = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+
+        return image
+    }
+}
+
+fileprivate class TextMagnifierRanged: TextMagnifier {
+    
+    var contentView: UIImageView = UIImageView()
+    var coverView: UIImageView = UIImageView()
+    
+    static let kMultiple: CGFloat = 1.2
+    static let kSize = CGSize(width: 141, height: 60)
+    static let kPadding = TextUtilities.textCGFloat(pixelHalf: 6)
+    static let kRadius: CGFloat = 6
+    static let kHeight: CGFloat = 32
+    static let kArrow: CGFloat = 14
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        contentView.frame = CGRect(x: TextMagnifierRanged.kPadding, y: TextMagnifierRanged.kPadding, width: TextMagnifierRanged.kSize.width - 2 * TextMagnifierRanged.kPadding, height: CGFloat(TextMagnifierRanged.kHeight))
+        contentView.layer.cornerRadius = CGFloat(TextMagnifierRanged.kRadius)
+        contentView.clipsToBounds = true
+        coverView.frame = CGRect()
+        coverView.frame.origin = CGPoint.zero
+        coverView.frame.size = TextMagnifierRanged.kSize
+        coverView.image = TextMagnifierRanged.coverImage()
+        addSubview(contentView)
+        addSubview(coverView)
+        layer.anchorPoint = CGPoint(x: 0.5, y: 1.2)
+    }
+    
+    convenience init() {
+        self.init(frame: CGRect.zero)
+        frame = CGRect()
+        frame.size = sizeThatFits(CGSize.zero)
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override var type: TextMagnifierType {
+        return .ranged
+    }
+    
+    override func sizeThatFits(_ size: CGSize) -> CGSize {
+        return TextMagnifierRanged.kSize
+    }
+    
+    func setSnapshot(_ snapshot: UIImage?) {
+        if captureFadeAnimation {
+            contentView.layer.removeAnimation(forKey: "contents")
+            let animation = CABasicAnimation()
+            animation.duration = kCaptureDisableFadeTime
+            animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
+            contentView.layer.add(animation, forKey: "contents")
+        }
+        contentView.image = snapshot
+    }
+    
+    func snapshot() -> UIImage? {
+        return contentView.image
+    }
+    
+    func snapshotSize() -> CGSize {
+        var size = CGSize.zero
+        size.width = floor((TextMagnifierRanged.kSize.width - 2 * TextMagnifierRanged.kPadding) / TextMagnifierRanged.kMultiple)
+        size.height = floor(TextMagnifierRanged.kHeight / TextMagnifierRanged.kMultiple)
+        return size
+    }
+    
+    func fitSize() -> CGSize {
+        return sizeThatFits(CGSize.zero)
+    }
+
+    static var image: UIImage?
+    class func coverImage() -> UIImage? {
+        if let i = image {
+            return i
+        }
+        let size: CGSize = kSize
+        var rect = CGRect()
+        rect.size = size
+        rect.origin = CGPoint.zero
+        UIGraphicsBeginImageContextWithOptions(size, _: false, _: 0)
+        let context = UIGraphicsGetCurrentContext()!
+        let boxPath = CGPath(rect: rect, transform: nil)
+        let path = CGMutablePath()
+        path.move(to: CGPoint(x: kPadding + kRadius, y: kPadding), transform: .identity)
+        path.addLine(to: CGPoint(x: size.width - kPadding - kRadius, y: kPadding), transform: .identity)
+        path.addQuadCurve(to: CGPoint(x: size.width - kPadding, y: kPadding + kRadius), control: CGPoint(x: size.width - kPadding, y: kPadding), transform: .identity)
+        path.addLine(to: CGPoint(x: size.width - kPadding, y: kHeight), transform: .identity)
+        path.addCurve(to: CGPoint(x: size.width - kPadding - kRadius, y: kPadding + kHeight), control1: CGPoint(x: size.width - kPadding, y: kPadding + kHeight), control2: CGPoint(x: size.width - kPadding - kRadius, y: kPadding + kHeight), transform: .identity)
+        path.addLine(to: CGPoint(x: size.width / 2 + kArrow, y: kPadding + kHeight), transform: .identity)
+        path.addLine(to: CGPoint(x: size.width / 2, y: kPadding + kHeight + kArrow), transform: .identity)
+        path.addLine(to: CGPoint(x: size.width / 2 - kArrow, y: kPadding + kHeight), transform: .identity)
+        path.addLine(to: CGPoint(x: kPadding + kRadius, y: kPadding + kHeight), transform: .identity)
+        path.addQuadCurve(to: CGPoint(x: kPadding, y: kHeight), control: CGPoint(x: kPadding, y: kPadding + kHeight), transform: .identity)
+        path.addLine(to: CGPoint(x: kPadding, y: kPadding + kRadius), transform: .identity)
+        path.addQuadCurve(to: CGPoint(x: kPadding + kRadius, y: kPadding), control: CGPoint(x: kPadding, y: kPadding), transform: .identity)
+        path.closeSubpath()
+        let arrowPath = CGMutablePath()
+        arrowPath.move(to: CGPoint(x: size.width / 2 - kArrow, y: TextUtilities.textCGFloat(pixelFloor: kPadding) + kHeight), transform: .identity)
+        arrowPath.addLine(to: CGPoint(x: size.width / 2 + kArrow, y: TextUtilities.textCGFloat(pixelFloor: kPadding) + kHeight), transform: .identity)
+        arrowPath.addLine(to: CGPoint(x: size.width / 2, y: kPadding + kHeight + kArrow), transform: .identity)
+        arrowPath.closeSubpath()
+        // inner shadow
+        context.saveGState()
+        do {
+            let blurRadius: CGFloat = 25
+            let offset = CGSize(width: 0, height: 15)
+            let shadowColor = UIColor(white: 0, alpha: 0.16).cgColor
+            let opaqueShadowColor = shadowColor.copy(alpha: 1.0)
+            context.addPath(path)
+            context.clip()
+            context.setAlpha(shadowColor.alpha)
+            context.beginTransparencyLayer(auxiliaryInfo: nil)
+            do {
+                context.setShadow(offset: offset, blur: blurRadius, color: opaqueShadowColor)
+                context.setBlendMode(CGBlendMode.sourceOut)
+                context.setFillColor(opaqueShadowColor!)
+                context.addPath(path)
+                context.fillPath()
+            }
+            context.endTransparencyLayer()
+        }
+        context.restoreGState()
+        // outer shadow
+        context.saveGState()
+        do {
+            context.addPath(boxPath)
+            context.addPath(path)
+            context.clip(using: .evenOdd)
+            let shadowColor = UIColor(white: 0, alpha: 0.32).cgColor
+            context.setShadow(offset: CGSize(width: 0, height: 1.5), blur: 3, color: shadowColor)
+            context.beginTransparencyLayer(auxiliaryInfo: nil)
+            do {
+                context.addPath(path)
+                UIColor(white: 0.7, alpha: 1.000).setFill()
+                context.fillPath()
+            }
+            context.endTransparencyLayer()
+        }
+        context.restoreGState()
+        
+        // arrow
+        context.saveGState()
+        do {
+            context.addPath(arrowPath)
+            UIColor(white: 1, alpha: 0.95).set()
+            context.fillPath()
+        }
+        context.restoreGState()
+        // stroke
+        context.saveGState()
+        do {
+            context.addPath(path)
+            UIColor(white: 0.6, alpha: 1).setStroke()
+            context.setLineWidth(TextUtilities.textCGFloat(fromPixel: 1))
+            context.strokePath()
+        }
+        context.restoreGState()
+        image = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        
+        return image
+    }
+}

+ 388 - 0
Pods/BSText/BSText/Component/TextSelectionView.swift

@@ -0,0 +1,388 @@
+//
+//  TextSelectionView.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/12/3.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+fileprivate let kMarkAlpha: CGFloat = 0.2
+fileprivate let kLineWidth: CGFloat = 2.0
+fileprivate let kBlinkDuration = 0.5
+fileprivate let kBlinkFadeDuration = 0.2
+fileprivate let kBlinkFirstDelay = 0.1
+fileprivate let kTouchTestExtend: CGFloat = 14.0
+fileprivate let kTouchDotExtend: CGFloat = 7.0
+
+
+/**
+ A single dot view. The frame should be foursquare.
+ Change the background color for display.
+ 
+ @discussion Typically, you should not use this class directly.
+ */
+@objc public class SelectionGrabberDot: UIView {
+    
+    /// Dont't access this property. It was used by `TextEffectWindow`.
+    @objc public var mirror: UIView = UIView()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        isUserInteractionEnabled = false
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override public func layoutSubviews() {
+        super.layoutSubviews()
+        let length = min(bounds.size.width, bounds.size.height)
+        layer.cornerRadius = length * 0.5
+        mirror.bounds = bounds
+        mirror.layer.cornerRadius = layer.cornerRadius
+    }
+    
+    func setBackgroundColor(_ backgroundColor: UIColor?) {
+        super.backgroundColor = backgroundColor
+        mirror.backgroundColor = backgroundColor
+    }
+}
+
+/**
+ A grabber (stick with a dot).
+ 
+ @discussion Typically, you should not use this class directly.
+ */
+@objc public class SelectionGrabber: UIView {
+    
+    /*/< the dot view */
+    @objc public private(set) var dot = SelectionGrabberDot(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
+    
+    /*/< don't support composite direction */
+    @objc public var dotDirection = TextDirection.none {
+        didSet {
+            addSubview(dot)
+            var frame: CGRect = dot.frame
+            let ofs: CGFloat = 0.5
+            if dotDirection == TextDirection.top {
+                frame.origin.y = -frame.size.height + ofs
+                frame.origin.x = (bounds.size.width - frame.size.width) / 2
+            } else if dotDirection == TextDirection.right {
+                frame.origin.x = bounds.size.width - ofs
+                frame.origin.y = (bounds.size.height - frame.size.height) / 2
+            } else if dotDirection == TextDirection.bottom {
+                frame.origin.y = bounds.size.height - ofs
+                frame.origin.x = (bounds.size.width - frame.size.width) / 2
+            } else if dotDirection == TextDirection.left {
+                frame.origin.x = -frame.size.width + ofs
+                frame.origin.y = (bounds.size.height - frame.size.height) / 2
+            } else {
+                dot.removeFromSuperview()
+            }
+            dot.frame = frame
+        }
+    }
+    
+    ///< tint color, default is nil
+    public var color: UIColor? {
+        willSet {
+            backgroundColor = newValue
+            dot.backgroundColor = newValue
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override public func layoutSubviews() {
+        super.layoutSubviews()
+        
+        let d = dotDirection
+        self.dotDirection = d
+    }
+    
+    func touchRect() -> CGRect {
+        var rect: CGRect = frame.insetBy(dx: -kTouchTestExtend, dy: -kTouchTestExtend)
+        var insets = UIEdgeInsets.zero
+        if dotDirection == TextDirection.top {
+            insets.top = -kTouchDotExtend
+        } else if dotDirection == TextDirection.right {
+            insets.right = -kTouchDotExtend
+        } else if dotDirection == TextDirection.bottom {
+            insets.bottom = -kTouchDotExtend
+        } else if dotDirection == TextDirection.left {
+            insets.left = -kTouchDotExtend
+        }
+        rect = rect.inset(by: insets)
+        return rect
+    }
+}
+
+/**
+ The selection view for text edit and select.
+ 
+ @discussion Typically, you should not use this class directly.
+ */
+@objc public class TextSelectionView: UIView {
+    
+    /*/< the holder view */
+    @objc public weak var hostView: UIView?
+    
+    /*/< the tint color */
+    @objc public var color: UIColor? {
+        didSet {
+            caretView.backgroundColor = color
+            startGrabber.color = color
+            endGrabber.color = color
+            for v in markViews {
+                v.backgroundColor = color
+            }
+        }
+    }
+    
+    /*/< whether the caret is blinks */
+    @objc public var caretBlinks = false {
+        willSet {
+            if caretBlinks != newValue {
+                caretView.alpha = 1
+                NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self._startBlinks), object: nil)
+                if newValue {
+                    perform(#selector(self._startBlinks), with: nil, afterDelay: kBlinkFirstDelay)
+                } else {
+                    caretTimer?.invalidate()
+                    caretTimer = nil
+                }
+            }
+        }
+    }
+    
+    /*/< whether the caret is visible */
+    @objc public var caretVisible = false {
+        didSet {
+            caretView.isHidden = !caretVisible
+            caretView.alpha = 1
+            NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self._startBlinks), object: nil)
+            if caretBlinks {
+                perform(#selector(self._startBlinks), with: nil, afterDelay: kBlinkFirstDelay)
+            }
+        }
+    }
+    
+    /*/< weather the text view is vertical form */
+    @objc public var verticalForm = false {
+        didSet {
+            if (verticalForm != oldValue) {
+                let c = caretRect
+                self.caretRect = c
+                startGrabber.dotDirection = verticalForm ? TextDirection.right : TextDirection.top
+                endGrabber.dotDirection = verticalForm ? TextDirection.left : TextDirection.bottom
+            }
+        }
+    }
+    
+    /*/< caret rect (width==0 or height==0) */
+    @objc public var caretRect = CGRect.zero {
+        didSet {
+            caretView.frame = _standardCaretRect(caretRect)
+            let minWidth = min(caretView.bounds.size.width, caretView.bounds.size.height)
+            caretView.layer.cornerRadius = minWidth / 2
+        }
+    }
+    
+    /*/< default is nil */
+    @objc public var selectionRects: [TextSelectionRect]? {
+        didSet {
+            for v in markViews {
+                v.removeFromSuperview()
+            }
+            markViews.removeAll()
+            startGrabber.isHidden = true
+            endGrabber.isHidden = true
+            (selectionRects as NSArray?)?.enumerateObjects({ r, idx, stop in
+                guard let tmpr = r as? TextSelectionRect else {
+                    return
+                }
+                var rect: CGRect = tmpr.rect
+                rect = rect.standardized
+                rect = TextUtilities.textCGRect(pixelRound: rect)
+                if tmpr.containsStart || tmpr.containsEnd {
+                    rect = self._standardCaretRect(rect)
+                    if tmpr.containsStart {
+                        self.startGrabber.isHidden = false
+                        self.startGrabber.frame = rect
+                    }
+                    if tmpr.containsEnd {
+                        self.endGrabber.isHidden = false
+                        self.endGrabber.frame = rect
+                    }
+                } else {
+                    if (rect.size.width > 0) && (rect.size.height > 0) {
+                        let mark = UIView(frame: rect)
+                        mark.backgroundColor = self.color
+                        mark.alpha = kMarkAlpha
+                        self.insertSubview(mark, at: 0)
+                        self.markViews.append(mark)
+                    }
+                }
+            })
+        }
+    }
+    
+    @objc public private(set) var caretView = UIView()
+    @objc public private(set) var startGrabber = SelectionGrabber()
+    @objc public private(set) var endGrabber = SelectionGrabber()
+    
+    private var caretTimer: Timer?
+    private lazy var markViews: [UIView] = []
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        isUserInteractionEnabled = false
+        clipsToBounds = false
+        
+        caretView.isHidden = true
+        startGrabber.dotDirection = TextDirection.top
+        startGrabber.isHidden = true
+        endGrabber.dotDirection = TextDirection.bottom
+        endGrabber.isHidden = true
+        addSubview(startGrabber)
+        addSubview(endGrabber)
+        addSubview(caretView)
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    deinit {
+        caretTimer?.invalidate()
+    }
+    
+    @objc func _startBlinks() {
+        caretTimer?.invalidate()
+        if caretVisible {
+            caretTimer = Timer.bs_scheduledTimer(with: kBlinkDuration, target: self, selector: #selector(self._doBlink), userInfo: nil, repeats: true)
+            RunLoop.current.add(caretTimer!, forMode: .default)
+        } else {
+            caretView.alpha = 1
+        }
+    }
+    
+    @objc func _doBlink() {
+        UIView.animate(withDuration: kBlinkFadeDuration, delay: 0, options: .curveEaseInOut, animations: {
+            if self.caretView.alpha == 1 {
+                self.caretView.alpha = 0
+            } else {
+                self.caretView.alpha = 1
+            }
+        })
+    }
+    
+    private func _standardCaretRect(_ caretRect: CGRect) -> CGRect {
+        var c = caretRect.standardized
+        if verticalForm {
+            if c.size.height == 0 {
+                c.size.height = kLineWidth
+                c.origin.y -= kLineWidth * 0.5
+            }
+            if c.origin.y < 0 {
+                c.origin.y = 0
+            } else if c.origin.y + c.size.height > bounds.size.height {
+                c.origin.y = bounds.size.height - c.size.height
+            }
+        } else {
+            if c.size.width == 0 {
+                c.size.width = kLineWidth
+                c.origin.x -= kLineWidth * 0.5
+            }
+            if c.origin.x < 0 {
+                c.origin.x = 0
+            } else if c.origin.x + c.size.width > bounds.size.width {
+                c.origin.x = bounds.size.width - c.size.width
+            }
+        }
+        c = TextUtilities.textCGRect(pixelRound: c)
+        if c.origin.x.isNaN || c.origin.x.isInfinite {
+            c.origin.x = 0
+        }
+        if c.origin.y.isNaN || c.origin.y.isInfinite {
+            c.origin.y = 0
+        }
+        if c.size.width.isNaN || c.size.width.isInfinite {
+            c.size.width = 0
+        }
+        if c.size.height.isNaN || c.size.height.isInfinite {
+            c.size.height = 0
+        }
+        return c
+    }
+    
+    @objc(isGrabberContainsPoint:)
+    public func isGrabberContains(_ point: CGPoint) -> Bool {
+        return isStartGrabberContains(point) || isEndGrabberContains(point)
+    }
+    
+    @objc(isStartGrabberContainsPoint:)
+    public func isStartGrabberContains(_ point: CGPoint) -> Bool {
+        if startGrabber.isHidden {
+            return false
+        }
+        let startRect: CGRect = startGrabber.touchRect()
+        let endRect: CGRect = endGrabber.touchRect()
+        if startRect.intersects(endRect) {
+            let distStart = TextUtilities.textCGPointGetDistance(to: point, p2: TextUtilities.textCGRectGetCenter(startRect))
+            let distEnd = TextUtilities.textCGPointGetDistance(to: point, p2: TextUtilities.textCGRectGetCenter(endRect))
+            if distEnd <= distStart {
+                return false
+            }
+        }
+        return startRect.contains(point)
+    }
+    
+    @objc(isEndGrabberContainsPoint:)
+    public func isEndGrabberContains(_ point: CGPoint) -> Bool {
+        if endGrabber.isHidden {
+            return false
+        }
+        let startRect: CGRect = startGrabber.touchRect()
+        let endRect: CGRect = endGrabber.touchRect()
+        if startRect.intersects(endRect) {
+            let distStart = TextUtilities.textCGPointGetDistance(to: point, p2: TextUtilities.textCGRectGetCenter(startRect))
+            let distEnd = TextUtilities.textCGPointGetDistance(to: point, p2: TextUtilities.textCGRectGetCenter(endRect))
+            if distEnd > distStart {
+                return false
+            }
+        }
+        return endRect.contains(point)
+    }
+    
+    @objc(isCaretContainsPoint:)
+    public func isCaretContains(_ point: CGPoint) -> Bool {
+        if caretVisible {
+            let rect: CGRect = caretRect.insetBy(dx: -kTouchTestExtend, dy: -kTouchTestExtend)
+            return rect.contains(point)
+        }
+        return false
+    }
+    
+    @objc(isSelectionRectsContainsPoint:)
+    public func isSelectionRectsContains(_ point: CGPoint) -> Bool {
+        guard let s = selectionRects, s.count > 0 else {
+            return false
+        }
+        for r in s {
+            if r.rect.contains(point) {
+                return true
+            }
+        }
+        return false
+    }
+}

+ 305 - 0
Pods/BSText/BSText/String/TextArchiver.swift

@@ -0,0 +1,305 @@
+//
+//  TextArchiver.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/10/25.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+import CoreImage
+
+/**
+ A subclass of `NSKeyedArchiver` which implement `NSKeyedArchiverDelegate` protocol.
+ 
+ The archiver can encode the object which contains
+ CGColor/CGImage/CTRunDelegate/.. (such as NSAttributedString).
+ */
+public class TextArchiver: NSKeyedArchiver, NSKeyedArchiverDelegate {
+    
+    override public class func archivedData(withRootObject rootObject: Any) -> Data {
+        
+        var data: Data
+        
+        if #available(iOS 11.0, *) {
+            let archiver = TextArchiver(requiringSecureCoding: false)
+            archiver.encodeRootObject(rootObject)
+
+            data = archiver.encodedData
+        } else {
+            let d = NSMutableData()
+            let archiver = TextArchiver(forWritingWith: d)
+            archiver.encodeRootObject(rootObject)
+            archiver.finishEncoding()
+            
+            data = d as Data
+        }
+        
+        return data
+    }
+    
+    override public class func archiveRootObject(_ rootObject: Any, toFile path: String) -> Bool {
+        
+        let data = self.archivedData(withRootObject: rootObject)
+        
+        var success = false
+        
+        do {
+            try data.write(to: URL(fileURLWithPath: path), options: .atomicWrite)
+            success = true
+        } catch let err {
+            print(err)
+        }
+        
+        return success
+    }
+    
+    @available(iOS, introduced: 10.0, deprecated: 12.0, message: "Use -initRequiringSecureCoding: instead")
+    private override init() {
+        super.init()
+    }
+    
+    @available(iOS, introduced: 2.0, deprecated: 12.0, message: "Use -initRequiringSecureCoding: instead")
+    private override init(forWritingWith data: NSMutableData) {
+        super.init(forWritingWith: data)
+        delegate = self
+    }
+    
+    @available(iOS 11.0, *)
+    private override init(requiringSecureCoding requiresSecureCoding: Bool) {
+        super.init(requiringSecureCoding: requiresSecureCoding)
+        delegate = self
+    }
+    
+    // MARK: - NSKeyedArchiverDelegate
+    public func archiver(_ archiver: NSKeyedArchiver, willEncode object: Any) -> Any? {
+        
+        let typeID = CFGetTypeID(object as CFTypeRef)
+        
+        if typeID == CTRunDelegateGetTypeID() {
+            
+            let runDelegate = object as! CTRunDelegate
+            let ref = CTRunDelegateGetRefCon(runDelegate)
+            
+            // UnsafeMutableRawPointer 需要用 load 或 (用 assumingMemoryBound 将 UnsafeMutableRawPointer 转为 UnsafeMutablePointer 然后取其 pointee), 不能用 unsafeBitCast
+            // 错误示例:unsafeBitCast(ref, to: TextRunDelegate.self) 会导致 Crash
+//            let p = ref.assumingMemoryBound(to: TextRunDelegate.self)
+//            return p.pointee
+            return ref.load(as: TextRunDelegate.self)
+            
+        } else if typeID == CTRubyAnnotationGetTypeID() {
+            
+            let ctRuby = object as! CTRubyAnnotation
+            let ruby = TextRubyAnnotation.ruby(with: ctRuby)
+            
+            return ruby
+            
+        } else if typeID == CGColor.typeID {
+            
+            let anObject = object as! CGColor
+            return BSCGColor(cgColor: anObject)
+            
+        } else if typeID == CGImage.typeID {
+            
+            let anObject = object as! CGImage
+            return BSCGImage(cgImage: anObject)
+        }
+        
+        return object
+    }
+}
+
+// MARK: - 临时用来解决粘贴gif图片的问题(rundelegate会提前释放),后续需要优化,如何保证CTRunDelegate的生命周期
+var k_sharedDelegate = [Int: CTRunDelegate]()
+
+/**
+ A subclass of `NSKeyedUnarchiver` which implement `NSKeyedUnarchiverDelegate`
+ protocol. The unarchiver can decode the data which is encoded by
+ `TextArchiver` or `NSKeyedArchiver`.
+ */
+public class TextUnarchiver: NSKeyedUnarchiver, NSKeyedUnarchiverDelegate {
+    
+    override public class func unarchiveObject(with data: Data) -> Any? {
+        if data.count == 0 {
+            return nil
+        }
+        
+        // MARK: - 临时修复 CTDelegate 的内存引用问题
+        if k_sharedDelegate.count > 0 {
+            TextUnarchiver.clearRef()
+        }
+        
+        var unarchiver: TextUnarchiver? = nil
+        
+        if #available(iOS 11.0, *) {
+            do {
+                unarchiver = try TextUnarchiver.init(forReadingFrom: data)
+                unarchiver?.requiresSecureCoding = false
+            } catch let err {
+                print(err)
+            }
+        } else {
+            unarchiver = TextUnarchiver.init(forReadingWith: data)
+        }
+        
+        return unarchiver?.decodeObject()
+    }
+    
+    override public class func unarchiveObject(withFile path: String) -> Any? {
+        let data = NSData(contentsOfFile: path) as Data?
+        if let aData = data {
+            return self.unarchiveObject(with: aData)
+        }
+        return nil
+    }
+    
+    @available(iOS, introduced: 2.0, deprecated: 12.0, message: "Use -initForReadingFromData:error: instead")
+    override private init() {
+        super.init()
+        delegate = self
+    }
+    
+    @available(iOS, introduced: 2.0, deprecated: 12.0, message: "Use -initForReadingFromData:error: instead")
+    override private init(forReadingWith data: Data) {
+        super.init(forReadingWith: data)
+        delegate = self
+    }
+    
+    @available(iOS 11.0, *)
+    override private init(forReadingFrom data: Data) throws {
+        try super.init(forReadingFrom: data)
+        
+        delegate = self
+    }
+    
+    // MARK: - NSKeyedUnarchiverDelegate
+    public func unarchiver(_ unarchiver: NSKeyedUnarchiver, didDecode object: Any?) -> Any? {
+        
+        guard let obj = object else {
+            return nil
+        }
+        
+        if type(of: obj) == TextRunDelegate.self {
+            
+            let runDelegate = obj as! TextRunDelegate
+            let ct = runDelegate.ctRunDelegate
+            // MARK: - 这里有强引用,为什么不强引用它会提前释放?
+            k_sharedDelegate[k_sharedDelegate.count] = ct
+            
+            return ct
+            
+        } else if type(of: obj) == TextRubyAnnotation.self {
+            
+            let ruby = object as! TextRubyAnnotation
+            let ct = ruby.ctRubyAnnotation
+            
+            return ct
+            
+        } else if type(of: obj) == BSCGColor.self {
+            
+            let color = obj as! BSCGColor
+            return color.cgColor
+            
+        } else if type(of: obj) == BSCGImage.self {
+            
+            let image = obj as! BSCGImage
+            return image.cgImage
+        }
+        
+        return object
+    }
+    
+    private class func clearRef() -> Void {
+        
+        k_sharedDelegate = [Int: CTRunDelegate]()
+    }
+}
+
+/**
+ A wrapper for CGColorRef. Used for Archive/Unarchive/Copy.
+ */
+@objc(_TtC6BSTextP33_C72E39273DDC44DAA5EC7067D26023719BSCGColor)
+fileprivate final class BSCGColor: NSObject, NSCopying, NSCoding, NSSecureCoding {
+    
+    var cgColor: CGColor?
+    
+    override init() {
+        super.init()
+    }
+    
+    @objc fileprivate convenience init(cgColor CGColor: CGColor?) {
+        self.init()
+        self.cgColor = CGColor
+    }
+    
+    // MARK: - NSCopying
+    @objc func copy(with zone: NSZone? = nil) -> Any {
+        let c = BSCGColor(cgColor: cgColor)
+        return c
+    }
+    
+    // MARK: - NSCoding
+    @objc func encode(with aCoder: NSCoder) {
+        var color: UIColor? = nil
+        if let aColor = cgColor {
+            color = UIColor(cgColor: aColor)
+        }
+        aCoder.encode(color, forKey: "color")
+    }
+    
+    @objc required init?(coder aDecoder: NSCoder) {
+        super.init()
+        let color = aDecoder.decodeObject(forKey: "color") as! UIColor?
+        self.cgColor = color?.cgColor
+    }
+    
+    // MARK: - NSSecureCoding
+    @objc public static var supportsSecureCoding: Bool {
+        return true
+    }
+}
+
+/**
+ A wrapper for CGImage. Used for Archive/Unarchive/Copy.
+ */
+@objc(_TtC6BSTextP33_C72E39273DDC44DAA5EC7067D26023719BSCGImage)
+fileprivate final class BSCGImage: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    var cgImage: CGImage?
+    
+    override init() {
+        super.init()
+    }
+    
+    @objc fileprivate convenience init(cgImage: CGImage?) {
+        self.init()
+        self.cgImage = cgImage
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let image = BSCGImage()
+        image.cgImage = cgImage
+        return image
+    }
+    
+    // MARK: - NSCoding
+    public func encode(with aCoder: NSCoder) {
+        var image: UIImage? = nil
+        if let anImage = cgImage {
+            image = UIImage(cgImage: anImage)
+        }
+        aCoder.encode(image, forKey: "image")
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init()
+        let image = aDecoder.decodeObject(forKey: "image") as? UIImage
+        self.cgImage = image?.cgImage
+    }
+    
+    // MARK: - NSSecureCoding
+    @objc public static var supportsSecureCoding: Bool {
+        return true
+    }
+}

+ 542 - 0
Pods/BSText/BSText/String/TextParser.swift

@@ -0,0 +1,542 @@
+//
+//  TextParser.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/10/23.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+/**
+ The TextParser protocol declares the required method for BSTextView and BSLabel
+ to modify the text during editing.
+ 
+ You can implement this protocol to add code highlighting or emoticon replacement for
+ BSTextView and BSLabel. See `TextSimpleMarkdownParser` and `TextSimpleEmoticonParser` for example.
+ */
+@objc public protocol TextParser: NSObjectProtocol {
+    /**
+     When text is changed in BSTextView or BSLabel, this method will be called.
+     
+     @param text  The original attributed string. This method may parse the text and
+     change the text attributes or content.
+     
+     @param selectedRange  Current selected range in `text`.
+     This method should correct the range if the text content is changed. If there's
+     no selected range (such as BSLabel), this value is NULL.
+     
+     @return If the 'text' is modified in this method, returns `YES`, otherwise returns `NO`.
+     */
+    @discardableResult
+    func parseText(_ text: NSMutableAttributedString?, selectedRange: NSRangePointer?) -> Bool
+}
+
+
+/**
+ A simple markdown parser.
+ 
+ It'a very simple markdown parser, you can use this parser to highlight some
+ small piece of markdown text.
+ 
+ This markdown parser use regular expression to parse text, slow and weak.
+ If you want to write a better parser, try these projests:
+ https://github.com/NimbusKit/markdown
+ https://github.com/dreamwieber/AttributedMarkdown
+ https://github.com/indragiek/CocoaMarkdown
+ 
+ Or you can use lex/yacc to generate your custom parser.
+ */
+public class TextSimpleMarkdownParser: NSObject, TextParser {
+    
+    private var font: UIFont?
+    ///< h1~h6
+    private var headerFonts: [UIFont] = []
+    private var boldFont: UIFont?
+    private var italicFont: UIFont?
+    private var boldItalicFont: UIFont?
+    private var monospaceFont: UIFont?
+    private var border = TextBorder()
+    ///< escape
+    private var regexEscape = try! NSRegularExpression(pattern: "(\\\\\\\\|\\\\\\`|\\\\\\*|\\\\\\_|\\\\\\(|\\\\\\)|\\\\\\[|\\\\\\]|\\\\#|\\\\\\+|\\\\\\-|\\\\\\!)", options: [])
+    ///< #header
+    private var regexHeader = try! NSRegularExpression(pattern: "^((\\#{1,6}[^#].*)|(\\#{6}.+))$", options: .anchorsMatchLines)
+    ///< header\n====
+    private var regexH1 = try! NSRegularExpression(pattern: "^[^=\\n][^\\n]*\\n=+$", options: .anchorsMatchLines)
+    ///< header\n----
+    private var regexH2 = try! NSRegularExpression(pattern: "^[^-\\n][^\\n]*\\n-+$", options: .anchorsMatchLines)
+    ///< ******
+    private var regexBreakline = try! NSRegularExpression(pattern: "^[ \\t]*([*-])[ \\t]*((\\1)[ \\t]*){2,}[ \\t]*$", options: .anchorsMatchLines)
+    ///< *text*  _text_
+    private var regexEmphasis = try! NSRegularExpression(pattern: "((?<!\\*)\\*(?=[^ \\t*])(.+?)(?<=[^ \\t*])\\*(?!\\*)|(?<!_)_(?=[^ \\t_])(.+?)(?<=[^ \\t_])_(?!_))", options: [])
+    ///< **text**
+    private var regexStrong = try! NSRegularExpression(pattern: "(?<!\\*)\\*{2}(?=[^ \\t*])(.+?)(?<=[^ \\t*])\\*{2}(?!\\*)", options: [])
+    ///< ***text*** ___text___
+    private var regexStrongEmphasis = try! NSRegularExpression(pattern: "((?<!\\*)\\*{3}(?=[^ \\t*])(.+?)(?<=[^ \\t*])\\*{3}(?!\\*)|(?<!_)_{3}(?=[^ \\t_])(.+?)(?<=[^ \\t_])_{3}(?!_))", options: [])
+    ///< __text__
+    private var regexUnderline = try! NSRegularExpression(pattern: "(?<!_)__(?=[^ \\t_])(.+?)(?<=[^ \\t_])\\__(?!_)", options: [])
+    ///< ~~text~~
+    private var regexStrikethrough = try! NSRegularExpression(pattern: "(?<!~)~~(?=[^ \\t~])(.+?)(?<=[^ \\t~])\\~~(?!~)", options: [])
+    ///< `text`
+    private var regexInlineCode = try! NSRegularExpression(pattern: "(?<!`)(`{1,3})([^`\n]+?)\\1(?!`)", options: [])
+    ///< [name](link)
+    private var regexLink = try! NSRegularExpression(pattern: "!?\\[([^\\[\\]]+)\\](\\(([^\\(\\)]+)\\)|\\[([^\\[\\]]+)\\])", options: [])
+    ///< [ref]:
+    private var regexLinkRefer = try! NSRegularExpression(pattern: "^[ \\t]*\\[[^\\[\\]]\\]:", options: .anchorsMatchLines)
+    ///< 1.text 2.text 3.text
+    private var regexList = try! NSRegularExpression(pattern: "^[ \\t]*([*+-]|\\d+[.])[ \\t]+", options: .anchorsMatchLines)
+    ///< > quote
+    private var regexBlockQuote = try! NSRegularExpression(pattern: "^[ \\t]*>[ \\t>]*", options: .anchorsMatchLines)
+    ///< \tcode \tcode
+    private var regexCodeBlock = try! NSRegularExpression(pattern: "(^\\s*$\\n)((( {4}|\\t).*(\\n|\\z))|(^\\s*$\\n))+", options: .anchorsMatchLines)
+    private var regexNotEmptyLine = try! NSRegularExpression(pattern: "^[ \\t]*[^ \\t]+[ \\t]*$", options: .anchorsMatchLines)
+    
+    private var _fontSize: CGFloat = 14
+    
+    /*/< default is 14 */
+    public var fontSize: CGFloat {
+        set {
+            if newValue < 1 {
+                _fontSize = 12
+            } else {
+                _fontSize = newValue
+            }
+            _updateFonts()
+        }
+        get {
+            return _fontSize
+        }
+    }
+    
+    private var _headerFontSize: CGFloat = 20
+    
+    /*/< default is 20 */
+    public var headerFontSize: CGFloat {
+        set {
+            if newValue < 1 {
+                _headerFontSize = 20
+            } else {
+                _headerFontSize = newValue
+            }
+            _updateFonts()
+        }
+        get {
+            return _headerFontSize
+        }
+    }
+    
+    public var textColor = UIColor.white
+    public var controlTextColor: UIColor?
+    public var headerTextColor: UIColor?
+    public var inlineTextColor: UIColor?
+    public var codeTextColor: UIColor?
+    public var linkTextColor: UIColor?
+    
+    ///< reset the color properties to pre-defined value.
+    @objc public func setColorWithBrightTheme() {
+        textColor = UIColor.black
+        controlTextColor = UIColor(white: 0.749, alpha: 1.000)
+        headerTextColor = UIColor(red: 1.000, green: 0.502, blue: 0.000, alpha: 1.000)
+        inlineTextColor = UIColor(white: 0.150, alpha: 1.000)
+        codeTextColor = UIColor(white: 0.150, alpha: 1.000)
+        linkTextColor = UIColor(red: 0.000, green: 0.478, blue: 0.962, alpha: 1.000)
+        border = TextBorder()
+        border.lineStyle = TextLineStyle.single
+        border.fillColor = UIColor(white: 0.184, alpha: 0.090)
+        border.strokeColor = UIColor(white: 0.546, alpha: 0.650)
+        border.insets = UIEdgeInsets(top: -1, left: 0, bottom: -1, right: 0)
+        border.cornerRadius = 2
+        border.strokeWidth = TextUtilities.textCGFloat(fromPixel: 1)
+    }
+    
+    ///< reset the color properties to pre-defined value.
+    @objc public func setColorWithDarkTheme() {
+        textColor = UIColor.white
+        controlTextColor = UIColor(white: 0.604, alpha: 1.000)
+        headerTextColor = UIColor(red: 0.558, green: 1.000, blue: 0.502, alpha: 1.000)
+        inlineTextColor = UIColor(red: 1.000, green: 0.862, blue: 0.387, alpha: 1.000)
+        codeTextColor = UIColor(white: 0.906, alpha: 1.000)
+        linkTextColor = UIColor(red: 0.000, green: 0.646, blue: 1.000, alpha: 1.000)
+        border = TextBorder()
+        border.lineStyle = TextLineStyle.single
+        border.fillColor = UIColor(white: 0.820, alpha: 0.130)
+        border.strokeColor = UIColor(white: 1.000, alpha: 0.280)
+        border.insets = UIEdgeInsets(top: -1, left: 0, bottom: -1, right: 0)
+        border.cornerRadius = 2
+        border.strokeWidth = TextUtilities.textCGFloat(fromPixel: 1)
+    }
+    
+    @objc public override init() {
+        super.init()
+        
+        _updateFonts()
+        setColorWithBrightTheme()
+    }
+    
+    private func _updateFonts() {
+        font = UIFont.systemFont(ofSize: fontSize)
+        headerFonts = [UIFont]()
+        for i in 0..<6 {
+            let size: CGFloat = headerFontSize - (headerFontSize - fontSize) / 5.0 * CGFloat(integerLiteral: i)
+            headerFonts.append(UIFont.systemFont(ofSize: size))
+        }
+        boldFont = TextUtilities.textFont(withBold: font)
+        italicFont = TextUtilities.textFont(withItalic: font)
+        boldItalicFont = TextUtilities.textFont(withBoldItalic: font)
+        monospaceFont = UIFont(name: "Menlo", size: fontSize)
+        if monospaceFont == nil {
+            monospaceFont = UIFont(name: "Courier", size: fontSize)
+        }
+    }
+    
+    private func lenghOfBeginWhite(in str: String?, with range: NSRange) -> Int {
+        guard let s = str else {
+            return 0
+        }
+        for i in 0..<range.length {
+            let c = String(s[(s.index(s.startIndex, offsetBy: i + range.location))])
+            if c != " " && c != "\t" && c != "\n" {
+                return i
+            }
+        }
+        return s.length
+    }
+    
+    private func lenghOfEndWhite(in str: String?, with range: NSRange) -> Int {
+        guard let s = str else {
+            return 0
+        }
+        var i = range.length - 1
+        while i >= 0 {
+            let c = String(s[(s.index(s.startIndex, offsetBy: i + range.location))])
+            if c != " " && c != "\t" && c != "\n" {
+                return (range.length - i)
+            }
+            i -= 1
+        }
+        return s.length
+    }
+    
+    private func lenghOfBeginChar(_ c: Character, in str: String?, with range: NSRange) -> Int {
+        guard let s = str, s != "" else {
+            return 0
+        }
+        for i in 0..<range.length {
+            if s[(s.index(s.startIndex, offsetBy: i + range.location))] != c {
+                return i
+            }
+        }
+        return s.length
+    }
+    
+    @objc public func parseText(_ text: NSMutableAttributedString?, selectedRange range: NSRangePointer?) -> Bool {
+        
+        guard let t = text, t.length > 0 else {
+            return false
+        }
+        
+        t.bs_removeAttributes(in: NSRange(location: 0, length: t.length))
+        t.bs_font = font
+        t.bs_color = textColor
+        let str = t.string
+        
+        regexEscape.replaceMatches(in: str as! NSMutableString, options: [], range: NSRange(location: 0, length: str.length), withTemplate: "@@")
+        
+        regexHeader.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r: NSRange = result!.range
+            let whiteLen = self.lenghOfBeginWhite(in: str, with: r)
+            var sharpLen = self.lenghOfBeginChar("#"["#".startIndex], in: str, with: NSRange(location: r.location + whiteLen, length: r.length - whiteLen))
+            if sharpLen > 6 {
+                sharpLen = 6
+            }
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: r.location, length: whiteLen + sharpLen))
+            text?.bs_set(color: self.headerTextColor, range: NSRange(location: r.location + whiteLen + sharpLen, length: r.length - whiteLen - sharpLen))
+            text?.bs_set(font: self.headerFonts[sharpLen - 1], range: r)
+        })
+        
+        regexH1.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            var linebreak: NSRange? = nil
+            if let tmpRange = str.range(of: "\n", options: [], range: Range(r, in: str)!, locale: nil) {
+                linebreak = NSRange(tmpRange, in: str)
+            }
+            
+            if (linebreak?.location ?? 0) != NSNotFound {
+                text?.bs_set(color: self.headerTextColor, range: NSRange(location: r.location, length: ((linebreak?.location ?? 0) - r.location)))
+                text?.bs_set(font: self.headerFonts.first, range: NSRange(location: r.location, length: ((linebreak?.location ?? 0) - r.location) + 1))
+                text?.bs_set(color: self.controlTextColor, range: NSRange(location: ((linebreak?.location ?? 0) + (linebreak?.length ?? 0)), length: (r.location + r.length - (linebreak?.location ?? 0) - (linebreak?.length ?? 0))))
+            }
+        })
+        
+        regexH2.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            var linebreak: NSRange? = nil
+            if let tmpRange = str.range(of: "\n", options: [], range: Range(r, in: str)!, locale: nil) {
+                linebreak = NSRange(tmpRange, in: str)
+            }
+            
+            if (linebreak?.location ?? 0) != NSNotFound {
+                text?.bs_set(color: self.headerTextColor, range: NSRange(location: r.location, length: ((linebreak?.location ?? 0) - r.location)))
+                text?.bs_set(font: self.headerFonts[1], range: NSRange(location: r.location, length: ((linebreak?.location ?? 0) - r.location) + 1))
+                text?.bs_set(color: self.controlTextColor, range: NSRange(location: ((linebreak?.location ?? 0) + (linebreak?.length ?? 0)), length: (r.location + r.length - (linebreak?.location ?? 0) - (linebreak?.length ?? 0))))
+            }
+        })
+        
+        regexBreakline.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            text?.bs_set(color: self.controlTextColor, range: (result?.range)!)
+        })
+        
+        regexEmphasis.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: r.location, length: 1))
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: (r.location + r.length) - 1, length: 1))
+            text?.bs_set(font: self.italicFont, range: NSRange(location: r.location + 1, length: r.length - 2))
+        })
+        
+        regexStrong.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: r.location, length: 2))
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: (r.location + r.length) - 2, length: 2))
+            text?.bs_set(font: self.boldFont, range: NSRange(location: r.location + 2, length: r.length - 4))
+        })
+        
+        regexStrongEmphasis.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: r.location, length: 3))
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: (r.location + r.length) - 3, length: 3))
+            text?.bs_set(font: self.boldItalicFont, range: NSRange(location: r.location + 3, length: r.length - 6))
+        })
+
+        regexUnderline.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: r.location, length: 2))
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: (r.location + r.length) - 2, length: 2))
+            text?.bs_set(textUnderline: TextDecoration.decoration(with: TextLineStyle.single, width: 1, color: nil), range: NSRange(location: r.location + 2, length: r.length - 4))
+        })
+        
+        regexStrikethrough.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: r.location, length: 2))
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: (r.location + r.length) - 2, length: 2))
+            text?.bs_set(textStrikethrough: TextDecoration.decoration(with: TextLineStyle.single, width: 1, color: nil), range: NSRange(location: r.location + 2, length: r.length - 4))
+        })
+        
+        regexInlineCode.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            let len: Int = self.lenghOfBeginChar("`", in: str, with: r)
+            
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: r.location, length: len))
+            text?.bs_set(color: self.controlTextColor, range: NSRange(location: (r.location + r.length) - len, length: len))
+            text?.bs_set(color: self.inlineTextColor, range: NSRange(location: r.location + len, length: r.length - 2 * len))
+            text?.bs_set(font: self.monospaceFont, range: r)
+            text?.bs_set(textBorder: (self.border.copy() as? TextBorder), range: r)
+        })
+
+        regexLink.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.linkTextColor, range: r)
+        })
+        
+        regexLinkRefer.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.controlTextColor, range: r)
+        })
+        
+        regexList.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.controlTextColor, range: r)
+        })
+
+        regexBlockQuote.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            text?.bs_set(color: self.controlTextColor, range: r)
+        })
+        
+        regexCodeBlock.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            let r = result!.range
+            let firstLineRange = self.regexNotEmptyLine.rangeOfFirstMatch(in: str, options: [], range: r)
+            
+            let lenStart = (firstLineRange.location != NSNotFound) ? (firstLineRange.location - r.location) : 0
+            let lenEnd: Int = self.lenghOfEndWhite(in: str, with: r)
+            if lenStart + lenEnd < r.length {
+                let codeR = NSRange(location: r.location + lenStart, length: r.length - lenStart - lenEnd)
+                text?.bs_set(color: self.codeTextColor, range: codeR)
+                text?.bs_set(font: self.monospaceFont, range: codeR)
+                let border = TextBorder()
+                border.lineStyle = TextLineStyle.single
+                border.fillColor = UIColor(white: 0.184, alpha: 0.090)
+                
+                border.strokeColor = UIColor(white: 0.200, alpha: 0.300)
+                border.insets = UIEdgeInsets(top: -1, left: 0, bottom: -1, right: 0)
+                border.cornerRadius = 3
+                border.strokeWidth = TextUtilities.textCGFloat(fromPixel: 2)
+                text?.bs_set(textBlockBorder: (self.border.copy() as? TextBorder), range: codeR)
+            }
+        })
+        
+        return true
+    }
+}
+
+/**
+ A simple emoticon parser.
+ 
+ Use this parser to map some specified piece of string to image emoticon.
+ Example: "Hello :smile:"  ->  "Hello 😀"
+ 
+ It can also be used to extend the "unicode emoticon".
+ */
+public class TextSimpleEmoticonParser: NSObject, TextParser {
+    
+    private var regex: NSRegularExpression?
+    private var mapper: [String : UIImage]?
+    private lazy var lock = DispatchSemaphore(value: 1)
+    
+    /**
+     The custom emoticon mapper.
+     The key is a specified plain string, such as @":smile:".
+     The value is a UIImage which will replace the specified plain string in text.
+     */
+    @objc public var emoticonMapper: [String : UIImage]? {
+        
+        get {
+            lock.wait()
+            let m = self.mapper
+            lock.signal()
+            
+            return m
+        }
+        set {
+            lock.wait()
+            self.mapper = newValue
+            
+            if let tmpMapper = newValue, tmpMapper.count > 0 {
+                var pattern = "("
+                let allKeys = tmpMapper.keys
+                let charset = CharacterSet(charactersIn: "$^?+*.,#|{}[]()\\")
+                var i = 0, max = allKeys.count
+                while i < max {
+                    var one = allKeys[allKeys.index(allKeys.startIndex, offsetBy: i)]
+                    // escape regex characters
+                    var ci = 0, cmax = one.count
+                    while ci < cmax {
+                        let c = String(one[one.index(one.startIndex, offsetBy: ci)])
+                        if charset.contains(Unicode.Scalar(c)!) {
+                            one.insert(contentsOf: "\\", at: one.index(one.startIndex, offsetBy: ci))
+                            ci += 1
+                            cmax += 1
+                        }
+                        ci += 1
+                    }
+                    pattern += one
+                    if i != max - 1 {
+                        pattern += "|"
+                    }
+                    i += 1
+                }
+                pattern += ")"
+                regex = try! NSRegularExpression(pattern: pattern, options: [])
+            } else {
+                regex = nil
+            }
+            
+            lock.signal()
+        }
+    }
+    
+    @objc public override init() {
+        super.init()
+    }
+    
+    // correct the selected range during text replacement
+    private func _replaceText(in range: NSRange, withLength length: Int, selectedRange: NSRange) -> NSRange {
+        var selectedRange = selectedRange
+        // no change
+        if range.length == length {
+            return selectedRange
+        }
+        // right
+        if range.location >= selectedRange.location + selectedRange.length {
+            return selectedRange
+        }
+        // left
+        if selectedRange.location >= range.location + range.length {
+            selectedRange.location = selectedRange.location + length - range.length
+            return selectedRange
+        }
+        // same
+        if NSEqualRanges(range, selectedRange) {
+            selectedRange.length = length
+            return selectedRange
+        }
+        // one edge same
+        if (range.location == selectedRange.location && range.length < selectedRange.length) || (range.location + range.length == selectedRange.location + selectedRange.length && range.length < selectedRange.length) {
+            selectedRange.length = selectedRange.length + length - range.length
+            return selectedRange
+        }
+        
+        selectedRange.location = range.location + length
+        selectedRange.length = 0
+        
+        return selectedRange
+    }
+
+    @objc public func parseText(_ text: NSMutableAttributedString?, selectedRange range: NSRangePointer?) -> Bool {
+
+        guard let t = text, t.length > 0 else {
+            return false
+        }
+
+        let tmpMapper: [AnyHashable : UIImage]?
+        let tmpRegex: NSRegularExpression?
+
+        lock.wait()
+        tmpMapper = mapper
+        tmpRegex = regex
+        lock.signal()
+
+        guard let tMapper = tmpMapper, tMapper.count > 0, tmpRegex != nil else {
+            return false
+        }
+
+        let matches = tmpRegex?.matches(in: t.string, options: [], range: NSRange(location: 0, length: t.length))
+        if matches?.count == 0 {
+            return false
+        }
+        var selectedRange = range != nil ? range!.pointee : NSRange(location: 0, length: 0)
+        var cutLength: Int = 0
+        
+        for one in matches ?? [] {
+            var oneRange = one.range
+            if oneRange.length == 0 {
+                continue
+            }
+            oneRange.location -= cutLength
+            let subStr = (t.string as NSString).substring(with: oneRange)
+            let emoticon = tMapper[subStr]
+            guard let _ = emoticon else {
+                continue
+            }
+            var fontSize: CGFloat = 12 // CoreText default value
+            
+            if let font = t.bs_attribute(NSAttributedString.Key.font, at: oneRange.location) as! CTFont? {
+                fontSize = CTFontGetSize(font)
+            }
+            let atr = NSAttributedString.bs_attachmentString(with: emoticon, fontSize: fontSize)
+            let backedstring = TextBackedString()
+            backedstring.string = subStr
+            atr?.bs_set(textBackedString: backedstring, range: NSRange(location: 0, length: atr?.length ?? 0))
+            text?.replaceCharacters(in: oneRange, with: atr?.string ?? "")
+            text?.bs_removeDiscontinuousAttributes(in: NSRange(location: oneRange.location, length: atr?.length ?? 0))
+            if let anAttributes = atr?.bs_attributes {
+                text?.addAttributes(anAttributes, range: NSRange(location: oneRange.location, length: atr?.length ?? 0))
+            }
+            selectedRange = _replaceText(in: oneRange, withLength: atr?.length ?? 0, selectedRange: selectedRange)
+            cutLength += oneRange.length - 1
+        }
+        
+        range?.pointee = selectedRange
+
+        return true
+    }
+}

+ 143 - 0
Pods/BSText/BSText/String/TextRubyAnnotation.swift

@@ -0,0 +1,143 @@
+//
+//  TextRubyAnnotation.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/10/24.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+import CoreText
+
+/**
+ Wrapper for CTRubyAnnotationRef.
+ 
+ Example:
+ 
+ TextRubyAnnotation *ruby = [TextRubyAnnotation new];
+ ruby.textBefore = @"zhù yīn";
+ CTRubyAnnotationRef ctRuby = ruby.ctRubyAnnotation;
+ 
+ */
+public class TextRubyAnnotation: NSObject, NSCopying, NSCoding, NSSecureCoding {
+    
+    /// Specifies how the ruby text and the base text should be aligned relative to each other.
+    @objc public var alignment = CTRubyAlignment.auto
+    
+    /// Specifies how the ruby text can overhang adjacent characters.
+    @objc public var overhang = CTRubyOverhang.auto
+    
+    /// Specifies the size of the annotation text as a percent of the size of the base text.
+    @objc public var sizeFactor: CGFloat = 0.5
+    
+    /// The ruby text is positioned before the base text;
+    /// i.e. above horizontal text and to the right of vertical text.
+    @objc public var textBefore: String?
+    
+    /// The ruby text is positioned after the base text;
+    /// i.e. below horizontal text and to the left of vertical text.
+    @objc public var textAfter: String?
+    
+    /// The ruby text is positioned to the right of the base text whether it is horizontal or vertical.
+    /// This is the way that Bopomofo annotations are attached to Chinese text in Taiwan.
+    @objc public var textInterCharacter: String?
+    
+    /// The ruby text follows the base text with no special styling.
+    @objc public var textInline: String?
+    
+    public override init() {
+        super.init()
+    }
+    
+    /**
+     Create a ruby object from CTRuby object.
+     
+     @param ctRuby  A CTRuby object.
+     
+     @return A ruby object, or nil when an error occurs.
+     */
+    @objc(rubyWithCTRubyRef:)
+    public class func ruby(with ctRuby: CTRubyAnnotation) -> TextRubyAnnotation {
+        
+        let one = TextRubyAnnotation()
+        
+        one.alignment = CTRubyAnnotationGetAlignment(ctRuby)
+        one.overhang = CTRubyAnnotationGetOverhang(ctRuby)
+        one.sizeFactor = CTRubyAnnotationGetSizeFactor(ctRuby)
+        one.textBefore = (CTRubyAnnotationGetTextForPosition(ctRuby, CTRubyPosition.before)) as String?
+        one.textAfter = (CTRubyAnnotationGetTextForPosition(ctRuby, CTRubyPosition.after)) as String?
+        one.textInterCharacter = (CTRubyAnnotationGetTextForPosition(ctRuby, CTRubyPosition.interCharacter)) as String?
+        one.textInline = (CTRubyAnnotationGetTextForPosition(ctRuby, CTRubyPosition.inline)) as String?
+        
+        return one
+    }
+    
+    
+    /**
+     Create a CTRuby object from the instance.
+     
+     @return A new CTRuby object, or NULL when an error occurs.
+     The returned value should be release after used.
+     */
+    @objc public func ctRubyAnnotation() -> CTRubyAnnotation? {
+        
+        let hiragana = (textBefore ?? "") as CFString
+        let furigana: UnsafeMutablePointer<CFTypeRef> = UnsafeMutablePointer<CFTypeRef>.allocate(capacity: Int(CTRubyPosition.count.rawValue))
+        defer {
+            furigana.deallocate()
+        }
+
+        furigana.initialize(repeating: ("" as CFString), count: 4)
+        furigana[Int(CTRubyPosition.before.rawValue)] = hiragana
+        furigana[Int(CTRubyPosition.after.rawValue)] = (textAfter ?? "") as CFString
+        furigana[Int(CTRubyPosition.interCharacter.rawValue)] = (textInterCharacter ?? "") as CFString
+        furigana[Int(CTRubyPosition.inline.rawValue)] = (textInline ?? "") as CFString
+
+        var ruby: CTRubyAnnotation!
+        furigana.withMemoryRebound(to: Optional<Unmanaged<CFString>>.self, capacity: 4) { ptr in
+            ruby = CTRubyAnnotationCreate(alignment, overhang, sizeFactor, ptr)
+        }
+        
+        return ruby
+    }
+    
+    // MARK: - NSCopying
+    @objc public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextRubyAnnotation()
+        one.alignment = alignment
+        one.overhang = overhang
+        one.sizeFactor = sizeFactor
+        one.textBefore = textBefore
+        one.textAfter = textAfter
+        one.textInterCharacter = textInterCharacter
+        one.textInline = textInline
+        return one
+    }
+    
+    // MARK: - NSCoding
+    @objc public func encode(with aCoder: NSCoder) {
+        aCoder.encode(alignment.rawValue, forKey: "alignment")
+        aCoder.encode(overhang.rawValue, forKey: "overhang")
+        aCoder.encode(Float(sizeFactor), forKey: "sizeFactor")
+        aCoder.encode(textBefore, forKey: "textBefore")
+        aCoder.encode(textAfter, forKey: "textAfter")
+        aCoder.encode(textInterCharacter, forKey: "textInterCharacter")
+        aCoder.encode(textInline, forKey: "textInline")
+    }
+    
+    @objc required convenience public init?(coder aDecoder: NSCoder) {
+        self.init()
+        alignment = CTRubyAlignment(rawValue: UInt8(aDecoder.decodeInt32(forKey: "alignment")))!
+        overhang = CTRubyOverhang(rawValue: UInt8(aDecoder.decodeInt32(forKey: "overhang")))!
+        sizeFactor = CGFloat(aDecoder.decodeFloat(forKey: "sizeFactor"))
+        textBefore = aDecoder.decodeObject(forKey: "textBefore") as? String
+        textAfter = aDecoder.decodeObject(forKey: "textAfter") as? String
+        textInterCharacter = aDecoder.decodeObject(forKey: "textInterCharacter") as? String
+        textInline = aDecoder.decodeObject(forKey: "textInline") as? String
+    }
+    
+    // MARK: - NSSecureCoding
+    @objc public static var supportsSecureCoding: Bool {
+        return true
+    }
+}

+ 84 - 0
Pods/BSText/BSText/String/TextRunDelegate.swift

@@ -0,0 +1,84 @@
+//
+//  TextRunDelegate.swift
+//  BSTextDemo
+//
+//  Created by BlueSky on 2018/10/19.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+import CoreText
+
+public class TextRunDelegate: NSObject, NSCopying, NSCoding, NSSecureCoding {
+    
+    var userInfo: NSMutableDictionary?
+    @objc public var ascent: CGFloat = 0
+    @objc public var descent: CGFloat = 0
+    @objc public var width: CGFloat = 0
+    
+    @objc public var ctRunDelegate: CTRunDelegate {
+        
+        get {
+            // MARK: - 此处要使用本类对象,否则之后需要取出 Delegate 的 ConRef 的时候会出问题
+            let extentBuffer = UnsafeMutablePointer<TextRunDelegate>.allocate(capacity: 1)
+            extentBuffer.initialize(to: self)
+            
+            var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (pointer) in
+                
+                pointer.deallocate()
+                
+            }, getAscent: { (pointer) -> CGFloat in
+                
+                let p = pointer.assumingMemoryBound(to: TextRunDelegate.self)
+                return p.pointee.ascent
+                
+            }, getDescent: { (pointer) -> CGFloat in
+                
+                let p = pointer.assumingMemoryBound(to: TextRunDelegate.self)
+                return p.pointee.descent
+                
+            }, getWidth: { (pointer) -> CGFloat in
+                
+                let p = pointer.assumingMemoryBound(to: TextRunDelegate.self)
+                return p.pointee.width
+            })
+            
+            return CTRunDelegateCreate(&callbacks, extentBuffer)!
+        }
+    }
+    
+    @objc public override init() {
+        super.init()
+    }
+    
+    // MARK: - NSCoding
+    @objc public func encode(with aCoder: NSCoder) {
+        aCoder.encode(Float(ascent), forKey: "ascent")
+        aCoder.encode(Float(descent), forKey: "descent")
+        aCoder.encode(Float(width), forKey: "width")
+        aCoder.encode(userInfo, forKey: "userInfo")
+    }
+    
+    @objc required public init?(coder aDecoder: NSCoder) {
+        super.init()
+        ascent = CGFloat(aDecoder.decodeFloat(forKey: "ascent"))
+        descent = CGFloat(aDecoder.decodeFloat(forKey: "descent"))
+        width = CGFloat(aDecoder.decodeFloat(forKey: "width"))
+        userInfo = aDecoder.decodeObject(forKey: "userInfo") as? NSMutableDictionary
+    }
+    
+    // MARK: - NSCopying
+    @objc public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextRunDelegate()
+        one.ascent = ascent
+        one.descent = descent
+        one.width = width
+        one.userInfo = userInfo
+        return one
+    }
+    
+    // MARK: - NSSecureCoding
+    @objc public static var supportsSecureCoding: Bool {
+        return true
+    }
+}

+ 2650 - 0
Pods/BSText/BSText/Utility/NSAttributedStringExtension.swift

@@ -0,0 +1,2650 @@
+//
+//  NSAttributedStringExtension.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/11/5.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+#if canImport(YYImage)
+import YYImage
+#endif
+
+/**
+ Get pre-defined attributes from attributed string.
+ All properties defined in UIKit, CoreText and BSText are included.
+ */
+extension NSAttributedString {
+    
+    /**
+     Returns the attributes at first charactor.
+     */
+    @objc public var bs_attributes: [NSAttributedString.Key : Any]? {
+        get {
+            return bs_attributes(at: 0)
+        }
+    }
+    
+    /**
+     The font of the text. (read-only)
+     
+     @discussion Default is Helvetica (Neue) 12.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:3.2  UIKit:6.0
+     */
+    @objc public var bs_font: UIFont? {
+        get {
+            return bs_font(at: 0)
+        }
+    }
+    
+    /**
+     A kerning adjustment. (read-only)
+     
+     @discussion Default is standard kerning. The kerning attribute indicate how many
+     points the following character should be shifted from its default offset as
+     defined by the current character's font in points; a positive kern indicates a
+     shift farther along and a negative kern indicates a shift closer to the current
+     character. If this attribute is not present, standard kerning will be used.
+     If this attribute is set to 0, no kerning will be done at all.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:3.2  UIKit:6.0
+     */
+    @objc public var bs_kern: NSNumber? {
+        get {
+            return bs_kern(at: 0)
+        }
+    }
+    
+    /**
+     The foreground color. (read-only)
+     
+     @discussion Default is Black.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:3.2  UIKit:6.0
+     */
+    @objc public var bs_color: UIColor? {
+        get {
+            return bs_color(at: 0)
+        }
+    }
+    
+    /**
+     The background color. (read-only)
+     
+     @discussion Default is nil (or no background).
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:6.0
+     */
+    @objc public var bs_backgroundColor: UIColor? {
+        get {
+            return bs_backgroundColor(at: 0)
+        }
+    }
+    
+    /**
+     The stroke width. (read-only)
+     
+     @discussion Default value is 0 (no stroke). This attribute, interpreted as
+     a percentage of font point size, controls the text drawing mode: positive
+     values effect drawing with stroke only; negative values are for stroke and fill.
+     A typical value for outlined text is 3.0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:3.2  UIKit:6.0
+     */
+    @objc public var bs_strokeWidth: NSNumber? {
+        get {
+            return bs_strokeWidth(at: 0)
+        }
+    }
+    
+    /**
+     The stroke color. (read-only)
+     
+     @discussion Default value is nil (same as foreground color).
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:3.2  UIKit:6.0
+     */
+    @objc public var bs_strokeColor: UIColor? {
+        get {
+            return bs_strokeColor(at: 0)
+        }
+    }
+    
+    /**
+     The text shadow. (read-only)
+     
+     @discussion Default value is nil (no shadow).
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:6.0
+     */
+    @objc public var bs_shadow: NSShadow? {
+        get {
+            return bs_shadow(at: 0)
+        }
+    }
+    
+    /**
+     The strikethrough style. (read-only)
+     
+     @discussion Default value is NSUnderlineStyleNone (no strikethrough).
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:6.0
+     */
+    @objc public var bs_strikethroughStyle: NSUnderlineStyle {
+        get {
+            return bs_strikethroughStyle(at: 0)
+        }
+    }
+    
+    /**
+     The strikethrough color. (read-only)
+     
+     @discussion Default value is nil (same as foreground color).
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:7.0
+     */
+    @objc public var bs_strikethroughColor: UIColor? {
+        get {
+            return bs_strikethroughColor(at: 0)
+        }
+    }
+    
+    /**
+     The underline style. (read-only)
+     
+     @discussion Default value is NSUnderlineStyleNone (no underline).
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:3.2  UIKit:6.0
+     */
+    @objc public var bs_underlineStyle: NSUnderlineStyle {
+        get {
+            return bs_underlineStyle(at: 0)
+        }
+    }
+    
+    /**
+     The underline color. (read-only)
+     
+     @discussion Default value is nil (same as foreground color).
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:3.2  UIKit:7.0
+     */
+    @objc public var bs_underlineColor: UIColor? {
+        get {
+            return bs_underlineColor(at: 0)
+        }
+    }
+    
+    /**
+     Ligature formation control. (read-only)
+     
+     @discussion Default is int value 1. The ligature attribute determines what kinds
+     of ligatures should be used when displaying the string. A value of 0 indicates
+     that only ligatures essential for proper rendering of text should be used,
+     1 indicates that standard ligatures should be used, and 2 indicates that all
+     available ligatures should be used. Which ligatures are standard depends on the
+     script and possibly the font.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:3.2  UIKit:6.0
+     */
+    @objc public var bs_ligature: NSNumber? {
+        get {
+            return bs_ligature(at: 0)
+        }
+    }
+    
+    /**
+     The text effect. (read-only)
+     
+     @discussion Default is nil (no effect). The only currently supported value
+     is NSTextEffectLetterpressStyle.
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:7.0
+     */
+    @objc public var bs_textEffect: String? {
+        get {
+            return bs_textEffect(at: 0)
+        }
+    }
+    
+    /**
+     The skew to be applied to glyphs. (read-only)
+     
+     @discussion Default is 0 (no skew).
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:7.0
+     */
+    @objc public var bs_obliqueness: NSNumber? {
+        get {
+            return bs_obliqueness(at: 0)
+        }
+    }
+    
+    /**
+     The log of the expansion factor to be applied to glyphs. (read-only)
+     
+     @discussion Default is 0 (no expansion).
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:7.0
+     */
+    @objc public var bs_expansion: NSNumber? {
+        get {
+            return bs_expansion(at: 0)
+        }
+    }
+    
+    /**
+     The character's offset from the baseline, in points. (read-only)
+     
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:7.0
+     */
+    @objc public var bs_baselineOffset: NSNumber? {
+        get {
+            return bs_baselineOffset(at: 0)
+        }
+    }
+    
+    /**
+     Glyph orientation control. (read-only)
+     
+     @discussion Default is NO. A value of NO indicates that horizontal glyph forms
+     are to be used, YES indicates that vertical glyph forms are to be used.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:4.3
+     */
+    @objc public var bs_verticalGlyphForm: Bool {
+        get {
+            return bs_verticalGlyphForm(at: 0)
+        }
+    }
+    
+    /**
+     Specifies text language. (read-only)
+     
+     @discussion Value must be a NSString containing a locale identifier. Default is
+     unset. When this attribute is set to a valid identifier, it will be used to select
+     localized glyphs (if supported by the font) and locale-specific line breaking rules.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:7.0
+     */
+    @objc public var bs_language: String? {
+        get {
+            return bs_language(at: 0)
+        }
+    }
+    
+    /**
+     Specifies a bidirectional override or embedding. (read-only)
+     
+     @discussion See alse NSWritingDirection and NSWritingDirectionAttributeName.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:7.0
+     */
+    @objc public var bs_writingDirection: [Any]? {
+        get {
+            return bs_writingDirection(at: 0)
+        }
+    }
+    
+    /**
+     An NSParagraphStyle object which is used to specify things like
+     line alignment, tab rulers, writing direction, etc. (read-only)
+     
+     @discussion Default is nil ([NSParagraphStyle defaultParagraphStyle]).
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_paragraphStyle: NSParagraphStyle? {
+        get {
+            return bs_paragraphStyle(at: 0)
+        }
+    }
+    
+    // MARK: - Get paragraph attribute as property
+    
+    /**
+     The text alignment (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion Natural text alignment is realized as left or right alignment
+     depending on the line sweep direction of the first script contained in the paragraph.
+     @discussion Default is NSTextAlignmentNatural.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_alignment: NSTextAlignment {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.alignment
+        }
+    }
+    
+    /**
+     The mode that should be used to break lines (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property contains the line break mode to be used laying out the paragraph's text.
+     @discussion Default is NSLineBreakByWordWrapping.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_lineBreakMode: NSLineBreakMode {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.lineBreakMode
+        }
+    }
+    
+    /**
+     The distance in points between the bottom of one line fragment and the top of the next.
+     (A wrapper for NSParagraphStyle) (read-only)
+     
+     @discussion This value is always nonnegative. This value is included in the line
+     fragment heights in the layout manager.
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_lineSpacing: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.lineSpacing
+        }
+    }
+    
+    /**
+     The space after the end of the paragraph (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property contains the space (measured in points) added at the
+     end of the paragraph to separate it from the following paragraph. This value must
+     be nonnegative. The space between paragraphs is determined by adding the previous
+     paragraph's paragraphSpacing and the current paragraph's paragraphSpacingBefore.
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_paragraphSpacing: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.paragraphSpacing
+        }
+    }
+    
+    /**
+     The distance between the paragraph's top and the beginning of its text content.
+     (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property contains the space (measured in points) between the
+     paragraph's top and the beginning of its text content.
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_paragraphSpacingBefore: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.paragraphSpacingBefore
+        }
+    }
+    
+    /**
+     The indentation of the first line (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property contains the distance (in points) from the leading margin
+     of a text container to the beginning of the paragraph's first line. This value
+     is always nonnegative.
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_firstLineHeadIndent: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.firstLineHeadIndent
+        }
+    }
+    
+    /**
+     The indentation of the receiver's lines other than the first. (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property contains the distance (in points) from the leading margin
+     of a text container to the beginning of lines other than the first. This value is
+     always nonnegative.
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_headIndent: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.headIndent
+        }
+    }
+    
+    /**
+     The trailing indentation (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion If positive, this value is the distance from the leading margin
+     (for example, the left margin in left-to-right text). If 0 or negative, it's the
+     distance from the trailing margin.
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_tailIndent: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.tailIndent
+        }
+    }
+    
+    /**
+     The receiver's minimum height (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property contains the minimum height in points that any line in
+     the receiver will occupy, regardless of the font size or size of any attached graphic.
+     This value must be nonnegative.
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_minimumLineHeight: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.minimumLineHeight
+        }
+    }
+    
+    /**
+     The receiver's maximum line height (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property contains the maximum height in points that any line in
+     the receiver will occupy, regardless of the font size or size of any attached graphic.
+     This value is always nonnegative. Glyphs and graphics exceeding this height will
+     overlap neighboring lines; however, a maximum height of 0 implies no line height limit.
+     Although this limit applies to the line itself, line spacing adds extra space between adjacent lines.
+     @discussion Default is 0 (no limit).
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_maximumLineHeight: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.maximumLineHeight
+        }
+    }
+    
+    /**
+     The line height multiple (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property contains the line break mode to be used laying out the paragraph's text.
+     @discussion Default is 0 (no multiple).
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_lineHeightMultiple: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.lineHeightMultiple
+        }
+    }
+    
+    /**
+     The base writing direction (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion If you specify NSWritingDirectionNaturalDirection, the receiver resolves
+     the writing direction to either NSWritingDirectionLeftToRight or NSWritingDirectionRightToLeft,
+     depending on the direction for the user's `language` preference setting.
+     @discussion Default is NSWritingDirectionNatural.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:6.0  UIKit:6.0
+     */
+    @objc public var bs_baseWritingDirection: NSWritingDirection {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.baseWritingDirection
+        }
+    }
+    
+    /**
+     The paragraph's threshold for hyphenation. (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion Valid values lie between 0 and 1.0 inclusive. Hyphenation is attempted
+     when the ratio of the text width (as broken without hyphenation) to the width of the
+     line fragment is less than the hyphenation factor. When the paragraph's hyphenation
+     factor is 0, the layout manager's hyphenation factor is used instead. When both
+     are 0, hyphenation is disabled.
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since UIKit:6.0
+     */
+    @objc public var bs_hyphenationFactor: Float {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.hyphenationFactor
+        }
+    }
+    
+    /**
+     The document-wide default tab interval (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion This property represents the default tab interval in points. Tabs after the
+     last specified in tabStops are placed at integer multiples of this distance (if positive).
+     @discussion Default is 0.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:7.0  UIKit:7.0
+     */
+    @objc public var bs_defaultTabInterval: CGFloat {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.defaultTabInterval
+        }
+    }
+    
+    /**
+     An array of NSTextTab objects representing the receiver's tab stops.
+     (A wrapper for NSParagraphStyle). (read-only)
+     
+     @discussion The NSTextTab objects, sorted by location, define the tab stops for
+     the paragraph style.
+     @discussion Default is 12 TabStops with 28.0 tab interval.
+     @discussion Get this property returns the first character's attribute.
+     @since CoreText:7.0  UIKit:7.0
+     */
+    @objc public var bs_tabStops: [NSTextTab]? {
+        get {
+            let style = bs_paragraphStyle ?? NSParagraphStyle.default
+            return style.tabStops
+        }
+    }
+    
+    // MARK: - Get BSText attribute as property
+    
+    /**
+     Unarchive string from data.
+     @param data  The archived attributed string data.
+     @return Returns nil if an error occurs.
+     */
+    @objc public class func bs_unarchive(from data: Data?) -> NSAttributedString? {
+        
+        guard let aData = data else {
+            return nil
+        }
+        
+        return TextUnarchiver.unarchiveObject(with: aData) as? NSAttributedString
+    }
+    
+    /**
+     Archive the string to data.
+     @return Returns nil if an error occurs.
+     */
+    @objc public func bs_archiveToData() -> Data? {
+        
+        return TextArchiver.archivedData(withRootObject: self)
+    }
+    
+    // MARK: - Retrieving character attribute information
+    
+    ///=============================================================================
+    /// @name Retrieving character attribute information
+    ///=============================================================================
+
+    /**
+     Returns the attributes for the character at a given index.
+     
+     @discussion Raises an `NSRangeException` if index lies beyond the end of the
+     receiver's characters.
+     
+     @param index  The index for which to return attributes.
+     This value must lie within the bounds of the receiver.
+     
+     @return The attributes for the character at index.
+     */
+    @objc(bs_attributesAtIndex:)
+    public func bs_attributes(at index: Int) -> [NSAttributedString.Key : Any]? {
+        
+        if index > self.length || self.length == 0 {
+            return nil
+        }
+        var idx = index
+        if self.length > 0 && index == self.length {
+            idx -= 1
+        }
+        return self.attributes(at: idx, effectiveRange: nil)
+    }
+    
+    /**
+     Returns the value for an attribute with a given name of the character at a given index.
+     
+     @discussion Raises an `NSRangeException` if index lies beyond the end of the
+     receiver's characters.
+     
+     @param attributeName  The name of an attribute.
+     @param index          The index for which to return attributes.
+     This value must not exceed the bounds of the receiver.
+     
+     @return The value for the attribute named `attributeName` of the character at
+     index `index`, or nil if there is no such attribute.
+     */
+    @objc(bs_attribute:atIndex:)
+    public func bs_attribute(_ attributeName: NSAttributedString.Key?, at index: Int) -> Any? {
+        if attributeName == nil {
+            return nil
+        }
+        if index > length || length == 0 {
+            return nil
+        }
+        var idx = index
+        if self.length > 0 && index == self.length {
+            idx -= 1
+        }
+        return self.attribute(attributeName!, at: idx, effectiveRange: nil)
+    }
+    
+    @objc(bs_fontAtIndex:)
+    public func bs_font(at index: Int) -> UIFont? {
+        /*
+         In iOS7 and later, UIFont is toll-free bridged to CTFontRef,
+         although Apple does not mention it in documentation.
+         
+         In iOS6, UIFont is a wrapper for CTFontRef, so CoreText can alse use UIfont,
+         but UILabel/UITextView cannot use CTFontRef.
+         
+         We use UIFont for both CoreText and UIKit.
+         */
+        let font: UIFont? = bs_attribute(NSAttributedString.Key.font, at: index) as? UIFont
+        return font
+    }
+    
+    @objc(bs_kernAtIndex:)
+    public func bs_kern(at index: Int) -> NSNumber? {
+        return bs_attribute(NSAttributedString.Key.kern, at: index) as? NSNumber
+    }
+    
+    @objc(bs_colorAtIndex:)
+    public func bs_color(at index: Int) -> UIColor? {
+        var color = bs_attribute(NSAttributedString.Key.foregroundColor, at: index) as? UIColor
+        if color == nil {
+            let ref = bs_attribute(NSAttributedString.Key(rawValue: kCTForegroundColorAttributeName as String), at: index)
+            if let aRef = ref {
+                color = UIColor(cgColor: aRef as! CGColor)
+            }
+        }
+//        if let aColor = color, !(aColor is UIColor) {
+//            if CFGetTypeID(color as CFTypeRef) == CGColor.typeID {
+//                color = UIColor(cgColor: aColor as! CGColor)
+//            } else {
+//                color = nil
+//            }
+//        }
+        return color
+    }
+    
+    @objc(bs_backgroundColorAtIndex:)
+    public func bs_backgroundColor(at index: Int) -> UIColor? {
+        return bs_attribute(NSAttributedString.Key.backgroundColor, at: index) as? UIColor
+    }
+    
+    @objc(bs_strokeWidthAtIndex:)
+    public func bs_strokeWidth(at index: Int) -> NSNumber? {
+        return bs_attribute(NSAttributedString.Key.strokeWidth, at: index) as? NSNumber
+    }
+    
+    @objc(bs_strokeColorAtIndex:)
+    public func bs_strokeColor(at index: Int) -> UIColor? {
+        var color = bs_attribute(NSAttributedString.Key.strokeColor, at: index)
+        if color == nil {
+            let ref = bs_attribute(NSAttributedString.Key(rawValue: kCTStrokeColorAttributeName as String), at: index)
+            if let aRef = ref {
+                color = UIColor(cgColor: aRef as! CGColor)
+            }
+        }
+        return color as? UIColor
+    }
+    
+    @objc(bs_shadowAtIndex:)
+    public func bs_shadow(at index: Int) -> NSShadow? {
+        return bs_attribute(NSAttributedString.Key.shadow, at: index) as? NSShadow
+    }
+    
+    @objc(bs_strikethroughStyleAtIndex:)
+    public func bs_strikethroughStyle(at index: Int) -> NSUnderlineStyle {
+        let style = bs_attribute(NSAttributedString.Key.strikethroughStyle, at: index)
+        return NSUnderlineStyle(rawValue: style as! Int)
+    }
+    
+    @objc(bs_strikethroughColorAtIndex:)
+    public func bs_strikethroughColor(at index: Int) -> UIColor? {
+        return bs_attribute(NSAttributedString.Key.strikethroughColor, at: index) as? UIColor
+    }
+    
+    @objc(bs_underlineStyleAtIndex:)
+    public func bs_underlineStyle(at index: Int) -> NSUnderlineStyle {
+        let style = bs_attribute(NSAttributedString.Key.underlineStyle, at: index)
+        return NSUnderlineStyle(rawValue: style as! Int)
+    }
+    
+    @objc(bs_underlineColorAtIndex:)
+    public func bs_underlineColor(at index: Int) -> UIColor? {
+        var color: UIColor? = nil
+        color = bs_attribute(NSAttributedString.Key.underlineColor, at: index) as? UIColor
+        if color == nil {
+            let ref = bs_attribute(NSAttributedString.Key(rawValue: kCTUnderlineColorAttributeName as String), at: index)
+            if let aRef = ref {
+                color = UIColor(cgColor: aRef as! CGColor)
+            }
+        }
+        return color
+    }
+    
+    @objc(bs_ligatureAtIndex:)
+    public func bs_ligature(at index: Int) -> NSNumber? {
+        return bs_attribute(NSAttributedString.Key.ligature, at: index) as? NSNumber
+    }
+    
+    @objc(bs_textEffectAtIndex:)
+    public func bs_textEffect(at index: Int) -> String? {
+        return bs_attribute(NSAttributedString.Key.textEffect, at: index) as? String
+    }
+    
+    @objc(bs_obliquenessAtIndex:)
+    public func bs_obliqueness(at index: Int) -> NSNumber? {
+        return bs_attribute(NSAttributedString.Key.obliqueness, at: index) as? NSNumber
+    }
+    
+    @objc(bs_expansionAtIndex:)
+    public func bs_expansion(at index: Int) -> NSNumber? {
+        return bs_attribute(NSAttributedString.Key.expansion, at: index) as? NSNumber
+    }
+
+    @objc(bs_baselineOffsetAtIndex:)
+    public func bs_baselineOffset(at index: Int) -> NSNumber? {
+        return bs_attribute(NSAttributedString.Key.baselineOffset, at: index) as? NSNumber
+    }
+    
+    @objc(bs_verticalGlyphFormAtIndex:)
+    public func bs_verticalGlyphForm(at index: Int) -> Bool {
+        let num = bs_attribute(NSAttributedString.Key.verticalGlyphForm, at: index) as? Int
+        return num != 0
+    }
+    
+    @objc(bs_languageAtIndex:)
+    public func bs_language(at index: Int) -> String? {
+        return bs_attribute(NSAttributedString.Key(rawValue: kCTLanguageAttributeName as String), at: index) as? String
+    }
+    
+    @objc(bs_writingDirectionAtIndex:)
+    public func bs_writingDirection(at index: Int) -> [Any]? {
+        return bs_attribute(NSAttributedString.Key(rawValue: kCTWritingDirectionAttributeName as String), at: index) as? [Any]
+    }
+    
+    @objc(bs_paragraphStyleAtIndex:)
+    public func bs_paragraphStyle(at index: Int) -> NSParagraphStyle? {
+        /*
+         NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef.
+         
+         CoreText can use both NSParagraphStyle and CTParagraphStyleRef,
+         but UILabel/UITextView can only use NSParagraphStyle.
+         
+         We use NSParagraphStyle in both CoreText and UIKit.
+         */
+        var style = bs_attribute(NSAttributedString.Key.paragraphStyle, at: index) as? NSParagraphStyle
+        if let s = style {
+            if CFGetTypeID(s) == CTParagraphStyleGetTypeID() {
+                style = NSParagraphStyle.bs_styleWith(ctStyle: style as! CTParagraphStyle)
+            }
+        }
+        return style
+    }
+    
+    
+    @objc(bs_alignmentAtIndex:)
+    public func bs_alignment(at index: Int) -> NSTextAlignment {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.alignment
+    }
+    
+    @objc(bs_lineBreakModeAtIndex:)
+    public func bs_lineBreakMode(at index: Int) -> NSLineBreakMode {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.lineBreakMode
+    }
+    
+    @objc(bs_lineSpacingAtIndex:)
+    public func bs_lineSpacing(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.lineSpacing
+    }
+    
+    @objc(bs_paragraphSpacingAtIndex:)
+    public func bs_paragraphSpacing(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.paragraphSpacing
+    }
+    
+    @objc(bs_paragraphSpacingBeforeAtIndex:)
+    public func bs_paragraphSpacingBefore(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.paragraphSpacingBefore
+    }
+    
+    @objc(bs_firstLineHeadIndentAtIndex:)
+    public func bs_firstLineHeadIndent(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.firstLineHeadIndent
+    }
+    
+    @objc(bs_headIndentAtIndex:)
+    public func bs_headIndent(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.headIndent
+    }
+    
+    @objc(bs_tailIndentAtIndex:)
+    public func bs_tailIndent(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.tailIndent
+    }
+    
+    @objc(bs_minimumLineHeightAtIndex:)
+    public func bs_minimumLineHeight(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.minimumLineHeight
+    }
+    
+    @objc(bs_maximumLineHeightAtIndex:)
+    public func bs_maximumLineHeight(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.maximumLineHeight
+    }
+    
+    @objc(bs_lineHeightMultipleAtIndex:)
+    public func bs_lineHeightMultiple(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.lineHeightMultiple
+    }
+    
+    @objc(bs_baseWritingDirectionAtIndex:)
+    public func bs_baseWritingDirection(at index: Int) -> NSWritingDirection {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.baseWritingDirection
+    }
+    
+    @objc(bs_hyphenationFactorAtIndex:)
+    public func bs_hyphenationFactor(at index: Int) -> Float {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.hyphenationFactor
+    }
+    
+    @objc(bs_defaultTabIntervalAtIndex:)
+    public func bs_defaultTabInterval(at index: Int) -> CGFloat {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.defaultTabInterval
+    }
+    
+    @objc(bs_tabStopsAtIndex:)
+    public func bs_tabStops(at index: Int) -> [NSTextTab]? {
+        
+        let style = bs_paragraphStyle(at: index) ?? NSParagraphStyle.default
+        return style.tabStops
+    }
+    
+    /**
+     The text shadow. (read-only)
+     
+     @discussion Default value is nil (no shadow).
+     @discussion Get this property returns the first character's attribute.
+     */
+    @objc public var bs_textShadow: TextShadow? {
+        get {
+            return bs_textShadow(at: 0)
+        }
+    }
+    
+    @objc(bs_textShadowAtIndex:)
+    public func bs_textShadow(at index: Int) -> TextShadow? {
+        return bs_attribute(NSAttributedString.Key(rawValue: TextAttribute.textShadowAttributeName), at: index) as? TextShadow
+    }
+    
+    /**
+     The text inner shadow. (read-only)
+     
+     @discussion Default value is nil (no shadow).
+     @discussion Get this property returns the first character's attribute.
+     */
+    @objc public var bs_textInnerShadow: TextShadow? {
+        get {
+            return bs_textInnerShadow(at: 0)
+        }
+    }
+    
+    @objc(bs_textInnerShadowAtIndex:)
+    public func bs_textInnerShadow(at index: Int) -> TextShadow? {
+        return bs_attribute(NSAttributedString.Key(rawValue: TextAttribute.textInnerShadowAttributeName), at: index) as? TextShadow
+    }
+    
+    /**
+     The text underline. (read-only)
+     
+     @discussion Default value is nil (no underline).
+     @discussion Get this property returns the first character's attribute.
+     */
+    @objc public var bs_textUnderline: TextDecoration? {
+        get {
+            return bs_textUnderline(at: 0)
+        }
+    }
+    
+    @objc(bs_textUnderlineAtIndex:)
+    public func bs_textUnderline(at index: Int) -> TextDecoration? {
+        return bs_attribute(NSAttributedString.Key(rawValue: TextAttribute.textUnderlineAttributeName), at: index) as? TextDecoration
+    }
+    
+    /**
+     The text strikethrough. (read-only)
+     
+     @discussion Default value is nil (no strikethrough).
+     @discussion Get this property returns the first character's attribute.
+     */
+    @objc public var bs_textStrikethrough: TextDecoration? {
+        get {
+            return bs_textStrikethrough(at: 0)
+        }
+    }
+    
+    @objc(bs_textStrikethroughAtIndex:)
+    public func bs_textStrikethrough(at index: Int) -> TextDecoration? {
+        return bs_attribute(NSAttributedString.Key(rawValue: TextAttribute.textStrikethroughAttributeName), at: index) as? TextDecoration
+    }
+    
+    /**
+     The text border. (read-only)
+     
+     @discussion Default value is nil (no border).
+     @discussion Get this property returns the first character's attribute.
+     */
+    @objc public var bs_textBorder: TextBorder? {
+        get {
+            return bs_textBorder(at: 0)
+        }
+    }
+    
+    @objc(bs_textBorderAtIndex:)
+    public func bs_textBorder(at index: Int) -> TextBorder? {
+        return bs_attribute(NSAttributedString.Key(rawValue: TextAttribute.textBorderAttributeName), at: index) as? TextBorder
+    }
+    
+    /**
+     The text background border. (read-only)
+     
+     @discussion Default value is nil (no background border).
+     @discussion Get this property returns the first character's attribute.
+     */
+    @objc public var bs_textBackgroundBorder: TextBorder? {
+        get {
+            return bs_textBackgroundBorder(at: 0)
+        }
+    }
+    
+    @objc(bs_textBackgroundBorderAtIndex:)
+    public func bs_textBackgroundBorder(at index: Int) -> TextBorder? {
+        return bs_attribute(NSAttributedString.Key(rawValue: TextAttribute.textBackedStringAttributeName), at: index) as? TextBorder
+    }
+    
+    /**
+     The glyph transform. (read-only)
+     
+     @discussion Default value is CGAffineTransformIdentity (no transform).
+     @discussion Get this property returns the first character's attribute.
+     */
+    @objc public var bs_textGlyphTransform: CGAffineTransform {
+        get {
+            return bs_textGlyphTransform(at: 0)
+        }
+    }
+    
+    @objc(bs_textGlyphTransformAtIndex:)
+    public func bs_textGlyphTransform(at index: Int) -> CGAffineTransform {
+        let value: NSValue? = bs_attribute(NSAttributedString.Key(rawValue: TextAttribute.textGlyphTransformAttributeName), at: index) as? NSValue
+        if value == nil {
+            return .identity
+        }
+        return (value?.cgAffineTransformValue)!
+    }
+    
+    
+    // MARK: - Query for BSText
+    
+    /**
+     Returns the plain text from a range.
+     If there's `TextBackedStringAttributeName` attribute, the backed string will
+     replace the attributed string range.
+     
+     @param range A range in receiver.
+     @return The plain text.
+     */
+    @objc(bs_plainTextForRange:)
+    public func bs_plainText(for range: NSRange) -> String? {
+        if range.location == NSNotFound || range.length == NSNotFound {
+            return nil
+        }
+        var result = ""
+        if range.length == 0 {
+            return result
+        }
+        let string = self.string
+        enumerateAttribute(NSAttributedString.Key(rawValue: TextAttribute.textBackedStringAttributeName), in: range, options: [], using: { value, range, stop in
+            let backed = value as? TextBackedString
+            if backed != nil && backed?.string != nil {
+                result += backed?.string ?? ""
+            } else {
+                result += (string as NSString).substring(with: range)
+            }
+        })
+        return result
+    }
+    
+    /**
+     Creates and returns an attachment.
+     
+     @param content      The attachment (UIImage/UIView/CALayer).
+     @param contentMode  The attachment's content mode.
+     @param width        The attachment's container width in layout.
+     @param ascent       The attachment's container ascent in layout.
+     @param descent      The attachment's container descent in layout.
+     
+     @return An attributed string, or nil if an error occurs.
+     */
+    @objc(bs_attachmentStringWithContent:contentMode:width:ascent:descent:)
+    public class func bs_attachmentString(with content: Any?, contentMode: UIView.ContentMode, width: CGFloat, ascent: CGFloat, descent: CGFloat) -> NSMutableAttributedString {
+        
+        let atr = NSMutableAttributedString(string: TextAttribute.textAttachmentToken)
+        let attach = TextAttachment()
+        attach.content = content
+        attach.contentMode = contentMode
+        atr.bs_set(textAttachment: attach, range: NSRange(location: 0, length: atr.length))
+        let delegate = TextRunDelegate()
+        delegate.width = width
+        delegate.ascent = ascent
+        delegate.descent = descent
+        let delegateRef = delegate.ctRunDelegate
+        atr.bs_set(runDelegate: delegateRef, range: NSRange(location: 0, length: atr.length))
+        return atr
+    }
+    
+    /**
+     Creates and returns an attachment.
+     
+     
+     Example: ContentMode:bottom Alignment:Top.
+     
+     The text      The attachment holder
+     ↓                ↓
+     ─────────┌──────────────────────┐───────
+     / \   │                      │ / ___|
+     / _ \  │                      │| |
+     / ___ \ │                      │| |___     ←── The text line
+     /_/   \_\│    ██████████████    │ \____|
+     ─────────│    ██████████████    │───────
+     │    ██████████████    │
+     │    ██████████████ ←───────────────── The attachment content
+     │    ██████████████    │
+     └──────────────────────┘
+     
+     @param content        The attachment (UIImage/UIView/CALayer).
+     @param contentMode    The attachment's content mode in attachment holder
+     @param attachmentSize The attachment holder's size in text layout.
+     @param font           The attachment will align to this font.
+     @param alignment      The attachment holder's alignment to text line.
+     
+     @return An attributed string, or nil if an error occurs.
+     */
+    @objc(bs_attachmentStringWithContent:contentMode:attachmentSize:alignToFont:alignment:)
+    public class func bs_attachmentString(with content: Any?, contentMode: UIView.ContentMode, attachmentSize: CGSize, alignTo font: UIFont?, alignment: TextVerticalAlignment) -> NSMutableAttributedString? {
+        
+        let atr = NSMutableAttributedString(string: TextAttribute.textAttachmentToken)
+        let attach = TextAttachment()
+        attach.content = content
+        attach.contentMode = contentMode
+        atr.bs_set(textAttachment: attach, range: NSRange(location: 0, length: atr.length))
+        let delegate = TextRunDelegate()
+        delegate.width = attachmentSize.width
+        switch alignment {
+        case .top:
+            delegate.ascent = font?.ascender ?? 0
+            delegate.descent = attachmentSize.height - (font?.ascender ?? 0)
+            if delegate.descent < 0 {
+                delegate.descent = 0
+                delegate.ascent = attachmentSize.height
+            }
+        case .center:
+            let fontHeight: CGFloat = (font?.ascender ?? 0) - (font?.descender ?? 0)
+            let yOffset: CGFloat = (font?.ascender ?? 0) - fontHeight * 0.5
+            delegate.ascent = attachmentSize.height * 0.5 + yOffset
+            delegate.descent = attachmentSize.height - delegate.ascent
+            if delegate.descent < 0 {
+                delegate.descent = 0
+                delegate.ascent = attachmentSize.height
+            }
+        case .bottom:
+            delegate.ascent = attachmentSize.height + (font?.descender ?? 0)
+            delegate.descent = -(font?.descender ?? 0)
+            if delegate.ascent < 0 {
+                delegate.ascent = 0
+                delegate.descent = attachmentSize.height
+            }
+        default:
+            delegate.ascent = attachmentSize.height
+            delegate.descent = 0
+        }
+        // Swift 中 CoreFoundation 对象 进行了自动内存管理,不需要手动释放
+        let delegateRef = delegate.ctRunDelegate
+        atr.bs_set(runDelegate: delegateRef, range: NSRange(location: 0, length: atr.length))
+        
+        return atr
+    }
+    
+    /**
+     Creates and returns an attahment from a fourquare image as if it was an emoji.
+     
+     @param emojiImage  A fourquare image.
+     @param fontSize    The font size.
+     
+     @return An attributed string, or nil if an error occurs.
+     */
+    @objc(bs_attachmentStringWithEmojiImage:fontSize:)
+    public class func bs_attachmentString(with emojiImage: UIImage?, fontSize: CGFloat) -> NSMutableAttributedString? {
+        guard let image = emojiImage, fontSize > 0 else {
+            return nil
+        }
+        var hasAnim = false
+        if (image.images?.count ?? 0) > 1 {
+            hasAnim = true
+        } else {
+            #if canImport(YYImage)
+            let frameCount = (image as? YYImage)?.animatedImageFrameCount() ?? 0
+            if frameCount > 1 {
+                hasAnim = true
+            }
+            #endif
+        }
+        let ascent = TextUtilities.textEmojiGetAscent(with: fontSize)
+        let descent = TextUtilities.textEmojiGetDescent(with: fontSize)
+        let bounding: CGRect = TextUtilities.textEmojiGetGlyphBoundingRect(with: fontSize)
+        let delegate = TextRunDelegate()
+        delegate.ascent = ascent
+        delegate.descent = descent
+        delegate.width = bounding.size.width + 2 * bounding.origin.x
+        let attachment = TextAttachment()
+        attachment.contentMode = UIView.ContentMode.scaleAspectFit
+        attachment.contentInsets = UIEdgeInsets(top: ascent - (bounding.size.height + bounding.origin.y), left: bounding.origin.x, bottom: descent + bounding.origin.y, right: bounding.origin.x)
+
+        if hasAnim {
+            #if canImport(YYImage)
+            let view = YYAnimatedImageView()
+            #else
+            let view = UIImageView()
+            #endif
+            
+            view.frame = bounding
+            view.image = image
+            view.contentMode = .scaleAspectFit
+            attachment.content = view
+        } else {
+            attachment.content = image
+        }
+        let atr = NSMutableAttributedString(string: TextAttribute.textAttachmentToken)
+        atr.bs_set(textAttachment: attachment, range: NSRange(location: 0, length: atr.length))
+        let ctDelegate = delegate.ctRunDelegate
+        atr.bs_set(runDelegate: ctDelegate, range: NSRange(location: 0, length: atr.length))
+        
+        return atr
+    }
+    
+    // MARK: - Utility
+    
+    /**
+     Returns NSMakeRange(0, self.length).
+     */
+    @objc public var bs_rangeOfAll: NSRange {
+        get {
+            return NSRange(location: 0, length: length)
+        }
+    }
+    
+    /**
+     If YES, it share the same attribute in entire text range.
+     */
+    @objc public func bs_isSharedAttributesInAllRange() -> Bool {
+        
+        var shared = true
+        var firstAttrs: [NSAttributedString.Key : Any]? = nil
+        enumerateAttributes(in: bs_rangeOfAll, options: .longestEffectiveRangeNotRequired, using: { attrs, range, stop in
+            if range.location == 0 {
+                firstAttrs = attrs
+            } else {
+                if firstAttrs?.count != attrs.count {
+                    shared = false
+                    stop.pointee = true
+                } else if let tmp = firstAttrs {
+                    if !(tmp as NSDictionary).isEqual(to: attrs) {
+                        shared = false
+                        stop.pointee = true
+                    }
+                }
+            }
+        })
+        return shared
+    }
+    
+    static var failSet: Set<AnyHashable>?
+    
+    /**
+     If YES, it can be drawn with the [drawWithRect:options:context:] method or displayed with UIKit.
+     If NO, it should be drawn with CoreText or BSText.
+     
+     @discussion If the method returns NO, it means that there's at least one attribute
+     which is not supported by UIKit (such as CTParagraphStyleRef). If display this string
+     in UIKit, it may lose some attribute, or even crash the app.
+     */
+    @objc public func bs_canDrawWithUIKit() -> Bool {
+        
+        if (NSAttributedString.failSet == nil) {
+            var failSet = Set<AnyHashable>()
+            let _ = failSet.insert(kCTGlyphInfoAttributeName)
+            let _ = failSet.insert(kCTCharacterShapeAttributeName)
+            let _ = failSet.insert(kCTLanguageAttributeName)
+            let _ = failSet.insert(kCTRunDelegateAttributeName)
+            let _ = failSet.insert(kCTBaselineClassAttributeName)
+            let _ = failSet.insert(kCTBaselineInfoAttributeName)
+            let _ = failSet.insert(kCTBaselineReferenceInfoAttributeName)
+            let _ = failSet.insert(kCTRubyAnnotationAttributeName)
+            let _ = failSet.insert(TextAttribute.textShadowAttributeName)
+            let _ = failSet.insert(TextAttribute.textInnerShadowAttributeName)
+            let _ = failSet.insert(TextAttribute.textUnderlineAttributeName)
+            let _ = failSet.insert(TextAttribute.textStrikethroughAttributeName)
+            let _ = failSet.insert(TextAttribute.textBorderAttributeName)
+            let _ = failSet.insert(TextAttribute.textBackgroundBorderAttributeName)
+            let _ = failSet.insert(TextAttribute.textBlockBorderAttributeName)
+            let _ = failSet.insert(TextAttribute.textAttachmentAttributeName)
+            let _ = failSet.insert(TextAttribute.textHighlightAttributeName)
+            let _ = failSet.insert(TextAttribute.textGlyphTransformAttributeName)
+
+            NSAttributedString.failSet = failSet
+        }
+
+        let failSet = NSAttributedString.failSet!
+
+        var result = true
+        enumerateAttributes(in: bs_rangeOfAll, options: .longestEffectiveRangeNotRequired, using: { (attrs, range, stop) in
+            if attrs.count == 0 {
+                return
+            }
+            for str: NSAttributedString.Key in attrs.keys {
+                if failSet.contains(str.rawValue) {
+                    result = false
+                    stop.pointee = true
+                    return
+                }
+            }
+
+            if attrs[NSAttributedString.Key(rawValue: kCTForegroundColorAttributeName as String)] != nil && attrs[NSAttributedString.Key.foregroundColor] == nil {
+                result = false
+                stop.pointee = true
+                return
+            }
+
+            if attrs[NSAttributedString.Key(rawValue: kCTStrokeColorAttributeName as String)] != nil && attrs[NSAttributedString.Key.strokeColor] == nil {
+                result = false
+                stop.pointee = true
+                return
+            }
+
+            if attrs[NSAttributedString.Key(rawValue: kCTUnderlineColorAttributeName as String)] != nil && attrs[NSAttributedString.Key.underlineColor] == nil {
+                result = false
+                stop.pointee = true
+                return
+            }
+
+            let style = attrs[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle
+            if style != nil && CFGetTypeID(style!) == CTParagraphStyleGetTypeID() {
+                result = false
+                stop.pointee = true
+                return
+            }
+        })
+        return result
+    }
+    
+}
+
+/**
+ Set pre-defined attributes to attributed string.
+ All properties defined in UIKit, CoreText and BSText are included.
+ */
+extension NSMutableAttributedString {
+    
+    // MARK: - Set character attribute
+    
+    /**
+     Sets the attributes to the entire text string.
+     
+     @discussion The old attributes will be removed.
+     
+     @param attributes  A dictionary containing the attributes to set, or nil to remove all attributes.
+     */
+    @objc public func bs_setAttributes(_ attributes: [NSAttributedString.Key : Any]?) {
+        self.bs_attributes = attributes     // setter
+    }
+    
+    /**
+     Returns the attributes at first charactor.
+     */
+    @objc public override var bs_attributes: [NSAttributedString.Key : Any]? {
+        set {
+            setAttributes([:], range: NSRange(location: 0, length: length))
+            
+            guard let attr = newValue else {
+                return
+            }
+            
+            for (_, ele) in attr.enumerated() {
+                self.bs_set(attribute: ele.key, value: ele.value)
+            }
+        }
+        get {
+            return super.bs_attributes
+        }
+    }
+    
+    
+    /**
+     Sets an attribute with the given name and value to the entire text string.
+     
+     @param name   A string specifying the attribute name.
+     @param value  The attribute value associated with name. Pass `nil` or `NSNull` to
+     remove the attribute.
+     */
+    @objc(bs_setAttribute:value:)
+    public func bs_set(attribute name: NSAttributedString.Key?, value: Any?) {
+        bs_set(attribute: name, value: value, range: NSRange(location: 0, length: length))
+    }
+    
+    /**
+     Sets an attribute with the given name and value to the characters in the specified range.
+     
+     @param name   A string specifying the attribute name.
+     @param value  The attribute value associated with name. Pass `nil` or `NSNull` to
+     remove the attribute.
+     @param range  The range of characters to which the specified attribute/value pair applies.
+     */
+    @objc(bs_setAttribute:value:range:)
+    public func bs_set(attribute name: NSAttributedString.Key?, value: Any?, range: NSRange) {
+        guard let n = name else {
+            return
+        }
+        if let aValue = value {
+            addAttribute(n, value: aValue, range: range)
+        } else {
+            removeAttribute(n, range: range)
+        }
+    }
+    
+    /**
+     Removes all attributes in the specified range.
+     
+     @param range  The range of characters.
+     */
+    @objc(bs_removeAttributesInRange:)
+    public func bs_removeAttributes(in range: NSRange) {
+        setAttributes(nil, range: range)
+    }
+    
+    // MARK: - Set character attribute as property
+    
+    @objc public override var bs_font: UIFont? {
+        set {
+            /*
+             In iOS7 and later, UIFont is toll-free bridged to CTFontRef,
+             although Apple does not mention it in documentation.
+             
+             In iOS6, UIFont is a wrapper for CTFontRef, so CoreText can alse use UIfont,
+             but UILabel/UITextView cannot use CTFontRef.
+             
+             We use UIFont for both CoreText and UIKit.
+             */
+            bs_set(font: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_font
+        }
+    }
+    
+    public override var bs_kern: NSNumber? {
+        set {
+            bs_set(kern: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_kern
+        }
+    }
+    
+    @objc public override var bs_color: UIColor? {
+        set {
+            bs_set(color: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_color
+        }
+    }
+    
+    @objc public override var bs_backgroundColor: UIColor? {
+        set {
+            bs_set(backgroundColor: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_backgroundColor
+        }
+    }
+    
+    public override var bs_strokeWidth: NSNumber? {
+        set {
+            bs_set(strokeWidth: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_strokeWidth
+        }
+    }
+    
+    public override var bs_strokeColor: UIColor? {
+        set {
+            bs_set(strokeColor: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_strokeColor
+        }
+    }
+    
+    public override var bs_shadow: NSShadow? {
+        get {
+            return super.bs_shadow
+        }
+        set {
+            bs_set(shadow: newValue, range: NSRange(location: 0, length: length))
+        }
+    }
+    
+    public override var bs_strikethroughStyle: NSUnderlineStyle {
+        set {
+            bs_set(strikethroughStyle: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_strikethroughStyle
+        }
+    }
+    
+    public override var bs_strikethroughColor: UIColor? {
+        set {
+            bs_set(strikethroughColor: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_strikethroughColor
+        }
+    }
+    
+    public override var bs_underlineStyle: NSUnderlineStyle {
+        set {
+            bs_set(underlineStyle: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_underlineStyle
+        }
+    }
+    
+    public override var bs_underlineColor: UIColor? {
+        set {
+            bs_set(underlineColor: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_underlineColor
+        }
+    }
+    
+    public override var bs_ligature: NSNumber? {
+        set {
+            bs_set(ligature: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_ligature
+        }
+    }
+    
+    public override var bs_textEffect: String? {
+        set {
+            bs_set(textEffect: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_textEffect
+        }
+    }
+    
+    public override var bs_obliqueness: NSNumber? {
+        set {
+            bs_set(obliqueness: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_obliqueness
+        }
+    }
+    
+    public override var bs_expansion: NSNumber? {
+        get {
+            return super.bs_expansion
+        }
+        set {
+            bs_set(expansion: newValue, range: NSRange(location: 0, length: length))
+        }
+    }
+    
+    public override var bs_baselineOffset: NSNumber? {
+        set {
+            bs_set(baselineOffset: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_baselineOffset
+        }
+    }
+    
+    public override var bs_verticalGlyphForm: Bool {
+        get {
+            return super.bs_verticalGlyphForm
+        }
+        set {
+            bs_set(verticalGlyphForm: newValue, range: NSRange(location: 0, length: length))
+        }
+    }
+    
+    public override var bs_language: String? {
+        get {
+            return super.bs_language
+        }
+        set {
+            bs_set(language: newValue, range: NSRange(location: 0, length: length))
+        }
+    }
+    
+    public override var bs_writingDirection: [Any]? {
+        get {
+            return super.bs_writingDirection
+        }
+        set {
+            bs_set(writingDirection: newValue, range: NSRange(location: 0, length: length))
+        }
+    }
+    
+    public override var bs_paragraphStyle: NSParagraphStyle? {
+        get {
+            return super.bs_paragraphStyle
+        }
+        set {
+            /*
+             NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef.
+             
+             CoreText can use both NSParagraphStyle and CTParagraphStyleRef,
+             but UILabel/UITextView can only use NSParagraphStyle.
+             
+             We use NSParagraphStyle in both CoreText and UIKit.
+             */
+            bs_set(paragraphStyle: newValue, range: NSRange(location: 0, length: length))
+        }
+    }
+    
+    public override var bs_alignment: NSTextAlignment {
+        set {
+            bs_set(alignment: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_alignment
+        }
+    }
+    
+    public override var bs_baseWritingDirection: NSWritingDirection {
+        set {
+            bs_set(baseWritingDirection: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_baseWritingDirection
+        }
+    }
+    
+    public override var bs_lineSpacing: CGFloat {
+        set {
+            bs_set(lineSpacing: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_lineSpacing
+        }
+    }
+    
+    public override var bs_paragraphSpacing: CGFloat {
+        set {
+            bs_set(paragraphSpacing: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_paragraphSpacing
+        }
+    }
+    
+    public override var bs_paragraphSpacingBefore: CGFloat {
+        set {
+            bs_set(paragraphSpacing: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_paragraphSpacingBefore
+        }
+    }
+    
+    public override var bs_firstLineHeadIndent: CGFloat {
+        set {
+            bs_set(firstLineHeadIndent: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_firstLineHeadIndent
+        }
+    }
+    
+    public override var bs_headIndent: CGFloat {
+        set {
+            bs_set(headIndent: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_headIndent
+        }
+    }
+    
+    public override var bs_tailIndent: CGFloat {
+        set {
+            bs_set(tailIndent: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_tailIndent
+        }
+    }
+    
+    public override var bs_lineBreakMode: NSLineBreakMode {
+        set {
+            bs_set(lineBreakMode: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_lineBreakMode
+        }
+    }
+    
+    public override var bs_minimumLineHeight: CGFloat {
+        set {
+            bs_set(minimumLineHeight: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_minimumLineHeight
+        }
+    }
+    
+    public override var bs_maximumLineHeight: CGFloat {
+        set {
+            bs_set(maximumLineHeight: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_maximumLineHeight
+        }
+    }
+    
+    public override var bs_lineHeightMultiple: CGFloat {
+        set {
+            bs_set(lineHeightMultiple: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_lineHeightMultiple
+        }
+    }
+    
+    public override var bs_hyphenationFactor: Float {
+        set {
+            bs_set(hyphenationFactor: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_hyphenationFactor
+        }
+    }
+    
+    public override var bs_defaultTabInterval: CGFloat {
+        set {
+            bs_set(defaultTabInterval: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_defaultTabInterval
+        }
+    }
+    
+    public override var bs_tabStops: [NSTextTab]? {
+        set {
+            bs_set(tabStops: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_tabStops
+        }
+    }
+    
+    public override var bs_textShadow: TextShadow? {
+        set {
+            bs_set(textShadow: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_textShadow
+        }
+    }
+    
+    public override var bs_textInnerShadow: TextShadow? {
+        set {
+            bs_set(textInnerShadow: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_textInnerShadow
+        }
+    }
+    
+    public override var bs_textUnderline: TextDecoration? {
+        set {
+            bs_set(textUnderline: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_textUnderline
+        }
+    }
+    
+    public override var bs_textStrikethrough: TextDecoration? {
+        set {
+            bs_set(textStrikethrough: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_textStrikethrough
+        }
+    }
+    
+    public override var bs_textBorder: TextBorder? {
+        set {
+            bs_set(textBorder: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_textBorder
+        }
+    }
+    
+    public override var bs_textBackgroundBorder: TextBorder? {
+        set {
+            bs_set(textBackgroundBorder: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_textBackgroundBorder
+        }
+    }
+    
+    public override var bs_textGlyphTransform: CGAffineTransform {
+        set {
+            bs_set(textGlyphTransform: newValue, range: NSRange(location: 0, length: length))
+        }
+        get {
+            return super.bs_textGlyphTransform
+        }
+    }
+    
+    // MARK: - Range Setter
+    
+    @objc(bs_setFont:range:)
+    public func bs_set(font: UIFont?, range: NSRange) {
+        /*
+         In iOS7 and later, UIFont is toll-free bridged to CTFontRef,
+         although Apple does not mention it in documentation.
+         
+         In iOS6, UIFont is a wrapper for CTFontRef, so CoreText can alse use UIfont,
+         but UILabel/UITextView cannot use CTFontRef.
+         
+         We use UIFont for both CoreText and UIKit.
+         */
+        bs_set(attribute: NSAttributedString.Key.font, value: font, range: range)
+    }
+    
+    @objc(bs_setKern:range:)
+    public func bs_set(kern: NSNumber?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.kern, value: kern, range: range)
+    }
+    
+    @objc(bs_setColor:range:)
+    public func bs_set(color: UIColor?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTForegroundColorAttributeName as String), value: color?.cgColor, range: range)
+        bs_set(attribute: NSAttributedString.Key.foregroundColor, value: color, range: range)
+    }
+    
+    @objc(bs_setBackgroundColor:range:)
+    public func bs_set(backgroundColor: UIColor?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.backgroundColor, value: backgroundColor, range: range)
+    }
+    
+    @objc(bs_setStrokeWidth:range:)
+    public func bs_set(strokeWidth: NSNumber?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.strokeWidth, value: strokeWidth, range: range)
+    }
+    
+    @objc(bs_setStrokeColor:range:)
+    public func bs_set(strokeColor: UIColor?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTStrokeColorAttributeName as String), value: strokeColor?.cgColor, range: range)
+        bs_set(attribute: NSAttributedString.Key.strokeColor, value: strokeColor, range: range)
+    }
+    
+    @objc(bs_setShadow:range:)
+    public func bs_set(shadow: NSShadow?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.shadow, value: shadow, range: range)
+    }
+    
+    @objc(bs_setStrikethroughStyle:range:)
+    public func bs_set(strikethroughStyle: NSUnderlineStyle, range: NSRange) {
+        let style = strikethroughStyle.rawValue == 0 ? nil : strikethroughStyle.rawValue
+        bs_set(attribute: NSAttributedString.Key.strikethroughStyle, value: style, range: range)
+    }
+    
+    @objc(bs_setStrikethroughColor:range:)
+    public func bs_set(strikethroughColor: UIColor?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.strikethroughColor, value: strikethroughColor, range: range)
+    }
+    
+    @objc(bs_setUnderlineStyle:range:)
+    public func bs_set(underlineStyle: NSUnderlineStyle, range: NSRange) {
+        let style = underlineStyle.rawValue == 0 ? nil : underlineStyle.rawValue
+        bs_set(attribute: NSAttributedString.Key.underlineStyle, value: style, range: range)
+    }
+    
+    @objc(bs_setUnderlineColor:range:)
+    public func bs_set(underlineColor: UIColor?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTUnderlineColorAttributeName as String), value: underlineColor?.cgColor, range: range)
+        bs_set(attribute: NSAttributedString.Key.underlineColor, value: underlineColor, range: range)
+    }
+    
+    @objc(bs_setLigature:range:)
+    public func bs_set(ligature: NSNumber?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.ligature, value: ligature, range: range)
+    }
+    
+    @objc(bs_setTextEffect:range:)
+    public func bs_set(textEffect: String?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.textEffect, value: textEffect, range: range)
+    }
+    
+    @objc(bs_setObliqueness:range:)
+    public func bs_set(obliqueness: NSNumber?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.obliqueness, value: obliqueness, range: range)
+    }
+    
+    @objc(bs_setExpansion:range:)
+    public func bs_set(expansion: NSNumber?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.expansion, value: expansion, range: range)
+    }
+    
+    @objc(bs_setBaselineOffset:range:)
+    public func bs_set(baselineOffset: NSNumber?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.baselineOffset, value: baselineOffset, range: range)
+    }
+    
+    @objc(bs_setVerticalGlyphForm:range:)
+    public func bs_set(verticalGlyphForm: Bool, range: NSRange) {
+        let v = verticalGlyphForm ? true : nil
+        bs_set(attribute: NSAttributedString.Key.verticalGlyphForm, value: v, range: range)
+    }
+    
+    @objc(bs_setLanguage:range:)
+    public func bs_set(language: String?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTLanguageAttributeName as String), value: language, range: range)
+    }
+    
+    @objc(bs_setWritingDirection:range:)
+    public func bs_set(writingDirection: [Any]?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTWritingDirectionAttributeName as String), value: writingDirection, range: range)
+    }
+    
+    @objc(bs_setParagraphStyle:range:)
+    public func bs_set(paragraphStyle: NSParagraphStyle?, range: NSRange) {
+        /*
+         NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef.
+         
+         CoreText can use both NSParagraphStyle and CTParagraphStyleRef,
+         but UILabel/UITextView can only use NSParagraphStyle.
+         
+         We use NSParagraphStyle in both CoreText and UIKit.
+         */
+        bs_set(attribute: NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range)
+    }
+    
+    @objc(bs_setAlignment:range:)
+    public func bs_set(alignment: NSTextAlignment, range: NSRange) {
+        
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.alignment == alignment {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.alignment == alignment {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.alignment = alignment
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setBaseWritingDirection:range:)
+    public func bs_set(baseWritingDirection: NSWritingDirection, range: NSRange) {
+        
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.baseWritingDirection == baseWritingDirection {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.baseWritingDirection == baseWritingDirection {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.baseWritingDirection = baseWritingDirection
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setLineSpacing:range:)
+    public func bs_set(lineSpacing: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.lineSpacing == lineSpacing {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.lineSpacing == lineSpacing {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.lineSpacing = lineSpacing
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setParagraphSpacing:range:)
+    public func bs_set(paragraphSpacing: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.paragraphSpacing == paragraphSpacing {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.paragraphSpacing == paragraphSpacing {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.paragraphSpacing = paragraphSpacing
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setParagraphSpacingBefore:range:)
+    public func bs_set(paragraphSpacingBefore: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.paragraphSpacingBefore == paragraphSpacingBefore {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.paragraphSpacingBefore == paragraphSpacingBefore {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.paragraphSpacingBefore = paragraphSpacingBefore
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setFirstLineHeadIndent:range:)
+    public func bs_set(firstLineHeadIndent: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.firstLineHeadIndent == firstLineHeadIndent {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.firstLineHeadIndent == firstLineHeadIndent {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.firstLineHeadIndent = firstLineHeadIndent
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setHeadIndent:range:)
+    public func bs_set(headIndent: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.headIndent == headIndent {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.headIndent == headIndent {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.headIndent = headIndent
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setTailIndent:range:)
+    public func bs_set(tailIndent: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.tailIndent == tailIndent {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.tailIndent == tailIndent {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.tailIndent = tailIndent
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setLineBreakMode:range:)
+    public func bs_set(lineBreakMode: NSLineBreakMode, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.lineBreakMode == lineBreakMode {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.lineBreakMode == lineBreakMode {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.lineBreakMode = lineBreakMode
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setMinimumLineHeight:range:)
+    public func bs_set(minimumLineHeight: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.minimumLineHeight == minimumLineHeight {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.minimumLineHeight == minimumLineHeight {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.minimumLineHeight = minimumLineHeight
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setMaximumLineHeight:range:)
+    public func bs_set(maximumLineHeight: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.maximumLineHeight == maximumLineHeight {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.maximumLineHeight == maximumLineHeight {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.maximumLineHeight = maximumLineHeight
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setLineHeightMultiple:range:)
+    public func bs_set(lineHeightMultiple: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.lineHeightMultiple == lineHeightMultiple {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.lineHeightMultiple == lineHeightMultiple {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.lineHeightMultiple = lineHeightMultiple
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setHyphenationFactor:range:)
+    public func bs_set(hyphenationFactor: Float, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.hyphenationFactor == hyphenationFactor {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.hyphenationFactor == hyphenationFactor {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.hyphenationFactor = hyphenationFactor
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setDefaultTabInterval:range:)
+    public func bs_set(defaultTabInterval: CGFloat, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.defaultTabInterval == defaultTabInterval {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.defaultTabInterval == defaultTabInterval {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            style?.defaultTabInterval = defaultTabInterval
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setTabStops:range:)
+    public func bs_set(tabStops: [NSTextTab]?, range: NSRange) {
+        enumerateAttribute(.paragraphStyle, in: range, options: [], using: { valuein, subRange, stop in
+            var style: NSMutableParagraphStyle? = nil
+            if var value = valuein as? NSParagraphStyle {
+                if CFGetTypeID(value) == CTParagraphStyleGetTypeID() {
+                    value = NSParagraphStyle.bs_styleWith(ctStyle: value as! CTParagraphStyle)
+                }
+                if value.tabStops == tabStops {
+                    return
+                }
+                if (value is NSMutableParagraphStyle) {
+                    style = value as? NSMutableParagraphStyle
+                } else {
+                    style = value as? NSMutableParagraphStyle
+                }
+            } else {
+                if NSParagraphStyle.default.tabStops == tabStops {
+                    return
+                }
+                style = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
+            }
+            if let aStops = tabStops {
+                style?.tabStops = aStops
+            }
+            self.bs_set(paragraphStyle: style, range: subRange)
+        })
+    }
+    
+    @objc(bs_setSuperscript:range:)
+    public func bs_set(superscript: NSNumber?, range: NSRange) {
+        var s = superscript
+        if (s == 0) {
+            s = nil
+        }
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTSuperscriptAttributeName as String), value: s, range: range)
+    }
+    
+    @objc(bs_setGlyphInfo:range:)
+    public func bs_set(glyphInfo: CTGlyphInfo?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTGlyphInfoAttributeName as String), value: glyphInfo, range: range)
+    }
+    
+    @objc(bs_setCharacterShape:range:)
+    public func bs_set(characterShape: NSNumber?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTCharacterShapeAttributeName as String), value: characterShape, range: range)
+    }
+    
+    @objc(bs_setRunDelegate:range:)
+    public func bs_set(runDelegate: CTRunDelegate?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String), value: runDelegate, range: range)
+    }
+    
+    @objc(bs_setBaselineClass:range:)
+    public func bs_set(baselineClass: CFString?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTBaselineClassAttributeName as String), value: baselineClass, range: range)
+    }
+    
+    @objc(bs_setBaselineInfo:range:)
+    public func bs_set(baselineInfo: CFDictionary?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTBaselineInfoAttributeName as String), value: baselineInfo, range: range)
+    }
+    
+    @objc(bs_setBaselineReferenceInfo:range:)
+    public func bs_set(referenceInfo: CFDictionary?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTBaselineReferenceInfoAttributeName as String), value: referenceInfo, range: range)
+    }
+    
+    @objc(bs_setRubyAnnotation:range:)
+    public func bs_set(rubyAnnotation ruby: CTRubyAnnotation?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: kCTRubyAnnotationAttributeName as String), value: ruby, range: range)
+    }
+    
+    @objc(bs_setAttachment:range:)
+    public func bs_set(attachment: NSTextAttachment?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.attachment, value: attachment, range: range)
+    }
+    
+    @objc(bs_setLink:range:)
+    public func bs_set(link: Any?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key.link, value: link, range: range)
+    }
+    
+    @objc(bs_setTextBackedString:range:)
+    public func bs_set(textBackedString: TextBackedString?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textBackedStringAttributeName), value: textBackedString, range: range)
+    }
+    
+    @objc(bs_setTextBinding:range:)
+    public func bs_set(textBinding: TextBinding?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textBindingAttributeName), value: textBinding, range: range)
+    }
+    
+    @objc(bs_setTextShadow:range:)
+    public func bs_set(textShadow: TextShadow?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textShadowAttributeName), value: textShadow, range: range)
+    }
+    
+    @objc(bs_setTextInnerShadow:range:)
+    public func bs_set(textInnerShadow: TextShadow?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textInnerShadowAttributeName), value: textInnerShadow, range: range)
+    }
+    
+    @objc(bs_setTextUnderline:range:)
+    public func bs_set(textUnderline: TextDecoration?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textUnderlineAttributeName), value: textUnderline, range: range)
+    }
+    
+    @objc(bs_setTextStrikethrough:range:)
+    public func bs_set(textStrikethrough: TextDecoration?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textStrikethroughAttributeName), value: textStrikethrough, range: range)
+    }
+    
+    @objc(bs_setTextBorder:range:)
+    public func bs_set(textBorder: TextBorder?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textBorderAttributeName), value: textBorder, range: range)
+    }
+    
+    @objc(bs_setTextBackgroundBorder:range:)
+    public func bs_set(textBackgroundBorder: TextBorder?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textBackgroundBorderAttributeName), value: textBackgroundBorder, range: range)
+    }
+    
+    @objc(bs_setTextAttachment:range:)
+    public func bs_set(textAttachment: TextAttachment?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textAttachmentAttributeName), value: textAttachment, range: range)
+    }
+    
+    @objc(bs_setTextHighlight:range:)
+    public func bs_set(textHighlight: TextHighlight?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textHighlightAttributeName), value: textHighlight, range: range)
+    }
+    
+    @objc(bs_setTextBlockBorder:range:)
+    public func bs_set(textBlockBorder: TextBorder?, range: NSRange) {
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textBlockBorderAttributeName), value: textBlockBorder, range: range)
+    }
+    
+    @objc(bs_setTextRubyAnnotation:range:)
+    public func bs_set(textRubyAnnotation ruby: TextRubyAnnotation?, range: NSRange) {
+        let rubyRef = ruby?.ctRubyAnnotation()
+        bs_set(rubyAnnotation: rubyRef, range: range)
+    }
+    
+    @objc(bs_setTextGlyphTransform:range:)
+    public func bs_set(textGlyphTransform: CGAffineTransform, range: NSRange) {
+        let value = textGlyphTransform.isIdentity ? nil : NSValue(cgAffineTransform: textGlyphTransform)
+        bs_set(attribute: NSAttributedString.Key(rawValue: TextAttribute.textGlyphTransformAttributeName), value: value, range: range)
+    }
+    
+    // Convenience methods for text highlight
+    
+    /**
+     Convenience method to set text highlight
+     
+     @param range           text range
+     @param color           text color (pass nil to ignore)
+     @param backgroundColor text background color when highlight
+     @param userInfo        user information dictionary (pass nil to ignore)
+     @param action          tap action when user tap the highlight (pass nil to ignore)
+     @param longPressAction long press action when user long press the highlight (pass nil to ignore)
+     */
+    @objc(bs_setTextHighlightRange:color:backgroundColor:userInfo:tapAction:longPress:)
+    public func bs_set(textHighlightRange range: NSRange, color: UIColor?, backgroundColor: UIColor?, userInfo: [AnyHashable : Any]?, tapAction action: TextAction?, longPress longPressAction: TextAction?) {
+        
+        let highlight = TextHighlight.highlight(with: backgroundColor)
+        highlight.userInfo = userInfo as NSDictionary?
+        highlight.tapAction = action
+        highlight.longPressAction = longPressAction
+        if color != nil {
+            bs_set(color: color, range: range)
+        }
+        bs_set(textHighlight: highlight, range: range)
+    }
+    
+    /**
+     Convenience method to set text highlight
+     
+     @param range           text range
+     @param color           text color (pass nil to ignore)
+     @param backgroundColor text background color when highlight
+     @param action          tap action when user tap the highlight (pass nil to ignore)
+     */
+    @objc(bs_setTextHighlightRange:color:backgroundColor:tapAction:)
+    public func bs_set(textHighlightRange range: NSRange, color: UIColor?, backgroundColor: UIColor?, tapAction action: TextAction?) {
+        bs_set(textHighlightRange: range, color: color, backgroundColor: backgroundColor, userInfo: nil, tapAction: action, longPress: nil)
+    }
+    
+    /**
+     Convenience method to set text highlight
+     
+     @param range           text range
+     @param color           text color (pass nil to ignore)
+     @param backgroundColor text background color when highlight
+     @param userInfo        tap action when user tap the highlight (pass nil to ignore)
+     */
+    @objc(bs_setTextHighlightRange:color:backgroundColor:userInfo:)
+    public func bs_set(textHighlightRange range: NSRange, color: UIColor?, backgroundColor: UIColor?, userInfo: [AnyHashable : Any]?) {
+        bs_set(textHighlightRange: range, color: color, backgroundColor: backgroundColor, userInfo: userInfo, tapAction: nil, longPress: nil)
+    }
+    
+    // MARK: - Utilities
+    
+    /**
+     Inserts into the receiver the characters of a given string at a given location.
+     The new string inherit the attributes of the first replaced character from location.
+     
+     @param string  The string to insert into the receiver, must not be nil.
+     @param location The location at which string is inserted. The location must not
+     exceed the bounds of the receiver.
+     @throw Raises an NSRangeException if the location out of bounds.
+     */
+    @objc(bs_insertString:atIndex:)
+    public func bs_insert(string: String?, at location: Int) {
+        guard let s = string else {
+            return
+        }
+        replaceCharacters(in: NSRange(location: location, length: 0), with: s)
+        bs_removeDiscontinuousAttributes(in: NSRange(location: location, length: s.count))
+    }
+    
+    /**
+     Adds to the end of the receiver the characters of a given string.
+     The new string inherit the attributes of the receiver's tail.
+     
+     @param string  The string to append to the receiver, must not be nil.
+     */
+    @objc(bs_appendString:)
+    public func bs_append(string: String?) {
+        guard let _ = string else {
+            return
+        }
+        let length = self.length
+        replaceCharacters(in: NSRange(location: length, length: 0), with: string!)
+        bs_removeDiscontinuousAttributes(in: NSRange(location: length, length: string!.count))
+    }
+    
+    static var clearColorToJoinedEmojiRegex: NSRegularExpression?
+    
+    /**
+     Set foreground color with [UIColor clearColor] in joined-emoji range.
+     Emoji drawing will not be affected by the foreground color.
+     
+     @discussion In iOS 8.3, Apple releases some new diversified emojis.
+     There's some single emoji which can be assembled to a new 'joined-emoji'.
+     The joiner is unicode character 'ZERO WIDTH JOINER' (U+200D).
+     For example: 👨👩👧👧 -> 👨‍👩‍👧‍👧.
+     
+     When there are more than 5 'joined-emoji' in a same CTLine, CoreText may render some
+     extra glyphs above the emoji. It's a bug in CoreText, try this method to avoid.
+     This bug is fixed in iOS 9.
+     */
+    @objc public func bs_setClearColorToJoinedEmoji() {
+        let str = string
+        if str.length < 8 {
+            return
+        }
+        // Most string do not contains the joined-emoji, test the joiner first.
+        var containsJoiner = false
+        let nsStr = str as NSString
+        
+        for i in 0..<nsStr.length {
+            let char: UniChar = nsStr.character(at: i)
+            if char == 0x200d {
+                // 'ZERO WIDTH JOINER' (U+200D)
+                containsJoiner = true
+                break
+            }
+        }
+        if !containsJoiner {
+            return
+        }
+        
+        if (NSMutableAttributedString.clearColorToJoinedEmojiRegex == nil) {
+            let regex = try? NSRegularExpression(pattern: "((👨‍👩‍👧‍👦|👨‍👩‍👦‍👦|👨‍👩‍👧‍👧|👩‍👩‍👧‍👦|👩‍👩‍👦‍👦|👩‍👩‍👧‍👧|👨‍👨‍👧‍👦|👨‍👨‍👦‍👦|👨‍👨‍👧‍👧)+|(👨‍👩‍👧|👩‍👩‍👦|👩‍👩‍👧|👨‍👨‍👦|👨‍👨‍👧))", options: [])
+            NSMutableAttributedString.clearColorToJoinedEmojiRegex = regex
+        }
+        let regex = NSMutableAttributedString.clearColorToJoinedEmojiRegex
+        
+        let clear = UIColor.clear
+        regex?.enumerateMatches(in: str, options: [], range: NSRange(location: 0, length: str.length), using: { result, flags, stop in
+            self.bs_set(color: clear, range: (result?.range)!)
+        })
+    }
+    
+    static var allDiscontinuousAttributeKeys: [NSAttributedString.Key]?
+    
+    /**
+     Returns all discontinuous attribute keys, such as RunDelegate/Attachment/Ruby.
+     
+     @discussion These attributes can only set to a specified range of text, and
+     should not extend to other range when editing text.
+     */
+    @objc public class func bs_allDiscontinuousAttributeKeys() -> [NSAttributedString.Key] {
+        
+        if allDiscontinuousAttributeKeys == nil {
+            
+            var keys = [NSAttributedString.Key]()
+            
+            keys.append(NSAttributedString.Key(kCTSuperscriptAttributeName as String))
+            keys.append(NSAttributedString.Key(kCTRunDelegateAttributeName as String))
+            keys.append(NSAttributedString.Key(TextAttribute.textBackedStringAttributeName))
+            keys.append(NSAttributedString.Key(TextAttribute.textBindingAttributeName))
+            keys.append(NSAttributedString.Key(TextAttribute.textAttachmentAttributeName))
+            keys.append(NSAttributedString.Key(kCTRubyAnnotationAttributeName as String))
+            keys.append(NSAttributedString.Key.attachment)
+            
+            allDiscontinuousAttributeKeys = keys
+        }
+        
+        let keys = allDiscontinuousAttributeKeys!
+        
+        return keys
+    }
+    
+    /**
+     Removes all discontinuous attributes in a specified range.
+     See `allDiscontinuousAttributeKeys`.
+     
+     @param range A text range.
+     */
+    @objc(bs_removeDiscontinuousAttributesInRange:)
+    public func bs_removeDiscontinuousAttributes(in range: NSRange) {
+        
+        for key in NSMutableAttributedString.bs_allDiscontinuousAttributeKeys() {
+            removeAttribute(key, range: range)
+        }
+    }
+}

+ 180 - 0
Pods/BSText/BSText/Utility/ParagraphStyleExtension.swift

@@ -0,0 +1,180 @@
+//
+//  ParagraphStyleExtension.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/10/23.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+/**
+ Provides extensions for `NSParagraphStyle` to work with CoreText.
+ */
+public extension NSParagraphStyle {
+    /**
+     Creates a new NSParagraphStyle object from the CoreText Style.
+     
+     @param ctStyle CoreText Paragraph Style.
+     
+     @return a new NSParagraphStyle
+     */
+    @objc(bs_styleWithCTStyle:)
+    class func bs_styleWith(ctStyle: CTParagraphStyle) -> NSParagraphStyle {
+        
+        let style = NSMutableParagraphStyle()
+        
+        var lineSpacing: CGFloat = 0
+        
+        // MARK: - YYText 中用的是 kCTParagraphStyleSpecifierLineSpacing
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.lineSpacingAdjustment, MemoryLayout<CGFloat>.size, &lineSpacing) {
+            style.lineSpacing = lineSpacing
+        }
+        
+        var paragraphSpacing: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.paragraphSpacing, MemoryLayout<CGFloat>.size, &paragraphSpacing) {
+            style.paragraphSpacing = paragraphSpacing
+        }
+        var alignment: CTTextAlignment?
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.alignment, MemoryLayout<CTTextAlignment>.size, &alignment) {
+            style.alignment = NSTextAlignment(alignment!)
+        }
+        var firstLineHeadIndent: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.firstLineHeadIndent, MemoryLayout<CGFloat>.size, &firstLineHeadIndent) {
+            style.firstLineHeadIndent = firstLineHeadIndent
+        }
+        var headIndent: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.headIndent, MemoryLayout<CGFloat>.size, &headIndent) {
+            style.headIndent = headIndent
+        }
+        var tailIndent: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.tailIndent, MemoryLayout<CGFloat>.size, &tailIndent) {
+            style.tailIndent = tailIndent
+        }
+        var lineBreakMode: CTLineBreakMode?
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.lineBreakMode, MemoryLayout<CTLineBreakMode>.size, &lineBreakMode) {
+            if let aMode = NSLineBreakMode(rawValue: Int(lineBreakMode!.rawValue)) {
+                style.lineBreakMode = aMode
+            }
+        }
+        var minimumLineHeight: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.minimumLineHeight, MemoryLayout<CGFloat>.size, &minimumLineHeight) {
+            style.minimumLineHeight = minimumLineHeight
+        }
+        var maximumLineHeight: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.maximumLineHeight, MemoryLayout<CGFloat>.size, &maximumLineHeight) {
+            style.maximumLineHeight = maximumLineHeight
+        }
+        var baseWritingDirection: CTWritingDirection?
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.baseWritingDirection, MemoryLayout<CTWritingDirection>.size, &baseWritingDirection) {
+            if let aDirection = NSWritingDirection(rawValue: Int(baseWritingDirection!.rawValue)) {
+                style.baseWritingDirection = aDirection
+            }
+        }
+        var lineHeightMultiple: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.lineHeightMultiple, MemoryLayout<CGFloat>.size, &lineHeightMultiple) {
+            style.lineHeightMultiple = lineHeightMultiple
+        }
+        var paragraphSpacingBefore: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.paragraphSpacingBefore, MemoryLayout<CGFloat>.size, &paragraphSpacingBefore) {
+            style.paragraphSpacingBefore = paragraphSpacingBefore
+        }
+        
+        var tabStops: CFArray?
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.tabStops, MemoryLayout<CFArray?>.size, &tabStops) {
+            
+            var tabs = [AnyHashable]()
+            (((tabStops) as? [Any]) as NSArray?)?.enumerateObjects({ obj, idx, stop in
+                let ctTab = obj
+                var tab: NSTextTab? = nil
+                if let aTab = CTTextTabGetOptions(ctTab as! CTTextTab) as? [NSTextTab.OptionKey : Any] {
+                    tab = NSTextTab(textAlignment: NSTextAlignment(CTTextTabGetAlignment(ctTab as! CTTextTab)), location: CGFloat(CTTextTabGetLocation(ctTab as! CTTextTab)), options: aTab)
+                }
+                if let aTab = tab {
+                    tabs.append(aTab)
+                }
+            })
+            if tabs.count != 0 {
+                if let aTabs = tabs as? [NSTextTab] {
+                    style.tabStops = aTabs
+                }
+            }
+        }
+        
+        var defaultTabInterval: CGFloat = 0
+        if CTParagraphStyleGetValueForSpecifier(ctStyle, CTParagraphStyleSpecifier.defaultTabInterval, MemoryLayout<CGFloat>.size, &defaultTabInterval) {
+            
+            style.defaultTabInterval = defaultTabInterval
+        }
+        
+        return style
+    }
+    /**
+     Creates and returns a CoreText Paragraph Style. (need call CFRelease() after used)
+     */
+    @objc func bs_CTStyle() -> CTParagraphStyle {
+        
+        var settings = [CTParagraphStyleSetting]()
+        
+        var lineSpacing: CGFloat = self.lineSpacing
+        settings.append(CTParagraphStyleSetting(spec: .lineSpacingAdjustment, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpacing))
+        
+        var paragraphSpacing: CGFloat = self.paragraphSpacing
+        settings.append(CTParagraphStyleSetting(spec: .paragraphSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &paragraphSpacing))
+        
+        var alignment = CTTextAlignment(self.alignment)
+        settings.append(CTParagraphStyleSetting(spec: .alignment, valueSize: MemoryLayout<CTTextAlignment>.size, value: &alignment))
+        
+        var firstLineHeadIndent: CGFloat = self.firstLineHeadIndent
+        settings.append(CTParagraphStyleSetting(spec: .firstLineHeadIndent, valueSize: MemoryLayout<CGFloat>.size, value: &firstLineHeadIndent))
+        
+        var headIndent: CGFloat = self.headIndent
+        settings.append(CTParagraphStyleSetting(spec: .headIndent, valueSize: MemoryLayout<CGFloat>.size, value: &headIndent))
+        
+        var tailIndent: CGFloat = self.tailIndent
+        settings.append(CTParagraphStyleSetting(spec: .tailIndent, valueSize: MemoryLayout<CGFloat>.size, value: &tailIndent))
+
+        var paraLineBreak = CTLineBreakMode(rawValue: UInt8(self.lineBreakMode.rawValue))
+        settings.append(CTParagraphStyleSetting(spec: .lineBreakMode, valueSize: MemoryLayout<CTLineBreakMode>.size, value: &paraLineBreak))
+        
+        var minimumLineHeight: CGFloat = self.minimumLineHeight
+        settings.append(CTParagraphStyleSetting(spec: .minimumLineHeight, valueSize: MemoryLayout<CGFloat>.size, value: &minimumLineHeight))
+        
+        var maximumLineHeight: CGFloat = self.maximumLineHeight
+        settings.append(CTParagraphStyleSetting(spec: .maximumLineHeight, valueSize: MemoryLayout<CGFloat>.size, value: &maximumLineHeight))
+        
+        var paraWritingDirection = CTWritingDirection(rawValue: Int8(self.baseWritingDirection.rawValue))
+        settings.append(CTParagraphStyleSetting(spec: .baseWritingDirection, valueSize: MemoryLayout<CTWritingDirection>.size, value: &paraWritingDirection))
+        
+        var lineHeightMultiple: CGFloat = self.lineHeightMultiple
+        settings.append(CTParagraphStyleSetting(spec: .lineHeightMultiple, valueSize: MemoryLayout<CGFloat>.size, value: &lineHeightMultiple))
+        
+        var paragraphSpacingBefore: CGFloat = self.paragraphSpacingBefore
+        settings.append(CTParagraphStyleSetting(spec: .paragraphSpacingBefore, valueSize: MemoryLayout<CGFloat>.size, value: &paragraphSpacingBefore))
+
+        if self.responds(to: #selector(getter: self.tabStops)) {
+            var tabs: [AnyHashable] = []
+            
+            let numTabs: Int = self.tabStops.count
+            if numTabs != 0 {
+                (self.tabStops as NSArray).enumerateObjects({ tab, idx, stop in
+                    let tab_: NSTextTab = tab as! NSTextTab
+                    
+                    let ctTab = CTTextTabCreate(CTTextAlignment.init(tab_.alignment), Double(tab_.location), tab_.options as CFDictionary)
+                    
+                    tabs.append(ctTab)
+                })
+                var tabStops = tabs
+                settings.append(CTParagraphStyleSetting(spec: .tabStops, valueSize: MemoryLayout<CFArray>.size, value: &tabStops))
+            }
+            
+            if self.responds(to: #selector(getter: self.defaultTabInterval)) {
+                var defaultTabInterval: CGFloat = self.defaultTabInterval
+                settings.append(CTParagraphStyleSetting(spec: .defaultTabInterval, valueSize: MemoryLayout<CGFloat>.size, value: &defaultTabInterval))
+            }
+        }
+        
+        let style = CTParagraphStyleCreate(settings, settings.count)
+        return style
+    }
+}

+ 98 - 0
Pods/BSText/BSText/Utility/StringExtension.swift

@@ -0,0 +1,98 @@
+//
+//  StringExtension.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/12/11.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import Foundation
+
+extension String {
+    
+    /// compatibility API -intValue for NSString
+    public var toInt: Int? {
+        return Int(self)
+    }
+    
+    /// compatibility API -floatValue for NSString
+    public var toFloat: Float? {
+        return Float(self)
+    }
+    
+    /// compatibility API -doubleValue for NSString
+    public var toDouble: Double? {
+        return Double(self)
+    }
+    
+    /// Remove the blank characters at both ends of the string
+    public func trim() -> String {
+        return self.trimmingCharacters(in: CharacterSet.whitespaces)
+    }
+    
+    // String's count is equal to String's character.count
+    /// compatibility API for NSString
+    public var length: Int {
+        return self.utf16.count
+    }
+    
+    public func indexOf(_ target: Character) -> Int? {
+        #if swift(>=5.0)
+        return self.firstIndex(of: target)?.utf16Offset(in: self)
+        #else
+        return self.firstIndex(of: target)?.encodedOffset
+        #endif
+    }
+    
+    public func subString(to: Int) -> String {
+        #if swift(>=5.0)
+        let endIndex = String.Index(utf16Offset: to, in: self)
+        #else
+        let endIndex = String.Index.init(encodedOffset: to)
+        #endif
+        let subStr = self[self.startIndex..<endIndex]
+        return String(subStr)
+    }
+    
+    public func subString(from: Int) -> String {
+        #if swift(>=5.0)
+        let startIndex = String.Index(utf16Offset: from, in: self)
+        #else
+        let startIndex = String.Index.init(encodedOffset: from)
+        #endif
+        let subStr = self[startIndex..<self.endIndex]
+        return String(subStr)
+    }
+    
+    public func subString(range: Range<String.Index>) -> String {
+        return String(self[range.lowerBound..<range.upperBound])
+    }
+    
+    public func subString(start: Int, end: Int) -> String {
+        #if swift(>=5.0)
+        let startIndex = String.Index(utf16Offset: start, in: self)
+        let endIndex = String.Index(utf16Offset: end, in: self)
+        #else
+        let startIndex = String.Index.init(encodedOffset: start)
+        let endIndex = String.Index.init(encodedOffset: end)
+        #endif
+        return String(self[startIndex..<endIndex])
+    }
+    
+    public func subString(withNSRange range: NSRange) -> String {
+        
+        return subString(start: range.location, end: range.location + range.length)
+    }
+    
+    /// NSRange 转化为 Range
+    public func range(from nsRange: NSRange) -> Range<String.Index>? {
+        guard
+            let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
+            let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
+            let from = String.Index(from16, within: self),
+            let to = String.Index(to16, within: self)
+            else { return nil }
+        
+        return from ..< to
+    }
+}

+ 286 - 0
Pods/BSText/BSText/Utility/TextAsyncLayer.swift

@@ -0,0 +1,286 @@
+//
+//  TextAsyncLayer.swift
+//  BSText
+//
+//  Created by Bruce on 2018/10/28.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+fileprivate var k_queueCount: Int = 1
+fileprivate var k_counter: Int32 = 0
+fileprivate var k_AsyncLayerGetDisplayQueues: [DispatchQueue] = {
+    
+    var arr = [DispatchQueue]()
+    let maxQueueCount = 16
+    k_queueCount = ProcessInfo.processInfo.activeProcessorCount
+    k_queueCount = k_queueCount < 1 ? 1 : k_queueCount > maxQueueCount ? maxQueueCount : k_queueCount
+    for _ in 0..<k_queueCount {
+        arr.append(DispatchQueue(label: "com.BlueSky.text.render", qos: .userInitiated))
+    }
+    
+    return arr
+}()
+
+/// Global display queue, used for content rendering.
+fileprivate func TextAsyncLayerGetDisplayQueue() -> DispatchQueue {
+    
+    let cur = Int(OSAtomicIncrement32(&k_counter))
+    return k_AsyncLayerGetDisplayQueues[cur % k_queueCount]
+}
+
+/**
+ The TextAsyncLayer's delegate protocol. The delegate of the TextAsyncLayer (typically a UIView)
+ must implements the method in this protocol.
+ */
+@objc public protocol TextAsyncLayerDelegate: NSObjectProtocol {
+    
+    /// This method is called to return a new display task when the layer's contents need update.
+    func newAsyncDisplayTask() -> TextAsyncLayerDisplayTask?
+}
+
+/**
+ The TextAsyncLayer class is a subclass of CALayer used for render contents asynchronously.
+ 
+ @discussion When the layer need update it's contents, it will ask the delegate
+ for a async display task to render the contents in a background queue.
+ */
+public class TextAsyncLayer: CALayer {
+    
+    /// Whether the render code is executed in background. Default is YES.
+    @objc public var displaysAsynchronously = false
+    
+    private var sentinel = TextSentinel()
+    
+    // MARK: - Override
+    override public class func defaultValue(forKey key: String) -> Any? {
+        if (key == "displaysAsynchronously") {
+            return true
+        } else {
+            return super.defaultValue(forKey: key)
+        }
+    }
+    
+    override public init() {
+        
+        displaysAsynchronously = true
+        
+        super.init()
+        
+        self.contentsScale = UIScreen.main.scale
+    }
+    
+    override public init(layer: Any) {
+        
+        displaysAsynchronously = true
+        
+        super.init(layer: layer)
+        
+        self.contentsScale = UIScreen.main.scale
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    deinit {
+        sentinel.increase()
+    }
+    
+    override public func setNeedsDisplay() {
+        _cancelAsyncDisplay()
+        super.setNeedsDisplay()
+    }
+    
+    override public func display() {
+        super.contents = super.contents
+        _displayAsync(displaysAsynchronously)
+    }
+    
+    private func _displayAsync(_ async: Bool) -> Void {
+        
+        weak var tmpdelegate = self.delegate as? TextAsyncLayerDelegate
+        let task: TextAsyncLayerDisplayTask? = tmpdelegate?.newAsyncDisplayTask()
+        if task?.display == nil {
+            if task?.willDisplay != nil {
+                task?.willDisplay!(self)
+            }
+            contents = nil
+            if task?.didDisplay != nil {
+                task?.didDisplay!(self, true)
+            }
+            return
+        }
+        
+        if async {
+            if task?.willDisplay != nil {
+                task?.willDisplay!(self)
+            }
+            let tmpsentinel = self.sentinel
+            let value = tmpsentinel.value
+            let isCancelled: (() -> Bool) = {
+                return value != tmpsentinel.value
+            }
+            let size: CGSize = bounds.size
+            let tmpopaque: Bool = self.isOpaque
+            let scale: CGFloat = contentsScale
+            let tmpbackgroundColor = (tmpopaque && self.backgroundColor != nil) ? self.backgroundColor! : nil
+            if size.width < 1 || size.height < 1 {
+                _ = self.contents
+                self.contents = nil
+                
+                if ((task?.didDisplay) != nil) {
+                    task?.didDisplay!(self, true)
+                }
+                
+                return
+            }
+            
+            TextAsyncLayerGetDisplayQueue().async(execute: {
+                if isCancelled() {
+                    return
+                }
+                UIGraphicsBeginImageContextWithOptions(size, _: tmpopaque, _: scale)
+                let context = UIGraphicsGetCurrentContext()
+                if tmpopaque && context != nil {
+                    context?.saveGState()
+                    do {
+                        if tmpbackgroundColor != nil || tmpbackgroundColor!.alpha < 1 {
+                            context?.setFillColor(UIColor.white.cgColor)
+                            context?.addRect(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
+                            context?.fillPath()
+                        }
+                        if tmpbackgroundColor != nil {
+                            context?.setFillColor(tmpbackgroundColor!)
+                            context?.addRect(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
+                            context?.fillPath()
+                        }
+                    }
+                    context?.restoreGState()
+                }
+                task?.display!(context, size, isCancelled)
+                if isCancelled() {
+                    UIGraphicsEndImageContext()
+                    DispatchQueue.main.async(execute: {
+                        if ((task?.didDisplay) != nil) {
+                            task?.didDisplay!(self, false)
+                        }
+                    })
+                    return
+                }
+                let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
+                UIGraphicsEndImageContext()
+                if isCancelled() {
+                    DispatchQueue.main.async(execute: {
+                        if ((task?.didDisplay) != nil) {
+                            task?.didDisplay!(self, false)
+                        }
+                    })
+                    return
+                }
+                DispatchQueue.main.async(execute: {
+                    if isCancelled() {
+                        if ((task?.didDisplay) != nil) {
+                            task?.didDisplay!(self, false)
+                        }
+                    } else {
+                        self.contents = image?.cgImage
+                        if ((task?.didDisplay) != nil) {
+                            task?.didDisplay!(self, true)
+                        }
+                    }
+                })
+            })
+        } else {
+            sentinel.increase()
+            if task?.willDisplay != nil {
+                task?.willDisplay!(self)
+            }
+            UIGraphicsBeginImageContextWithOptions(bounds.size, _: self.isOpaque, _: contentsScale)
+            let context = UIGraphicsGetCurrentContext()
+            if self.isOpaque && context != nil {
+                var size: CGSize = bounds.size
+                size.width *= contentsScale
+                size.height *= contentsScale
+                context?.saveGState()
+                do {
+                    if self.backgroundColor == nil || self.backgroundColor!.alpha < 1 {
+                        context?.setFillColor(UIColor.white.cgColor)
+                        context?.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height))
+                        context?.fillPath()
+                    }
+                    if self.backgroundColor != nil {
+                        context?.setFillColor(self.backgroundColor!)
+                        context?.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height))
+                        context?.fillPath()
+                    }
+                }
+                context?.restoreGState()
+            }
+            task?.display!(context, bounds.size, {
+                return false
+            })
+            
+            let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
+            UIGraphicsEndImageContext()
+            contents = image?.cgImage
+            if task?.didDisplay != nil {
+                task?.didDisplay!(self, true)
+            }
+        }
+    }
+    
+    private func _cancelAsyncDisplay() {
+        sentinel.increase()
+    }
+}
+
+/**
+ A display task used by TextAsyncLayer to render the contents in background queue.
+ */
+public class TextAsyncLayerDisplayTask: NSObject {
+    
+    /**
+     This block will be called before the asynchronous drawing begins.
+     It will be called on the main thread.
+     
+     block param layer: The layer.
+     */
+    @objc public var willDisplay: ((_ layer: CALayer?) -> Void)?
+    
+    /**
+     This block is called to draw the layer's contents.
+     
+     @discussion This block may be called on main thread or background thread,
+     so is should be thread-safe.
+     
+     block param context:      A new bitmap content created by layer.
+     block param size:         The content size (typically same as layer's bound size).
+     block param isCancelled:  If this block returns `YES`, the method should cancel the
+     drawing process and return as quickly as possible.
+     */
+    @objc public var display: ((_ context: CGContext?, _ size: CGSize, _ isCancelled: @escaping () -> Bool) -> Void)?
+    
+    
+    /**
+     This block will be called after the asynchronous drawing finished.
+     It will be called on the main thread.
+     
+     block param layer:  The layer.
+     block param finished:  If the draw process is cancelled, it's `NO`, otherwise it's `YES`;
+     */
+    @objc public var didDisplay: ((_ layer: CALayer, _ finished: Bool) -> Void)?
+}
+
+/// a thread safe incrementing counter.
+fileprivate struct TextSentinel {
+    /// Returns the current value of the counter.
+    private(set) var value: Int32 = 0
+    
+    /// Increase the value atomically. @return The new value.
+    @discardableResult mutating func increase() -> Int32 {
+        
+        return OSAtomicIncrement32(&value)
+    }
+}

+ 927 - 0
Pods/BSText/BSText/Utility/TextAttribute.swift

@@ -0,0 +1,927 @@
+//
+//  TextAttribute.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/11/1.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+import CoreText
+
+// MARK: - Enum Define
+
+/// The attribute type
+@objc public enum TextAttributeType : Int {
+    case none = 0
+    ///< UIKit attributes, such as UILabel/UITextField/drawInRect.
+    case uiKit = 1      // (1 << 0)
+    ///< CoreText attributes, used by CoreText.
+    case coreText = 2   // (1 << 1)
+    ///< Text attributes, used by BSText.
+    case bsText = 4     // (1 << 2)
+}
+
+/**
+ Line style in Text (similar to NSUnderlineStyle).
+ */
+@objc public enum TextLineStyle: Int {
+    
+    // basic style (bitmask:0xFF)
+    ///< (        ) Do not draw a line (Default).
+    case none = 0x00
+    ///< (──────) Draw a single line.
+    case single = 0x01
+    ///< (━━━━━━━) Draw a thick line.
+    case thick = 0x02
+    ///< (══════) Draw a double line.
+    case double = 0x09
+    
+    // style pattern (bitmask:0xF00)
+    ///< (────────) Draw a solid line (Default).
+//    case patternSolid = 0x000
+    ///< (‑ ‑ ‑ ‑ ‑ ‑) Draw a line of dots.
+    case patternDot = 0x100
+    ///< (— — — —) Draw a line of dashes.
+    case patternDash = 0x200
+    ///< (— ‑ — ‑ — ‑) Draw a line of alternating dashes and dots.
+    case patternDashDot = 0x300
+    ///< (— ‑ ‑ — ‑ ‑) Draw a line of alternating dashes and two dots.
+    case patternDashDotDot = 0x400
+    ///< (••••••••••••) Draw a line of small circle dots.
+    case patternCircleDot = 0x900
+}
+
+/**
+ Text vertical alignment.
+ */
+@objc public enum TextVerticalAlignment : Int {
+    ///< Top alignment.
+    case top = 0
+    ///< Center alignment.
+    case center = 1
+    ///< Bottom alignment.
+    case bottom = 2
+}
+
+/**
+ The direction define in Text.
+ */
+@objc public enum TextDirection : Int {
+    
+    case none = 0
+    case top = 1        // 1 << 0
+    case right = 2      // 1 << 1
+    case bottom = 4     // 1 << 2
+    case left = 8       // 1 << 3
+}
+
+/**
+ The trunction type, tells the truncation engine which type of truncation is being requested.
+ */
+@objc public enum TextTruncationType : Int {
+    /// No truncate.
+    case none = 0
+    /// Truncate at the beginning of the line, leaving the end portion visible.
+    case start = 1
+    /// Truncate at the end of the line, leaving the start portion visible.
+    case end = 2
+    /// Truncate in the middle of the line, leaving both the start and the end portions visible.
+    case middle = 3
+}
+
+// MARK: - Attribute Value Define
+
+/**
+ The tap/long press action callback defined in Text.
+ 
+ @param containerView The text container view (such as Label/TextView).
+ @param text          The whole text.
+ @param range         The text range in `text` (if no range, the range.location is NSNotFound).
+ @param rect          The text frame in `containerView` (if no data, the rect is CGRectNull).
+ */
+//typealias TextAction = (UIView?, NSAttributedString?, NSRange, CGRect) -> Void
+
+
+public class TextAttribute: NSObject {
+    
+    // MARK: - Attribute Name Defined in Text
+    
+    /// The value of this attribute is a `TextBackedString` object.
+    /// Use this attribute to store the original plain text if it is replaced by something else (such as attachment).
+    @objc public static let textBackedStringAttributeName = "TextBackedString"
+    
+    /// The value of this attribute is a `TextBinding` object.
+    /// Use this attribute to bind a range of text together, as if it was a single charactor.
+    @objc public static let textBindingAttributeName = "TextBinding"
+    
+    /// The value of this attribute is a `TextShadow` object.
+    /// Use this attribute to add shadow to a range of text.
+    /// Shadow will be drawn below text glyphs. Use TextShadow.subShadow to add multi-shadow.
+    @objc public static let textShadowAttributeName = "TextShadow"
+    
+    /// The value of this attribute is a `TextShadow` object.
+    /// Use this attribute to add inner shadow to a range of text.
+    /// Inner shadow will be drawn above text glyphs. Use TextShadow.subShadow to add multi-shadow.
+    @objc public static let textInnerShadowAttributeName = "TextInnerShadow"
+    
+    /// The value of this attribute is a `TextDecoration` object.
+    /// Use this attribute to add underline to a range of text.
+    /// The underline will be drawn below text glyphs.
+    @objc public static let textUnderlineAttributeName = "TextUnderline"
+    
+    /// The value of this attribute is a `TextDecoration` object.
+    /// Use this attribute to add strikethrough (de@objc public static lete line) to a range of text.
+    /// The strikethrough will be drawn above text glyphs.
+    @objc public static let textStrikethroughAttributeName = "TextStrikethrough"
+    
+    /// The value of this attribute is a `TextBorder` object.
+    /// Use this attribute to add cover border or cover color to a range of text.
+    /// The border will be drawn above the text glyphs.
+    @objc public static let textBorderAttributeName = "TextBorder"
+    
+    /// The value of this attribute is a `TextBorder` object.
+    /// Use this attribute to add background border or background color to a range of text.
+    /// The border will be drawn below the text glyphs.
+    @objc public static let textBackgroundBorderAttributeName = "TextBackgroundBorder"
+    
+    /// The value of this attribute is a `TextBorder` object.
+    /// Use this attribute to add a code block border to one or more line of text.
+    /// The border will be drawn below the text glyphs.
+    @objc public static let textBlockBorderAttributeName = "TextBlockBorder"
+    
+    /// The value of this attribute is a `TextAttachment` object.
+    /// Use this attribute to add attachment to text.
+    /// It should be used in conjunction with a CTRunDelegate.
+    @objc public static let textAttachmentAttributeName = "TextAttachment"
+    
+    /// The value of this attribute is a `TextHighlight` object.
+    /// Use this attribute to add a touchable highlight state to a range of text.
+    @objc public static let textHighlightAttributeName = "TextHighlight"
+    
+    /// The value of this attribute is a `NSValue` object stores CGAffineTransform.
+    /// Use this attribute to add transform to each glyph in a range of text.
+    @objc public static let textGlyphTransformAttributeName = "TextGlyphTransform"
+    
+    // MARK: - String Token Define
+    
+    ///< Object replacement character (U+FFFC), used for text attachment.
+    @objc public static let textAttachmentToken = "\u{FFFC}"
+    
+    ///< Horizontal ellipsis (U+2026), used for text truncation  "…".
+    @objc public static let textTruncationToken = "\u{2026}"
+    
+    static var kTextAttributeTypeDic: NSDictionary?
+    
+    @objc public static func textAttributeGetType(name: String) -> TextAttributeType {
+        
+        if let d = kTextAttributeTypeDic {
+            return d.object(forKey: name) as? TextAttributeType ?? TextAttributeType.none
+        }
+        
+        let dic = NSMutableDictionary()
+        let All = TextAttributeType.uiKit.rawValue | TextAttributeType.coreText.rawValue | TextAttributeType.bsText.rawValue
+        let CoreText_BSText = TextAttributeType.coreText.rawValue | TextAttributeType.bsText.rawValue
+        let UIKit_BSText = TextAttributeType.uiKit.rawValue | TextAttributeType.bsText.rawValue
+        let UIKit_CoreText = TextAttributeType.uiKit.rawValue | TextAttributeType.coreText.rawValue
+        let UIKit = TextAttributeType.uiKit.rawValue
+        let CoreText = TextAttributeType.coreText.rawValue
+        let BSText = TextAttributeType.bsText.rawValue
+        
+        dic[NSAttributedString.Key.font] = All
+        dic[NSAttributedString.Key.kern] = All
+        dic[NSAttributedString.Key.foregroundColor] = UIKit
+        dic[kCTForegroundColorAttributeName] = CoreText
+        dic[kCTForegroundColorFromContextAttributeName] = CoreText
+        dic[NSAttributedString.Key.backgroundColor] = UIKit
+        dic[NSAttributedString.Key.strokeWidth] = All
+        dic[NSAttributedString.Key.strokeColor] = UIKit
+        dic[kCTStrokeColorAttributeName] = CoreText_BSText
+        dic[NSAttributedString.Key.shadow] = UIKit_BSText
+        dic[NSAttributedString.Key.strikethroughStyle] = UIKit
+        dic[NSAttributedString.Key.underlineStyle] = UIKit_CoreText
+        dic[kCTUnderlineColorAttributeName] = CoreText
+        dic[NSAttributedString.Key.ligature] = All
+        dic[kCTSuperscriptAttributeName] = UIKit //it's a CoreText attrubite, but only supported by UIKit...
+        dic[NSAttributedString.Key.verticalGlyphForm] = All
+        dic[kCTGlyphInfoAttributeName] = CoreText_BSText
+        dic[kCTCharacterShapeAttributeName] = CoreText_BSText
+        dic[kCTRunDelegateAttributeName] = CoreText_BSText
+        dic[kCTBaselineClassAttributeName] = CoreText_BSText
+        dic[kCTBaselineInfoAttributeName] = CoreText_BSText
+        dic[kCTBaselineReferenceInfoAttributeName] = CoreText_BSText
+        dic[kCTWritingDirectionAttributeName] = CoreText_BSText
+        dic[NSAttributedString.Key.paragraphStyle] = All
+        
+        dic[NSAttributedString.Key.strikethroughColor] = UIKit
+        dic[NSAttributedString.Key.underlineColor] = UIKit
+        dic[NSAttributedString.Key.textEffect] = UIKit
+        dic[NSAttributedString.Key.obliqueness] = UIKit
+        dic[NSAttributedString.Key.expansion] = UIKit
+        dic[kCTLanguageAttributeName] = CoreText_BSText
+        dic[NSAttributedString.Key.baselineOffset] = UIKit
+        dic[NSAttributedString.Key.writingDirection] = All
+        dic[NSAttributedString.Key.attachment] = UIKit
+        dic[NSAttributedString.Key.link] = UIKit
+        dic[kCTRubyAnnotationAttributeName] = CoreText
+        
+        dic[TextAttribute.textBackedStringAttributeName] = BSText
+        dic[TextAttribute.textBindingAttributeName] = BSText
+        dic[TextAttribute.textShadowAttributeName] = BSText
+        dic[TextAttribute.textInnerShadowAttributeName] = BSText
+        dic[TextAttribute.textUnderlineAttributeName] = BSText
+        dic[TextAttribute.textStrikethroughAttributeName] = BSText
+        dic[TextAttribute.textBorderAttributeName] = BSText
+        dic[TextAttribute.textBackgroundBorderAttributeName] = BSText
+        dic[TextAttribute.textBlockBorderAttributeName] = BSText
+        dic[TextAttribute.textAttachmentAttributeName] = BSText
+        dic[TextAttribute.textHighlightAttributeName] = BSText
+        dic[TextAttribute.textGlyphTransformAttributeName] = BSText
+        
+        kTextAttributeTypeDic = (dic.copy() as! NSDictionary)
+        
+        return kTextAttributeTypeDic!.object(forKey: name) as? TextAttributeType ?? TextAttributeType.none
+    }
+}
+
+
+/**
+ TextBackedString objects are used by the NSAttributedString class cluster
+ as the values for text backed string attributes (stored in the attributed
+ string under the key named TextBackedStringAttributeName).
+ 
+ It may used for copy/paste plain text from attributed string.
+ Example: If :) is replace by a custom emoji (such as😊), the backed string can be set to @":)".
+ */
+public class TextBackedString: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    @objc public var string: String?
+    
+    public override init() {
+        super.init()
+    }
+    
+    ///< backed string
+    @objc public class func stringWithString(_ string: String) -> TextBackedString {
+        let one = TextBackedString()
+        one.string = string
+        return one
+    }
+    
+    // MARK: - NSCoding
+    public func encode(with aCoder: NSCoder) {
+        aCoder.encode(string, forKey: "string")
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init()
+        string = aDecoder.decodeObject(forKey: "string") as? String
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextBackedString()
+        one.string = string
+        return one
+    }
+    
+    // MARK: - NSSecureCoding
+    public static var supportsSecureCoding: Bool {
+        return true
+    }
+}
+
+
+/**
+ TextBinding objects are used by the NSAttributedString class cluster
+ as the values for shadow attributes (stored in the attributed string under
+ the key named TextBindingAttributeName).
+ 
+ Add this to a range of text will make the specified characters 'binding together'.
+ TextView will treat the range of text as a single character during text
+ selection and edit.
+ */
+public class TextBinding: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    @objc public var deleteConfirm = false
+    
+    public override init() {
+        super.init()
+    }
+    
+    ///< confirm the range when delete in TextView
+    @objc(bindingWithDeleteConfirm:)
+    public class func binding(with deleteConfirm: Bool) -> TextBinding {
+        
+        let one = TextBinding()
+        one.deleteConfirm = deleteConfirm
+        
+        return one
+    }
+    
+    // MARK: - NSCoding
+    public func encode(with aCoder: NSCoder) {
+        aCoder.encode(deleteConfirm, forKey: "deleteConfirm")
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init()
+        deleteConfirm = aDecoder.decodeBool(forKey: "deleteConfirm")
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextBinding()
+        one.deleteConfirm = deleteConfirm
+        return one
+    }
+    
+    // MARK: - NSSecureCoding
+    public static var supportsSecureCoding: Bool {
+        return true
+    }
+}
+
+/**
+ TextShadow objects are used by the NSAttributedString class cluster
+ as the values for shadow attributes (stored in the attributed string under
+ the key named TextShadowAttributeName or TextInnerShadowAttributeName).
+ 
+ It's similar to `NSShadow`, but offers more options.
+ */
+public class TextShadow: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    /*/< shadow color */
+    @objc public var color: UIColor?
+    
+    /*/< shadow offset */
+    @objc public var offset = CGSize.zero
+    
+    /*/< shadow blur radius */
+    @objc public var radius: CGFloat = 0
+    
+    /*/< shadow blend mode */
+    @objc public var blendMode = CGBlendMode.normal
+    
+    ///< a sub shadow which will be added above the parent shadow
+    @objc public var subShadow: TextShadow?
+    
+    public override init() {
+        super.init()
+    }
+    
+    @objc public class func shadowWithColor(_ color: UIColor?, offset: CGSize, radius: CGFloat) -> TextShadow {
+        
+        let one = TextShadow()
+        
+        one.color = color
+        one.offset = offset
+        one.radius = radius
+        
+        return one
+    }
+    
+    ///< convert NSShadow to TextShadow
+    @objc(shadowWithNSShadow:)
+    public class func shadow(with nsShadow: NSShadow?) -> TextShadow? {
+        
+        guard let _ = nsShadow else {
+            return nil
+        }
+        
+        let shadow = TextShadow()
+        shadow.offset = nsShadow!.shadowOffset
+        shadow.radius = nsShadow!.shadowBlurRadius
+        let color = nsShadow!.shadowColor
+        
+        if color != nil {
+            var c: UIColor?
+            if CGColor.typeID == CFGetTypeID(color! as CFTypeRef) {
+                c = UIColor(cgColor: color! as! CGColor)
+            }
+            if (color is UIColor) {
+                shadow.color = c!
+            }
+        }
+        
+        return shadow
+    }
+    
+    ///< convert TextShadow to NSShadow
+    @objc public func nsShadow() -> NSShadow? {
+        let shadow = NSShadow()
+        shadow.shadowOffset = offset
+        shadow.shadowBlurRadius = radius
+        shadow.shadowColor = color
+        return shadow
+    }
+    
+    // MARK: - NSCoding
+    public func encode(with aCoder: NSCoder) {
+        aCoder.encode(color, forKey: "color")
+        aCoder.encode(Float(radius), forKey: "radius")
+        aCoder.encode(offset, forKey: "offset")
+        aCoder.encode(subShadow, forKey: "subShadow")
+    }
+    
+    required public init?(coder aDecoder: NSCoder) {
+        super.init()
+        
+        color = aDecoder.decodeObject(forKey: "color") as? UIColor
+        radius = CGFloat(aDecoder.decodeFloat(forKey: "radius"))
+        offset = aDecoder.decodeCGSize(forKey: "offset")
+        subShadow = aDecoder.decodeObject(forKey: "subShadow") as? TextShadow
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        
+        let one = TextShadow()
+        
+        one.color = color
+        one.radius = radius
+        one.offset = offset
+        one.subShadow = subShadow?.copy() as? TextShadow
+        
+        return one
+    }
+    
+    // MARK: - NSSecureCoding
+    public static var supportsSecureCoding: Bool {
+        return true
+    }
+}
+
+/**
+ TextDecorationLine objects are used by the NSAttributedString class cluster
+ as the values for decoration line attributes (stored in the attributed string under
+ the key named TextUnderlineAttributeName or TextStrikethroughAttributeName).
+ 
+ When it's used as underline, the line is drawn below text glyphs;
+ when it's used as strikethrough, the line is drawn above text glyphs.
+ */
+public class TextDecoration: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    /*/< line style */
+    @objc public var style = TextLineStyle.none
+    
+    /*/< line width (nil means automatic width) */
+    @objc public var width: NSNumber?
+    
+    /*/< line color (nil means automatic color) */
+    @objc public var color: UIColor?
+    
+    ///< line shadow
+    @objc public var shadow: TextShadow?
+    
+    
+    @objc(decorationWithStyle:)
+    public class func decoration(with style: TextLineStyle) -> TextDecoration {
+        
+        let one = TextDecoration()
+        one.style = style
+        
+        return one
+    }
+    
+    @objc(decorationWithStyle:width:color:)
+    public class func decoration(with style: TextLineStyle, width: NSNumber?, color: UIColor?) -> TextDecoration {
+        
+        let one = TextDecoration()
+        
+        one.style = style
+        one.width = width
+        one.color = color
+        
+        return one
+    }
+    
+    public override init() {
+        self.style = TextLineStyle.single
+        
+        super.init()
+    }
+    
+    // MARK: - NSCoding
+    public func encode(with aCoder: NSCoder) {
+        aCoder.encode(style.rawValue, forKey: "style")
+        aCoder.encode(width, forKey: "width")
+        aCoder.encode(color, forKey: "color")
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init()
+        
+        style = TextLineStyle(rawValue: aDecoder.decodeInteger(forKey: "style"))!
+        width = aDecoder.decodeObject(forKey: "width") as? NSNumber
+        color = aDecoder.decodeObject(forKey: "color") as? UIColor
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        
+        let one = TextDecoration()
+        one.style = style
+        one.width = width
+        one.color = color
+        return one
+    }
+    
+    // MARK: - NSSecureCoding
+    public static var supportsSecureCoding: Bool {
+        return true
+    }
+}
+
+
+/**
+ TextBorder objects are used by the NSAttributedString class cluster
+ as the values for border attributes (stored in the attributed string under
+ the key named TextBorderAttributeName or TextBackgroundBorderAttributeName).
+ 
+ It can be used to draw a border around a range of text, or draw a background
+ to a range of text.
+ 
+ Example:
+ ╭──────╮
+ │ Text │
+ ╰──────╯
+ */
+public class TextBorder: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    /*/< border line style */
+    @objc public var lineStyle = TextLineStyle.single
+    
+    /*/< border line width */
+    @objc public var strokeWidth: CGFloat = 0
+    
+    /*/< border line color */
+    @objc public var strokeColor: UIColor?
+    
+    /*/< border line join : CGLineJoin */
+    @objc public var lineJoin = CGLineJoin.miter
+    
+    /*/< border insets for text bounds */
+    @objc public var insets = UIEdgeInsets.zero
+    
+    /*/< border corder radius */
+    @objc public var cornerRadius: CGFloat = 0
+    
+    /*/< border shadow */
+    @objc public var shadow: TextShadow?
+    
+    ///< inner fill color
+    @objc public var fillColor: UIColor?
+    
+    
+    @objc(borderWithLineStyle:lineWidth:strokeColor:)
+    public class func border(with lineStyle: TextLineStyle, lineWidth: CGFloat, strokeColor: UIColor?) -> TextBorder {
+        let one = TextBorder()
+        one.lineStyle = lineStyle
+        one.strokeWidth = lineWidth
+        one.strokeColor = strokeColor
+        return one
+    }
+    
+    @objc(borderWithFillColor:cornerRadius:)
+    public class func border(with fillColor: UIColor?, cornerRadius: CGFloat) -> TextBorder {
+        let one = TextBorder()
+        one.fillColor = fillColor
+        one.cornerRadius = cornerRadius
+        one.insets = UIEdgeInsets(top: -2, left: 0, bottom: 0, right: -2)
+        return one
+    }
+    
+    override public init() {
+        super.init()
+    }
+    
+    // MARK: - NSSecureCoding
+    public static var supportsSecureCoding: Bool {
+        return true
+    }
+    
+    // MARK: - NSCoding
+    public func encode(with aCoder: NSCoder) {
+        aCoder.encode(lineStyle.rawValue, forKey: "lineStyle")
+        aCoder.encode(Float(strokeWidth), forKey: "strokeWidth")
+        aCoder.encode(strokeColor, forKey: "strokeColor")
+        aCoder.encode(lineJoin.rawValue, forKey: "lineJoin")
+        aCoder.encode(insets, forKey: "insets")
+        aCoder.encode(Float(cornerRadius), forKey: "cornerRadius")
+        aCoder.encode(shadow, forKey: "shadow")
+        aCoder.encode(fillColor, forKey: "fillColor")
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init()
+        lineStyle = TextLineStyle(rawValue: aDecoder.decodeInteger(forKey: "lineStyle"))!
+        strokeWidth = CGFloat(aDecoder.decodeFloat(forKey: "strokeWidth"))
+        strokeColor = aDecoder.decodeObject(forKey: "strokeColor") as! UIColor?
+        lineJoin = CGLineJoin(rawValue: aDecoder.decodeInt32(forKey: "lineJoin"))!  // join
+        insets = aDecoder.decodeUIEdgeInsets(forKey: "insets")
+        cornerRadius = CGFloat(aDecoder.decodeFloat(forKey: "cornerRadius"))
+        shadow = aDecoder.decodeObject(forKey: "shadow") as! TextShadow?
+        fillColor = aDecoder.decodeObject(forKey: "fillColor") as! UIColor?
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextBorder()
+        one.lineStyle = lineStyle
+        one.strokeWidth = strokeWidth
+        one.strokeColor = strokeColor
+        one.lineJoin = lineJoin
+        one.insets = insets
+        one.cornerRadius = cornerRadius
+        one.shadow = shadow?.copy() as? TextShadow
+        one.fillColor = fillColor
+        return one
+    }
+}
+
+/**
+ TextAttachment objects are used by the NSAttributedString class cluster
+ as the values for attachment attributes (stored in the attributed string under
+ the key named TextAttachmentAttributeName).
+ 
+ When display an attributed string which contains `TextAttachment` object,
+ the content will be placed in text metric. If the content is `UIImage`,
+ then it will be drawn to CGContext; if the content is `UIView` or `CALayer`,
+ then it will be added to the text container's view or layer.
+ */
+public class TextAttachment: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    /*/< Supported type: UIImage, UIView, CALayer */
+    @objc public var content: Any?
+    
+    /*/< Content display mode. */
+    @objc public var contentMode = UIView.ContentMode.scaleToFill
+    
+    /*/< The insets when drawing content. */
+    @objc public var contentInsets = UIEdgeInsets()
+    
+    ///< The user information dictionary.
+    @objc public var userInfo: NSDictionary?
+    
+    public override init() {
+        super.init()
+    }
+    
+    @objc public class func attachmentWithContent(content: Any?) -> TextAttachment {
+        let one = TextAttachment()
+        one.content = content
+        return one
+    }
+    
+    // MARK: - NSCoding
+    public func encode(with aCoder: NSCoder) {
+        aCoder.encode(content, forKey: "content")
+        aCoder.encode(contentInsets, forKey: "contentInsets")
+        aCoder.encode(userInfo, forKey: "userInfo")
+    }
+    
+    required public init?(coder aDecoder: NSCoder) {
+        super.init()
+        content = aDecoder.decodeObject(forKey: "content")
+        contentInsets = aDecoder.decodeUIEdgeInsets(forKey: "contentInsets")
+        userInfo = aDecoder.decodeObject(forKey: "userInfo") as? NSDictionary
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        
+        let one = TextAttachment()
+        if let c = (content as? NSObject), c.responds(to: #selector(NSObject.copy)) {
+            one.content = c.copy()
+        } else {
+            one.content = content
+        }
+        one.contentInsets = contentInsets
+        one.userInfo = userInfo?.copy() as? NSDictionary
+        return one
+    }
+    
+    // MARK: - NSSecureCoding
+    @objc public static var supportsSecureCoding: Bool {
+        return true
+    }
+}
+
+/**
+ TextHighlight objects are used by the NSAttributedString class cluster
+ as the values for touchable highlight attributes (stored in the attributed string
+ under the key named TextHighlightAttributeName).
+ 
+ When display an attributed string in `Label` or `TextView`, the range of
+ highlight text can be toucheds down by users. If a range of text is turned into
+ highlighted state, the `attributes` in `TextHighlight` will be used to modify
+ (set or remove) the original attributes in the range for display.
+ */
+public class TextHighlight: NSObject, NSCoding, NSCopying, NSSecureCoding {
+    
+    /**
+     Attributes that you can apply to text in an attributed string when highlight.
+     Key:   Same as CoreText/Text Attribute Name.
+     Value: Modify attribute value when highlight (nil for remove attribute).
+     */
+    @objc public private(set) var attributes = [NSAttributedString.Key : Any]()
+    
+    /**
+     The user information dictionary, default is nil.
+     */
+    @objc public var userInfo: NSDictionary?
+    
+    /**
+     Tap action when user tap the highlight, default is nil.
+     If the value is nil, TextView or Label will ask it's delegate to handle the tap action.
+     */
+    @objc public var tapAction: (TextAction)?
+    
+    /**
+     Long press action when user long press the highlight, default is nil.
+     If the value is nil, TextView or Label will ask it's delegate to handle the long press action.
+     */
+    @objc public var longPressAction: (TextAction)?
+    
+    /**
+     Creates a highlight object with specified attributes.
+     
+     @param attributes The attributes which will replace original attributes when highlight,
+     If the value is NSNull, it will removed when highlight.
+     */
+    @objc(highlightWithAttributes:)
+    public class func highlight(with attributes: [NSAttributedString.Key : Any]?) -> TextHighlight {
+        let one = TextHighlight()
+        if let attr = attributes {
+            one.attributes = attr
+        }
+        return one
+    }
+    
+    /**
+     Convenience methods to create a default highlight with the specifeid background color.
+     
+     @param backgroundColor The background border color.
+     */
+    @objc(highlightWithBackgroundColor:)
+    public class func highlight(with backgroundColor: UIColor?) -> TextHighlight {
+        let highlightBorder = TextBorder()
+        highlightBorder.insets = UIEdgeInsets(top: -2, left: -1, bottom: -2, right: -1)
+        highlightBorder.cornerRadius = 3
+        highlightBorder.fillColor = backgroundColor
+        let one = TextHighlight()
+        one.backgroundBorder = highlightBorder
+        return one
+    }
+    
+    override public init() {
+        super.init()
+    }
+    
+    public convenience init(attributes: [NSAttributedString.Key : Any]?) {
+        self.init()
+        if let attr = attributes {
+            self.attributes = attr
+        }
+    }
+    
+    // MARK: - NSCoding
+    public func encode(with aCoder: NSCoder) {
+        let data = TextArchiver.archivedData(withRootObject: attributes)
+        aCoder.encode(data, forKey: "attributes")
+    }
+    
+    required public init?(coder aDecoder: NSCoder) {
+        super.init()
+        
+        let data = aDecoder.decodeObject(forKey: "attributes") as? Data
+        if let attr = (TextUnarchiver.unarchiveObject(with: data!) as? [NSAttributedString.Key : Any]) {
+            attributes = attr
+        }
+    }
+    
+    // MARK: - NSCopying
+    public func copy(with zone: NSZone? = nil) -> Any {
+        let one = TextHighlight()
+        one.attributes = self.attributes
+        return one
+    }
+    
+    // MARK: - NSSecureCoding
+    @objc public static var supportsSecureCoding: Bool {
+        return true
+    }
+    
+    // MARK: - Convenience methods below to set the `attributes`.
+    
+    @objc public func setTextAttribute(_ attribute: String, value: Any?) {
+        attributes[NSAttributedString.Key(rawValue: attribute)] = value
+    }
+    
+    @objc public var font: UIFont? {
+        set(font) {
+            if let f = font {
+                let ctFont = CTFontCreateWithName(f.fontName as CFString, f.pointSize, nil)
+                attributes[NSAttributedString.Key(rawValue: kCTFontAttributeName as String)] = ctFont
+            } else {
+                attributes[NSAttributedString.Key(rawValue: kCTFontAttributeName as String)] = nil
+            }
+        }
+        get {
+            return attributes[NSAttributedString.Key(rawValue: kCTFontAttributeName as String)] as? UIFont
+        }
+    }
+    
+    @objc public var color: UIColor? {
+        set(color) {
+            attributes[NSAttributedString.Key(rawValue: kCTForegroundColorAttributeName as String)] = color?.cgColor
+            attributes[NSAttributedString.Key.foregroundColor] = color
+        }
+        get {
+            return attributes[NSAttributedString.Key.foregroundColor] as? UIColor
+        }
+    }
+    
+    @objc public var strokeWidth: NSNumber? {
+        set(width) {
+            attributes[NSAttributedString.Key(rawValue: kCTStrokeWidthAttributeName as String)] = width
+        }
+        get {
+            return attributes[NSAttributedString.Key(rawValue: kCTStrokeWidthAttributeName as String)] as? NSNumber
+        }
+    }
+    
+    @objc public var strokeColor: UIColor? {
+        set(color) {
+            attributes[NSAttributedString.Key(rawValue: kCTStrokeColorAttributeName as String)] = color?.cgColor
+            attributes[NSAttributedString.Key.strokeColor] = color
+        }
+        get {
+            return attributes[NSAttributedString.Key.strokeColor] as? UIColor
+        }
+    }
+    
+    @objc public var shadow: TextShadow? {
+        set(shadow) {
+            setTextAttribute(TextAttribute.textShadowAttributeName, value: shadow)
+        }
+        get {
+            fatalError("Here have not getter")
+        }
+    }
+    
+    @objc public var innerShadow: TextShadow? {
+        set(shadow) {
+            setTextAttribute(TextAttribute.textInnerShadowAttributeName, value: shadow)
+        }
+        get {
+            fatalError("Here have not getter")
+        }
+    }
+    
+    @objc public var underline: TextDecoration? {
+        set(underline) {
+            setTextAttribute(TextAttribute.textUnderlineAttributeName, value: underline)
+        }
+        get {
+            fatalError("Here have not getter")
+        }
+    }
+    
+    @objc public var strikethrough: TextDecoration? {
+        set(strikethrough) {
+            setTextAttribute(TextAttribute.textStrikethroughAttributeName, value: strikethrough)
+        }
+        get {
+            fatalError("Here have not getter")
+        }
+    }
+    
+    @objc public var backgroundBorder: TextBorder? {
+        set(border) {
+            setTextAttribute(TextAttribute.textBackgroundBorderAttributeName, value: border)
+        }
+        get {
+            fatalError("Here have not getter")
+        }
+    }
+    
+    @objc public var border: TextBorder? {
+        set(border) {
+            setTextAttribute(TextAttribute.textBorderAttributeName, value: border)
+        }
+        get {
+            fatalError("Here have not getter")
+        }
+    }
+    
+    @objc public var attachment: TextAttachment? {
+        set(attachment) {
+            setTextAttribute(TextAttribute.textAttachmentAttributeName, value: attachment)
+        }
+        get {
+            fatalError("Here have not getter")
+        }
+    }
+}

+ 81 - 0
Pods/BSText/BSText/Utility/TextTransaction.swift

@@ -0,0 +1,81 @@
+//
+//  TextTransaction.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/10/22.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+import CoreFoundation
+
+fileprivate var transactionSet = Set<TextTransaction>()
+
+private let kRunLoopObserverCallBack: CFRunLoopObserverCallBack = { _, _, _ in
+    if transactionSet.count == 0 {
+        return
+    }
+    let currentSet = transactionSet
+    transactionSet = Set<TextTransaction>()
+    
+    for transaction in currentSet {
+        let _ = transaction.target.perform(transaction.selector)
+    }
+}
+
+private let kTextTransactionSetup: Int = {
+    
+    let runloop = CFRunLoopGetMain()
+    
+    let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue, true,  0, kRunLoopObserverCallBack, nil)
+    CFRunLoopAddObserver(runloop, observer, .commonModes)
+    
+    return 0
+}()
+
+public class TextTransaction: NSObject {
+    
+    fileprivate var target: AnyObject
+    fileprivate var selector: Selector
+    
+    @objc(transactionWithTarget:selector:)
+    public class func transaction(with target: AnyObject, selector: Selector) -> TextTransaction {
+        
+        let t = TextTransaction(target: target, selector: selector)
+        return t
+    }
+    
+    public init(target: AnyObject, selector: Selector) {
+        
+        self.target = target
+        self.selector = selector
+        
+        super.init()
+    }
+    
+    @objc public func commit() {
+        
+        let _ = kTextTransactionSetup
+        transactionSet.insert(self)
+    }
+    
+    override public var hash: Int {
+        get {
+            let v1 = selector.hashValue
+            let v2 = target.hash!
+            return v1 ^ v2
+        }
+    }
+    
+    override public func isEqual(_ object: Any?) -> Bool {
+        
+        guard let other = (object as? TextTransaction) else {
+            return false
+        }
+        if self === other {
+            return true
+        }
+
+        return other.selector == selector && other.target === self.target
+    }
+}

+ 873 - 0
Pods/BSText/BSText/Utility/TextUtilities.swift

@@ -0,0 +1,873 @@
+//
+//  TextUtilities.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/10/23.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+import Accelerate
+
+public class TextUtilities: NSObject {
+    
+    // MARK: - getter
+    @objc public static let isAppExtension: Bool = {
+        
+        let cls: AnyClass? = NSClassFromString("UIApplication")
+        if cls == nil || !(cls?.responds(to: #selector(getter: UIApplication.shared)) ?? false) {
+            return true
+        }
+        if Bundle.main.bundlePath.hasSuffix(".appex") {
+            return true
+        }
+        
+        return false
+    }()
+    
+    @objc public static var sharedApplication: UIApplication? {
+        
+        return TextUtilities.isAppExtension ? nil : UIApplication.shared
+    }
+    
+    public static func numberSwap<T>(_ a: inout T, b: inout T) {
+        (a, b) = (b, a)
+    }
+    
+    @objc public static func textClamp(x: CGFloat, low: CGFloat, high: CGFloat) -> CGFloat {
+        return (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
+    }
+    
+    /**
+     Whether the character is 'line break char':
+     U+000D (\\r or CR)
+     U+2028 (Unicode line separator)
+     U+000A (\\n or LF)
+     U+2029 (Unicode paragraph separator)
+     
+     @param c  A character
+     @return YES or NO.
+     */
+    @objc @inline(__always) public static func textIsLinebreakChar(_ c: unichar) -> Bool {
+        switch c {
+        case unichar(0x000D), unichar(0x2028), unichar(0x000A), unichar(0x2029):
+            return true
+        default:
+            return false
+        }
+    }
+    
+    /**
+     Whether the string is a 'line break':
+     U+000D (\\r or CR)
+     U+2028 (Unicode line separator)
+     U+000A (\\n or LF)
+     U+2029 (Unicode paragraph separator)
+     \\r\\n, in that order (also known as CRLF)
+     
+     @param str A string
+     @return YES or NO.
+     */
+    @objc @inline(__always) public static func textIsLinebreakString(_ str: String?) -> Bool {
+        
+        guard let s = str as NSString?, s.length > 0, s.length <= 2 else {
+            return false
+        }
+        
+        if s.length == 1 {
+            let c = unichar(s.character(at: 0))
+            return TextUtilities.textIsLinebreakChar((c))
+        } else {
+            return (s.substring(to: 1) == "\r") && (s.substring(from: 1) == "\n")
+        }
+    }
+    
+    /**
+     If the string has a 'line break' suffix, return the 'line break' length.
+     
+     @param str  A string.
+     @return The length of the tail line break: 0, 1 or 2.
+     */
+    @objc @inline(__always) public static func textLinebreakTailLength(_ str: String?) -> Int {
+        
+        guard let s = str as NSString?, s.length > 0 else {
+            return 0
+        }
+        if s.length == 1 {
+            return TextUtilities.textIsLinebreakChar(s.character(at: 0)) ? 1 : 0
+        } else {
+            let c2 = s.character(at: s.length - 1)
+            if TextUtilities.textIsLinebreakChar((c2)) {
+                let c1 = s.character(at: s.length - 2)
+                if String(c1) == "\r" && String(c2) == "\n" {
+                    return 2
+                } else {
+                    return 1
+                }
+            } else {
+                return 0
+            }
+        }
+    }
+    
+    /**
+     Convert `UIDataDetectorTypes` to `NSTextCheckingType`.
+     
+     @param types  The `UIDataDetectorTypes` type.
+     @return The `NSTextCheckingType` type.
+     */
+    @objc(textCheckingTypeFromUIDataDetectorType:)
+    @inline(__always) public static func textCheckingType(from types: UIDataDetectorTypes) -> NSTextCheckingResult.CheckingType {
+        
+        var t = NSTextCheckingResult.CheckingType(rawValue: 0)
+        if types.rawValue & UIDataDetectorTypes.phoneNumber.rawValue != 0 {
+            t.insert(.phoneNumber)
+        }
+        if types.rawValue & UIDataDetectorTypes.link.rawValue != 0 {
+            t.insert(.link)
+        }
+        if types.rawValue & UIDataDetectorTypes.address.rawValue != 0 {
+            t.insert(.address)
+        }
+        if types.rawValue & UIDataDetectorTypes.calendarEvent.rawValue != 0 {
+            t.insert(.date)
+        }
+        return t
+    }
+    
+    /**
+     Whether the font is `AppleColorEmoji` font.
+     
+     @param font  A font.
+     @return YES: the font is Emoji, NO: the font is not Emoji.
+     */
+    @objc @inline(__always) public static func textUIFontIsEmoji(_ font: UIFont?) -> Bool {
+        
+        return font?.fontName == "AppleColorEmoji"
+    }
+    
+    /**
+     Whether the font is `AppleColorEmoji` font.
+     
+     @param font  A font.
+     @return YES: the font is Emoji, NO: the font is not Emoji.
+     */
+    @objc @inline(__always) public static func textCTFontIsEmoji(_ font: CTFont?) -> Bool {
+        
+        guard let _ = font else {
+            return false
+        }
+        
+        let name = CTFontCopyPostScriptName(font!)
+        if CFEqual("AppleColorEmoji" as CFTypeRef, name) {
+            return true
+        }
+        
+        return false
+    }
+    
+    /**
+     Whether the font is `AppleColorEmoji` font.
+     
+     @param font  A font.
+     @return YES: the font is Emoji, NO: the font is not Emoji.
+     */
+    @objc @inline(__always) public static func textCGFontIsEmoji(_ font: CGFont?) -> Bool {
+        
+        let name = font?.postScriptName
+        if let n = name, CFEqual("AppleColorEmoji" as CFTypeRef, n) {
+            return true
+        }
+        
+        return false
+    }
+    
+    /**
+     Whether the font contains color bitmap glyphs.
+     
+     @discussion Only `AppleColorEmoji` contains color bitmap glyphs in iOS system fonts.
+     @param font  A font.
+     @return YES: the font contains color bitmap glyphs, NO: the font has no color bitmap glyph.
+     */
+    @objc @inline(__always) public static func textCTFontContainsColorBitmapGlyphs(_ font: CTFont?) -> Bool {
+        
+        guard let f = font else {
+            return false
+        }
+        return (CTFontGetSymbolicTraits(f).rawValue & CTFontSymbolicTraits.traitColorGlyphs.rawValue) != 0
+    }
+    
+    /**
+     Get the character set which should rotate in vertical form.
+     @return The shared character set.
+     */
+    @objc public static let textVerticalFormRotateCharacterSet: NSMutableCharacterSet = {
+        
+        let tmpSet = NSMutableCharacterSet()
+        tmpSet.addCharacters(in: NSRange(location: 0x1100, length: 256)) // Hangul Jamo
+        tmpSet.addCharacters(in: NSRange(location: 0x2460, length: 160)) // Enclosed Alphanumerics
+        tmpSet.addCharacters(in: NSRange(location: 0x2600, length: 256)) // Miscellaneous Symbols
+        tmpSet.addCharacters(in: NSRange(location: 0x2700, length: 192)) // Dingbats
+        tmpSet.addCharacters(in: NSRange(location: 0x2e80, length: 128)) // CJK Radicals Supplement
+        tmpSet.addCharacters(in: NSRange(location: 0x2f00, length: 224)) // Kangxi Radicals
+        tmpSet.addCharacters(in: NSRange(location: 0x2ff0, length: 16)) // Ideographic Description Characters
+        tmpSet.addCharacters(in: NSRange(location: 0x3000, length: 64)) // CJK Symbols and Punctuation
+        tmpSet.removeCharacters(in: NSRange(location: 0x3008, length: 10))
+        tmpSet.removeCharacters(in: NSRange(location: 0x3014, length: 12))
+        tmpSet.addCharacters(in: NSRange(location: 0x3040, length: 96)) // Hiragana
+        tmpSet.addCharacters(in: NSRange(location: 0x30a0, length: 96)) // Katakana
+        tmpSet.addCharacters(in: NSRange(location: 0x3100, length: 48)) // Bopomofo
+        tmpSet.addCharacters(in: NSRange(location: 0x3130, length: 96)) // Hangul Compatibility Jamo
+        tmpSet.addCharacters(in: NSRange(location: 0x3190, length: 16)) // Kanbun
+        tmpSet.addCharacters(in: NSRange(location: 0x31a0, length: 32)) // Bopomofo Extended
+        tmpSet.addCharacters(in: NSRange(location: 0x31c0, length: 48)) // CJK Strokes
+        tmpSet.addCharacters(in: NSRange(location: 0x31f0, length: 16)) // Katakana Phonetic Extensions
+        tmpSet.addCharacters(in: NSRange(location: 0x3200, length: 256)) // Enclosed CJK Letters and Months
+        tmpSet.addCharacters(in: NSRange(location: 0x3300, length: 256)) // CJK Compatibility
+        tmpSet.addCharacters(in: NSRange(location: 0x3400, length: 2582)) // CJK Unified Ideographs Extension A
+        tmpSet.addCharacters(in: NSRange(location: 0x4e00, length: 20941)) // CJK Unified Ideographs
+        tmpSet.addCharacters(in: NSRange(location: 0xac00, length: 11172)) // Hangul Syllables
+        tmpSet.addCharacters(in: NSRange(location: 0xd7b0, length: 80)) // Hangul Jamo Extended-B
+        tmpSet.addCharacters(in: "") // U+F8FF (Private Use Area)
+        tmpSet.addCharacters(in: NSRange(location: 0xf900, length: 512)) // CJK Compatibility Ideographs
+        tmpSet.addCharacters(in: NSRange(location: 0xfe10, length: 16)) // Vertical Forms
+        tmpSet.addCharacters(in: NSRange(location: 0xff00, length: 240)) // Halfwidth and Fullwidth Forms
+        tmpSet.addCharacters(in: NSRange(location: 0x1f200, length: 256)) // Enclosed Ideographic Supplement
+        tmpSet.addCharacters(in: NSRange(location: 0x1f300, length: 768)) // Enclosed Ideographic Supplement
+        tmpSet.addCharacters(in: NSRange(location: 0x1f600, length: 80)) // Emoticons (Emoji)
+        tmpSet.addCharacters(in: NSRange(location: 0x1f680, length: 128)) // Transport and Map Symbols
+        // See http://unicode-table.com/ for more information.
+        
+        return tmpSet
+    }()
+    
+    /**
+     Whether the glyph is bitmap.
+     
+     @param font  The glyph's font.
+     @param glyph The glyph which is created from the specified font.
+     @return YES: the glyph is bitmap, NO: the glyph is vector.
+     */
+    @objc @inline(__always) public static func textCGGlyphIsBitmap(_ font: CTFont?, glyph: CGGlyph) -> Bool {
+        
+        if !TextUtilities.textCTFontContainsColorBitmapGlyphs(font) {
+            return false
+        }
+        if CTFontCreatePathForGlyph(font!, glyph, nil) != nil {
+            return false
+        }
+        
+        return true
+    }
+    
+    /**
+     Get the `AppleColorEmoji` font's ascent with a specified font size.
+     It may used to create custom emoji.
+     
+     @param fontSize  The specified font size.
+     @return The font ascent.
+     */
+    @objc(textEmojiGetAscentWithFontSize:)
+    @inline(__always) public static func textEmojiGetAscent(with fontSize: CGFloat) -> CGFloat {
+        if fontSize < 16 {
+            return 1.25 * fontSize
+        } else if 16 <= fontSize && fontSize <= 24 {
+            return 0.5 * fontSize + 12
+        } else {
+            return fontSize
+        }
+    }
+    
+    /**
+     Get the `AppleColorEmoji` font's descent with a specified font size.
+     It may used to create custom emoji.
+     
+     @param fontSize  The specified font size.
+     @return The font descent.
+     */
+    @objc(textEmojiGetDescentWithFontSize:)
+    @inline(__always) public static func textEmojiGetDescent(with fontSize: CGFloat) -> CGFloat {
+        if fontSize < 16 {
+            return 0.390625 * fontSize
+        } else if 16 <= fontSize && fontSize <= 24 {
+            return 0.15625 * fontSize + 3.75
+        } else {
+            return 0.3125 * fontSize
+        }
+    }
+    
+    /**
+     Get the `AppleColorEmoji` font's glyph bounding rect with a specified font size.
+     It may used to create custom emoji.
+     
+     @param fontSize  The specified font size.
+     @return The font glyph bounding rect.
+     */
+    @objc(textEmojiGetGlyphBoundingRectWithFontSize:)
+    @inline(__always) public static func textEmojiGetGlyphBoundingRect(with fontSize: CGFloat) -> CGRect {
+        
+        var rect = CGRect(x: 0.75, y: 0, width: 0, height: 0)
+        
+        rect.size.height = textEmojiGetAscent(with: fontSize)
+        rect.size.width = rect.size.height
+        
+        if fontSize < 16 {
+            rect.origin.y = -0.2525 * fontSize
+        } else if 16 <= fontSize && fontSize <= 24 {
+            rect.origin.y = 0.1225 * fontSize - 6
+        } else {
+            rect.origin.y = -0.1275 * fontSize
+        }
+        return rect
+    }
+    
+    /**
+     Get the character set which should rotate and move in vertical form.
+     @return The shared character set.
+     */
+    @objc public static let textVerticalFormRotateAndMoveCharacterSet: NSCharacterSet = {
+        
+        return NSCharacterSet(charactersIn: ",。、.")
+    }()
+    
+    /// Convert degrees to radians. textDegreesToRadians:
+    @objc(textRadiansFromDegrees:)
+    @inline(__always) public static func textRadians(from degrees: CGFloat) -> CGFloat {
+        return degrees * .pi / 180
+    }
+    
+    /// Convert radians to degrees. textRadiansToDegrees:
+    @objc(textDegreesFromRadians:)
+    @inline(__always) public static func textDegrees(from radians: CGFloat) -> CGFloat {
+        return radians * 180 / .pi
+    }
+    
+    /// Get the transform rotation.
+    /// @return the rotation in radians [-PI, PI] ([-180°, 180°])
+    @objc(textCGAffineTransformGetRotation:)
+    @inline(__always) public static func textCGAffineTransformGetRotation(_ transform: CGAffineTransform) -> CGFloat {
+        return atan2(transform.b, transform.a)
+    }
+    
+    /// Get the transform's scale.x
+    @objc(textCGAffineTransformGetScaleX:)
+    @inline(__always) public static func textCGAffineTransformGetScaleX(_ transform: CGAffineTransform) -> CGFloat {
+        return sqrt(transform.a * transform.a + transform.c * transform.c)
+    }
+    
+    /// Get the transform's scale.y
+    @objc(textCGAffineTransformGetScaleY:)
+    @inline(__always) public static func textCGAffineTransformGetScaleY(_ transform: CGAffineTransform) -> CGFloat {
+        return sqrt(transform.b * transform.b + transform.d * transform.d)
+    }
+    /// Get the transform's translate.x
+    @objc(textCGAffineTransformGetTranslateX:)
+    @inline(__always) public static func textCGAffineTransformGetTranslateX(_ transform: CGAffineTransform) -> CGFloat {
+        return transform.tx
+    }
+    /// Get the transform's translate.y
+    @objc(textCGAffineTransformGetTranslateY:)
+    @inline(__always) public static func textCGAffineTransformGetTranslateY(_ transform: CGAffineTransform) -> CGFloat {
+        return transform.ty
+    }
+    
+    /// 矩阵求逆
+    private static func matrix_invert(_ matrix: inout [Double]) -> Int {
+        
+        // 这样写矩阵中总元素个数大于 8 的时候会发生越界导致 Crash
+//        var pivot : __CLPK_integer = 0
+//        var workspace = 0
+        // 这样写个数不受限制
+        let pivot = UnsafeMutablePointer<__CLPK_integer>.allocate(capacity: matrix.count)
+        let workspace = UnsafeMutablePointer<Double>.allocate(capacity: matrix.count)
+        defer {
+            pivot.deallocate()
+            workspace.deallocate()
+        }
+        
+        var error: __CLPK_integer = 0
+        
+        var n = __CLPK_integer(sqrt(Double(matrix.count)))
+        var m = n
+        var lda = n
+        
+        dgetrf_(&m, &n, &matrix, &lda, pivot, &error)
+        
+        if error != 0 {
+            return Int(error)
+        }
+        
+        dgetri_(&m, &matrix, &lda, pivot, workspace, &n, &error)
+        
+        return Int(error)
+    }
+    
+    /**
+     If you have 3 pair of points transformed by a same CGAffineTransform:
+     p1 (transform->) q1
+     p2 (transform->) q2
+     p3 (transform->) q3
+     This method returns the original transform matrix from these 3 pair of points.
+     
+     @see http://stackoverflow.com/questions/13291796/calculate-values-for-a-cgaffinetransform-from-three-points-in-each-of-two-uiview
+     */
+    @objc(textCGAffineTransformGetFromPoints::)
+    public static func textCGAffineTransformGet(from before: [CGPoint], _ after: [CGPoint]) -> CGAffineTransform {
+        
+        var p1: CGPoint, p2: CGPoint, p3: CGPoint, q1: CGPoint, q2: CGPoint, q3: CGPoint
+        
+        p1 = before[0]
+        p2 = before[1]
+        p3 = before[2]
+        q1 = after[0]
+        q2 = after[1]
+        q3 = after[2]
+        
+        var A = [Double](repeating: 0, count: 36)
+        A[0] = Double(p1.x); A[1] = Double(p1.y); A[2] = 0; A[3] = 0; A[4] = 1; A[5] = 0
+        A[6] = 0; A[7] = 0; A[8] = Double(p1.x); A[9] = Double(p1.y); A[10] = 0; A[11] = 1
+        A[12] = Double(p2.x); A[13] = Double(p2.y); A[14] = 0; A[15] = 0; A[16] = 1; A[17] = 0
+        A[18] = 0; A[19] = 0; A[20] = Double(p2.x); A[21] = Double(p2.y); A[22] = 0; A[23] = 1
+        A[24] = Double(p3.x); A[25] = Double(p3.y); A[26] = 0; A[27] = 0; A[28] = 1; A[29] = 0
+        A[30] = 0; A[31] = 0; A[32] = Double(p3.x); A[33] = Double(p3.y); A[34] = 0; A[35] = 1
+        
+        let error = matrix_invert(&A)
+        if error != 0 {
+            return .identity
+        }
+        var B = [Double](repeating: 0, count: 6)
+        B[0] = Double(q1.x)
+        B[1] = Double(q1.y)
+        B[2] = Double(q2.x)
+        B[3] = Double(q2.y)
+        B[4] = Double(q3.x)
+        B[5] = Double(q3.y)
+        var M = [Double](repeating: 0, count: 6)
+        M[0] = A[0] * B[0] + A[1] * B[1] + A[2] * B[2] + A[3] * B[3] + A[4] * B[4] + A[5] * B[5]
+        M[1] = A[6] * B[0] + A[7] * B[1] + A[8] * B[2] + A[9] * B[3] + A[10] * B[4] + A[11] * B[5]
+        M[2] = A[12] * B[0] + A[13] * B[1] + A[14] * B[2] + A[15] * B[3] + A[16] * B[4] + A[17] * B[5]
+        M[3] = A[18] * B[0] + A[19] * B[1] + A[20] * B[2] + A[21] * B[3] + A[22] * B[4] + A[23] * B[5]
+        M[4] = A[24] * B[0] + A[25] * B[1] + A[26] * B[2] + A[27] * B[3] + A[28] * B[4] + A[29] * B[5]
+        M[5] = A[30] * B[0] + A[31] * B[1] + A[32] * B[2] + A[33] * B[3] + A[34] * B[4] + A[35] * B[5]
+        
+        let transform = CGAffineTransform(a: CGFloat(M[0]), b: CGFloat(M[2]), c: CGFloat(M[1]), d: CGFloat(M[3]), tx: CGFloat(M[4]), ty: CGFloat(M[5]))
+        
+        return transform
+    }
+    
+    /// Get the transform which can converts a point from the coordinate system of a given view to another.
+    @objc(textCGAffineTransformGetFromView:to:)
+    public static func textCGAffineTransformGet(from: UIView?, to: UIView?) -> CGAffineTransform {
+        guard let _ = from, let _ = to else {
+            return .identity
+        }
+        var before = [CGPoint](repeating: CGPoint.zero, count: 3)
+        var after = [CGPoint](repeating: CGPoint.zero, count: 3)
+        before[0] = CGPoint(x: 0, y: 0)
+        before[1] = CGPoint(x: 0, y: 1)
+        before[2] = CGPoint(x: 1, y: 0)
+        after[0] = from!.bs_convertPoint(before[0], toViewOrWindow: to)
+        after[1] = from!.bs_convertPoint(before[1], toViewOrWindow: to)
+        after[2] = from!.bs_convertPoint(before[2], toViewOrWindow: to)
+        
+        return textCGAffineTransformGet(from: before, after)
+    }
+    
+    /// Create a skew transform.
+    @objc @inline(__always) public static func textCGAffineTransformMakeSkew(_ x: CGFloat, y: CGFloat) -> CGAffineTransform {
+        var transform: CGAffineTransform = .identity
+        transform.c = -x
+        transform.b = y
+        return transform
+    }
+    
+    /// Negates/inverts a UIEdgeInsets.
+    @objc @inline(__always) public static func textUIEdgeInsetsInvert(_ insets: UIEdgeInsets) -> UIEdgeInsets {
+        return UIEdgeInsets(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right)
+    }
+    
+    static var textCAGravityToUIViewContentModeDic = [CALayerContentsGravity.center: UIView.ContentMode.center.rawValue,
+                                                      CALayerContentsGravity.top: UIView.ContentMode.top.rawValue,
+                                                      CALayerContentsGravity.bottom: UIView.ContentMode.bottom.rawValue,
+                                                      CALayerContentsGravity.left: UIView.ContentMode.left.rawValue,
+                                                      CALayerContentsGravity.right: UIView.ContentMode.right.rawValue,
+                                                      CALayerContentsGravity.topLeft: UIView.ContentMode.topLeft.rawValue,
+                                                      CALayerContentsGravity.topRight: UIView.ContentMode.topRight.rawValue,
+                                                      CALayerContentsGravity.bottomLeft: UIView.ContentMode.bottomLeft.rawValue,
+                                                      CALayerContentsGravity.bottomRight: UIView.ContentMode.bottomRight.rawValue,
+                                                      CALayerContentsGravity.resize: UIView.ContentMode.scaleToFill.rawValue,
+                                                      CALayerContentsGravity.resizeAspect: UIView.ContentMode.scaleAspectFit.rawValue,
+                                                      CALayerContentsGravity.resizeAspectFill: UIView.ContentMode.scaleAspectFill.rawValue]
+    
+    @objc public static func textCAGravityToUIViewContentMode(_ gravity: CALayerContentsGravity?) -> UIView.ContentMode {
+        
+        guard let g = gravity else {
+            return .scaleToFill
+        }
+        
+        return (UIView.ContentMode(rawValue: textCAGravityToUIViewContentModeDic[g]!))!
+    }
+    
+    /// Convert UIViewContentMode to CALayer's gravity string.
+    @objc public static func textUIViewContentModeToCAGravity(contentMode: UIView.ContentMode) -> String {
+        switch contentMode {
+        case .scaleToFill:
+            return CALayerContentsGravity.resize.rawValue
+        case .scaleAspectFit:
+            return CALayerContentsGravity.resizeAspect.rawValue
+        case .scaleAspectFill:
+            return CALayerContentsGravity.resizeAspectFill.rawValue
+        case .redraw:
+            return CALayerContentsGravity.resize.rawValue
+        case .center:
+            return CALayerContentsGravity.center.rawValue
+        case .top:
+            return CALayerContentsGravity.top.rawValue
+        case .bottom:
+            return CALayerContentsGravity.bottom.rawValue
+        case .left:
+            return CALayerContentsGravity.left.rawValue
+        case .right:
+            return CALayerContentsGravity.right.rawValue
+        case .topLeft:
+            return CALayerContentsGravity.topLeft.rawValue
+        case .topRight:
+            return CALayerContentsGravity.topRight.rawValue
+        case .bottomLeft:
+            return CALayerContentsGravity.bottomLeft.rawValue
+        case .bottomRight:
+            return CALayerContentsGravity.bottomRight.rawValue
+        default:
+            return CALayerContentsGravity.resize.rawValue
+        }
+    }
+    
+    /**
+     Returns a rectangle to fit the `rect` with specified content mode.
+     
+     @param rect The constrant rect
+     @param size The content size
+     @param contentMode The content mode
+     @return A rectangle for the given content mode.
+     @discussion UIViewContentModeRedraw is same as UIViewContentModeScaleToFill.
+     */
+    @objc(textCGRectFitWithContentMode:rect:size:)
+    public static func textCGRectFit(with contentMode: UIView.ContentMode, rect: CGRect, size: CGSize) -> CGRect {
+        
+        var tmprect = rect.standardized
+        var size = size
+        
+        size.width = size.width < 0 ? -size.width : size.width
+        size.height = size.height < 0 ? -size.height : size.height
+        let center = CGPoint(x: tmprect.midX, y: tmprect.midY)
+        switch contentMode {
+        case .scaleAspectFit, .scaleAspectFill:
+            if tmprect.size.width < 0.01 || tmprect.size.height < 0.01 || size.width < 0.01 || size.height < 0.01 {
+                tmprect.origin = center
+                tmprect.size = CGSize.zero
+            } else {
+                var scale: CGFloat
+                if contentMode == .scaleAspectFit {
+                    if size.width / size.height < tmprect.size.width / tmprect.size.height {
+                        scale = tmprect.size.height / size.height
+                    } else {
+                        scale = tmprect.size.width / size.width
+                    }
+                } else {
+                    if size.width / size.height < tmprect.size.width / tmprect.size.height {
+                        scale = tmprect.size.width / size.width
+                    } else {
+                        scale = tmprect.size.height / size.height
+                    }
+                }
+                size.width *= scale
+                size.height *= scale
+                tmprect.size = size
+                tmprect.origin = CGPoint(x: center.x - size.width * 0.5, y: center.y - size.height * 0.5)
+            }
+        case .center:
+            tmprect.size = size
+            tmprect.origin = CGPoint(x: center.x - size.width * 0.5, y: center.y - size.height * 0.5)
+        case .top:
+            tmprect.origin.x = center.x - size.width * 0.5
+            tmprect.size = size
+        case .bottom:
+            tmprect.origin.x = center.x - size.width * 0.5
+            tmprect.origin.y += tmprect.size.height - size.height
+            tmprect.size = size
+        case .left:
+            tmprect.origin.y = center.y - size.height * 0.5
+        case .right:
+            tmprect.origin.y = center.y - size.height * 0.5
+            tmprect.origin.x += tmprect.size.width - size.width
+            tmprect.size = size
+        case .topLeft:
+            tmprect.size = size
+        case .topRight:
+            tmprect.origin.x += tmprect.size.width - size.width
+            tmprect.size = size
+        case .bottomLeft:
+            tmprect.origin.y += tmprect.size.height - size.height
+            tmprect.size = size
+        case .bottomRight:
+            tmprect.origin.x += tmprect.size.width - size.width
+            tmprect.origin.y += tmprect.size.height - size.height
+            tmprect.size = size
+        case .scaleToFill, .redraw:
+            return rect
+        default:
+            return rect
+        }
+        return tmprect
+    }
+    
+    /// Get main screen's scale.
+    @objc public static var textScreenScale = UIScreen.main.scale
+    
+    /// Get main screen's size. Height is always larger than width.
+    @objc public static var textScreenSize = CGSize(width: min(UIScreen.main.bounds.size.height, UIScreen.main.bounds.size.width), height: max(UIScreen.main.bounds.size.height, UIScreen.main.bounds.size.width))
+    
+    /// Convert point to pixel.
+    @objc(textCGFloatToPixel:)
+    @inline(__always) public static func textCGFloat(toPixel value: CGFloat) -> CGFloat {
+        return value * textScreenScale
+    }
+    
+    /// Convert pixel to point.
+    @objc(textCGFloatFromPixel:)
+    @inline(__always) public static func textCGFloat(fromPixel value: CGFloat) -> CGFloat {
+        return value / textScreenScale
+    }
+    
+    /// floor point value for pixel-aligned
+    @objc(textCGFloatPixelFloor:)
+    @inline(__always) public static func textCGFloat(pixelFloor value: CGFloat) -> CGFloat {
+        let scale = textScreenScale
+        return floor(value * scale) / scale
+    }
+    
+    /// round point value for pixel-aligned
+    @objc(textCGFloatPixelRound:)
+    @inline(__always) public static func textCGFloat(pixelRound value: CGFloat) -> CGFloat {
+        let scale = textScreenScale
+        return CGFloat(round(Double(value * scale)) / Double(scale))
+    }
+    
+    /// ceil point value for pixel-aligned
+    @objc(textCGFloatPixelCeil:)
+    @inline(__always) public static func textCGFloat(pixelCeil value: CGFloat) -> CGFloat {
+        let scale = textScreenScale
+        return ceil(value * scale) / scale
+    }
+    
+    /// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned)
+    @objc(textCGFloatPixelHalf:)
+    @inline(__always) public static func textCGFloat(pixelHalf value: CGFloat) -> CGFloat {
+        let scale = textScreenScale
+        return (floor(value * scale) + 0.5) / scale
+    }
+    
+    /// floor point value for pixel-aligned
+    @objc(textCGPointPixelFloor:)
+    @inline(__always) public static func TextCGPoint(pixelFloor point: CGPoint) -> CGPoint {
+        let scale = textScreenScale
+        return CGPoint(x: floor(point.x * scale) / scale, y: floor(point.y * scale) / scale)
+    }
+    
+    /// round point value for pixel-aligned
+    @objc(textCGPointPixelRound:)
+    @inline(__always) public static func TextCGPoint(pixelRound point: CGPoint) -> CGPoint {
+        let scale = Double(textScreenScale)
+        return CGPoint(x: CGFloat(round(Double(point.x) * scale) / scale), y: CGFloat(round(Double(point.y) * scale) / scale))
+    }
+    
+    /// ceil point value for pixel-aligned
+    @objc(textCGPointPixelCeil:)
+    @inline(__always) public static func TextCGPoint(pixelCeil point: CGPoint) -> CGPoint {
+        let scale = textScreenScale
+        return CGPoint(x: ceil(point.x * scale) / scale, y: ceil(point.y * scale) / scale)
+    }
+    
+    /// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned)
+    @objc(textCGPointPixelHalf:)
+    @inline(__always) public static func TextCGPoint(pixelHalf point: CGPoint) -> CGPoint {
+        let scale = textScreenScale
+        return CGPoint(x: (floor(point.x * scale) + 0.5) / scale, y: (floor(point.y * scale) + 0.5) / scale)
+    }
+    
+    /// floor point value for pixel-aligned
+    @objc(textCGSizePixelFloor:)
+    @inline(__always) public static func TextCGSize(pixelFloor size: CGSize) -> CGSize {
+        let scale = textScreenScale
+        return CGSize(width: floor(size.width * scale) / scale, height: floor(size.height * scale) / scale)
+    }
+    
+    /// round point value for pixel-aligned
+    @objc(textCGSizePixelRound:)
+    @inline(__always) public static func TextCGSize(pixelRound size: CGSize) -> CGSize {
+        let scale = textScreenScale
+        return CGSize(width: CGFloat(round(Double(size.width * scale)) / Double(scale)), height: CGFloat(round(Double(size.height * scale)) / Double(scale)))
+    }
+    
+    /// ceil point value for pixel-aligned
+    @objc(textCGSizePixelCeil:)
+    @inline(__always) public static func TextCGSize(pixelCeil size: CGSize) -> CGSize {
+        let scale = textScreenScale
+        return CGSize(width: ceil(size.width * scale) / scale, height: ceil(size.height * scale) / scale)
+    }
+    
+    /// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned)
+    @objc(textCGSizePixelHalf:)
+    @inline(__always) public static func TextCGSize(pixelHalf size: CGSize) -> CGSize {
+        let scale = textScreenScale
+        return CGSize(width: (floor(size.width * scale) + 0.5) / scale, height: (floor(size.height * scale) + 0.5) / scale)
+    }
+    
+    /// floor point value for pixel-aligned
+    @objc(textCGRectPixelFloor:)
+    @inline(__always) public static func textCGRect(pixelFloor rect: CGRect) -> CGRect {
+        let origin: CGPoint = TextCGPoint(pixelCeil: rect.origin)
+        let corner = TextCGPoint(pixelFloor: CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height))
+        var ret = CGRect(x: origin.x, y: origin.y, width: corner.x - origin.x, height: corner.y - origin.y)
+        if ret.size.width < 0 {
+            ret.size.width = 0
+        }
+        if ret.size.height < 0 {
+            ret.size.height = 0
+        }
+        return ret
+    }
+    
+    /// round point value for pixel-aligned
+    @objc(textCGRectPixelRound:)
+    @inline(__always) public static func textCGRect(pixelRound rect: CGRect) -> CGRect {
+        let origin: CGPoint = TextCGPoint(pixelRound: rect.origin)
+        let corner = TextCGPoint(pixelRound: CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height))
+        return CGRect(x: origin.x, y: origin.y, width: corner.x - origin.x, height: corner.y - origin.y)
+    }
+    
+    /// ceil point value for pixel-aligned
+    @objc(textCGRectPixelCeil:)
+    @inline(__always) public static func textCGRect(pixelCeil rect: CGRect) -> CGRect {
+        let origin: CGPoint = TextCGPoint(pixelFloor: rect.origin)
+        let corner = TextCGPoint(pixelCeil: CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height))
+        return CGRect(x: origin.x, y: origin.y, width: corner.x - origin.x, height: corner.y - origin.y)
+    }
+    
+    /// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned)
+    @objc(textCGRectPixelHalf:)
+    @inline(__always) public static func textCGRect(pixelHalf rect: CGRect) -> CGRect {
+        let origin: CGPoint = TextCGPoint(pixelHalf: rect.origin)
+        let corner = TextCGPoint(pixelHalf: CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height))
+        return CGRect(x: origin.x, y: origin.y, width: corner.x - origin.x, height: corner.y - origin.y)
+    }
+    
+    /// Returns the center for the rectangle.
+    @objc(textCGRectGetCenter:)
+    @inline(__always) public static func textCGRectGetCenter(_ rect: CGRect) -> CGPoint {
+        return CGPoint(x: rect.midX, y: rect.midY)
+    }
+    
+    /// Returns the area of the rectangle.
+    @objc(textCGRectGetArea:)
+    @inline(__always) public static func textCGRectGetArea(_ rect: CGRect) -> CGFloat {
+        var rect = rect
+        if rect.isNull {
+            return 0
+        }
+        rect = rect.standardized
+        return rect.size.width * rect.size.height
+    }
+    
+    /// Returns the distance between two points.
+    @objc(textCGPointGetDistanceToPoint:p2:)
+    @inline(__always) public static func textCGPointGetDistance(to p1: CGPoint, p2: CGPoint) -> CGFloat {
+        return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y))
+    }
+    
+    /// Returns the minmium distance between a point to a rectangle.
+    @objc(textCGPointGetDistanceToRect:r:)
+    @inline(__always) public static func textCGPointGetDistance(to p: CGPoint, r: CGRect) -> CGFloat {
+        let rect = r.standardized
+        if rect.contains(p) {
+            return 0
+        }
+        var distV: CGFloat
+        var distH: CGFloat
+        if rect.minY <= p.y && p.y <= rect.maxY {
+            distV = 0
+        } else {
+            distV = p.y < rect.minY ? rect.minY - p.y : p.y - rect.maxY
+        }
+        if rect.minX <= p.x && p.x <= rect.maxX {
+            distH = 0
+        } else {
+            distH = p.x < rect.minX ? rect.minX - p.x : p.x - rect.maxX
+        }
+        return max(distV, distH)
+    }
+
+    /// floor UIEdgeInset for pixel-aligned
+    @objc(textUIEdgeInsetPixelFloor:)
+    @inline(__always) public static func textUIEdgeInset(pixelFloor insets: UIEdgeInsets) -> UIEdgeInsets {
+        var i = insets
+        i.top = textCGFloat(pixelFloor: insets.top)
+        i.left = textCGFloat(pixelFloor: insets.left)
+        i.bottom = textCGFloat(pixelFloor: insets.bottom)
+        i.right = textCGFloat(pixelFloor: insets.right)
+        return i
+    }
+    
+    /// ceil UIEdgeInset for pixel-aligned
+    @objc(textUIEdgeInsetPixelCeil:)
+    @inline(__always) public static func textUIEdgeInset(pixelCeil insets: UIEdgeInsets) -> UIEdgeInsets {
+        var i = insets
+        i.top = textCGFloat(pixelCeil: insets.top)
+        i.left = textCGFloat(pixelCeil: insets.left)
+        i.bottom = textCGFloat(pixelCeil: insets.bottom)
+        i.right = textCGFloat(pixelCeil: insets.right)
+        return i
+    }
+    
+    @objc(textFontWithBold:)
+    @inline(__always) public static func textFont(withBold font: UIFont?) -> UIFont? {
+        if let aBold = font?.fontDescriptor.withSymbolicTraits(.traitBold) {
+            return UIFont(descriptor: aBold, size: font?.pointSize ?? 0)
+        }
+        return nil
+    }
+    
+    @objc(textFontWithItalic:)
+    @inline(__always) public static func textFont(withItalic font: UIFont?) -> UIFont? {
+        if let anItalic = font?.fontDescriptor.withSymbolicTraits(.traitItalic) {
+            return UIFont(descriptor: anItalic, size: font?.pointSize ?? 0)
+        }
+        return nil
+    }
+    
+    @objc(textFontWithBoldItalic:)
+    @inline(__always) public static func textFont(withBoldItalic font: UIFont?) -> UIFont? {
+        if let anItalic = font?.fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) {
+            return UIFont(descriptor: anItalic, size: font?.pointSize ?? 0)
+        }
+        return nil
+    }
+    
+    /**
+     Convert CFRange to NSRange
+     @param cfRange CFRange @return NSRange
+     */
+    @objc(textNSRangeFromCFRange:)
+    @inline(__always) public static func textNSRange(from cfRange: CFRange) -> NSRange {
+        return NSRange(location: cfRange.location, length: cfRange.length)
+    }
+    
+    /**
+     Convert NSRange to CFRange
+     @param nsRange NSRange @return CFRange
+     */
+    @objc(textCFRangeFromNSRange:)
+    @inline(__always) public static func textCFRange(from nsRange: NSRange) -> CFRange {
+        return CFRangeMake(nsRange.location, nsRange.length)
+    }
+}

+ 160 - 0
Pods/BSText/BSText/Utility/UIPasteboardExtension.swift

@@ -0,0 +1,160 @@
+//
+//  UIPasteboardExtension.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/11/8.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+import MobileCoreServices
+
+#if canImport(YYImage)
+import YYImage
+#endif
+
+/**
+ Extend UIPasteboard to support image and attributed string.
+ */
+extension UIPasteboard {
+    
+    @objc public static let UTTypeAttributedString = "com.BlueSky.PasteboardAttributedString"
+    @objc public static let UTTypeWEBP = "com.google.webp"
+    
+    /*/< PNG file data */
+    @objc public var bs_PNGData: Data? {
+        set {
+            if let aData = newValue {
+                setData(aData, forPasteboardType: kUTTypePNG as String)
+            }
+        }
+        get {
+            return data(forPasteboardType: kUTTypePNG as String)
+        }
+    }
+    
+    /*/< JPEG file data */
+    @objc public var bs_JPEGData: Data? {
+        set {
+            if let aData = newValue {
+                setData(aData, forPasteboardType: kUTTypeJPEG as String)
+            }
+        }
+        get {
+            return data(forPasteboardType: kUTTypeJPEG as String)
+        }
+    }
+    
+    /*/< GIF file data */
+    @objc public var bs_GIFData: Data? {
+        set {
+            if let aData = newValue {
+                setData(aData, forPasteboardType: kUTTypeGIF as String)
+            }
+        }
+        get {
+            return data(forPasteboardType: kUTTypeGIF as String)
+        }
+    }
+    
+    /*/< WebP file data */
+    @objc public var bs_WEBPData: Data? {
+        set {
+            if let aData = newValue {
+                setData(aData, forPasteboardType: UIPasteboard.UTTypeWEBP)
+            }
+        }
+        get {
+            return data(forPasteboardType: UIPasteboard.UTTypeWEBP)
+        }
+    }
+    
+    /*/< image file data */
+    @objc public var bs_ImageData: Data? {
+        set {
+            if let aData = newValue {
+                setData(aData, forPasteboardType: kUTTypeImage as String)
+            }
+        }
+        get {
+            return data(forPasteboardType: kUTTypeImage as String)
+        }
+    }
+    
+    /// Attributed string,
+    /// Set this attributed will also set the string property which is copy from the attributed string.
+    /// If the attributed string contains one or more image, it will also set the `images` property.
+    @objc public var bs_AttributedString: NSAttributedString? {
+        
+        set {
+            string = newValue?.bs_plainText(for: NSRange(location: 0, length: newValue!.length))
+            
+            if let data = newValue?.bs_archiveToData() {
+                
+                let item = [UIPasteboard.UTTypeAttributedString: data]
+                
+                self.addItems([item])
+            }
+            
+            newValue?.enumerateAttribute(NSAttributedString.Key(rawValue: TextAttribute.textAttachmentAttributeName), in: NSRange(location: 0, length: newValue!.length), options: .longestEffectiveRangeNotRequired, using: { atta, range, stop in
+                
+                guard let attachment = atta as? TextAttachment else {
+                    return
+                }
+                
+                // save image
+                var simpleImage: UIImage? = nil
+                if (attachment.content is UIImage) {
+                    simpleImage = attachment.content as? UIImage
+                } else if (attachment.content is UIImageView) {
+                    simpleImage = (attachment.content as? UIImageView)?.image
+                }
+
+                if let anImage = simpleImage {
+                    let item = ["com.apple.uikit.image": anImage]
+                    self.addItems([item])
+                }
+                
+                #if canImport(YYImage)
+                // save animated image
+                if (attachment.content is UIImageView) {
+                    let imageView = attachment.content as! UIImageView
+                    
+                    let image: UIImage? = imageView.image
+                    if (image is YYImage) {
+                        
+                        if let data = image?.value(forKey: "animatedImageData") as? Data {
+                            let type = image?.value(forKey: "animatedImageType") as? UInt ?? 0
+                            switch type {
+                            case YYImageType.GIF.rawValue:
+                                let s = kUTTypeGIF as String
+                                let item = [s: data]
+                                addItems([item])
+                            case YYImageType.PNG.rawValue:
+                                // APNG
+                                let s = kUTTypePNG as String
+                                let item = [s: data]
+                                addItems([item])
+                            case YYImageType.webP.rawValue:
+                                let s = UIPasteboard.UTTypeWEBP as String
+                                let item = [s: data]
+                                addItems([item])
+                            default:
+                                break
+                            }
+                        }
+                    }
+                }
+                #endif
+            })
+        }
+        get {
+            for item in items {
+                if let data = item[UIPasteboard.UTTypeAttributedString] as? Data {
+                    return NSAttributedString.bs_unarchive(from: data)
+                }
+            }
+            return nil
+        }
+    }
+}

+ 134 - 0
Pods/BSText/BSText/Utility/UIViewExtension.swift

@@ -0,0 +1,134 @@
+//
+//  UIViewExtension.swift
+//  BSText
+//
+//  Created by BlueSky on 2018/10/22.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import UIKit
+
+public extension UIView {
+    
+    @objc var bs_viewController: UIViewController? {
+        get {
+            var view: UIView? = self
+            while (view != nil) {
+                let nextResponder = view?.next
+                if (nextResponder is UIViewController) {
+                    return nextResponder as? UIViewController
+                }
+                view = view?.superview
+            }
+            return nil
+        }
+    }
+    
+    @objc var bs_visibleAlpha: CGFloat {
+        get {
+            if (self is UIWindow) {
+                if isHidden {
+                    return 0
+                }
+                return self.alpha
+            }
+            if !(window != nil) {
+                return 0
+            }
+            var alpha: CGFloat = 1
+            var v: UIView? = self
+            while v != nil {
+                if v!.isHidden {
+                    alpha = 0
+                    break
+                }
+                alpha *= v!.alpha
+                v = v!.superview
+            }
+            return alpha
+        }
+    }
+    
+    @objc func bs_convertPoint(_ point: CGPoint, toViewOrWindow view: UIView?) -> CGPoint {
+        var point = point
+        if view == nil {
+            if (self is UIWindow) {
+                return (self as? UIWindow)?.convert(point, to: nil) ?? CGPoint.zero
+            } else {
+                return convert(point, to: nil)
+            }
+        }
+        let from: UIWindow? = (self is UIWindow) ? (self as! UIWindow) : window
+        let to = (view is UIWindow) ? (view as? UIWindow) : view?.window
+        if (from == nil || to == nil) || (from == to) {
+            return convert(point, to: view)
+        }
+        point = convert(point, to: from)
+        point = to?.convert(point, from: from) ?? CGPoint.zero
+        point = view?.convert(point, from: to) ?? CGPoint.zero
+        return point
+    }
+    
+    @objc func bs_convertPoint(_ point: CGPoint, fromViewOrWindow view: UIView?) -> CGPoint {
+        var point = point
+        if view == nil {
+            if (self is UIWindow) {
+                return (self as? UIWindow)?.convert(point, from: nil) ?? CGPoint.zero
+            } else {
+                return convert(point, from: nil)
+            }
+        }
+        let from = (view is UIWindow) ? (view as? UIWindow) : view?.window
+        let to: UIWindow? = (self is UIWindow) ? (self as! UIWindow) : window
+        if (from == nil || to == nil) || (from == to) {
+            return convert(point, from: view)
+        }
+        point = from?.convert(point, from: view) ?? CGPoint.zero
+        point = to?.convert(point, from: from) ?? CGPoint.zero
+        point = convert(point, from: to)
+        return point
+    }
+    
+    @objc func bs_convertRect(_ rect: CGRect, toViewOrWindow view: UIView?) -> CGRect {
+        var rect = rect
+        if view == nil {
+            if (self is UIWindow) {
+                return (self as? UIWindow)?.convert(rect, to: nil) ?? CGRect.zero
+            } else {
+                return convert(rect, to: nil)
+            }
+        }
+        let from: UIWindow? = (self is UIWindow) ? (self as! UIWindow) : window
+        let to = (view is UIWindow) ? (view as? UIWindow) : view?.window
+        if from == nil || to == nil {
+            return convert(rect, to: view)
+        }
+        if from == to {
+            return convert(rect, to: view)
+        }
+        rect = convert(rect, to: from)
+        rect = to?.convert(rect, from: from) ?? CGRect.zero
+        rect = view?.convert(rect, from: to) ?? CGRect.zero
+        return rect
+    }
+    
+    @objc func bs_convertRect(_ rect: CGRect, fromViewOrWindow view: UIView?) -> CGRect {
+        var rect = rect
+        if view == nil {
+            if (self is UIWindow) {
+                return (self as? UIWindow)?.convert(rect, from: nil) ?? CGRect.zero
+            } else {
+                return convert(rect, from: nil)
+            }
+        }
+        let from = (view is UIWindow) ? (view as? UIWindow) : view?.window
+        let to: UIWindow? = (self is UIWindow) ? (self as! UIWindow) : window
+        if (from == nil || to == nil) || (from == to) {
+            return convert(rect, from: view)
+        }
+        rect = from?.convert(rect, from: view) ?? CGRect.zero
+        rect = to?.convert(rect, from: from) ?? CGRect.zero
+        rect = convert(rect, from: to)
+        return rect
+    }
+}

+ 87 - 0
Pods/BSText/BSText/Utility/WeakTimerProxy.swift

@@ -0,0 +1,87 @@
+//
+//  WeakTimerProxy.swift
+//  BSText
+//
+//  Created by Bruce on 2018/10/21.
+//  Copyright © 2019 GeekBruce. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+// MARK: - 目前没有找到 Swift 下基于 NSProxy 的好的解决方案
+/// 处理 NSTimer、CADisplayLink 引用循环的代理类
+public class WeakTimerProxy: NSObject {
+    
+    weak var target: NSObjectProtocol?
+    // MARK: - 目前的处理 方法中还不能带参数,否则会崩溃
+    var sel: Selector?
+    /// required,实例化timer之后需要将timer赋值给proxy,否则就算target释放了,timer本身依然会继续运行
+    public weak var timer: Timer?
+    public weak var displayLink: CADisplayLink?
+    
+    public required init(target: NSObjectProtocol?, sel: Selector?) {
+        self.target = target
+        self.sel = sel
+        super.init()
+        // 加强安全保护
+        guard target?.responds(to: sel) == true else {
+            return
+        }
+        // 将target的selector替换为redirectionMethod,该方法会重新处理事件
+        let method = class_getInstanceMethod(self.classForCoder, #selector(WeakTimerProxy.redirectionMethod))!
+        class_replaceMethod(self.classForCoder, sel!, method_getImplementation(method), method_getTypeEncoding(method))
+    }
+    
+    @objc func redirectionMethod () {
+        // 如果target未被释放,则调用target方法,否则释放timer
+        if self.target != nil {
+            self.target!.perform(self.sel)
+        } else {
+            self.timer?.invalidate()
+            self.displayLink?.invalidate()
+            print("WeakProxy: invalidate timer.")
+        }
+    }
+    
+    override public func forwardingTarget(for aSelector: Selector!) -> Any? {
+        
+        if self.target?.responds(to: aSelector) == true {
+            return self.target
+        } else {
+            self.timer?.invalidate()
+            self.displayLink?.invalidate()
+            return self
+        }
+    }
+}
+
+// 使用案例
+// 外部调用target直接传 self 就行,有代理类,可以放心直接使用,不用担心循环引用
+//self.timer = Timer.bs_scheduledTimer(timeInterval: 3, target: self, selector: #selector(autoScroll), userInfo: nil, repeats: true)
+public extension Timer {
+    
+    @objc(bs_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:)
+    class func bs_scheduledTimer(with timeInterval: TimeInterval, target: NSObjectProtocol, selector: Selector, userInfo aInfo: Any?, repeats yesOrNo: Bool) -> Timer {
+        
+        let proxy = WeakTimerProxy.init(target: target, sel: selector)
+        let timer = Timer.scheduledTimer(timeInterval: timeInterval, target: proxy, selector: selector, userInfo:aInfo, repeats: yesOrNo)
+        proxy.timer = timer
+        
+        return timer
+    }
+}
+
+public extension CADisplayLink {
+    
+    @objc(bs_displayLinkWithTarget:selector:)
+    class func bs_displayLink(with target: NSObjectProtocol, selector: Selector) -> CADisplayLink {
+        
+        let proxy = WeakTimerProxy.init(target: target, sel: selector)
+        let displayLink = CADisplayLink.init(target: proxy, selector: selector)
+        displayLink.add(to: RunLoop.main, forMode: .common)
+        proxy.displayLink = displayLink
+
+        return displayLink
+    }
+}

+ 21 - 0
Pods/BSText/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Bruce.Liu
+
+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.

+ 1159 - 0
Pods/BSText/README.md

@@ -0,0 +1,1159 @@
+# BSText(The Swift Version of YYText)
+
+[![License MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/a1049145827/BSText/master/LICENSE)&nbsp;
+[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)&nbsp;
+[![CocoaPods](https://img.shields.io/cocoapods/v/BSText.svg?style=flat)](https://cocoapods.org/pods/BSText)&nbsp;
+[![CocoaPods](http://img.shields.io/cocoapods/p/BSText.svg?style=flat)](http://cocoadocs.org/docsets/BSText)&nbsp;
+[![Support](https://img.shields.io/badge/support-iOS%208%2B%20-blue.svg?style=flat)](https://www.apple.com/nl/ios/)&nbsp;
+[![Build Status](https://travis-ci.org/a1049145827/BSText.svg?branch=master)](https://travis-ci.org/a1049145827/BSText)
+
+![鼠奎特](https://github.com/a1049145827/Resources/raw/master/framework/BSText/squirrel.jpg)
+
+Powerful text framework for iOS to display and edit rich text (the prefix 'BS' is come from BlueSky, The BlueSky Studio who created films named "Ice Age", Here is The cute squirrel.).<br/>
+
+# Features
+
+- UILabel and UITextView API compatible
+- High performance asynchronous text layout and rendering
+- Extended CoreText attributes with more text effects
+- Text attachments with UIImage, UIView and CALayer
+- Custom highlight text range to allow user interact with
+- Text parser support (built in markdown/emoticon parser)
+- Text container path and exclusion paths support
+- Vertical form layout support (for CJK text)
+- Image and attributed text copy/paste support
+- Attributed text placeholder support
+- Custom keyboard view support
+- Undo and redo control
+- Attributed text archiver and unarchiver support
+- Multi-language and VoiceOver support
+- Fully documented
+
+# Architecture
+
+All Same as [YYText](https://github.com/ibireme/YYText)
+
+# Text Attributes
+
+### BSText supported attributes
+
+<table>
+  <thead>
+    <tr>
+      <th>Demo</th>
+      <th>Attribute Name</th>
+      <th>Class</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextAttachment.gif" width="200"></td>
+      <td>TextAttachment</td>
+      <td>TextAttachment</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextHighlight.gif" width="200"></td>
+      <td>TextHighlight</td>
+      <td>TextHighlight</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBinding.gif" width="200"></td>
+      <td>TextBinding</td>
+      <td>TextBinding</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextShadow.png" width="200"></td>
+      <td>TextShadow<br/>TextInnerShadow</td>
+      <td>TextShadow</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBorder.png" width="200"></td>
+      <td>TextBorder</td>
+      <td>TextBorder</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBackgroundBorder.png" width="200"></td>
+      <td>TextBackgroundBorder</td>
+      <td>TextBorder</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBlockBorder.png" width="200"></td>
+      <td>TextBlockBorder</td>
+      <td>TextBorder</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Obliqueness.png" width="200"></td>
+      <td>TextGlyphTransform</td>
+      <td>NSValue(CGAffineTransform)</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Underline.png" width="200"></td>
+      <td>TextUnderline</td>
+      <td>TextDecoration</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Strikethrough.png" width="200"></td>
+      <td>TextStrickthrough</td>
+      <td>TextDecoration</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBackedString.png" width="200"></td>
+      <td>TextBackedString</td>
+      <td>TextBackedString</td>
+    </tr>
+  </tbody>
+</table>
+
+### CoreText attributes which is supported by BSText
+
+<table>
+  <thead>
+    <tr>
+      <th>Demo</th>
+      <th>Attribute Name</th>
+      <th>Class</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Font.png" width="200"></td>
+      <td> Font </td>
+      <td>UIFont(CTFontRef)</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Kern.png" width="200"></td>
+      <td> Kern </td>
+      <td> NSNumber </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Stroke.png" width="200"></td>
+      <td> StrokeWidth </td>
+      <td> NSNumber </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/StrokeColor.png" width="200"></td>
+      <td> StrokeColor </td>
+      <td> CGColorRef </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Shadow.png" width="200"></td>
+      <td> Shadow </td>
+      <td> NSShadow </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Ligature.png" width="200"></td>
+      <td> Ligature </td>
+      <td> NSNumber </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/VerticalForms.png" width="200"></td>
+      <td> VerticalGlyphForm </td>
+      <td> NSNumber(BOOL) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/WriteDirection.png" width="200"></td>
+      <td> WritingDirection </td>
+      <td> NSArray(NSNumber) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/RunDelegate.png" width="200"></td>
+      <td> RunDelegate </td>
+      <td> CTRunDelegateRef </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/Alignment.png" width="200"></td>
+      <td> TextAlignment </td>
+      <td> NSParagraphStyle <br/>(NSTextAlignment) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/LineBreakMode.png" width="200"></td>
+      <td> LineBreakMode </td>
+      <td> NSParagraphStyle <br/>(NSLineBreakMode) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/LineSpacing.png" width="200"></td>
+      <td> LineSpacing </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/ParagraphSpacing.png" width="200"></td>
+      <td> ParagraphSpacing <br/> ParagraphSpacingBefore </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/FirstLineHeadIndent.png" width="200"></td>
+      <td> FirstLineHeadIndent </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/HeadIndent.png" width="200"></td>
+      <td> HeadIndent </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/TailIndent.png" width="200"></td>
+      <td> TailIndent </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/MinimumLineHeight.png" width="200"></td>
+      <td> MinimumLineHeight </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/MaximumLineHeight.png" width="200"></td>
+      <td> MaximumLineHeight </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/LineHeightMultiple.png" width="200"></td>
+      <td> LineHeightMultiple </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/BaseWritingDirection.png" width="200"></td>
+      <td> BaseWritingDirection </td>
+      <td> NSParagraphStyle <br/>(NSWritingDirection) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/Tab.png" width="200"></td>
+      <td> DefaultTabInterval <br/> TabStops </td>
+      <td> NSParagraphStyle <br/>CGFloat/NSArray(NSTextTab)</td>
+    </tr>
+  </tbody>
+</table>
+
+
+
+# Usage
+
+### Basic
+
+```swift
+// BSLabel (similar to UILabel)
+let label = BSLabel()
+label.frame = ...
+label.font = ...
+label.textColor = ...
+label.textAlignment = ...
+label.lineBreakMode = ...
+label.numberOfLines = ...
+label.text = ...
+    
+// BSTextView (similar to UITextView)
+let textView = BSTextView()
+textView.frame = ...
+textView.font = ...
+textView.textColor = ...
+textView.dataDetectorTypes = ...
+textView.placeHolderText = ...
+textView.placeHolderTextColor = ...
+textView.delegate = ...
+```
+
+### Attributed text
+
+```swift
+// 1. Create an attributed string.
+let text = NSMutableAttributedString(string: "Some Text, blabla...")
+    
+// 2. Set attributes to text, you can use almost all CoreText attributes.
+text.bs_font = UIFont.boldSystemFont(ofSize:30)
+text.bs_color = UIColor.blue
+text.bs_set(color: UIColor.red, range: NSRange(location: 0, length: 4))
+text.bs_lineSpacing = 10
+    
+// 3. Set to BSLabel or BSTextView.
+let label = BSLabel()
+label.frame = CGRect(x: 15, y: 100, width: 200, height: 80)
+label.attributedText = text
+    
+let textView = BSTextView()
+textView.frame = CGRect(x: 15, y: 200, width: 200, height: 80)
+textView.attributedText = text
+```
+
+### Text highlight
+
+You can use some convenience methods to set text highlight:
+
+```swift
+text.bs_set(textHighlightRange: range,
+            color: UIColor.blue,
+            backgroundColor: UIColor.gray) { (view, text, range, rect) in
+    print("tap text range:...")
+}
+```
+
+Or set the text highlight with your custom config:
+
+```swift
+// 1. Create a 'highlight' attribute for text.
+let border = TextBorder.border(with: UIColor.gray, cornerRadius: 3)
+
+let highlight = TextHighlight()
+highlight.color = .white
+highlight.backgroundBorder = highlightBorder
+highlight.tapAction = { (containerView, text, range, rect) in
+    print("tap text range:...")
+    // you can also set the action handler to BSLabel or BSTextView.
+}
+
+// 2. Add 'highlight' attribute to a range of text.
+let attributedText = NSMutableAttributedString(string: " ")
+attributedText.bs_set(textHighlight: highlight, range: highlightRange)
+
+// 3. Set text to label or text view.
+let label = BSLabel()
+label.attributedText = attributedText
+
+let textView = BSTextView()
+textView.delegate = self
+textView.attributedText = ...
+
+// 4. Receive user interactive action.
+label.highlightTapAction = { (containerView, text, range, rect) in
+    print("tap text range:...")
+};
+label.highlightLongPressAction = { (containerView, text, range, rect) in
+    print("tap text range:...")
+};
+
+// MARK: - TextViewDelegate
+func textView(_ textView: BSTextView, didTap highlight: TextHighlight, in characterRange: NSRange, rect: CGRect) {
+    print("tap text range:...")
+}
+func textView(_ textView: BSTextView, didLongPress highlight: TextHighlight, in characterRange: NSRange, rect: CGRect) {
+    print("tap text range:...")
+}
+```
+
+### Text attachments
+
+```swift
+let text = NSMutableAttributedString()
+let font = UIFont.systemFont(ofSize: 16)
+
+// UIImage attachment
+let image = UIImage.init(named: "dribbble64_imageio")
+guard let attachment = NSMutableAttributedString.bs_attachmentString(with: image, contentMode: .center, attachmentSize: image?.size ?? .zero, alignTo: font, alignment: .center) else {
+    return
+}
+text.append(attachment)
+
+// UIView attachment
+let switcher = UISwitch()
+switcher.sizeToFit()
+guard let attachment1 = NSMutableAttributedString.bs_attachmentString(with: switcher, contentMode: .center, attachmentSize: switcher.frame.size, alignTo: font, alignment: .center) else {
+    return
+}
+text.append(attachment1)
+
+// CALayer attachment
+let layer = CAShapeLayer()
+layer.path = ...
+guard let attachment2 = NSMutableAttributedString.bs_attachmentString(with: layer, contentMode: .center, attachmentSize: layer.frame.size, alignTo: font, alignment: .center) else {
+    return
+}
+text.append(attachment2)
+```
+
+### Text layout calculation
+
+```swift
+let text = NSAttributedString()
+let size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude)
+let container = TextContainer()
+container.size = size
+guard let layout = TextLayout(container: container, text: text) else {
+    return
+}
+
+// get text bounding
+layout.textBoundingRect // get bounding rect
+layout.textBoundingSize // get bounding size
+
+// query text layout
+layout.lineIndex(for: CGPoint(x: 10, y: 10))
+layout.closestLineIndex(for: CGPoint(x: 10, y: 10))
+layout.closestPosition(to: CGPoint(x: 10, y: 10))
+layout.textRange(at: CGPoint(x: 10, y: 10))
+layout.rect(for: TextRange(range: NSRange(location: 10, length: 2)))
+layout.selectionRects(for: TextRange(range: NSRange(location: 10, length: 2)))
+
+// text layout display
+let label = BSLabel()
+label.frame = CGRect(x: 0, y: 0, width: layout.textBoundingSize.width, height: layout.textBoundingSize.height)
+label.textLayout = layout;
+```
+
+### Adjust text line position
+
+```swift
+// Convenience methods:
+// 1. Create a text line position modifier, implements `TextLinePositionModifier` protocol.
+// 2. Set it to label or text view.
+	
+let modifier = TextLinePositionSimpleModifier()
+modifier.fixedLineHeight = 24
+	
+let label = BSLabel()
+label.linePositionModifier = modifier
+	
+// Fully control
+let modifier = TextLinePositionSimpleModifier()
+modifier.fixedLineHeight = 24
+	
+let container = TextContainer()
+container.size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude)
+container.linePositionModifier = modifier
+	
+guard let layout = TextLayout(container: container, text: text) else {
+    return
+}
+let label = BSLabel()
+label.size = layout.textBoundingSize
+label.textLayout = layout
+```
+
+### Asynchronous layout and rendering
+
+```swift
+// If you have performance issues,
+// you may enable the asynchronous display mode.
+let label = BSLabel()
+label.displaysAsynchronously = true
+
+// If you want to get the highest performance, you should do 
+// text layout with `TextLayout` class in background thread.
+let label = BSLabel()
+label.displaysAsynchronously = true
+label.ignoreCommonProperties = true
+    
+DispatchQueue.global().async {
+    // Create attributed string.
+    let text = NSMutableAttributedString(string: "Some Text")
+    text.bs_font = UIFont.systemFont(ofSize: 16)
+    text.bs_color = UIColor.gray
+    text.bs_set(color: .red, range: NSRange(location: 0, length: 4))
+
+    // Create text container
+    let container = TextContainer()
+    container.size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude);
+    container.maximumNumberOfRows = 0;
+
+    // Generate a text layout.
+    let layout = TextLayout(container: container, text: text)
+
+    DispatchQueue.main.async {
+        label.frame = CGRect(x: 0, y: 0, width: layout.textBoundingSize.width, height: layout.textBoundingSize.height)
+        label.textLayout = layout;
+    }
+}
+```
+
+### Text container control
+
+```swift
+let label = BSLabel()
+label.textContainerPath = UIBezierPath(...)
+label.exclusionPaths = [UIBezierPath(), ...]
+label.textContainerInset = UIEdgeInsets(...)
+label.verticalForm = true/false
+    
+let textView = BSTextView()
+textView.exclusionPaths = [UIBezierPath(), ...]
+textView.textContainerInset = UIEdgeInsets(...)
+textView.verticalForm = true/false
+
+```
+
+### Text parser
+
+```swift
+// 1. Create a text parser
+let simpleEmoticonParser = TextSimpleEmoticonParser()
+var mapper = [String: UIImage]()
+mapper[":smile:"] = UIImage.init(named: "smile.png")
+mapper[":cool:"] = UIImage.init(named: "cool.png")
+mapper[":cry:"] = UIImage.init(named: "cry.png")
+mapper[":wink:"] = UIImage.init(named: "wink.png")
+simpleEmoticonParser.emoticonMapper = mapper;
+
+let markdownParser = TextSimpleMarkdownParser()
+markdownParser.setColorWithDarkTheme()
+
+let parser = MyCustomParser() // custom parser
+
+// 2. Attach parser to label or text view
+let label = BSLabel()
+label.textParser = parser
+
+let textView = BSTextView()
+textView.textParser = parser
+
+```
+
+### Debug
+
+```swift
+// Set a shared debug option to show text layout result.
+let debugOption = TextDebugOption()
+debugOption.baselineColor = .red
+debugOption.ctFrameBorderColor = .red
+debugOption.ctLineFillColor = UIColor(red: 0, green: 0.463, blue: 1, alpha: 0.18)
+debugOption.cgGlyphBorderColor = UIColor(red: 1, green: 0.524, blue: 0, alpha: 0.2)
+TextDebugOption.setSharedDebugOption(debugOption)
+
+```
+
+### More examples
+
+See `Demo/BSTextDemo.xcodeproj` for more examples:
+
+<img src="https://raw.github.com/a1049145827/BSText/master/Demo/DemoSnapshot/text_path.gif" width="320">
+<img src="https://raw.github.com/a1049145827/BSText/master/Demo/DemoSnapshot/text_markdown.gif" width="320">
+<img src="https://raw.github.com/a1049145827/BSText/master/Demo/DemoSnapshot/text_vertical.gif" width="320">
+<img src="https://raw.github.com/a1049145827/BSText/master/Demo/DemoSnapshot/text_paste.gif" width="320">
+
+# Installation
+
+### CocoaPods
+
+1. Add `pod 'BSText'` to your Podfile.
+2. Run `pod install` or `pod update`.
+3. Import Module `import BSText`, use `@import BSText;` in OC project.
+
+### Carthage
+
+1. Add `github "a1049148527/BSText"` to your Cartfile.
+2. Run `carthage update --platform ios` and add the framework to your project.
+3. Import Module `import BSText`, use `@import BSText;` in OC project.
+
+### Manually
+
+1. Download all the files in the `BSText` subdirectory.
+2. Add the source files to your Xcode project.
+3. Link with required frameworks:
+   - UIKit
+   - CoreFoundation
+   - CoreText
+   - QuartzCore
+   - Accelerate
+   - MobileCoreServices
+4. Now you can use it.
+
+### Notice
+
+You may add [YYImage](https://github.com/ibireme/YYImage) or [YYWebImage](https://github.com/ibireme/YYWebImage) to your project if you want to support animated image (GIF/APNG/WebP).
+
+# Documentation
+
+API documentation is same as YYText, you can see it on [CocoaDocs](http://cocoadocs.org/docsets/YYText/).<br/>
+You can also install documentation locally using [appledoc](https://github.com/tomaz/appledoc).
+
+# Requirements
+
+This library requires `iOS 8.0+` and `Xcode 10.0+`.
+
+# License
+
+BSText is released under the MIT license. See LICENSE file for details.
+
+## <br/><br/>
+
+# 中文介绍
+
+功能强大的 iOS 富文本编辑与显示框架。<br/>
+(该项目是 [YYText](https://github.com/ibireme/YYText) 的 Swift 版本,项目的前缀 'BS' 来自于 BlueSky,就是创作了《冰河世纪》系列电影的 BlueSky 工作室)
+
+# 特性
+
+- API 兼容 UILabel 和 UITextView
+- 支持高性能的异步排版和渲染
+- 扩展了 CoreText 的属性以支持更多文字效果
+- 支持 UIImage、UIView、CALayer 作为图文混排元素
+- 支持添加自定义样式的、可点击的文本高亮范围
+- 支持自定义文本解析 (内置简单的 Markdown/表情解析)
+- 支持文本容器路径、内部留空路径的控制
+- 支持文字竖排版,可用于编辑和显示中日韩文本
+- 支持图片和富文本的复制粘贴
+- 文本编辑时,支持富文本占位符
+- 支持自定义键盘视图
+- 撤销和重做次数的控制
+- 富文本的序列化与反序列化支持
+- 支持多语言,支持 VoiceOver
+- 全部代码都有文档注释
+
+# 架构
+
+本项目架构与 [YYText ](https://github.com/ibireme/YYText) 保持一致
+
+# 文本属性
+
+### BSText 原生支持的属性
+
+<table>
+  <thead>
+    <tr>
+      <th>Demo</th>
+      <th>Attribute Name</th>
+      <th>Class</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextAttachment.gif" width="200"></td>
+      <td>TextAttachment</td>
+      <td>TextAttachment</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextHighlight.gif" width="200"></td>
+      <td>TextHighlight</td>
+      <td>TextHighlight</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBinding.gif" width="200"></td>
+      <td>TextBinding</td>
+      <td>TextBinding</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextShadow.png" width="200"></td>
+      <td>TextShadow<br/>TextInnerShadow</td>
+      <td>TextShadow</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBorder.png" width="200"></td>
+      <td>TextBorder</td>
+      <td>TextBorder</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBackgroundBorder.png" width="200"></td>
+      <td>TextBackgroundBorder</td>
+      <td>TextBorder</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBlockBorder.png" width="200"></td>
+      <td>TextBlockBorder</td>
+      <td>TextBorder</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Obliqueness.png" width="200"></td>
+      <td>TextGlyphTransform</td>
+      <td> NSValue(CGAffineTransform)</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Underline.png" width="200"></td>
+      <td>TextUnderline</td>
+      <td>TextDecoration</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Strikethrough.png" width="200"></td>
+      <td>TextStrickthrough</td>
+      <td>TextDecoration</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/YYText Extended/TextBackedString.png" width="200"></td>
+      <td>TextBackedString</td>
+      <td>TextBackedString</td>
+    </tr>
+  </tbody>
+</table>
+
+
+
+### BSText 支持的 CoreText 属性
+
+<table>
+  <thead>
+    <tr>
+      <th>Demo</th>
+      <th>Attribute Name</th>
+      <th>Class</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Font.png" width="200"></td>
+      <td> Font </td>
+      <td>UIFont(CTFontRef)</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Kern.png" width="200"></td>
+      <td> Kern </td>
+      <td>NSNumber</td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Stroke.png" width="200"></td>
+      <td> StrokeWidth </td>
+      <td> NSNumber </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/StrokeColor.png" width="200"></td>
+      <td> StrokeColor </td>
+      <td> CGColorRef </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Shadow.png" width="200"></td>
+      <td> Shadow </td>
+      <td> NSShadow </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Ligature.png" width="200"></td>
+      <td> Ligature </td>
+      <td> NSNumber </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/VerticalForms.png" width="200"></td>
+      <td> VerticalGlyphForm </td>
+      <td> NSNumber(BOOL) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/WriteDirection.png" width="200"></td>
+      <td> WritingDirection </td>
+      <td> NSArray(NSNumber) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/RunDelegate.png" width="200"></td>
+      <td> RunDelegate </td>
+      <td> CTRunDelegateRef </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/Alignment.png" width="200"></td>
+      <td> TextAlignment </td>
+      <td> NSParagraphStyle <br/>(NSTextAlignment) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/LineBreakMode.png" width="200"></td>
+      <td> LineBreakMode </td>
+      <td> NSParagraphStyle <br/>(NSLineBreakMode) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/LineSpacing.png" width="200"></td>
+      <td> LineSpacing </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/ParagraphSpacing.png" width="200"></td>
+      <td> ParagraphSpacing <br/> ParagraphSpacingBefore </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/FirstLineHeadIndent.png" width="200"></td>
+      <td> FirstLineHeadIndent </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/HeadIndent.png" width="200"></td>
+      <td> HeadIndent </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/TailIndent.png" width="200"></td>
+      <td> TailIndent </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/MinimumLineHeight.png" width="200"></td>
+      <td> MinimumLineHeight </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/MaximumLineHeight.png" width="200"></td>
+      <td> MaximumLineHeight </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/LineHeightMultiple.png" width="200"></td>
+      <td> LineHeightMultiple </td>
+      <td> NSParagraphStyle <br/>(CGFloat) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/BaseWritingDirection.png" width="200"></td>
+      <td> BaseWritingDirection </td>
+      <td> NSParagraphStyle <br/>(NSWritingDirection) </td>
+    </tr>
+    <tr>
+      <td><img src="https://raw.github.com/a1049145827/BSText/master/Attributes/CoreText and TextKit/Paragraph/Tab.png" width="200"></td>
+      <td> DefaultTabInterval <br/> TabStops </td>
+      <td> NSParagraphStyle <br/>CGFloat/NSArray(NSTextTab)</td>
+    </tr>
+  </tbody>
+</table>
+
+
+
+# 用法
+
+### 基本用法
+
+```swift
+// BSLabel (和 UILabel 用法一致)
+let label = BSLabel()
+label.frame = ...
+label.font = ...
+label.textColor = ...
+label.textAlignment = ...
+label.lineBreakMode = ...
+label.numberOfLines = ...
+label.text = ...
+    
+// BSTextView (和 UITextView 用法一致)
+let textView = BSTextView()
+textView.frame = ...
+textView.font = ...
+textView.textColor = ...
+textView.dataDetectorTypes = ...
+textView.placeHolderText = ...
+textView.placeHolderTextColor = ...
+textView.delegate = ...
+
+```
+
+### 属性文本
+
+```swift
+// 1. 创建一个属性文本
+let text = NSMutableAttributedString(string: "Some Text, blabla...")
+    
+// 2. 为文本设置属性
+text.bs_font = UIFont.boldSystemFont(ofSize:30)
+text.bs_color = UIColor.blue
+text.bs_set(color: UIColor.red, range: NSRange(location: 0, length: 4))
+text.bs_lineSpacing = 10
+    
+// 3. 赋值到 BSLabel 或 BSTextView
+let label = BSLabel()
+label.frame = CGRect(x: 15, y: 100, width: 200, height: 80)
+label.attributedText = text
+    
+let textView = BSTextView()
+textView.frame = CGRect(x: 15, y: 200, width: 200, height: 80)
+textView.attributedText = text
+
+```
+
+### 文本高亮
+
+你可以用一些已经封装好的简便方法来设置文本高亮:
+
+```swift
+text.bs_set(textHighlightRange: range,
+            color: UIColor.blue,
+            backgroundColor: UIColor.gray) { (view, text, range, rect) in
+    print("tap text range:...")
+}
+
+```
+
+或者用更复杂的办法来调节文本高亮的细节:
+
+```swift
+// 1. 创建一个"高亮"属性,当用户点击了高亮区域的文本时,"高亮"属性会替换掉原本的属性
+let border = TextBorder.border(with: UIColor.gray, cornerRadius: 3)
+
+let highlight = TextHighlight()
+highlight.color = .white
+highlight.backgroundBorder = highlightBorder
+highlight.tapAction = { (containerView, text, range, rect) in
+    print("tap text range:...")
+    // 你也可以把事件回调放到 BSLabel 和 BSTextView 来处理。
+}
+
+// 2. 把"高亮"属性设置到某个文本范围
+let attributedText = NSMutableAttributedString(string: " ")
+attributedText.bs_set(textHighlight: highlight, range: highlightRange)
+    
+// 3. 把属性文本设置到 BSLabel 或 BSTextView
+let label = BSLabel()
+label.attributedText = attributedText
+
+let textView = BSTextView()
+textView.delegate = self
+textView.attributedText = ...
+    
+// 4. 接受事件回调
+label.highlightTapAction = { (containerView, text, range, rect) in
+    print("tap text range:...")
+};
+label.highlightLongPressAction = { (containerView, text, range, rect) in
+    print("tap text range:...")
+};
+
+// MARK: - TextViewDelegate
+func textView(_ textView: BSTextView, didTap highlight: TextHighlight, in characterRange: NSRange, rect: CGRect) {
+    print("tap text range:...")
+}
+func textView(_ textView: BSTextView, didLongPress highlight: TextHighlight, in characterRange: NSRange, rect: CGRect) {
+    print("tap text range:...")
+}
+
+```
+
+### 图文混排
+
+```swift
+let text = NSMutableAttributedString()
+let font = UIFont.systemFont(ofSize: 16)
+	
+// 嵌入 UIImage
+let image = UIImage.init(named: "dribbble64_imageio")
+guard let attachment = NSMutableAttributedString.bs_attachmentString(with: image, contentMode: .center, attachmentSize: image?.size ?? .zero, alignTo: font, alignment: .center) else {
+    return
+}
+text.append(attachment)
+	
+// 嵌入 UIView
+let switcher = UISwitch()
+switcher.sizeToFit()
+guard let attachment1 = NSMutableAttributedString.bs_attachmentString(with: switcher, contentMode: .center, attachmentSize: switcher.frame.size, alignTo: font, alignment: .center) else {
+    return
+}
+text.append(attachment1)
+	
+// 嵌入 CALayer
+let layer = CAShapeLayer()
+layer.path = ...
+guard let attachment2 = NSMutableAttributedString.bs_attachmentString(with: layer, contentMode: .center, attachmentSize: layer.frame.size, alignTo: font, alignment: .center) else {
+    return
+}
+text.append(attachment2)
+
+```
+
+### 文本布局计算
+
+```swift
+let text = NSAttributedString()
+let size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude)
+let container = TextContainer()
+container.size = size
+guard let layout = TextLayout(container: container, text: text) else {
+    return
+}
+	
+// 获取文本显示位置和大小
+layout.textBoundingRect // get bounding rect
+layout.textBoundingSize // get bounding size
+	
+ // 查询文本排版结果
+layout.lineIndex(for: CGPoint(x: 10, y: 10))
+layout.closestLineIndex(for: CGPoint(x: 10, y: 10))
+layout.closestPosition(to: CGPoint(x: 10, y: 10))
+layout.textRange(at: CGPoint(x: 10, y: 10))
+layout.rect(for: TextRange(range: NSRange(location: 10, length: 2)))
+layout.selectionRects(for: TextRange(range: NSRange(location: 10, length: 2)))
+	
+// 显示文本排版结果
+let label = BSLabel()
+label.frame = CGRect(x: 0, y: 0, width: layout.textBoundingSize.width, height: layout.textBoundingSize.height)
+label.textLayout = layout;
+
+```
+
+### 文本行位置调整
+
+```swift
+// 由于中文、英文、Emoji 等字体高度不一致,或者富文本中出现了不同字号的字体,
+// 可能会造成每行文字的高度不一致。这里可以添加一个修改器来实现固定行高,或者自定义文本行位置。
+  
+// 简单的方法:
+// 1. 创建一个文本行位置修改类,实现 `TextLinePositionModifier` 协议。
+// 2. 设置到 Label 或 TextView。
+
+let modifier = TextLinePositionSimpleModifier()
+modifier.fixedLineHeight = 24
+  
+let label = BSLabel()
+label.linePositionModifier = modifier
+
+// 完全控制:
+let modifier = TextLinePositionSimpleModifier()
+modifier.fixedLineHeight = 24
+  
+let container = TextContainer()
+container.size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude)
+container.linePositionModifier = modifier
+  
+guard let layout = TextLayout(container: container, text: text) else {
+    return
+}
+let label = BSLabel()
+label.size = layout.textBoundingSize
+label.textLayout = layout
+
+```
+
+### 异步排版和渲染
+
+```swift  
+// 如果你在显示字符串时有性能问题,可以这样开启异步模式:
+let label = BSLabel()
+label.displaysAsynchronously = true
+    
+// 如果需要获得最高的性能,你可以在后台线程用 `TextLayout` 进行预排版: 
+let label = BSLabel()
+label.displaysAsynchronously = true // 开启异步绘制
+label.ignoreCommonProperties = true // 忽略除了 textLayout 之外的其他属性
+
+DispatchQueue.global().async {
+    // 创建属性字符串
+    let text = NSMutableAttributedString(string: "Some Text")
+    text.bs_font = UIFont.systemFont(ofSize: 16)
+    text.bs_color = UIColor.gray
+    text.bs_set(color: .red, range: NSRange(location: 0, length: 4))
+
+    // 创建文本容器
+    let container = TextContainer()
+    container.size = CGSize(width: 100, height: CGFloat.greatestFiniteMagnitude);
+    container.maximumNumberOfRows = 0;
+
+    // 生成排版结果
+    let layout = TextLayout(container: container, text: text)
+
+    DispatchQueue.main.async {
+        label.frame = CGRect(x: 0, y: 0, width: layout.textBoundingSize.width, height: layout.textBoundingSize.height)
+        label.textLayout = layout;
+    }
+}
+
+```
+
+### 文本容器控制
+
+```swift
+let label = BSLabel()
+label.textContainerPath = UIBezierPath(...)
+label.exclusionPaths = [UIBezierPath(), ...]
+label.textContainerInset = UIEdgeInsets(...)
+label.verticalForm = true/false
+    
+let textView = BSTextView()
+textView.exclusionPaths = [UIBezierPath(), ...]
+textView.textContainerInset = UIEdgeInsets(...)
+textView.verticalForm = true/false
+
+```
+
+### 文本解析
+
+```swift
+// 1. 创建一个解析器
+	
+// 内置简单的表情解析
+let simpleEmoticonParser = TextSimpleEmoticonParser()
+var mapper = [String: UIImage]()
+mapper[":smile:"] = UIImage.init(named: "smile.png")
+mapper[":cool:"] = UIImage.init(named: "cool.png")
+mapper[":cry:"] = UIImage.init(named: "cry.png")
+mapper[":wink:"] = UIImage.init(named: "wink.png")
+simpleEmoticonParser.emoticonMapper = mapper;
+	
+// 内置简单的 markdown 解析
+let markdownParser = TextSimpleMarkdownParser()
+markdownParser.setColorWithDarkTheme()
+    
+// 实现 `TextParser` 协议的自定义解析器
+let parser = MyCustomParser()
+    
+// 2. 把解析器添加到 BSLabel 或 BSTextView
+let label = BSLabel()
+label.textParser = parser
+
+let textView = BSTextView()
+textView.textParser = parser
+
+```
+
+### Debug
+
+```swift
+// 设置一个全局的 debug option 来显示排版结果。
+let debugOption = TextDebugOption()
+debugOption.baselineColor = .red
+debugOption.ctFrameBorderColor = .red
+debugOption.ctLineFillColor = UIColor(red: 0, green: 0.463, blue: 1, alpha: 0.18)
+debugOption.cgGlyphBorderColor = UIColor(red: 1, green: 0.524, blue: 0, alpha: 0.2)
+TextDebugOption.setSharedDebugOption(debugOption)
+
+```
+
+### 更多示例
+
+查看演示工程 `Demo/BSTextDemo.xcodeproj`:
+
+<img src="https://raw.github.com/a1049145827/BSText/master/Demo/DemoSnapshot/text_path.gif" width="320">
+<img src="https://raw.github.com/a1049145827/BSText/master/Demo/DemoSnapshot/text_markdown.gif" width="320">
+<br/> <br/>
+<img src="https://raw.github.com/a1049145827/BSText/master/Demo/DemoSnapshot/text_vertical.gif" width="320">
+<img src="https://raw.github.com/a1049145827/BSText/master/Demo/DemoSnapshot/text_paste.gif" width="320">
+
+# 安装
+
+### CocoaPods
+
+1. 在 Podfile 中添加 `pod 'BSText'`。
+
+   ```
+   source 'https://github.com/CocoaPods/Specs.git'
+   platform :ios, '8.0'
+   use_frameworks!
+   
+   target 'MyApp' do
+     # your other pod
+     # ...
+     pod 'BSText', '~> 1.0'
+   end
+   
+   ```
+
+2. 执行 `pod install` 或 `pod update`。
+
+3. 导入模块 `import BSText`,OC 项目中使用 `@import BSText;`。
+
+### Carthage
+
+1. 在 Cartfile 中添加 `github "a1049145827/BSText"`。
+2. 执行 `carthage update --platform ios` 并将生成的 framework 添加到你的工程。
+3. 导入模块 `import BSText`,OC 项目中使用 `@import BSText;`。
+
+### 手动安装
+
+1. 下载 BSText 文件夹内的所有内容。
+2. 将 BSText 内的源文件添加(拖放)到你的工程。
+3. 链接以下 frameworks:
+   - UIKit
+   - CoreFoundation
+   - CoreText
+   - QuartzCore
+   - Accelerate
+   - MobileCoreServices
+4. 导入模块 `import BSText`,OC 项目中使用 `@import BSText;`。
+
+### 注意
+
+你可以添加 [YYImage](https://github.com/ibireme/YYImage) 或 [YYWebImage](https://github.com/ibireme/YYWebImage) 到你的工程,以支持动画格式(GIF/APNG/WebP)的图片。
+
+# 文档
+
+本项目目前还没有生成在线文档,你可以在 [CocoaDocs](http://cocoadocs.org/docsets/YYText/) 查看 YYText 的在线 API 文档,也可以用 [appledoc](https://github.com/tomaz/appledoc) 本地生成文档。
+
+# 系统要求
+
+该项目最低支持 `iOS 8.0` 和 `Xcode 10.0`。
+
+# 已知问题
+
+- 与 YYText 一样,BSText 并不能支持所有 CoreText/TextKit 的属性,比如 NSBackgroundColor、NSStrikethrough、NSUnderline、NSAttachment、NSLink 等,但 BSText 中基本都有对应属性作为替代。详情见上方表格。
+- BSTextView 未实现局部刷新,所以在输入和编辑大量的文本(比如超过大概五千个汉字、或大概一万个英文字符)时会出现较明显的卡顿现象。
+- 竖排版时,添加 exclusionPaths 在少数情况下可能会导致文本显示空白。
+- 当添加了非矩形的 textContainerPath,并且有嵌入大于文本排版方向宽度的 RunDelegate 时,RunDelegate 之后的文字会无法显示。这是 CoreText 的 Bug(或者说是 Feature)。
+
+# 许可证
+
+BSText 使用 MIT 许可证,详情见 LICENSE 文件。
+
+
+
+
+

+ 17 - 5
Pods/Manifest.lock

@@ -1,40 +1,52 @@
 PODS:
+  - BSText (1.1.3):
+    - YYImage
   - IQKeyboardManagerSwift (6.5.12)
   - Kingfisher (7.10.0)
   - MJRefresh (3.7.5)
   - ObjectMapper (4.2.0)
   - SnapKit (5.7.1)
-  - Toast-Swift (5.1.1)
+  - SVProgressHUD (2.3.1):
+    - SVProgressHUD/Core (= 2.3.1)
+  - SVProgressHUD/Core (2.3.1)
   - TYCyclePagerView (1.2.0)
+  - YYImage (1.0.4):
+    - YYImage/Core (= 1.0.4)
+  - YYImage/Core (1.0.4)
 
 DEPENDENCIES:
+  - BSText
   - IQKeyboardManagerSwift (= 6.5.12)
   - Kingfisher (= 7.10.0)
   - MJRefresh (= 3.7.5)
   - ObjectMapper (= 4.2)
   - SnapKit
-  - Toast-Swift
+  - SVProgressHUD
   - TYCyclePagerView
 
 SPEC REPOS:
   trunk:
+    - BSText
     - IQKeyboardManagerSwift
     - Kingfisher
     - MJRefresh
     - ObjectMapper
     - SnapKit
-    - Toast-Swift
+    - SVProgressHUD
     - TYCyclePagerView
+    - YYImage
 
 SPEC CHECKSUMS:
+  BSText: fde17ab2d7b591745f73a408de0eeed063009b6a
   IQKeyboardManagerSwift: 371b08cb39664fb56030f5345c815a4ffc74bbc0
   Kingfisher: a18f05d3b6d37d8650ee4a3e61d57a28fc6207f6
   MJRefresh: fdf5e979eb406a0341468932d1dfc8b7f9fce961
   ObjectMapper: 1eb41f610210777375fa806bf161dc39fb832b81
   SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
-  Toast-Swift: 7a03a532afe3a560d4044bc7c237e2864d295173
+  SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22
   TYCyclePagerView: 2b051dade0615c70784aa34f40c646feeddb7344
+  YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
 
-PODFILE CHECKSUM: d845390dcb5d2a1807fc43839de70d8677485883
+PODFILE CHECKSUM: ffe07205f591638b00c11baae131cf7d7b5a44f5
 
 COCOAPODS: 1.16.2

Fișier diff suprimat deoarece este prea mare
+ 659 - 527
Pods/Pods.xcodeproj/project.pbxproj


+ 3 - 3
Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/Toast-Swift.xcscheme → Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/BSText.xcscheme

@@ -14,9 +14,9 @@
             buildForAnalyzing = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B990BD87169C76A3ED3FE8A9258D91A3"
-               BuildableName = "Toast_Swift.framework"
-               BlueprintName = "Toast-Swift"
+               BlueprintIdentifier = "618B6C5209A8B455CDBCBB0BA0108D8F"
+               BuildableName = "BSText.framework"
+               BlueprintName = "BSText"
                ReferencedContainer = "container:Pods.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>

+ 3 - 3
Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/Toast-Swift-Toast-Swift.xcscheme → Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/SVProgressHUD.xcscheme

@@ -14,9 +14,9 @@
             buildForAnalyzing = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C344E85893FCDEE5B70436E0A4A3472B"
-               BuildableName = "Toast-Swift.bundle"
-               BlueprintName = "Toast-Swift-Toast-Swift"
+               BlueprintIdentifier = "1C8D67D8B72D6BA42CCEDB648537A340"
+               BuildableName = "SVProgressHUD.framework"
+               BlueprintName = "SVProgressHUD"
                ReferencedContainer = "container:Pods.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>

+ 58 - 0
Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/YYImage.xcscheme

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1600"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "822E44240F2922DAB12018A6B649BD19"
+               BuildableName = "YYImage.framework"
+               BlueprintName = "YYImage"
+               ReferencedContainer = "container:Pods.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 10 - 27
Pods/Pods.xcodeproj/xcuserdata/100years.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -4,82 +4,65 @@
 <dict>
 	<key>SchemeUserState</key>
 	<dict>
+		<key>BSText.xcscheme</key>
+		<dict>
+			<key>isShown</key>
+			<false/>
+		</dict>
 		<key>IQKeyboardManagerSwift.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>0</integer>
 		</dict>
 		<key>Kingfisher-Kingfisher.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>2</integer>
 		</dict>
 		<key>Kingfisher.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>1</integer>
 		</dict>
 		<key>MJRefresh.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>3</integer>
 		</dict>
 		<key>ObjectMapper.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>4</integer>
 		</dict>
 		<key>Pods-TSLiveWallpaper.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>5</integer>
 		</dict>
-		<key>SnapKit-SnapKit_Privacy.xcscheme</key>
+		<key>SVProgressHUD.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>7</integer>
 		</dict>
-		<key>SnapKit.xcscheme</key>
+		<key>SnapKit-SnapKit_Privacy.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>6</integer>
 		</dict>
-		<key>TYCyclePagerView.xcscheme</key>
+		<key>SnapKit.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>10</integer>
 		</dict>
-		<key>Toast-Swift-Toast-Swift.xcscheme</key>
+		<key>TYCyclePagerView.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>9</integer>
 		</dict>
-		<key>Toast-Swift.xcscheme</key>
+		<key>YYImage.xcscheme</key>
 		<dict>
 			<key>isShown</key>
 			<false/>
-			<key>orderHint</key>
-			<integer>8</integer>
 		</dict>
 	</dict>
 	<key>SuppressBuildableAutocreation</key>

+ 21 - 0
Pods/SVProgressHUD/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2011-2023 Sam Vermette, Tobias Totzek and contributors.
+
+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.

+ 221 - 0
Pods/SVProgressHUD/README.md

@@ -0,0 +1,221 @@
+# SVProgressHUD
+
+![Pod Version](https://img.shields.io/cocoapods/v/SVProgressHUD.svg?style=flat)
+![Pod Platform](https://img.shields.io/cocoapods/p/SVProgressHUD.svg?style=flat)
+![Pod License](https://img.shields.io/cocoapods/l/SVProgressHUD.svg?style=flat)
+[![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://swift.org/package-manager/)
+[![CocoaPods compatible](https://img.shields.io/badge/CocoaPods-compatible-green.svg?style=flat)](https://cocoapods.org)
+[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-green.svg?style=flat)](https://github.com/Carthage/Carthage)
+
+`SVProgressHUD` is a clean and easy-to-use HUD meant to display the progress of an ongoing task on iOS and tvOS.
+
+![SVProgressHUD](https://raw.githubusercontent.com/SVProgressHUD/SVProgressHUD/master/Images/SVProgressHUD.png)
+
+## Installation
+
+### Swift Package Manager
+
+[Swift Package Manager](https://swift.org/package-manager/) (SwiftPM) is a tool for managing the distribution of Swift code. It simplifies the process of managing Swift package dependencies.
+
+To integrate `SVProgressHUD` into your project using SwiftPM:
+
+1. In Xcode, select **File > Add Package Dependency**.
+2. Enter the following package repository URL: https://github.com/SVProgressHUD/SVProgressHUD.git
+3. Choose the appropriate version (e.g. a specific version, branch, or commit).
+4. Add `SVProgressHUD` to your target dependencies.
+
+`SVProgressHUD` requires at least Swift tools version 5.3.
+
+### From CocoaPods
+
+[CocoaPods](http://cocoapods.org) is a dependency manager for Objective-C, which automates and simplifies the process of using 3rd-party libraries like `SVProgressHUD` in your projects. First, add the following line to your [Podfile](http://guides.cocoapods.org/using/using-cocoapods.html):
+
+```ruby
+pod 'SVProgressHUD'
+```
+
+If you want to use the latest features of `SVProgressHUD` use normal external source dependencies.
+
+```ruby
+pod 'SVProgressHUD', :git => 'https://github.com/SVProgressHUD/SVProgressHUD.git'
+```
+
+This pulls from the `master` branch directly.
+
+Second, install `SVProgressHUD` into your project:
+
+```ruby
+pod install
+```
+
+### Carthage
+
+[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate `SVProgressHUD` into your Xcode project using Carthage, specify it in your `Cartfile`:
+
+```ogdl
+github "SVProgressHUD/SVProgressHUD"
+```
+
+Run `carthage bootstrap` to build the framework in your repository's Carthage directory. You can then include it in your target's `carthage copy-frameworks` build phase. For more information on this, please see [Carthage's documentation](https://github.com/carthage/carthage#if-youre-building-for-ios-tvos-or-watchos).
+
+### Manually
+
+* Drag the `SVProgressHUD/SVProgressHUD` folder into your project.
+* Take care that `SVProgressHUD.bundle` is added to `Targets->Build Phases->Copy Bundle Resources`.
+* Add the **QuartzCore** framework to your project.
+
+## Swift
+
+Even though `SVProgressHUD` is written in Objective-C, it can be used in Swift with no hassle.
+
+If you use [CocoaPods](http://cocoapods.org) add the following line to your [Podfile](http://guides.cocoapods.org/using/using-cocoapods.html):
+
+```ruby
+use_frameworks!
+```
+
+If you added `SVProgressHUD` manually, just add a [bridging header](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html) file to your project with the `SVProgressHUD` header included.
+
+## Usage
+
+(see sample Xcode project in `/Demo`)
+
+`SVProgressHUD` is created as a singleton (i.e. it doesn't need to be explicitly allocated and instantiated; you directly call `[SVProgressHUD method]` / `SVProgressHUD.method()`).
+
+**Use `SVProgressHUD` wisely! Only use it if you absolutely need to perform a task before taking the user forward. Bad use case examples: pull to refresh, infinite scrolling, sending message.**
+
+Using `SVProgressHUD` in your app will usually look as simple as this.
+
+**Objective-C:**
+
+```objective-c
+[SVProgressHUD show];
+dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+    // time-consuming task
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [SVProgressHUD dismiss];
+    });
+});
+```
+
+**Swift:**
+
+```swift
+SVProgressHUD.show()
+DispatchQueue.global(qos: .default).async {
+    // time-consuming task
+    DispatchQueue.main.async {
+        SVProgressHUD.dismiss()
+    }
+}
+```
+
+### Showing the HUD
+
+You can show the status of indeterminate tasks using one of the following:
+
+```objective-c
++ (void)show;
++ (void)showWithStatus:(NSString*)string;
+```
+
+If you'd like the HUD to reflect the progress of a task, use one of these:
+
+```objective-c
++ (void)showProgress:(CGFloat)progress;
++ (void)showProgress:(CGFloat)progress status:(NSString*)status;
+```
+
+### Dismissing the HUD
+
+The HUD can be dismissed using:
+
+```objective-c
++ (void)dismiss;
++ (void)dismissWithDelay:(NSTimeInterval)delay;
+```
+
+If you'd like to stack HUDs, you can balance out every show call using:
+
+```
++ (void)popActivity;
+```
+
+The HUD will get dismissed once the `popActivity` calls will match the number of show calls.
+
+Or show an image with status before getting dismissed a little bit later. The display time depends on `minimumDismissTimeInterval` and the length of the given string.
+
+```objective-c
++ (void)showInfoWithStatus:(NSString*)string;
++ (void)showSuccessWithStatus:(NSString*)string;
++ (void)showErrorWithStatus:(NSString*)string;
++ (void)showImage:(UIImage*)image status:(NSString*)string;
+```
+
+## Customization
+
+`SVProgressHUD` is designed with flexibility in mind, providing a myriad of customization options to fit the look and feel of your application seamlessly.
+
+* Appearance: Make use of the `UI_APPEARANCE_SELECTOR` to adjust styles, colors, fonts, size, and images app-wide.
+* Behavior: Control visibility durations, display delays, and animation speeds.
+* Feedback: Enhance the user experience with options for haptic feedback and motion effects.
+
+For a comprehensive list of properties and detailed explanations, refer to the `SVProgressHUD.h` file in the API documentation.
+
+### Hint
+
+As standard `SVProgressHUD` offers three preconfigured styles:
+
+* `SVProgressHUDStyleAutomatic`: Automatically switch between the light and dark style
+* `SVProgressHUDStyleLight`: White background with black spinner and text
+* `SVProgressHUDStyleDark`: Black background with white spinner and text
+
+If you want to use custom colors use `setForegroundColor:` and/or `setBackgroundColor:`. These implicitly set the HUD's style to `SVProgressHUDStyleCustom`.
+
+## Haptic Feedback
+
+Available on iPhone 7 and newer, `SVProgressHUD` can automatically trigger haptic feedback depending on which HUD is being displayed. The feedback maps as follows:
+
+* `showSuccessWithStatus:` <-> `UINotificationFeedbackTypeSuccess`
+* `showInfoWithStatus:` <-> `UINotificationFeedbackTypeWarning`
+* `showErrorWithStatus:` <-> `UINotificationFeedbackTypeError`
+
+To enable this functionality, use `setHapticsEnabled:`.
+
+## Notifications
+
+`SVProgressHUD` posts four notifications via `NSNotificationCenter` in response to being shown/dismissed:
+
+* `SVProgressHUDWillAppearNotification` when the show animation starts
+* `SVProgressHUDDidAppearNotification` when the show animation completes
+* `SVProgressHUDWillDisappearNotification` when the dismiss animation starts
+* `SVProgressHUDDidDisappearNotification` when the dismiss animation completes
+
+Each notification passes a `userInfo` dictionary holding the HUD's status string (if any), retrievable via `SVProgressHUDStatusUserInfoKey`.
+
+`SVProgressHUD` also posts `SVProgressHUDDidReceiveTouchEventNotification` when users touch on the overall screen or `SVProgressHUDDidTouchDownInsideNotification` when a user touches on the HUD directly. For these notifications `userInfo` is not passed but the object parameter contains the `UIEvent` that related to the touch.
+
+## App Extensions
+
+When using `SVProgressHUD` in an App Extension, `#define SV_APP_EXTENSIONS` to avoid using unavailable APIs. This will be done automatically when using the `AppExtension` CocoaPods subspec. Additionally, call `setViewForExtension:` from your extensions view controller with `self.view`.
+
+## Contributing to this project
+
+If you have feature requests or bug reports, feel free to help out by sending pull requests or by [creating new issues](https://github.com/SVProgressHUD/SVProgressHUD/issues/new). Please take a moment to
+review the guidelines written by [Nicolas Gallagher](https://github.com/necolas):
+
+* [Bug reports](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md#bugs)
+* [Feature requests](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md#features)
+* [Pull requests](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md#pull-requests)
+
+## License
+
+`SVProgressHUD` is distributed under the terms and conditions of the [MIT license](https://github.com/SVProgressHUD/SVProgressHUD/blob/master/LICENSE). The success, error and info icons used on iOS 12 are made by [Freepik](http://www.freepik.com) from [Flaticon](https://www.flaticon.com) and are licensed under [Creative Commons BY 3.0](https://creativecommons.org/licenses/by/3.0/).
+
+## Privacy
+
+`SVProgressHUD` does not collect any data. A [privacy manifest file](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files) is [provided](SVProgressHUD/PrivacyInfo.xcprivacy).
+
+## Credits
+
+`SVProgressHUD` is brought to you by Sam Vermette, [Tobias Totzek](https://totzek.me) and [contributors to the project](https://github.com/SVProgressHUD/SVProgressHUD/contributors). If you're using `SVProgressHUD` in your project, attribution would be very appreciated.

+ 1 - 1
Pods/Toast-Swift/Toast/Resources/PrivacyInfo.xcprivacy → Pods/SVProgressHUD/SVProgressHUD/PrivacyInfo.xcprivacy

@@ -11,4 +11,4 @@
 	<key>NSPrivacyAccessedAPITypes</key>
 	<array/>
 </dict>
-</plist>
+</plist>

+ 17 - 0
Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.h

@@ -0,0 +1,17 @@
+//
+//  SVIndefiniteAnimatedView.h
+//  SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD
+//
+//  Copyright (c) 2014-2023 Guillaume Campagna and contributors. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface SVIndefiniteAnimatedView : UIView
+
+@property (nonatomic, assign) CGFloat strokeThickness;
+@property (nonatomic, assign) CGFloat radius;
+@property (nonatomic, strong) UIColor *strokeColor;
+
+@end
+

+ 142 - 0
Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.m

@@ -0,0 +1,142 @@
+//
+//  SVIndefiniteAnimatedView.m
+//  SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD
+//
+//  Copyright (c) 2014-2023 Guillaume Campagna and contributors. All rights reserved.
+//
+
+#import "SVIndefiniteAnimatedView.h"
+#import "SVProgressHUD.h"
+
+@interface SVIndefiniteAnimatedView ()
+
+@property (nonatomic, strong) CAShapeLayer *indefiniteAnimatedLayer;
+
+@end
+
+@implementation SVIndefiniteAnimatedView
+
+- (void)willMoveToSuperview:(UIView*)newSuperview {
+    if (newSuperview) {
+        [self layoutAnimatedLayer];
+    } else {
+        [_indefiniteAnimatedLayer removeFromSuperlayer];
+        _indefiniteAnimatedLayer = nil;
+    }
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+
+    [self layoutAnimatedLayer];
+}
+
+- (void)layoutAnimatedLayer {
+    CALayer *layer = self.indefiniteAnimatedLayer;
+
+    if (!layer.superlayer) {
+        [self.layer addSublayer:layer];
+    }
+    
+    CGFloat widthDiff = CGRectGetWidth(self.bounds) - CGRectGetWidth(layer.bounds);
+    CGFloat heightDiff = CGRectGetHeight(self.bounds) - CGRectGetHeight(layer.bounds);
+    layer.position = CGPointMake(CGRectGetWidth(self.bounds) - CGRectGetWidth(layer.bounds) / 2 - widthDiff / 2, CGRectGetHeight(self.bounds) - CGRectGetHeight(layer.bounds) / 2 - heightDiff / 2);
+}
+
+- (CAShapeLayer*)indefiniteAnimatedLayer {
+    if(!_indefiniteAnimatedLayer) {
+        CGPoint arcCenter = CGPointMake(self.radius+self.strokeThickness/2+5, self.radius+self.strokeThickness/2+5);
+        UIBezierPath* smoothedPath = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:self.radius startAngle:(CGFloat) (M_PI*3/2) endAngle:(CGFloat) (M_PI/2+M_PI*5) clockwise:YES];
+        
+        _indefiniteAnimatedLayer = [CAShapeLayer layer];
+        _indefiniteAnimatedLayer.contentsScale = [[UIScreen mainScreen] scale];
+        _indefiniteAnimatedLayer.frame = CGRectMake(0.0f, 0.0f, arcCenter.x*2, arcCenter.y*2);
+        _indefiniteAnimatedLayer.fillColor = [UIColor clearColor].CGColor;
+        _indefiniteAnimatedLayer.strokeColor = self.strokeColor.CGColor;
+        _indefiniteAnimatedLayer.lineWidth = self.strokeThickness;
+        _indefiniteAnimatedLayer.lineCap = kCALineCapRound;
+        _indefiniteAnimatedLayer.lineJoin = kCALineJoinBevel;
+        _indefiniteAnimatedLayer.path = smoothedPath.CGPath;
+        
+        CALayer *maskLayer = [CALayer layer];
+        
+        NSBundle *imageBundle = [SVProgressHUD imageBundle];
+        
+        maskLayer.contents = (__bridge id)[[UIImage imageNamed:@"angle-mask.png" inBundle:imageBundle compatibleWithTraitCollection:nil] CGImage];
+        maskLayer.frame = _indefiniteAnimatedLayer.bounds;
+        _indefiniteAnimatedLayer.mask = maskLayer;
+        
+        NSTimeInterval animationDuration = 1;
+        CAMediaTimingFunction *linearCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
+        
+        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
+        animation.fromValue = (id) 0;
+        animation.toValue = @(M_PI*2);
+        animation.duration = animationDuration;
+        animation.timingFunction = linearCurve;
+        animation.removedOnCompletion = NO;
+        animation.repeatCount = INFINITY;
+        animation.fillMode = kCAFillModeForwards;
+        animation.autoreverses = NO;
+        [_indefiniteAnimatedLayer.mask addAnimation:animation forKey:@"rotate"];
+        
+        CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
+        animationGroup.duration = animationDuration;
+        animationGroup.repeatCount = INFINITY;
+        animationGroup.removedOnCompletion = NO;
+        animationGroup.timingFunction = linearCurve;
+        
+        CABasicAnimation *strokeStartAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
+        strokeStartAnimation.fromValue = @0.015;
+        strokeStartAnimation.toValue = @0.515;
+        
+        CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
+        strokeEndAnimation.fromValue = @0.485;
+        strokeEndAnimation.toValue = @0.985;
+        
+        animationGroup.animations = @[strokeStartAnimation, strokeEndAnimation];
+        [_indefiniteAnimatedLayer addAnimation:animationGroup forKey:@"progress"];
+        
+    }
+    return _indefiniteAnimatedLayer;
+}
+
+- (void)setFrame:(CGRect)frame {
+    if(!CGRectEqualToRect(frame, super.frame)) {
+        [super setFrame:frame];
+        
+        if(self.superview) {
+            [self layoutAnimatedLayer];
+        }
+    }
+    
+}
+
+- (void)setRadius:(CGFloat)radius {
+    if(radius != _radius) {
+        _radius = radius;
+        
+        [_indefiniteAnimatedLayer removeFromSuperlayer];
+        _indefiniteAnimatedLayer = nil;
+        
+        if(self.superview) {
+            [self layoutAnimatedLayer];
+        }
+    }
+}
+
+- (void)setStrokeColor:(UIColor*)strokeColor {
+    _strokeColor = strokeColor;
+    _indefiniteAnimatedLayer.strokeColor = strokeColor.CGColor;
+}
+
+- (void)setStrokeThickness:(CGFloat)strokeThickness {
+    _strokeThickness = strokeThickness;
+    _indefiniteAnimatedLayer.lineWidth = _strokeThickness;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    return CGSizeMake((self.radius+self.strokeThickness/2+5)*2, (self.radius+self.strokeThickness/2+5)*2);
+}
+
+@end

+ 17 - 0
Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.h

@@ -0,0 +1,17 @@
+//
+//  SVProgressAnimatedView.h
+//  SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD
+//
+//  Copyright (c) 2017-2023 Tobias Totzek and contributors. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface SVProgressAnimatedView : UIView
+
+@property (nonatomic, assign) CGFloat radius;
+@property (nonatomic, assign) CGFloat strokeThickness;
+@property (nonatomic, strong) UIColor *strokeColor;
+@property (nonatomic, assign) CGFloat strokeEnd;
+
+@end

+ 96 - 0
Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.m

@@ -0,0 +1,96 @@
+//
+//  SVProgressAnimatedView.m
+//  SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD
+//
+//  Copyright (c) 2017-2023 Tobias Totzek and contributors. All rights reserved.
+//
+
+#import "SVProgressAnimatedView.h"
+
+@interface SVProgressAnimatedView ()
+
+@property (nonatomic, strong) CAShapeLayer *ringAnimatedLayer;
+
+@end
+
+@implementation SVProgressAnimatedView
+
+- (void)willMoveToSuperview:(UIView*)newSuperview {
+    if (newSuperview) {
+        [self layoutAnimatedLayer];
+    } else {
+        [_ringAnimatedLayer removeFromSuperlayer];
+        _ringAnimatedLayer = nil;
+    }
+}
+
+- (void)layoutAnimatedLayer {
+    CALayer *layer = self.ringAnimatedLayer;
+    [self.layer addSublayer:layer];
+    
+    CGFloat widthDiff = CGRectGetWidth(self.bounds) - CGRectGetWidth(layer.bounds);
+    CGFloat heightDiff = CGRectGetHeight(self.bounds) - CGRectGetHeight(layer.bounds);
+    layer.position = CGPointMake(CGRectGetWidth(self.bounds) - CGRectGetWidth(layer.bounds) / 2 - widthDiff / 2, CGRectGetHeight(self.bounds) - CGRectGetHeight(layer.bounds) / 2 - heightDiff / 2);
+}
+
+- (CAShapeLayer*)ringAnimatedLayer {
+    if(!_ringAnimatedLayer) {
+        CGPoint arcCenter = CGPointMake(self.radius+self.strokeThickness/2+5, self.radius+self.strokeThickness/2+5);
+        UIBezierPath* smoothedPath = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:self.radius startAngle:(CGFloat)-M_PI_2 endAngle:(CGFloat) (M_PI + M_PI_2) clockwise:YES];
+        
+        _ringAnimatedLayer = [CAShapeLayer layer];
+        _ringAnimatedLayer.contentsScale = [[UIScreen mainScreen] scale];
+        _ringAnimatedLayer.frame = CGRectMake(0.0f, 0.0f, arcCenter.x*2, arcCenter.y*2);
+        _ringAnimatedLayer.fillColor = [UIColor clearColor].CGColor;
+        _ringAnimatedLayer.strokeColor = self.strokeColor.CGColor;
+        _ringAnimatedLayer.lineWidth = self.strokeThickness;
+        _ringAnimatedLayer.lineCap = kCALineCapRound;
+        _ringAnimatedLayer.lineJoin = kCALineJoinBevel;
+        _ringAnimatedLayer.path = smoothedPath.CGPath;
+    }
+    return _ringAnimatedLayer;
+}
+
+- (void)setFrame:(CGRect)frame {
+    if(!CGRectEqualToRect(frame, super.frame)) {
+        [super setFrame:frame];
+        
+        if(self.superview) {
+            [self layoutAnimatedLayer];
+        }
+    }
+}
+
+- (void)setRadius:(CGFloat)radius {
+    if(radius != _radius) {
+        _radius = radius;
+        
+        [_ringAnimatedLayer removeFromSuperlayer];
+        _ringAnimatedLayer = nil;
+        
+        if(self.superview) {
+            [self layoutAnimatedLayer];
+        }
+    }
+}
+
+- (void)setStrokeColor:(UIColor*)strokeColor {
+    _strokeColor = strokeColor;
+    _ringAnimatedLayer.strokeColor = strokeColor.CGColor;
+}
+
+- (void)setStrokeThickness:(CGFloat)strokeThickness {
+    _strokeThickness = strokeThickness;
+    _ringAnimatedLayer.lineWidth = _strokeThickness;
+}
+
+- (void)setStrokeEnd:(CGFloat)strokeEnd {
+    _strokeEnd = strokeEnd;
+    _ringAnimatedLayer.strokeEnd = _strokeEnd;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    return CGSizeMake((self.radius+self.strokeThickness/2+5)*2, (self.radius+self.strokeThickness/2+5)*2);
+}
+
+@end

BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@2x.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@3x.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@2x.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@3x.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@2x.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@3x.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@2x.png


BIN
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@3x.png


+ 392 - 0
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.h

@@ -0,0 +1,392 @@
+//
+//  SVProgressHUD.h
+//  SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD
+//
+//  Copyright (c) 2011-2023 Sam Vermette and contributors. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import <AvailabilityMacros.h>
+
+extern NSString * _Nonnull const SVProgressHUDDidReceiveTouchEventNotification;
+extern NSString * _Nonnull const SVProgressHUDDidTouchDownInsideNotification;
+extern NSString * _Nonnull const SVProgressHUDWillDisappearNotification;
+extern NSString * _Nonnull const SVProgressHUDDidDisappearNotification;
+extern NSString * _Nonnull const SVProgressHUDWillAppearNotification;
+extern NSString * _Nonnull const SVProgressHUDDidAppearNotification;
+
+extern NSString * _Nonnull const SVProgressHUDStatusUserInfoKey;
+
+/// Represents the appearance style of the HUD.
+typedef NS_ENUM(NSInteger, SVProgressHUDStyle) {
+    /// White HUD with black text. HUD background will be blurred.
+    SVProgressHUDStyleLight NS_SWIFT_NAME(light),
+    
+    /// Black HUD with white text. HUD background will be blurred.
+    SVProgressHUDStyleDark NS_SWIFT_NAME(dark),
+    
+    /// Uses the fore- and background color properties.
+    SVProgressHUDStyleCustom NS_SWIFT_NAME(custom),
+    
+    /// Automatically switch between light or dark mode appearance.
+    SVProgressHUDStyleAutomatic NS_SWIFT_NAME(automatic)
+};
+
+/// Represents the type of mask to be applied when the HUD is displayed.
+typedef NS_ENUM(NSUInteger, SVProgressHUDMaskType) {
+    /// Allow user interactions while HUD is displayed.
+    SVProgressHUDMaskTypeNone NS_SWIFT_NAME(none) = 1,
+    
+    /// Don't allow user interactions with background objects.
+    SVProgressHUDMaskTypeClear NS_SWIFT_NAME(clear),
+    
+    /// Don't allow user interactions and dim the UI behind the HUD (as in iOS 7+).
+    SVProgressHUDMaskTypeBlack NS_SWIFT_NAME(black),
+    
+    /// Don't allow user interactions and dim the UI with an UIAlertView-like background gradient (as in iOS 6).
+    SVProgressHUDMaskTypeGradient NS_SWIFT_NAME(gradient),
+    
+    /// Don't allow user interactions and dim the UI behind the HUD with a custom color.
+    SVProgressHUDMaskTypeCustom NS_SWIFT_NAME(custom)
+};
+
+/// Represents the animation type of the HUD when it's shown or hidden.
+typedef NS_ENUM(NSUInteger, SVProgressHUDAnimationType) {
+    /// Custom flat animation (indefinite animated ring).
+    SVProgressHUDAnimationTypeFlat NS_SWIFT_NAME(flat),
+    
+    /// iOS native UIActivityIndicatorView.
+    SVProgressHUDAnimationTypeNative NS_SWIFT_NAME(native)
+};
+
+typedef void (^SVProgressHUDShowCompletion)(void);
+typedef void (^SVProgressHUDDismissCompletion)(void);
+
+@interface SVProgressHUD : UIView
+
+#pragma mark - Customization
+
+/// Represents the default style for the HUD.
+/// @discussion Default: SVProgressHUDStyleAutomatic.
+@property (assign, nonatomic) SVProgressHUDStyle defaultStyle UI_APPEARANCE_SELECTOR;
+
+/// Represents the type of mask applied when the HUD is displayed.
+/// @discussion Default: SVProgressHUDMaskTypeNone.
+@property (assign, nonatomic) SVProgressHUDMaskType defaultMaskType UI_APPEARANCE_SELECTOR;
+
+/// Defines the animation type used when the HUD is displayed.
+/// @discussion Default: SVProgressHUDAnimationTypeFlat.
+@property (assign, nonatomic) SVProgressHUDAnimationType defaultAnimationType UI_APPEARANCE_SELECTOR;
+
+/// The container view used for displaying the HUD. If nil, the default window level is used.
+@property (strong, nonatomic, nullable) UIView *containerView;
+
+/// The minimum size for the HUD. Useful for maintaining a consistent size when the message might cause resizing.
+/// @discussion Default: CGSizeZero.
+@property (assign, nonatomic) CGSize minimumSize UI_APPEARANCE_SELECTOR;
+
+/// Thickness of the ring shown in the HUD.
+/// @discussion Default: 2 pt.
+@property (assign, nonatomic) CGFloat ringThickness UI_APPEARANCE_SELECTOR;
+
+/// Radius of the ring shown in the HUD when there's associated text.
+/// @discussion Default: 18 pt.
+@property (assign, nonatomic) CGFloat ringRadius UI_APPEARANCE_SELECTOR;
+
+/// Radius of the ring shown in the HUD when there's no associated text.
+/// @discussion Default: 24 pt.
+@property (assign, nonatomic) CGFloat ringNoTextRadius UI_APPEARANCE_SELECTOR;
+
+/// Corner radius of the HUD view.
+/// @discussion Default: 14 pt.
+@property (assign, nonatomic) CGFloat cornerRadius UI_APPEARANCE_SELECTOR;
+
+/// Font used for text within the HUD.
+/// @discussion Default: [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline].
+@property (strong, nonatomic, nonnull) UIFont *font UI_APPEARANCE_SELECTOR;
+
+/// Background color of the HUD.
+/// @discussion Default: [UIColor whiteColor].
+@property (strong, nonatomic, nonnull) UIColor *backgroundColor UI_APPEARANCE_SELECTOR;
+
+/// Foreground color used for content in the HUD.
+/// @discussion Default: [UIColor blackColor].
+@property (strong, nonatomic, nonnull) UIColor *foregroundColor UI_APPEARANCE_SELECTOR;
+
+/// Color for any foreground images in the HUD.
+/// @discussion Default: same as foregroundColor.
+@property (strong, nonatomic, nullable) UIColor *foregroundImageColor UI_APPEARANCE_SELECTOR;
+
+/// Color for the background layer behind the HUD.
+/// @discussion Default: [UIColor colorWithWhite:0 alpha:0.4].
+@property (strong, nonatomic, nonnull) UIColor *backgroundLayerColor UI_APPEARANCE_SELECTOR;
+
+/// Size of any images displayed within the HUD.
+/// @discussion Default: 28x28 pt.
+@property (assign, nonatomic) CGSize imageViewSize UI_APPEARANCE_SELECTOR;
+
+/// Indicates whether images within the HUD should be tinted.
+/// @discussion Default: YES.
+@property (assign, nonatomic) BOOL shouldTintImages UI_APPEARANCE_SELECTOR;
+
+/// The image displayed when showing informational messages.
+/// @discussion Default: info.circle from SF Symbols (iOS 13+) or the bundled info image provided by Freepik.
+@property (strong, nonatomic, nonnull) UIImage *infoImage UI_APPEARANCE_SELECTOR;
+
+/// The image displayed when showing success messages.
+/// @discussion Default: checkmark from SF Symbols (iOS 13+) or the bundled success image provided by Freepik.
+@property (strong, nonatomic, nonnull) UIImage *successImage UI_APPEARANCE_SELECTOR;
+
+/// The image displayed when showing error messages.
+/// @discussion Default: xmark from SF Symbols (iOS 13+) or the bundled error image provided by Freepik.
+@property (strong, nonatomic, nonnull) UIImage *errorImage UI_APPEARANCE_SELECTOR;
+
+/// A specific view for extensions. This property is only used if #define SV_APP_EXTENSIONS is set.
+/// @discussion Default: nil.
+@property (strong, nonatomic, nonnull) UIView *viewForExtension UI_APPEARANCE_SELECTOR;
+
+/// The interval in seconds to wait before displaying the HUD. If the HUD is displayed before this time elapses, this timer is reset.
+/// @discussion Default: 0 seconds.
+@property (assign, nonatomic) NSTimeInterval graceTimeInterval;
+
+/// The minimum amount of time in seconds the HUD will display.
+/// @discussion Default: 5.0 seconds.
+@property (assign, nonatomic) NSTimeInterval minimumDismissTimeInterval;
+
+/// The maximum amount of time in seconds the HUD will display.
+/// @discussion Default: CGFLOAT_MAX.
+@property (assign, nonatomic) NSTimeInterval maximumDismissTimeInterval;
+
+/// Offset from the center position, can be used to adjust the HUD position.
+/// @discussion Default: 0, 0.
+@property (assign, nonatomic) UIOffset offsetFromCenter UI_APPEARANCE_SELECTOR;
+
+/// Duration of the fade-in animation when showing the HUD.
+/// @discussion Default: 0.15.
+@property (assign, nonatomic) NSTimeInterval fadeInAnimationDuration UI_APPEARANCE_SELECTOR;
+
+/// Duration of the fade-out animation when hiding the HUD.
+/// @discussion Default: 0.15.
+@property (assign, nonatomic) NSTimeInterval fadeOutAnimationDuration UI_APPEARANCE_SELECTOR;
+
+/// The maximum window level on which the HUD can be displayed.
+/// @discussion Default: UIWindowLevelNormal.
+@property (assign, nonatomic) UIWindowLevel maxSupportedWindowLevel;
+
+/// Indicates if haptic feedback should be used.
+/// @discussion Default: NO.
+@property (assign, nonatomic) BOOL hapticsEnabled;
+
+/// Indicates if motion effects should be applied to the HUD.
+/// @discussion Default: YES.
+@property (assign, nonatomic) BOOL motionEffectEnabled;
+
+@property (class, strong, nonatomic, readonly, nonnull) NSBundle *imageBundle;
+
+/// Sets the default style for the HUD.
+/// @param style The desired style for the HUD.
++ (void)setDefaultStyle:(SVProgressHUDStyle)style;
+
+/// Sets the default mask type for the HUD.
+/// @param maskType The mask type to apply.
++ (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType;
+
+/// Sets the default animation type for the HUD.
+/// @param type The desired animation type.
++ (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type;
+
+/// Sets the container view for the HUD.
+/// @param containerView The view to contain the HUD.
++ (void)setContainerView:(nullable UIView*)containerView;
+
+/// Sets the minimum size for the HUD.
+/// @param minimumSize The minimum size for the HUD.
++ (void)setMinimumSize:(CGSize)minimumSize;
+
+/// Sets the ring thickness for the HUD.
+/// @param ringThickness Thickness of the ring.
++ (void)setRingThickness:(CGFloat)ringThickness;
+
+/// Sets the ring radius for the HUD.
+/// @param radius Radius of the ring.
++ (void)setRingRadius:(CGFloat)radius;
+
+/// Sets the no text ring radius for the HUD.
+/// @param radius Radius of the ring when no text is displayed.
++ (void)setRingNoTextRadius:(CGFloat)radius;
+
+/// Sets the corner radius for the HUD.
+/// @param cornerRadius Desired corner radius.
++ (void)setCornerRadius:(CGFloat)cornerRadius;
+
+/// Sets the border color for the HUD.
+/// @param color Desired border color.
++ (void)setBorderColor:(nonnull UIColor*)color;
+
+/// Sets the border width for the HUD.
+/// @param width Desired border width.
++ (void)setBorderWidth:(CGFloat)width;
+
+/// Sets the font for the HUD's text.
+/// @param font Desired font for the text.
++ (void)setFont:(nonnull UIFont*)font;
+
+/// Sets the foreground color for the HUD.
+/// @param color Desired foreground color.
+/// @discussion These implicitly set the HUD's style to `SVProgressHUDStyleCustom`.
++ (void)setForegroundColor:(nonnull UIColor*)color;
+
+/// Sets the foreground image color for the HUD.
+/// @param color Desired color for the image.
+/// @discussion These implicitly set the HUD's style to `SVProgressHUDStyleCustom`.
++ (void)setForegroundImageColor:(nullable UIColor*)color;
+
+/// Sets the background color for the HUD.
+/// @param color Desired background color.
+/// @discussion These implicitly set the HUD's style to `SVProgressHUDStyleCustom`.
++ (void)setBackgroundColor:(nonnull UIColor*)color;
+
+/// Sets a custom blur effect for the HUD view.
+/// @param blurEffect Desired blur effect.
+/// @discussion These implicitly set the HUD's style to `SVProgressHUDStyleCustom`.
++ (void)setHudViewCustomBlurEffect:(nullable UIBlurEffect*)blurEffect;
+
+/// Sets the background layer color for the HUD.
+/// @param color Desired color for the background layer.
++ (void)setBackgroundLayerColor:(nonnull UIColor*)color;
+
+/// Sets the size for the HUD's image view.
+/// @param size Desired size for the image view.
++ (void)setImageViewSize:(CGSize)size;
+
+/// Determines if images should be tinted in the HUD.
+/// @param shouldTintImages Whether images should be tinted.
++ (void)setShouldTintImages:(BOOL)shouldTintImages;
+
+/// Sets the info image for the HUD.
+/// @param image The desired info image.
++ (void)setInfoImage:(nonnull UIImage*)image;
+
+/// Sets the success image for the HUD.
+/// @param image The desired success image.
++ (void)setSuccessImage:(nonnull UIImage*)image;
+
+/// Sets the error image for the HUD.
+/// @param image The desired error image.
++ (void)setErrorImage:(nonnull UIImage*)image;
+
+/// Sets the view for extensions.
+/// @param view The desired view for extensions.
++ (void)setViewForExtension:(nonnull UIView*)view;
+
+/// Sets the grace time interval for the HUD.
+/// @param interval Desired grace time interval.
++ (void)setGraceTimeInterval:(NSTimeInterval)interval;
+
+/// Sets the minimum dismiss time interval.
+/// @param interval The minimum time interval, in seconds, that the HUD should be displayed.
++ (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval;
+
+/// Sets the maximum dismiss time interval.
+/// @param interval The maximum time interval, in seconds, that the HUD should be displayed.
++ (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval;
+
+/// Sets the fade-in animation duration.
+/// @param duration The duration, in seconds, for the fade-in animation.
++ (void)setFadeInAnimationDuration:(NSTimeInterval)duration;
+
+/// Sets the fade-out animation duration.
+/// @param duration The duration, in seconds, for the fade-out animation.
++ (void)setFadeOutAnimationDuration:(NSTimeInterval)duration;
+
+/// Sets the max supported window level.
+/// @param windowLevel The UIWindowLevel to which the HUD should be displayed.
++ (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel;
+
+/// Determines if haptics are enabled.
+/// @param hapticsEnabled A boolean that determines if haptic feedback is enabled.
++ (void)setHapticsEnabled:(BOOL)hapticsEnabled;
+
+/// Determines if motion effect is enabled.
+/// @param motionEffectEnabled A boolean that determines if motion effects are enabled.
++ (void)setMotionEffectEnabled:(BOOL)motionEffectEnabled;
+
+
+#pragma mark - Show Methods
+
+/// Shows the HUD without any additional status message.
++ (void)show;
+
+/// Shows the HUD with a provided status message.
+/// @param status The message to be displayed alongside the HUD.
++ (void)showWithStatus:(nullable NSString*)status;
+
+/// Display methods to show progress on the HUD.
+
+/// Shows the HUD with a progress indicator.
+/// @param progress A float value between 0.0 and 1.0 indicating the progress.
++ (void)showProgress:(float)progress;
+
+/// Shows the HUD with a progress indicator and a provided status message.
+/// @param progress A float value between 0.0 and 1.0 indicating the progress.
+/// @param status The message to be displayed alongside the progress indicator.
++ (void)showProgress:(float)progress status:(nullable NSString*)status;
+
+/// Updates the current status of the loading HUD.
+/// @param status The new status message to update the HUD with.
++ (void)setStatus:(nullable NSString*)status;
+
+/// Shows an info status with the provided message.
+/// @param status The info message to be displayed.
++ (void)showInfoWithStatus:(nullable NSString*)status;
+
+/// Shows a success status with the provided message.
+/// @param status The success message to be displayed.
++ (void)showSuccessWithStatus:(nullable NSString*)status;
+
+/// Shows an error status with the provided message.
+/// @param status The error message to be displayed.
++ (void)showErrorWithStatus:(nullable NSString*)status;
+
+/// Shows a custom image with the provided status message.
+/// @param image The custom image to be displayed.
+/// @param status The message to accompany the custom image.
++ (void)showImage:(nonnull UIImage*)image status:(nullable NSString*)status;
+
+/// Sets the offset from the center for the HUD.
+/// @param offset The UIOffset value indicating how much the HUD should be offset from its center position.
++ (void)setOffsetFromCenter:(UIOffset)offset;
+
+/// Resets the offset to center the HUD.
++ (void)resetOffsetFromCenter;
+
+/// Decreases the activity count, dismissing the HUD if count reaches 0.
++ (void)popActivity;
+
+/// Dismisses the HUD immediately.
++ (void)dismiss;
+
+/// Dismisses the HUD and triggers a completion block.
+/// @param completion A block that gets executed after the HUD is dismissed.
++ (void)dismissWithCompletion:(nullable SVProgressHUDDismissCompletion)completion;
+
+/// Dismisses the HUD after a specified delay.
+/// @param delay The time in seconds after which the HUD should be dismissed.
++ (void)dismissWithDelay:(NSTimeInterval)delay;
+
+/// Dismisses the HUD after a specified delay and triggers a completion block.
+/// @param delay The time in seconds after which the HUD should be dismissed.
+/// @param completion A block that gets executed after the HUD is dismissed.
++ (void)dismissWithDelay:(NSTimeInterval)delay completion:(nullable SVProgressHUDDismissCompletion)completion;
+
+/// Checks if the HUD is currently visible.
+/// @return A boolean value indicating whether the HUD is visible.
++ (BOOL)isVisible;
+
+/// Calculates the display duration based on a given string's length.
+/// @param string The string whose length determines the display duration.
+/// @return A time interval representing the display duration.
++ (NSTimeInterval)displayDurationForString:(nullable NSString*)string;
+
+@end
+

+ 1524 - 0
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.m

@@ -0,0 +1,1524 @@
+//
+//  SVProgressHUD.h
+//  SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD
+//
+//  Copyright (c) 2011-2023 Sam Vermette and contributors. All rights reserved.
+//
+
+#if !__has_feature(objc_arc)
+#error SVProgressHUD is ARC only. Either turn on ARC for the project or use -fobjc-arc flag
+#endif
+
+#import "SVProgressHUD.h"
+#import "SVIndefiniteAnimatedView.h"
+#import "SVProgressAnimatedView.h"
+#import "SVRadialGradientLayer.h"
+
+NSString * const SVProgressHUDDidReceiveTouchEventNotification = @"SVProgressHUDDidReceiveTouchEventNotification";
+NSString * const SVProgressHUDDidTouchDownInsideNotification = @"SVProgressHUDDidTouchDownInsideNotification";
+NSString * const SVProgressHUDWillDisappearNotification = @"SVProgressHUDWillDisappearNotification";
+NSString * const SVProgressHUDDidDisappearNotification = @"SVProgressHUDDidDisappearNotification";
+NSString * const SVProgressHUDWillAppearNotification = @"SVProgressHUDWillAppearNotification";
+NSString * const SVProgressHUDDidAppearNotification = @"SVProgressHUDDidAppearNotification";
+
+NSString * const SVProgressHUDStatusUserInfoKey = @"SVProgressHUDStatusUserInfoKey";
+
+static const CGFloat SVProgressHUDParallaxDepthPoints = 10.0f;
+static const CGFloat SVProgressHUDUndefinedProgress = -1;
+static const CGFloat SVProgressHUDDefaultAnimationDuration = 0.15f;
+static const CGFloat SVProgressHUDVerticalSpacing = 12.0f;
+static const CGFloat SVProgressHUDHorizontalSpacing = 12.0f;
+static const CGFloat SVProgressHUDLabelSpacing = 8.0f;
+
+
+@interface SVProgressHUD ()
+
+@property (nonatomic, strong) NSTimer *graceTimer;
+@property (nonatomic, strong) NSTimer *fadeOutTimer;
+
+@property (nonatomic, strong) UIControl *controlView;
+@property (nonatomic, strong) UIView *backgroundView;
+@property (nonatomic, strong) SVRadialGradientLayer *backgroundRadialGradientLayer;
+@property (nonatomic, strong) UIVisualEffectView *hudView;
+@property (nonatomic, strong) UIBlurEffect *hudViewCustomBlurEffect;
+@property (nonatomic, strong) UILabel *statusLabel;
+@property (nonatomic, strong) UIImageView *imageView;
+
+@property (nonatomic, strong) UIView *indefiniteAnimatedView;
+@property (nonatomic, strong) SVProgressAnimatedView *ringView;
+@property (nonatomic, strong) SVProgressAnimatedView *backgroundRingView;
+
+@property (nonatomic, readwrite) CGFloat progress;
+@property (nonatomic, readwrite) NSUInteger activityCount;
+
+@property (nonatomic, readonly) CGFloat visibleKeyboardHeight;
+@property (nonatomic, readonly) UIWindow *frontWindow;
+
+#if TARGET_OS_IOS
+@property (nonatomic, strong) UINotificationFeedbackGenerator *hapticGenerator;
+#endif
+
+@end
+
+@implementation SVProgressHUD {
+    BOOL _isInitializing;
+}
+
++ (SVProgressHUD*)sharedView {
+    static dispatch_once_t once;
+    
+    static SVProgressHUD *sharedView;
+#if !defined(SV_APP_EXTENSIONS)
+     dispatch_once(&once, ^{ sharedView = [[self alloc] initWithFrame:[SVProgressHUD mainWindow].bounds]; });
+#else
+    dispatch_once(&once, ^{ sharedView = [[self alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; });
+#endif
+    return sharedView;
+}
+
++ (UIWindow *)mainWindow {
+    if (@available(iOS 13.0, *)) {
+        for (UIWindowScene* windowScene in [UIApplication sharedApplication].connectedScenes) {
+            if (windowScene.activationState == UISceneActivationStateForegroundActive) {
+                return windowScene.windows.firstObject;
+            }
+        }
+        // If a window has not been returned by now, the first scene's window is returned (regardless of activationState).
+        UIWindowScene *windowScene = (UIWindowScene *)[[UIApplication sharedApplication].connectedScenes allObjects].firstObject;
+        return windowScene.windows.firstObject;
+    } else {
+#if TARGET_OS_IOS
+        return [[[UIApplication sharedApplication] delegate] window];
+#else
+        return [UIApplication sharedApplication].keyWindow;
+#endif
+    }
+}
+
++ (NSBundle *)imageBundle {
+#if defined(SWIFTPM_MODULE_BUNDLE)
+     NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
+#else
+     NSBundle *bundle = [NSBundle bundleForClass:[SVProgressHUD class]];
+#endif
+     NSURL *url = [bundle URLForResource:@"SVProgressHUD" withExtension:@"bundle"];
+     return [NSBundle bundleWithURL:url];
+ }
+
+#pragma mark - Setters
+
++ (void)setStatus:(NSString*)status {
+    [[self sharedView] setStatus:status];
+}
+
++ (void)setDefaultStyle:(SVProgressHUDStyle)style {
+    [self sharedView].defaultStyle = style;
+}
+
++ (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType {
+    [self sharedView].defaultMaskType = maskType;
+}
+
++ (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type {
+    [self sharedView].defaultAnimationType = type;
+}
+
++ (void)setContainerView:(nullable UIView*)containerView {
+    [self sharedView].containerView = containerView;
+}
+
++ (void)setMinimumSize:(CGSize)minimumSize {
+    [self sharedView].minimumSize = minimumSize;
+}
+
++ (void)setRingThickness:(CGFloat)ringThickness {
+    [self sharedView].ringThickness = ringThickness;
+}
+
++ (void)setRingRadius:(CGFloat)radius {
+    [self sharedView].ringRadius = radius;
+}
+
++ (void)setRingNoTextRadius:(CGFloat)radius {
+    [self sharedView].ringNoTextRadius = radius;
+}
+
++ (void)setCornerRadius:(CGFloat)cornerRadius {
+    [self sharedView].cornerRadius = cornerRadius;
+}
+
++ (void)setBorderColor:(nonnull UIColor*)color {
+    [self sharedView].hudView.layer.borderColor = color.CGColor;
+}
+
++ (void)setBorderWidth:(CGFloat)width {
+    [self sharedView].hudView.layer.borderWidth = width;
+}
+
++ (void)setFont:(UIFont*)font {
+    [self sharedView].font = font;
+}
+
++ (void)setForegroundColor:(UIColor*)color {
+    [self sharedView].foregroundColor = color;
+    [self setDefaultStyle:SVProgressHUDStyleCustom];
+}
+
++ (void)setForegroundImageColor:(UIColor *)color {
+    [self sharedView].foregroundImageColor = color;
+    [self setDefaultStyle:SVProgressHUDStyleCustom];
+}
+
++ (void)setBackgroundColor:(UIColor*)color {
+    [self sharedView].backgroundColor = color;
+    [self setDefaultStyle:SVProgressHUDStyleCustom];
+}
+
++ (void)setHudViewCustomBlurEffect:(UIBlurEffect*)blurEffect {
+    [self sharedView].hudViewCustomBlurEffect = blurEffect;
+    [self setDefaultStyle:SVProgressHUDStyleCustom];
+}
+
++ (void)setBackgroundLayerColor:(UIColor*)color {
+    [self sharedView].backgroundLayerColor = color;
+}
+
++ (void)setImageViewSize:(CGSize)size {
+    [self sharedView].imageViewSize = size;
+}
+
++ (void)setShouldTintImages:(BOOL)shouldTintImages {
+    [self sharedView].shouldTintImages = shouldTintImages;
+}
+
++ (void)setInfoImage:(UIImage*)image {
+    [self sharedView].infoImage = image;
+}
+
++ (void)setSuccessImage:(UIImage*)image {
+    [self sharedView].successImage = image;
+}
+
++ (void)setErrorImage:(UIImage*)image {
+    [self sharedView].errorImage = image;
+}
+
++ (void)setViewForExtension:(UIView*)view {
+    [self sharedView].viewForExtension = view;
+}
+
++ (void)setGraceTimeInterval:(NSTimeInterval)interval {
+    [self sharedView].graceTimeInterval = interval;
+}
+
++ (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval {
+    [self sharedView].minimumDismissTimeInterval = interval;
+}
+
++ (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval {
+    [self sharedView].maximumDismissTimeInterval = interval;
+}
+
++ (void)setFadeInAnimationDuration:(NSTimeInterval)duration {
+    [self sharedView].fadeInAnimationDuration = duration;
+}
+
++ (void)setFadeOutAnimationDuration:(NSTimeInterval)duration {
+    [self sharedView].fadeOutAnimationDuration = duration;
+}
+
++ (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel {
+    [self sharedView].maxSupportedWindowLevel = windowLevel;
+}
+
++ (void)setHapticsEnabled:(BOOL)hapticsEnabled {
+    [self sharedView].hapticsEnabled = hapticsEnabled;
+}
+
++ (void)setMotionEffectEnabled:(BOOL)motionEffectEnabled {
+    [self sharedView].motionEffectEnabled = motionEffectEnabled;
+}
+
+#pragma mark - Show Methods
+
++ (void)show {
+    [self showWithStatus:nil];
+}
+
++ (void)showWithStatus:(NSString*)status {
+    [self showProgress:SVProgressHUDUndefinedProgress status:status];
+}
+
++ (void)showProgress:(float)progress {
+    [self showProgress:progress status:nil];
+}
+
++ (void)showProgress:(float)progress status:(NSString*)status {
+    [[self sharedView] showProgress:progress status:status];
+}
+
+
+#pragma mark - Show, then automatically dismiss methods
+
++ (void)showInfoWithStatus:(NSString*)status {
+    [self showImage:[self sharedView].infoImage status:status];
+    
+#if TARGET_OS_IOS
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [[self sharedView].hapticGenerator notificationOccurred:UINotificationFeedbackTypeWarning];
+    });
+#endif
+}
+
++ (void)showSuccessWithStatus:(NSString*)status {
+    [self showImage:[self sharedView].successImage status:status];
+
+#if TARGET_OS_IOS
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [[self sharedView].hapticGenerator notificationOccurred:UINotificationFeedbackTypeSuccess];
+    });
+#endif
+}
+
++ (void)showErrorWithStatus:(NSString*)status {
+    [self showImage:[self sharedView].errorImage status:status];
+    
+#if TARGET_OS_IOS
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [[self sharedView].hapticGenerator notificationOccurred:UINotificationFeedbackTypeError];
+    });
+#endif
+}
+
++ (void)showImage:(UIImage*)image status:(NSString*)status {
+    NSTimeInterval displayInterval = [self displayDurationForString:status];
+    [[self sharedView] showImage:image status:status duration:displayInterval];
+}
+
+
+#pragma mark - Dismiss Methods
+
++ (void)popActivity {
+    if([self sharedView].activityCount > 0) {
+        [self sharedView].activityCount--;
+    }
+    if([self sharedView].activityCount == 0) {
+        [[self sharedView] dismiss];
+    }
+}
+
++ (void)dismiss {
+    [self dismissWithDelay:0.0 completion:nil];
+}
+
++ (void)dismissWithCompletion:(SVProgressHUDDismissCompletion)completion {
+    [self dismissWithDelay:0.0 completion:completion];
+}
+
++ (void)dismissWithDelay:(NSTimeInterval)delay {
+    [self dismissWithDelay:delay completion:nil];
+}
+
++ (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion {
+    [[self sharedView] dismissWithDelay:delay completion:completion];
+}
+
+
+#pragma mark - Offset
+
++ (void)setOffsetFromCenter:(UIOffset)offset {
+    [self sharedView].offsetFromCenter = offset;
+}
+
++ (void)resetOffsetFromCenter {
+    [self setOffsetFromCenter:UIOffsetZero];
+}
+
+
+#pragma mark - Instance Methods
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    if((self = [super initWithFrame:frame])) {
+        _isInitializing = YES;
+        
+        self.userInteractionEnabled = NO;
+        self.activityCount = 0;
+        
+        self.backgroundView.alpha = 0.0f;
+        self.imageView.alpha = 0.0f;
+        self.statusLabel.alpha = 0.0f;
+        self.indefiniteAnimatedView.alpha = 0.0f;
+        self.ringView.alpha = self.backgroundRingView.alpha = 0.0f;
+        
+
+        _backgroundColor = [UIColor whiteColor];
+        _foregroundColor = [UIColor blackColor];
+        _backgroundLayerColor = [UIColor colorWithWhite:0 alpha:0.4];
+        
+        // Set default values
+        _defaultMaskType = SVProgressHUDMaskTypeNone;
+        _defaultStyle = SVProgressHUDStyleAutomatic;
+        _defaultAnimationType = SVProgressHUDAnimationTypeFlat;
+        _minimumSize = CGSizeZero;
+        _font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
+        
+        _imageViewSize = CGSizeMake(28.0f, 28.0f);
+        _shouldTintImages = YES;
+        
+        NSBundle *imageBundle = [SVProgressHUD imageBundle];
+        
+        if (@available(iOS 13.0, *)) {
+            _infoImage = [UIImage systemImageNamed:@"info.circle"];
+            _successImage = [UIImage systemImageNamed:@"checkmark"];
+            _errorImage = [UIImage systemImageNamed:@"xmark"];
+        } else {
+            _infoImage = [UIImage imageWithContentsOfFile:[imageBundle pathForResource:@"info" ofType:@"png"]];
+            _successImage = [UIImage imageWithContentsOfFile:[imageBundle pathForResource:@"success" ofType:@"png"]];
+            _errorImage = [UIImage imageWithContentsOfFile:[imageBundle pathForResource:@"error" ofType:@"png"]];
+        }
+        
+        _ringThickness = 2.0f;
+        _ringRadius = 18.0f;
+        _ringNoTextRadius = 24.0f;
+        
+        _cornerRadius = 14.0f;
+		
+        _graceTimeInterval = 0.0f;
+        _minimumDismissTimeInterval = 5.0;
+        _maximumDismissTimeInterval = CGFLOAT_MAX;
+
+        _fadeInAnimationDuration = SVProgressHUDDefaultAnimationDuration;
+        _fadeOutAnimationDuration = SVProgressHUDDefaultAnimationDuration;
+        
+        _maxSupportedWindowLevel = UIWindowLevelNormal;
+        
+        _hapticsEnabled = NO;
+        _motionEffectEnabled = YES;
+        
+        // Accessibility support
+        self.accessibilityIdentifier = @"SVProgressHUD";
+        self.isAccessibilityElement = YES;
+        
+        _isInitializing = NO;
+    }
+    return self;
+}
+
+- (void)updateHUDFrame {
+    // Check if an image or progress ring is displayed
+    BOOL imageUsed = (self.imageView.image) && !(self.imageView.hidden) && (self.imageViewSize.height > 0 && self.imageViewSize.width > 0);
+    BOOL progressUsed = self.imageView.hidden;
+    
+    // Calculate size of string
+    CGRect labelRect = CGRectZero;
+    CGFloat labelHeight = 0.0f;
+    CGFloat labelWidth = 0.0f;
+    
+    if(self.statusLabel.text) {
+        CGSize constraintSize = CGSizeMake(200.0f, 300.0f);
+        labelRect = [self.statusLabel.text boundingRectWithSize:constraintSize
+                                                        options:(NSStringDrawingOptions)(NSStringDrawingUsesFontLeading | NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin)
+                                                     attributes:@{NSFontAttributeName: self.statusLabel.font}
+                                                        context:NULL];
+        labelHeight = ceilf(CGRectGetHeight(labelRect));
+        labelWidth = ceilf(CGRectGetWidth(labelRect));
+    }
+    
+    // Calculate hud size based on content
+    // For the beginning use default values, these
+    // might get update if string is too large etc.
+    CGFloat hudWidth;
+    CGFloat hudHeight;
+    
+    CGFloat contentWidth = 0.0f;
+    CGFloat contentHeight = 0.0f;
+    
+    if(imageUsed || progressUsed) {
+        contentWidth = CGRectGetWidth(imageUsed ? self.imageView.frame : self.indefiniteAnimatedView.frame);
+        contentHeight = CGRectGetHeight(imageUsed ? self.imageView.frame : self.indefiniteAnimatedView.frame);
+    }
+    
+    // |-spacing-content-spacing-|
+    hudWidth = SVProgressHUDHorizontalSpacing + MAX(labelWidth, contentWidth) + SVProgressHUDHorizontalSpacing;
+    
+    // |-spacing-content-(labelSpacing-label-)spacing-|
+    hudHeight = SVProgressHUDVerticalSpacing + labelHeight + contentHeight + SVProgressHUDVerticalSpacing;
+    if(self.statusLabel.text && (imageUsed || progressUsed)){
+        // Add spacing if both content and label are used
+        hudHeight += SVProgressHUDLabelSpacing;
+    }
+    
+    // Update values on subviews
+    self.hudView.bounds = CGRectMake(0.0f, 0.0f, MAX(self.minimumSize.width, hudWidth), MAX(self.minimumSize.height, hudHeight));
+    
+    // Animate value update
+    [CATransaction begin];
+    [CATransaction setDisableActions:YES];
+    
+    // Spinner and image view
+    CGFloat centerY;
+    if(self.statusLabel.text) {
+        CGFloat yOffset = MAX(SVProgressHUDVerticalSpacing, (self.minimumSize.height - contentHeight - SVProgressHUDLabelSpacing - labelHeight) / 2.0f);
+        centerY = yOffset + contentHeight / 2.0f;
+    } else {
+        centerY = CGRectGetMidY(self.hudView.bounds);
+    }
+    self.indefiniteAnimatedView.center = CGPointMake(CGRectGetMidX(self.hudView.bounds), centerY);
+    if(self.progress != SVProgressHUDUndefinedProgress) {
+        self.backgroundRingView.center = self.ringView.center = CGPointMake(CGRectGetMidX(self.hudView.bounds), centerY);
+    }
+    self.imageView.center = CGPointMake(CGRectGetMidX(self.hudView.bounds), centerY);
+
+    // Label
+    if(imageUsed || progressUsed) {
+        centerY = CGRectGetMaxY(imageUsed ? self.imageView.frame : self.indefiniteAnimatedView.frame) + SVProgressHUDLabelSpacing + labelHeight / 2.0f;
+    } else {
+        centerY = CGRectGetMidY(self.hudView.bounds);
+    }
+    self.statusLabel.frame = labelRect;
+    self.statusLabel.center = CGPointMake(CGRectGetMidX(self.hudView.bounds), centerY);
+    
+    [CATransaction commit];
+}
+
+#if TARGET_OS_IOS
+- (void)updateMotionEffectForOrientation:(UIInterfaceOrientation)orientation {
+    bool isPortrait = UIInterfaceOrientationIsPortrait(orientation);
+    UIInterpolatingMotionEffectType xMotionEffectType = isPortrait ? UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis : UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis;
+    UIInterpolatingMotionEffectType yMotionEffectType = isPortrait ? UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis : UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis;
+    [self updateMotionEffectForXMotionEffectType:xMotionEffectType yMotionEffectType:yMotionEffectType];
+}
+#endif
+
+- (void)updateMotionEffectForXMotionEffectType:(UIInterpolatingMotionEffectType)xMotionEffectType yMotionEffectType:(UIInterpolatingMotionEffectType)yMotionEffectType {
+    UIInterpolatingMotionEffect *effectX = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:xMotionEffectType];
+    effectX.minimumRelativeValue = @(-SVProgressHUDParallaxDepthPoints);
+    effectX.maximumRelativeValue = @(SVProgressHUDParallaxDepthPoints);
+    
+    UIInterpolatingMotionEffect *effectY = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" type:yMotionEffectType];
+    effectY.minimumRelativeValue = @(-SVProgressHUDParallaxDepthPoints);
+    effectY.maximumRelativeValue = @(SVProgressHUDParallaxDepthPoints);
+    
+    UIMotionEffectGroup *effectGroup = [UIMotionEffectGroup new];
+    effectGroup.motionEffects = @[effectX, effectY];
+    
+    // Clear old motion effect, then add new motion effects
+    self.hudView.motionEffects = @[];
+    [self.hudView addMotionEffect:effectGroup];
+}
+
+- (void)updateViewHierarchy {
+    // Add the overlay to the application window if necessary
+    if(!self.controlView.superview) {
+        if(self.containerView){
+            [self.containerView addSubview:self.controlView];
+        } else {
+#if !defined(SV_APP_EXTENSIONS)
+            [self.frontWindow addSubview:self.controlView];
+#else
+            // If SVProgressHUD is used inside an app extension add it to the given view
+            if(self.viewForExtension) {
+                [self.viewForExtension addSubview:self.controlView];
+            }
+#endif
+        }
+    } else {
+        // The HUD is already on screen, but maybe not in front. Therefore
+        // ensure that overlay will be on top of rootViewController (which may
+        // be changed during runtime).
+        [self.controlView.superview bringSubviewToFront:self.controlView];
+    }
+    
+    // Add self to the overlay view
+    if(!self.superview) {
+        [self.controlView addSubview:self];
+    }
+}
+
+- (void)setStatus:(NSString*)status {
+    self.statusLabel.text = status;
+    self.statusLabel.hidden = status.length == 0;
+    [self updateHUDFrame];
+}
+
+- (void)setGraceTimer:(NSTimer*)timer {
+    if(_graceTimer) {
+        [_graceTimer invalidate];
+        _graceTimer = nil;
+    }
+    if(timer) {
+        _graceTimer = timer;
+    }
+}
+
+- (void)setFadeOutTimer:(NSTimer*)timer {
+    if(_fadeOutTimer) {
+        [_fadeOutTimer invalidate];
+        _fadeOutTimer = nil;
+    }
+    if(timer) {
+        _fadeOutTimer = timer;
+    }
+}
+
+
+#pragma mark - Notifications and their handling
+
+- (void)registerNotifications {
+#if TARGET_OS_IOS
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(positionHUD:)
+                                                 name:UIApplicationDidChangeStatusBarOrientationNotification
+                                               object:nil];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(positionHUD:)
+                                                 name:UIKeyboardWillHideNotification
+                                               object:nil];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(positionHUD:)
+                                                 name:UIKeyboardDidHideNotification
+                                               object:nil];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(positionHUD:)
+                                                 name:UIKeyboardWillShowNotification
+                                               object:nil];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(positionHUD:)
+                                                 name:UIKeyboardDidShowNotification
+                                               object:nil];
+#endif
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(positionHUD:)
+                                                 name:UIApplicationDidBecomeActiveNotification
+                                               object:nil];
+}
+
+- (NSDictionary*)notificationUserInfo {
+    return (self.statusLabel.text ? @{SVProgressHUDStatusUserInfoKey : self.statusLabel.text} : nil);
+}
+
+- (void)positionHUD:(NSNotification*)notification {
+    CGFloat keyboardHeight = 0.0f;
+    double animationDuration = 0.0;
+
+#if !defined(SV_APP_EXTENSIONS) && TARGET_OS_IOS
+    self.frame =  [SVProgressHUD mainWindow].bounds;
+    UIInterfaceOrientation orientation = UIApplication.sharedApplication.statusBarOrientation;
+#elif !defined(SV_APP_EXTENSIONS) && !TARGET_OS_IOS
+    self.frame = [SVProgressHUD mainWindow].bounds;
+#else
+    if (self.viewForExtension) {
+        self.frame = self.viewForExtension.frame;
+    } else {
+        self.frame = UIScreen.mainScreen.bounds;
+    }
+#if TARGET_OS_IOS
+    UIInterfaceOrientation orientation = CGRectGetWidth(self.frame) > CGRectGetHeight(self.frame) ? UIInterfaceOrientationLandscapeLeft : UIInterfaceOrientationPortrait;
+#endif
+#endif
+    
+#if TARGET_OS_IOS
+    // Get keyboardHeight in regard to current state
+    if(notification) {
+        NSDictionary* keyboardInfo = [notification userInfo];
+        CGRect keyboardFrame = [keyboardInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
+        animationDuration = [keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
+        
+        if(notification.name == UIKeyboardWillShowNotification || notification.name == UIKeyboardDidShowNotification) {
+            keyboardHeight = CGRectGetWidth(keyboardFrame);
+            
+            if(UIInterfaceOrientationIsPortrait(orientation)) {
+                keyboardHeight = CGRectGetHeight(keyboardFrame);
+            }
+        }
+    } else {
+        keyboardHeight = self.visibleKeyboardHeight;
+    }
+#endif
+    
+    // Get the currently active frame of the display (depends on orientation)
+    CGRect orientationFrame = self.bounds;
+
+#if !defined(SV_APP_EXTENSIONS) && TARGET_OS_IOS
+    CGRect statusBarFrame = UIApplication.sharedApplication.statusBarFrame;
+#else
+    CGRect statusBarFrame = CGRectZero;
+#endif
+    
+    if (_motionEffectEnabled) {
+#if TARGET_OS_IOS
+        // Update the motion effects in regard to orientation
+        [self updateMotionEffectForOrientation:orientation];
+#else
+        [self updateMotionEffectForXMotionEffectType:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis yMotionEffectType:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
+#endif
+    }
+    
+    // Calculate available height for display
+    CGFloat activeHeight = CGRectGetHeight(orientationFrame);
+    if(keyboardHeight > 0) {
+        activeHeight += CGRectGetHeight(statusBarFrame) * 2;
+    }
+    activeHeight -= keyboardHeight;
+    
+    CGFloat posX = CGRectGetMidX(orientationFrame);
+    CGFloat posY = floorf(activeHeight*0.45f);
+
+    CGFloat rotateAngle = 0.0;
+    CGPoint newCenter = CGPointMake(posX, posY);
+    
+    if(notification) {
+        // Animate update if notification was present
+        [UIView animateWithDuration:animationDuration
+                              delay:0
+                            options:(UIViewAnimationOptions) (UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState)
+                         animations:^{
+                             [self moveToPoint:newCenter rotateAngle:rotateAngle];
+                             [self.hudView setNeedsDisplay];
+                         } completion:nil];
+    } else {
+        [self moveToPoint:newCenter rotateAngle:rotateAngle];
+    }
+}
+
+- (void)moveToPoint:(CGPoint)newCenter rotateAngle:(CGFloat)angle {
+    self.hudView.transform = CGAffineTransformMakeRotation(angle);
+    if (self.containerView) {
+        self.hudView.center = CGPointMake(self.containerView.center.x + self.offsetFromCenter.horizontal, self.containerView.center.y + self.offsetFromCenter.vertical);
+    } else {
+        self.hudView.center = CGPointMake(newCenter.x + self.offsetFromCenter.horizontal, newCenter.y + self.offsetFromCenter.vertical);
+    }
+}
+
+
+#pragma mark - Event handling
+
+- (void)controlViewDidReceiveTouchEvent:(id)sender forEvent:(UIEvent*)event {
+    [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidReceiveTouchEventNotification
+                                                        object:self
+                                                      userInfo:[self notificationUserInfo]];
+    
+    UITouch *touch = event.allTouches.anyObject;
+    CGPoint touchLocation = [touch locationInView:self];
+    
+    if(CGRectContainsPoint(self.hudView.frame, touchLocation)) {
+        [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidTouchDownInsideNotification
+                                                            object:self
+                                                          userInfo:[self notificationUserInfo]];
+    }
+}
+
+
+#pragma mark - Master show/dismiss methods
+
+- (void)showProgress:(float)progress status:(NSString*)status {
+    __weak SVProgressHUD *weakSelf = self;
+    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
+        __strong SVProgressHUD *strongSelf = weakSelf;
+        if(strongSelf){
+            if(strongSelf.fadeOutTimer) {
+                strongSelf.activityCount = 0;
+            }
+            
+            // Stop timer
+            strongSelf.fadeOutTimer = nil;
+            strongSelf.graceTimer = nil;
+            
+            // Update / Check view hierarchy to ensure the HUD is visible
+            [strongSelf updateViewHierarchy];
+            
+            // Reset imageView and fadeout timer if an image is currently displayed
+            strongSelf.imageView.hidden = YES;
+            strongSelf.imageView.image = nil;
+            
+            // Update text and set progress to the given value
+            strongSelf.statusLabel.hidden = status.length == 0;
+            strongSelf.statusLabel.text = status;
+            strongSelf.progress = progress;
+            
+            // Choose the "right" indicator depending on the progress
+            if(progress >= 0) {
+                // Cancel the indefiniteAnimatedView, then show the ringLayer
+                [strongSelf cancelIndefiniteAnimatedViewAnimation];
+                
+                // Add ring to HUD
+                if(!strongSelf.ringView.superview){
+                    [strongSelf.hudView.contentView addSubview:strongSelf.ringView];
+                }
+                if(!strongSelf.backgroundRingView.superview){
+                    [strongSelf.hudView.contentView addSubview:strongSelf.backgroundRingView];
+                }
+                
+                // Set progress animated
+                [CATransaction begin];
+                [CATransaction setDisableActions:YES];
+                strongSelf.ringView.strokeEnd = progress;
+                [CATransaction commit];
+                
+                // Update the activity count
+                if(progress == 0) {
+                    strongSelf.activityCount++;
+                }
+            } else {
+                // Cancel the ringLayer animation, then show the indefiniteAnimatedView
+                [strongSelf cancelRingLayerAnimation];
+                
+                // Add indefiniteAnimatedView to HUD
+                [strongSelf.hudView.contentView addSubview:strongSelf.indefiniteAnimatedView];
+                if([strongSelf.indefiniteAnimatedView respondsToSelector:@selector(startAnimating)]) {
+                    [(id)strongSelf.indefiniteAnimatedView startAnimating];
+                }
+                
+                // Update the activity count
+                strongSelf.activityCount++;
+            }
+            
+            // Fade in delayed if a grace time is set
+            if (self.graceTimeInterval > 0.0 && self.backgroundView.alpha == 0.0f) {
+                strongSelf.graceTimer = [NSTimer timerWithTimeInterval:self.graceTimeInterval target:strongSelf selector:@selector(fadeIn:) userInfo:nil repeats:NO];
+                [[NSRunLoop mainRunLoop] addTimer:strongSelf.graceTimer forMode:NSRunLoopCommonModes];
+            } else {
+                [strongSelf fadeIn:nil];
+            }
+            
+            // Tell the Haptics Generator to prepare for feedback, which may come soon
+#if TARGET_OS_IOS
+            [strongSelf.hapticGenerator prepare];
+#endif
+        }
+    }];
+}
+
+- (void)showImage:(UIImage*)image status:(NSString*)status duration:(NSTimeInterval)duration {
+    __weak SVProgressHUD *weakSelf = self;
+    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
+        __strong SVProgressHUD *strongSelf = weakSelf;
+        if(strongSelf){
+            // Stop timer
+            strongSelf.fadeOutTimer = nil;
+            strongSelf.graceTimer = nil;
+            
+            // Update / Check view hierarchy to ensure the HUD is visible
+            [strongSelf updateViewHierarchy];
+            
+            // Reset progress and cancel any running animation
+            strongSelf.progress = SVProgressHUDUndefinedProgress;
+            [strongSelf cancelRingLayerAnimation];
+            [strongSelf cancelIndefiniteAnimatedViewAnimation];
+            
+            // Update imageView
+            if (self.shouldTintImages) {
+                if (image.renderingMode != UIImageRenderingModeAlwaysTemplate) {
+                    strongSelf.imageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+                } else {
+                    strongSelf.imageView.image = image;
+                }
+                strongSelf.imageView.tintColor = strongSelf.foregroundImageColorForStyle;
+            } else {
+                strongSelf.imageView.image = image;
+            }
+            strongSelf.imageView.hidden = NO;
+            
+            // Update text
+            strongSelf.statusLabel.hidden = status.length == 0;
+            strongSelf.statusLabel.text = status;
+            
+            // Fade in delayed if a grace time is set
+            // An image will be dismissed automatically. Thus pass the duration as userInfo.
+            if (self.graceTimeInterval > 0.0 && self.backgroundView.alpha == 0.0f) {
+                strongSelf.graceTimer = [NSTimer timerWithTimeInterval:self.graceTimeInterval target:strongSelf selector:@selector(fadeIn:) userInfo:@(duration) repeats:NO];
+                [[NSRunLoop mainRunLoop] addTimer:strongSelf.graceTimer forMode:NSRunLoopCommonModes];
+            } else {
+                [strongSelf fadeIn:@(duration)];
+            }
+        }
+    }];
+}
+
+- (void)fadeIn:(id)data {
+    // Update the HUDs frame to the new content and position HUD
+    [self updateHUDFrame];
+    [self positionHUD:nil];
+    
+    // Update accessibility as well as user interaction
+    // \n cause to read text twice so remove "\n" new line character before setting up accessiblity label
+    NSString *accessibilityString = [[self.statusLabel.text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@" "];
+    if(self.defaultMaskType != SVProgressHUDMaskTypeNone) {
+        self.controlView.userInteractionEnabled = YES;
+        self.accessibilityLabel =  accessibilityString ?: NSLocalizedString(@"Loading", nil);
+        self.isAccessibilityElement = YES;
+        self.controlView.accessibilityViewIsModal = YES;
+    } else {
+        self.controlView.userInteractionEnabled = NO;
+        self.hudView.accessibilityLabel = accessibilityString ?: NSLocalizedString(@"Loading", nil);
+        self.isAccessibilityElement = NO;
+        self.hudView.isAccessibilityElement = YES;
+        self.controlView.accessibilityViewIsModal = NO;
+    }
+    
+    // Get duration
+    id duration = [data isKindOfClass:[NSTimer class]] ? ((NSTimer *)data).userInfo : data;
+    
+    // Show if not already visible
+    if(self.backgroundView.alpha != 1.0f) {
+        // Post notification to inform user
+        [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDWillAppearNotification
+                                                            object:self
+                                                          userInfo:[self notificationUserInfo]];
+        
+        // Zoom HUD a little to to make a nice appear / pop up animation
+        self.hudView.transform = self.hudView.transform = CGAffineTransformScale(self.hudView.transform, 1.3f, 1.3f);
+        
+        __block void (^animationsBlock)(void) = ^{
+            // Zoom HUD a little to make a nice appear / pop up animation
+            self.hudView.transform = CGAffineTransformIdentity;
+            
+            // Fade in all effects (colors, blur, etc.)
+            [self fadeInEffects];
+        };
+        
+        __block void (^completionBlock)(void) = ^{
+            // Check if we really achieved to show the HUD (<=> alpha)
+            // and the change of these values has not been cancelled in between e.g. due to a dismissal
+            if(self.backgroundView.alpha == 1.0f){
+                // Register observer <=> we now have to handle orientation changes etc.
+                [self registerNotifications];
+                
+                // Post notification to inform user
+                [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidAppearNotification
+                                                                    object:self
+                                                                  userInfo:[self notificationUserInfo]];
+                
+                // Update accessibility
+                UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
+                UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.statusLabel.text);
+                
+                // Dismiss automatically if a duration was passed as userInfo. We start a timer
+                // which then will call dismiss after the predefined duration
+                if(duration){
+                    self.fadeOutTimer = [NSTimer timerWithTimeInterval:[(NSNumber *)duration doubleValue] target:self selector:@selector(dismiss) userInfo:nil repeats:NO];
+                    [[NSRunLoop mainRunLoop] addTimer:self.fadeOutTimer forMode:NSRunLoopCommonModes];
+                }
+            }
+        };
+        
+        // Animate appearance
+        if (self.fadeInAnimationDuration > 0) {
+            // Animate appearance
+            [UIView animateWithDuration:self.fadeInAnimationDuration
+                                  delay:0
+                                options:(UIViewAnimationOptions) (UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
+                             animations:^{
+                                 animationsBlock();
+                             } completion:^(BOOL finished) {
+                                 completionBlock();
+                             }];
+        } else {
+            animationsBlock();
+            completionBlock();
+        }
+        
+        // Inform iOS to redraw the view hierarchy
+        [self setNeedsDisplay];
+    } else {
+        // Update accessibility
+        UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
+        UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.statusLabel.text);
+        
+        // Dismiss automatically if a duration was passed as userInfo. We start a timer
+        // which then will call dismiss after the predefined duration
+        if(duration){
+            self.fadeOutTimer = [NSTimer timerWithTimeInterval:[(NSNumber *)duration doubleValue] target:self selector:@selector(dismiss) userInfo:nil repeats:NO];
+            [[NSRunLoop mainRunLoop] addTimer:self.fadeOutTimer forMode:NSRunLoopCommonModes];
+        }
+    }
+}
+
+- (void)dismiss {
+    [self dismissWithDelay:0.0 completion:nil];
+}
+
+- (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion {
+    __weak SVProgressHUD *weakSelf = self;
+    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
+        __strong SVProgressHUD *strongSelf = weakSelf;
+        if(strongSelf){
+            
+            // Post notification to inform user
+            [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDWillDisappearNotification
+                                                                object:nil
+                                                              userInfo:[strongSelf notificationUserInfo]];
+            
+            // Reset activity count
+            strongSelf.activityCount = 0;
+            
+            __block void (^animationsBlock)(void) = ^{
+                // Shrink HUD a little to make a nice disappear animation
+                strongSelf.hudView.transform = CGAffineTransformScale(strongSelf.hudView.transform, 1/1.3f, 1/1.3f);
+                
+                // Fade out all effects (colors, blur, etc.)
+                [strongSelf fadeOutEffects];
+            };
+            
+            __block void (^completionBlock)(void) = ^{
+                // Check if we really achieved to dismiss the HUD (<=> alpha values are applied)
+                // and the change of these values has not been cancelled in between e.g. due to a new show
+                if(self.backgroundView.alpha == 0.0f){
+                    // Clean up view hierarchy (overlays)
+                    [strongSelf.controlView removeFromSuperview];
+                    [strongSelf.backgroundView removeFromSuperview];
+                    [strongSelf.hudView removeFromSuperview];
+                    [strongSelf removeFromSuperview];
+                    
+                    // Reset progress and cancel any running animation
+                    strongSelf.progress = SVProgressHUDUndefinedProgress;
+                    [strongSelf cancelRingLayerAnimation];
+                    [strongSelf cancelIndefiniteAnimatedViewAnimation];
+                    
+                    // Remove observer <=> we do not have to handle orientation changes etc.
+                    [[NSNotificationCenter defaultCenter] removeObserver:strongSelf];
+                    
+                    // Post notification to inform user
+                    [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidDisappearNotification
+                                                                        object:strongSelf
+                                                                      userInfo:[strongSelf notificationUserInfo]];
+                    
+                    // Tell the rootViewController to update the StatusBar appearance
+#if !defined(SV_APP_EXTENSIONS) && TARGET_OS_IOS
+                    UIViewController *rootController = [SVProgressHUD mainWindow].rootViewController;
+                    [rootController setNeedsStatusBarAppearanceUpdate];
+#endif
+                    
+                    // Run an (optional) completionHandler
+                    if (completion) {
+                        completion();
+                    }
+                }
+            };
+            
+            // UIViewAnimationOptionBeginFromCurrentState AND a delay doesn't always work as expected
+            // When UIViewAnimationOptionBeginFromCurrentState is set, animateWithDuration: evaluates the current
+            // values to check if an animation is necessary. The evaluation happens at function call time and not
+            // after the delay => the animation is sometimes skipped. Therefore we delay using dispatch_after.
+            
+            dispatch_time_t dipatchTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC));
+            dispatch_after(dipatchTime, dispatch_get_main_queue(), ^{
+                
+                // Stop timer
+                strongSelf.graceTimer = nil;
+                
+                if (strongSelf.fadeOutAnimationDuration > 0) {
+                    // Animate appearance
+                    [UIView animateWithDuration:strongSelf.fadeOutAnimationDuration
+                                          delay:0
+                                        options:(UIViewAnimationOptions) (UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState)
+                                     animations:^{
+                                         animationsBlock();
+                                     } completion:^(BOOL finished) {
+                                         completionBlock();
+                                     }];
+                } else {
+                    animationsBlock();
+                    completionBlock();
+                }
+            });
+            
+            // Inform iOS to redraw the view hierarchy
+            [strongSelf setNeedsDisplay];
+        }
+    }];
+}
+
+
+#pragma mark - Ring progress animation
+
+- (UIView*)indefiniteAnimatedView {
+    // Get the correct spinner for defaultAnimationType
+    if(self.defaultAnimationType == SVProgressHUDAnimationTypeFlat){
+        // Check if spinner exists and is an object of different class
+        if(_indefiniteAnimatedView && ![_indefiniteAnimatedView isKindOfClass:[SVIndefiniteAnimatedView class]]){
+            [_indefiniteAnimatedView removeFromSuperview];
+            _indefiniteAnimatedView = nil;
+        }
+        
+        if(!_indefiniteAnimatedView){
+            _indefiniteAnimatedView = [[SVIndefiniteAnimatedView alloc] initWithFrame:CGRectZero];
+        }
+        
+        // Update styling
+        SVIndefiniteAnimatedView *indefiniteAnimatedView = (SVIndefiniteAnimatedView*)_indefiniteAnimatedView;
+        indefiniteAnimatedView.strokeColor = self.foregroundImageColorForStyle;
+        indefiniteAnimatedView.strokeThickness = self.ringThickness;
+        indefiniteAnimatedView.radius = self.statusLabel.text ? self.ringRadius : self.ringNoTextRadius;
+    } else {
+        // Check if spinner exists and is an object of different class
+        if(_indefiniteAnimatedView && ![_indefiniteAnimatedView isKindOfClass:[UIActivityIndicatorView class]]){
+            [_indefiniteAnimatedView removeFromSuperview];
+            _indefiniteAnimatedView = nil;
+        }
+        
+        if(!_indefiniteAnimatedView){
+            _indefiniteAnimatedView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
+        }
+        
+        // Update styling
+        UIActivityIndicatorView *activityIndicatorView = (UIActivityIndicatorView*)_indefiniteAnimatedView;
+        activityIndicatorView.color = self.foregroundImageColorForStyle;
+    }
+    [_indefiniteAnimatedView sizeToFit];
+    
+    return _indefiniteAnimatedView;
+}
+
+- (SVProgressAnimatedView*)ringView {
+    if(!_ringView) {
+        _ringView = [[SVProgressAnimatedView alloc] initWithFrame:CGRectZero];
+    }
+    
+    // Update styling
+    _ringView.strokeColor = self.foregroundImageColorForStyle;
+    _ringView.strokeThickness = self.ringThickness;
+    _ringView.radius = self.statusLabel.text ? self.ringRadius : self.ringNoTextRadius;
+    
+    return _ringView;
+}
+
+- (SVProgressAnimatedView*)backgroundRingView {
+    if(!_backgroundRingView) {
+        _backgroundRingView = [[SVProgressAnimatedView alloc] initWithFrame:CGRectZero];
+        _backgroundRingView.strokeEnd = 1.0f;
+    }
+    
+    // Update styling
+    _backgroundRingView.strokeColor = [self.foregroundImageColorForStyle colorWithAlphaComponent:0.1f];
+    _backgroundRingView.strokeThickness = self.ringThickness;
+    _backgroundRingView.radius = self.statusLabel.text ? self.ringRadius : self.ringNoTextRadius;
+    
+    return _backgroundRingView;
+}
+
+- (void)cancelRingLayerAnimation {
+    // Animate value update, stop animation
+    [CATransaction begin];
+    [CATransaction setDisableActions:YES];
+    
+    [self.hudView.layer removeAllAnimations];
+    self.ringView.strokeEnd = 0.0f;
+    
+    [CATransaction commit];
+    
+    // Remove from view
+    [self.ringView removeFromSuperview];
+    [self.backgroundRingView removeFromSuperview];
+}
+
+- (void)cancelIndefiniteAnimatedViewAnimation {
+    // Stop animation
+    if([self.indefiniteAnimatedView respondsToSelector:@selector(stopAnimating)]) {
+        [(id)self.indefiniteAnimatedView stopAnimating];
+    }
+    // Remove from view
+    [self.indefiniteAnimatedView removeFromSuperview];
+}
+
+
+#pragma mark - Utilities
+
++ (BOOL)isVisible {
+    // Checking one alpha value is sufficient as they are all the same
+    return [self sharedView].backgroundView.alpha > 0.0f;
+}
+
+
+#pragma mark - Getters
+
++ (NSTimeInterval)displayDurationForString:(NSString*)string {
+    CGFloat minimum = MAX((CGFloat)string.length * 0.06 + 0.5, [self sharedView].minimumDismissTimeInterval);
+    return MIN(minimum, [self sharedView].maximumDismissTimeInterval);
+}
+
+- (UIColor*)foregroundColorForStyle {
+    SVProgressHUDStyle style = [self defaultStyleResolvingAutomatic];
+    
+    if(style == SVProgressHUDStyleLight) {
+        return [UIColor blackColor];
+    } else if(style == SVProgressHUDStyleDark) {
+        return [UIColor whiteColor];
+    } else {
+        return self.foregroundColor;
+    }
+}
+
+- (UIColor*)foregroundImageColorForStyle {
+    if (self.foregroundImageColor) {
+        return self.foregroundImageColor;
+    } else {
+        return [self foregroundColorForStyle];
+    }
+}
+
+- (UIColor*)backgroundColorForStyle {
+    SVProgressHUDStyle style = [self defaultStyleResolvingAutomatic];
+
+    if(style == SVProgressHUDStyleLight) {
+        return [UIColor whiteColor];
+    } else if(style == SVProgressHUDStyleDark) {
+        return [UIColor blackColor];
+    } else {
+        return self.backgroundColor;
+    }
+}
+
+- (UIControl*)controlView {
+    if(!_controlView) {
+        _controlView = [UIControl new];
+        _controlView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+        _controlView.backgroundColor = [UIColor clearColor];
+        _controlView.userInteractionEnabled = YES;
+        [_controlView addTarget:self action:@selector(controlViewDidReceiveTouchEvent:forEvent:) forControlEvents:UIControlEventTouchDown];
+    }
+    
+    // Update frame
+#if !defined(SV_APP_EXTENSIONS)
+    _controlView.frame = [SVProgressHUD mainWindow].bounds;
+#else
+    _controlView.frame = [UIScreen mainScreen].bounds;
+#endif
+    
+    return _controlView;
+}
+
+-(UIView *)backgroundView {
+    if(!_backgroundView){
+        _backgroundView = [UIView new];
+        _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    }
+    if(!_backgroundView.superview){
+        [self insertSubview:_backgroundView belowSubview:self.hudView];
+    }
+    
+    // Update styling
+    if(self.defaultMaskType == SVProgressHUDMaskTypeGradient){
+        if(!_backgroundRadialGradientLayer){
+            _backgroundRadialGradientLayer = [SVRadialGradientLayer layer];
+        }
+        if(!_backgroundRadialGradientLayer.superlayer){
+            [_backgroundView.layer insertSublayer:_backgroundRadialGradientLayer atIndex:0];
+        }
+        _backgroundView.backgroundColor = [UIColor clearColor];
+    } else {
+        if(_backgroundRadialGradientLayer && _backgroundRadialGradientLayer.superlayer){
+            [_backgroundRadialGradientLayer removeFromSuperlayer];
+        }
+        if(self.defaultMaskType == SVProgressHUDMaskTypeBlack){
+            _backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.4];
+        } else if(self.defaultMaskType == SVProgressHUDMaskTypeCustom){
+            _backgroundView.backgroundColor = self.backgroundLayerColor;
+        } else {
+            _backgroundView.backgroundColor = [UIColor clearColor];
+        }
+    }
+
+    // Update frame
+    if(_backgroundView){
+        _backgroundView.frame = self.bounds;
+    }
+    if(_backgroundRadialGradientLayer){
+        _backgroundRadialGradientLayer.frame = self.bounds;
+        
+        // Calculate the new center of the gradient, it may change if keyboard is visible
+        CGPoint gradientCenter = self.center;
+        gradientCenter.y = (self.bounds.size.height - self.visibleKeyboardHeight)/2;
+        _backgroundRadialGradientLayer.gradientCenter = gradientCenter;
+        [_backgroundRadialGradientLayer setNeedsDisplay];
+    }
+    
+    return _backgroundView;
+}
+- (UIVisualEffectView*)hudView {
+    if(!_hudView) {
+        _hudView = [UIVisualEffectView new];
+        _hudView.layer.masksToBounds = YES;
+        _hudView.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin;
+    }
+    if(!_hudView.superview) {
+        [self addSubview:_hudView];
+    }
+    
+    // Update styling
+    _hudView.layer.cornerRadius = self.cornerRadius;
+    
+    return _hudView;
+}
+
+- (UILabel*)statusLabel {
+    if(!_statusLabel) {
+        _statusLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+        _statusLabel.backgroundColor = [UIColor clearColor];
+        _statusLabel.adjustsFontSizeToFitWidth = YES;
+        _statusLabel.adjustsFontForContentSizeCategory = YES;
+        _statusLabel.textAlignment = NSTextAlignmentCenter;
+        _statusLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters;
+        _statusLabel.numberOfLines = 0;
+    }
+    if(!_statusLabel.superview) {
+      [self.hudView.contentView addSubview:_statusLabel];
+    }
+    
+    // Update styling
+    _statusLabel.textColor = self.foregroundColorForStyle;
+    _statusLabel.font = self.font;
+
+    return _statusLabel;
+}
+
+- (UIImageView*)imageView {
+    if(_imageView && !CGSizeEqualToSize(_imageView.bounds.size, _imageViewSize)) {
+        [_imageView removeFromSuperview];
+        _imageView = nil;
+    }
+    
+    if(!_imageView) {
+        _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, _imageViewSize.width, _imageViewSize.height)];
+    }
+    if(!_imageView.superview) {
+        [self.hudView.contentView addSubview:_imageView];
+    }
+    
+    return _imageView;
+}
+
+
+#pragma mark - Helper
+
+- (SVProgressHUDStyle) defaultStyleResolvingAutomatic {
+    if(self.defaultStyle == SVProgressHUDStyleAutomatic) {
+        return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? SVProgressHUDStyleDark : SVProgressHUDStyleLight;
+    }
+    
+    return self.defaultStyle;
+}
+
+- (CGFloat)visibleKeyboardHeight {
+#if !defined(SV_APP_EXTENSIONS)
+    UIWindow *keyboardWindow = nil;
+    for (UIWindow *testWindow in UIApplication.sharedApplication.windows) {
+        if(![testWindow.class isEqual:UIWindow.class]) {
+            keyboardWindow = testWindow;
+            break;
+        }
+    }
+    
+    for (__strong UIView *possibleKeyboard in keyboardWindow.subviews) {
+        NSString *viewName = NSStringFromClass(possibleKeyboard.class);
+        if([viewName hasPrefix:@"UI"]){
+            if([viewName hasSuffix:@"PeripheralHostView"] || [viewName hasSuffix:@"Keyboard"]){
+                return CGRectGetHeight(possibleKeyboard.bounds);
+            } else if ([viewName hasSuffix:@"InputSetContainerView"]){
+                for (__strong UIView *possibleKeyboardSubview in possibleKeyboard.subviews) {
+                    viewName = NSStringFromClass(possibleKeyboardSubview.class);
+                    if([viewName hasPrefix:@"UI"] && [viewName hasSuffix:@"InputSetHostView"]) {
+                        CGRect convertedRect = [possibleKeyboard convertRect:possibleKeyboardSubview.frame toView:self];
+                        CGRect intersectedRect = CGRectIntersection(convertedRect, self.bounds);
+                        if (!CGRectIsNull(intersectedRect)) {
+                            return CGRectGetHeight(intersectedRect);
+                        }
+                    }
+                }
+            }
+        }
+    }
+#endif
+    return 0;
+}
+    
+- (UIWindow *)frontWindow {
+#if !defined(SV_APP_EXTENSIONS)
+    NSEnumerator *frontToBackWindows = [UIApplication.sharedApplication.windows reverseObjectEnumerator];
+    for (UIWindow *window in frontToBackWindows) {
+        BOOL windowOnMainScreen = window.screen == UIScreen.mainScreen;
+        BOOL windowIsVisible = !window.hidden && window.alpha > 0;
+        BOOL windowLevelSupported = (window.windowLevel >= UIWindowLevelNormal && window.windowLevel <= self.maxSupportedWindowLevel);
+        BOOL windowKeyWindow = window.isKeyWindow;
+			
+        if(windowOnMainScreen && windowIsVisible && windowLevelSupported && windowKeyWindow) {
+            return window;
+        }
+    }
+#endif
+    return nil;
+}
+    
+- (void)fadeInEffects {
+    if(self.defaultStyle != SVProgressHUDStyleCustom) {
+        // Add blur effect
+        UIBlurEffectStyle blurEffectStyle;
+#if TARGET_OS_IOS
+        if (@available(iOS 13.0, *)) {
+            blurEffectStyle = [self defaultStyleResolvingAutomatic] == SVProgressHUDStyleLight ? UIBlurEffectStyleSystemMaterial : UIBlurEffectStyleSystemMaterialDark;
+        } else {
+            blurEffectStyle = [self defaultStyleResolvingAutomatic] == SVProgressHUDStyleLight ? UIBlurEffectStyleLight : UIBlurEffectStyleDark;
+        }
+#else
+        blurEffectStyle = [self defaultStyleResolvingAutomatic] == SVProgressHUDStyleLight ? UIBlurEffectStyleLight : UIBlurEffectStyleDark;
+#endif
+        
+        UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:blurEffectStyle];
+        self.hudView.effect = blurEffect;
+        
+        // We omit UIVibrancy effect and use a suitable background color as an alternative.
+        // This will make everything more readable. See the following for details:
+        // https://www.omnigroup.com/developer/how-to-make-text-in-a-uivisualeffectview-readable-on-any-background
+        
+        self.hudView.backgroundColor = [self.backgroundColorForStyle colorWithAlphaComponent:0.6f];
+    } else {
+        self.hudView.effect = self.hudViewCustomBlurEffect;
+        self.hudView.backgroundColor =  self.backgroundColorForStyle;
+    }
+
+    // Fade in views
+    self.backgroundView.alpha = 1.0f;
+    
+    self.imageView.alpha = 1.0f;
+    self.statusLabel.alpha = 1.0f;
+    self.indefiniteAnimatedView.alpha = 1.0f;
+    self.ringView.alpha = self.backgroundRingView.alpha = 1.0f;
+}
+
+- (void)fadeOutEffects
+{
+    if(self.defaultStyle != SVProgressHUDStyleCustom) {
+        // Remove blur effect
+        self.hudView.effect = nil;
+    }
+
+    // Remove background color
+    self.hudView.backgroundColor = [UIColor clearColor];
+    
+    // Fade out views
+    self.backgroundView.alpha = 0.0f;
+    
+    self.imageView.alpha = 0.0f;
+    self.statusLabel.alpha = 0.0f;
+    self.indefiniteAnimatedView.alpha = 0.0f;
+    self.ringView.alpha = self.backgroundRingView.alpha = 0.0f;
+}
+
+#if TARGET_OS_IOS
+- (UINotificationFeedbackGenerator *)hapticGenerator {
+	// Only return if haptics are enabled
+	if(!self.hapticsEnabled) {
+		return nil;
+	}
+	
+	if(!_hapticGenerator) {
+		_hapticGenerator = [[UINotificationFeedbackGenerator alloc] init];
+	}
+	return _hapticGenerator;
+}
+#endif
+
+    
+#pragma mark - UIAppearance Setters
+
+- (void)setDefaultStyle:(SVProgressHUDStyle)style {
+    if (!_isInitializing) _defaultStyle = style;
+}
+
+- (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType {
+    if (!_isInitializing) _defaultMaskType = maskType;
+}
+
+- (void)setDefaultAnimationType:(SVProgressHUDAnimationType)animationType {
+    if (!_isInitializing) _defaultAnimationType = animationType;
+}
+
+- (void)setContainerView:(UIView *)containerView {
+    if (!_isInitializing) _containerView = containerView;
+}
+
+- (void)setMinimumSize:(CGSize)minimumSize {
+    if (!_isInitializing) _minimumSize = minimumSize;
+}
+
+- (void)setRingThickness:(CGFloat)ringThickness {
+    if (!_isInitializing) _ringThickness = ringThickness;
+}
+
+- (void)setRingRadius:(CGFloat)ringRadius {
+    if (!_isInitializing) _ringRadius = ringRadius;
+}
+
+- (void)setRingNoTextRadius:(CGFloat)ringNoTextRadius {
+    if (!_isInitializing) _ringNoTextRadius = ringNoTextRadius;
+}
+
+- (void)setCornerRadius:(CGFloat)cornerRadius {
+    if (!_isInitializing) _cornerRadius = cornerRadius;
+}
+
+- (void)setFont:(UIFont*)font {
+    if (!_isInitializing) _font = font;
+}
+
+- (void)setForegroundColor:(UIColor*)color {
+    if (!_isInitializing) _foregroundColor = color;
+}
+
+- (void)setForegroundImageColor:(UIColor *)color {
+    if (!_isInitializing) _foregroundImageColor = color;
+}
+
+- (void)setBackgroundColor:(UIColor*)color {
+    if (!_isInitializing) _backgroundColor = color;
+}
+
+- (void)setBackgroundLayerColor:(UIColor*)color {
+    if (!_isInitializing) _backgroundLayerColor = color;
+}
+
+- (void)setShouldTintImages:(BOOL)shouldTintImages {
+    if (!_isInitializing) _shouldTintImages = shouldTintImages;
+}
+
+- (void)setInfoImage:(UIImage*)image {
+    if (!_isInitializing) _infoImage = image;
+}
+
+- (void)setSuccessImage:(UIImage*)image {
+    if (!_isInitializing) _successImage = image;
+}
+
+- (void)setErrorImage:(UIImage*)image {
+    if (!_isInitializing) _errorImage = image;
+}
+
+- (void)setViewForExtension:(UIView*)view {
+    if (!_isInitializing) _viewForExtension = view;
+}
+
+- (void)setOffsetFromCenter:(UIOffset)offset {
+    if (!_isInitializing) _offsetFromCenter = offset;
+}
+
+- (void)setMinimumDismissTimeInterval:(NSTimeInterval)minimumDismissTimeInterval {
+    if (!_isInitializing) _minimumDismissTimeInterval = minimumDismissTimeInterval;
+}
+
+- (void)setFadeInAnimationDuration:(NSTimeInterval)duration {
+    if (!_isInitializing) _fadeInAnimationDuration = duration;
+}
+
+- (void)setFadeOutAnimationDuration:(NSTimeInterval)duration {
+    if (!_isInitializing) _fadeOutAnimationDuration = duration;
+}
+
+- (void)setMaxSupportedWindowLevel:(UIWindowLevel)maxSupportedWindowLevel {
+    if (!_isInitializing) _maxSupportedWindowLevel = maxSupportedWindowLevel;
+}
+
+@end

+ 14 - 0
Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.h

@@ -0,0 +1,14 @@
+//
+//  SVRadialGradientLayer.h
+//  SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD
+//
+//  Copyright (c) 2014-2023 Tobias Totzek and contributors. All rights reserved.
+//
+
+#import <QuartzCore/QuartzCore.h>
+
+@interface SVRadialGradientLayer : CALayer
+
+@property (nonatomic) CGPoint gradientCenter;
+
+@end

+ 25 - 0
Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.m

@@ -0,0 +1,25 @@
+//
+//  SVRadialGradientLayer.m
+//  SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD
+//
+//  Copyright (c) 2014-2023 Tobias Totzek and contributors. All rights reserved.
+//
+
+#import "SVRadialGradientLayer.h"
+
+@implementation SVRadialGradientLayer
+
+- (void)drawInContext:(CGContextRef)context {
+    size_t locationsCount = 2;
+    CGFloat locations[2] = {0.0f, 1.0f};
+    CGFloat colors[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.75f};
+    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
+    CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, colors, locations, locationsCount);
+    CGColorSpaceRelease(colorSpace);
+
+    float radius = MIN(self.bounds.size.width , self.bounds.size.height);
+    CGContextDrawRadialGradient (context, gradient, self.gradientCenter, 0, self.gradientCenter, radius, kCGGradientDrawsAfterEndLocation);
+    CGGradientRelease(gradient);
+}
+
+@end

+ 1 - 1
Pods/Target Support Files/Toast-Swift/Toast-Swift-Info.plist → Pods/Target Support Files/BSText/BSText-Info.plist

@@ -15,7 +15,7 @@
   <key>CFBundlePackageType</key>
   <string>FMWK</string>
   <key>CFBundleShortVersionString</key>
-  <string>5.1.1</string>
+  <string>1.1.3</string>
   <key>CFBundleSignature</key>
   <string>????</string>
   <key>CFBundleVersion</key>

+ 5 - 0
Pods/Target Support Files/BSText/BSText-dummy.m

@@ -0,0 +1,5 @@
+#import <Foundation/Foundation.h>
+@interface PodsDummy_BSText : NSObject
+@end
+@implementation PodsDummy_BSText
+@end

+ 0 - 0
Pods/Target Support Files/Toast-Swift/Toast-Swift-prefix.pch → Pods/Target Support Files/BSText/BSText-prefix.pch


+ 17 - 0
Pods/Target Support Files/BSText/BSText-umbrella.h

@@ -0,0 +1,17 @@
+#ifdef __OBJC__
+#import <UIKit/UIKit.h>
+#else
+#ifndef FOUNDATION_EXPORT
+#if defined(__cplusplus)
+#define FOUNDATION_EXPORT extern "C"
+#else
+#define FOUNDATION_EXPORT extern
+#endif
+#endif
+#endif
+
+#import "BSText.h"
+
+FOUNDATION_EXPORT double BSTextVersionNumber;
+FOUNDATION_EXPORT const unsigned char BSTextVersionString[];
+

+ 16 - 0
Pods/Target Support Files/BSText/BSText.debug.xcconfig

@@ -0,0 +1,16 @@
+CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
+CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/BSText
+FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/YYImage"
+GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
+LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
+OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "AssetsLibrary" -framework "CoreFoundation" -framework "ImageIO" -framework "MobileCoreServices" -framework "QuartzCore" -framework "UIKit" -framework "YYImage"
+OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings
+PODS_BUILD_DIR = ${BUILD_DIR}
+PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
+PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
+PODS_ROOT = ${SRCROOT}
+PODS_TARGET_SRCROOT = ${PODS_ROOT}/BSText
+PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
+PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
+SKIP_INSTALL = YES
+USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

+ 6 - 0
Pods/Target Support Files/BSText/BSText.modulemap

@@ -0,0 +1,6 @@
+framework module BSText {
+  umbrella header "BSText-umbrella.h"
+
+  export *
+  module * { export * }
+}

+ 16 - 0
Pods/Target Support Files/BSText/BSText.release.xcconfig

@@ -0,0 +1,16 @@
+CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
+CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/BSText
+FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/YYImage"
+GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
+LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
+OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "AssetsLibrary" -framework "CoreFoundation" -framework "ImageIO" -framework "MobileCoreServices" -framework "QuartzCore" -framework "UIKit" -framework "YYImage"
+OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings
+PODS_BUILD_DIR = ${BUILD_DIR}
+PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
+PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
+PODS_ROOT = ${SRCROOT}
+PODS_TARGET_SRCROOT = ${PODS_ROOT}/BSText
+PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
+PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
+SKIP_INSTALL = YES
+USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

+ 70 - 18
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-acknowledgements.markdown

@@ -1,6 +1,31 @@
 # Acknowledgements
 This application makes use of the following third party libraries:
 
+## BSText
+
+MIT License
+
+Copyright (c) 2019 Bruce.Liu
+
+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.
+
+
 ## IQKeyboardManagerSwift
 
 MIT License
@@ -87,6 +112,31 @@ The above copyright notice and this permission notice shall be included in all c
 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.
 
 
+## SVProgressHUD
+
+MIT License
+
+Copyright (c) 2011-2023 Sam Vermette, Tobias Totzek and contributors.
+
+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.
+
+
 ## SnapKit
 
 Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
@@ -135,27 +185,29 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 
 
-## Toast-Swift
+## YYImage
+
+The MIT License (MIT)
+
+Copyright (c) 2015 ibireme <ibireme@gmail.com>
 
-Copyright (c) 2015-2024 Charles Scalesse.
+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:
 
-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 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.
 
-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.
 
 Generated by CocoaPods - https://cocoapods.org

+ 85 - 21
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-acknowledgements.plist

@@ -16,6 +16,37 @@
 			<key>FooterText</key>
 			<string>MIT License
 
+Copyright (c) 2019 Bruce.Liu
+
+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.
+</string>
+			<key>License</key>
+			<string>MIT</string>
+			<key>Title</key>
+			<string>BSText</string>
+			<key>Type</key>
+			<string>PSGroupSpecifier</string>
+		</dict>
+		<dict>
+			<key>FooterText</key>
+			<string>MIT License
+
 Copyright (c) 2013-2017 Iftekhar Qurashi
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -122,6 +153,37 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
 			<key>Type</key>
 			<string>PSGroupSpecifier</string>
 		</dict>
+		<dict>
+			<key>FooterText</key>
+			<string>MIT License
+
+Copyright (c) 2011-2023 Sam Vermette, Tobias Totzek and contributors.
+
+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.
+</string>
+			<key>License</key>
+			<string>MIT</string>
+			<key>Title</key>
+			<string>SVProgressHUD</string>
+			<key>Type</key>
+			<string>PSGroupSpecifier</string>
+		</dict>
 		<dict>
 			<key>FooterText</key>
 			<string>Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
@@ -184,31 +246,33 @@ SOFTWARE.
 		</dict>
 		<dict>
 			<key>FooterText</key>
-			<string>Copyright (c) 2015-2024 Charles Scalesse.
-
-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.
+			<string>The MIT License (MIT)
+
+Copyright (c) 2015 ibireme &lt;ibireme@gmail.com&gt;
+
+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.
+
 </string>
 			<key>License</key>
 			<string>MIT</string>
 			<key>Title</key>
-			<string>Toast-Swift</string>
+			<string>YYImage</string>
 			<key>Type</key>
 			<string>PSGroupSpecifier</string>
 		</dict>

+ 3 - 1
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks-Debug-input-files.xcfilelist

@@ -1,8 +1,10 @@
 ${PODS_ROOT}/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks.sh
+${BUILT_PRODUCTS_DIR}/BSText/BSText.framework
 ${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework
 ${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework
 ${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework
 ${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework
+${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework
 ${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
 ${BUILT_PRODUCTS_DIR}/TYCyclePagerView/TYCyclePagerView.framework
-${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework
+${BUILT_PRODUCTS_DIR}/YYImage/YYImage.framework

+ 3 - 1
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks-Debug-output-files.xcfilelist

@@ -1,7 +1,9 @@
+${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BSText.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MJRefresh.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ObjectMapper.framework
+${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TYCyclePagerView.framework
-${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast_Swift.framework
+${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YYImage.framework

+ 3 - 1
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks-Release-input-files.xcfilelist

@@ -1,8 +1,10 @@
 ${PODS_ROOT}/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks.sh
+${BUILT_PRODUCTS_DIR}/BSText/BSText.framework
 ${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework
 ${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework
 ${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework
 ${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework
+${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework
 ${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
 ${BUILT_PRODUCTS_DIR}/TYCyclePagerView/TYCyclePagerView.framework
-${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework
+${BUILT_PRODUCTS_DIR}/YYImage/YYImage.framework

+ 3 - 1
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks-Release-output-files.xcfilelist

@@ -1,7 +1,9 @@
+${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BSText.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MJRefresh.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ObjectMapper.framework
+${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework
 ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TYCyclePagerView.framework
-${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast_Swift.framework
+${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YYImage.framework

+ 6 - 2
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper-frameworks.sh

@@ -176,22 +176,26 @@ code_sign_if_enabled() {
 }
 
 if [[ "$CONFIGURATION" == "Debug" ]]; then
+  install_framework "${BUILT_PRODUCTS_DIR}/BSText/BSText.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework"
+  install_framework "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/TYCyclePagerView/TYCyclePagerView.framework"
-  install_framework "${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework"
+  install_framework "${BUILT_PRODUCTS_DIR}/YYImage/YYImage.framework"
 fi
 if [[ "$CONFIGURATION" == "Release" ]]; then
+  install_framework "${BUILT_PRODUCTS_DIR}/BSText/BSText.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework"
+  install_framework "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
   install_framework "${BUILT_PRODUCTS_DIR}/TYCyclePagerView/TYCyclePagerView.framework"
-  install_framework "${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework"
+  install_framework "${BUILT_PRODUCTS_DIR}/YYImage/YYImage.framework"
 fi
 if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
   wait

+ 5 - 5
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper.debug.xcconfig

@@ -1,13 +1,13 @@
 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
 CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
-FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" "${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift"
+FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BSText" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" "${PODS_CONFIGURATION_BUILD_DIR}/YYImage"
 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
-HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView/TYCyclePagerView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift/Toast_Swift.framework/Headers"
+HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BSText/BSText.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView/TYCyclePagerView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YYImage/YYImage.framework/Headers"
 LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
 LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift
-OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView/TYCyclePagerView.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift/Toast_Swift.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift"
-OTHER_LDFLAGS = $(inherited) -l"swiftCoreGraphics" -framework "Accelerate" -framework "CFNetwork" -framework "CoreGraphics" -framework "Foundation" -framework "IQKeyboardManagerSwift" -framework "Kingfisher" -framework "MJRefresh" -framework "ObjectMapper" -framework "QuartzCore" -framework "SnapKit" -framework "TYCyclePagerView" -framework "Toast_Swift" -framework "UIKit" -weak_framework "Combine" -weak_framework "SwiftUI"
-OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "-F${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "-F${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "-F${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "-F${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" "-F${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift"
+OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/BSText/BSText.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView/TYCyclePagerView.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/YYImage/YYImage.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/BSText" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/YYImage"
+OTHER_LDFLAGS = $(inherited) -l"swiftCoreGraphics" -l"z" -framework "Accelerate" -framework "AssetsLibrary" -framework "BSText" -framework "CFNetwork" -framework "CoreFoundation" -framework "CoreGraphics" -framework "Foundation" -framework "IQKeyboardManagerSwift" -framework "ImageIO" -framework "Kingfisher" -framework "MJRefresh" -framework "MobileCoreServices" -framework "ObjectMapper" -framework "QuartzCore" -framework "SVProgressHUD" -framework "SnapKit" -framework "TYCyclePagerView" -framework "UIKit" -framework "YYImage" -weak_framework "Combine" -weak_framework "SwiftUI"
+OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/BSText" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "-F${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "-F${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "-F${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "-F${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "-F${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" "-F${PODS_CONFIGURATION_BUILD_DIR}/YYImage"
 OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
 PODS_BUILD_DIR = ${BUILD_DIR}
 PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

+ 5 - 5
Pods/Target Support Files/Pods-TSLiveWallpaper/Pods-TSLiveWallpaper.release.xcconfig

@@ -1,13 +1,13 @@
 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
 CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
-FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" "${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift"
+FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BSText" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" "${PODS_CONFIGURATION_BUILD_DIR}/YYImage"
 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
-HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView/TYCyclePagerView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift/Toast_Swift.framework/Headers"
+HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/BSText/BSText.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView/TYCyclePagerView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YYImage/YYImage.framework/Headers"
 LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
 LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift
-OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView/TYCyclePagerView.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift/Toast_Swift.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift"
-OTHER_LDFLAGS = $(inherited) -l"swiftCoreGraphics" -framework "Accelerate" -framework "CFNetwork" -framework "CoreGraphics" -framework "Foundation" -framework "IQKeyboardManagerSwift" -framework "Kingfisher" -framework "MJRefresh" -framework "ObjectMapper" -framework "QuartzCore" -framework "SnapKit" -framework "TYCyclePagerView" -framework "Toast_Swift" -framework "UIKit" -weak_framework "Combine" -weak_framework "SwiftUI"
-OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "-F${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "-F${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "-F${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "-F${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" "-F${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift"
+OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/BSText/BSText.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView/TYCyclePagerView.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/YYImage/YYImage.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/BSText" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/YYImage"
+OTHER_LDFLAGS = $(inherited) -l"swiftCoreGraphics" -l"z" -framework "Accelerate" -framework "AssetsLibrary" -framework "BSText" -framework "CFNetwork" -framework "CoreFoundation" -framework "CoreGraphics" -framework "Foundation" -framework "IQKeyboardManagerSwift" -framework "ImageIO" -framework "Kingfisher" -framework "MJRefresh" -framework "MobileCoreServices" -framework "ObjectMapper" -framework "QuartzCore" -framework "SVProgressHUD" -framework "SnapKit" -framework "TYCyclePagerView" -framework "UIKit" -framework "YYImage" -weak_framework "Combine" -weak_framework "SwiftUI"
+OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/BSText" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "-F${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "-F${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "-F${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "-F${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "-F${PODS_CONFIGURATION_BUILD_DIR}/TYCyclePagerView" "-F${PODS_CONFIGURATION_BUILD_DIR}/YYImage"
 OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
 PODS_BUILD_DIR = ${BUILD_DIR}
 PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

+ 5 - 3
Pods/Target Support Files/Toast-Swift/ResourceBundle-Toast-Swift-Toast-Swift-Info.plist → Pods/Target Support Files/SVProgressHUD/SVProgressHUD-Info.plist

@@ -4,6 +4,8 @@
 <dict>
   <key>CFBundleDevelopmentRegion</key>
   <string>${PODS_DEVELOPMENT_LANGUAGE}</string>
+  <key>CFBundleExecutable</key>
+  <string>${EXECUTABLE_NAME}</string>
   <key>CFBundleIdentifier</key>
   <string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
   <key>CFBundleInfoDictionaryVersion</key>
@@ -11,13 +13,13 @@
   <key>CFBundleName</key>
   <string>${PRODUCT_NAME}</string>
   <key>CFBundlePackageType</key>
-  <string>BNDL</string>
+  <string>FMWK</string>
   <key>CFBundleShortVersionString</key>
-  <string>5.1.1</string>
+  <string>2.3.1</string>
   <key>CFBundleSignature</key>
   <string>????</string>
   <key>CFBundleVersion</key>
-  <string>1</string>
+  <string>${CURRENT_PROJECT_VERSION}</string>
   <key>NSPrincipalClass</key>
   <string></string>
 </dict>

+ 5 - 0
Pods/Target Support Files/SVProgressHUD/SVProgressHUD-dummy.m

@@ -0,0 +1,5 @@
+#import <Foundation/Foundation.h>
+@interface PodsDummy_SVProgressHUD : NSObject
+@end
+@implementation PodsDummy_SVProgressHUD
+@end

+ 0 - 4
Pods/Target Support Files/Toast-Swift/Toast-Swift-umbrella.h → Pods/Target Support Files/SVProgressHUD/SVProgressHUD-prefix.pch

@@ -10,7 +10,3 @@
 #endif
 #endif
 
-
-FOUNDATION_EXPORT double Toast_SwiftVersionNumber;
-FOUNDATION_EXPORT const unsigned char Toast_SwiftVersionString[];
-

+ 20 - 0
Pods/Target Support Files/SVProgressHUD/SVProgressHUD-umbrella.h

@@ -0,0 +1,20 @@
+#ifdef __OBJC__
+#import <UIKit/UIKit.h>
+#else
+#ifndef FOUNDATION_EXPORT
+#if defined(__cplusplus)
+#define FOUNDATION_EXPORT extern "C"
+#else
+#define FOUNDATION_EXPORT extern
+#endif
+#endif
+#endif
+
+#import "SVIndefiniteAnimatedView.h"
+#import "SVProgressAnimatedView.h"
+#import "SVProgressHUD.h"
+#import "SVRadialGradientLayer.h"
+
+FOUNDATION_EXPORT double SVProgressHUDVersionNumber;
+FOUNDATION_EXPORT const unsigned char SVProgressHUDVersionString[];
+

+ 2 - 4
Pods/Target Support Files/Toast-Swift/Toast-Swift.debug.xcconfig → Pods/Target Support Files/SVProgressHUD/SVProgressHUD.debug.xcconfig

@@ -1,14 +1,12 @@
 CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
-CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift
+CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD
 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
-LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
 OTHER_LDFLAGS = $(inherited) -framework "QuartzCore"
-OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings
 PODS_BUILD_DIR = ${BUILD_DIR}
 PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
 PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
 PODS_ROOT = ${SRCROOT}
-PODS_TARGET_SRCROOT = ${PODS_ROOT}/Toast-Swift
+PODS_TARGET_SRCROOT = ${PODS_ROOT}/SVProgressHUD
 PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
 PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
 SKIP_INSTALL = YES

+ 6 - 0
Pods/Target Support Files/SVProgressHUD/SVProgressHUD.modulemap

@@ -0,0 +1,6 @@
+framework module SVProgressHUD {
+  umbrella header "SVProgressHUD-umbrella.h"
+
+  export *
+  module * { export * }
+}

+ 2 - 4
Pods/Target Support Files/Toast-Swift/Toast-Swift.release.xcconfig → Pods/Target Support Files/SVProgressHUD/SVProgressHUD.release.xcconfig

@@ -1,14 +1,12 @@
 CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
-CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Toast-Swift
+CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD
 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
-LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
 OTHER_LDFLAGS = $(inherited) -framework "QuartzCore"
-OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings
 PODS_BUILD_DIR = ${BUILD_DIR}
 PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
 PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
 PODS_ROOT = ${SRCROOT}
-PODS_TARGET_SRCROOT = ${PODS_ROOT}/Toast-Swift
+PODS_TARGET_SRCROOT = ${PODS_ROOT}/SVProgressHUD
 PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
 PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
 SKIP_INSTALL = YES

+ 0 - 5
Pods/Target Support Files/Toast-Swift/Toast-Swift-dummy.m

@@ -1,5 +0,0 @@
-#import <Foundation/Foundation.h>
-@interface PodsDummy_Toast_Swift : NSObject
-@end
-@implementation PodsDummy_Toast_Swift
-@end

+ 0 - 6
Pods/Target Support Files/Toast-Swift/Toast-Swift.modulemap

@@ -1,6 +0,0 @@
-framework module Toast_Swift {
-  umbrella header "Toast-Swift-umbrella.h"
-
-  export *
-  module * { export * }
-}

+ 26 - 0
Pods/Target Support Files/YYImage/YYImage-Info.plist

@@ -0,0 +1,26 @@
+<?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>CFBundleDevelopmentRegion</key>
+  <string>${PODS_DEVELOPMENT_LANGUAGE}</string>
+  <key>CFBundleExecutable</key>
+  <string>${EXECUTABLE_NAME}</string>
+  <key>CFBundleIdentifier</key>
+  <string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
+  <key>CFBundleInfoDictionaryVersion</key>
+  <string>6.0</string>
+  <key>CFBundleName</key>
+  <string>${PRODUCT_NAME}</string>
+  <key>CFBundlePackageType</key>
+  <string>FMWK</string>
+  <key>CFBundleShortVersionString</key>
+  <string>1.0.4</string>
+  <key>CFBundleSignature</key>
+  <string>????</string>
+  <key>CFBundleVersion</key>
+  <string>${CURRENT_PROJECT_VERSION}</string>
+  <key>NSPrincipalClass</key>
+  <string></string>
+</dict>
+</plist>

+ 5 - 0
Pods/Target Support Files/YYImage/YYImage-dummy.m

@@ -0,0 +1,5 @@
+#import <Foundation/Foundation.h>
+@interface PodsDummy_YYImage : NSObject
+@end
+@implementation PodsDummy_YYImage
+@end

+ 12 - 0
Pods/Target Support Files/YYImage/YYImage-prefix.pch

@@ -0,0 +1,12 @@
+#ifdef __OBJC__
+#import <UIKit/UIKit.h>
+#else
+#ifndef FOUNDATION_EXPORT
+#if defined(__cplusplus)
+#define FOUNDATION_EXPORT extern "C"
+#else
+#define FOUNDATION_EXPORT extern
+#endif
+#endif
+#endif
+

+ 21 - 0
Pods/Target Support Files/YYImage/YYImage-umbrella.h

@@ -0,0 +1,21 @@
+#ifdef __OBJC__
+#import <UIKit/UIKit.h>
+#else
+#ifndef FOUNDATION_EXPORT
+#if defined(__cplusplus)
+#define FOUNDATION_EXPORT extern "C"
+#else
+#define FOUNDATION_EXPORT extern
+#endif
+#endif
+#endif
+
+#import "YYAnimatedImageView.h"
+#import "YYFrameImage.h"
+#import "YYImage.h"
+#import "YYImageCoder.h"
+#import "YYSpriteSheetImage.h"
+
+FOUNDATION_EXPORT double YYImageVersionNumber;
+FOUNDATION_EXPORT const unsigned char YYImageVersionString[];
+

+ 13 - 0
Pods/Target Support Files/YYImage/YYImage.debug.xcconfig

@@ -0,0 +1,13 @@
+CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
+CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/YYImage
+GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
+OTHER_LDFLAGS = $(inherited) -l"z" -framework "Accelerate" -framework "AssetsLibrary" -framework "CoreFoundation" -framework "ImageIO" -framework "MobileCoreServices" -framework "QuartzCore" -framework "UIKit"
+PODS_BUILD_DIR = ${BUILD_DIR}
+PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
+PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
+PODS_ROOT = ${SRCROOT}
+PODS_TARGET_SRCROOT = ${PODS_ROOT}/YYImage
+PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
+PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
+SKIP_INSTALL = YES
+USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

+ 6 - 0
Pods/Target Support Files/YYImage/YYImage.modulemap

@@ -0,0 +1,6 @@
+framework module YYImage {
+  umbrella header "YYImage-umbrella.h"
+
+  export *
+  module * { export * }
+}

+ 13 - 0
Pods/Target Support Files/YYImage/YYImage.release.xcconfig

@@ -0,0 +1,13 @@
+CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
+CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/YYImage
+GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
+OTHER_LDFLAGS = $(inherited) -l"z" -framework "Accelerate" -framework "AssetsLibrary" -framework "CoreFoundation" -framework "ImageIO" -framework "MobileCoreServices" -framework "QuartzCore" -framework "UIKit"
+PODS_BUILD_DIR = ${BUILD_DIR}
+PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
+PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
+PODS_ROOT = ${SRCROOT}
+PODS_TARGET_SRCROOT = ${PODS_ROOT}/YYImage
+PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
+PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
+SKIP_INSTALL = YES
+USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

+ 0 - 20
Pods/Toast-Swift/LICENSE

@@ -1,20 +0,0 @@
-Copyright (c) 2015-2024 Charles Scalesse.
-
-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.

+ 0 - 143
Pods/Toast-Swift/README.md

@@ -1,143 +0,0 @@
-Toast-Swift
-=============
-
-[![CocoaPods Version](https://img.shields.io/cocoapods/v/Toast-Swift.svg)](http://cocoadocs.org/docsets/Toast-Swift)
-[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
-
-Toast-Swift is a Swift extension that adds toast notifications to the `UIView` object class. It is intended to be simple, lightweight, and easy to use. Most toast notifications can be triggered with a single line of code.
-
-**Toast-Swift is a native Swift port of [Toast for iOS](https://github.com/scalessec/Toast "Toast for iOS").**
-
-Screenshots
----------
-![Toast-Swift Screenshots](toast_swift_screenshot.jpg)
-
-
-Basic Examples
----------
-```swift
-// basic usage
-self.view.makeToast("This is a piece of toast")
-
-// toast with a specific duration and position
-self.view.makeToast("This is a piece of toast", duration: 3.0, position: .top)
-
-// toast presented with multiple options and with a completion closure
-self.view.makeToast("This is a piece of toast", duration: 2.0, point: CGPoint(x: 110.0, y: 110.0), title: "Toast Title", image: UIImage(named: "toast.png")) { didTap in
-    if didTap {
-        print("completion from tap")
-    } else {
-        print("completion without tap")
-    }
-}
-
-// display toast with an activity spinner
-self.view.makeToastActivity(.center)
-
-// display any view as toast
-self.view.showToast(myView)
-
-// immediately hides all toast views in self.view
-self.view.hideAllToasts()
-```
-
-But wait, there's more!
----------
-```swift
-// create a new style
-var style = ToastStyle()
-
-// this is just one of many style options
-style.messageColor = .blue
-
-// present the toast with the new style
-self.view.makeToast("This is a piece of toast", duration: 3.0, position: .bottom, style: style)
-
-// or perhaps you want to use this style for all toasts going forward?
-// just set the shared style and there's no need to provide the style again
-ToastManager.shared.style = style
-self.view.makeToast("This is a piece of toast") // now uses the shared style
-
-// toggle "tap to dismiss" functionality
-ToastManager.shared.isTapToDismissEnabled = true
-
-// toggle queueing behavior
-ToastManager.shared.isQueueEnabled = true
-```
-
-See the demo project for more examples.
-
-
-Setup Instructions
-------------------
-
-[CocoaPods](http://cocoapods.org)
-------------------
-
-To integrate Toast-Swift into your Xcode project using CocoaPods, specify it in your `Podfile`:
-
-```ruby
-pod 'Toast-Swift', '~> 5.1.0'
-```
-
-and in your code add `import Toast_Swift`.
-
-[Carthage](https://github.com/Carthage/Carthage)
-------------------
-
-To integrate Toast-Swift into your Xcode project using Carthage, specify it in your `Cartfile`:
-
-```ogdl
-github "scalessec/Toast-Swift" ~> 5.1.0
-```
-
-Run `carthage update` to build the framework and drag the built `ToastSwiftFramework.framework` into your Xcode project.
-
-and in your code add `import ToastSwiftFramework`.
-
-[Swift Package Manager](https://swift.org/package-manager/)
-------------------
-
-When using Xcode 11 or later, you can install `Toast` by going to your Project settings > `Swift Packages` and add the repository by providing the GitHub URL. Alternatively, you can go to `File` > `Swift Packages` > `Add Package Dependencies...`
-
-Manually
-------------------
-
-1. Add `Toast.swift` to your project.
-2. Grab yourself a cold 🍺.
-
-Compatibility
-------------------
-* Version `5.x.x` requires Swift 5 and Xcode 10.2 or later.
-* Version `4.x.x` requires Swift 4.2 and Xcode 10.
-* Version `3.x.x` requires Swift 4 and Xcode 9.
-* Version `2.x.x` requires Swift 3 and Xcode 8.
-* Version `1.4.x` requires Swift 2.2 and Xcode 7.3. 
-* Version `1.0.0` can be used with Swift 2.1 and earlier versions of Xcode.
-
-Privacy
------------
-Toast-Swift does not collect any data. A [privacy manifest](Toast/Resources/PrivacyInfo.xcprivacy) is provided with the library. See [Apple's documentation](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files) for related details.
- 
-MIT License
------------
-    Copyright (c) 2015-2024 Charles Scalesse.
-
-    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.

+ 0 - 797
Pods/Toast-Swift/Toast/Toast.swift

@@ -1,797 +0,0 @@
-//
-//  Toast.swift
-//  Toast-Swift
-//
-//  Copyright (c) 2015-2024 Charles Scalesse.
-//
-//  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
-import ObjectiveC
-
-/**
- Toast is a Swift extension that adds toast notifications to the `UIView` object class.
- It is intended to be simple, lightweight, and easy to use. Most toast notifications 
- can be triggered with a single line of code.
- 
- The `makeToast` methods create a new view and then display it as toast.
- 
- The `showToast` methods display any view as toast.
- 
- */
-public extension UIView {
-    
-    /**
-     Keys used for associated objects.
-     */
-    private struct ToastKeys {
-        static var timer = malloc(1)
-        static var duration = malloc(1)
-        static var point = malloc(1)
-        static var completion = malloc(1)
-        static var activeToasts = malloc(1)
-        static var activityView = malloc(1)
-        static var queue = malloc(1)
-    }
-    
-    /**
-     Swift closures can't be directly associated with objects via the
-     Objective-C runtime, so the (ugly) solution is to wrap them in a
-     class that can be used with associated objects.
-     */
-    private class ToastCompletionWrapper {
-        let completion: ((Bool) -> Void)?
-        
-        init(_ completion: ((Bool) -> Void)?) {
-            self.completion = completion
-        }
-    }
-    
-    private enum ToastError: Error {
-        case missingParameters
-    }
-    
-    private var activeToasts: NSMutableArray {
-        get {
-            if let activeToasts = objc_getAssociatedObject(self, &ToastKeys.activeToasts) as? NSMutableArray {
-                return activeToasts
-            } else {
-                let activeToasts = NSMutableArray()
-                objc_setAssociatedObject(self, &ToastKeys.activeToasts, activeToasts, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
-                return activeToasts
-            }
-        }
-    }
-    
-    private var queue: NSMutableArray {
-        get {
-            if let queue = objc_getAssociatedObject(self, &ToastKeys.queue) as? NSMutableArray {
-                return queue
-            } else {
-                let queue = NSMutableArray()
-                objc_setAssociatedObject(self, &ToastKeys.queue, queue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
-                return queue
-            }
-        }
-    }
-    
-    // MARK: - Make Toast Methods
-    
-    /**
-     Creates and presents a new toast view.
-     
-     @param message The message to be displayed
-     @param duration The toast duration
-     @param position The toast's position
-     @param title The title
-     @param image The image
-     @param style The style. The shared style will be used when nil
-     @param completion The completion closure, executed after the toast view disappears.
-            didTap will be `true` if the toast view was dismissed from a tap.
-     */
-    func makeToast(_ message: String?, duration: TimeInterval = ToastManager.shared.duration, position: ToastPosition = ToastManager.shared.position, title: String? = nil, image: UIImage? = nil, style: ToastStyle = ToastManager.shared.style, completion: ((_ didTap: Bool) -> Void)? = nil) {
-        do {
-            let toast = try toastViewForMessage(message, title: title, image: image, style: style)
-            showToast(toast, duration: duration, position: position, completion: completion)
-        } catch ToastError.missingParameters {
-            print("Error: message, title, and image are all nil")
-        } catch {}
-    }
-    
-    /**
-     Creates a new toast view and presents it at a given center point.
-     
-     @param message The message to be displayed
-     @param duration The toast duration
-     @param point The toast's center point
-     @param title The title
-     @param image The image
-     @param style The style. The shared style will be used when nil
-     @param completion The completion closure, executed after the toast view disappears.
-            didTap will be `true` if the toast view was dismissed from a tap.
-     */
-    func makeToast(_ message: String?, duration: TimeInterval = ToastManager.shared.duration, point: CGPoint, title: String?, image: UIImage?, style: ToastStyle = ToastManager.shared.style, completion: ((_ didTap: Bool) -> Void)?) {
-        do {
-            let toast = try toastViewForMessage(message, title: title, image: image, style: style)
-            showToast(toast, duration: duration, point: point, completion: completion)
-        } catch ToastError.missingParameters {
-            print("Error: message, title, and image cannot all be nil")
-        } catch {}
-    }
-    
-    // MARK: - Show Toast Methods
-    
-    /**
-     Displays any view as toast at a provided position and duration. The completion closure
-     executes when the toast view completes. `didTap` will be `true` if the toast view was
-     dismissed from a tap.
-     
-     @param toast The view to be displayed as toast
-     @param duration The notification duration
-     @param position The toast's position
-     @param completion The completion block, executed after the toast view disappears.
-     didTap will be `true` if the toast view was dismissed from a tap.
-     */
-    func showToast(_ toast: UIView, duration: TimeInterval = ToastManager.shared.duration, position: ToastPosition = ToastManager.shared.position, completion: ((_ didTap: Bool) -> Void)? = nil) {
-        let point = position.centerPoint(forToast: toast, inSuperview: self)
-        showToast(toast, duration: duration, point: point, completion: completion)
-    }
-    
-    /**
-     Displays any view as toast at a provided center point and duration. The completion closure
-     executes when the toast view completes. `didTap` will be `true` if the toast view was
-     dismissed from a tap.
-     
-     @param toast The view to be displayed as toast
-     @param duration The notification duration
-     @param point The toast's center point
-     @param completion The completion block, executed after the toast view disappears.
-     didTap will be `true` if the toast view was dismissed from a tap.
-     */
-    func showToast(_ toast: UIView, duration: TimeInterval = ToastManager.shared.duration, point: CGPoint, completion: ((_ didTap: Bool) -> Void)? = nil) {
-        objc_setAssociatedObject(toast, &ToastKeys.completion, ToastCompletionWrapper(completion), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-        
-        if ToastManager.shared.isQueueEnabled, activeToasts.count > 0 {
-            objc_setAssociatedObject(toast, &ToastKeys.duration, NSNumber(value: duration), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-            objc_setAssociatedObject(toast, &ToastKeys.point, NSValue(cgPoint: point), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-            
-            queue.add(toast)
-        } else {
-            showToast(toast, duration: duration, point: point)
-        }
-    }
-    
-    // MARK: - Hide Toast Methods
-    
-    /**
-     Hides the active toast. If there are multiple toasts active in a view, this method
-     hides the oldest toast (the first of the toasts to have been presented).
-     
-     @see `hideAllToasts()` to remove all active toasts from a view.
-     
-     @warning This method has no effect on activity toasts. Use `hideToastActivity` to
-     hide activity toasts.
-     
-    */
-    func hideToast() {
-        guard let activeToast = activeToasts.firstObject as? UIView else { return }
-        hideToast(activeToast)
-    }
-    
-    /**
-     Hides an active toast.
-     
-     @param toast The active toast view to dismiss. Any toast that is currently being displayed
-     on the screen is considered active.
-     
-     @warning this does not clear a toast view that is currently waiting in the queue.
-     */
-    func hideToast(_ toast: UIView) {
-        guard activeToasts.contains(toast) else { return }
-        hideToast(toast, fromTap: false)
-    }
-    
-    /**
-     Hides all toast views.
-     
-     @param includeActivity If `true`, toast activity will also be hidden. Default is `false`.
-     @param clearQueue If `true`, removes all toast views from the queue. Default is `true`.
-    */
-    func hideAllToasts(includeActivity: Bool = false, clearQueue: Bool = true) {
-        if clearQueue {
-            clearToastQueue()
-        }
-        
-        activeToasts.compactMap { $0 as? UIView }
-                    .forEach { hideToast($0) }
-        
-        if includeActivity {
-            hideToastActivity()
-        }
-    }
-    
-    /**
-     Removes all toast views from the queue. This has no effect on toast views that are
-     active. Use `hideAllToasts(clearQueue:)` to hide the active toasts views and clear
-     the queue.
-     */
-    func clearToastQueue() {
-        queue.removeAllObjects()
-    }
-    
-    // MARK: - Activity Methods
-    
-    /**
-     Creates and displays a new toast activity indicator view at a specified position.
-    
-     @warning Only one toast activity indicator view can be presented per superview. Subsequent
-     calls to `makeToastActivity(position:)` will be ignored until `hideToastActivity()` is called.
-    
-     @warning `makeToastActivity(position:)` works independently of the `showToast` methods. Toast
-     activity views can be presented and dismissed while toast views are being displayed.
-     `makeToastActivity(position:)` has no effect on the queueing behavior of the `showToast` methods.
-    
-     @param position The toast's position
-     */
-    func makeToastActivity(_ position: ToastPosition) {
-        // sanity
-        guard objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView == nil else { return }
-        
-        let toast = createToastActivityView()
-        let point = position.centerPoint(forToast: toast, inSuperview: self)
-        makeToastActivity(toast, point: point)
-    }
-    
-    /**
-     Creates and displays a new toast activity indicator view at a specified position.
-     
-     @warning Only one toast activity indicator view can be presented per superview. Subsequent
-     calls to `makeToastActivity(position:)` will be ignored until `hideToastActivity()` is called.
-     
-     @warning `makeToastActivity(position:)` works independently of the `showToast` methods. Toast
-     activity views can be presented and dismissed while toast views are being displayed.
-     `makeToastActivity(position:)` has no effect on the queueing behavior of the `showToast` methods.
-     
-     @param point The toast's center point
-     */
-    func makeToastActivity(_ point: CGPoint) {
-        // sanity
-        guard objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView == nil else { return }
-        
-        let toast = createToastActivityView()
-        makeToastActivity(toast, point: point)
-    }
-    
-    /**
-     Dismisses the active toast activity indicator view.
-     */
-    func hideToastActivity() {
-        if let toast = objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView {
-            UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseIn, .beginFromCurrentState], animations: {
-                toast.alpha = 0.0
-            }) { _ in
-                toast.removeFromSuperview()
-                objc_setAssociatedObject(self, &ToastKeys.activityView, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
-            }
-        }
-    }
-    
-    // MARK: - Helper Methods
-    
-    /**
-     Returns `true` if a toast view or toast activity view is actively being displayed.
-     */
-    func isShowingToast() -> Bool {
-        return activeToasts.count > 0 || objc_getAssociatedObject(self, &ToastKeys.activityView) != nil
-    }
-    
-    // MARK: - Private Activity Methods
-    
-    private func makeToastActivity(_ toast: UIView, point: CGPoint) {
-        toast.alpha = 0.0
-        toast.center = point
-        
-        objc_setAssociatedObject(self, &ToastKeys.activityView, toast, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
-        
-        self.addSubview(toast)
-        
-        UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: .curveEaseOut, animations: {
-            toast.alpha = 1.0
-        })
-    }
-    
-    private func createToastActivityView() -> UIView {
-        let style = ToastManager.shared.style
-        
-        let activityView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: style.activitySize.width, height: style.activitySize.height))
-        activityView.backgroundColor = style.activityBackgroundColor
-        activityView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
-        activityView.layer.cornerRadius = style.cornerRadius
-        
-        if style.displayShadow {
-            activityView.layer.shadowColor = style.shadowColor.cgColor
-            activityView.layer.shadowOpacity = style.shadowOpacity
-            activityView.layer.shadowRadius = style.shadowRadius
-            activityView.layer.shadowOffset = style.shadowOffset
-        }
-        
-        let activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
-        activityIndicatorView.center = CGPoint(x: activityView.bounds.size.width / 2.0, y: activityView.bounds.size.height / 2.0)
-        activityView.addSubview(activityIndicatorView)
-        activityIndicatorView.color = style.activityIndicatorColor
-        activityIndicatorView.startAnimating()
-        
-        return activityView
-    }
-    
-    // MARK: - Private Show/Hide Methods
-    
-    private func showToast(_ toast: UIView, duration: TimeInterval, point: CGPoint) {
-        toast.center = point
-        toast.alpha = 0.0
-        
-        if ToastManager.shared.isTapToDismissEnabled {
-            let recognizer = UITapGestureRecognizer(target: self, action: #selector(UIView.handleToastTapped(_:)))
-            toast.addGestureRecognizer(recognizer)
-            toast.isUserInteractionEnabled = true
-            toast.isExclusiveTouch = true
-        }
-        
-        activeToasts.add(toast)
-        self.addSubview(toast)
-
-        let timer = Timer(timeInterval: duration, target: self, selector: #selector(UIView.toastTimerDidFinish(_:)), userInfo: toast, repeats: false)
-        objc_setAssociatedObject(toast, &ToastKeys.timer, timer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
-
-        UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseOut, .allowUserInteraction], animations: {
-            toast.alpha = 1.0
-        }) { _ in
-            guard let timer = objc_getAssociatedObject(toast, &ToastKeys.timer) as? Timer else { return }
-            RunLoop.main.add(timer, forMode: .common)
-        }
-        
-        UIAccessibility.post(notification: .screenChanged, argument: toast)
-    }
-    
-    private func hideToast(_ toast: UIView, fromTap: Bool) {
-        if let timer = objc_getAssociatedObject(toast, &ToastKeys.timer) as? Timer {
-            timer.invalidate()
-        }
-        
-        UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseIn, .beginFromCurrentState], animations: {
-            toast.alpha = 0.0
-        }) { _ in
-            toast.removeFromSuperview()
-            self.activeToasts.remove(toast)
-            
-            if let wrapper = objc_getAssociatedObject(toast, &ToastKeys.completion) as? ToastCompletionWrapper, let completion = wrapper.completion {
-                completion(fromTap)
-            }
-            
-            if let nextToast = self.queue.firstObject as? UIView, let duration = objc_getAssociatedObject(nextToast, &ToastKeys.duration) as? NSNumber, let point = objc_getAssociatedObject(nextToast, &ToastKeys.point) as? NSValue {
-                self.queue.removeObject(at: 0)
-                self.showToast(nextToast, duration: duration.doubleValue, point: point.cgPointValue)
-            }
-        }
-    }
-    
-    // MARK: - Events
-    
-    @objc
-    private func handleToastTapped(_ recognizer: UITapGestureRecognizer) {
-        guard let toast = recognizer.view else { return }
-        hideToast(toast, fromTap: true)
-    }
-    
-    @objc
-    private func toastTimerDidFinish(_ timer: Timer) {
-        guard let toast = timer.userInfo as? UIView else { return }
-        hideToast(toast)
-    }
-    
-    // MARK: - Toast Construction
-    
-    /**
-     Creates a new toast view with any combination of message, title, and image.
-     The look and feel is configured via the style. Unlike the `makeToast` methods,
-     this method does not present the toast view automatically. One of the `showToast`
-     methods must be used to present the resulting view.
-    
-     @warning if message, title, and image are all nil, this method will throw
-     `ToastError.missingParameters`
-    
-     @param message The message to be displayed
-     @param title The title
-     @param image The image
-     @param style The style. The shared style will be used when nil
-     @throws `ToastError.missingParameters` when message, title, and image are all nil
-     @return The newly created toast view
-    */
-    func toastViewForMessage(_ message: String?, title: String?, image: UIImage?, style: ToastStyle) throws -> UIView {
-        // sanity
-        guard message != nil || title != nil || image != nil else {
-            throw ToastError.missingParameters
-        }
-        
-        var messageLabel: UILabel?
-        var titleLabel: UILabel?
-        var imageView: UIImageView?
-        
-        let wrapperView = UIView()
-        wrapperView.backgroundColor = style.backgroundColor
-        wrapperView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
-        wrapperView.layer.cornerRadius = style.cornerRadius
-        
-        if style.displayShadow {
-            wrapperView.layer.shadowColor = style.shadowColor.cgColor
-            wrapperView.layer.shadowOpacity = style.shadowOpacity
-            wrapperView.layer.shadowRadius = style.shadowRadius
-            wrapperView.layer.shadowOffset = style.shadowOffset
-        }
-        
-        if let image = image {
-            imageView = UIImageView(image: image)
-            imageView?.contentMode = .scaleAspectFit
-            imageView?.frame = CGRect(x: style.horizontalPadding, y: style.verticalPadding, width: style.imageSize.width, height: style.imageSize.height)
-        }
-        
-        var imageRect = CGRect.zero
-        
-        if let imageView = imageView {
-            imageRect.origin.x = style.horizontalPadding
-            imageRect.origin.y = style.verticalPadding
-            imageRect.size.width = imageView.bounds.size.width
-            imageRect.size.height = imageView.bounds.size.height
-        }
-
-        if let title = title {
-            titleLabel = UILabel()
-            titleLabel?.numberOfLines = style.titleNumberOfLines
-            titleLabel?.font = style.titleFont
-            titleLabel?.textAlignment = style.titleAlignment
-            titleLabel?.lineBreakMode = .byTruncatingTail
-            titleLabel?.textColor = style.titleColor
-            titleLabel?.backgroundColor = UIColor.clear
-            titleLabel?.text = title;
-            
-            let maxTitleSize = CGSize(width: (self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: self.bounds.size.height * style.maxHeightPercentage)
-            let titleSize = titleLabel?.sizeThatFits(maxTitleSize)
-            if let titleSize = titleSize {
-                titleLabel?.frame = CGRect(x: 0.0, y: 0.0, width: titleSize.width, height: titleSize.height)
-            }
-        }
-        
-        if let message = message {
-            messageLabel = UILabel()
-            messageLabel?.text = message
-            messageLabel?.numberOfLines = style.messageNumberOfLines
-            messageLabel?.font = style.messageFont
-            messageLabel?.textAlignment = style.messageAlignment
-            messageLabel?.lineBreakMode = .byTruncatingTail;
-            messageLabel?.textColor = style.messageColor
-            messageLabel?.backgroundColor = UIColor.clear
-            
-            let maxMessageSize = CGSize(width: (self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: self.bounds.size.height * style.maxHeightPercentage)
-            let messageSize = messageLabel?.sizeThatFits(maxMessageSize)
-            if let messageSize = messageSize {
-                let actualWidth = min(messageSize.width, maxMessageSize.width)
-                let actualHeight = min(messageSize.height, maxMessageSize.height)
-                messageLabel?.frame = CGRect(x: 0.0, y: 0.0, width: actualWidth, height: actualHeight)
-            }
-        }
-  
-        var titleRect = CGRect.zero
-        
-        if let titleLabel = titleLabel {
-            titleRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding
-            titleRect.origin.y = style.verticalPadding
-            titleRect.size.width = titleLabel.bounds.size.width
-            titleRect.size.height = titleLabel.bounds.size.height
-        }
-        
-        var messageRect = CGRect.zero
-        
-        if let messageLabel = messageLabel {
-            messageRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding
-            messageRect.origin.y = titleRect.origin.y + titleRect.size.height + style.verticalPadding
-            messageRect.size.width = messageLabel.bounds.size.width
-            messageRect.size.height = messageLabel.bounds.size.height
-        }
-        
-        let longerWidth = max(titleRect.size.width, messageRect.size.width)
-        let longerX = max(titleRect.origin.x, messageRect.origin.x)
-        let wrapperWidth = max((imageRect.size.width + (style.horizontalPadding * 2.0)), (longerX + longerWidth + style.horizontalPadding))
-        
-        let textMaxY = messageRect.size.height <= 0.0 && titleRect.size.height > 0.0 ? titleRect.maxY : messageRect.maxY
-        let wrapperHeight = max((textMaxY + style.verticalPadding), (imageRect.size.height + (style.verticalPadding * 2.0)))
-        
-        wrapperView.frame = CGRect(x: 0.0, y: 0.0, width: wrapperWidth, height: wrapperHeight)
-        
-        if let titleLabel = titleLabel {
-            titleRect.size.width = longerWidth
-            titleLabel.frame = titleRect
-            wrapperView.addSubview(titleLabel)
-        }
-        
-        if let messageLabel = messageLabel {
-            messageRect.size.width = longerWidth
-            messageLabel.frame = messageRect
-            wrapperView.addSubview(messageLabel)
-        }
-        
-        if let imageView = imageView {
-            wrapperView.addSubview(imageView)
-        }
-        
-        return wrapperView
-    }
-    
-}
-
-// MARK: - Toast Style
-
-/**
- `ToastStyle` instances define the look and feel for toast views created via the
- `makeToast` methods as well for toast views created directly with
- `toastViewForMessage(message:title:image:style:)`.
-
- @warning `ToastStyle` offers relatively simple styling options for the default
- toast view. If you require a toast view with more complex UI, it probably makes more
- sense to create your own custom UIView subclass and present it with the `showToast`
- methods.
-*/
-public struct ToastStyle {
-
-    public init() {}
-    
-    /**
-     The background color. Default is `.black` at 80% opacity.
-    */
-    public var backgroundColor: UIColor = UIColor.black.withAlphaComponent(0.8)
-    
-    /**
-     The title color. Default is `UIColor.whiteColor()`.
-    */
-    public var titleColor: UIColor = .white
-    
-    /**
-     The message color. Default is `.white`.
-    */
-    public var messageColor: UIColor = .white
-    
-    /**
-     A percentage value from 0.0 to 1.0, representing the maximum width of the toast
-     view relative to it's superview. Default is 0.8 (80% of the superview's width).
-    */
-    public var maxWidthPercentage: CGFloat = 0.8 {
-        didSet {
-            maxWidthPercentage = max(min(maxWidthPercentage, 1.0), 0.0)
-        }
-    }
-    
-    /**
-     A percentage value from 0.0 to 1.0, representing the maximum height of the toast
-     view relative to it's superview. Default is 0.8 (80% of the superview's height).
-    */
-    public var maxHeightPercentage: CGFloat = 0.8 {
-        didSet {
-            maxHeightPercentage = max(min(maxHeightPercentage, 1.0), 0.0)
-        }
-    }
-    
-    /**
-     The spacing from the horizontal edge of the toast view to the content. When an image
-     is present, this is also used as the padding between the image and the text.
-     Default is 10.0.
-     
-    */
-    public var horizontalPadding: CGFloat = 10.0
-    
-    /**
-     The spacing from the vertical edge of the toast view to the content. When a title
-     is present, this is also used as the padding between the title and the message.
-     Default is 10.0. On iOS11+, this value is added added to the `safeAreaInset.top`
-     and `safeAreaInsets.bottom`.
-    */
-    public var verticalPadding: CGFloat = 10.0
-    
-    /**
-     The corner radius. Default is 10.0.
-    */
-    public var cornerRadius: CGFloat = 10.0;
-    
-    /**
-     The title font. Default is `.boldSystemFont(16.0)`.
-    */
-    public var titleFont: UIFont = .boldSystemFont(ofSize: 16.0)
-    
-    /**
-     The message font. Default is `.systemFont(ofSize: 16.0)`.
-    */
-    public var messageFont: UIFont = .systemFont(ofSize: 16.0)
-    
-    /**
-     The title text alignment. Default is `NSTextAlignment.Left`.
-    */
-    public var titleAlignment: NSTextAlignment = .left
-    
-    /**
-     The message text alignment. Default is `NSTextAlignment.Left`.
-    */
-    public var messageAlignment: NSTextAlignment = .left
-    
-    /**
-     The maximum number of lines for the title. The default is 0 (no limit).
-    */
-    public var titleNumberOfLines = 0
-    
-    /**
-     The maximum number of lines for the message. The default is 0 (no limit).
-    */
-    public var messageNumberOfLines = 0
-    
-    /**
-     Enable or disable a shadow on the toast view. Default is `false`.
-    */
-    public var displayShadow = false
-    
-    /**
-     The shadow color. Default is `.black`.
-     */
-    public var shadowColor: UIColor = .black
-    
-    /**
-     A value from 0.0 to 1.0, representing the opacity of the shadow.
-     Default is 0.8 (80% opacity).
-    */
-    public var shadowOpacity: Float = 0.8 {
-        didSet {
-            shadowOpacity = max(min(shadowOpacity, 1.0), 0.0)
-        }
-    }
-
-    /**
-     The shadow radius. Default is 6.0.
-    */
-    public var shadowRadius: CGFloat = 6.0
-    
-    /**
-     The shadow offset. The default is 4 x 4.
-    */
-    public var shadowOffset = CGSize(width: 4.0, height: 4.0)
-    
-    /**
-     The image size. The default is 80 x 80.
-    */
-    public var imageSize = CGSize(width: 80.0, height: 80.0)
-    
-    /**
-     The size of the toast activity view when `makeToastActivity(position:)` is called.
-     Default is 100 x 100.
-    */
-    public var activitySize = CGSize(width: 100.0, height: 100.0)
-    
-    /**
-     The fade in/out animation duration. Default is 0.2.
-     */
-    public var fadeDuration: TimeInterval = 0.2
-    
-    /**
-     Activity indicator color. Default is `.white`.
-     */
-    public var activityIndicatorColor: UIColor = .white
-    
-    /**
-     Activity background color. Default is `.black` at 80% opacity.
-     */
-    public var activityBackgroundColor: UIColor = UIColor.black.withAlphaComponent(0.8)
-    
-}
-
-// MARK: - Toast Manager
-
-/**
- `ToastManager` provides general configuration options for all toast
- notifications. Backed by a singleton instance.
-*/
-public class ToastManager {
-    
-    /**
-     The `ToastManager` singleton instance.
-     
-     */
-    public static let shared = ToastManager()
-    
-    /**
-     The shared style. Used whenever toastViewForMessage(message:title:image:style:) is called
-     with with a nil style.
-     
-     */
-    public var style = ToastStyle()
-    
-    /**
-     Enables or disables tap to dismiss on toast views. Default is `true`.
-     
-     */
-    public var isTapToDismissEnabled = true
-    
-    /**
-     Enables or disables queueing behavior for toast views. When `true`,
-     toast views will appear one after the other. When `false`, multiple toast
-     views will appear at the same time (potentially overlapping depending
-     on their positions). This has no effect on the toast activity view,
-     which operates independently of normal toast views. Default is `false`.
-     
-     */
-    public var isQueueEnabled = false
-    
-    /**
-     The default duration. Used for the `makeToast` and
-     `showToast` methods that don't require an explicit duration.
-     Default is 3.0.
-     
-     */
-    public var duration: TimeInterval = 3.0
-    
-    /**
-     Sets the default position. Used for the `makeToast` and
-     `showToast` methods that don't require an explicit position.
-     Default is `ToastPosition.Bottom`.
-     
-     */
-    public var position: ToastPosition = .bottom
-    
-}
-
-// MARK: - ToastPosition
-
-public enum ToastPosition {
-    case top
-    case center
-    case bottom
-    
-    fileprivate func centerPoint(forToast toast: UIView, inSuperview superview: UIView) -> CGPoint {
-        let topPadding: CGFloat = ToastManager.shared.style.verticalPadding + superview.csSafeAreaInsets.top
-        let bottomPadding: CGFloat = ToastManager.shared.style.verticalPadding + superview.csSafeAreaInsets.bottom
-        
-        switch self {
-        case .top:
-            return CGPoint(x: superview.bounds.size.width / 2.0, y: (toast.frame.size.height / 2.0) + topPadding)
-        case .center:
-            return CGPoint(x: superview.bounds.size.width / 2.0, y: superview.bounds.size.height / 2.0)
-        case .bottom:
-            return CGPoint(x: superview.bounds.size.width / 2.0, y: (superview.bounds.size.height - (toast.frame.size.height / 2.0)) - bottomPadding)
-        }
-    }
-}
-
-// MARK: - Private UIView Extensions
-
-private extension UIView {
-    
-    var csSafeAreaInsets: UIEdgeInsets {
-        if #available(iOS 11.0, *) {
-            return self.safeAreaInsets
-        } else {
-            return .zero
-        }
-    }
-    
-}

+ 22 - 0
Pods/YYImage/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 ibireme <ibireme@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.
+

+ 384 - 0
Pods/YYImage/README.md

@@ -0,0 +1,384 @@
+YYImage
+==============
+[![License MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/ibireme/YYImage/master/LICENSE)&nbsp;
+[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)&nbsp;
+[![CocoaPods](http://img.shields.io/cocoapods/v/YYImage.svg?style=flat)](http://cocoapods.org/?q= YYImage)&nbsp;
+[![CocoaPods](http://img.shields.io/cocoapods/p/YYImage.svg?style=flat)](http://cocoapods.org/?q= YYImage)&nbsp;
+[![Support](https://img.shields.io/badge/support-iOS%206%2B%20-blue.svg?style=flat)](https://www.apple.com/nl/ios/)&nbsp;
+[![Build Status](https://travis-ci.org/ibireme/YYImage.svg?branch=master)](https://travis-ci.org/ibireme/YYImage)
+
+Image framework for iOS to display/encode/decode animated WebP, APNG, GIF, and more.<br/>
+(It's a component of [YYKit](https://github.com/ibireme/YYKit))
+
+![niconiconi~](https://raw.github.com/ibireme/YYImage/master/Demo/YYImageDemo/niconiconi@2x.gif
+)
+
+Features
+==============
+- Display/encode/decode animated image with these types:<br/>&nbsp;&nbsp;&nbsp;&nbsp;WebP, APNG, GIF.
+- Display/encode/decode still image with these types:<br/>&nbsp;&nbsp;&nbsp;&nbsp;WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS.
+- Baseline/progressive/interlaced image decode with these types:<br/>&nbsp;&nbsp;&nbsp;&nbsp;PNG, GIF, JPEG, BMP.
+- Display frame based image animation and sprite sheet animation.
+- Dynamic memory buffer for lower memory usage.
+- Fully compatible with UIImage and UIImageView class.
+- Extendable protocol for custom image animation.
+- Fully documented.
+
+Usage
+==============
+
+###Display animated image
+	
+	// File: ani@3x.gif
+	UIImage *image = [YYImage imageNamed:@"ani.gif"];
+	UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
+	[self.view addSubView:imageView];
+
+
+###Display frame animation
+	
+	// Files: frame1.png, frame2.png, frame3.png
+	NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
+	NSArray *times = @[@0.1, @0.2, @0.1];
+	UIImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
+	UIImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
+	[self.view addSubView:imageView];
+
+###Display sprite sheet animation
+
+	// 8 * 12 sprites in a single sheet image
+	UIImage *spriteSheet = [UIImage imageNamed:@"sprite-sheet"];
+	NSMutableArray *contentRects = [NSMutableArray new];
+	NSMutableArray *durations = [NSMutableArray new];
+	for (int j = 0; j < 12; j++) {
+	   for (int i = 0; i < 8; i++) {
+	       CGRect rect;
+	       rect.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
+	       rect.origin.x = img.size.width / 8 * i;
+	       rect.origin.y = img.size.height / 12 * j;
+	       [contentRects addObject:[NSValue valueWithCGRect:rect]];
+	       [durations addObject:@(1 / 60.0)];
+	   }
+	}
+	YYSpriteSheetImage *sprite;
+	sprite = [[YYSpriteSheetImage alloc] initWithSpriteSheetImage:img
+	                                                contentRects:contentRects
+	                                              frameDurations:durations
+	                                                   loopCount:0];
+	YYAnimatedImageView *imageView = [YYAnimatedImageView new];
+	imageView.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
+	imageView.image = sprite;
+	[self.view addSubView:imageView];
+
+###Animation control
+	
+	YYAnimatedImageView *imageView = ...;
+	// pause:
+	[imageView stopAnimating];
+	// play:
+	[imageView startAnimating];
+	// set frame index:
+	imageView.currentAnimatedImageIndex = 12;
+	// get current status
+	image.currentIsPlayingAnimation;
+	
+###Image decoder
+		
+	// Decode single frame:
+	NSData *data = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
+	YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
+	UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
+	
+	// Progressive:
+	NSMutableData *data = [NSMutableData new];
+	YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0];
+	while(newDataArrived) {
+	   [data appendData:newData];
+	   [decoder updateData:data final:NO];
+	   if (decoder.frameCount > 0) {
+	       UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
+	       // progressive display...
+	   }
+	}
+	[decoder updateData:data final:YES];
+	UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
+	// final display...
+
+###Image encoder
+	
+	// Encode still image:
+	YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
+	jpegEncoder.quality = 0.9;
+	[jpegEncoder addImage:image duration:0];
+	NSData jpegData = [jpegEncoder encode];
+	 
+	// Encode animated image:
+	YYImageEncoder *webpEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeWebP];
+	webpEncoder.loopCount = 5;
+	[webpEncoder addImage:image0 duration:0.1];
+	[webpEncoder addImage:image1 duration:0.15];
+	[webpEncoder addImage:image2 duration:0.2];
+	NSData webpData = [webpEncoder encode];
+
+###Image type detection
+
+	// Get image type from image data
+	YYImageType type = YYImageDetectType(data); 
+	if (type == YYImageTypePNG) ...
+	
+	
+Installation
+==============
+
+### CocoaPods
+
+1. Update cocoapods to the latest version.
+2. Add `pod 'YYImage'` to your Podfile.
+3. Run `pod install` or `pod update`.
+4. Import \<YYImage/YYImage.h\>.
+5. Notice: it doesn't include WebP subspec by default, if you want to support WebP format, you may add `pod 'YYImage/WebP'` to your Podfile.
+
+### Carthage
+
+1. Add `github "ibireme/YYImage"` to your Cartfile.
+2. Run `carthage update --platform ios` and add the framework to your project.
+3. Import \<YYImage/YYImage.h\>.
+4. Notice: carthage framework doesn't include WebP component, if you want to support WebP format, use CocoaPods or install manually.
+
+### Manually
+
+1. Download all the files in the YYImage subdirectory.
+2. Add the source files to your Xcode project.
+3. Link with required frameworks:
+	* UIKit
+	* CoreFoundation
+	* QuartzCore
+	* AssetsLibrary
+	* ImageIO
+	* Accelerate
+	* MobileCoreServices
+	* libz
+4. Import `YYImage.h`.
+5. Notice: if you want to support WebP format, you may add `Vendor/WebP.framework`(static library) to your Xcode project.
+
+FAQ
+==============
+_Q: Why I can't display WebP image?_
+
+A: Make sure you added the `WebP.framework` in your project. You may call `YYImageWebPAvailable()` to check whether the WebP subspec is installed correctly.
+
+_Q: Why I can't play APNG animation?_
+
+A: You should disable the `Compress PNG Files` and `Remove Text Metadata From PNG Files` in your project's build settings. Or you can rename your APNG file's extension name with `apng`.
+
+Documentation
+==============
+Full API documentation is available on [CocoaDocs](http://cocoadocs.org/docsets/YYImage/).<br/>
+You can also install documentation locally using [appledoc](https://github.com/tomaz/appledoc).
+
+
+
+Requirements
+==============
+This library requires `iOS 6.0+` and `Xcode 7.0+`.
+
+
+License
+==============
+YYImage is provided under the MIT license. See LICENSE file for details.
+
+
+<br/><br/>
+---
+中文介绍
+==============
+YYImage: 功能强大的 iOS 图像框架。<br/>
+(该项目是 [YYKit](https://github.com/ibireme/YYKit) 组件之一)
+
+![niconiconi~](https://raw.github.com/ibireme/YYImage/master/Demo/YYImageDemo/niconiconi@2x.gif
+)
+
+特性
+==============
+- 支持以下类型动画图像的播放/编码/解码:<br/>
+  &nbsp;&nbsp;&nbsp;&nbsp;WebP, APNG, GIF。
+- 支持以下类型静态图像的显示/编码/解码:<br>
+  &nbsp;&nbsp;&nbsp;&nbsp;WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。
+- 支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:<br/>
+  &nbsp;&nbsp;&nbsp;&nbsp;PNG, GIF, JPEG, BMP。
+- 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画。
+- 高效的动态内存缓存管理,以保证高性能低内存的动画播放。
+- 完全兼容 UIImage 和 UIImageView,使用方便。
+- 保留可扩展的接口,以支持自定义动画。
+- 每个类和方法都有完善的文档注释。
+
+
+用法
+==============
+
+###显示动画类型的图片
+	
+	// 文件: ani@3x.gif
+	UIImage *image = [YYImage imageNamed:@"ani.gif"];
+	UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
+	[self.view addSubView:imageView];
+
+
+###播放帧动画
+	
+	// 文件: frame1.png, frame2.png, frame3.png
+	NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
+	NSArray *times = @[@0.1, @0.2, @0.1];
+	UIImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
+	UIImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
+	[self.view addSubView:imageView];
+
+###播放 sprite sheet 动画
+
+	// 8 * 12 sprites in a single sheet image
+	UIImage *spriteSheet = [UIImage imageNamed:@"sprite-sheet"];
+	NSMutableArray *contentRects = [NSMutableArray new];
+	NSMutableArray *durations = [NSMutableArray new];
+	for (int j = 0; j < 12; j++) {
+	   for (int i = 0; i < 8; i++) {
+	       CGRect rect;
+	       rect.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
+	       rect.origin.x = img.size.width / 8 * i;
+	       rect.origin.y = img.size.height / 12 * j;
+	       [contentRects addObject:[NSValue valueWithCGRect:rect]];
+	       [durations addObject:@(1 / 60.0)];
+	   }
+	}
+	YYSpriteSheetImage *sprite;
+	sprite = [[YYSpriteSheetImage alloc] initWithSpriteSheetImage:img
+	                                                contentRects:contentRects
+	                                              frameDurations:durations
+	                                                   loopCount:0];
+	YYAnimatedImageView *imageView = [YYAnimatedImageView new];
+	imageView.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
+	imageView.image = sprite;
+	[self.view addSubView:imageView];
+
+###动画播放控制
+	
+	YYAnimatedImageView *imageView = ...;
+	// 暂停:
+	[imageView stopAnimating];
+	// 播放:
+	[imageView startAnimating];
+	// 设置播放进度:
+	imageView.currentAnimatedImageIndex = 12;
+	// 获取播放状态:
+	image.currentIsPlayingAnimation;
+	//上面两个属性都支持 KVO。
+	
+###图片解码
+		
+	// 解码单帧图片:
+	NSData *data = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
+	YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
+	UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
+	
+	// 渐进式图片解码 (可用于图片下载显示):
+	NSMutableData *data = [NSMutableData new];
+	YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0];
+	while(newDataArrived) {
+	   [data appendData:newData];
+	   [decoder updateData:data final:NO];
+	   if (decoder.frameCount > 0) {
+	       UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
+	       // progressive display...
+	   }
+	}
+	[decoder updateData:data final:YES];
+	UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
+	// final display...
+
+###图片编码
+	
+	// 编码静态图 (支持各种常见图片格式):
+	YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
+	jpegEncoder.quality = 0.9;
+	[jpegEncoder addImage:image duration:0];
+	NSData jpegData = [jpegEncoder encode];
+	 
+	// 编码动态图 (支持 GIF/APNG/WebP):
+	YYImageEncoder *webpEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeWebP];
+	webpEncoder.loopCount = 5;
+	[webpEncoder addImage:image0 duration:0.1];
+	[webpEncoder addImage:image1 duration:0.15];
+	[webpEncoder addImage:image2 duration:0.2];
+	NSData webpData = [webpEncoder encode];
+	
+###图片类型探测
+
+	// 获取图片类型
+	YYImageType type = YYImageDetectType(data); 
+	if (type == YYImageTypePNG) ...
+	
+
+安装
+==============
+
+### CocoaPods
+
+1. 将 cocoapods 更新至最新版本.
+2. 在 Podfile 中添加 `pod 'YYImage'`。
+3. 执行 `pod install` 或 `pod update`。
+4. 导入 \<YYImage/YYImage.h\>。
+5. 注意:pod 配置并没有包含 WebP 组件, 如果你需要支持 WebP,可以在 Podfile 中添加 `pod 'YYImage/WebP'`。
+
+### Carthage
+
+1. 在 Cartfile 中添加 `github "ibireme/YYImage"`。
+2. 执行 `carthage update --platform ios` 并将生成的 framework 添加到你的工程。
+3. 导入 \<YYImage/YYImage.h\>。
+4. 注意:carthage framework 并没有包含 WebP 组件。如果你需要支持 WebP,可以用 CocoaPods 安装,或者手动安装。
+
+### 手动安装
+
+1. 下载 YYImage 文件夹内的所有内容。
+2. 将 YYImage 内的源文件添加(拖放)到你的工程。
+3. 链接以下 frameworks:
+	* UIKit
+	* CoreFoundation
+	* QuartzCore
+	* AssetsLibrary
+	* ImageIO
+	* Accelerate
+	* MobileCoreServices
+	* libz
+4. 导入 `YYImage.h`。
+5. 注意:如果你需要支持 WebP,可以将 `Vendor/WebP.framework`(静态库) 加入你的工程。
+
+常见问题
+==============
+_Q: 为什么我不能显示 WebP 图片?_
+
+A: 确保 `WebP.framework` 已经被添加到你的工程内了。你可以调用 `YYImageWebPAvailable()` 来检查一下 WebP 组件是否被正确安装。
+
+_Q: 为什么我不能播放 APNG 动画?_
+
+A: 你应该禁用 Build Settings 中的 `Compress PNG Files` 和 `Remove Text Metadata From PNG Files`. 或者你也可以把 APNG 文件的扩展名改为`apng`.
+
+文档
+==============
+你可以在 [CocoaDocs](http://cocoadocs.org/docsets/YYImage/) 查看在线 API 文档,也可以用 [appledoc](https://github.com/tomaz/appledoc) 本地生成文档。
+
+
+系统要求
+==============
+该项目最低支持 `iOS 6.0` 和 `Xcode 7.0`。
+
+
+许可证
+==============
+YYImage 使用 MIT 许可证,详情见 LICENSE 文件。
+
+
+相关链接
+==============
+[移动端图片格式调研](http://blog.ibireme.com/2015/11/02/mobile_image_benchmark/)<br/>
+
+[iOS 处理图片的一些小 Tip](http://blog.ibireme.com/2015/11/02/ios_image_tips/)
+

+ 125 - 0
Pods/YYImage/YYImage/YYAnimatedImageView.h

@@ -0,0 +1,125 @@
+//
+//  YYAnimatedImageView.h
+//  YYImage <https://github.com/ibireme/YYImage>
+//
+//  Created by ibireme on 14/10/19.
+//  Copyright (c) 2015 ibireme.
+//
+//  This source code is licensed under the MIT-style license found in the
+//  LICENSE file in the root directory of this source tree.
+//
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ An image view for displaying animated image.
+ 
+ @discussion It is a fully compatible `UIImageView` subclass.
+ If the `image` or `highlightedImage` property adopt to the `YYAnimatedImage` protocol,
+ then it can be used to play the multi-frame animation. The animation can also be 
+ controlled with the UIImageView methods `-startAnimating`, `-stopAnimating` and `-isAnimating`.
+ 
+ This view request the frame data just in time. When the device has enough free memory, 
+ this view may cache some or all future frames in an inner buffer for lower CPU cost.
+ Buffer size is dynamically adjusted based on the current state of the device memory.
+ 
+ Sample Code:
+ 
+     // ani@3x.gif
+     YYImage *image = [YYImage imageNamed:@"ani"];
+     YYAnimatedImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
+     [view addSubView:imageView];
+ */
+@interface YYAnimatedImageView : UIImageView
+
+/**
+ If the image has more than one frame, set this value to `YES` will automatically 
+ play/stop the animation when the view become visible/invisible.
+ 
+ The default value is `YES`.
+ */
+@property (nonatomic) BOOL autoPlayAnimatedImage;
+
+/**
+ Index of the currently displayed frame (index from 0).
+ 
+ Set a new value to this property will cause to display the new frame immediately.
+ If the new value is invalid, this method has no effect.
+ 
+ You can add an observer to this property to observe the playing status.
+ */
+@property (nonatomic) NSUInteger currentAnimatedImageIndex;
+
+/**
+ Whether the image view is playing animation currently.
+ 
+ You can add an observer to this property to observe the playing status.
+ */
+@property (nonatomic, readonly) BOOL currentIsPlayingAnimation;
+
+/**
+ The animation timer's runloop mode, default is `NSRunLoopCommonModes`.
+ 
+ Set this property to `NSDefaultRunLoopMode` will make the animation pause during
+ UIScrollView scrolling.
+ */
+@property (nonatomic, copy) NSString *runloopMode;
+
+/**
+ The max size (in bytes) for inner frame buffer size, default is 0 (dynamically).
+ 
+ When the device has enough free memory, this view will request and decode some or 
+ all future frame image into an inner buffer. If this property's value is 0, then 
+ the max buffer size will be dynamically adjusted based on the current state of 
+ the device free memory. Otherwise, the buffer size will be limited by this value.
+ 
+ When receive memory warning or app enter background, the buffer will be released 
+ immediately, and may grow back at the right time.
+ */
+@property (nonatomic) NSUInteger maxBufferSize;
+
+@end
+
+
+
+/**
+ The YYAnimatedImage protocol declares the required methods for animated image
+ display with YYAnimatedImageView.
+ 
+ Subclass a UIImage and implement this protocol, so that instances of that class 
+ can be set to YYAnimatedImageView.image or YYAnimatedImageView.highlightedImage
+ to display animation.
+ 
+ See `YYImage` and `YYFrameImage` for example.
+ */
+@protocol YYAnimatedImage <NSObject>
+@required
+/// Total animated frame count.
+/// It the frame count is less than 1, then the methods below will be ignored.
+- (NSUInteger)animatedImageFrameCount;
+
+/// Animation loop count, 0 means infinite looping.
+- (NSUInteger)animatedImageLoopCount;
+
+/// Bytes per frame (in memory). It may used to optimize memory buffer size.
+- (NSUInteger)animatedImageBytesPerFrame;
+
+/// Returns the frame image from a specified index.
+/// This method may be called on background thread.
+/// @param index  Frame index (zero based).
+- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
+
+/// Returns the frames's duration from a specified index.
+/// @param index  Frame index (zero based).
+- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
+
+@optional
+/// A rectangle in image coordinates defining the subrectangle of the image that
+/// will be displayed. The rectangle should not outside the image's bounds.
+/// It may used to display sprite animation with a single image (sprite sheet).
+- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
+@end
+
+NS_ASSUME_NONNULL_END

+ 672 - 0
Pods/YYImage/YYImage/YYAnimatedImageView.m

@@ -0,0 +1,672 @@
+//
+//  YYAnimatedImageView.m
+//  YYImage <https://github.com/ibireme/YYImage>
+//
+//  Created by ibireme on 14/10/19.
+//  Copyright (c) 2015 ibireme.
+//
+//  This source code is licensed under the MIT-style license found in the
+//  LICENSE file in the root directory of this source tree.
+//
+
+#import "YYAnimatedImageView.h"
+#import "YYImageCoder.h"
+#import <pthread.h>
+#import <mach/mach.h>
+
+
+#define BUFFER_SIZE (10 * 1024 * 1024) // 10MB (minimum memory buffer size)
+
+#define LOCK(...) dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER); \
+__VA_ARGS__; \
+dispatch_semaphore_signal(self->_lock);
+
+#define LOCK_VIEW(...) dispatch_semaphore_wait(view->_lock, DISPATCH_TIME_FOREVER); \
+__VA_ARGS__; \
+dispatch_semaphore_signal(view->_lock);
+
+
+static int64_t _YYDeviceMemoryTotal() {
+    int64_t mem = [[NSProcessInfo processInfo] physicalMemory];
+    if (mem < -1) mem = -1;
+        return mem;
+}
+
+static int64_t _YYDeviceMemoryFree() {
+    mach_port_t host_port = mach_host_self();
+    mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t);
+    vm_size_t page_size;
+    vm_statistics_data_t vm_stat;
+    kern_return_t kern;
+    
+    kern = host_page_size(host_port, &page_size);
+    if (kern != KERN_SUCCESS) return -1;
+    kern = host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size);
+    if (kern != KERN_SUCCESS) return -1;
+    return vm_stat.free_count * page_size;
+}
+
+/**
+ A proxy used to hold a weak object.
+ It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink.
+ */
+@interface _YYImageWeakProxy : NSProxy
+@property (nonatomic, weak, readonly) id target;
+- (instancetype)initWithTarget:(id)target;
++ (instancetype)proxyWithTarget:(id)target;
+@end
+
+@implementation _YYImageWeakProxy
+- (instancetype)initWithTarget:(id)target {
+    _target = target;
+    return self;
+}
++ (instancetype)proxyWithTarget:(id)target {
+    return [[_YYImageWeakProxy alloc] initWithTarget:target];
+}
+- (id)forwardingTargetForSelector:(SEL)selector {
+    return _target;
+}
+- (void)forwardInvocation:(NSInvocation *)invocation {
+    void *null = NULL;
+    [invocation setReturnValue:&null];
+}
+- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
+    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
+}
+- (BOOL)respondsToSelector:(SEL)aSelector {
+    return [_target respondsToSelector:aSelector];
+}
+- (BOOL)isEqual:(id)object {
+    return [_target isEqual:object];
+}
+- (NSUInteger)hash {
+    return [_target hash];
+}
+- (Class)superclass {
+    return [_target superclass];
+}
+- (Class)class {
+    return [_target class];
+}
+- (BOOL)isKindOfClass:(Class)aClass {
+    return [_target isKindOfClass:aClass];
+}
+- (BOOL)isMemberOfClass:(Class)aClass {
+    return [_target isMemberOfClass:aClass];
+}
+- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
+    return [_target conformsToProtocol:aProtocol];
+}
+- (BOOL)isProxy {
+    return YES;
+}
+- (NSString *)description {
+    return [_target description];
+}
+- (NSString *)debugDescription {
+    return [_target debugDescription];
+}
+@end
+
+
+
+
+typedef NS_ENUM(NSUInteger, YYAnimatedImageType) {
+    YYAnimatedImageTypeNone = 0,
+    YYAnimatedImageTypeImage,
+    YYAnimatedImageTypeHighlightedImage,
+    YYAnimatedImageTypeImages,
+    YYAnimatedImageTypeHighlightedImages,
+};
+
+@interface YYAnimatedImageView() {
+    @package
+    UIImage <YYAnimatedImage> *_curAnimatedImage;
+    
+    dispatch_once_t _onceToken;
+    dispatch_semaphore_t _lock; ///< lock for _buffer
+    NSOperationQueue *_requestQueue; ///< image request queue, serial
+    
+    CADisplayLink *_link; ///< ticker for change frame
+    NSTimeInterval _time; ///< time after last frame
+    
+    UIImage *_curFrame; ///< current frame to display
+    NSUInteger _curIndex; ///< current frame index (from 0)
+    NSUInteger _totalFrameCount; ///< total frame count
+    
+    BOOL _loopEnd; ///< whether the loop is end.
+    NSUInteger _curLoop; ///< current loop count (from 0)
+    NSUInteger _totalLoop; ///< total loop count, 0 means infinity
+    
+    NSMutableDictionary *_buffer; ///< frame buffer
+    BOOL _bufferMiss; ///< whether miss frame on last opportunity
+    NSUInteger _maxBufferCount; ///< maximum buffer count
+    NSInteger _incrBufferCount; ///< current allowed buffer count (will increase by step)
+    
+    CGRect _curContentsRect;
+    BOOL _curImageHasContentsRect; ///< image has implementated "animatedImageContentsRectAtIndex:"
+}
+@property (nonatomic, readwrite) BOOL currentIsPlayingAnimation;
+- (void)calcMaxBufferCount;
+@end
+
+/// An operation for image fetch
+@interface _YYAnimatedImageViewFetchOperation : NSOperation
+@property (nonatomic, weak) YYAnimatedImageView *view;
+@property (nonatomic, assign) NSUInteger nextIndex;
+@property (nonatomic, strong) UIImage <YYAnimatedImage> *curImage;
+@end
+
+@implementation _YYAnimatedImageViewFetchOperation
+- (void)main {
+    __strong YYAnimatedImageView *view = _view;
+    if (!view) return;
+    if ([self isCancelled]) return;
+    view->_incrBufferCount++;
+    if (view->_incrBufferCount == 0) [view calcMaxBufferCount];
+    if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) {
+        view->_incrBufferCount = view->_maxBufferCount;
+    }
+    NSUInteger idx = _nextIndex;
+    NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount;
+    NSUInteger total = view->_totalFrameCount;
+    view = nil;
+    
+    for (int i = 0; i < max; i++, idx++) {
+        @autoreleasepool {
+            if (idx >= total) idx = 0;
+            if ([self isCancelled]) break;
+            __strong YYAnimatedImageView *view = _view;
+            if (!view) break;
+            LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil));
+            
+            if (miss) {
+                UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
+                img = img.yy_imageByDecoded;
+                if ([self isCancelled]) break;
+                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
+                view = nil;
+            }
+        }
+    }
+}
+@end
+
+@implementation YYAnimatedImageView
+
+- (instancetype)init {
+    self = [super init];
+    _runloopMode = NSRunLoopCommonModes;
+    _autoPlayAnimatedImage = YES;
+    return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    _runloopMode = NSRunLoopCommonModes;
+    _autoPlayAnimatedImage = YES;
+    return self;
+}
+
+- (instancetype)initWithImage:(UIImage *)image {
+    self = [super init];
+    _runloopMode = NSRunLoopCommonModes;
+    _autoPlayAnimatedImage = YES;
+    self.frame = (CGRect) {CGPointZero, image.size };
+    self.image = image;
+    return self;
+}
+
+- (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage {
+    self = [super init];
+    _runloopMode = NSRunLoopCommonModes;
+    _autoPlayAnimatedImage = YES;
+    CGSize size = image ? image.size : highlightedImage.size;
+    self.frame = (CGRect) {CGPointZero, size };
+    self.image = image;
+    self.highlightedImage = highlightedImage;
+    return self;
+}
+
+// init the animated params.
+- (void)resetAnimated {
+    dispatch_once(&_onceToken, ^{
+        _lock = dispatch_semaphore_create(1);
+        _buffer = [NSMutableDictionary new];
+        _requestQueue = [[NSOperationQueue alloc] init];
+        _requestQueue.maxConcurrentOperationCount = 1;
+        _link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
+        if (_runloopMode) {
+            [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
+        }
+        _link.paused = YES;
+        
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
+    });
+    
+    [_requestQueue cancelAllOperations];
+    LOCK(
+         if (_buffer.count) {
+             NSMutableDictionary *holder = _buffer;
+             _buffer = [NSMutableDictionary new];
+             dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
+                 // Capture the dictionary to global queue,
+                 // release these images in background to avoid blocking UI thread.
+                 [holder class];
+             });
+         }
+    );
+    _link.paused = YES;
+    _time = 0;
+    if (_curIndex != 0) {
+        [self willChangeValueForKey:@"currentAnimatedImageIndex"];
+        _curIndex = 0;
+        [self didChangeValueForKey:@"currentAnimatedImageIndex"];
+    }
+    _curAnimatedImage = nil;
+    _curFrame = nil;
+    _curLoop = 0;
+    _totalLoop = 0;
+    _totalFrameCount = 1;
+    _loopEnd = NO;
+    _bufferMiss = NO;
+    _incrBufferCount = 0;
+}
+
+- (void)setImage:(UIImage *)image {
+    if (self.image == image) return;
+    [self setImage:image withType:YYAnimatedImageTypeImage];
+}
+
+- (void)setHighlightedImage:(UIImage *)highlightedImage {
+    if (self.highlightedImage == highlightedImage) return;
+    [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage];
+}
+
+- (void)setAnimationImages:(NSArray *)animationImages {
+    if (self.animationImages == animationImages) return;
+    [self setImage:animationImages withType:YYAnimatedImageTypeImages];
+}
+
+- (void)setHighlightedAnimationImages:(NSArray *)highlightedAnimationImages {
+    if (self.highlightedAnimationImages == highlightedAnimationImages) return;
+    [self setImage:highlightedAnimationImages withType:YYAnimatedImageTypeHighlightedImages];
+}
+
+- (void)setHighlighted:(BOOL)highlighted {
+    [super setHighlighted:highlighted];
+    if (_link) [self resetAnimated];
+    [self imageChanged];
+}
+
+- (id)imageForType:(YYAnimatedImageType)type {
+    switch (type) {
+        case YYAnimatedImageTypeNone: return nil;
+        case YYAnimatedImageTypeImage: return self.image;
+        case YYAnimatedImageTypeHighlightedImage: return self.highlightedImage;
+        case YYAnimatedImageTypeImages: return self.animationImages;
+        case YYAnimatedImageTypeHighlightedImages: return self.highlightedAnimationImages;
+    }
+    return nil;
+}
+
+- (YYAnimatedImageType)currentImageType {
+    YYAnimatedImageType curType = YYAnimatedImageTypeNone;
+    if (self.highlighted) {
+        if (self.highlightedAnimationImages.count) curType = YYAnimatedImageTypeHighlightedImages;
+        else if (self.highlightedImage) curType = YYAnimatedImageTypeHighlightedImage;
+    }
+    if (curType == YYAnimatedImageTypeNone) {
+        if (self.animationImages.count) curType = YYAnimatedImageTypeImages;
+        else if (self.image) curType = YYAnimatedImageTypeImage;
+    }
+    return curType;
+}
+
+- (void)setImage:(id)image withType:(YYAnimatedImageType)type {
+    [self stopAnimating];
+    if (_link) [self resetAnimated];
+    _curFrame = nil;
+    switch (type) {
+        case YYAnimatedImageTypeNone: break;
+        case YYAnimatedImageTypeImage: super.image = image; break;
+        case YYAnimatedImageTypeHighlightedImage: super.highlightedImage = image; break;
+        case YYAnimatedImageTypeImages: super.animationImages = image; break;
+        case YYAnimatedImageTypeHighlightedImages: super.highlightedAnimationImages = image; break;
+    }
+    [self imageChanged];
+}
+
+- (void)imageChanged {
+    YYAnimatedImageType newType = [self currentImageType];
+    id newVisibleImage = [self imageForType:newType];
+    NSUInteger newImageFrameCount = 0;
+    BOOL hasContentsRect = NO;
+    if ([newVisibleImage isKindOfClass:[UIImage class]] &&
+        [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
+        newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
+        if (newImageFrameCount > 1) {
+            hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
+        }
+    }
+    if (!hasContentsRect && _curImageHasContentsRect) {
+        if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
+            [CATransaction begin];
+            [CATransaction setDisableActions:YES];
+            self.layer.contentsRect = CGRectMake(0, 0, 1, 1);
+            [CATransaction commit];
+        }
+    }
+    _curImageHasContentsRect = hasContentsRect;
+    if (hasContentsRect) {
+        CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
+        [self setContentsRect:rect forImage:newVisibleImage];
+    }
+    
+    if (newImageFrameCount > 1) {
+        [self resetAnimated];
+        _curAnimatedImage = newVisibleImage;
+        _curFrame = newVisibleImage;
+        _totalLoop = _curAnimatedImage.animatedImageLoopCount;
+        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
+        [self calcMaxBufferCount];
+    }
+    [self setNeedsDisplay];
+    [self didMoved];
+}
+
+// dynamically adjust buffer size for current memory.
+- (void)calcMaxBufferCount {
+    int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame;
+    if (bytes == 0) bytes = 1024;
+    
+    int64_t total = _YYDeviceMemoryTotal();
+    int64_t free = _YYDeviceMemoryFree();
+    int64_t max = MIN(total * 0.2, free * 0.6);
+    max = MAX(max, BUFFER_SIZE);
+    if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max;
+    double maxBufferCount = (double)max / (double)bytes;
+    if (maxBufferCount < 1) maxBufferCount = 1;
+    else if (maxBufferCount > 512) maxBufferCount = 512;
+    _maxBufferCount = maxBufferCount;
+}
+
+- (void)dealloc {
+    [_requestQueue cancelAllOperations];
+    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
+    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
+    [_link invalidate];
+}
+
+- (BOOL)isAnimating {
+    return self.currentIsPlayingAnimation;
+}
+
+- (void)stopAnimating {
+    [super stopAnimating];
+    [_requestQueue cancelAllOperations];
+    _link.paused = YES;
+    self.currentIsPlayingAnimation = NO;
+}
+
+- (void)startAnimating {
+    YYAnimatedImageType type = [self currentImageType];
+    if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {
+        NSArray *images = [self imageForType:type];
+        if (images.count > 0) {
+            [super startAnimating];
+            self.currentIsPlayingAnimation = YES;
+        }
+    } else {
+        if (_curAnimatedImage && _link.paused) {
+            _curLoop = 0;
+            _loopEnd = NO;
+            _link.paused = NO;
+            self.currentIsPlayingAnimation = YES;
+        }
+    }
+}
+
+- (void)didReceiveMemoryWarning:(NSNotification *)notification {
+    [_requestQueue cancelAllOperations];
+    [_requestQueue addOperationWithBlock: ^{
+        _incrBufferCount = -60 - (int)(arc4random() % 120); // about 1~3 seconds to grow back..
+        NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
+        LOCK(
+             NSArray * keys = _buffer.allKeys;
+             for (NSNumber * key in keys) {
+                 if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
+                     [_buffer removeObjectForKey:key];
+                 }
+             }
+        )//LOCK
+    }];
+}
+
+- (void)didEnterBackground:(NSNotification *)notification {
+    [_requestQueue cancelAllOperations];
+    NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
+    LOCK(
+         NSArray * keys = _buffer.allKeys;
+         for (NSNumber * key in keys) {
+             if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
+                 [_buffer removeObjectForKey:key];
+             }
+         }
+     )//LOCK
+}
+
+- (void)step:(CADisplayLink *)link {
+    UIImage <YYAnimatedImage> *image = _curAnimatedImage;
+    NSMutableDictionary *buffer = _buffer;
+    UIImage *bufferedImage = nil;
+    NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
+    BOOL bufferIsFull = NO;
+    
+    if (!image) return;
+    if (_loopEnd) { // view will keep in last frame
+        [self stopAnimating];
+        return;
+    }
+    
+    NSTimeInterval delay = 0;
+    if (!_bufferMiss) {
+        _time += link.duration;
+        delay = [image animatedImageDurationAtIndex:_curIndex];
+        if (_time < delay) return;
+        _time -= delay;
+        if (nextIndex == 0) {
+            _curLoop++;
+            if (_curLoop >= _totalLoop && _totalLoop != 0) {
+                _loopEnd = YES;
+                [self stopAnimating];
+                [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
+                return; // stop at last frame
+            }
+        }
+        delay = [image animatedImageDurationAtIndex:nextIndex];
+        if (_time > delay) _time = delay; // do not jump over frame
+    }
+    LOCK(
+         bufferedImage = buffer[@(nextIndex)];
+         if (bufferedImage) {
+             if ((int)_incrBufferCount < _totalFrameCount) {
+                 [buffer removeObjectForKey:@(nextIndex)];
+             }
+             [self willChangeValueForKey:@"currentAnimatedImageIndex"];
+             _curIndex = nextIndex;
+             [self didChangeValueForKey:@"currentAnimatedImageIndex"];
+             _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
+             if (_curImageHasContentsRect) {
+                 _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
+                 [self setContentsRect:_curContentsRect forImage:_curFrame];
+             }
+             nextIndex = (_curIndex + 1) % _totalFrameCount;
+             _bufferMiss = NO;
+             if (buffer.count == _totalFrameCount) {
+                 bufferIsFull = YES;
+             }
+         } else {
+             _bufferMiss = YES;
+         }
+    )//LOCK
+    
+    if (!_bufferMiss) {
+        [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
+    }
+    
+    if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity
+        _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
+        operation.view = self;
+        operation.nextIndex = nextIndex;
+        operation.curImage = image;
+        [_requestQueue addOperation:operation];
+    }
+}
+
+- (void)displayLayer:(CALayer *)layer {
+    if (_curFrame) {
+        layer.contents = (__bridge id)_curFrame.CGImage;
+    }
+}
+
+- (void)setContentsRect:(CGRect)rect forImage:(UIImage *)image{
+    CGRect layerRect = CGRectMake(0, 0, 1, 1);
+    if (image) {
+        CGSize imageSize = image.size;
+        if (imageSize.width > 0.01 && imageSize.height > 0.01) {
+            layerRect.origin.x = rect.origin.x / imageSize.width;
+            layerRect.origin.y = rect.origin.y / imageSize.height;
+            layerRect.size.width = rect.size.width / imageSize.width;
+            layerRect.size.height = rect.size.height / imageSize.height;
+            layerRect = CGRectIntersection(layerRect, CGRectMake(0, 0, 1, 1));
+            if (CGRectIsNull(layerRect) || CGRectIsEmpty(layerRect)) {
+                layerRect = CGRectMake(0, 0, 1, 1);
+            }
+        }
+    }
+    [CATransaction begin];
+    [CATransaction setDisableActions:YES];
+    self.layer.contentsRect = layerRect;
+    [CATransaction commit];
+}
+
+- (void)didMoved {
+    if (self.autoPlayAnimatedImage) {
+        if(self.superview && self.window) {
+            [self startAnimating];
+        } else {
+            [self stopAnimating];
+        }
+    }
+}
+
+- (void)didMoveToWindow {
+    [super didMoveToWindow];
+    [self didMoved];
+}
+
+- (void)didMoveToSuperview {
+    [super didMoveToSuperview];
+    [self didMoved];
+}
+
+- (void)setCurrentAnimatedImageIndex:(NSUInteger)currentAnimatedImageIndex {
+    if (!_curAnimatedImage) return;
+    if (currentAnimatedImageIndex >= _curAnimatedImage.animatedImageFrameCount) return;
+    if (_curIndex == currentAnimatedImageIndex) return;
+    
+    void (^block)() = ^{
+        LOCK(
+             [_requestQueue cancelAllOperations];
+             [_buffer removeAllObjects];
+             [self willChangeValueForKey:@"currentAnimatedImageIndex"];
+             _curIndex = currentAnimatedImageIndex;
+             [self didChangeValueForKey:@"currentAnimatedImageIndex"];
+             _curFrame = [_curAnimatedImage animatedImageFrameAtIndex:_curIndex];
+             if (_curImageHasContentsRect) {
+                 _curContentsRect = [_curAnimatedImage animatedImageContentsRectAtIndex:_curIndex];
+             }
+             _time = 0;
+             _loopEnd = NO;
+             _bufferMiss = NO;
+             [self.layer setNeedsDisplay];
+        )//LOCK
+    };
+    
+    if (pthread_main_np()) {
+        block();
+    } else {
+        dispatch_async(dispatch_get_main_queue(), block);
+    }
+}
+
+- (NSUInteger)currentAnimatedImageIndex {
+    return _curIndex;
+}
+
+- (void)setRunloopMode:(NSString *)runloopMode {
+    if ([_runloopMode isEqual:runloopMode]) return;
+    if (_link) {
+        if (_runloopMode) {
+            [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
+        }
+        if (runloopMode.length) {
+            [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:runloopMode];
+        }
+    }
+    _runloopMode = runloopMode.copy;
+}
+
+#pragma mark - Override NSObject(NSKeyValueObservingCustomization)
+
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+    if ([key isEqualToString:@"currentAnimatedImageIndex"]) {
+        return NO;
+    }
+    return [super automaticallyNotifiesObserversForKey:key];
+}
+
+#pragma mark - NSCoding
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder {
+    self = [super initWithCoder:aDecoder];
+    _runloopMode = [aDecoder decodeObjectForKey:@"runloopMode"];
+    if (_runloopMode.length == 0) _runloopMode = NSRunLoopCommonModes;
+    if ([aDecoder containsValueForKey:@"autoPlayAnimatedImage"]) {
+        _autoPlayAnimatedImage = [aDecoder decodeBoolForKey:@"autoPlayAnimatedImage"];
+    } else {
+        _autoPlayAnimatedImage = YES;
+    }
+    
+    UIImage *image = [aDecoder decodeObjectForKey:@"YYAnimatedImage"];
+    UIImage *highlightedImage = [aDecoder decodeObjectForKey:@"YYHighlightedAnimatedImage"];
+    if (image) {
+        self.image = image;
+        [self setImage:image withType:YYAnimatedImageTypeImage];
+    }
+    if (highlightedImage) {
+        self.highlightedImage = highlightedImage;
+        [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage];
+    }
+    return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [super encodeWithCoder:aCoder];
+    [aCoder encodeObject:_runloopMode forKey:@"runloopMode"];
+    [aCoder encodeBool:_autoPlayAnimatedImage forKey:@"autoPlayAnimatedImage"];
+    
+    BOOL ani, multi;
+    ani = [self.image conformsToProtocol:@protocol(YYAnimatedImage)];
+    multi = (ani && ((UIImage <YYAnimatedImage> *)self.image).animatedImageFrameCount > 1);
+    if (multi) [aCoder encodeObject:self.image forKey:@"YYAnimatedImage"];
+    
+    ani = [self.highlightedImage conformsToProtocol:@protocol(YYAnimatedImage)];
+    multi = (ani && ((UIImage <YYAnimatedImage> *)self.highlightedImage).animatedImageFrameCount > 1);
+    if (multi) [aCoder encodeObject:self.highlightedImage forKey:@"YYHighlightedAnimatedImage"];
+}
+
+@end

+ 109 - 0
Pods/YYImage/YYImage/YYFrameImage.h

@@ -0,0 +1,109 @@
+//
+//  YYFrameImage.h
+//  YYImage <https://github.com/ibireme/YYImage>
+//
+//  Created by ibireme on 14/12/9.
+//  Copyright (c) 2015 ibireme.
+//
+//  This source code is licensed under the MIT-style license found in the
+//  LICENSE file in the root directory of this source tree.
+//
+
+#import <UIKit/UIKit.h>
+
+#if __has_include(<YYImage/YYImage.h>)
+#import <YYImage/YYAnimatedImageView.h>
+#elif __has_include(<YYWebImage/YYImage.h>)
+#import <YYWebImage/YYAnimatedImageView.h>
+#else
+#import "YYAnimatedImageView.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ An image to display frame-based animation.
+ 
+ @discussion It is a fully compatible `UIImage` subclass.
+ It only support system image format such as png and jpeg.
+ The animation can be played by YYAnimatedImageView.
+ 
+ Sample Code:
+     
+     NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
+     NSArray *times = @[@0.1, @0.2, @0.1];
+     YYFrameImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
+     YYAnimatedImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
+     [view addSubView:imageView];
+ */
+@interface YYFrameImage : UIImage <YYAnimatedImage>
+
+/**
+ Create a frame animated image from files.
+ 
+ @param paths            An array of NSString objects, contains the full or 
+                         partial path to each image file.
+                         e.g. @[@"/ani/1.png",@"/ani/2.png",@"/ani/3.png"]
+ 
+ @param oneFrameDuration The duration (in seconds) per frame.
+ 
+ @param loopCount        The animation loop count, 0 means infinite.
+ 
+ @return An initialized YYFrameImage object, or nil when an error occurs.
+ */
+- (nullable instancetype)initWithImagePaths:(NSArray<NSString *> *)paths
+                           oneFrameDuration:(NSTimeInterval)oneFrameDuration
+                                  loopCount:(NSUInteger)loopCount;
+
+/**
+ Create a frame animated image from files.
+ 
+ @param paths          An array of NSString objects, contains the full or
+                       partial path to each image file.
+                       e.g. @[@"/ani/frame1.png",@"/ani/frame2.png",@"/ani/frame3.png"]
+ 
+ @param frameDurations An array of NSNumber objects, contains the duration (in seconds) per frame.
+                       e.g. @[@0.1, @0.2, @0.3];
+ 
+ @param loopCount      The animation loop count, 0 means infinite.
+ 
+ @return An initialized YYFrameImage object, or nil when an error occurs.
+ */
+- (nullable instancetype)initWithImagePaths:(NSArray<NSString *> *)paths
+                             frameDurations:(NSArray<NSNumber *> *)frameDurations
+                                  loopCount:(NSUInteger)loopCount;
+
+/**
+ Create a frame animated image from an array of data.
+ 
+ @param dataArray        An array of NSData objects.
+ 
+ @param oneFrameDuration The duration (in seconds) per frame.
+ 
+ @param loopCount        The animation loop count, 0 means infinite.
+ 
+ @return An initialized YYFrameImage object, or nil when an error occurs.
+ */
+- (nullable instancetype)initWithImageDataArray:(NSArray<NSData *> *)dataArray
+                               oneFrameDuration:(NSTimeInterval)oneFrameDuration
+                                      loopCount:(NSUInteger)loopCount;
+
+/**
+ Create a frame animated image from an array of data.
+ 
+ @param dataArray      An array of NSData objects.
+ 
+ @param frameDurations An array of NSNumber objects, contains the duration (in seconds) per frame.
+                       e.g. @[@0.1, @0.2, @0.3];
+ 
+ @param loopCount      The animation loop count, 0 means infinite.
+ 
+ @return An initialized YYFrameImage object, or nil when an error occurs.
+ */
+- (nullable instancetype)initWithImageDataArray:(NSArray<NSData *> *)dataArray
+                                 frameDurations:(NSArray *)frameDurations
+                                      loopCount:(NSUInteger)loopCount;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 150 - 0
Pods/YYImage/YYImage/YYFrameImage.m

@@ -0,0 +1,150 @@
+//
+//  YYFrameImage.m
+//  YYImage <https://github.com/ibireme/YYImage>
+//
+//  Created by ibireme on 14/12/9.
+//  Copyright (c) 2015 ibireme.
+//
+//  This source code is licensed under the MIT-style license found in the
+//  LICENSE file in the root directory of this source tree.
+//
+
+#import "YYFrameImage.h"
+#import "YYImageCoder.h"
+
+
+/**
+ Return the path scale.
+ 
+ e.g.
+ <table>
+ <tr><th>Path            </th><th>Scale </th></tr>
+ <tr><td>"icon.png"      </td><td>1     </td></tr>
+ <tr><td>"icon@2x.png"   </td><td>2     </td></tr>
+ <tr><td>"icon@2.5x.png" </td><td>2.5   </td></tr>
+ <tr><td>"icon@2x"       </td><td>1     </td></tr>
+ <tr><td>"icon@2x..png"  </td><td>1     </td></tr>
+ <tr><td>"icon@2x.png/"  </td><td>1     </td></tr>
+ </table>
+ */
+static CGFloat _NSStringPathScale(NSString *string) {
+    if (string.length == 0 || [string hasSuffix:@"/"]) return 1;
+    NSString *name = string.stringByDeletingPathExtension;
+    __block CGFloat scale = 1;
+    
+    NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:@"@[0-9]+\\.?[0-9]*x$" options:NSRegularExpressionAnchorsMatchLines error:nil];
+    [pattern enumerateMatchesInString:name options:kNilOptions range:NSMakeRange(0, name.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        if (result.range.location >= 3) {
+            scale = [string substringWithRange:NSMakeRange(result.range.location + 1, result.range.length - 2)].doubleValue;
+        }
+    }];
+    
+    return scale;
+}
+
+
+
+@implementation YYFrameImage {
+    NSUInteger _loopCount;
+    NSUInteger _oneFrameBytes;
+    NSArray *_imagePaths;
+    NSArray *_imageDatas;
+    NSArray *_frameDurations;
+}
+
+- (instancetype)initWithImagePaths:(NSArray *)paths oneFrameDuration:(NSTimeInterval)oneFrameDuration loopCount:(NSUInteger)loopCount {
+    NSMutableArray *durations = [NSMutableArray new];
+    for (int i = 0, max = (int)paths.count; i < max; i++) {
+        [durations addObject:@(oneFrameDuration)];
+    }
+    return [self initWithImagePaths:paths frameDurations:durations loopCount:loopCount];
+}
+
+- (instancetype)initWithImagePaths:(NSArray *)paths frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount {
+    if (paths.count == 0) return nil;
+    if (paths.count != frameDurations.count) return nil;
+    
+    NSString *firstPath = paths[0];
+    NSData *firstData = [NSData dataWithContentsOfFile:firstPath];
+    CGFloat scale = _NSStringPathScale(firstPath);
+    UIImage *firstCG = [[[UIImage alloc] initWithData:firstData] yy_imageByDecoded];
+    self = [self initWithCGImage:firstCG.CGImage scale:scale orientation:UIImageOrientationUp];
+    if (!self) return nil;
+    long frameByte = CGImageGetBytesPerRow(firstCG.CGImage) * CGImageGetHeight(firstCG.CGImage);
+    _oneFrameBytes = (NSUInteger)frameByte;
+    _imagePaths = paths.copy;
+    _frameDurations = frameDurations.copy;
+    _loopCount = loopCount;
+    
+    return self;
+}
+
+- (instancetype)initWithImageDataArray:(NSArray *)dataArray oneFrameDuration:(NSTimeInterval)oneFrameDuration loopCount:(NSUInteger)loopCount {
+    NSMutableArray *durations = [NSMutableArray new];
+    for (int i = 0, max = (int)dataArray.count; i < max; i++) {
+        [durations addObject:@(oneFrameDuration)];
+    }
+    return [self initWithImageDataArray:dataArray frameDurations:durations loopCount:loopCount];
+}
+
+- (instancetype)initWithImageDataArray:(NSArray *)dataArray frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount {
+    if (dataArray.count == 0) return nil;
+    if (dataArray.count != frameDurations.count) return nil;
+    
+    NSData *firstData = dataArray[0];
+    CGFloat scale = [UIScreen mainScreen].scale;
+    UIImage *firstCG = [[[UIImage alloc] initWithData:firstData] yy_imageByDecoded];
+    self = [self initWithCGImage:firstCG.CGImage scale:scale orientation:UIImageOrientationUp];
+    if (!self) return nil;
+    long frameByte = CGImageGetBytesPerRow(firstCG.CGImage) * CGImageGetHeight(firstCG.CGImage);
+    _oneFrameBytes = (NSUInteger)frameByte;
+    _imageDatas = dataArray.copy;
+    _frameDurations = frameDurations.copy;
+    _loopCount = loopCount;
+    
+    return self;
+}
+
+#pragma mark - YYAnimtedImage
+
+- (NSUInteger)animatedImageFrameCount {
+    if (_imagePaths) {
+        return _imagePaths.count;
+    } else if (_imageDatas) {
+        return _imageDatas.count;
+    } else {
+        return 1;
+    }
+}
+
+- (NSUInteger)animatedImageLoopCount {
+    return _loopCount;
+}
+
+- (NSUInteger)animatedImageBytesPerFrame {
+    return _oneFrameBytes;
+}
+
+- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
+    if (_imagePaths) {
+        if (index >= _imagePaths.count) return nil;
+        NSString *path = _imagePaths[index];
+        CGFloat scale = _NSStringPathScale(path);
+        NSData *data = [NSData dataWithContentsOfFile:path];
+        return [[UIImage imageWithData:data scale:scale] yy_imageByDecoded];
+    } else if (_imageDatas) {
+        if (index >= _imageDatas.count) return nil;
+        NSData *data = _imageDatas[index];
+        return [[UIImage imageWithData:data scale:[UIScreen mainScreen].scale] yy_imageByDecoded];
+    } else {
+        return index == 0 ? self : nil;
+    }
+}
+
+- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
+    if (index >= _frameDurations.count) return 0;
+    NSNumber *num = _frameDurations[index];
+    return [num doubleValue];
+}
+
+@end

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff