Browse Source

- Converted Obj-C demo with Autolayout
- Enhanced demo project and added various examples demonstrating almost all properties of library.
- Enhanced library Settings Controller.
- Moved some contents of README.md file to other files.

hackiftekhar 9 năm trước cách đây
mục cha
commit
6becc06ca3
100 tập tin đã thay đổi với 22120 bổ sung501 xóa
  1. 307 20
      Demo.xcodeproj/project.pbxproj
  2. 19 0
      Demo/Objective_C_Demo/Cell/ChatTableViewCell.h
  3. 34 0
      Demo/Objective_C_Demo/Cell/ChatTableViewCell.m
  4. 19 0
      Demo/Objective_C_Demo/Cell/ColorTableViewCell.h
  5. 13 0
      Demo/Objective_C_Demo/Cell/ColorTableViewCell.m
  6. 15 0
      Demo/Objective_C_Demo/Cell/ImageSwitchTableViewCell.h
  7. 13 0
      Demo/Objective_C_Demo/Cell/ImageSwitchTableViewCell.m
  8. 0 1
      Demo/Objective_C_Demo/Cell/NavigationTableViewCell.m
  9. 0 1
      Demo/Objective_C_Demo/Cell/OptionTableViewCell.m
  10. 0 1
      Demo/Objective_C_Demo/Cell/StepperTableViewCell.m
  11. 0 1
      Demo/Objective_C_Demo/Cell/SwitchTableViewCell.m
  12. 18 0
      Demo/Objective_C_Demo/Cell/TextFieldTableViewCell.h
  13. 13 0
      Demo/Objective_C_Demo/Cell/TextFieldTableViewCell.m
  14. 1 1
      Demo/Objective_C_Demo/Resources/Info.plist
  15. 585 248
      Demo/Objective_C_Demo/Storyboard/Main.storyboard
  16. 26 0
      Demo/Objective_C_Demo/Third Party/Color Picker TextField/ColorPickerTextField.h
  17. 199 0
      Demo/Objective_C_Demo/Third Party/Color Picker TextField/ColorPickerTextField.m
  18. 15 0
      Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorButton.h
  19. 73 0
      Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorButton.m
  20. 28 0
      Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorPickerView.h
  21. 164 0
      Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorPickerView.m
  22. 21 0
      Demo/Objective_C_Demo/Third Party/Color Picker TextField/UIColor+HexColors.h
  23. 110 0
      Demo/Objective_C_Demo/Third Party/Color Picker TextField/UIColor+HexColors.m
  24. 27 0
      Demo/Objective_C_Demo/ViewController/BottomBlankSpaceViewController.m
  25. 13 0
      Demo/Objective_C_Demo/ViewController/ChatViewController.h
  26. 105 0
      Demo/Objective_C_Demo/ViewController/ChatViewController.m
  27. 23 1
      Demo/Objective_C_Demo/ViewController/CollectionViewDemoController.m
  28. 0 13
      Demo/Objective_C_Demo/ViewController/CustomSubclassView.h
  29. 0 21
      Demo/Objective_C_Demo/ViewController/CustomSubclassView.m
  30. 137 12
      Demo/Objective_C_Demo/ViewController/CustomViewController.m
  31. 24 1
      Demo/Objective_C_Demo/ViewController/ExampleTableViewController.m
  32. 13 0
      Demo/Objective_C_Demo/ViewController/LayoutGuideViewController.h
  33. 13 0
      Demo/Objective_C_Demo/ViewController/LayoutGuideViewController.m
  34. 49 15
      Demo/Objective_C_Demo/ViewController/ManualToolbarViewController.m
  35. 48 3
      Demo/Objective_C_Demo/ViewController/NavigationBarViewController.m
  36. 13 0
      Demo/Objective_C_Demo/ViewController/RefreshLayoutViewController.h
  37. 38 0
      Demo/Objective_C_Demo/ViewController/RefreshLayoutViewController.m
  38. 26 0
      Demo/Objective_C_Demo/ViewController/ScrollViewController.m
  39. 108 50
      Demo/Objective_C_Demo/ViewController/Settings/SettingsViewController.m
  40. 24 1
      Demo/Objective_C_Demo/ViewController/SpecialCaseViewController.m
  41. 26 0
      Demo/Objective_C_Demo/ViewController/TableViewInContainerViewController.m
  42. 0 3
      Demo/Objective_C_Demo/ViewController/TextFieldViewController.h
  43. 32 66
      Demo/Objective_C_Demo/ViewController/TextFieldViewController.m
  44. 24 1
      Demo/Objective_C_Demo/ViewController/TextSelectionViewController.m
  45. 13 0
      Demo/Objective_C_Demo/ViewController/TextViewController.h
  46. 13 0
      Demo/Objective_C_Demo/ViewController/TextViewController.m
  47. 0 3
      Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.h
  48. 24 31
      Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.m
  49. 39 7
      Demo/Objective_C_Demo/ViewController/ViewController.m
  50. 27 0
      Demo/Objective_C_Demo/ViewController/WebViewController.m
  51. 13 0
      Demo/Objective_C_Demo/ViewController/YYTextViewController.h
  52. 42 0
      Demo/Objective_C_Demo/ViewController/YYTextViewController.m
  53. 1415 0
      Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.h
  54. 1403 0
      Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.m
  55. 37 0
      Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.h
  56. 224 0
      Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.m
  57. 41 0
      Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.h
  58. 146 0
      Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.m
  59. 72 0
      Demo/Objective_C_Demo/YYText/UIView+YYText.h
  60. 123 0
      Demo/Objective_C_Demo/YYText/UIView+YYText.m
  61. 380 0
      Demo/Objective_C_Demo/YYText/YYLabel.h
  62. 1305 0
      Demo/Objective_C_Demo/YYText/YYLabel.m
  63. 50 0
      Demo/Objective_C_Demo/YYText/YYText.h
  64. 33 0
      Demo/Objective_C_Demo/YYText/YYTextArchiver.h
  65. 252 0
      Demo/Objective_C_Demo/YYText/YYTextArchiver.m
  66. 79 0
      Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.h
  67. 235 0
      Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.m
  68. 347 0
      Demo/Objective_C_Demo/YYText/YYTextAttribute.h
  69. 524 0
      Demo/Objective_C_Demo/YYText/YYTextAttribute.m
  70. 55 0
      Demo/Objective_C_Demo/YYText/YYTextContainerView.h
  71. 144 0
      Demo/Objective_C_Demo/YYText/YYTextContainerView.m
  72. 95 0
      Demo/Objective_C_Demo/YYText/YYTextDebugOption.h
  73. 140 0
      Demo/Objective_C_Demo/YYText/YYTextDebugOption.m
  74. 52 0
      Demo/Objective_C_Demo/YYText/YYTextEffectWindow.h
  75. 420 0
      Demo/Objective_C_Demo/YYText/YYTextEffectWindow.m
  76. 87 0
      Demo/Objective_C_Demo/YYText/YYTextInput.h
  77. 152 0
      Demo/Objective_C_Demo/YYText/YYTextInput.m
  78. 98 0
      Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.h
  79. 521 0
      Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.m
  80. 571 0
      Demo/Objective_C_Demo/YYText/YYTextLayout.h
  81. 3361 0
      Demo/Objective_C_Demo/YYText/YYTextLayout.m
  82. 84 0
      Demo/Objective_C_Demo/YYText/YYTextLine.h
  83. 167 0
      Demo/Objective_C_Demo/YYText/YYTextLine.m
  84. 52 0
      Demo/Objective_C_Demo/YYText/YYTextMagnifier.h
  85. 355 0
      Demo/Objective_C_Demo/YYText/YYTextMagnifier.m
  86. 91 0
      Demo/Objective_C_Demo/YYText/YYTextParser.h
  87. 417 0
      Demo/Objective_C_Demo/YYText/YYTextParser.m
  88. 78 0
      Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.h
  89. 83 0
      Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.m
  90. 68 0
      Demo/Objective_C_Demo/YYText/YYTextRunDelegate.h
  91. 71 0
      Demo/Objective_C_Demo/YYText/YYTextRunDelegate.m
  92. 78 0
      Demo/Objective_C_Demo/YYText/YYTextSelectionView.h
  93. 329 0
      Demo/Objective_C_Demo/YYText/YYTextSelectionView.m
  94. 42 0
      Demo/Objective_C_Demo/YYText/YYTextTransaction.h
  95. 81 0
      Demo/Objective_C_Demo/YYText/YYTextTransaction.m
  96. 563 0
      Demo/Objective_C_Demo/YYText/YYTextUtilities.h
  97. 309 0
      Demo/Objective_C_Demo/YYText/YYTextUtilities.m
  98. 410 0
      Demo/Objective_C_Demo/YYText/YYTextView.h
  99. 3832 0
      Demo/Objective_C_Demo/YYText/YYTextView.m
  100. 61 0
      Demo/Objective_C_Demo/YYText/YYTextWeakProxy.h

+ 307 - 20
Demo.xcodeproj/project.pbxproj

@@ -12,6 +12,71 @@
 		4C7FCC271C6A507E00537BA0 /* IQUIScrollView+Additions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FCC261C6A507E00537BA0 /* IQUIScrollView+Additions.m */; };
 		4C7FCC291C6A57FF00537BA0 /* IQUIScrollView+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FCC281C6A57FF00537BA0 /* IQUIScrollView+Additions.swift */; };
 		4CE611A01B98B7250020591A /* DemoObjCUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6119F1B98B7250020591A /* DemoObjCUITests.m */; };
+		4CE74F411CF0465C0093AC0C /* YYTextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F401CF0465C0093AC0C /* YYTextViewController.m */; };
+		4CE74F441CF047610093AC0C /* RefreshLayoutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F431CF047610093AC0C /* RefreshLayoutViewController.m */; };
+		4CE74F471CF0809C0093AC0C /* LayoutGuideViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F461CF0809C0093AC0C /* LayoutGuideViewController.m */; };
+		4CE74F4A1CF080B60093AC0C /* ChatViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F491CF080B60093AC0C /* ChatViewController.m */; };
+		4CE74F4D1CF080C10093AC0C /* TextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F4C1CF080C10093AC0C /* TextViewController.m */; };
+		4CE74F581CF185F70093AC0C /* ColorPickerTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F501CF185F70093AC0C /* ColorPickerTextField.m */; };
+		4CE74F591CF185F70093AC0C /* ColorPickerTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F501CF185F70093AC0C /* ColorPickerTextField.m */; };
+		4CE74F5A1CF185F70093AC0C /* HFColorButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F531CF185F70093AC0C /* HFColorButton.m */; };
+		4CE74F5B1CF185F70093AC0C /* HFColorButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F531CF185F70093AC0C /* HFColorButton.m */; };
+		4CE74F5C1CF185F70093AC0C /* HFColorPickerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F551CF185F70093AC0C /* HFColorPickerView.m */; };
+		4CE74F5D1CF185F70093AC0C /* HFColorPickerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F551CF185F70093AC0C /* HFColorPickerView.m */; };
+		4CE74F5E1CF185F70093AC0C /* UIColor+HexColors.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F571CF185F70093AC0C /* UIColor+HexColors.m */; };
+		4CE74F5F1CF185F70093AC0C /* UIColor+HexColors.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F571CF185F70093AC0C /* UIColor+HexColors.m */; };
+		4CE74F621CF1864E0093AC0C /* ColorTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F611CF1864E0093AC0C /* ColorTableViewCell.m */; };
+		4CE74F651CF186720093AC0C /* TextFieldTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F641CF186720093AC0C /* TextFieldTableViewCell.m */; };
+		4CE74F681CF1BBCC0093AC0C /* ImageSwitchTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F671CF1BBCC0093AC0C /* ImageSwitchTableViewCell.m */; };
+		4CE74F9B1CF206E40093AC0C /* NSAttributedString+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F6B1CF206E40093AC0C /* NSAttributedString+YYText.m */; };
+		4CE74F9C1CF206E40093AC0C /* NSAttributedString+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F6B1CF206E40093AC0C /* NSAttributedString+YYText.m */; };
+		4CE74F9D1CF206E40093AC0C /* NSParagraphStyle+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F6D1CF206E40093AC0C /* NSParagraphStyle+YYText.m */; };
+		4CE74F9E1CF206E40093AC0C /* NSParagraphStyle+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F6D1CF206E40093AC0C /* NSParagraphStyle+YYText.m */; };
+		4CE74F9F1CF206E40093AC0C /* UIPasteboard+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F6F1CF206E40093AC0C /* UIPasteboard+YYText.m */; };
+		4CE74FA01CF206E40093AC0C /* UIPasteboard+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F6F1CF206E40093AC0C /* UIPasteboard+YYText.m */; };
+		4CE74FA11CF206E40093AC0C /* UIView+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F711CF206E40093AC0C /* UIView+YYText.m */; };
+		4CE74FA21CF206E40093AC0C /* UIView+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F711CF206E40093AC0C /* UIView+YYText.m */; };
+		4CE74FA31CF206E40093AC0C /* YYLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F731CF206E40093AC0C /* YYLabel.m */; };
+		4CE74FA41CF206E40093AC0C /* YYLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F731CF206E40093AC0C /* YYLabel.m */; };
+		4CE74FA51CF206E40093AC0C /* YYTextArchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F761CF206E40093AC0C /* YYTextArchiver.m */; };
+		4CE74FA61CF206E40093AC0C /* YYTextArchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F761CF206E40093AC0C /* YYTextArchiver.m */; };
+		4CE74FA71CF206E40093AC0C /* YYTextAsyncLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F781CF206E40093AC0C /* YYTextAsyncLayer.m */; };
+		4CE74FA81CF206E40093AC0C /* YYTextAsyncLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F781CF206E40093AC0C /* YYTextAsyncLayer.m */; };
+		4CE74FA91CF206E40093AC0C /* YYTextAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F7A1CF206E40093AC0C /* YYTextAttribute.m */; };
+		4CE74FAA1CF206E40093AC0C /* YYTextAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F7A1CF206E40093AC0C /* YYTextAttribute.m */; };
+		4CE74FAB1CF206E40093AC0C /* YYTextContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F7C1CF206E40093AC0C /* YYTextContainerView.m */; };
+		4CE74FAC1CF206E40093AC0C /* YYTextContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F7C1CF206E40093AC0C /* YYTextContainerView.m */; };
+		4CE74FAD1CF206E40093AC0C /* YYTextDebugOption.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F7E1CF206E40093AC0C /* YYTextDebugOption.m */; };
+		4CE74FAE1CF206E40093AC0C /* YYTextDebugOption.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F7E1CF206E40093AC0C /* YYTextDebugOption.m */; };
+		4CE74FAF1CF206E40093AC0C /* YYTextEffectWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F801CF206E40093AC0C /* YYTextEffectWindow.m */; };
+		4CE74FB01CF206E40093AC0C /* YYTextEffectWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F801CF206E40093AC0C /* YYTextEffectWindow.m */; };
+		4CE74FB11CF206E40093AC0C /* YYTextInput.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F821CF206E40093AC0C /* YYTextInput.m */; };
+		4CE74FB21CF206E40093AC0C /* YYTextInput.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F821CF206E40093AC0C /* YYTextInput.m */; };
+		4CE74FB31CF206E40093AC0C /* YYTextKeyboardManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F841CF206E40093AC0C /* YYTextKeyboardManager.m */; };
+		4CE74FB41CF206E40093AC0C /* YYTextKeyboardManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F841CF206E40093AC0C /* YYTextKeyboardManager.m */; };
+		4CE74FB51CF206E40093AC0C /* YYTextLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F861CF206E40093AC0C /* YYTextLayout.m */; };
+		4CE74FB61CF206E40093AC0C /* YYTextLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F861CF206E40093AC0C /* YYTextLayout.m */; };
+		4CE74FB71CF206E40093AC0C /* YYTextLine.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F881CF206E40093AC0C /* YYTextLine.m */; };
+		4CE74FB81CF206E40093AC0C /* YYTextLine.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F881CF206E40093AC0C /* YYTextLine.m */; };
+		4CE74FB91CF206E40093AC0C /* YYTextMagnifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F8A1CF206E40093AC0C /* YYTextMagnifier.m */; };
+		4CE74FBA1CF206E40093AC0C /* YYTextMagnifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F8A1CF206E40093AC0C /* YYTextMagnifier.m */; };
+		4CE74FBB1CF206E40093AC0C /* YYTextParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F8C1CF206E40093AC0C /* YYTextParser.m */; };
+		4CE74FBC1CF206E40093AC0C /* YYTextParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F8C1CF206E40093AC0C /* YYTextParser.m */; };
+		4CE74FBD1CF206E40093AC0C /* YYTextRubyAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F8E1CF206E40093AC0C /* YYTextRubyAnnotation.m */; };
+		4CE74FBE1CF206E40093AC0C /* YYTextRubyAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F8E1CF206E40093AC0C /* YYTextRubyAnnotation.m */; };
+		4CE74FBF1CF206E40093AC0C /* YYTextRunDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F901CF206E40093AC0C /* YYTextRunDelegate.m */; };
+		4CE74FC01CF206E40093AC0C /* YYTextRunDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F901CF206E40093AC0C /* YYTextRunDelegate.m */; };
+		4CE74FC11CF206E40093AC0C /* YYTextSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F921CF206E40093AC0C /* YYTextSelectionView.m */; };
+		4CE74FC21CF206E40093AC0C /* YYTextSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F921CF206E40093AC0C /* YYTextSelectionView.m */; };
+		4CE74FC31CF206E40093AC0C /* YYTextTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F941CF206E40093AC0C /* YYTextTransaction.m */; };
+		4CE74FC41CF206E40093AC0C /* YYTextTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F941CF206E40093AC0C /* YYTextTransaction.m */; };
+		4CE74FC51CF206E40093AC0C /* YYTextUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F961CF206E40093AC0C /* YYTextUtilities.m */; };
+		4CE74FC61CF206E40093AC0C /* YYTextUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F961CF206E40093AC0C /* YYTextUtilities.m */; };
+		4CE74FC71CF206E40093AC0C /* YYTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F981CF206E40093AC0C /* YYTextView.m */; };
+		4CE74FC81CF206E40093AC0C /* YYTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F981CF206E40093AC0C /* YYTextView.m */; };
+		4CE74FC91CF206E40093AC0C /* YYTextWeakProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F9A1CF206E40093AC0C /* YYTextWeakProxy.m */; };
+		4CE74FCA1CF206E40093AC0C /* YYTextWeakProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74F9A1CF206E40093AC0C /* YYTextWeakProxy.m */; };
+		4CE74FCD1CF2174E0093AC0C /* ChatTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE74FCC1CF2174E0093AC0C /* ChatTableViewCell.m */; };
 		4CEC3A6B1B8CECDF00909DCA /* IQNSArray+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC3A581B8CECDF00909DCA /* IQNSArray+Sort.swift */; };
 		4CEC3A6C1B8CECDF00909DCA /* IQUITextFieldView+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC3A591B8CECDF00909DCA /* IQUITextFieldView+Additions.swift */; };
 		4CEC3A6D1B8CECDF00909DCA /* IQUIView+Hierarchy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC3A5A1B8CECDF00909DCA /* IQUIView+Hierarchy.swift */; };
@@ -27,7 +92,6 @@
 		4CEC3A771B8CECDF00909DCA /* IQToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC3A671B8CECDF00909DCA /* IQToolbar.swift */; };
 		4CEC3A781B8CECDF00909DCA /* IQUIView+IQKeyboardToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEC3A681B8CECDF00909DCA /* IQUIView+IQKeyboardToolbar.swift */; };
 		4CEC3A791B8CECDF00909DCA /* IQKeyboardManager.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 4CEC3A6A1B8CECDF00909DCA /* IQKeyboardManager.bundle */; };
-		C0017B771BAD941200BD1D70 /* CustomSubclassView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0017B761BAD941200BD1D70 /* CustomSubclassView.swift */; };
 		C0017B791BAD943400BD1D70 /* CustomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0017B781BAD943400BD1D70 /* CustomViewController.swift */; };
 		C03C87F51B8DCBF100295DFA /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03C87F41B8DCBF100295DFA /* SettingsViewController.swift */; };
 		C03C87F71B8DCC1400295DFA /* OptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03C87F61B8DCC1400295DFA /* OptionsViewController.swift */; };
@@ -68,7 +132,6 @@
 		C0CB62031B884FA100C33368 /* IQDropDownTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB61A61B884D4100C33368 /* IQDropDownTextField.m */; };
 		C0CB62041B884FA100C33368 /* BottomBlankSpaceViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB61A91B884D4100C33368 /* BottomBlankSpaceViewController.m */; };
 		C0CB62051B884FA100C33368 /* CollectionViewDemoController.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB61AB1B884D4100C33368 /* CollectionViewDemoController.m */; };
-		C0CB62061B884FA100C33368 /* CustomSubclassView.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB61AD1B884D4100C33368 /* CustomSubclassView.m */; };
 		C0CB62071B884FA100C33368 /* CustomViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB61AF1B884D4100C33368 /* CustomViewController.m */; };
 		C0CB62081B884FA100C33368 /* ExampleTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB61B11B884D4100C33368 /* ExampleTableViewController.m */; };
 		C0CB62091B884FA100C33368 /* ManualToolbarViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB61B31B884D4100C33368 /* ManualToolbarViewController.m */; };
@@ -98,7 +161,6 @@
 		C0CB62481B8850AE00C33368 /* IQToolbar.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB62371B8850AE00C33368 /* IQToolbar.m */; };
 		C0CB62491B8850AE00C33368 /* IQUIView+IQKeyboardToolbar.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB62391B8850AE00C33368 /* IQUIView+IQKeyboardToolbar.m */; };
 		C0CB624A1B8850AE00C33368 /* IQKeyboardManager.bundle in Resources */ = {isa = PBXBuildFile; fileRef = C0CB623C1B8850AE00C33368 /* IQKeyboardManager.bundle */; };
-		C0CB62721B88541F00C33368 /* CustomSubclassView.m in Sources */ = {isa = PBXBuildFile; fileRef = C0CB61AD1B884D4100C33368 /* CustomSubclassView.m */; };
 		C0CB62741B8856B300C33368 /* IQKeyboardManagerScreenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = C0CB62731B8856B300C33368 /* IQKeyboardManagerScreenshot.png */; };
 		C0CB62751B8856B300C33368 /* IQKeyboardManagerScreenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = C0CB62731B8856B300C33368 /* IQKeyboardManagerScreenshot.png */; };
 /* End PBXBuildFile section */
@@ -113,6 +175,81 @@
 		4CE6119D1B98B7250020591A /* DemoObjCUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoObjCUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		4CE6119F1B98B7250020591A /* DemoObjCUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DemoObjCUITests.m; sourceTree = "<group>"; };
 		4CE611A11B98B7250020591A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		4CE74F3F1CF0465C0093AC0C /* YYTextViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextViewController.h; sourceTree = "<group>"; };
+		4CE74F401CF0465C0093AC0C /* YYTextViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextViewController.m; sourceTree = "<group>"; };
+		4CE74F421CF047610093AC0C /* RefreshLayoutViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RefreshLayoutViewController.h; sourceTree = "<group>"; };
+		4CE74F431CF047610093AC0C /* RefreshLayoutViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RefreshLayoutViewController.m; sourceTree = "<group>"; };
+		4CE74F451CF0809C0093AC0C /* LayoutGuideViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutGuideViewController.h; sourceTree = "<group>"; };
+		4CE74F461CF0809C0093AC0C /* LayoutGuideViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutGuideViewController.m; sourceTree = "<group>"; };
+		4CE74F481CF080B60093AC0C /* ChatViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatViewController.h; sourceTree = "<group>"; };
+		4CE74F491CF080B60093AC0C /* ChatViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatViewController.m; sourceTree = "<group>"; };
+		4CE74F4B1CF080C10093AC0C /* TextViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextViewController.h; sourceTree = "<group>"; };
+		4CE74F4C1CF080C10093AC0C /* TextViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextViewController.m; sourceTree = "<group>"; };
+		4CE74F4F1CF185F70093AC0C /* ColorPickerTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ColorPickerTextField.h; sourceTree = "<group>"; };
+		4CE74F501CF185F70093AC0C /* ColorPickerTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ColorPickerTextField.m; sourceTree = "<group>"; };
+		4CE74F521CF185F70093AC0C /* HFColorButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HFColorButton.h; sourceTree = "<group>"; };
+		4CE74F531CF185F70093AC0C /* HFColorButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HFColorButton.m; sourceTree = "<group>"; };
+		4CE74F541CF185F70093AC0C /* HFColorPickerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HFColorPickerView.h; sourceTree = "<group>"; };
+		4CE74F551CF185F70093AC0C /* HFColorPickerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HFColorPickerView.m; sourceTree = "<group>"; };
+		4CE74F561CF185F70093AC0C /* UIColor+HexColors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+HexColors.h"; sourceTree = "<group>"; };
+		4CE74F571CF185F70093AC0C /* UIColor+HexColors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+HexColors.m"; sourceTree = "<group>"; };
+		4CE74F601CF1864E0093AC0C /* ColorTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ColorTableViewCell.h; sourceTree = "<group>"; };
+		4CE74F611CF1864E0093AC0C /* ColorTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ColorTableViewCell.m; sourceTree = "<group>"; };
+		4CE74F631CF186720093AC0C /* TextFieldTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextFieldTableViewCell.h; sourceTree = "<group>"; };
+		4CE74F641CF186720093AC0C /* TextFieldTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextFieldTableViewCell.m; sourceTree = "<group>"; };
+		4CE74F661CF1BBCC0093AC0C /* ImageSwitchTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageSwitchTableViewCell.h; sourceTree = "<group>"; };
+		4CE74F671CF1BBCC0093AC0C /* ImageSwitchTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageSwitchTableViewCell.m; sourceTree = "<group>"; };
+		4CE74F6A1CF206E40093AC0C /* NSAttributedString+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+YYText.h"; sourceTree = "<group>"; };
+		4CE74F6B1CF206E40093AC0C /* NSAttributedString+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+YYText.m"; sourceTree = "<group>"; };
+		4CE74F6C1CF206E40093AC0C /* NSParagraphStyle+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+YYText.h"; sourceTree = "<group>"; };
+		4CE74F6D1CF206E40093AC0C /* NSParagraphStyle+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSParagraphStyle+YYText.m"; sourceTree = "<group>"; };
+		4CE74F6E1CF206E40093AC0C /* UIPasteboard+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIPasteboard+YYText.h"; sourceTree = "<group>"; };
+		4CE74F6F1CF206E40093AC0C /* UIPasteboard+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIPasteboard+YYText.m"; sourceTree = "<group>"; };
+		4CE74F701CF206E40093AC0C /* UIView+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+YYText.h"; sourceTree = "<group>"; };
+		4CE74F711CF206E40093AC0C /* UIView+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+YYText.m"; sourceTree = "<group>"; };
+		4CE74F721CF206E40093AC0C /* YYLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYLabel.h; sourceTree = "<group>"; };
+		4CE74F731CF206E40093AC0C /* YYLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYLabel.m; sourceTree = "<group>"; };
+		4CE74F741CF206E40093AC0C /* YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYText.h; sourceTree = "<group>"; };
+		4CE74F751CF206E40093AC0C /* YYTextArchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextArchiver.h; sourceTree = "<group>"; };
+		4CE74F761CF206E40093AC0C /* YYTextArchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextArchiver.m; sourceTree = "<group>"; };
+		4CE74F771CF206E40093AC0C /* YYTextAsyncLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAsyncLayer.h; sourceTree = "<group>"; };
+		4CE74F781CF206E40093AC0C /* YYTextAsyncLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAsyncLayer.m; sourceTree = "<group>"; };
+		4CE74F791CF206E40093AC0C /* YYTextAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAttribute.h; sourceTree = "<group>"; };
+		4CE74F7A1CF206E40093AC0C /* YYTextAttribute.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAttribute.m; sourceTree = "<group>"; };
+		4CE74F7B1CF206E40093AC0C /* YYTextContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextContainerView.h; sourceTree = "<group>"; };
+		4CE74F7C1CF206E40093AC0C /* YYTextContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextContainerView.m; sourceTree = "<group>"; };
+		4CE74F7D1CF206E40093AC0C /* YYTextDebugOption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextDebugOption.h; sourceTree = "<group>"; };
+		4CE74F7E1CF206E40093AC0C /* YYTextDebugOption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextDebugOption.m; sourceTree = "<group>"; };
+		4CE74F7F1CF206E40093AC0C /* YYTextEffectWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextEffectWindow.h; sourceTree = "<group>"; };
+		4CE74F801CF206E40093AC0C /* YYTextEffectWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextEffectWindow.m; sourceTree = "<group>"; };
+		4CE74F811CF206E40093AC0C /* YYTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextInput.h; sourceTree = "<group>"; };
+		4CE74F821CF206E40093AC0C /* YYTextInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextInput.m; sourceTree = "<group>"; };
+		4CE74F831CF206E40093AC0C /* YYTextKeyboardManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextKeyboardManager.h; sourceTree = "<group>"; };
+		4CE74F841CF206E40093AC0C /* YYTextKeyboardManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextKeyboardManager.m; sourceTree = "<group>"; };
+		4CE74F851CF206E40093AC0C /* YYTextLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextLayout.h; sourceTree = "<group>"; };
+		4CE74F861CF206E40093AC0C /* YYTextLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextLayout.m; sourceTree = "<group>"; };
+		4CE74F871CF206E40093AC0C /* YYTextLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextLine.h; sourceTree = "<group>"; };
+		4CE74F881CF206E40093AC0C /* YYTextLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextLine.m; sourceTree = "<group>"; };
+		4CE74F891CF206E40093AC0C /* YYTextMagnifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextMagnifier.h; sourceTree = "<group>"; };
+		4CE74F8A1CF206E40093AC0C /* YYTextMagnifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextMagnifier.m; sourceTree = "<group>"; };
+		4CE74F8B1CF206E40093AC0C /* YYTextParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextParser.h; sourceTree = "<group>"; };
+		4CE74F8C1CF206E40093AC0C /* YYTextParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextParser.m; sourceTree = "<group>"; };
+		4CE74F8D1CF206E40093AC0C /* YYTextRubyAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRubyAnnotation.h; sourceTree = "<group>"; };
+		4CE74F8E1CF206E40093AC0C /* YYTextRubyAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRubyAnnotation.m; sourceTree = "<group>"; };
+		4CE74F8F1CF206E40093AC0C /* YYTextRunDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRunDelegate.h; sourceTree = "<group>"; };
+		4CE74F901CF206E40093AC0C /* YYTextRunDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRunDelegate.m; sourceTree = "<group>"; };
+		4CE74F911CF206E40093AC0C /* YYTextSelectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextSelectionView.h; sourceTree = "<group>"; };
+		4CE74F921CF206E40093AC0C /* YYTextSelectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextSelectionView.m; sourceTree = "<group>"; };
+		4CE74F931CF206E40093AC0C /* YYTextTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextTransaction.h; sourceTree = "<group>"; };
+		4CE74F941CF206E40093AC0C /* YYTextTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextTransaction.m; sourceTree = "<group>"; };
+		4CE74F951CF206E40093AC0C /* YYTextUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextUtilities.h; sourceTree = "<group>"; };
+		4CE74F961CF206E40093AC0C /* YYTextUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextUtilities.m; sourceTree = "<group>"; };
+		4CE74F971CF206E40093AC0C /* YYTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextView.h; sourceTree = "<group>"; };
+		4CE74F981CF206E40093AC0C /* YYTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextView.m; sourceTree = "<group>"; };
+		4CE74F991CF206E40093AC0C /* YYTextWeakProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextWeakProxy.h; sourceTree = "<group>"; };
+		4CE74F9A1CF206E40093AC0C /* YYTextWeakProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextWeakProxy.m; sourceTree = "<group>"; };
+		4CE74FCB1CF2174E0093AC0C /* ChatTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatTableViewCell.h; sourceTree = "<group>"; };
+		4CE74FCC1CF2174E0093AC0C /* ChatTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatTableViewCell.m; sourceTree = "<group>"; };
 		4CEC3A581B8CECDF00909DCA /* IQNSArray+Sort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IQNSArray+Sort.swift"; sourceTree = "<group>"; };
 		4CEC3A591B8CECDF00909DCA /* IQUITextFieldView+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IQUITextFieldView+Additions.swift"; sourceTree = "<group>"; };
 		4CEC3A5A1B8CECDF00909DCA /* IQUIView+Hierarchy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IQUIView+Hierarchy.swift"; sourceTree = "<group>"; };
@@ -135,7 +272,6 @@
 		9D1F46981977B06C0057B4A2 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
 		9D9A0FC218C9DB5700585D3F /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
 		9D9A0FC418C9DB5F00585D3F /* Twitter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Twitter.framework; path = System/Library/Frameworks/Twitter.framework; sourceTree = SDKROOT; };
-		C0017B761BAD941200BD1D70 /* CustomSubclassView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSubclassView.swift; sourceTree = "<group>"; };
 		C0017B781BAD943400BD1D70 /* CustomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomViewController.swift; sourceTree = "<group>"; };
 		C03C87F41B8DCBF100295DFA /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
 		C03C87F61B8DCC1400295DFA /* OptionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionsViewController.swift; sourceTree = "<group>"; };
@@ -167,8 +303,6 @@
 		C0CB61A91B884D4100C33368 /* BottomBlankSpaceViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BottomBlankSpaceViewController.m; sourceTree = "<group>"; };
 		C0CB61AA1B884D4100C33368 /* CollectionViewDemoController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CollectionViewDemoController.h; sourceTree = "<group>"; };
 		C0CB61AB1B884D4100C33368 /* CollectionViewDemoController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CollectionViewDemoController.m; sourceTree = "<group>"; };
-		C0CB61AC1B884D4100C33368 /* CustomSubclassView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomSubclassView.h; sourceTree = "<group>"; };
-		C0CB61AD1B884D4100C33368 /* CustomSubclassView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSubclassView.m; sourceTree = "<group>"; };
 		C0CB61AE1B884D4100C33368 /* CustomViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomViewController.h; sourceTree = "<group>"; };
 		C0CB61AF1B884D4100C33368 /* CustomViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomViewController.m; sourceTree = "<group>"; };
 		C0CB61B01B884D4100C33368 /* ExampleTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExampleTableViewController.h; sourceTree = "<group>"; };
@@ -286,6 +420,86 @@
 			path = DemoObjCUITests;
 			sourceTree = "<group>";
 		};
+		4CE74F4E1CF185F70093AC0C /* Color Picker TextField */ = {
+			isa = PBXGroup;
+			children = (
+				4CE74F4F1CF185F70093AC0C /* ColorPickerTextField.h */,
+				4CE74F501CF185F70093AC0C /* ColorPickerTextField.m */,
+				4CE74F511CF185F70093AC0C /* HFColorPicker */,
+				4CE74F561CF185F70093AC0C /* UIColor+HexColors.h */,
+				4CE74F571CF185F70093AC0C /* UIColor+HexColors.m */,
+			);
+			path = "Color Picker TextField";
+			sourceTree = "<group>";
+		};
+		4CE74F511CF185F70093AC0C /* HFColorPicker */ = {
+			isa = PBXGroup;
+			children = (
+				4CE74F521CF185F70093AC0C /* HFColorButton.h */,
+				4CE74F531CF185F70093AC0C /* HFColorButton.m */,
+				4CE74F541CF185F70093AC0C /* HFColorPickerView.h */,
+				4CE74F551CF185F70093AC0C /* HFColorPickerView.m */,
+			);
+			path = HFColorPicker;
+			sourceTree = "<group>";
+		};
+		4CE74F691CF206E40093AC0C /* YYText */ = {
+			isa = PBXGroup;
+			children = (
+				4CE74F6A1CF206E40093AC0C /* NSAttributedString+YYText.h */,
+				4CE74F6B1CF206E40093AC0C /* NSAttributedString+YYText.m */,
+				4CE74F6C1CF206E40093AC0C /* NSParagraphStyle+YYText.h */,
+				4CE74F6D1CF206E40093AC0C /* NSParagraphStyle+YYText.m */,
+				4CE74F6E1CF206E40093AC0C /* UIPasteboard+YYText.h */,
+				4CE74F6F1CF206E40093AC0C /* UIPasteboard+YYText.m */,
+				4CE74F701CF206E40093AC0C /* UIView+YYText.h */,
+				4CE74F711CF206E40093AC0C /* UIView+YYText.m */,
+				4CE74F721CF206E40093AC0C /* YYLabel.h */,
+				4CE74F731CF206E40093AC0C /* YYLabel.m */,
+				4CE74F741CF206E40093AC0C /* YYText.h */,
+				4CE74F751CF206E40093AC0C /* YYTextArchiver.h */,
+				4CE74F761CF206E40093AC0C /* YYTextArchiver.m */,
+				4CE74F771CF206E40093AC0C /* YYTextAsyncLayer.h */,
+				4CE74F781CF206E40093AC0C /* YYTextAsyncLayer.m */,
+				4CE74F791CF206E40093AC0C /* YYTextAttribute.h */,
+				4CE74F7A1CF206E40093AC0C /* YYTextAttribute.m */,
+				4CE74F7B1CF206E40093AC0C /* YYTextContainerView.h */,
+				4CE74F7C1CF206E40093AC0C /* YYTextContainerView.m */,
+				4CE74F7D1CF206E40093AC0C /* YYTextDebugOption.h */,
+				4CE74F7E1CF206E40093AC0C /* YYTextDebugOption.m */,
+				4CE74F7F1CF206E40093AC0C /* YYTextEffectWindow.h */,
+				4CE74F801CF206E40093AC0C /* YYTextEffectWindow.m */,
+				4CE74F811CF206E40093AC0C /* YYTextInput.h */,
+				4CE74F821CF206E40093AC0C /* YYTextInput.m */,
+				4CE74F831CF206E40093AC0C /* YYTextKeyboardManager.h */,
+				4CE74F841CF206E40093AC0C /* YYTextKeyboardManager.m */,
+				4CE74F851CF206E40093AC0C /* YYTextLayout.h */,
+				4CE74F861CF206E40093AC0C /* YYTextLayout.m */,
+				4CE74F871CF206E40093AC0C /* YYTextLine.h */,
+				4CE74F881CF206E40093AC0C /* YYTextLine.m */,
+				4CE74F891CF206E40093AC0C /* YYTextMagnifier.h */,
+				4CE74F8A1CF206E40093AC0C /* YYTextMagnifier.m */,
+				4CE74F8B1CF206E40093AC0C /* YYTextParser.h */,
+				4CE74F8C1CF206E40093AC0C /* YYTextParser.m */,
+				4CE74F8D1CF206E40093AC0C /* YYTextRubyAnnotation.h */,
+				4CE74F8E1CF206E40093AC0C /* YYTextRubyAnnotation.m */,
+				4CE74F8F1CF206E40093AC0C /* YYTextRunDelegate.h */,
+				4CE74F901CF206E40093AC0C /* YYTextRunDelegate.m */,
+				4CE74F911CF206E40093AC0C /* YYTextSelectionView.h */,
+				4CE74F921CF206E40093AC0C /* YYTextSelectionView.m */,
+				4CE74F931CF206E40093AC0C /* YYTextTransaction.h */,
+				4CE74F941CF206E40093AC0C /* YYTextTransaction.m */,
+				4CE74F951CF206E40093AC0C /* YYTextUtilities.h */,
+				4CE74F961CF206E40093AC0C /* YYTextUtilities.m */,
+				4CE74F971CF206E40093AC0C /* YYTextView.h */,
+				4CE74F981CF206E40093AC0C /* YYTextView.m */,
+				4CE74F991CF206E40093AC0C /* YYTextWeakProxy.h */,
+				4CE74F9A1CF206E40093AC0C /* YYTextWeakProxy.m */,
+			);
+			name = YYText;
+			path = ../YYText;
+			sourceTree = "<group>";
+		};
 		4CEC3A561B8CECDF00909DCA /* IQKeyboardManagerSwift */ = {
 			isa = PBXGroup;
 			children = (
@@ -441,6 +655,14 @@
 				C0CB61981B884D4100C33368 /* StepperTableViewCell.m */,
 				C0CB61991B884D4100C33368 /* SwitchTableViewCell.h */,
 				C0CB619A1B884D4100C33368 /* SwitchTableViewCell.m */,
+				4CE74F601CF1864E0093AC0C /* ColorTableViewCell.h */,
+				4CE74F611CF1864E0093AC0C /* ColorTableViewCell.m */,
+				4CE74F631CF186720093AC0C /* TextFieldTableViewCell.h */,
+				4CE74F641CF186720093AC0C /* TextFieldTableViewCell.m */,
+				4CE74F661CF1BBCC0093AC0C /* ImageSwitchTableViewCell.h */,
+				4CE74F671CF1BBCC0093AC0C /* ImageSwitchTableViewCell.m */,
+				4CE74FCB1CF2174E0093AC0C /* ChatTableViewCell.h */,
+				4CE74FCC1CF2174E0093AC0C /* ChatTableViewCell.m */,
 			);
 			path = Cell;
 			sourceTree = "<group>";
@@ -465,7 +687,9 @@
 		C0CB61A31B884D4100C33368 /* Third Party */ = {
 			isa = PBXGroup;
 			children = (
+				4CE74F4E1CF185F70093AC0C /* Color Picker TextField */,
 				C0CB61A41B884D4100C33368 /* IQDropDownTextField */,
+				4CE74F691CF206E40093AC0C /* YYText */,
 			);
 			path = "Third Party";
 			sourceTree = "<group>";
@@ -487,8 +711,6 @@
 				C0CB61A91B884D4100C33368 /* BottomBlankSpaceViewController.m */,
 				C0CB61AA1B884D4100C33368 /* CollectionViewDemoController.h */,
 				C0CB61AB1B884D4100C33368 /* CollectionViewDemoController.m */,
-				C0CB61AC1B884D4100C33368 /* CustomSubclassView.h */,
-				C0CB61AD1B884D4100C33368 /* CustomSubclassView.m */,
 				C0CB61AE1B884D4100C33368 /* CustomViewController.h */,
 				C0CB61AF1B884D4100C33368 /* CustomViewController.m */,
 				C0CB61B01B884D4100C33368 /* ExampleTableViewController.h */,
@@ -513,6 +735,16 @@
 				C0CB61C81B884D4100C33368 /* ViewController.m */,
 				C0CB61C91B884D4100C33368 /* WebViewController.h */,
 				C0CB61CA1B884D4100C33368 /* WebViewController.m */,
+				4CE74F3F1CF0465C0093AC0C /* YYTextViewController.h */,
+				4CE74F401CF0465C0093AC0C /* YYTextViewController.m */,
+				4CE74F421CF047610093AC0C /* RefreshLayoutViewController.h */,
+				4CE74F431CF047610093AC0C /* RefreshLayoutViewController.m */,
+				4CE74F451CF0809C0093AC0C /* LayoutGuideViewController.h */,
+				4CE74F461CF0809C0093AC0C /* LayoutGuideViewController.m */,
+				4CE74F481CF080B60093AC0C /* ChatViewController.h */,
+				4CE74F491CF080B60093AC0C /* ChatViewController.m */,
+				4CE74F4B1CF080C10093AC0C /* TextViewController.h */,
+				4CE74F4C1CF080C10093AC0C /* TextViewController.m */,
 			);
 			path = ViewController;
 			sourceTree = "<group>";
@@ -594,7 +826,6 @@
 				C03C87F31B8DCBC900295DFA /* Settings */,
 				C0CB61DA1B884D4100C33368 /* BottomBlankSpaceViewController.swift */,
 				C0CB61DB1B884D4100C33368 /* CollectionViewDemoController.swift */,
-				C0017B761BAD941200BD1D70 /* CustomSubclassView.swift */,
 				C0017B781BAD943400BD1D70 /* CustomViewController.swift */,
 				C0CB61DC1B884D4100C33368 /* ExampleTableViewController.swift */,
 				C0CB61DD1B884D4100C33368 /* ManualToolbarViewController.swift */,
@@ -840,44 +1071,70 @@
 			files = (
 				4CEC3A721B8CECDF00909DCA /* IQKeyboardManager.swift in Sources */,
 				C03C87F71B8DCC1400295DFA /* OptionsViewController.swift in Sources */,
+				4CE74FC81CF206E40093AC0C /* YYTextView.m in Sources */,
 				C0CB61F01B884F2700C33368 /* ManualToolbarViewController.swift in Sources */,
+				4CE74FB41CF206E40093AC0C /* YYTextKeyboardManager.m in Sources */,
+				4CE74FA41CF206E40093AC0C /* YYLabel.m in Sources */,
 				C0CB61F21B884F2700C33368 /* ScrollViewController.swift in Sources */,
+				4CE74FA81CF206E40093AC0C /* YYTextAsyncLayer.m in Sources */,
 				C03C87FB1B8DCED400295DFA /* OptionTableViewCell.swift in Sources */,
 				C03C87FF1B8DCEF400295DFA /* SwitchTableViewCell.swift in Sources */,
 				4CEC3A751B8CECDF00909DCA /* IQBarButtonItem.swift in Sources */,
+				4CE74FB81CF206E40093AC0C /* YYTextLine.m in Sources */,
 				4CEC3A6E1B8CECDF00909DCA /* IQUIViewController+Additions.swift in Sources */,
+				4CE74FAC1CF206E40093AC0C /* YYTextContainerView.m in Sources */,
 				C03C87FD1B8DCEE700295DFA /* StepperTableViewCell.swift in Sources */,
 				C0CB61EF1B884F2700C33368 /* ExampleTableViewController.swift in Sources */,
+				4CE74F9C1CF206E40093AC0C /* NSAttributedString+YYText.m in Sources */,
 				C0CB61F31B884F2700C33368 /* SpecialCaseViewController.swift in Sources */,
 				4C7FCC291C6A57FF00537BA0 /* IQUIScrollView+Additions.swift in Sources */,
+				4CE74FB01CF206E40093AC0C /* YYTextEffectWindow.m in Sources */,
+				4CE74F5D1CF185F70093AC0C /* HFColorPickerView.m in Sources */,
 				4CEC3A731B8CECDF00909DCA /* IQKeyboardReturnKeyHandler.swift in Sources */,
 				4CEC3A6B1B8CECDF00909DCA /* IQNSArray+Sort.swift in Sources */,
 				4CEC3A711B8CECDF00909DCA /* IQKeyboardManagerConstantsInternal.swift in Sources */,
+				4CE74FA21CF206E40093AC0C /* UIView+YYText.m in Sources */,
 				4CEC3A6F1B8CECDF00909DCA /* IQUIWindow+Hierarchy.swift in Sources */,
 				C0CB61FC1B884F7900C33368 /* AppDelegate.swift in Sources */,
 				C0CB61ED1B884F2700C33368 /* BottomBlankSpaceViewController.swift in Sources */,
+				4CE74FB21CF206E40093AC0C /* YYTextInput.m in Sources */,
+				4CE74FA61CF206E40093AC0C /* YYTextArchiver.m in Sources */,
+				4CE74FAE1CF206E40093AC0C /* YYTextDebugOption.m in Sources */,
+				4CE74FCA1CF206E40093AC0C /* YYTextWeakProxy.m in Sources */,
 				4CEC3A761B8CECDF00909DCA /* IQTitleBarButtonItem.swift in Sources */,
 				C0CB61EE1B884F2700C33368 /* CollectionViewDemoController.swift in Sources */,
 				C0CB61F81B884F2700C33368 /* ViewController.swift in Sources */,
+				4CE74FAA1CF206E40093AC0C /* YYTextAttribute.m in Sources */,
 				4CEC3A6D1B8CECDF00909DCA /* IQUIView+Hierarchy.swift in Sources */,
+				4CE74F591CF185F70093AC0C /* ColorPickerTextField.m in Sources */,
 				4CEC3A771B8CECDF00909DCA /* IQToolbar.swift in Sources */,
+				4CE74F5F1CF185F70093AC0C /* UIColor+HexColors.m in Sources */,
 				4CEC3A781B8CECDF00909DCA /* IQUIView+IQKeyboardToolbar.swift in Sources */,
+				4CE74FB61CF206E40093AC0C /* YYTextLayout.m in Sources */,
+				4CE74FBC1CF206E40093AC0C /* YYTextParser.m in Sources */,
 				4CEC3A701B8CECDF00909DCA /* IQKeyboardManagerConstants.swift in Sources */,
 				C03C87F51B8DCBF100295DFA /* SettingsViewController.swift in Sources */,
+				4CE74F9E1CF206E40093AC0C /* NSParagraphStyle+YYText.m in Sources */,
+				4CE74F5B1CF185F70093AC0C /* HFColorButton.m in Sources */,
 				C0CB61F71B884F2700C33368 /* TextViewSpecialCaseViewController.swift in Sources */,
+				4CE74FBA1CF206E40093AC0C /* YYTextMagnifier.m in Sources */,
 				C0CB62151B884FA700C33368 /* IQDropDownTextField.m in Sources */,
+				4CE74FC21CF206E40093AC0C /* YYTextSelectionView.m in Sources */,
+				4CE74FC01CF206E40093AC0C /* YYTextRunDelegate.m in Sources */,
 				C0CB61F61B884F2700C33368 /* TextSelectionViewController.swift in Sources */,
-				C0CB62721B88541F00C33368 /* CustomSubclassView.m in Sources */,
-				C0017B771BAD941200BD1D70 /* CustomSubclassView.swift in Sources */,
+				4CE74FBE1CF206E40093AC0C /* YYTextRubyAnnotation.m in Sources */,
 				C0CB61F11B884F2700C33368 /* NavigationBarViewController.swift in Sources */,
 				C0017B791BAD943400BD1D70 /* CustomViewController.swift in Sources */,
 				4CEC3A6C1B8CECDF00909DCA /* IQUITextFieldView+Additions.swift in Sources */,
 				C0CB61F41B884F2700C33368 /* TableViewInContainerViewController.swift in Sources */,
 				4CEC3A741B8CECDF00909DCA /* IQTextView.swift in Sources */,
+				4CE74FC41CF206E40093AC0C /* YYTextTransaction.m in Sources */,
+				4CE74FA01CF206E40093AC0C /* UIPasteboard+YYText.m in Sources */,
 				4C631B181CB24AAD0078E59F /* IQPreviousNextView.swift in Sources */,
 				C0CB61F91B884F2700C33368 /* WebViewController.swift in Sources */,
 				C0CB61F51B884F2700C33368 /* TextFieldViewController.swift in Sources */,
 				C03C87F91B8DCEC400295DFA /* NavigationTableViewCell.swift in Sources */,
+				4CE74FC61CF206E40093AC0C /* YYTextUtilities.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -885,44 +1142,80 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				4CE74F9D1CF206E40093AC0C /* NSParagraphStyle+YYText.m in Sources */,
+				4CE74FC71CF206E40093AC0C /* YYTextView.m in Sources */,
+				4CE74FBF1CF206E40093AC0C /* YYTextRunDelegate.m in Sources */,
+				4CE74FBB1CF206E40093AC0C /* YYTextParser.m in Sources */,
 				C0CB62421B8850AE00C33368 /* IQKeyboardManager.m in Sources */,
 				C0CB620B1B884FA100C33368 /* ScrollViewController.m in Sources */,
+				4CE74FC51CF206E40093AC0C /* YYTextUtilities.m in Sources */,
 				C0CB62011B884FA100C33368 /* SwitchTableViewCell.m in Sources */,
 				C0CB62401B8850AE00C33368 /* IQUIViewController+Additions.m in Sources */,
 				C0CB62411B8850AE00C33368 /* IQUIWindow+Hierarchy.m in Sources */,
-				C0CB62061B884FA100C33368 /* CustomSubclassView.m in Sources */,
+				4CE74FB71CF206E40093AC0C /* YYTextLine.m in Sources */,
+				4CE74F441CF047610093AC0C /* RefreshLayoutViewController.m in Sources */,
+				4CE74FB31CF206E40093AC0C /* YYTextKeyboardManager.m in Sources */,
+				4CE74FBD1CF206E40093AC0C /* YYTextRubyAnnotation.m in Sources */,
 				C0CB620A1B884FA100C33368 /* NavigationBarViewController.m in Sources */,
 				C0CB62101B884FA100C33368 /* TextFieldViewController.m in Sources */,
+				4CE74F9F1CF206E40093AC0C /* UIPasteboard+YYText.m in Sources */,
 				C0CB620F1B884FA100C33368 /* TableViewInContainerViewController.m in Sources */,
+				4CE74F4D1CF080C10093AC0C /* TextViewController.m in Sources */,
 				C0CB620E1B884FA100C33368 /* SpecialCaseViewController.m in Sources */,
 				C0CB62031B884FA100C33368 /* IQDropDownTextField.m in Sources */,
+				4CE74F581CF185F70093AC0C /* ColorPickerTextField.m in Sources */,
+				4CE74F681CF1BBCC0093AC0C /* ImageSwitchTableViewCell.m in Sources */,
 				C0CB62091B884FA100C33368 /* ManualToolbarViewController.m in Sources */,
+				4CE74FC11CF206E40093AC0C /* YYTextSelectionView.m in Sources */,
+				4CE74F411CF0465C0093AC0C /* YYTextViewController.m in Sources */,
 				C0CB623D1B8850AE00C33368 /* IQNSArray+Sort.m in Sources */,
 				C0CB62071B884FA100C33368 /* CustomViewController.m in Sources */,
 				C0CB62471B8850AE00C33368 /* IQTitleBarButtonItem.m in Sources */,
+				4CE74F4A1CF080B60093AC0C /* ChatViewController.m in Sources */,
 				C0CB62051B884FA100C33368 /* CollectionViewDemoController.m in Sources */,
+				4CE74FC31CF206E40093AC0C /* YYTextTransaction.m in Sources */,
 				C0CB61FF1B884FA100C33368 /* OptionTableViewCell.m in Sources */,
 				C0CB620D1B884FA100C33368 /* SettingsViewController.m in Sources */,
 				4C631B161CB24A8E0078E59F /* IQPreviousNextView.m in Sources */,
+				4CE74FB11CF206E40093AC0C /* YYTextInput.m in Sources */,
+				4CE74F5A1CF185F70093AC0C /* HFColorButton.m in Sources */,
 				C0CB62461B8850AE00C33368 /* IQBarButtonItem.m in Sources */,
 				C0CB62081B884FA100C33368 /* ExampleTableViewController.m in Sources */,
+				4CE74FB91CF206E40093AC0C /* YYTextMagnifier.m in Sources */,
 				C0CB61FD1B884FA100C33368 /* AppDelegate.m in Sources */,
 				C0CB62491B8850AE00C33368 /* IQUIView+IQKeyboardToolbar.m in Sources */,
+				4CE74FAB1CF206E40093AC0C /* YYTextContainerView.m in Sources */,
 				C0CB623F1B8850AE00C33368 /* IQUIView+Hierarchy.m in Sources */,
+				4CE74FAD1CF206E40093AC0C /* YYTextDebugOption.m in Sources */,
+				4CE74F471CF0809C0093AC0C /* LayoutGuideViewController.m in Sources */,
+				4CE74FAF1CF206E40093AC0C /* YYTextEffectWindow.m in Sources */,
 				C0CB62481B8850AE00C33368 /* IQToolbar.m in Sources */,
+				4CE74F5C1CF185F70093AC0C /* HFColorPickerView.m in Sources */,
+				4CE74FC91CF206E40093AC0C /* YYTextWeakProxy.m in Sources */,
 				C0CB623E1B8850AE00C33368 /* IQUITextFieldView+Additions.m in Sources */,
 				C0CB62431B8850AE00C33368 /* IQKeyboardReturnKeyHandler.m in Sources */,
 				C0CB61FE1B884FA100C33368 /* NavigationTableViewCell.m in Sources */,
+				4CE74F9B1CF206E40093AC0C /* NSAttributedString+YYText.m in Sources */,
+				4CE74FA51CF206E40093AC0C /* YYTextArchiver.m in Sources */,
+				4CE74FCD1CF2174E0093AC0C /* ChatTableViewCell.m in Sources */,
+				4CE74FB51CF206E40093AC0C /* YYTextLayout.m in Sources */,
 				C0CB62021B884FA100C33368 /* main.m in Sources */,
 				C0CB620C1B884FA100C33368 /* OptionsViewController.m in Sources */,
 				C0CB62041B884FA100C33368 /* BottomBlankSpaceViewController.m in Sources */,
 				C0CB62141B884FA100C33368 /* WebViewController.m in Sources */,
+				4CE74FA11CF206E40093AC0C /* UIView+YYText.m in Sources */,
 				C0CB62001B884FA100C33368 /* StepperTableViewCell.m in Sources */,
 				C0CB62131B884FA100C33368 /* ViewController.m in Sources */,
+				4CE74FA71CF206E40093AC0C /* YYTextAsyncLayer.m in Sources */,
 				4C7FCC271C6A507E00537BA0 /* IQUIScrollView+Additions.m in Sources */,
 				C0CB62121B884FA100C33368 /* TextViewSpecialCaseViewController.m in Sources */,
 				C0CB62451B8850AE00C33368 /* IQTextView.m in Sources */,
+				4CE74F621CF1864E0093AC0C /* ColorTableViewCell.m in Sources */,
+				4CE74FA31CF206E40093AC0C /* YYLabel.m in Sources */,
 				C0CB62111B884FA100C33368 /* TextSelectionViewController.m in Sources */,
+				4CE74F5E1CF185F70093AC0C /* UIColor+HexColors.m in Sources */,
+				4CE74F651CF186720093AC0C /* TextFieldTableViewCell.m in Sources */,
+				4CE74FA91CF206E40093AC0C /* YYTextAttribute.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -989,10 +1282,7 @@
 				CLANG_WARN_UNREACHABLE_CODE = YES;
 				CODE_SIGN_IDENTITY = "iPhone Developer";
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				GCC_PREPROCESSOR_DEFINITIONS = (
-					"DEBUG=1",
-					"$(inherited)",
-				);
+				GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)";
 				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				INFOPLIST_FILE = Demo/Swift_Demo/Resources/Info.plist;
@@ -1120,10 +1410,7 @@
 				CODE_SIGN_IDENTITY = "iPhone Developer: Iftekhar Qurashi (RFN29V7Q6Y)";
 				GCC_PRECOMPILE_PREFIX_HEADER = YES;
 				GCC_PREFIX_HEADER = "";
-				GCC_PREPROCESSOR_DEFINITIONS = (
-					"IQKEYBOARDMANAGER_DEBUG=1",
-					"DEBUG=1",
-				);
+				GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)";
 				INFOPLIST_FILE = Demo/Objective_C_Demo/Resources/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

+ 19 - 0
Demo/Objective_C_Demo/Cell/ChatTableViewCell.h

@@ -0,0 +1,19 @@
+//
+//  ChatTableViewCell.h
+//  Demo
+//
+//  Created by IEMacBook01 on 22/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface ChatLabel : UILabel
+
+@end
+
+@interface ChatTableViewCell : UITableViewCell
+
+@property (strong, nonatomic) IBOutlet ChatLabel *chatLabel;
+
+@end

+ 34 - 0
Demo/Objective_C_Demo/Cell/ChatTableViewCell.m

@@ -0,0 +1,34 @@
+//
+//  ChatTableViewCell.m
+//  Demo
+//
+//  Created by IEMacBook01 on 22/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "ChatTableViewCell.h"
+
+@implementation ChatLabel
+
+-(CGSize)sizeThatFits:(CGSize)size
+{
+    CGSize sizeThatFits = [super sizeThatFits:size];
+    sizeThatFits.width += 10;
+    sizeThatFits.height += 10;
+    return sizeThatFits;
+}
+
+-(CGSize)intrinsicContentSize
+{
+    CGSize sizeThatFits = [super intrinsicContentSize];
+    sizeThatFits.width += 10;
+    sizeThatFits.height += 10;
+    return sizeThatFits;
+}
+
+
+@end
+
+@implementation ChatTableViewCell
+
+@end

+ 19 - 0
Demo/Objective_C_Demo/Cell/ColorTableViewCell.h

@@ -0,0 +1,19 @@
+//
+//  ColorTableViewCell.h
+//  Demo
+//
+//  Created by IEMacBook01 on 22/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import "ColorPickerTextField.h"
+
+@interface ColorTableViewCell : UITableViewCell
+
+@property (strong, nonatomic) IBOutlet UILabel *labelTitle;
+@property (strong, nonatomic) IBOutlet UILabel *labelSubtitle;
+
+@property (strong, nonatomic) IBOutlet ColorPickerTextField *colorPickerTextField;
+
+@end

+ 13 - 0
Demo/Objective_C_Demo/Cell/ColorTableViewCell.m

@@ -0,0 +1,13 @@
+//
+//  ColorTableViewCell.m
+//  Demo
+//
+//  Created by IEMacBook01 on 22/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "ColorTableViewCell.h"
+
+@implementation ColorTableViewCell
+
+@end

+ 15 - 0
Demo/Objective_C_Demo/Cell/ImageSwitchTableViewCell.h

@@ -0,0 +1,15 @@
+//
+//  ImageSwitchTableViewCell.h
+//  Demo
+//
+//  Created by IEMacBook01 on 22/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "SwitchTableViewCell.h"
+
+@interface ImageSwitchTableViewCell : SwitchTableViewCell
+
+@property(nonatomic, strong) IBOutlet UIImageView *arrowImageView;
+
+@end

+ 13 - 0
Demo/Objective_C_Demo/Cell/ImageSwitchTableViewCell.m

@@ -0,0 +1,13 @@
+//
+//  ImageSwitchTableViewCell.m
+//  Demo
+//
+//  Created by IEMacBook01 on 22/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "ImageSwitchTableViewCell.h"
+
+@implementation ImageSwitchTableViewCell
+
+@end

+ 0 - 1
Demo/Objective_C_Demo/Cell/NavigationTableViewCell.m

@@ -14,7 +14,6 @@
 - (void)awakeFromNib
 {
     [super awakeFromNib];
-    self.backgroundColor = [UIColor clearColor];
 }
 
 @end

+ 0 - 1
Demo/Objective_C_Demo/Cell/OptionTableViewCell.m

@@ -14,7 +14,6 @@
 - (void)awakeFromNib
 {
     [super awakeFromNib];
-    self.backgroundColor = [UIColor clearColor];
 }
 
 @end

+ 0 - 1
Demo/Objective_C_Demo/Cell/StepperTableViewCell.m

@@ -15,7 +15,6 @@
 - (void)awakeFromNib
 {
     [super awakeFromNib];
-    self.backgroundColor = [UIColor clearColor];
 }
 
 @end

+ 0 - 1
Demo/Objective_C_Demo/Cell/SwitchTableViewCell.m

@@ -14,7 +14,6 @@
 - (void)awakeFromNib
 {
     [super awakeFromNib];
-    self.backgroundColor = [UIColor clearColor];
 }
 
 @end

+ 18 - 0
Demo/Objective_C_Demo/Cell/TextFieldTableViewCell.h

@@ -0,0 +1,18 @@
+//
+//  TextFieldTableViewCell.h
+//  Demo
+//
+//  Created by IEMacBook01 on 22/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface TextFieldTableViewCell : UITableViewCell
+
+@property (strong, nonatomic) IBOutlet UILabel *labelTitle;
+@property (strong, nonatomic) IBOutlet UILabel *labelSubtitle;
+
+@property (strong, nonatomic) IBOutlet UITextField *textField;
+
+@end

+ 13 - 0
Demo/Objective_C_Demo/Cell/TextFieldTableViewCell.m

@@ -0,0 +1,13 @@
+//
+//  TextFieldTableViewCell.m
+//  Demo
+//
+//  Created by IEMacBook01 on 22/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "TextFieldTableViewCell.h"
+
+@implementation TextFieldTableViewCell
+
+@end

+ 1 - 1
Demo/Objective_C_Demo/Resources/Info.plist

@@ -31,7 +31,7 @@
 		<string>armv7</string>
 	</array>
 	<key>UIStatusBarStyle</key>
-	<string>UIStatusBarStyleLightContent</string>
+	<string>UIStatusBarStyleDefault</string>
 	<key>UISupportedInterfaceOrientations</key>
 	<array>
 		<string>UIInterfaceOrientationPortrait</string>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 585 - 248
Demo/Objective_C_Demo/Storyboard/Main.storyboard


+ 26 - 0
Demo/Objective_C_Demo/Third Party/Color Picker TextField/ColorPickerTextField.h

@@ -0,0 +1,26 @@
+//
+//  ColorPickerTextField.h
+//  IQKeyboard
+//
+//  Created by Iftekhar on 27/09/14.
+//  Copyright (c) 2014 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@class ColorPickerTextField;
+
+@protocol ColorPickerTextFieldDelegate <UITextFieldDelegate>
+
+@optional
+-(void)colorPickerTextField:(ColorPickerTextField*)textField selectedColorAttributes:(NSDictionary*)colorAttributes;
+
+@end
+
+@interface ColorPickerTextField : UITextField
+
+@property(nonatomic, strong) UIColor *selectedColor;
+@property(strong, nonatomic) NSDictionary *selectedColorAttributes;
+@property (weak,nonatomic)id<ColorPickerTextFieldDelegate> delegate;
+
+@end

+ 199 - 0
Demo/Objective_C_Demo/Third Party/Color Picker TextField/ColorPickerTextField.m

@@ -0,0 +1,199 @@
+//
+//  ColorPickerTextField.m
+//  IQKeyboard
+//
+//  Created by Iftekhar on 27/09/14.
+//  Copyright (c) 2014 Iftekhar. All rights reserved.
+//
+
+#import "ColorPickerTextField.h"
+#import "HFColorPickerView.h"
+#import "HFColorButton.h"
+#import "UIColor+HexColors.h"
+
+@interface ColorPickerTextField ()<HFColorPickerViewDelegate>
+
+@end
+
+@implementation ColorPickerTextField
+{
+    HFColorPickerView *colorPickerView;
+    HFColorButton *circleView;
+}
+
+@dynamic delegate;
+
+-(BOOL)canPerformAction:(SEL)action withSender:(id)sender
+{
+    return NO;
+//    if (@selector(cut:) == action || @selector(copy:) == action || @selector(paste:) == action || @selector(select:) == action || @selector(selectAll:) == action)
+//    {
+//        return NO;
+//    }
+//    else
+//    {
+//        return YES;
+//    }
+}
+
+
++(NSArray *)colorAttributes
+{
+    return @[@{@"name":@"No Color",
+               @"color":[UIColor clearColor]},
+             
+             @{@"name":@"Black",
+               @"color":[UIColor colorWithRed:0 green:0 blue:0 alpha:1]},
+             
+             @{@"name":@"Dark Gray",
+               @"color":[UIColor colorWithRed:0.333 green:0.333 blue:0.333 alpha:1]},
+             
+             @{@"name":@"Gray",
+               @"color":[UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1]},
+             
+             @{@"name":@"White",
+               @"color":[UIColor colorWithRed:1 green:1 blue:1 alpha:1.0]},
+             
+             
+             
+             @{@"name":@"Brown",
+               @"color":[UIColor colorWithRed:121.0/255.0f green:85.0/255.0f blue:72.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Red",
+               @"color":[UIColor colorWithRed:244.0/255.0f green:67.0/255.0f blue:54.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Deep Orange",
+               @"color":[UIColor colorWithRed:255.0/255.0 green:87.0/255.0 blue:34.0/255.0 alpha:1.0]},
+             
+             @{@"name":@"Orange",
+               @"color":[UIColor colorWithRed:255.0/255.0 green:152.0/255.0 blue:0.0/255.0 alpha:1.0]},
+             
+             @{@"name":@"Amber",
+               @"color":[UIColor colorWithRed:255.0/255.0 green:193.0/255.0 blue:7.0/255.0 alpha:1.0]},
+             
+
+             
+             @{@"name":@"Teal",
+               @"color":[UIColor colorWithRed:0.0/255.0f green:150.0/255.0f blue:136.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Green",
+               @"color":[UIColor colorWithRed:76.0/255.0f green:175.0/255.0f blue:80.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Light Green",
+               @"color":[UIColor colorWithRed:139.0/255.0f green:195.0/255.0f blue:74.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Lime",
+               @"color":[UIColor colorWithRed:205.0/255.0f green:220.0/255.0f blue:57.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Yellow",
+               @"color":[UIColor colorWithRed:255.0/255.0f green:235.0/255.0f blue:59.0/255.0f alpha:1.0f]},
+             
+             
+             
+             @{@"name":@"Indigo",
+               @"color":[UIColor colorWithRed:63.0/255.0f green:81.0/255.0f blue:181.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Blue Gray",
+               @"color":[UIColor colorWithRed:96.0/255.0f green:125.0/255.0f blue:139.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Blue",
+               @"color":[UIColor colorWithRed:33.0/255.0f green:150.0/255.0f blue:243.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Light Blue",
+               @"color":[UIColor colorWithRed:3.0/255.0f green:169.0/255.0f blue:244.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Cyan",
+               @"color":[UIColor colorWithRed:0.0/255.0f green:188.0/255.0f blue:212.0/255.0f alpha:1.0f]},
+             
+
+             
+             @{@"name":@"Deep Purple",
+               @"color":[UIColor colorWithRed:103.0/255.0f green:58.0/255.0f blue:183.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Purple",
+               @"color":[UIColor colorWithRed:156.0/255.0f green:39.0/255.0f blue:176.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Pink",
+               @"color":[UIColor colorWithRed:233.0/255.0f green:30.0/255.0f blue:99.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Tomato",
+               @"color":[UIColor  colorWithRed:255.0/255.0f green:99.0/255.0f blue:71.0/255.0f alpha:1.0f]},
+             
+             @{@"name":@"Wheat",
+               @"color":[UIColor colorWithRed:255.0/255.0f green:222.0/255.0f blue:179.0/255.0f alpha:1.0f]},
+             ];
+}
+
+-(void)commonInit
+{
+    colorPickerView = [[HFColorPickerView alloc] initWithFrame:CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, 180)];
+    colorPickerView.layer.shadowColor = [UIColor blackColor].CGColor;
+    colorPickerView.layer.shadowOffset = CGSizeMake(0, 1);
+    colorPickerView.layer.shadowRadius = 2;
+    colorPickerView.layer.shadowOpacity = 0.3;
+    colorPickerView.backgroundColor = [UIColor clearColor];
+    colorPickerView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
+    colorPickerView.delegate = self;
+    colorPickerView.colors = [[[self class] colorAttributes] valueForKey:@"color"];
+    self.inputView = colorPickerView;
+   
+    circleView = [[HFColorButton alloc] initWithFrame:CGRectMake(0,0, 25, 25)];
+    circleView.selected = YES;
+    circleView.userInteractionEnabled = NO;
+    self.rightView = circleView;
+    self.rightViewMode = UITextFieldViewModeAlways;
+    self.tintColor = [UIColor clearColor];
+    
+    self.selectedColor = [UIColor clearColor];
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (self) {
+        [self commonInit];
+    }
+    return self;
+}
+
+-(void)awakeFromNib
+{
+    [super awakeFromNib];
+
+    [self commonInit];
+}
+
+-(void)setSelectedColor:(UIColor *)selectedColor
+{
+    _selectedColor = selectedColor;
+
+    NSArray *colorArray = [[self class] colorAttributes];
+    for (int i =0; i<[colorArray count]; i++)
+    {
+        NSDictionary *colorAttributes = [colorArray objectAtIndex:i];
+        
+        UIColor *color = [colorAttributes objectForKey:@"color"];
+        
+        if ([[color hexValue] isEqualToString:[selectedColor hexValue]] && CGColorGetAlpha(color.CGColor) == CGColorGetAlpha(selectedColor.CGColor))
+        {
+            _selectedColorAttributes = colorAttributes;
+            self.text = [colorAttributes objectForKey:@"name"];
+            colorPickerView.selectedIndex = i;
+            circleView.color = color;
+            [circleView setNeedsDisplay];
+            break;
+        }
+    }
+}
+
+- (void)colorPicker:(HFColorPickerView*)colorPickerView selectedColor:(UIColor*)selectedColor
+{
+    self.selectedColor = selectedColor;
+
+    if ([self.delegate respondsToSelector:@selector(colorPickerTextField:selectedColorAttributes:)])
+    {
+        [self.delegate colorPickerTextField:self selectedColorAttributes:self.selectedColorAttributes];
+    }
+}
+
+@end

+ 15 - 0
Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorButton.h

@@ -0,0 +1,15 @@
+//
+//  HFColorButton.h
+//  HFColorPickerDemo
+//
+//  Created by Hendrik Frahmann on 30.04.14.
+//  Copyright (c) 2014 Hendrik Frahmann. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface HFColorButton : UIButton
+
+@property (nonatomic, strong) UIColor* color;
+
+@end

+ 73 - 0
Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorButton.m

@@ -0,0 +1,73 @@
+//
+//  HFColorButton.m
+//  HFColorPickerDemo
+//
+//  Created by Hendrik Frahmann on 30.04.14.
+//  Copyright (c) 2014 Hendrik Frahmann. All rights reserved.
+//
+
+#import "HFColorButton.h"
+
+@interface HFColorButton()
+{
+    CGContextRef context;
+}
+@end
+
+
+@implementation HFColorButton
+
+@synthesize color = _color;
+
+static inline float radians(double degrees) { return degrees * M_PI / 180; }
+
+- (void)drawRect:(CGRect)rect
+{
+    CGRect parentViewBounds = self.bounds;
+    
+    CGFloat centerX = CGRectGetWidth(parentViewBounds) / 2;
+    CGFloat centerY = CGRectGetHeight(parentViewBounds) / 2;
+    
+    CGFloat radius = self.bounds.size.width / 2;
+    
+    // Get the graphics context and clear it
+    if(context == nil)
+        context = UIGraphicsGetCurrentContext();
+    CGContextClearRect(context, rect);
+    
+    CGFloat colorRadius = radius * 0.6;
+    
+    if(self.selected)
+    {
+        colorRadius = radius * 0.7;
+        
+        CGContextSetStrokeColorWithColor(context, [[UIColor colorWithRed:1 green:1 blue:1 alpha:1.0] CGColor]);
+        CGContextSetLineWidth(context, 5);
+        CGContextAddArc(context, centerX, centerY, radius*0.85,  radians(0), radians(360), 0);
+        CGContextClosePath(context);
+        CGContextStrokePath(context);
+    }
+    
+    
+    CGContextSetShadow(context, CGSizeMake(0,0), 0);
+    
+    if (CGColorGetAlpha(_color.CGColor) != 0.0)
+    {
+        CGContextSetFillColor(context, CGColorGetComponents([_color CGColor]));
+        CGContextMoveToPoint(context, centerX, centerY);
+        CGContextAddArc(context, centerX, centerY, colorRadius,  radians(0), radians(360), 0);
+        CGContextClosePath(context);
+        CGContextFillPath(context);
+    }
+    else
+    {
+        CGContextSetStrokeColorWithColor(context, [[UIColor colorWithRed:0 green:0 blue:0 alpha:1.0] CGColor]);
+        CGContextSetLineWidth(context, 1);
+        CGContextAddArc(context, centerX, centerY, colorRadius,  radians(0), radians(360), 0);
+        CGContextClosePath(context);
+        CGContextStrokePath(context);
+    }
+}
+
+
+@end

+ 28 - 0
Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorPickerView.h

@@ -0,0 +1,28 @@
+//
+//  HFColorPickerView.h
+//  HFColorPickerDemo
+//
+//  Created by Hendrik Frahmann on 30.04.14.
+//  Copyright (c) 2014 Hendrik Frahmann. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol HFColorPickerViewDelegate;
+
+@interface HFColorPickerView : UIView
+
+@property (nonatomic, assign) IBOutlet id<HFColorPickerViewDelegate> delegate;
+@property (nonatomic, strong) NSArray* colors;
+@property (nonatomic) CGFloat buttonDiameter;
+@property (nonatomic) NSInteger selectedIndex;
+@property (nonatomic) NSUInteger numberOfColorsPerRow;
+
+@end
+
+
+@protocol HFColorPickerViewDelegate <NSObject>
+
+- (void)colorPicker:(HFColorPickerView*)colorPickerView selectedColor:(UIColor*)selectedColor;
+
+@end

+ 164 - 0
Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorPickerView.m

@@ -0,0 +1,164 @@
+//
+//  HFColorPickerView.m
+//  HFColorPickerDemo
+//
+//  Created by Hendrik Frahmann on 30.04.14.
+//  Copyright (c) 2014 Hendrik Frahmann. All rights reserved.
+//
+
+#import "HFColorPickerView.h"
+#import "HFColorButton.h"
+
+@interface HFColorPickerView()
+
+@property (nonatomic, strong) NSMutableArray* colorButtons;
+
+- (void)setupColorButtons;
+- (void)buttonClicked:(id)sender;
+- (void)selectButton:(HFColorButton*)button;
+- (void)calculateButtonFrames;
+
+@end
+
+
+@implementation HFColorPickerView
+
+@synthesize colorButtons   = _colorButtons;
+@synthesize colors         = _colors;
+@synthesize buttonDiameter = _buttonDiameter;
+@synthesize selectedIndex  = _selectedIndex;
+@synthesize numberOfColorsPerRow       = _numberOfColorsPerRow;
+
+- (void)setColors:(NSArray *)colors
+{
+    _colors = colors;
+    [self setupColorButtons];
+}
+
+- (void)setButtonDiameter:(CGFloat)buttonDiameter
+{
+    _buttonDiameter = buttonDiameter;
+    [self calculateButtonFrames];
+}
+
+- (void)setSelectedIndex:(NSInteger)selectedIndex
+{
+    if(selectedIndex >= _colorButtons.count)
+        selectedIndex = _colorButtons.count - 1;
+    
+    _selectedIndex = selectedIndex;
+    
+    HFColorButton* button = [_colorButtons objectAtIndex:selectedIndex];
+    [self selectButton:button];
+}
+
+- (CGFloat)buttonDiameter
+{
+    if(_buttonDiameter == 0.0)
+        _buttonDiameter = 40.0;
+    return _buttonDiameter;
+}
+
+-(NSUInteger)numberOfColorsPerRow
+{
+    if (_numberOfColorsPerRow == 0)
+        _numberOfColorsPerRow = 5;
+    
+    return _numberOfColorsPerRow;
+}
+
+- (NSMutableArray*)colorButtons
+{
+    if(_colorButtons == nil)
+        _colorButtons = [NSMutableArray new];
+    return _colorButtons;
+}
+
+- (void)setupColorButtons
+{
+    // remove all buttons
+    for (HFColorButton* button in self.colorButtons)
+    {
+        [button removeFromSuperview];
+    }
+    [_colorButtons removeAllObjects];
+    
+    CGFloat buttonCount = 0;
+    
+    // create new buttons
+    for (UIColor* color in _colors)
+    {
+        HFColorButton* button = [HFColorButton new];
+        [button setColor:color];
+        [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
+        [button setClipsToBounds:NO];
+        
+        if(buttonCount == 0)
+            button.selected = YES;
+        buttonCount++;
+        
+        [self addSubview:button];
+        [_colorButtons addObject:button];
+    }
+    
+    [self calculateButtonFrames];
+}
+
+-(void)layoutSubviews
+{
+    [self calculateButtonFrames];
+}
+
+- (void)calculateButtonFrames
+{
+    NSInteger buttonCount = self.colorButtons.count;
+    
+    NSInteger buttonsPerRow = self.numberOfColorsPerRow;
+    if(buttonsPerRow > buttonCount)
+        buttonsPerRow = buttonCount;
+    
+    NSInteger numberOfRows = ceil((CGFloat)buttonCount/(CGFloat)buttonsPerRow);
+
+    CGFloat buttonWidth = self.buttonDiameter;
+    CGFloat rowWidth = self.frame.size.width/buttonsPerRow;
+    CGFloat rowHeight = self.frame.size.height/numberOfRows;
+
+    CGFloat i = 0;
+    CGFloat j = 0;
+    
+    NSInteger currentIndex = 0;
+    for (HFColorButton* button in self.colorButtons)
+    {
+        button.frame = CGRectMake(0, 0, buttonWidth, buttonWidth);
+        button.center = CGPointMake(i * rowWidth + rowWidth/2,
+                                    j * rowHeight + rowHeight/2);
+        
+        currentIndex++;
+        j = currentIndex/buttonsPerRow;
+        i = currentIndex%buttonsPerRow;
+    }
+}
+
+- (void)buttonClicked:(id)sender
+{
+    NSInteger index = [_colorButtons indexOfObject:sender];
+    if(index >= 0)
+    {
+        [self selectButton:sender];
+        
+        UIColor* color = [_colors objectAtIndex:index];
+        if(_delegate != nil)
+            [_delegate colorPicker:self selectedColor:color];
+    }
+}
+
+- (void)selectButton:(HFColorButton *)button
+{
+    for (HFColorButton* button in self.colorButtons)
+    {
+        button.selected = NO;
+    }
+    button.selected = YES;
+}
+
+@end

+ 21 - 0
Demo/Objective_C_Demo/Third Party/Color Picker TextField/UIColor+HexColors.h

@@ -0,0 +1,21 @@
+//
+//  UIColor+HexColors.h
+//  KiwiHarness
+//
+//  Created by Tim Duckett on 07/09/2012.
+//  Copyright (c) 2012 Charismatic Megafauna Ltd. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface UIColor (HexColors)
+
+@property(nonatomic, readonly) NSString *hexValue;
+@property(nonatomic, readonly) CGFloat alpha;
+@property(nonatomic, readonly) CGFloat red;
+@property(nonatomic, readonly) CGFloat green;
+@property(nonatomic, readonly) CGFloat blue;
+
++(UIColor *)colorWithHexString:(NSString *)hexString;
+
+@end

+ 110 - 0
Demo/Objective_C_Demo/Third Party/Color Picker TextField/UIColor+HexColors.m

@@ -0,0 +1,110 @@
+//
+//  UIColor+HexColors.m
+//  KiwiHarness
+//
+//  Created by Tim on 07/09/2012.
+//  Copyright (c) 2012 Charismatic Megafauna Ltd. All rights reserved.
+//
+
+#import "UIColor+HexColors.h"
+
+@implementation UIColor (HexColors)
+
+-(CGFloat)alpha
+{
+    return CGColorGetAlpha(self.CGColor);
+}
+
+-(CGFloat)red
+{
+    CGFloat red;
+    
+    [self getRed:&red green:NULL blue:NULL alpha:NULL];
+    
+    return red;
+}
+
+-(CGFloat)green
+{
+    CGFloat green;
+    
+    [self getRed:NULL green:&green blue:NULL alpha:NULL];
+    
+    return green;
+}
+
+-(CGFloat)blue
+{
+    CGFloat blue;
+    
+    [self getRed:NULL green:NULL blue:&blue alpha:NULL];
+    
+    return blue;
+}
+
++(UIColor *)colorWithHexString:(NSString *)hexString {
+
+    if ([hexString length] != 6) {
+        return nil;
+    }
+    
+    // Brutal and not-very elegant test for non hex-numeric characters
+    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^a-fA-F|0-9]" options:0 error:NULL];
+    NSUInteger match = [regex numberOfMatchesInString:hexString options:NSMatchingReportCompletion range:NSMakeRange(0, [hexString length])];
+    
+    if (match != 0) {
+        return nil;
+    }
+    
+    NSRange rRange = NSMakeRange(0, 2);
+    NSString *rComponent = [hexString substringWithRange:rRange];
+    unsigned rVal = 0;
+    NSScanner *rScanner = [NSScanner scannerWithString:rComponent];
+    [rScanner scanHexInt:&rVal];
+    float rRetVal = (float)rVal / 254;
+    
+
+    NSRange gRange = NSMakeRange(2, 2);
+    NSString *gComponent = [hexString substringWithRange:gRange];
+    unsigned gVal = 0;
+    NSScanner *gScanner = [NSScanner scannerWithString:gComponent];
+    [gScanner scanHexInt:&gVal];
+    float gRetVal = (float)gVal / 254;
+
+    NSRange bRange = NSMakeRange(4, 2);
+    NSString *bComponent = [hexString substringWithRange:bRange];
+    unsigned bVal = 0;
+    NSScanner *bScanner = [NSScanner scannerWithString:bComponent];
+    [bScanner scanHexInt:&bVal];
+    float bRetVal = (float)bVal / 254;
+    
+    return [UIColor colorWithRed:rRetVal green:gRetVal blue:bRetVal alpha:1.0f];
+
+}
+
+-(NSString *)hexValue
+{
+    if (self == [UIColor whiteColor])
+    {
+        // Special case, as white doesn't fall into the RGB color space
+        return @"ffffff";
+    }
+ 
+    CGFloat red;
+    CGFloat blue;
+    CGFloat green;
+    CGFloat alpha;
+    
+    [self getRed:&red green:&green blue:&blue alpha:&alpha];
+    
+    int redDec = (int)(red * 255);
+    int greenDec = (int)(green * 255);
+    int blueDec = (int)(blue * 255);
+    
+    NSString *returnString = [NSString stringWithFormat:@"%02x%02x%02x", (unsigned int)redDec, (unsigned int)greenDec, (unsigned int)blueDec];
+
+    return returnString;
+    
+}
+
+@end

+ 27 - 0
Demo/Objective_C_Demo/ViewController/BottomBlankSpaceViewController.m

@@ -9,6 +9,10 @@
 #import "BottomBlankSpaceViewController.h"
 #import "IQKeyboardManager.h"
 
+@interface BottomBlankSpaceViewController () <UIPopoverPresentationControllerDelegate>
+
+@end
+
 @implementation BottomBlankSpaceViewController
 {
     IBOutlet UISwitch *switchPreventShowingBottomBlankSpace;
@@ -26,6 +30,29 @@
     [[IQKeyboardManager sharedManager] setPreventShowingBottomBlankSpace:switchPreventShowingBottomBlankSpace.on];
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
+
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {
     return YES;

+ 13 - 0
Demo/Objective_C_Demo/ViewController/ChatViewController.h

@@ -0,0 +1,13 @@
+//
+//  ChatViewController.h
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface ChatViewController : UIViewController
+
+@end

+ 105 - 0
Demo/Objective_C_Demo/ViewController/ChatViewController.m

@@ -0,0 +1,105 @@
+//
+//  ChatViewController.m
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "ChatViewController.h"
+#import "ChatTableViewCell.h"
+#import "UIScrollView+ScrollToBottom.h"
+
+@interface ChatViewController ()<UITableViewDataSource,UITableViewDelegate,UITextFieldDelegate>
+
+@property (strong, nonatomic) IBOutlet UITableView *tableView;
+
+@end
+
+@implementation ChatViewController
+{
+    NSMutableArray<NSString*> *texts;
+    
+    IBOutlet UIButton *buttonSend;
+    IBOutlet UITextField *inputTextField;
+}
+
+-(void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    texts = [[NSMutableArray alloc] initWithObjects:@"This is demo text chat. Enter your message and hit `Send` to add more chat.", nil];
+    
+    inputTextField.inputAccessoryView = [[UIView alloc] init];
+}
+
+-(void)viewWillAppear:(BOOL)animated
+{
+    [super viewWillAppear:animated];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldDidChange:) name:UITextFieldTextDidChangeNotification object:inputTextField];
+}
+
+-(void)viewWillDisappear:(BOOL)animated
+{
+    [super viewWillDisappear:animated];
+    
+    [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:inputTextField];
+}
+
+-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return texts.count;
+}
+
+-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    return 100;
+}
+
+-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    return UITableViewAutomaticDimension;
+}
+
+-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    ChatTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ChatTableViewCell" forIndexPath:indexPath];
+    cell.chatLabel.text = texts[indexPath.row];
+    return cell;
+}
+
+-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    [tableView deselectRowAtIndexPath:indexPath animated:YES];
+}
+
+- (IBAction)sendAction:(UIButton *)sender
+{
+    NSString *text = [inputTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+    
+    if (text.length != 0)
+    {
+        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[_tableView numberOfRowsInSection:0] inSection:0];
+
+        [texts addObject:text];
+        inputTextField.text = @"";
+        buttonSend.enabled = NO;
+        
+        [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
+        [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:YES];
+    }
+}
+
+-(void)textFieldDidBeginEditing:(UITextField *)textField
+{
+
+}
+
+-(void)textFieldDidChange:(NSNotification*)notification
+{
+    NSString *text = [inputTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+    buttonSend.enabled = text.length != 0;
+}
+
+@end

+ 23 - 1
Demo/Objective_C_Demo/ViewController/CollectionViewDemoController.m

@@ -8,7 +8,7 @@
 
 #import "CollectionViewDemoController.h"
 
-@interface CollectionViewDemoController ()<UICollectionViewDelegate>
+@interface CollectionViewDemoController ()<UICollectionViewDelegate,UIPopoverPresentationControllerDelegate>
 
 @property (strong, nonatomic) IBOutlet UICollectionView *collectionView;
 
@@ -37,6 +37,28 @@
     return cell;
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
 
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {

+ 0 - 13
Demo/Objective_C_Demo/ViewController/CustomSubclassView.h

@@ -1,13 +0,0 @@
-//
-//  CustomSubclassView.h
-//  IQKeyboardManager
-//
-//  Created by InfoEnum02 on 21/04/15.
-//  Copyright (c) 2015 Iftekhar. All rights reserved.
-//
-
-#import <UIKit/UIKit.h>
-
-@interface CustomSubclassView : UIView
-
-@end

+ 0 - 21
Demo/Objective_C_Demo/ViewController/CustomSubclassView.m

@@ -1,21 +0,0 @@
-//
-//  CustomSubclassView.m
-//  IQKeyboardManager
-//
-//  Created by InfoEnum02 on 21/04/15.
-//  Copyright (c) 2015 Iftekhar. All rights reserved.
-//
-
-#import "CustomSubclassView.h"
-
-@implementation CustomSubclassView
-
-/*
-// Only override drawRect: if you perform custom drawing.
-// An empty implementation adversely affects performance during animation.
-- (void)drawRect:(CGRect)rect {
-    // Drawing code
-}
-*/
-
-@end

+ 137 - 12
Demo/Objective_C_Demo/ViewController/CustomViewController.m

@@ -8,15 +8,28 @@
 
 #import "CustomViewController.h"
 #import "IQKeyboardManager.h"
-#import "CustomSubclassView.h"
 #import "IQKeyboardReturnKeyHandler.h"
+#import "IQPreviousNextView.h"
 
-@interface CustomViewController ()
+@interface CustomViewController ()<UIPopoverPresentationControllerDelegate>
 {
+    IBOutlet UIView *settingsView;
     IQKeyboardReturnKeyHandler *returnHandler;
+    
     IBOutlet UISwitch *switchDisableViewController;
+    IBOutlet UISwitch *switchEnableViewController;
+
     IBOutlet UISwitch *switchDisableToolbar;
-    IBOutlet UISwitch *switchConsiderPreviousNext;
+    IBOutlet UISwitch *switchEnableToolbar;
+    
+    IBOutlet UISwitch *switchDisableTouchResign;
+    IBOutlet UISwitch *switchEnableTouchResign;
+    
+    IBOutlet UISwitch *switchAllowPreviousNext;
+    
+    
+    
+    IBOutlet NSLayoutConstraint *settingsTopConstraint;
 }
 
 @end
@@ -26,28 +39,60 @@
 - (void)viewDidLoad {
     [super viewDidLoad];
     
+    settingsView.layer.shadowColor = [[UIColor blackColor] CGColor];
+    settingsView.layer.shadowOffset = CGSizeZero;
+    settingsView.layer.shadowRadius = 5.0;
+    settingsView.layer.shadowOpacity = 0.5;
+    
     returnHandler = [[IQKeyboardReturnKeyHandler alloc] initWithViewController:self];
     returnHandler.lastTextFieldReturnKeyType = UIReturnKeyDone;
     // Do any additional setup after loading the view.
 }
 
+- (IBAction)tapAction:(UITapGestureRecognizer *)sender
+{
+    if (sender.state == UIGestureRecognizerStateEnded)
+    {
+        [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction|7<<16 animations:^{
+
+            if (settingsTopConstraint.constant != 0)
+            {
+                settingsTopConstraint.constant = 0;
+            }
+            else
+            {
+                settingsTopConstraint.constant = -settingsView.frame.size.height+30;
+            }
+            
+            [self.view setNeedsLayout];
+            [self.view layoutIfNeeded];
+            
+        } completion:^(BOOL finished) {
+
+        }];
+    }
+}
+
 -(void)viewWillAppear:(BOOL)animated
 {
     [super viewWillAppear:animated];
+    
     switchDisableViewController.on = ([[[IQKeyboardManager sharedManager] disabledDistanceHandlingClasses] containsObject:[self class]]);
+    switchEnableViewController.on = ([[[IQKeyboardManager sharedManager] enabledDistanceHandlingClasses] containsObject:[self class]]);
+    
     switchDisableToolbar.on = ([[[IQKeyboardManager sharedManager] disabledToolbarClasses] containsObject:[self class]]);
-    switchConsiderPreviousNext.on = ([[[IQKeyboardManager sharedManager] toolbarPreviousNextAllowedClasses] containsObject:[self class]]);
-}
+    switchEnableToolbar.on = ([[[IQKeyboardManager sharedManager] enabledToolbarClasses] containsObject:[self class]]);
 
-- (void)didReceiveMemoryWarning {
-    [super didReceiveMemoryWarning];
-    // Dispose of any resources that can be recreated.
+    switchDisableTouchResign.on = ([[[IQKeyboardManager sharedManager] disabledTouchResignedClasses] containsObject:[self class]]);
+    switchEnableTouchResign.on = ([[[IQKeyboardManager sharedManager] enabledTouchResignedClasses] containsObject:[self class]]);
+
+    switchAllowPreviousNext.on = ([[[IQKeyboardManager sharedManager] toolbarPreviousNextAllowedClasses] containsObject:[IQPreviousNextView class]]);
 }
 
 - (IBAction)disableInViewControllerAction:(UISwitch *)sender
 {
     [self.view endEditing:YES];
-
+    
     if (sender.on)
     {
         [[[IQKeyboardManager sharedManager] disabledDistanceHandlingClasses] addObject:[self class]];
@@ -58,6 +103,20 @@
     }
 }
 
+- (IBAction)enableInViewControllerAction:(UISwitch *)sender
+{
+    [self.view endEditing:YES];
+    
+    if (sender.on)
+    {
+        [[[IQKeyboardManager sharedManager] enabledDistanceHandlingClasses] addObject:[self class]];
+    }
+    else
+    {
+        [[[IQKeyboardManager sharedManager] enabledDistanceHandlingClasses] removeObject:[self class]];
+    }
+}
+
 - (IBAction)disableToolbarAction:(UISwitch *)sender
 {
     [self.view endEditing:YES];
@@ -72,18 +131,84 @@
     }
 }
 
-- (IBAction)considerPreviousNextAction:(UISwitch *)sender
+- (IBAction)enableToolbarAction:(UISwitch *)sender
+{
+    [self.view endEditing:YES];
+    
+    if (sender.on)
+    {
+        [[[IQKeyboardManager sharedManager] enabledToolbarClasses] addObject:[self class]];
+    }
+    else
+    {
+        [[[IQKeyboardManager sharedManager] enabledToolbarClasses] removeObject:[self class]];
+    }
+}
+
+- (IBAction)disableTouchOutsideAction:(UISwitch *)sender
+{
+    [self.view endEditing:YES];
+    
+    if (sender.on)
+    {
+        [[[IQKeyboardManager sharedManager] disabledTouchResignedClasses] addObject:[self class]];
+    }
+    else
+    {
+        [[[IQKeyboardManager sharedManager] disabledTouchResignedClasses] removeObject:[self class]];
+    }
+}
+
+- (IBAction)enableTouchOutsideAction:(UISwitch *)sender
 {
     [self.view endEditing:YES];
     
     if (sender.on)
     {
-        [[[IQKeyboardManager sharedManager] toolbarPreviousNextAllowedClasses] addObject:[CustomSubclassView class]];
+        [[[IQKeyboardManager sharedManager] enabledTouchResignedClasses] addObject:[self class]];
     }
     else
     {
-        [[[IQKeyboardManager sharedManager] toolbarPreviousNextAllowedClasses] removeObject:[CustomSubclassView class]];
+        [[[IQKeyboardManager sharedManager] enabledTouchResignedClasses] removeObject:[self class]];
     }
 }
 
+- (IBAction)allowedPreviousNextAction:(UISwitch *)sender
+{
+    [self.view endEditing:YES];
+    
+    if (sender.on)
+    {
+        [[[IQKeyboardManager sharedManager] toolbarPreviousNextAllowedClasses] addObject:[IQPreviousNextView class]];
+    }
+    else
+    {
+        [[[IQKeyboardManager sharedManager] toolbarPreviousNextAllowedClasses] removeObject:[IQPreviousNextView class]];
+    }
+}
+
+
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
+
 @end

+ 24 - 1
Demo/Objective_C_Demo/ViewController/ExampleTableViewController.m

@@ -8,7 +8,7 @@
 
 #import "ExampleTableViewController.h"
 
-@interface ExampleTableViewController ()<UITableViewDataSource,UITableViewDelegate>
+@interface ExampleTableViewController ()<UITableViewDataSource,UITableViewDelegate,UIPopoverPresentationControllerDelegate>
 
 @end
 
@@ -68,4 +68,27 @@
     return cell;
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
+
 @end

+ 13 - 0
Demo/Objective_C_Demo/ViewController/LayoutGuideViewController.h

@@ -0,0 +1,13 @@
+//
+//  LayoutGuideViewController.h
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface LayoutGuideViewController : UIViewController
+
+@end

+ 13 - 0
Demo/Objective_C_Demo/ViewController/LayoutGuideViewController.m

@@ -0,0 +1,13 @@
+//
+//  LayoutGuideViewController.m
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "LayoutGuideViewController.h"
+
+@implementation LayoutGuideViewController
+
+@end

+ 49 - 15
Demo/Objective_C_Demo/ViewController/ManualToolbarViewController.m

@@ -10,7 +10,7 @@
 #import "IQUIView+IQKeyboardToolbar.h"
 
 
-@interface ManualToolbarViewController ()
+@interface ManualToolbarViewController ()<UIPopoverPresentationControllerDelegate>
 
 -(void)previousAction:(id)sender;
 -(void)nextAction:(id)sender;
@@ -34,26 +34,25 @@
 {
     [super viewDidLoad];
     
-    [textField1 addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:)];
+    [textField1 addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:) shouldShowPlaceholder:YES];
     [textField1 setEnablePrevious:NO next:YES];
     
-    [textField2 addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:)];
+    [textField2 addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:) shouldShowPlaceholder:YES];
+    [textField2 setEnablePrevious:YES next:NO];
 
-    [textView3 addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:)];
+    [textView3 addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:) shouldShowPlaceholder:YES];
 
-    [textField4 addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:)];
-    [textField4 setEnablePrevious:YES next:NO];
+    [textField4 setTitleTarget:self action:@selector(titleAction:)];
+    textField4.placeholderText = @"Saved Users";
+    
+    [textField4 addDoneOnKeyboardWithTarget:self action:@selector(doneAction:) shouldShowPlaceholder:YES];
     
     textField5.inputAccessoryView = [[UIView alloc] init];
 }
 
 -(void)previousAction:(id)sender
 {
-    if ([textField4 isFirstResponder])
-    {
-        [textField2 becomeFirstResponder];
-    }
-    else if ([textField2 isFirstResponder])
+    if ([textField2 isFirstResponder])
     {
         [textView3 becomeFirstResponder];
     }
@@ -73,10 +72,6 @@
     {
         [textField2 becomeFirstResponder];
     }
-    else if ([textField2 isFirstResponder])
-    {
-        [textField4 becomeFirstResponder];
-    }
 }
 
 -(void)doneAction:(id)sender
@@ -84,6 +79,45 @@
     [self.view endEditing:YES];
 }
 
+-(void)titleAction:(UIButton*)button
+{
+    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
+    
+    [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
+    
+    [alertController addAction:[UIAlertAction actionWithTitle:@"test@example.com" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
+        textField4.text = @"test@example.com";
+    }]];
+    
+    [alertController addAction:[UIAlertAction actionWithTitle:@"demo@example.com" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
+        textField4.text = @"demo@example.com";
+    }]];
+    
+    [self presentViewController:alertController animated:YES completion:nil];
+}
+
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
 
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {

+ 48 - 3
Demo/Objective_C_Demo/ViewController/NavigationBarViewController.m

@@ -3,27 +3,72 @@
 //  IQKeyboard
 
 #import "NavigationBarViewController.h"
+#import "IQKeyboardReturnKeyHandler.h"
+#import "IQUIView+IQKeyboardToolbar.h"
 
-@interface NavigationBarViewController ()<UITextFieldDelegate>
+@interface NavigationBarViewController ()<UITextFieldDelegate,UIPopoverPresentationControllerDelegate>
 
 @end
 
 @implementation NavigationBarViewController
 {
-    __weak IBOutlet UITextField *textField2;
+    IQKeyboardReturnKeyHandler *returnKeyHandler;
+    IBOutlet UITextField *textField2;
+    IBOutlet UITextField *textField3;
     IBOutlet UIScrollView *scrollView;
 }
 
+-(void)dealloc
+{
+    returnKeyHandler = nil;
+}
+
 - (void)viewDidLoad
 {
     [super viewDidLoad];
+    
+    textField3.placeholderText = @"This is the customised placeholder title for displaying as toolbar title";
+    
+    returnKeyHandler = [[IQKeyboardReturnKeyHandler alloc] initWithViewController:self];
+    [returnKeyHandler setLastTextFieldReturnKeyType:UIReturnKeyDone];
 }
 
 -(void)viewWillAppear:(BOOL)animated
 {
     [super viewWillAppear:animated];
+}
+
+- (IBAction)enableScrollAction:(UISwitch *)sender {
+    
+    scrollView.scrollEnabled = sender.on;
+}
+
+- (IBAction)shouldHideTitle:(UISwitch *)sender
+{
+    textField2.shouldHideTitle = !textField2.shouldHideTitle;
+}
+
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
 
-    scrollView.contentSize = self.view.bounds.size;
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
 }
 
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation

+ 13 - 0
Demo/Objective_C_Demo/ViewController/RefreshLayoutViewController.h

@@ -0,0 +1,13 @@
+//
+//  RefreshLayoutViewController.h
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface RefreshLayoutViewController : UIViewController
+
+@end

+ 38 - 0
Demo/Objective_C_Demo/ViewController/RefreshLayoutViewController.m

@@ -0,0 +1,38 @@
+//
+//  RefreshLayoutViewController.m
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "RefreshLayoutViewController.h"
+#import "IQKeyboardManager.h"
+
+@interface RefreshLayoutViewController ()
+
+@property (strong, nonatomic) IBOutlet NSLayoutConstraint *textViewHeightConstraint;
+
+@end
+
+@implementation RefreshLayoutViewController
+
+
+
+
+
+- (IBAction)stepperChanged:(UIStepper *)sender
+{
+    [UIView animateWithDuration:0.1 animations:^{
+        self.textViewHeightConstraint.constant = sender.value;
+        [self.view setNeedsLayout];
+        [self.view layoutIfNeeded];
+    }];
+}
+
+- (IBAction)reloadLayoutAction:(UIButton *)sender
+{
+    [[IQKeyboardManager sharedManager] reloadLayoutIfNeeded];
+}
+
+@end

+ 26 - 0
Demo/Objective_C_Demo/ViewController/ScrollViewController.m

@@ -4,6 +4,10 @@
 
 #import "ScrollViewController.h"
 
+@interface ScrollViewController ()<UIPopoverPresentationControllerDelegate>
+
+@end
+
 @implementation ScrollViewController
 
 #pragma mark - View lifecycle
@@ -45,6 +49,28 @@
     return cell;
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
 
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {

+ 108 - 50
Demo/Objective_C_Demo/ViewController/Settings/SettingsViewController.m

@@ -14,9 +14,11 @@
 #import "SwitchTableViewCell.h"
 #import "StepperTableViewCell.h"
 #import "NavigationTableViewCell.h"
+#import "ColorTableViewCell.h"
+#import "TextFieldTableViewCell.h"
+#import "ImageSwitchTableViewCell.h"
 
-
-@interface SettingsViewController ()<OptionsViewControllerDelegate>
+@interface SettingsViewController ()<OptionsViewControllerDelegate,ColorPickerTextFieldDelegate>
 
 @end
 
@@ -34,30 +36,27 @@
     [super viewDidLoad];
     
     sectionTitles = @[@"UIKeyboard handling",
-                     @"IQToolbar handling",
-                     @"UITextView handling",
-                     @"UIKeyboard appearance overriding",
-                     @"Resign first responder handling",
-                     @"UISound handling",
-                     @"UIAnimation handling"];
+                      @"IQToolbar handling",
+                      @"UIKeyboard appearance overriding",
+                      @"Resign first responder handling",
+                      @"UISound handling",
+                      @"IQKeyboardManager Debug"];
 
     
     keyboardManagerProperties = @[@[@"Enable", @"Keyboard Distance From TextField", @"Prevent Showing Bottom Blank Space"],
-                                 @[@"Enable AutoToolbar",@"Toolbar Manage Behaviour",@"Should Toolbar Uses TextField TintColor",@"Should Show TextField Placeholder",@"Placeholder Font"],
-                                 @[@"Can Adjust TextView"],
-                                 @[@"Override Keyboard Appearance",@"UIKeyboard Appearance"],
-                                 @[@"Should Resign On Touch Outside"],
-                                 @[@"Should Play Input Clicks"],
-                                 @[@"Should Adopt Default Keyboard Animation"]];
+                                  @[@"Enable AutoToolbar",@"Toolbar Manage Behaviour",@"Should Toolbar Uses TextField TintColor",@"Should Show TextField Placeholder",@"Placeholder Font",@"Toolbar Tint Color",@"Toolbar Done BarButtonItem Image",@"Toolbar Done Button Text"],
+                                  @[@"Override Keyboard Appearance",@"UIKeyboard Appearance"],
+                                  @[@"Should Resign On Touch Outside"],
+                                  @[@"Should Play Input Clicks"],
+                                  @[@"Debugging logs in Console"]];
 
     
     keyboardManagerPropertyDetails = @[@[@"Enable/Disable IQKeyboardManager",@"Set keyboard distance from textField",@"Prevent to show blank space between UIKeyboard and View"],
-                                       @[@"Automatic add the IQToolbar on UIKeyboard",@"AutoToolbar previous/next button managing behaviour",@"Uses textField's tintColor property for IQToolbar",@"Add the textField's placeholder text on IQToolbar",@"UIFont for IQToolbar placeholder text"],
-                                       @[@"Adjust textView's frame when it is too big in height"],
+                                       @[@"Automatic add the IQToolbar on UIKeyboard",@"AutoToolbar previous/next button managing behaviour",@"Uses textField's tintColor property for IQToolbar",@"Add the textField's placeholder text on IQToolbar",@"UIFont for IQToolbar placeholder text",@"Override toolbar tintColor property",@"Replace toolbar done button text with provided image",@"Override toolbar done button text"],
                                        @[@"Override the keyboardAppearance for all UITextField/UITextView",@"All the UITextField keyboardAppearance is set using this property"],
-                                      @[@"Resigns Keyboard on touching outside of UITextField/View"],
+                                       @[@"Resigns Keyboard on touching outside of UITextField/View"],
                                        @[@"Plays inputClick sound on next/previous/done click"],
-                                       @[@"Uses keyboard default animation curve style to move view"]];
+                                       @[@"Setting enableDebugging to YES/No to turn on/off debugging mode"]];
 }
 
 - (IBAction)doneAction:(UIBarButtonItem *)sender
@@ -104,24 +103,32 @@
 - (void)shouldShowTextFieldPlaceholder:(UISwitch *)sender
 {
     [[IQKeyboardManager sharedManager] setShouldShowTextFieldPlaceholder:sender.on];
-
+    
     [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
 }
 
-/**  UITextView handling    */
-
-- (void)canAdjustTextViewAction:(UISwitch *)sender
+- (void)toolbarDoneBarButtonItemImage:(UISwitch *)sender
 {
-    [[IQKeyboardManager sharedManager] setCanAdjustTextView:sender.on];
+    if (sender.on)
+    {
+        [[IQKeyboardManager sharedManager] setToolbarDoneBarButtonItemImage:[UIImage imageNamed:@"IQButtonBarArrowDown"]];
+    }
+    else
+    {
+        [[IQKeyboardManager sharedManager] setToolbarDoneBarButtonItemImage:nil];
+    }
+    
+    [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
 }
 
+
 /**  "Keyboard appearance overriding    */
 
 - (void)overrideKeyboardAppearanceAction:(UISwitch *)sender
 {
     [[IQKeyboardManager sharedManager] setOverrideKeyboardAppearance:sender.on];
 
-    [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:3] withRowAnimation:UITableViewRowAnimationFade];
+    [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:2] withRowAnimation:UITableViewRowAnimationFade];
 }
 
 /**  Resign first responder handling    */
@@ -138,16 +145,26 @@
     [[IQKeyboardManager sharedManager] setShouldPlayInputClicks:sender.on];
 }
 
-/**  Animation handling     */
+/**  Debugging         */
 
-- (void)shouldAdoptDefaultKeyboardAnimation:(UISwitch *)sender
+- (void)enableDebugging:(UISwitch *)sender
 {
-    [[IQKeyboardManager sharedManager] setShouldAdoptDefaultKeyboardAnimation:sender.on];
+    [[IQKeyboardManager sharedManager] setEnableDebugging:sender.on];
 }
 
 
 #pragma mark - Table view data source
 
+-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    return 80;
+}
+
+-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    return UITableViewAutomaticDimension;
+}
+
 -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
 {
     return sectionTitles.count;
@@ -178,15 +195,14 @@
             }
         }
             break;
-        case 3:
+        case 2:
         {
             return ([[IQKeyboardManager sharedManager] overrideKeyboardAppearance] == NO)  ?  1:  [keyboardManagerProperties[section] count];
         }
             break;
-        case 2:
+        case 3:
         case 4:
         case 5:
-        case 6:
             return [keyboardManagerProperties[section] count];
             break;
 
@@ -306,29 +322,45 @@
                     return cell;
                 }
                     break;
-            }
-        }
-            break;
-        case 2:
-        {
-            switch (indexPath.row)
-            {
-                case 0:
+                case 5:
                 {
-                    SwitchTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([SwitchTableViewCell class])];
-                    cell.switchEnable.enabled = YES;
+                    ColorTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([ColorTableViewCell class])];
                     cell.labelTitle.text = keyboardManagerProperties[indexPath.section][indexPath.row];
                     cell.labelSubtitle.text = keyboardManagerPropertyDetails[indexPath.section][indexPath.row];
-                    cell.switchEnable.on = [[IQKeyboardManager sharedManager] canAdjustTextView];
+                    cell.colorPickerTextField.selectedColor = [[IQKeyboardManager sharedManager] toolbarTintColor];
+                    cell.colorPickerTextField.tag = 15;
+                    cell.colorPickerTextField.delegate = self;
+                    return cell;
+                }
+                    break;
+                case 6:
+                {
+                    ImageSwitchTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([ImageSwitchTableViewCell class])];
+                    cell.labelTitle.text = keyboardManagerProperties[indexPath.section][indexPath.row];
+                    cell.labelSubtitle.text = keyboardManagerPropertyDetails[indexPath.section][indexPath.row];
+                    cell.arrowImageView.image = [[IQKeyboardManager sharedManager] toolbarDoneBarButtonItemImage];
+                    cell.switchEnable.on = [[IQKeyboardManager sharedManager] toolbarDoneBarButtonItemImage] != nil;
                     [cell.switchEnable removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
-                    [cell.switchEnable addTarget:self action:@selector(canAdjustTextViewAction:) forControlEvents:UIControlEventValueChanged];
+                    [cell.switchEnable addTarget:self action:@selector(toolbarDoneBarButtonItemImage:) forControlEvents:UIControlEventValueChanged];
+
+                    return cell;
+                }
+                    break;
+                case 7:
+                {
+                    TextFieldTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([TextFieldTableViewCell class])];
+                    cell.labelTitle.text = keyboardManagerProperties[indexPath.section][indexPath.row];
+                    cell.labelSubtitle.text = keyboardManagerPropertyDetails[indexPath.section][indexPath.row];
+                    cell.textField.text = [[IQKeyboardManager sharedManager] toolbarDoneBarButtonItemText];
+                    cell.textField.tag = 17;
+                    cell.textField.delegate = self;
                     return cell;
                 }
                     break;
             }
         }
             break;
-        case 3:
+        case 2:
         {
             switch (indexPath.row)
             {
@@ -355,7 +387,7 @@
             }
         }
             break;
-        case 4:
+        case 3:
         {
             switch (indexPath.row)
             {
@@ -374,7 +406,7 @@
             }
         }
             break;
-        case 5:
+        case 4:
         {
             switch (indexPath.row)
             {
@@ -393,7 +425,7 @@
             }
         }
             break;
-        case 6:
+        case 5:
         {
             switch (indexPath.row)
             {
@@ -403,15 +435,16 @@
                     cell.switchEnable.enabled = YES;
                     cell.labelTitle.text = keyboardManagerProperties[indexPath.section][indexPath.row];
                     cell.labelSubtitle.text = keyboardManagerPropertyDetails[indexPath.section][indexPath.row];
-                    cell.switchEnable.on = [[IQKeyboardManager sharedManager] shouldAdoptDefaultKeyboardAnimation];
+                    cell.switchEnable.on = [[IQKeyboardManager sharedManager] enableDebugging];
                     [cell.switchEnable removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
-                    [cell.switchEnable addTarget:self action:@selector(shouldAdoptDefaultKeyboardAnimation:) forControlEvents:UIControlEventValueChanged];
+                    [cell.switchEnable addTarget:self action:@selector(enableDebugging:) forControlEvents:UIControlEventValueChanged];
                     return cell;
                 }
                     break;
             }
         }
             break;
+            
     }
     
     return nil;
@@ -422,6 +455,31 @@
     [tableView deselectRowAtIndexPath:indexPath animated:YES];
 }
 
+-(void)colorPickerTextField:(ColorPickerTextField*)textField selectedColorAttributes:(NSDictionary*)colorAttributes
+{
+    if (textField.tag == 15)
+    {
+        UIColor *color = colorAttributes[@"color"];
+        
+        if ([color isEqual:[UIColor clearColor]])
+        {
+            [[IQKeyboardManager sharedManager] setToolbarTintColor:nil];
+        }
+        else
+        {
+            [[IQKeyboardManager sharedManager] setToolbarTintColor:colorAttributes[@"color"]];
+        }
+    }
+}
+
+-(void)textFieldDidEndEditing:(UITextField *)textField
+{
+    if (textField.tag == 17)
+    {
+        [[IQKeyboardManager sharedManager] setToolbarDoneBarButtonItemText:[textField.text length]?textField.text:nil];
+    }
+}
+
 -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
 {
     if ([segue.identifier isEqualToString:NSStringFromClass([OptionsViewController class])])
@@ -454,7 +512,7 @@
                 controller.selectedIndex = [fonts indexOfObject:placeholderFont];
             }
         }
-        else if (selectedIndexPathForOptions.section == 3 && selectedIndexPathForOptions.row == 1)
+        else if (selectedIndexPathForOptions.section == 2 && selectedIndexPathForOptions.row == 1)
         {
             controller.title = @"Keyboard Appearance";
             controller.options = @[@"UIKeyboardAppearance Default",@"UIKeyboardAppearance Dark",@"UIKeyboardAppearance Light"];
@@ -475,7 +533,7 @@
         
         [[IQKeyboardManager sharedManager] setPlaceholderFont:fonts[index]];
     }
-    else if (selectedIndexPathForOptions.section == 3 && selectedIndexPathForOptions.row == 1)
+    else if (selectedIndexPathForOptions.section == 2 && selectedIndexPathForOptions.row == 1)
     {
         [[IQKeyboardManager sharedManager] setKeyboardAppearance:index];
     }

+ 24 - 1
Demo/Objective_C_Demo/ViewController/SpecialCaseViewController.m

@@ -7,7 +7,7 @@
 #import "IQKeyboardManager.h"
 #import "IQUIView+IQKeyboardToolbar.h"
 
-@interface SpecialCaseViewController ()<UISearchBarDelegate,UITextFieldDelegate,UITextViewDelegate,UIGestureRecognizerDelegate>
+@interface SpecialCaseViewController ()<UISearchBarDelegate,UITextFieldDelegate,UITextViewDelegate,UIGestureRecognizerDelegate,UIPopoverPresentationControllerDelegate>
 
 -(void)updateUI;
 
@@ -157,6 +157,29 @@
 {
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
+
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {
     return YES;

+ 26 - 0
Demo/Objective_C_Demo/ViewController/TableViewInContainerViewController.m

@@ -8,6 +8,10 @@
 
 #import "TableViewInContainerViewController.h"
 
+@interface TableViewInContainerViewController ()<UIPopoverPresentationControllerDelegate>
+
+@end
+
 @implementation TableViewInContainerViewController
 
 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
@@ -43,6 +47,28 @@
     return cell;
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
 
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {

+ 0 - 3
Demo/Objective_C_Demo/ViewController/TextFieldViewController.h

@@ -8,11 +8,8 @@
 {    
     IBOutlet UIButton *buttonPush;
     IBOutlet UIButton *buttonPresent;    
-    IBOutlet UIBarButtonItem *barButtonDisable;
 }
 
 - (IBAction)presentClicked:(id)sender;
 
--(IBAction)disableKeyboardManager:(UIBarButtonItem*)barButton;
-
 @end

+ 32 - 66
Demo/Objective_C_Demo/ViewController/TextFieldViewController.m

@@ -4,67 +4,46 @@
 
 #import "TextFieldViewController.h"
 #import "IQKeyboardManager.h"
-#import "IQKeyboardReturnKeyHandler.h"
 #import "IQDropDownTextField.h"
 #import "IQUIView+IQKeyboardToolbar.h"
+#import "IQUITextFieldView+Additions.h"
 
-@interface TextFieldViewController ()
-
--(void)refreshUI;
+@interface TextFieldViewController ()<UIPopoverPresentationControllerDelegate>
 
 @end
 
 @implementation TextFieldViewController
 {
     IBOutlet UITextField *textField3;
-    IQKeyboardReturnKeyHandler *returnKeyHandler;
     IBOutlet IQDropDownTextField *dropDownTextField;
 }
 
 #pragma mark - View lifecycle
 
--(IBAction)disableKeyboardManager:(UIBarButtonItem*)barButton
-{
-    if ([[IQKeyboardManager sharedManager] isEnabled])
-    {
-        [[IQKeyboardManager sharedManager] setEnable:NO];
-    }
-    else
-    {
-        [[IQKeyboardManager sharedManager] setEnable:YES];
-    }
-
-    [self refreshUI];
-}
-
 -(void)previousAction:(UITextField*)textField
 {
-    NSLog(@"%@ : %@",textField,NSStringFromSelector(_cmd));
+    NSLog(@"%@",NSStringFromSelector(_cmd));
 }
 
 -(void)nextAction:(UITextField*)textField
 {
-    NSLog(@"%@ : %@",textField,NSStringFromSelector(_cmd));
+    NSLog(@"%@",NSStringFromSelector(_cmd));
 }
 
 -(void)doneAction:(UITextField*)textField
 {
-    NSLog(@"%@ : %@",textField,NSStringFromSelector(_cmd));
+    NSLog(@"%@",NSStringFromSelector(_cmd));
 }
 
 - (void)viewDidLoad
 {
     [super viewDidLoad];
     
-    [textField3 setTitleTarget:self action:@selector(titleAction:)];
-    textField3.placeholderText = @"Saved Passwords";
-    
-    [dropDownTextField setCustomPreviousTarget:self action:@selector(previousAction:)];
-    [dropDownTextField setCustomNextTarget:self action:@selector(nextAction:)];
-    [dropDownTextField setCustomDoneTarget:self action:@selector(doneAction:)];
+    [textField3 setCustomPreviousTarget:self action:@selector(previousAction:)];
+    [textField3 setCustomNextTarget:self action:@selector(nextAction:)];
+    [textField3 setCustomDoneTarget:self action:@selector(doneAction:)];
     
-    returnKeyHandler = [[IQKeyboardReturnKeyHandler alloc] initWithViewController:self];
-    [returnKeyHandler setLastTextFieldReturnKeyType:UIReturnKeyDone];
+    dropDownTextField.keyboardDistanceFromTextField = 150;
     
     [dropDownTextField setItemList:@[@"Zero Line Of Code",
                                      @"No More UIScrollView",
@@ -85,23 +64,6 @@
                                      @"play sound on next/prev/done"]];
 }
 
--(void)titleAction:(UIButton*)button
-{
-    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
-    
-    [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
-    
-    [alertController addAction:[UIAlertAction actionWithTitle:@"test@example.com" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
-        textField3.text = @"test";
-    }]];
-    
-    [alertController addAction:[UIAlertAction actionWithTitle:@"demo@example.com" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
-        textField3.text = @"demo";
-    }]];
-    
-    [self presentViewController:alertController animated:YES completion:nil];
-}
-
 -(void)viewWillAppear:(BOOL)animated
 {
     [super viewWillAppear:animated];
@@ -111,25 +73,6 @@
         [buttonPush setHidden:YES];
         [buttonPresent setTitle:@"Dismiss" forState:UIControlStateNormal];
     }
-
-    [self refreshUI];
-}
-
--(void)refreshUI
-{
-    if ([[IQKeyboardManager sharedManager] isEnabled])
-    {
-        [barButtonDisable setTitle:@"Disable"];
-    }
-    else
-    {
-        [barButtonDisable setTitle:@"Enable"];
-    }
-}
-
--(void)dealloc
-{
-    returnKeyHandler = nil;
 }
 
 - (IBAction)presentClicked:(id)sender
@@ -166,6 +109,29 @@
     }
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
+
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {
     return YES;

+ 24 - 1
Demo/Objective_C_Demo/ViewController/TextSelectionViewController.m

@@ -4,7 +4,7 @@
 
 #import "TextSelectionViewController.h"
 
-@interface TextSelectionViewController ()
+@interface TextSelectionViewController ()<UIPopoverPresentationControllerDelegate>
 
 @property (nonatomic, strong) NSArray *data;
 
@@ -60,6 +60,29 @@
     return cell;
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
+
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {
     return YES;

+ 13 - 0
Demo/Objective_C_Demo/ViewController/TextViewController.h

@@ -0,0 +1,13 @@
+//
+//  TextViewController.h
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface TextViewController : UIViewController
+
+@end

+ 13 - 0
Demo/Objective_C_Demo/ViewController/TextViewController.m

@@ -0,0 +1,13 @@
+//
+//  TextViewController.m
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "TextViewController.h"
+
+@implementation TextViewController
+
+@end

+ 0 - 3
Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.h

@@ -8,11 +8,8 @@
 {
     IBOutlet UIButton *buttonPush;
     IBOutlet UIButton *buttonPresent;
-    IBOutlet UIBarButtonItem *barButtonAdjust;
 }
 
 - (IBAction)presentClicked:(id)sender;
 
--(IBAction)canAdjustTextView:(UIBarButtonItem*)barButton;
-
 @end

+ 24 - 31
Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.m

@@ -5,28 +5,12 @@
 #import "TextViewSpecialCaseViewController.h"
 #import "IQKeyboardManager.h"
 
-@interface TextViewSpecialCaseViewController ()
-
--(void)refreshUI;
+@interface TextViewSpecialCaseViewController ()<UIPopoverPresentationControllerDelegate>
 
 @end
 
 @implementation TextViewSpecialCaseViewController
 
--(IBAction)canAdjustTextView:(UIBarButtonItem*)barButton
-{
-    if ([[IQKeyboardManager sharedManager] canAdjustTextView])
-    {
-        [[IQKeyboardManager sharedManager] setCanAdjustTextView:NO];
-    }
-    else
-    {
-        [[IQKeyboardManager sharedManager] setCanAdjustTextView:YES];
-    }
-    
-    [self refreshUI];
-}
-
 #pragma mark - View lifecycle
 
 - (void)viewDidLoad
@@ -43,20 +27,6 @@
 -(void)viewWillAppear:(BOOL)animated
 {
     [super viewWillAppear:animated];
-    
-    [self refreshUI];
-}
-
--(void)refreshUI
-{
-    if ([[IQKeyboardManager sharedManager] canAdjustTextView])
-    {
-        [barButtonAdjust setTitle:@"Disable Adjust"];
-    }
-    else
-    {
-        [barButtonAdjust setTitle:@"Enable Adjust"];
-    }
 }
 
 -(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
@@ -97,6 +67,29 @@
     }
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
+
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {
     return YES;

+ 39 - 7
Demo/Objective_C_Demo/ViewController/ViewController.m

@@ -5,7 +5,7 @@
 #import "ViewController.h"
 #import "IQKeyboardManager.h"
 
-@interface ViewController ()
+@interface ViewController ()<UIPopoverPresentationControllerDelegate>
 
 @end
 
@@ -37,16 +37,48 @@
     [[IQKeyboardManager sharedManager] setToolbarManageBehaviour:IQAutoToolbarByPosition];
 }
 
--(BOOL)shouldAutorotate
+//- (nullable UIViewController *)presentationController:(UIPresentationController *)controller viewControllerForAdaptivePresentationStyle:(UIModalPresentationStyle)style
+//{
+//    if (style == UIModalPresentationOverFullScreen)
+//    {
+//        return controller.presentedViewController;
+//    }
+//    else
+//    {
+//        return nil;
+//    }
+//    NSLog(@"%@",controller.presentedViewController);
+//    NSLog(@"%@",controller.presentingViewController);
+//    return controller.presentedViewController;
+//}
+
+
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
 {
-    return NO;
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
 }
 
--(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+-(BOOL)shouldAutorotate
 {
-    UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
-    cell.backgroundColor = [UIColor clearColor];
-    return cell;
+    return NO;
 }
 
 -(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation

+ 27 - 0
Demo/Objective_C_Demo/ViewController/WebViewController.m

@@ -4,6 +4,10 @@
 
 #import "WebViewController.h"
 
+@interface WebViewController ()<UIPopoverPresentationControllerDelegate>
+
+@end
+
 @implementation WebViewController
 {
     UIActivityIndicatorView *activity;
@@ -36,6 +40,29 @@
     [activity stopAnimating];
 }
 
+-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
+{
+    if ([segue.identifier isEqualToString:@"SettingsNavigationController"])
+    {
+        segue.destinationViewController.modalPresentationStyle = UIModalPresentationPopover;
+        segue.destinationViewController.popoverPresentationController.barButtonItem = sender;
+        
+        CGFloat heightWidth = MAX(CGRectGetWidth([[UIScreen mainScreen] bounds]), CGRectGetHeight([[UIScreen mainScreen] bounds]));
+        segue.destinationViewController.preferredContentSize = CGSizeMake(heightWidth, heightWidth);
+        segue.destinationViewController.popoverPresentationController.delegate = self;
+    }
+}
+
+- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
+{
+    return UIModalPresentationNone;
+}
+
+-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
+{
+    [self.view endEditing:YES];
+}
+
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {
     return YES;

+ 13 - 0
Demo/Objective_C_Demo/ViewController/YYTextViewController.h

@@ -0,0 +1,13 @@
+//
+//  YYTextViewController.h
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface YYTextViewController : UIViewController
+
+@end

+ 42 - 0
Demo/Objective_C_Demo/ViewController/YYTextViewController.m

@@ -0,0 +1,42 @@
+//
+//  YYTextViewController.m
+//  Demo
+//
+//  Created by IEMacBook01 on 21/05/16.
+//  Copyright © 2016 Iftekhar. All rights reserved.
+//
+
+#import "YYTextViewController.h"
+#import "IQKeyboardManager.h"
+
+#import "YYTextView.h"
+
+
+@interface YYTextViewController ()<YYTextViewDelegate>
+
+@end
+
+@implementation YYTextViewController
+{
+    IBOutlet YYTextView *textView;
+}
+
++(void)initialize
+{
+    [super initialize];
+    
+    [[IQKeyboardManager sharedManager] registerTextFieldViewClass:[YYTextView class] didBeginEditingNotificationName:YYTextViewTextDidBeginEditingNotification didEndEditingNotificationName:YYTextViewTextDidEndEditingNotification];
+}
+
+-(void)viewDidLoad
+{
+    [super viewDidLoad];
+    textView.placeholderText=@"This is placeholder text of YYTextView";
+}
+
+- (void)textViewDidBeginEditing:(YYTextView *)tv
+{
+    [tv reloadInputViews];
+}
+
+@end

+ 1415 - 0
Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.h

@@ -0,0 +1,1415 @@
+//
+//  NSAttributedString+YYText.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/7.
+//  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>
+#import <CoreText/CoreText.h>
+
+#if __has_include(<YYText/YYText.h>)
+#import <YYText/YYTextAttribute.h>
+#import <YYText/YYTextRubyAnnotation.h>
+#else
+#import "YYTextAttribute.h"
+#import "YYTextRubyAnnotation.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ Get pre-defined attributes from attributed string.
+ All properties defined in UIKit, CoreText and YYText are included.
+ */
+@interface NSAttributedString (YYText)
+
+/**
+ Archive the string to data.
+ @return Returns nil if an error occurs.
+ */
+- (nullable NSData *)yy_archiveToData;
+
+/**
+ Unarchive string from data.
+ @param data  The archived attributed string data.
+ @return Returns nil if an error occurs.
+ */
++ (nullable instancetype)yy_unarchiveFromData:(NSData *)data;
+
+
+
+#pragma mark - Retrieving character attribute information
+///=============================================================================
+/// @name Retrieving character attribute information
+///=============================================================================
+
+/**
+ Returns the attributes at first charactor.
+ */
+@property (nullable, nonatomic, copy, readonly) NSDictionary<NSString *, id> *yy_attributes;
+
+/**
+ 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.
+ */
+- (nullable NSDictionary<NSString *, id> *)yy_attributesAtIndex:(NSUInteger)index;
+
+/**
+ 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.
+ */
+- (nullable id)yy_attribute:(NSString *)attributeName atIndex:(NSUInteger)index;
+
+
+#pragma mark - Get character attribute as property
+///=============================================================================
+/// @name Get character attribute as property
+///=============================================================================
+
+/**
+ 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  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) UIFont *yy_font;
+- (nullable UIFont *)yy_fontAtIndex:(NSUInteger)index;
+
+/**
+ 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.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  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) NSNumber *yy_kern;
+- (nullable NSNumber *)yy_kernAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) UIColor *yy_color;
+- (nullable UIColor *)yy_colorAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nullable, nonatomic, strong, readonly) UIColor *yy_backgroundColor;
+- (nullable UIColor *)yy_backgroundColorAtIndex:(NSUInteger)index;
+
+/**
+ The stroke width. (read-only)
+ 
+ @discussion Default value is 0.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
+ */
+@property (nullable, nonatomic, strong, readonly) NSNumber *yy_strokeWidth;
+- (nullable NSNumber *)yy_strokeWidthAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nullable, nonatomic, strong, readonly) UIColor *yy_strokeColor;
+- (nullable UIColor *)yy_strokeColorAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) NSShadow *yy_shadow;
+- (nullable NSShadow *)yy_shadowAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nonatomic, readonly) NSUnderlineStyle yy_strikethroughStyle;
+- (NSUnderlineStyle)yy_strikethroughStyleAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nullable, nonatomic, strong, readonly) UIColor *yy_strikethroughColor;
+- (nullable UIColor *)yy_strikethroughColorAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nonatomic, readonly) NSUnderlineStyle yy_underlineStyle;
+- (NSUnderlineStyle)yy_underlineStyleAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nullable, nonatomic, strong, readonly) UIColor *yy_underlineColor;
+- (nullable UIColor *)yy_underlineColorAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) NSNumber *yy_ligature;
+- (nullable NSNumber *)yy_ligatureAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nullable, nonatomic, strong, readonly) NSString *yy_textEffect;
+- (nullable NSString *)yy_textEffectAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nullable, nonatomic, strong, readonly) NSNumber *yy_obliqueness;
+- (nullable NSNumber *)yy_obliquenessAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nullable, nonatomic, strong, readonly) NSNumber *yy_expansion;
+- (nullable NSNumber *)yy_expansionAtIndex:(NSUInteger)index;
+
+/**
+ 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
+ */
+@property (nullable, nonatomic, strong, readonly) NSNumber *yy_baselineOffset;
+- (nullable NSNumber *)yy_baselineOffsetAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) BOOL yy_verticalGlyphForm;
+- (BOOL)yy_verticalGlyphFormAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:7.0
+ */
+@property (nullable, nonatomic, strong, readonly) NSString *yy_language;
+- (nullable NSString *)yy_languageAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) NSArray<NSNumber *> *yy_writingDirection;
+- (nullable NSArray<NSNumber *> *)yy_writingDirectionAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) NSParagraphStyle *yy_paragraphStyle;
+- (nullable NSParagraphStyle *)yy_paragraphStyleAtIndex:(NSUInteger)index;
+
+#pragma mark - Get paragraph attribute as property
+///=============================================================================
+/// @name 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  YYText:6.0
+ */
+@property (nonatomic, readonly) NSTextAlignment yy_alignment;
+- (NSTextAlignment)yy_alignmentAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) NSLineBreakMode yy_lineBreakMode;
+- (NSLineBreakMode)yy_lineBreakModeAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_lineSpacing;
+- (CGFloat)yy_lineSpacingAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_paragraphSpacing;
+- (CGFloat)yy_paragraphSpacingAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_paragraphSpacingBefore;
+- (CGFloat)yy_paragraphSpacingBeforeAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_firstLineHeadIndent;
+- (CGFloat)yy_firstLineHeadIndentAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_headIndent;
+- (CGFloat)yy_headIndentAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_tailIndent;
+- (CGFloat)yy_tailIndentAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_minimumLineHeight;
+- (CGFloat)yy_minimumLineHeightAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_maximumLineHeight;
+- (CGFloat)yy_maximumLineHeightAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) CGFloat yy_lineHeightMultiple;
+- (CGFloat)yy_lineHeightMultipleAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:6.0
+ */
+@property (nonatomic, readonly) NSWritingDirection yy_baseWritingDirection;
+- (NSWritingDirection)yy_baseWritingDirectionAtIndex:(NSUInteger)index;
+
+/**
+ The paragraph's threshold for hyphenation. (A wrapper for NSParagraphStyle). (read-only)
+ 
+ @discussion Valid values lie between 0.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.0, the layout manager's hyphenation factor is used instead. When both 
+ are 0.0, hyphenation is disabled.
+ @discussion Default is 0.
+ @discussion Get this property returns the first character's attribute.
+ @since UIKit:6.0
+ */
+@property (nonatomic, readonly) float yy_hyphenationFactor;
+- (float)yy_hyphenationFactorAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:7.0
+ */
+@property (nonatomic, readonly) CGFloat yy_defaultTabInterval;
+- (CGFloat)yy_defaultTabIntervalAtIndex:(NSUInteger)index;
+
+/**
+ 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  YYText:7.0
+ */
+@property (nullable, nonatomic, copy, readonly) NSArray<NSTextTab *> *yy_tabStops;
+- (nullable NSArray<NSTextTab *> *)yy_tabStopsAtIndex:(NSUInteger)index;
+
+#pragma mark - Get YYText attribute as property
+///=============================================================================
+/// @name Get YYText attribute as property
+///=============================================================================
+
+/**
+ The text shadow. (read-only)
+ 
+ @discussion Default value is nil (no shadow).
+ @discussion Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) YYTextShadow *yy_textShadow;
+- (nullable YYTextShadow *)yy_textShadowAtIndex:(NSUInteger)index;
+
+/**
+ The text inner shadow. (read-only)
+ 
+ @discussion Default value is nil (no shadow).
+ @discussion Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) YYTextShadow *yy_textInnerShadow;
+- (nullable YYTextShadow *)yy_textInnerShadowAtIndex:(NSUInteger)index;
+
+/**
+ The text underline. (read-only)
+ 
+ @discussion Default value is nil (no underline).
+ @discussion Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) YYTextDecoration *yy_textUnderline;
+- (nullable YYTextDecoration *)yy_textUnderlineAtIndex:(NSUInteger)index;
+
+/**
+ The text strikethrough. (read-only)
+ 
+ @discussion Default value is nil (no strikethrough).
+ @discussion Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) YYTextDecoration *yy_textStrikethrough;
+- (nullable YYTextDecoration *)yy_textStrikethroughAtIndex:(NSUInteger)index;
+
+/**
+ The text border. (read-only)
+ 
+ @discussion Default value is nil (no border).
+ @discussion Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) YYTextBorder *yy_textBorder;
+- (nullable YYTextBorder *)yy_textBorderAtIndex:(NSUInteger)index;
+
+/**
+ The text background border. (read-only)
+ 
+ @discussion Default value is nil (no background border).
+ @discussion Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readonly) YYTextBorder *yy_textBackgroundBorder;
+- (nullable YYTextBorder *)yy_textBackgroundBorderAtIndex:(NSUInteger)index;
+
+/**
+ The glyph transform. (read-only)
+ 
+ @discussion Default value is CGAffineTransformIdentity (no transform).
+ @discussion Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nonatomic, readonly) CGAffineTransform yy_textGlyphTransform;
+- (CGAffineTransform)yy_textGlyphTransformAtIndex:(NSUInteger)index;
+
+
+#pragma mark - Query for YYText
+///=============================================================================
+/// @name Query for YYText
+///=============================================================================
+
+/**
+ Returns the plain text from a range.
+ If there's `YYTextBackedStringAttributeName` attribute, the backed string will
+ replace the attributed string range.
+ 
+ @param range A range in receiver.
+ @return The plain text.
+ */
+- (nullable NSString *)yy_plainTextForRange:(NSRange)range;
+
+
+#pragma mark - Create attachment string for YYText
+///=============================================================================
+/// @name Create attachment string for YYText
+///=============================================================================
+
+/**
+ 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.
+ @since YYText:6.0
+ */
++ (NSMutableAttributedString *)yy_attachmentStringWithContent:(nullable id)content
+                                                  contentMode:(UIViewContentMode)contentMode
+                                                        width:(CGFloat)width
+                                                       ascent:(CGFloat)ascent
+                                                      descent:(CGFloat)descent;
+
+/**
+ 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 fontSize       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.
+ @since YYText:6.0
+ */
++ (NSMutableAttributedString *)yy_attachmentStringWithContent:(nullable id)content
+                                                  contentMode:(UIViewContentMode)contentMode
+                                               attachmentSize:(CGSize)attachmentSize
+                                                  alignToFont:(UIFont *)font
+                                                    alignment:(YYTextVerticalAlignment)alignment;
+
+/**
+ Creates and returns an attahment from a fourquare image as if it was an emoji.
+ 
+ @param image     A fourquare image.
+ @param fontSize  The font size.
+ 
+ @return An attributed string, or nil if an error occurs.
+ @since YYText:6.0
+ */
++ (nullable NSMutableAttributedString *)yy_attachmentStringWithEmojiImage:(UIImage *)image
+                                                                 fontSize:(CGFloat)fontSize;
+
+#pragma mark - Utility
+///=============================================================================
+/// @name Utility
+///=============================================================================
+
+/**
+ Returns NSMakeRange(0, self.length).
+ */
+- (NSRange)yy_rangeOfAll;
+
+/**
+ If YES, it share the same attribute in entire text range.
+ */
+- (BOOL)yy_isSharedAttributesInAllRange;
+
+/**
+ 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 YYText.
+ 
+ @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.
+ */
+- (BOOL)yy_canDrawWithUIKit;
+
+@end
+
+
+
+
+/**
+ Set pre-defined attributes to attributed string.
+ All properties defined in UIKit, CoreText and YYText are included.
+ */
+@interface NSMutableAttributedString (YYText)
+
+#pragma mark - Set character attribute
+///=============================================================================
+/// @name 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.
+ */
+- (void)yy_setAttributes:(nullable NSDictionary<NSString *, id> *)attributes;
+- (void)setYy_attributes:(nullable NSDictionary<NSString *, id> *)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.
+ */
+- (void)yy_setAttribute:(NSString *)name value:(nullable id)value;
+
+/**
+ 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.
+ */
+- (void)yy_setAttribute:(NSString *)name value:(nullable id)value range:(NSRange)range;
+
+/**
+ Removes all attributes in the specified range.
+ 
+ @param range  The range of characters.
+ */
+- (void)yy_removeAttributesInRange:(NSRange)range;
+
+
+#pragma mark - Set character attribute as property
+///=============================================================================
+/// @name Set character attribute as property
+///=============================================================================
+
+/**
+ The font of the text.
+ 
+ @discussion Default is Helvetica (Neue) 12.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:3.2  UIKit:6.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) UIFont *yy_font;
+- (void)yy_setFont:(nullable UIFont *)font range:(NSRange)range;
+
+/**
+ A kerning adjustment.
+ 
+ @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.0, no kerning will be done at all.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:3.2  UIKit:6.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSNumber *yy_kern;
+- (void)yy_setKern:(nullable NSNumber *)kern range:(NSRange)range;
+
+/**
+ The foreground color.
+ 
+ @discussion Default is Black.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:3.2  UIKit:6.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) UIColor *yy_color;
+- (void)yy_setColor:(nullable UIColor *)color range:(NSRange)range;
+
+/**
+ The background color.
+ 
+ @discussion Default is nil (or no background).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) UIColor *yy_backgroundColor;
+- (void)yy_setBackgroundColor:(nullable UIColor *)backgroundColor range:(NSRange)range;
+
+/**
+ The stroke width.
+ 
+ @discussion Default value is 0.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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:3.2  UIKit:6.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSNumber *yy_strokeWidth;
+- (void)yy_setStrokeWidth:(nullable NSNumber *)strokeWidth range:(NSRange)range;
+
+/**
+ The stroke color.
+ 
+ @discussion Default value is nil (same as foreground color).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:3.2  UIKit:6.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) UIColor *yy_strokeColor;
+- (void)yy_setStrokeColor:(nullable UIColor *)strokeColor range:(NSRange)range;
+
+/**
+ The text shadow.
+ 
+ @discussion Default value is nil (no shadow).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:6.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSShadow *yy_shadow;
+- (void)yy_setShadow:(nullable NSShadow *)shadow range:(NSRange)range;
+
+/**
+ The strikethrough style.
+ 
+ @discussion Default value is NSUnderlineStyleNone (no strikethrough).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:6.0
+ */
+@property (nonatomic, readwrite) NSUnderlineStyle yy_strikethroughStyle;
+- (void)yy_setStrikethroughStyle:(NSUnderlineStyle)strikethroughStyle range:(NSRange)range;
+
+/**
+ The strikethrough color.
+ 
+ @discussion Default value is nil (same as foreground color).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:7.0
+ */
+@property (nullable, nonatomic, strong, readwrite) UIColor *yy_strikethroughColor;
+- (void)yy_setStrikethroughColor:(nullable UIColor *)strikethroughColor range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+
+/**
+ The underline style.
+ 
+ @discussion Default value is NSUnderlineStyleNone (no underline).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:3.2  UIKit:6.0
+ */
+@property (nonatomic, readwrite) NSUnderlineStyle yy_underlineStyle;
+- (void)yy_setUnderlineStyle:(NSUnderlineStyle)underlineStyle range:(NSRange)range;
+
+/**
+ The underline color.
+ 
+ @discussion Default value is nil (same as foreground color).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:3.2  UIKit:7.0
+ */
+@property (nullable, nonatomic, strong, readwrite) UIColor *yy_underlineColor;
+- (void)yy_setUnderlineColor:(nullable UIColor *)underlineColor range:(NSRange)range;
+
+/**
+ Ligature formation control.
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:3.2  UIKit:6.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSNumber *yy_ligature;
+- (void)yy_setLigature:(nullable NSNumber *)ligature range:(NSRange)range;
+
+/**
+ The text effect.
+ 
+ @discussion Default is nil (no effect). The only currently supported value
+ is NSTextEffectLetterpressStyle.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:7.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSString *yy_textEffect;
+- (void)yy_setTextEffect:(nullable NSString *)textEffect range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+
+/**
+ The skew to be applied to glyphs. 
+ 
+ @discussion Default is 0 (no skew).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:7.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSNumber *yy_obliqueness;
+- (void)yy_setObliqueness:(nullable NSNumber *)obliqueness range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+
+/**
+ The log of the expansion factor to be applied to glyphs.
+ 
+ @discussion Default is 0 (no expansion).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:7.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSNumber *yy_expansion;
+- (void)yy_setExpansion:(nullable NSNumber *)expansion range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+
+/**
+ The character's offset from the baseline, in points. 
+ 
+ @discussion Default is 0.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:7.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSNumber *yy_baselineOffset;
+- (void)yy_setBaselineOffset:(nullable NSNumber *)baselineOffset range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+
+/**
+ Glyph orientation control.
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:4.3  YYText:6.0
+ */
+@property (nonatomic, readwrite) BOOL yy_verticalGlyphForm;
+- (void)yy_setVerticalGlyphForm:(BOOL)verticalGlyphForm range:(NSRange)range;
+
+/**
+ Specifies text language.
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:7.0  YYText:7.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSString *yy_language;
+- (void)yy_setLanguage:(nullable NSString *)language range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+
+/**
+ Specifies a bidirectional override or embedding.
+ 
+ @discussion See alse NSWritingDirection and NSWritingDirectionAttributeName.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:7.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSArray<NSNumber *> *yy_writingDirection;
+- (void)yy_setWritingDirection:(nullable NSArray<NSNumber *> *)writingDirection range:(NSRange)range;
+
+/**
+ An NSParagraphStyle object which is used to specify things like
+ line alignment, tab rulers, writing direction, etc.
+ 
+ @discussion Default is nil ([NSParagraphStyle defaultParagraphStyle]).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) NSParagraphStyle *yy_paragraphStyle;
+- (void)yy_setParagraphStyle:(nullable NSParagraphStyle *)paragraphStyle range:(NSRange)range;
+
+
+#pragma mark - Set paragraph attribute as property
+///=============================================================================
+/// @name Set paragraph attribute as property
+///=============================================================================
+
+/**
+ The text alignment (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) NSTextAlignment yy_alignment;
+- (void)yy_setAlignment:(NSTextAlignment)alignment range:(NSRange)range;
+
+/**
+ The mode that should be used to break lines (A wrapper for NSParagraphStyle).
+ 
+ @discussion This property contains the line break mode to be used laying out the paragraph's text.
+ @discussion Default is NSLineBreakByWordWrapping.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) NSLineBreakMode yy_lineBreakMode;
+- (void)yy_setLineBreakMode:(NSLineBreakMode)lineBreakMode range:(NSRange)range;
+
+/**
+ The distance in points between the bottom of one line fragment and the top of the next.
+ (A wrapper for NSParagraphStyle)
+ 
+ @discussion This value is always nonnegative. This value is included in the line
+ fragment heights in the layout manager.
+ @discussion Default is 0.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_lineSpacing;
+- (void)yy_setLineSpacing:(CGFloat)lineSpacing range:(NSRange)range;
+
+/**
+ The space after the end of the paragraph (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_paragraphSpacing;
+- (void)yy_setParagraphSpacing:(CGFloat)paragraphSpacing range:(NSRange)range;
+
+/**
+ The distance between the paragraph's top and the beginning of its text content.
+ (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_paragraphSpacingBefore;
+- (void)yy_setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore range:(NSRange)range;
+
+/**
+ The indentation of the first line (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_firstLineHeadIndent;
+- (void)yy_setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent range:(NSRange)range;
+
+/**
+ The indentation of the receiver's lines other than the first. (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_headIndent;
+- (void)yy_setHeadIndent:(CGFloat)headIndent range:(NSRange)range;
+
+/**
+ The trailing indentation (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_tailIndent;
+- (void)yy_setTailIndent:(CGFloat)tailIndent range:(NSRange)range;
+
+/**
+ The receiver's minimum height (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_minimumLineHeight;
+- (void)yy_setMinimumLineHeight:(CGFloat)minimumLineHeight range:(NSRange)range;
+
+/**
+ The receiver's maximum line height (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_maximumLineHeight;
+- (void)yy_setMaximumLineHeight:(CGFloat)maximumLineHeight range:(NSRange)range;
+
+/**
+ The line height multiple (A wrapper for NSParagraphStyle).
+ 
+ @discussion This property contains the line break mode to be used laying out the paragraph's text.
+ @discussion Default is 0 (no multiple).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_lineHeightMultiple;
+- (void)yy_setLineHeightMultiple:(CGFloat)lineHeightMultiple range:(NSRange)range;
+
+/**
+ The base writing direction (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:6.0  UIKit:6.0  YYText:6.0
+ */
+@property (nonatomic, readwrite) NSWritingDirection yy_baseWritingDirection;
+- (void)yy_setBaseWritingDirection:(NSWritingDirection)baseWritingDirection range:(NSRange)range;
+
+/**
+ The paragraph's threshold for hyphenation. (A wrapper for NSParagraphStyle).
+ 
+ @discussion Valid values lie between 0.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.0, the layout manager's hyphenation factor is used instead. When both
+ are 0.0, hyphenation is disabled.
+ @discussion Default is 0.
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since UIKit:6.0
+ */
+@property (nonatomic, readwrite) float yy_hyphenationFactor;
+- (void)yy_setHyphenationFactor:(float)hyphenationFactor range:(NSRange)range;
+
+/**
+ The document-wide default tab interval (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:7.0  UIKit:7.0  YYText:7.0
+ */
+@property (nonatomic, readwrite) CGFloat yy_defaultTabInterval;
+- (void)yy_setDefaultTabInterval:(CGFloat)defaultTabInterval range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+
+/**
+ An array of NSTextTab objects representing the receiver's tab stops.
+ (A wrapper for NSParagraphStyle).
+ 
+ @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 Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since CoreText:7.0  UIKit:7.0  YYText:7.0
+ */
+@property (nullable, nonatomic, copy, readwrite) NSArray<NSTextTab *> *yy_tabStops;
+- (void)yy_setTabStops:(nullable NSArray<NSTextTab *> *)tabStops range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+
+#pragma mark - Set YYText attribute as property
+///=============================================================================
+/// @name Set YYText attribute as property
+///=============================================================================
+
+/**
+ The text shadow.
+ 
+ @discussion Default value is nil (no shadow).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) YYTextShadow *yy_textShadow;
+- (void)yy_setTextShadow:(nullable YYTextShadow *)textShadow range:(NSRange)range;
+
+/**
+ The text inner shadow.
+ 
+ @discussion Default value is nil (no shadow).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) YYTextShadow *yy_textInnerShadow;
+- (void)yy_setTextInnerShadow:(nullable YYTextShadow *)textInnerShadow range:(NSRange)range;
+
+/**
+ The text underline.
+ 
+ @discussion Default value is nil (no underline).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) YYTextDecoration *yy_textUnderline;
+- (void)yy_setTextUnderline:(nullable YYTextDecoration *)textUnderline range:(NSRange)range;
+
+/**
+ The text strikethrough.
+ 
+ @discussion Default value is nil (no strikethrough).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) YYTextDecoration *yy_textStrikethrough;
+- (void)yy_setTextStrikethrough:(nullable YYTextDecoration *)textStrikethrough range:(NSRange)range;
+
+/**
+ The text border.
+ 
+ @discussion Default value is nil (no border).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) YYTextBorder *yy_textBorder;
+- (void)yy_setTextBorder:(nullable YYTextBorder *)textBorder range:(NSRange)range;
+
+/**
+ The text background border.
+ 
+ @discussion Default value is nil (no background border).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nullable, nonatomic, strong, readwrite) YYTextBorder *yy_textBackgroundBorder;
+- (void)yy_setTextBackgroundBorder:(nullable YYTextBorder *)textBackgroundBorder range:(NSRange)range;
+
+/**
+ The glyph transform.
+ 
+ @discussion Default value is CGAffineTransformIdentity (no transform).
+ @discussion Set this property applies to the entire text string.
+             Get this property returns the first character's attribute.
+ @since YYText:6.0
+ */
+@property (nonatomic, readwrite) CGAffineTransform yy_textGlyphTransform;
+- (void)yy_setTextGlyphTransform:(CGAffineTransform)textGlyphTransform range:(NSRange)range;
+
+
+#pragma mark - Set discontinuous attribute for range
+///=============================================================================
+/// @name Set discontinuous attribute for range
+///=============================================================================
+
+- (void)yy_setSuperscript:(nullable NSNumber *)superscript range:(NSRange)range;
+- (void)yy_setGlyphInfo:(nullable CTGlyphInfoRef)glyphInfo range:(NSRange)range;
+- (void)yy_setCharacterShape:(nullable NSNumber *)characterShape range:(NSRange)range;
+- (void)yy_setRunDelegate:(nullable CTRunDelegateRef)runDelegate range:(NSRange)range;
+- (void)yy_setBaselineClass:(nullable CFStringRef)baselineClass range:(NSRange)range;
+- (void)yy_setBaselineInfo:(nullable CFDictionaryRef)baselineInfo range:(NSRange)range;
+- (void)yy_setBaselineReferenceInfo:(nullable CFDictionaryRef)referenceInfo range:(NSRange)range;
+- (void)yy_setRubyAnnotation:(nullable CTRubyAnnotationRef)ruby range:(NSRange)range NS_AVAILABLE_IOS(8_0);
+- (void)yy_setAttachment:(nullable NSTextAttachment *)attachment range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+- (void)yy_setLink:(nullable id)link range:(NSRange)range NS_AVAILABLE_IOS(7_0);
+- (void)yy_setTextBackedString:(nullable YYTextBackedString *)textBackedString range:(NSRange)range;
+- (void)yy_setTextBinding:(nullable YYTextBinding *)textBinding range:(NSRange)range;
+- (void)yy_setTextAttachment:(nullable YYTextAttachment *)textAttachment range:(NSRange)range;
+- (void)yy_setTextHighlight:(nullable YYTextHighlight *)textHighlight range:(NSRange)range;
+- (void)yy_setTextBlockBorder:(nullable YYTextBorder *)textBlockBorder range:(NSRange)range;
+- (void)yy_setTextRubyAnnotation:(nullable YYTextRubyAnnotation *)ruby range:(NSRange)range NS_AVAILABLE_IOS(8_0);
+
+
+#pragma mark - Convenience methods for text highlight
+///=============================================================================
+/// @name 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 tapAction       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)
+ */
+- (void)yy_setTextHighlightRange:(NSRange)range
+                           color:(nullable UIColor *)color
+                 backgroundColor:(nullable UIColor *)backgroundColor
+                        userInfo:(nullable NSDictionary *)userInfo
+                       tapAction:(nullable YYTextAction)tapAction
+                 longPressAction:(nullable YYTextAction)longPressAction;
+
+/**
+ 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 tapAction       tap action when user tap the highlight (pass nil to ignore)
+ */
+- (void)yy_setTextHighlightRange:(NSRange)range
+                           color:(nullable UIColor *)color
+                 backgroundColor:(nullable UIColor *)backgroundColor
+                       tapAction:(nullable YYTextAction)tapAction;
+
+/**
+ 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)
+ */
+- (void)yy_setTextHighlightRange:(NSRange)range
+                           color:(nullable UIColor *)color
+                 backgroundColor:(nullable UIColor *)backgroundColor
+                        userInfo:(nullable NSDictionary *)userInfo;
+
+#pragma mark - Utilities
+///=============================================================================
+/// @name 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.
+ */
+- (void)yy_insertString:(NSString *)string atIndex:(NSUInteger)location;
+
+/**
+ 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.
+ */
+- (void)yy_appendString:(NSString *)string;
+
+/**
+ 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.
+ */
+- (void)yy_setClearColorToJoinedEmoji;
+
+/**
+ Removes all discontinuous attributes in a specified range.
+ See `allDiscontinuousAttributeKeys`.
+ 
+ @param range A text range.
+ */
+- (void)yy_removeDiscontinuousAttributesInRange:(NSRange)range;
+
+/**
+ 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.
+ */
++ (NSArray<NSString *> *)yy_allDiscontinuousAttributeKeys;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 1403 - 0
Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.m

@@ -0,0 +1,1403 @@
+//
+//  NSAttributedString+YYText.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/7.
+//  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 "NSAttributedString+YYText.h"
+#import "NSParagraphStyle+YYText.h"
+#import "YYTextArchiver.h"
+#import "YYTextRunDelegate.h"
+#import "YYTextUtilities.h"
+#import <CoreFoundation/CoreFoundation.h>
+
+
+// Dummy class for category
+@interface NSAttributedString_YYText : NSObject @end
+@implementation NSAttributedString_YYText @end
+
+
+static double _YYDeviceSystemVersion() {
+    static double version;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        version = [UIDevice currentDevice].systemVersion.doubleValue;
+    });
+    return version;
+}
+
+#ifndef kSystemVersion
+#define kSystemVersion _YYDeviceSystemVersion()
+#endif
+
+#ifndef kiOS6Later
+#define kiOS6Later (kSystemVersion >= 6)
+#endif
+
+#ifndef kiOS7Later
+#define kiOS7Later (kSystemVersion >= 7)
+#endif
+
+#ifndef kiOS8Later
+#define kiOS8Later (kSystemVersion >= 8)
+#endif
+
+#ifndef kiOS9Later
+#define kiOS9Later (kSystemVersion >= 9)
+#endif
+
+
+
+@implementation NSAttributedString (YYText)
+
+- (NSData *)yy_archiveToData {
+    NSData *data = nil;
+    @try {
+        data = [YYTextArchiver archivedDataWithRootObject:self];
+    }
+    @catch (NSException *exception) {
+        NSLog(@"%@",exception);
+    }
+    return data;
+}
+
++ (instancetype)yy_unarchiveFromData:(NSData *)data {
+    NSAttributedString *one = nil;
+    @try {
+        one = [YYTextUnarchiver unarchiveObjectWithData:data];
+    }
+    @catch (NSException *exception) {
+        NSLog(@"%@",exception);
+    }
+    return one;
+}
+
+- (NSDictionary *)yy_attributesAtIndex:(NSUInteger)index {
+    if (self.length > 0 && index == self.length) index--;
+    return [self attributesAtIndex:index effectiveRange:NULL];
+}
+
+- (id)yy_attribute:(NSString *)attributeName atIndex:(NSUInteger)index {
+    if (!attributeName) return nil;
+    if (self.length == 0) return nil;
+    if (self.length > 0 && index == self.length) index--;
+    return [self attribute:attributeName atIndex:index effectiveRange:NULL];
+}
+
+- (NSDictionary *)yy_attributes {
+    return [self yy_attributesAtIndex:0];
+}
+
+- (UIFont *)yy_font {
+    return [self yy_fontAtIndex:0];
+}
+
+- (UIFont *)yy_fontAtIndex:(NSUInteger)index {
+    /*
+     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.
+     */
+    UIFont *font = [self yy_attribute:NSFontAttributeName atIndex:index];
+    if (kSystemVersion <= 6) {
+        if (font) {
+            if (CFGetTypeID((__bridge CFTypeRef)(font)) == CTFontGetTypeID()) {
+                CTFontRef CTFont = (__bridge CTFontRef)(font);
+                CFStringRef name = CTFontCopyPostScriptName(CTFont);
+                CGFloat size = CTFontGetSize(CTFont);
+                if (!name) {
+                    font = nil;
+                } else {
+                    font = [UIFont fontWithName:(__bridge NSString *)(name) size:size];
+                    CFRelease(name);
+                }
+            }
+        }
+    }
+    return font;
+}
+
+- (NSNumber *)yy_kern {
+    return [self yy_kernAtIndex:0];
+}
+
+- (NSNumber *)yy_kernAtIndex:(NSUInteger)index {
+    return [self yy_attribute:NSKernAttributeName atIndex:index];
+}
+
+- (UIColor *)yy_color {
+    return [self yy_colorAtIndex:0];
+}
+
+- (UIColor *)yy_colorAtIndex:(NSUInteger)index {
+    UIColor *color = [self yy_attribute:NSForegroundColorAttributeName atIndex:index];
+    if (!color) {
+        CGColorRef ref = (__bridge CGColorRef)([self yy_attribute:(NSString *)kCTForegroundColorAttributeName atIndex:index]);
+        color = [UIColor colorWithCGColor:ref];
+    }
+    if (color && ![color isKindOfClass:[UIColor class]]) {
+        if (CFGetTypeID((__bridge CFTypeRef)(color)) == CGColorGetTypeID()) {
+            color = [UIColor colorWithCGColor:(__bridge CGColorRef)(color)];
+        } else {
+            color = nil;
+        }
+    }
+    return color;
+}
+
+- (UIColor *)yy_backgroundColor {
+    return [self yy_backgroundColorAtIndex:0];
+}
+
+- (UIColor *)yy_backgroundColorAtIndex:(NSUInteger)index {
+    return [self yy_attribute:NSBackgroundColorAttributeName atIndex:index];
+}
+
+- (NSNumber *)yy_strokeWidth {
+    return [self yy_strokeWidthAtIndex:0];
+}
+
+- (NSNumber *)yy_strokeWidthAtIndex:(NSUInteger)index {
+    return [self yy_attribute:NSStrokeWidthAttributeName atIndex:index];
+}
+
+- (UIColor *)yy_strokeColor {
+    return [self yy_strokeColorAtIndex:0];
+}
+
+- (UIColor *)yy_strokeColorAtIndex:(NSUInteger)index {
+    UIColor *color = [self yy_attribute:NSStrokeColorAttributeName atIndex:index];
+    if (!color) {
+        CGColorRef ref = (__bridge CGColorRef)([self yy_attribute:(NSString *)kCTStrokeColorAttributeName atIndex:index]);
+        color = [UIColor colorWithCGColor:ref];
+    }
+    return color;
+}
+
+- (NSShadow *)yy_shadow {
+    return [self yy_shadowAtIndex:0];
+}
+
+- (NSShadow *)yy_shadowAtIndex:(NSUInteger)index {
+    return [self yy_attribute:NSShadowAttributeName atIndex:index];
+}
+
+- (NSUnderlineStyle)yy_strikethroughStyle {
+    return [self yy_strikethroughStyleAtIndex:0];
+}
+
+- (NSUnderlineStyle)yy_strikethroughStyleAtIndex:(NSUInteger)index {
+    NSNumber *style = [self yy_attribute:NSStrikethroughStyleAttributeName atIndex:index];
+    return style.integerValue;
+}
+
+- (UIColor *)yy_strikethroughColor {
+    return [self yy_strikethroughColorAtIndex:0];
+}
+
+- (UIColor *)yy_strikethroughColorAtIndex:(NSUInteger)index {
+    if (kSystemVersion >= 7) {
+        return [self yy_attribute:NSStrikethroughColorAttributeName atIndex:index];
+    }
+    return nil;
+}
+
+- (NSUnderlineStyle)yy_underlineStyle {
+    return [self yy_underlineStyleAtIndex:0];
+}
+
+- (NSUnderlineStyle)yy_underlineStyleAtIndex:(NSUInteger)index {
+    NSNumber *style = [self yy_attribute:NSUnderlineStyleAttributeName atIndex:index];
+    return style.integerValue;
+}
+
+- (UIColor *)yy_underlineColor {
+    return [self yy_underlineColorAtIndex:0];
+}
+
+- (UIColor *)yy_underlineColorAtIndex:(NSUInteger)index {
+    UIColor *color = nil;
+    if (kSystemVersion >= 7) {
+        color = [self yy_attribute:NSUnderlineColorAttributeName atIndex:index];
+    }
+    if (!color) {
+        CGColorRef ref = (__bridge CGColorRef)([self yy_attribute:(NSString *)kCTUnderlineColorAttributeName atIndex:index]);
+        color = [UIColor colorWithCGColor:ref];
+    }
+    return color;
+}
+
+- (NSNumber *)yy_ligature {
+    return [self yy_ligatureAtIndex:0];
+}
+
+- (NSNumber *)yy_ligatureAtIndex:(NSUInteger)index {
+    return [self yy_attribute:NSLigatureAttributeName atIndex:index];
+}
+
+- (NSString *)yy_textEffect {
+    return [self yy_textEffectAtIndex:0];
+}
+
+- (NSString *)yy_textEffectAtIndex:(NSUInteger)index {
+    if (kSystemVersion >= 7) {
+        return [self yy_attribute:NSTextEffectAttributeName atIndex:index];
+    }
+    return nil;
+}
+
+- (NSNumber *)yy_obliqueness {
+    return [self yy_obliquenessAtIndex:0];
+}
+
+- (NSNumber *)yy_obliquenessAtIndex:(NSUInteger)index {
+    if (kSystemVersion >= 7) {
+        return [self yy_attribute:NSObliquenessAttributeName atIndex:index];
+    }
+    return nil;
+}
+
+- (NSNumber *)yy_expansion {
+    return [self yy_expansionAtIndex:0];
+}
+
+- (NSNumber *)yy_expansionAtIndex:(NSUInteger)index {
+    if (kSystemVersion >= 7) {
+        return [self yy_attribute:NSExpansionAttributeName atIndex:index];
+    }
+    return nil;
+}
+
+- (NSNumber *)yy_baselineOffset {
+    return [self yy_baselineOffsetAtIndex:0];
+}
+
+- (NSNumber *)yy_baselineOffsetAtIndex:(NSUInteger)index {
+    if (kSystemVersion >= 7) {
+        return [self yy_attribute:NSBaselineOffsetAttributeName atIndex:index];
+    }
+    return nil;
+}
+
+- (BOOL)yy_verticalGlyphForm {
+    return [self yy_verticalGlyphFormAtIndex:0];
+}
+
+- (BOOL)yy_verticalGlyphFormAtIndex:(NSUInteger)index {
+    NSNumber *num = [self yy_attribute:NSVerticalGlyphFormAttributeName atIndex:index];
+    return num.boolValue;
+}
+
+- (NSString *)yy_language {
+    return [self yy_languageAtIndex:0];
+}
+
+- (NSString *)yy_languageAtIndex:(NSUInteger)index {
+    if (kSystemVersion >= 7) {
+        return [self yy_attribute:(id)kCTLanguageAttributeName atIndex:index];
+    }
+    return nil;
+}
+
+- (NSArray *)yy_writingDirection {
+    return [self yy_writingDirectionAtIndex:0];
+}
+
+- (NSArray *)yy_writingDirectionAtIndex:(NSUInteger)index {
+    return [self yy_attribute:(id)kCTWritingDirectionAttributeName atIndex:index];
+}
+
+- (NSParagraphStyle *)yy_paragraphStyle {
+    return [self yy_paragraphStyleAtIndex:0];
+}
+
+- (NSParagraphStyle *)yy_paragraphStyleAtIndex:(NSUInteger)index {
+    /*
+     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.
+     */
+    NSParagraphStyle *style = [self yy_attribute:NSParagraphStyleAttributeName atIndex:index];
+    if (style) {
+        if (CFGetTypeID((__bridge CFTypeRef)(style)) == CTParagraphStyleGetTypeID()) { \
+            style = [NSParagraphStyle yy_styleWithCTStyle:(__bridge CTParagraphStyleRef)(style)];
+        }
+    }
+    return style;
+}
+
+#define ParagraphAttribute(_attr_) \
+NSParagraphStyle *style = self.yy_paragraphStyle; \
+if (!style) style = [NSParagraphStyle defaultParagraphStyle]; \
+return style. _attr_;
+
+#define ParagraphAttributeAtIndex(_attr_) \
+NSParagraphStyle *style = [self yy_paragraphStyleAtIndex:index]; \
+if (!style) style = [NSParagraphStyle defaultParagraphStyle]; \
+return style. _attr_;
+
+- (NSTextAlignment)yy_alignment {
+    ParagraphAttribute(alignment);
+}
+
+- (NSLineBreakMode)yy_lineBreakMode {
+    ParagraphAttribute(lineBreakMode);
+}
+
+- (CGFloat)yy_lineSpacing {
+    ParagraphAttribute(lineSpacing);
+}
+
+- (CGFloat)yy_paragraphSpacing {
+    ParagraphAttribute(paragraphSpacing);
+}
+
+- (CGFloat)yy_paragraphSpacingBefore {
+    ParagraphAttribute(paragraphSpacingBefore);
+}
+
+- (CGFloat)yy_firstLineHeadIndent {
+    ParagraphAttribute(firstLineHeadIndent);
+}
+
+- (CGFloat)yy_headIndent {
+    ParagraphAttribute(headIndent);
+}
+
+- (CGFloat)yy_tailIndent {
+    ParagraphAttribute(tailIndent);
+}
+
+- (CGFloat)yy_minimumLineHeight {
+    ParagraphAttribute(minimumLineHeight);
+}
+
+- (CGFloat)yy_maximumLineHeight {
+    ParagraphAttribute(maximumLineHeight);
+}
+
+- (CGFloat)yy_lineHeightMultiple {
+    ParagraphAttribute(lineHeightMultiple);
+}
+
+- (NSWritingDirection)yy_baseWritingDirection {
+    ParagraphAttribute(baseWritingDirection);
+}
+
+- (float)yy_hyphenationFactor {
+    ParagraphAttribute(hyphenationFactor);
+}
+
+- (CGFloat)yy_defaultTabInterval {
+    if (!kiOS7Later) return 0;
+    ParagraphAttribute(defaultTabInterval);
+}
+
+- (NSArray *)yy_tabStops {
+    if (!kiOS7Later) return nil;
+    ParagraphAttribute(tabStops);
+}
+
+- (NSTextAlignment)yy_alignmentAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(alignment);
+}
+
+- (NSLineBreakMode)yy_lineBreakModeAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(lineBreakMode);
+}
+
+- (CGFloat)yy_lineSpacingAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(lineSpacing);
+}
+
+- (CGFloat)yy_paragraphSpacingAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(paragraphSpacing);
+}
+
+- (CGFloat)yy_paragraphSpacingBeforeAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(paragraphSpacingBefore);
+}
+
+- (CGFloat)yy_firstLineHeadIndentAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(firstLineHeadIndent);
+}
+
+- (CGFloat)yy_headIndentAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(headIndent);
+}
+
+- (CGFloat)yy_tailIndentAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(tailIndent);
+}
+
+- (CGFloat)yy_minimumLineHeightAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(minimumLineHeight);
+}
+
+- (CGFloat)yy_maximumLineHeightAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(maximumLineHeight);
+}
+
+- (CGFloat)yy_lineHeightMultipleAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(lineHeightMultiple);
+}
+
+- (NSWritingDirection)yy_baseWritingDirectionAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(baseWritingDirection);
+}
+
+- (float)yy_hyphenationFactorAtIndex:(NSUInteger)index {
+    ParagraphAttributeAtIndex(hyphenationFactor);
+}
+
+- (CGFloat)yy_defaultTabIntervalAtIndex:(NSUInteger)index {
+    if (!kiOS7Later) return 0;
+    ParagraphAttributeAtIndex(defaultTabInterval);
+}
+
+- (NSArray *)yy_tabStopsAtIndex:(NSUInteger)index {
+    if (!kiOS7Later) return nil;
+    ParagraphAttributeAtIndex(tabStops);
+}
+
+#undef ParagraphAttribute
+#undef ParagraphAttributeAtIndex
+
+- (YYTextShadow *)yy_textShadow {
+    return [self yy_textShadowAtIndex:0];
+}
+
+- (YYTextShadow *)yy_textShadowAtIndex:(NSUInteger)index {
+    return [self yy_attribute:YYTextShadowAttributeName atIndex:index];
+}
+
+- (YYTextShadow *)yy_textInnerShadow {
+    return [self yy_textInnerShadowAtIndex:0];
+}
+
+- (YYTextShadow *)yy_textInnerShadowAtIndex:(NSUInteger)index {
+    return [self yy_attribute:YYTextInnerShadowAttributeName atIndex:index];
+}
+
+- (YYTextDecoration *)yy_textUnderline {
+    return [self yy_textUnderlineAtIndex:0];
+}
+
+- (YYTextDecoration *)yy_textUnderlineAtIndex:(NSUInteger)index {
+    return [self yy_attribute:YYTextUnderlineAttributeName atIndex:index];
+}
+
+- (YYTextDecoration *)yy_textStrikethrough {
+    return [self yy_textStrikethroughAtIndex:0];
+}
+
+- (YYTextDecoration *)yy_textStrikethroughAtIndex:(NSUInteger)index {
+    return [self yy_attribute:YYTextStrikethroughAttributeName atIndex:index];
+}
+
+- (YYTextBorder *)yy_textBorder {
+    return [self yy_textBorderAtIndex:0];
+}
+
+- (YYTextBorder *)yy_textBorderAtIndex:(NSUInteger)index {
+    return [self yy_attribute:YYTextBorderAttributeName atIndex:index];
+}
+
+- (YYTextBorder *)yy_textBackgroundBorder {
+    return [self yy_textBackgroundBorderAtIndex:0];
+}
+
+- (YYTextBorder *)yy_textBackgroundBorderAtIndex:(NSUInteger)index {
+    return [self yy_attribute:YYTextBackedStringAttributeName atIndex:index];
+}
+
+- (CGAffineTransform)yy_textGlyphTransform {
+    return [self yy_textGlyphTransformAtIndex:0];
+}
+
+- (CGAffineTransform)yy_textGlyphTransformAtIndex:(NSUInteger)index {
+    NSValue *value = [self yy_attribute:YYTextGlyphTransformAttributeName atIndex:index];
+    if (!value) return CGAffineTransformIdentity;
+    return [value CGAffineTransformValue];
+}
+
+- (NSString *)yy_plainTextForRange:(NSRange)range {
+    if (range.location == NSNotFound ||range.length == NSNotFound) return nil;
+    NSMutableString *result = [NSMutableString string];
+    if (range.length == 0) return result;
+    NSString *string = self.string;
+    [self enumerateAttribute:YYTextBackedStringAttributeName inRange:range options:kNilOptions usingBlock:^(id value, NSRange range, BOOL *stop) {
+        YYTextBackedString *backed = value;
+        if (backed && backed.string) {
+            [result appendString:backed.string];
+        } else {
+            [result appendString:[string substringWithRange:range]];
+        }
+    }];
+    return result;
+}
+
++ (NSMutableAttributedString *)yy_attachmentStringWithContent:(id)content
+                                                  contentMode:(UIViewContentMode)contentMode
+                                                        width:(CGFloat)width
+                                                       ascent:(CGFloat)ascent
+                                                      descent:(CGFloat)descent {
+    NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
+    
+    YYTextAttachment *attach = [YYTextAttachment new];
+    attach.content = content;
+    attach.contentMode = contentMode;
+    [atr yy_setTextAttachment:attach range:NSMakeRange(0, atr.length)];
+    
+    YYTextRunDelegate *delegate = [YYTextRunDelegate new];
+    delegate.width = width;
+    delegate.ascent = ascent;
+    delegate.descent = descent;
+    CTRunDelegateRef delegateRef = delegate.CTRunDelegate;
+    [atr yy_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)];
+    if (delegate) CFRelease(delegateRef);
+    
+    return atr;
+}
+
++ (NSMutableAttributedString *)yy_attachmentStringWithContent:(id)content
+                                                  contentMode:(UIViewContentMode)contentMode
+                                               attachmentSize:(CGSize)attachmentSize
+                                                  alignToFont:(UIFont *)font
+                                                    alignment:(YYTextVerticalAlignment)alignment {
+    NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
+    
+    YYTextAttachment *attach = [YYTextAttachment new];
+    attach.content = content;
+    attach.contentMode = contentMode;
+    [atr yy_setTextAttachment:attach range:NSMakeRange(0, atr.length)];
+    
+    YYTextRunDelegate *delegate = [YYTextRunDelegate new];
+    delegate.width = attachmentSize.width;
+    switch (alignment) {
+        case YYTextVerticalAlignmentTop: {
+            delegate.ascent = font.ascender;
+            delegate.descent = attachmentSize.height - font.ascender;
+            if (delegate.descent < 0) {
+                delegate.descent = 0;
+                delegate.ascent = attachmentSize.height;
+            }
+        } break;
+        case YYTextVerticalAlignmentCenter: {
+            CGFloat fontHeight = font.ascender - font.descender;
+            CGFloat yOffset = font.ascender - 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;
+            }
+        } break;
+        case YYTextVerticalAlignmentBottom: {
+            delegate.ascent = attachmentSize.height + font.descender;
+            delegate.descent = -font.descender;
+            if (delegate.ascent < 0) {
+                delegate.ascent = 0;
+                delegate.descent = attachmentSize.height;
+            }
+        } break;
+        default: {
+            delegate.ascent = attachmentSize.height;
+            delegate.descent = 0;
+        } break;
+    }
+    
+    CTRunDelegateRef delegateRef = delegate.CTRunDelegate;
+    [atr yy_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)];
+    if (delegate) CFRelease(delegateRef);
+    
+    return atr;
+}
+
++ (NSMutableAttributedString *)yy_attachmentStringWithEmojiImage:(UIImage *)image
+                                                        fontSize:(CGFloat)fontSize {
+    if (!image || fontSize <= 0) return nil;
+    
+    BOOL hasAnim = NO;
+    if (image.images.count > 1) {
+        hasAnim = YES;
+    } else if (NSProtocolFromString(@"YYAnimatedImage") &&
+               [image conformsToProtocol:NSProtocolFromString(@"YYAnimatedImage")]) {
+        NSNumber *frameCount = [image valueForKey:@"animatedImageFrameCount"];
+        if (frameCount.intValue > 1) hasAnim = YES;
+    }
+    
+    CGFloat ascent = YYTextEmojiGetAscentWithFontSize(fontSize);
+    CGFloat descent = YYTextEmojiGetDescentWithFontSize(fontSize);
+    CGRect bounding = YYTextEmojiGetGlyphBoundingRectWithFontSize(fontSize);
+    
+    YYTextRunDelegate *delegate = [YYTextRunDelegate new];
+    delegate.ascent = ascent;
+    delegate.descent = descent;
+    delegate.width = bounding.size.width + 2 * bounding.origin.x;
+    
+    YYTextAttachment *attachment = [YYTextAttachment new];
+    attachment.contentMode = UIViewContentModeScaleAspectFit;
+    attachment.contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), bounding.origin.x, descent + bounding.origin.y, bounding.origin.x);
+    if (hasAnim) {
+        Class imageClass = NSClassFromString(@"YYAnimatedImageView");
+        if (!imageClass) imageClass = [UIImageView class];
+        UIImageView *view = (id)[imageClass new];
+        view.frame = bounding;
+        view.image = image;
+        view.contentMode = UIViewContentModeScaleAspectFit;
+        attachment.content = view;
+    } else {
+        attachment.content = image;
+    }
+    
+    NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
+    [atr yy_setTextAttachment:attachment range:NSMakeRange(0, atr.length)];
+    CTRunDelegateRef ctDelegate = delegate.CTRunDelegate;
+    [atr yy_setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)];
+    if (ctDelegate) CFRelease(ctDelegate);
+    
+    return atr;
+}
+
+- (NSRange)yy_rangeOfAll {
+    return NSMakeRange(0, self.length);
+}
+
+- (BOOL)yy_isSharedAttributesInAllRange {
+    __block BOOL shared = YES;
+    __block NSDictionary *firstAttrs = nil;
+    [self enumerateAttributesInRange:self.yy_rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
+        if (range.location == 0) {
+            firstAttrs = attrs;
+        } else {
+            if (firstAttrs.count != attrs.count) {
+                shared = NO;
+                *stop = YES;
+            } else if (firstAttrs) {
+                if (![firstAttrs isEqualToDictionary:attrs]) {
+                    shared = NO;
+                    *stop = YES;
+                }
+            }
+        }
+    }];
+    return shared;
+}
+
+- (BOOL)yy_canDrawWithUIKit {
+    static NSMutableSet *failSet;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        failSet = [NSMutableSet new];
+        [failSet addObject:(id)kCTGlyphInfoAttributeName];
+        [failSet addObject:(id)kCTCharacterShapeAttributeName];
+        if (kiOS7Later) {
+            [failSet addObject:(id)kCTLanguageAttributeName];
+        }
+        [failSet addObject:(id)kCTRunDelegateAttributeName];
+        [failSet addObject:(id)kCTBaselineClassAttributeName];
+        [failSet addObject:(id)kCTBaselineInfoAttributeName];
+        [failSet addObject:(id)kCTBaselineReferenceInfoAttributeName];
+        if (kiOS8Later) {
+            [failSet addObject:(id)kCTRubyAnnotationAttributeName];
+        }
+        [failSet addObject:YYTextShadowAttributeName];
+        [failSet addObject:YYTextInnerShadowAttributeName];
+        [failSet addObject:YYTextUnderlineAttributeName];
+        [failSet addObject:YYTextStrikethroughAttributeName];
+        [failSet addObject:YYTextBorderAttributeName];
+        [failSet addObject:YYTextBackgroundBorderAttributeName];
+        [failSet addObject:YYTextBlockBorderAttributeName];
+        [failSet addObject:YYTextAttachmentAttributeName];
+        [failSet addObject:YYTextHighlightAttributeName];
+        [failSet addObject:YYTextGlyphTransformAttributeName];
+    });
+    
+#define Fail { result = NO; *stop = YES; return; }
+    __block BOOL result = YES;
+    [self enumerateAttributesInRange:self.yy_rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
+        if (attrs.count == 0) return;
+        for (NSString *str in attrs.allKeys) {
+            if ([failSet containsObject:str]) Fail;
+        }
+        if (!kiOS7Later) {
+            UIFont *font = attrs[NSFontAttributeName];
+            if (CFGetTypeID((__bridge CFTypeRef)(font)) == CTFontGetTypeID()) Fail;
+        }
+        if (attrs[(id)kCTForegroundColorAttributeName] && !attrs[NSForegroundColorAttributeName]) Fail;
+        if (attrs[(id)kCTStrokeColorAttributeName] && !attrs[NSStrokeColorAttributeName]) Fail;
+        if (attrs[(id)kCTUnderlineColorAttributeName]) {
+            if (!kiOS7Later) Fail;
+            if (!attrs[NSUnderlineColorAttributeName]) Fail;
+        }
+        NSParagraphStyle *style = attrs[NSParagraphStyleAttributeName];
+        if (style && CFGetTypeID((__bridge CFTypeRef)(style)) == CTParagraphStyleGetTypeID()) Fail;
+    }];
+    return result;
+#undef Fail
+}
+
+@end
+
+@implementation NSMutableAttributedString (YYText)
+
+- (void)yy_setAttributes:(NSDictionary *)attributes {
+    [self setYy_attributes:attributes];
+}
+
+- (void)setYy_attributes:(NSDictionary *)attributes {
+    if (attributes == (id)[NSNull null]) attributes = nil;
+    [self setAttributes:@{} range:NSMakeRange(0, self.length)];
+    [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+        [self yy_setAttribute:key value:obj];
+    }];
+}
+
+- (void)yy_setAttribute:(NSString *)name value:(id)value {
+    [self yy_setAttribute:name value:value range:NSMakeRange(0, self.length)];
+}
+
+- (void)yy_setAttribute:(NSString *)name value:(id)value range:(NSRange)range {
+    if (!name || [NSNull isEqual:name]) return;
+    if (value && ![NSNull isEqual:value]) [self addAttribute:name value:value range:range];
+    else [self removeAttribute:name range:range];
+}
+
+- (void)yy_removeAttributesInRange:(NSRange)range {
+    [self setAttributes:nil range:range];
+}
+
+#pragma mark - Property Setter
+
+- (void)setYy_font:(UIFont *)font {
+    /*
+     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.
+     */
+    [self yy_setFont:font range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_kern:(NSNumber *)kern {
+    [self yy_setKern:kern range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_color:(UIColor *)color {
+    [self yy_setColor:color range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_backgroundColor:(UIColor *)backgroundColor {
+    [self yy_setBackgroundColor:backgroundColor range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_strokeWidth:(NSNumber *)strokeWidth {
+    [self yy_setStrokeWidth:strokeWidth range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_strokeColor:(UIColor *)strokeColor {
+    [self yy_setStrokeColor:strokeColor range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_shadow:(NSShadow *)shadow {
+    [self yy_setShadow:shadow range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_strikethroughStyle:(NSUnderlineStyle)strikethroughStyle {
+    [self yy_setStrikethroughStyle:strikethroughStyle range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_strikethroughColor:(UIColor *)strikethroughColor {
+    [self yy_setStrokeColor:strikethroughColor range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_underlineStyle:(NSUnderlineStyle)underlineStyle {
+    [self yy_setUnderlineStyle:underlineStyle range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_underlineColor:(UIColor *)underlineColor {
+    [self yy_setUnderlineColor:underlineColor range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_ligature:(NSNumber *)ligature {
+    [self yy_setLigature:ligature range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_textEffect:(NSString *)textEffect {
+    [self yy_setTextEffect:textEffect range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_obliqueness:(NSNumber *)obliqueness {
+    [self yy_setObliqueness:obliqueness range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_expansion:(NSNumber *)expansion {
+    [self yy_setExpansion:expansion range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_baselineOffset:(NSNumber *)baselineOffset {
+    [self yy_setBaselineOffset:baselineOffset range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_verticalGlyphForm:(BOOL)verticalGlyphForm {
+    [self yy_setVerticalGlyphForm:verticalGlyphForm range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_language:(NSString *)language {
+    [self yy_setLanguage:language range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_writingDirection:(NSArray *)writingDirection {
+    [self yy_setWritingDirection:writingDirection range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_paragraphStyle:(NSParagraphStyle *)paragraphStyle {
+    /*
+     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.
+     */
+    [self yy_setParagraphStyle:paragraphStyle range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_alignment:(NSTextAlignment)alignment {
+    [self yy_setAlignment:alignment range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_baseWritingDirection:(NSWritingDirection)baseWritingDirection {
+    [self yy_setBaseWritingDirection:baseWritingDirection range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_lineSpacing:(CGFloat)lineSpacing {
+    [self yy_setLineSpacing:lineSpacing range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_paragraphSpacing:(CGFloat)paragraphSpacing {
+    [self yy_setParagraphSpacing:paragraphSpacing range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_paragraphSpacingBefore:(CGFloat)paragraphSpacingBefore {
+    [self yy_setParagraphSpacing:paragraphSpacingBefore range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_firstLineHeadIndent:(CGFloat)firstLineHeadIndent {
+    [self yy_setFirstLineHeadIndent:firstLineHeadIndent range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_headIndent:(CGFloat)headIndent {
+    [self yy_setHeadIndent:headIndent range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_tailIndent:(CGFloat)tailIndent {
+    [self yy_setTailIndent:tailIndent range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_lineBreakMode:(NSLineBreakMode)lineBreakMode {
+    [self yy_setLineBreakMode:lineBreakMode range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_minimumLineHeight:(CGFloat)minimumLineHeight {
+    [self yy_setMinimumLineHeight:minimumLineHeight range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_maximumLineHeight:(CGFloat)maximumLineHeight {
+    [self yy_setMaximumLineHeight:maximumLineHeight range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_lineHeightMultiple:(CGFloat)lineHeightMultiple {
+    [self yy_setLineHeightMultiple:lineHeightMultiple range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_hyphenationFactor:(float)hyphenationFactor {
+    [self yy_setHyphenationFactor:hyphenationFactor range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_defaultTabInterval:(CGFloat)defaultTabInterval {
+    [self yy_setDefaultTabInterval:defaultTabInterval range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_tabStops:(NSArray *)tabStops {
+    [self yy_setTabStops:tabStops range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_textShadow:(YYTextShadow *)textShadow {
+    [self yy_setTextShadow:textShadow range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_textInnerShadow:(YYTextShadow *)textInnerShadow {
+    [self yy_setTextInnerShadow:textInnerShadow range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_textUnderline:(YYTextDecoration *)textUnderline {
+    [self yy_setTextUnderline:textUnderline range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_textStrikethrough:(YYTextDecoration *)textStrikethrough {
+    [self yy_setTextStrikethrough:textStrikethrough range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_textBorder:(YYTextBorder *)textBorder {
+    [self yy_setTextBorder:textBorder range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_textBackgroundBorder:(YYTextBorder *)textBackgroundBorder {
+    [self yy_setTextBackgroundBorder:textBackgroundBorder range:NSMakeRange(0, self.length)];
+}
+
+- (void)setYy_textGlyphTransform:(CGAffineTransform)textGlyphTransform {
+    [self yy_setTextGlyphTransform:textGlyphTransform range:NSMakeRange(0, self.length)];
+}
+
+#pragma mark - Range Setter
+
+- (void)yy_setFont:(UIFont *)font range:(NSRange)range {
+    /*
+     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.
+     */
+    [self yy_setAttribute:NSFontAttributeName value:font range:range];
+}
+
+- (void)yy_setKern:(NSNumber *)kern range:(NSRange)range {
+    [self yy_setAttribute:NSKernAttributeName value:kern range:range];
+}
+
+- (void)yy_setColor:(UIColor *)color range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTForegroundColorAttributeName value:(id)color.CGColor range:range];
+    [self yy_setAttribute:NSForegroundColorAttributeName value:color range:range];
+}
+
+- (void)yy_setBackgroundColor:(UIColor *)backgroundColor range:(NSRange)range {
+    [self yy_setAttribute:NSBackgroundColorAttributeName value:backgroundColor range:range];
+}
+
+- (void)yy_setStrokeWidth:(NSNumber *)strokeWidth range:(NSRange)range {
+    [self yy_setAttribute:NSStrokeWidthAttributeName value:strokeWidth range:range];
+}
+
+- (void)yy_setStrokeColor:(UIColor *)strokeColor range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTStrokeColorAttributeName value:(id)strokeColor.CGColor range:range];
+    [self yy_setAttribute:NSStrokeColorAttributeName value:strokeColor range:range];
+}
+
+- (void)yy_setShadow:(NSShadow *)shadow range:(NSRange)range {
+    [self yy_setAttribute:NSShadowAttributeName value:shadow range:range];
+}
+
+- (void)yy_setStrikethroughStyle:(NSUnderlineStyle)strikethroughStyle range:(NSRange)range {
+    NSNumber *style = strikethroughStyle == 0 ? nil : @(strikethroughStyle);
+    [self yy_setAttribute:NSStrikethroughStyleAttributeName value:style range:range];
+}
+
+- (void)yy_setStrikethroughColor:(UIColor *)strikethroughColor range:(NSRange)range {
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:NSStrikethroughColorAttributeName value:strikethroughColor range:range];
+    }
+}
+
+- (void)yy_setUnderlineStyle:(NSUnderlineStyle)underlineStyle range:(NSRange)range {
+    NSNumber *style = underlineStyle == 0 ? nil : @(underlineStyle);
+    [self yy_setAttribute:NSUnderlineStyleAttributeName value:style range:range];
+}
+
+- (void)yy_setUnderlineColor:(UIColor *)underlineColor range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTUnderlineColorAttributeName value:(id)underlineColor.CGColor range:range];
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:NSUnderlineColorAttributeName value:underlineColor range:range];
+    }
+}
+
+- (void)yy_setLigature:(NSNumber *)ligature range:(NSRange)range {
+    [self yy_setAttribute:NSLigatureAttributeName value:ligature range:range];
+}
+
+- (void)yy_setTextEffect:(NSString *)textEffect range:(NSRange)range {
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:NSTextEffectAttributeName value:textEffect range:range];
+    }
+}
+
+- (void)yy_setObliqueness:(NSNumber *)obliqueness range:(NSRange)range {
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:NSObliquenessAttributeName value:obliqueness range:range];
+    }
+}
+
+- (void)yy_setExpansion:(NSNumber *)expansion range:(NSRange)range {
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:NSExpansionAttributeName value:expansion range:range];
+    }
+}
+
+- (void)yy_setBaselineOffset:(NSNumber *)baselineOffset range:(NSRange)range {
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:NSBaselineOffsetAttributeName value:baselineOffset range:range];
+    }
+}
+
+- (void)yy_setVerticalGlyphForm:(BOOL)verticalGlyphForm range:(NSRange)range {
+    NSNumber *v = verticalGlyphForm ? @(YES) : nil;
+    [self yy_setAttribute:NSVerticalGlyphFormAttributeName value:v range:range];
+}
+
+- (void)yy_setLanguage:(NSString *)language range:(NSRange)range {
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:(id)kCTLanguageAttributeName value:language range:range];
+    }
+}
+
+- (void)yy_setWritingDirection:(NSArray *)writingDirection range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTWritingDirectionAttributeName value:writingDirection range:range];
+}
+
+- (void)yy_setParagraphStyle:(NSParagraphStyle *)paragraphStyle range:(NSRange)range {
+    /*
+     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.
+     */
+    [self yy_setAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
+}
+
+#define ParagraphStyleSet(_attr_) \
+[self enumerateAttribute:NSParagraphStyleAttributeName \
+                 inRange:range \
+                 options:kNilOptions \
+              usingBlock: ^(NSParagraphStyle *value, NSRange subRange, BOOL *stop) { \
+                  NSMutableParagraphStyle *style = nil; \
+                  if (value) { \
+                      if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \
+                          value = [NSParagraphStyle yy_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \
+                      } \
+                      if (value. _attr_ == _attr_) return; \
+                      if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \
+                          style = (id)value; \
+                      } else { \
+                          style = value.mutableCopy; \
+                      } \
+                  } else { \
+                      if ([NSParagraphStyle defaultParagraphStyle]. _attr_ == _attr_) return; \
+                      style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; \
+                  } \
+                  style. _attr_ = _attr_; \
+                  [self yy_setParagraphStyle:style range:subRange]; \
+              }];
+
+- (void)yy_setAlignment:(NSTextAlignment)alignment range:(NSRange)range {
+    ParagraphStyleSet(alignment);
+}
+
+- (void)yy_setBaseWritingDirection:(NSWritingDirection)baseWritingDirection range:(NSRange)range {
+    ParagraphStyleSet(baseWritingDirection);
+}
+
+- (void)yy_setLineSpacing:(CGFloat)lineSpacing range:(NSRange)range {
+    ParagraphStyleSet(lineSpacing);
+}
+
+- (void)yy_setParagraphSpacing:(CGFloat)paragraphSpacing range:(NSRange)range {
+    ParagraphStyleSet(paragraphSpacing);
+}
+
+- (void)yy_setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore range:(NSRange)range {
+    ParagraphStyleSet(paragraphSpacingBefore);
+}
+
+- (void)yy_setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent range:(NSRange)range {
+    ParagraphStyleSet(firstLineHeadIndent);
+}
+
+- (void)yy_setHeadIndent:(CGFloat)headIndent range:(NSRange)range {
+    ParagraphStyleSet(headIndent);
+}
+
+- (void)yy_setTailIndent:(CGFloat)tailIndent range:(NSRange)range {
+    ParagraphStyleSet(tailIndent);
+}
+
+- (void)yy_setLineBreakMode:(NSLineBreakMode)lineBreakMode range:(NSRange)range {
+    ParagraphStyleSet(lineBreakMode);
+}
+
+- (void)yy_setMinimumLineHeight:(CGFloat)minimumLineHeight range:(NSRange)range {
+    ParagraphStyleSet(minimumLineHeight);
+}
+
+- (void)yy_setMaximumLineHeight:(CGFloat)maximumLineHeight range:(NSRange)range {
+    ParagraphStyleSet(maximumLineHeight);
+}
+
+- (void)yy_setLineHeightMultiple:(CGFloat)lineHeightMultiple range:(NSRange)range {
+    ParagraphStyleSet(lineHeightMultiple);
+}
+
+- (void)yy_setHyphenationFactor:(float)hyphenationFactor range:(NSRange)range {
+    ParagraphStyleSet(hyphenationFactor);
+}
+
+- (void)yy_setDefaultTabInterval:(CGFloat)defaultTabInterval range:(NSRange)range {
+    if (!kiOS7Later) return;
+    ParagraphStyleSet(defaultTabInterval);
+}
+
+- (void)yy_setTabStops:(NSArray *)tabStops range:(NSRange)range {
+    if (!kiOS7Later) return;
+    ParagraphStyleSet(tabStops);
+}
+
+#undef ParagraphStyleSet
+
+- (void)yy_setSuperscript:(NSNumber *)superscript range:(NSRange)range {
+    if ([superscript isEqualToNumber:@(0)]) {
+        superscript = nil;
+    }
+    [self yy_setAttribute:(id)kCTSuperscriptAttributeName value:superscript range:range];
+}
+
+- (void)yy_setGlyphInfo:(CTGlyphInfoRef)glyphInfo range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTGlyphInfoAttributeName value:(__bridge id)glyphInfo range:range];
+}
+
+- (void)yy_setCharacterShape:(NSNumber *)characterShape range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTCharacterShapeAttributeName value:characterShape range:range];
+}
+
+- (void)yy_setRunDelegate:(CTRunDelegateRef)runDelegate range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTRunDelegateAttributeName value:(__bridge id)runDelegate range:range];
+}
+
+- (void)yy_setBaselineClass:(CFStringRef)baselineClass range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTBaselineClassAttributeName value:(__bridge id)baselineClass range:range];
+}
+
+- (void)yy_setBaselineInfo:(CFDictionaryRef)baselineInfo range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTBaselineInfoAttributeName value:(__bridge id)baselineInfo range:range];
+}
+
+- (void)yy_setBaselineReferenceInfo:(CFDictionaryRef)referenceInfo range:(NSRange)range {
+    [self yy_setAttribute:(id)kCTBaselineReferenceInfoAttributeName value:(__bridge id)referenceInfo range:range];
+}
+
+- (void)yy_setRubyAnnotation:(CTRubyAnnotationRef)ruby range:(NSRange)range {
+    if (kSystemVersion >= 8) {
+        [self yy_setAttribute:(id)kCTRubyAnnotationAttributeName value:(__bridge id)ruby range:range];
+    }
+}
+
+- (void)yy_setAttachment:(NSTextAttachment *)attachment range:(NSRange)range {
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:NSAttachmentAttributeName value:attachment range:range];
+    }
+}
+
+- (void)yy_setLink:(id)link range:(NSRange)range {
+    if (kSystemVersion >= 7) {
+        [self yy_setAttribute:NSLinkAttributeName value:link range:range];
+    }
+}
+
+- (void)yy_setTextBackedString:(YYTextBackedString *)textBackedString range:(NSRange)range {
+    [self yy_setAttribute:YYTextBackedStringAttributeName value:textBackedString range:range];
+}
+
+- (void)yy_setTextBinding:(YYTextBinding *)textBinding range:(NSRange)range {
+    [self yy_setAttribute:YYTextBindingAttributeName value:textBinding range:range];
+}
+
+- (void)yy_setTextShadow:(YYTextShadow *)textShadow range:(NSRange)range {
+    [self yy_setAttribute:YYTextShadowAttributeName value:textShadow range:range];
+}
+
+- (void)yy_setTextInnerShadow:(YYTextShadow *)textInnerShadow range:(NSRange)range {
+    [self yy_setAttribute:YYTextInnerShadowAttributeName value:textInnerShadow range:range];
+}
+
+- (void)yy_setTextUnderline:(YYTextDecoration *)textUnderline range:(NSRange)range {
+    [self yy_setAttribute:YYTextUnderlineAttributeName value:textUnderline range:range];
+}
+
+- (void)yy_setTextStrikethrough:(YYTextDecoration *)textStrikethrough range:(NSRange)range {
+    [self yy_setAttribute:YYTextStrikethroughAttributeName value:textStrikethrough range:range];
+}
+
+- (void)yy_setTextBorder:(YYTextBorder *)textBorder range:(NSRange)range {
+    [self yy_setAttribute:YYTextBorderAttributeName value:textBorder range:range];
+}
+
+- (void)yy_setTextBackgroundBorder:(YYTextBorder *)textBackgroundBorder range:(NSRange)range {
+    [self yy_setAttribute:YYTextBackgroundBorderAttributeName value:textBackgroundBorder range:range];
+}
+
+- (void)yy_setTextAttachment:(YYTextAttachment *)textAttachment range:(NSRange)range {
+    [self yy_setAttribute:YYTextAttachmentAttributeName value:textAttachment range:range];
+}
+
+- (void)yy_setTextHighlight:(YYTextHighlight *)textHighlight range:(NSRange)range {
+    [self yy_setAttribute:YYTextHighlightAttributeName value:textHighlight range:range];
+}
+
+- (void)yy_setTextBlockBorder:(YYTextBorder *)textBlockBorder range:(NSRange)range {
+    [self yy_setAttribute:YYTextBlockBorderAttributeName value:textBlockBorder range:range];
+}
+
+- (void)yy_setTextRubyAnnotation:(YYTextRubyAnnotation *)ruby range:(NSRange)range {
+    if (kiOS8Later) {
+        CTRubyAnnotationRef rubyRef = [ruby CTRubyAnnotation];
+        [self yy_setRubyAnnotation:rubyRef range:range];
+        if (rubyRef) CFRelease(rubyRef);
+    }
+}
+
+- (void)yy_setTextGlyphTransform:(CGAffineTransform)textGlyphTransform range:(NSRange)range {
+    NSValue *value = CGAffineTransformIsIdentity(textGlyphTransform) ? nil : [NSValue valueWithCGAffineTransform:textGlyphTransform];
+    [self yy_setAttribute:YYTextGlyphTransformAttributeName value:value range:range];
+}
+
+- (void)yy_setTextHighlightRange:(NSRange)range
+                           color:(UIColor *)color
+                 backgroundColor:(UIColor *)backgroundColor
+                        userInfo:(NSDictionary *)userInfo
+                       tapAction:(YYTextAction)tapAction
+                 longPressAction:(YYTextAction)longPressAction {
+    YYTextHighlight *highlight = [YYTextHighlight highlightWithBackgroundColor:backgroundColor];
+    highlight.userInfo = userInfo;
+    highlight.tapAction = tapAction;
+    highlight.longPressAction = longPressAction;
+    if (color) [self yy_setColor:color range:range];
+    [self yy_setTextHighlight:highlight range:range];
+}
+
+- (void)yy_setTextHighlightRange:(NSRange)range
+                           color:(UIColor *)color
+                 backgroundColor:(UIColor *)backgroundColor
+                       tapAction:(YYTextAction)tapAction {
+    [self yy_setTextHighlightRange:range
+                         color:color
+               backgroundColor:backgroundColor
+                      userInfo:nil
+                     tapAction:tapAction
+               longPressAction:nil];
+}
+
+- (void)yy_setTextHighlightRange:(NSRange)range
+                           color:(UIColor *)color
+                 backgroundColor:(UIColor *)backgroundColor
+                        userInfo:(NSDictionary *)userInfo {
+    [self yy_setTextHighlightRange:range
+                         color:color
+               backgroundColor:backgroundColor
+                      userInfo:userInfo
+                     tapAction:nil
+               longPressAction:nil];
+}
+
+- (void)yy_insertString:(NSString *)string atIndex:(NSUInteger)location {
+    [self replaceCharactersInRange:NSMakeRange(location, 0) withString:string];
+    [self yy_removeDiscontinuousAttributesInRange:NSMakeRange(location, string.length)];
+}
+
+- (void)yy_appendString:(NSString *)string {
+    NSUInteger length = self.length;
+    [self replaceCharactersInRange:NSMakeRange(length, 0) withString:string];
+    [self yy_removeDiscontinuousAttributesInRange:NSMakeRange(length, string.length)];
+}
+
+- (void)yy_setClearColorToJoinedEmoji {
+    NSString *str = self.string;
+    if (str.length < 8) return;
+    
+    // Most string do not contains the joined-emoji, test the joiner first.
+    BOOL containsJoiner = NO;
+    {
+        CFStringRef cfStr = (__bridge CFStringRef)str;
+        BOOL needFree = NO;
+        UniChar *chars = NULL;
+        chars = (void *)CFStringGetCharactersPtr(cfStr);
+        if (!chars) {
+            chars = malloc(str.length * sizeof(UniChar));
+            if (chars) {
+                needFree = YES;
+                CFStringGetCharacters(cfStr, CFRangeMake(0, str.length), chars);
+            }
+        }
+        if (!chars) { // fail to get unichar..
+            containsJoiner = YES;
+        } else {
+            for (int i = 0, max = (int)str.length; i < max; i++) {
+                if (chars[i] == 0x200D) { // 'ZERO WIDTH JOINER' (U+200D)
+                    containsJoiner = YES;
+                    break;
+                }
+            }
+            if (needFree) free(chars);
+        }
+    }
+    if (!containsJoiner) return;
+    
+    // NSRegularExpression is designed to be immutable and thread safe.
+    static NSRegularExpression *regex;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        regex = [NSRegularExpression regularExpressionWithPattern:@"((👨‍👩‍👧‍👦|👨‍👩‍👦‍👦|👨‍👩‍👧‍👧|👩‍👩‍👧‍👦|👩‍👩‍👦‍👦|👩‍👩‍👧‍👧|👨‍👨‍👧‍👦|👨‍👨‍👦‍👦|👨‍👨‍👧‍👧)+|(👨‍👩‍👧|👩‍👩‍👦|👩‍👩‍👧|👨‍👨‍👦|👨‍👨‍👧))" options:kNilOptions error:nil];
+    });
+    
+    UIColor *clear = [UIColor clearColor];
+    [regex enumerateMatchesInString:str options:kNilOptions range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        [self yy_setColor:clear range:result.range];
+    }];
+}
+
+- (void)yy_removeDiscontinuousAttributesInRange:(NSRange)range {
+    NSArray *keys = [NSMutableAttributedString yy_allDiscontinuousAttributeKeys];
+    for (NSString *key in keys) {
+        [self removeAttribute:key range:range];
+    }
+}
+
++ (NSArray *)yy_allDiscontinuousAttributeKeys {
+    static NSMutableArray *keys;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        keys = @[(id)kCTSuperscriptAttributeName,
+                 (id)kCTRunDelegateAttributeName,
+                 YYTextBackedStringAttributeName,
+                 YYTextBindingAttributeName,
+                 YYTextAttachmentAttributeName].mutableCopy;
+        if (kiOS8Later) {
+            [keys addObject:(id)kCTRubyAnnotationAttributeName];
+        }
+        if (kiOS7Later) {
+            [keys addObject:NSAttachmentAttributeName];
+        }
+    });
+    return keys;
+}
+
+@end

+ 37 - 0
Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.h

@@ -0,0 +1,37 @@
+//
+//  NSParagraphStyle+YYText.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/7.
+//  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
+
+/**
+ Provides extensions for `NSParagraphStyle` to work with CoreText.
+ */
+@interface NSParagraphStyle (YYText)
+
+/**
+ Creates a new NSParagraphStyle object from the CoreText Style.
+ 
+ @param CTStyle CoreText Paragraph Style.
+ 
+ @return a new NSParagraphStyle
+ */
++ (nullable NSParagraphStyle *)yy_styleWithCTStyle:(CTParagraphStyleRef)CTStyle;
+
+/**
+ Creates and returns a CoreText Paragraph Style. (need call CFRelease() after used)
+ */
+- (nullable CTParagraphStyleRef)yy_CTStyle CF_RETURNS_RETAINED;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 224 - 0
Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.m

@@ -0,0 +1,224 @@
+//
+//  NSParagraphStyle+YYText.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/7.
+//  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 "NSParagraphStyle+YYText.h"
+#import "YYTextAttribute.h"
+#import <CoreText/CoreText.h>
+
+// Dummy class for category
+@interface NSParagraphStyle_YYText : NSObject @end
+@implementation NSParagraphStyle_YYText @end
+
+
+@implementation NSParagraphStyle (YYText)
+
++ (NSParagraphStyle *)yy_styleWithCTStyle:(CTParagraphStyleRef)CTStyle {
+    if (CTStyle == NULL) return nil;
+    
+    NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
+    
+    CGFloat lineSpacing;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing)) {
+        style.lineSpacing = lineSpacing;
+    }
+    
+    CGFloat paragraphSpacing;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &paragraphSpacing)) {
+        style.paragraphSpacing = paragraphSpacing;
+    }
+    
+    CTTextAlignment alignment;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &alignment)) {
+        style.alignment = NSTextAlignmentFromCTTextAlignment(alignment);
+    }
+    
+    CGFloat firstLineHeadIndent;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineHeadIndent)) {
+        style.firstLineHeadIndent = firstLineHeadIndent;
+    }
+    
+    CGFloat headIndent;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &headIndent)) {
+        style.headIndent = headIndent;
+    }
+    
+    CGFloat tailIndent;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), &tailIndent)) {
+        style.tailIndent = tailIndent;
+    }
+    
+    CTLineBreakMode lineBreakMode;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreakMode)) {
+        style.lineBreakMode = (NSLineBreakMode)lineBreakMode;
+    }
+    
+    CGFloat minimumLineHeight;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &minimumLineHeight)) {
+        style.minimumLineHeight = minimumLineHeight;
+    }
+    
+    CGFloat maximumLineHeight;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &maximumLineHeight)) {
+        style.maximumLineHeight = maximumLineHeight;
+    }
+    
+    CTWritingDirection baseWritingDirection;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(CTWritingDirection), &baseWritingDirection)) {
+        style.baseWritingDirection = (NSWritingDirection)baseWritingDirection;
+    }
+    
+    CGFloat lineHeightMultiple;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple)) {
+        style.lineHeightMultiple = lineHeightMultiple;
+    }
+    
+    CGFloat paragraphSpacingBefore;
+    if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), &paragraphSpacingBefore)) {
+        style.paragraphSpacingBefore = paragraphSpacingBefore;
+    }
+    
+    if ([style respondsToSelector:@selector(tabStops)]) {
+        CFArrayRef tabStops;
+        if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierTabStops, sizeof(CFArrayRef), &tabStops)) {
+            if ([style respondsToSelector:@selector(setTabStops:)]) {
+                NSMutableArray *tabs = [NSMutableArray new];
+                [((__bridge NSArray *)(tabStops))enumerateObjectsUsingBlock : ^(id obj, NSUInteger idx, BOOL *stop) {
+                    CTTextTabRef ctTab = (__bridge CFTypeRef)obj;
+                    
+                    NSTextTab *tab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentFromCTTextAlignment(CTTextTabGetAlignment(ctTab)) location:CTTextTabGetLocation(ctTab) options:(__bridge id)CTTextTabGetOptions(ctTab)];
+                    [tabs addObject:tab];
+                }];
+                if (tabs.count) {
+                    style.tabStops = tabs;
+                }
+            }
+        }
+        
+        CGFloat defaultTabInterval;
+        if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(CGFloat), &defaultTabInterval)) {
+            if ([style respondsToSelector:@selector(setDefaultTabInterval:)]) {
+                style.defaultTabInterval = defaultTabInterval;
+            }
+        }
+    }
+    
+    return style;
+}
+
+- (CTParagraphStyleRef)yy_CTStyle CF_RETURNS_RETAINED {
+    CTParagraphStyleSetting set[kCTParagraphStyleSpecifierCount] = { 0 };
+    int count = 0;
+    
+    CGFloat lineSpacing = self.lineSpacing;
+    set[count].spec = kCTParagraphStyleSpecifierLineSpacing;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &lineSpacing;
+    count++;
+    
+    CGFloat paragraphSpacing = self.paragraphSpacing;
+    set[count].spec = kCTParagraphStyleSpecifierParagraphSpacing;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &paragraphSpacing;
+    count++;
+    
+    CTTextAlignment alignment = NSTextAlignmentToCTTextAlignment(self.alignment);
+    set[count].spec = kCTParagraphStyleSpecifierAlignment;
+    set[count].valueSize = sizeof(CTTextAlignment);
+    set[count].value = &alignment;
+    count++;
+    
+    CGFloat firstLineHeadIndent = self.firstLineHeadIndent;
+    set[count].spec = kCTParagraphStyleSpecifierFirstLineHeadIndent;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &firstLineHeadIndent;
+    count++;
+    
+    CGFloat headIndent = self.headIndent;
+    set[count].spec = kCTParagraphStyleSpecifierHeadIndent;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &headIndent;
+    count++;
+    
+    CGFloat tailIndent = self.tailIndent;
+    set[count].spec = kCTParagraphStyleSpecifierTailIndent;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &tailIndent;
+    count++;
+    
+    CTLineBreakMode paraLineBreak = (CTLineBreakMode)self.lineBreakMode;
+    set[count].spec = kCTParagraphStyleSpecifierLineBreakMode;
+    set[count].valueSize = sizeof(CTLineBreakMode);
+    set[count].value = &paraLineBreak;
+    count++;
+    
+    CGFloat minimumLineHeight = self.minimumLineHeight;
+    set[count].spec = kCTParagraphStyleSpecifierMinimumLineHeight;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &minimumLineHeight;
+    count++;
+    
+    CGFloat maximumLineHeight = self.maximumLineHeight;
+    set[count].spec = kCTParagraphStyleSpecifierMaximumLineHeight;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &maximumLineHeight;
+    count++;
+    
+    CTWritingDirection paraWritingDirection = (CTWritingDirection)self.baseWritingDirection;
+    set[count].spec = kCTParagraphStyleSpecifierBaseWritingDirection;
+    set[count].valueSize = sizeof(CTWritingDirection);
+    set[count].value = &paraWritingDirection;
+    count++;
+    
+    CGFloat lineHeightMultiple = self.lineHeightMultiple;
+    set[count].spec = kCTParagraphStyleSpecifierLineHeightMultiple;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &lineHeightMultiple;
+    count++;
+    
+    CGFloat paragraphSpacingBefore = self.paragraphSpacingBefore;
+    set[count].spec = kCTParagraphStyleSpecifierParagraphSpacingBefore;
+    set[count].valueSize = sizeof(CGFloat);
+    set[count].value = &paragraphSpacingBefore;
+    count++;
+    
+    if([self respondsToSelector:@selector(tabStops)]) {
+        NSMutableArray *tabs = [NSMutableArray array];
+        if ([self respondsToSelector:@selector(tabStops)]) {
+            NSInteger numTabs = self.tabStops.count;
+            if (numTabs) {
+                [self.tabStops enumerateObjectsUsingBlock: ^(NSTextTab *tab, NSUInteger idx, BOOL *stop) {
+                    CTTextTabRef ctTab = CTTextTabCreate(NSTextAlignmentToCTTextAlignment(tab.alignment), tab.location, (__bridge CFTypeRef)tab.options);
+                    [tabs addObject:(__bridge id)ctTab];
+                    CFRelease(ctTab);
+                }];
+                
+                CFArrayRef tabStops = (__bridge CFArrayRef)(tabs);
+                set[count].spec = kCTParagraphStyleSpecifierTabStops;
+                set[count].valueSize = sizeof(CFArrayRef);
+                set[count].value = &tabStops;
+                count++;
+            }
+        }
+        
+        if ([self respondsToSelector:@selector(defaultTabInterval)]) {
+            CGFloat defaultTabInterval = self.defaultTabInterval;
+            set[count].spec = kCTParagraphStyleSpecifierDefaultTabInterval;
+            set[count].valueSize = sizeof(CGFloat);
+            set[count].value = &defaultTabInterval;
+            count++;
+        }
+    }
+    
+    CTParagraphStyleRef style = CTParagraphStyleCreate(set, count);
+    return style;
+}
+
+@end

+ 41 - 0
Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.h

@@ -0,0 +1,41 @@
+//
+//  UIPasteboard+YYText.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/2.
+//  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
+
+/**
+ Extend UIPasteboard to support image and attributed string.
+ */
+@interface UIPasteboard (YYText)
+
+@property (nullable, nonatomic, copy) NSData *yy_PNGData;    ///< PNG file data
+@property (nullable, nonatomic, copy) NSData *yy_JPEGData;   ///< JPEG file data
+@property (nullable, nonatomic, copy) NSData *yy_GIFData;    ///< GIF file data
+@property (nullable, nonatomic, copy) NSData *yy_WEBPData;   ///< WebP file data
+@property (nullable, nonatomic, copy) NSData *yy_ImageData;  ///< image file data
+
+/// 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.
+@property (nullable, nonatomic, copy) NSAttributedString *yy_AttributedString;
+
+@end
+
+
+/// The name identifying the attributed string in pasteboard.
+UIKIT_EXTERN NSString *const YYTextPasteboardTypeAttributedString;
+
+/// The UTI Type identifying WebP data in pasteboard.
+UIKIT_EXTERN NSString *const YYTextUTTypeWEBP;
+
+NS_ASSUME_NONNULL_END

+ 146 - 0
Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.m

@@ -0,0 +1,146 @@
+//
+//  UIPasteboard+YYText.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/2.
+//  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 "UIPasteboard+YYText.h"
+#import "NSAttributedString+YYText.h"
+#import <MobileCoreServices/MobileCoreServices.h>
+
+
+#if __has_include("YYImage.h")
+#import "YYImage.h"
+#define YYTextAnimatedImageAvailable 1
+#elif __has_include(<YYImage/YYImage.h>)
+#import <YYImage/YYImage.h>
+#define YYTextAnimatedImageAvailable 1
+#elif __has_include(<YYWebImage/YYImage.h>)
+#import <YYWebImage/YYImage.h>
+#define YYTextAnimatedImageAvailable 1
+#else
+#define YYTextAnimatedImageAvailable 0
+#endif
+
+
+// Dummy class for category
+@interface UIPasteboard_YYText : NSObject @end
+@implementation UIPasteboard_YYText @end
+
+
+NSString *const YYTextPasteboardTypeAttributedString = @"com.ibireme.NSAttributedString";
+NSString *const YYTextUTTypeWEBP = @"com.google.webp";
+
+@implementation UIPasteboard (YYText)
+
+
+- (void)setYy_PNGData:(NSData *)PNGData {
+    [self setData:PNGData forPasteboardType:(id)kUTTypePNG];
+}
+
+- (NSData *)yy_PNGData {
+    return [self dataForPasteboardType:(id)kUTTypePNG];
+}
+
+- (void)setYy_JPEGData:(NSData *)JPEGData {
+    [self setData:JPEGData forPasteboardType:(id)kUTTypeJPEG];
+}
+
+- (NSData *)yy_JPEGData {
+    return [self dataForPasteboardType:(id)kUTTypeJPEG];
+}
+
+- (void)setYy_GIFData:(NSData *)GIFData {
+    [self setData:GIFData forPasteboardType:(id)kUTTypeGIF];
+}
+
+- (NSData *)yy_GIFData {
+    return [self dataForPasteboardType:(id)kUTTypeGIF];
+}
+
+- (void)setYy_WEBPData:(NSData *)WEBPData {
+    [self setData:WEBPData forPasteboardType:YYTextUTTypeWEBP];
+}
+
+- (NSData *)yy_WEBPData {
+    return [self dataForPasteboardType:YYTextUTTypeWEBP];
+}
+
+- (void)setYy_ImageData:(NSData *)imageData {
+    [self setData:imageData forPasteboardType:(id)kUTTypeImage];
+}
+
+- (NSData *)yy_ImageData {
+    return [self dataForPasteboardType:(id)kUTTypeImage];
+}
+
+- (void)setYy_AttributedString:(NSAttributedString *)attributedString {
+    self.string = [attributedString yy_plainTextForRange:NSMakeRange(0, attributedString.length)];
+    NSData *data = [attributedString yy_archiveToData];
+    if (data) {
+        NSDictionary *item = @{YYTextPasteboardTypeAttributedString : data};
+        [self addItems:@[item]];
+    }
+    [attributedString enumerateAttribute:YYTextAttachmentAttributeName inRange:NSMakeRange(0, attributedString.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(YYTextAttachment *attachment, NSRange range, BOOL *stop) {
+        
+        // save image
+        UIImage *simpleImage = nil;
+        if ([attachment.content isKindOfClass:[UIImage class]]) {
+            simpleImage = attachment.content;
+        } else if ([attachment.content isKindOfClass:[UIImageView class]]) {
+            simpleImage = ((UIImageView *)attachment.content).image;
+        }
+        if (simpleImage) {
+            NSDictionary *item = @{@"com.apple.uikit.image" : simpleImage};
+            [self addItems:@[item]];
+        }
+        
+#if YYTextAnimatedImageAvailable
+        // save animated image
+        if ([attachment.content isKindOfClass:[UIImageView class]]) {
+            UIImageView *imageView = attachment.content;
+            Class aniImageClass = NSClassFromString(@"YYImage");
+            UIImage *image = imageView.image;
+            if (aniImageClass && [image isKindOfClass:aniImageClass]) {
+                NSData *data = [image valueForKey:@"animatedImageData"];
+                NSNumber *type = [image valueForKey:@"animatedImageType"];
+                if (data) {
+                    switch (type.unsignedIntegerValue) {
+                        case YYImageTypeGIF: {
+                            NSDictionary *item = @{(id)kUTTypeGIF : data};
+                            [self addItems:@[item]];
+                        } break;
+                        case YYImageTypePNG: { // APNG
+                            NSDictionary *item = @{(id)kUTTypePNG : data};
+                            [self addItems:@[item]];
+                        } break;
+                        case YYImageTypeWebP: {
+                            NSDictionary *item = @{(id)YYTextUTTypeWEBP : data};
+                            [self addItems:@[item]];
+                        } break;
+                        default: break;
+                    }
+                }
+            }
+        }
+#endif
+        
+    }];
+}
+
+- (NSAttributedString *)yy_AttributedString {
+    for (NSDictionary *items in self.items) {
+        NSData *data = items[YYTextPasteboardTypeAttributedString];
+        if (data) {
+            return [NSAttributedString yy_unarchiveFromData:data];
+        }
+    }
+    return nil;
+}
+
+@end

+ 72 - 0
Demo/Objective_C_Demo/YYText/UIView+YYText.h

@@ -0,0 +1,72 @@
+//
+//  UIView+YYText.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 13/4/3.
+//  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
+
+/**
+ Provides extensions for `UIView`.
+ */
+@interface UIView (YYText)
+
+/**
+ Returns the view's view controller (may be nil).
+ */
+@property (nullable, nonatomic, readonly) UIViewController *yy_viewController;
+
+/**
+ Returns the visible alpha on screen, taking into account superview and window.
+ */
+@property (nonatomic, readonly) CGFloat yy_visibleAlpha;
+
+/**
+ Converts a point from the receiver's coordinate system to that of the specified view or window.
+ 
+ @param point A point specified in the local coordinate system (bounds) of the receiver.
+ @param view  The view or window into whose coordinate system point is to be converted.
+ If view is nil, this method instead converts to window base coordinates.
+ @return The point converted to the coordinate system of view.
+ */
+- (CGPoint)yy_convertPoint:(CGPoint)point toViewOrWindow:(UIView *)view;
+
+/**
+ Converts a point from the coordinate system of a given view or window to that of the receiver.
+ 
+ @param point A point specified in the local coordinate system (bounds) of view.
+ @param view  The view or window with point in its coordinate system.
+ If view is nil, this method instead converts from window base coordinates.
+ @return The point converted to the local coordinate system (bounds) of the receiver.
+ */
+- (CGPoint)yy_convertPoint:(CGPoint)point fromViewOrWindow:(UIView *)view;
+
+/**
+ Converts a rectangle from the receiver's coordinate system to that of another view or window.
+ 
+ @param rect A rectangle specified in the local coordinate system (bounds) of the receiver.
+ @param view The view or window that is the target of the conversion operation. If view is nil, this method instead converts to window base coordinates.
+ @return The converted rectangle.
+ */
+- (CGRect)yy_convertRect:(CGRect)rect toViewOrWindow:(UIView *)view;
+
+/**
+ Converts a rectangle from the coordinate system of another view or window to that of the receiver.
+ 
+ @param rect A rectangle specified in the local coordinate system (bounds) of view.
+ @param view The view or window with rect in its coordinate system.
+ If view is nil, this method instead converts from window base coordinates.
+ @return The converted rectangle.
+ */
+- (CGRect)yy_convertRect:(CGRect)rect fromViewOrWindow:(UIView *)view;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 123 - 0
Demo/Objective_C_Demo/YYText/UIView+YYText.m

@@ -0,0 +1,123 @@
+//
+//  UIView+YYText.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 13/4/3.
+//  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 "UIView+YYText.h"
+
+// Dummy class for category
+@interface UIView_YYText : NSObject @end
+@implementation UIView_YYText @end
+
+
+@implementation UIView (YYText)
+
+- (UIViewController *)yy_viewController {
+    for (UIView *view = self; view; view = view.superview) {
+        UIResponder *nextResponder = [view nextResponder];
+        if ([nextResponder isKindOfClass:[UIViewController class]]) {
+            return (UIViewController *)nextResponder;
+        }
+    }
+    return nil;
+}
+
+- (CGFloat)yy_visibleAlpha {
+    if ([self isKindOfClass:[UIWindow class]]) {
+        if (self.hidden) return 0;
+        return self.alpha;
+    }
+    if (!self.window) return 0;
+    CGFloat alpha = 1;
+    UIView *v = self;
+    while (v) {
+        if (v.hidden) {
+            alpha = 0;
+            break;
+        }
+        alpha *= v.alpha;
+        v = v.superview;
+    }
+    return alpha;
+}
+
+- (CGPoint)yy_convertPoint:(CGPoint)point toViewOrWindow:(UIView *)view {
+    if (!view) {
+        if ([self isKindOfClass:[UIWindow class]]) {
+            return [((UIWindow *)self) convertPoint:point toWindow:nil];
+        } else {
+            return [self convertPoint:point toView:nil];
+        }
+    }
+    
+    UIWindow *from = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window;
+    UIWindow *to = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
+    if ((!from || !to) || (from == to)) return [self convertPoint:point toView:view];
+    point = [self convertPoint:point toView:from];
+    point = [to convertPoint:point fromWindow:from];
+    point = [view convertPoint:point fromView:to];
+    return point;
+}
+
+- (CGPoint)yy_convertPoint:(CGPoint)point fromViewOrWindow:(UIView *)view {
+    if (!view) {
+        if ([self isKindOfClass:[UIWindow class]]) {
+            return [((UIWindow *)self) convertPoint:point fromWindow:nil];
+        } else {
+            return [self convertPoint:point fromView:nil];
+        }
+    }
+    
+    UIWindow *from = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
+    UIWindow *to = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window;
+    if ((!from || !to) || (from == to)) return [self convertPoint:point fromView:view];
+    point = [from convertPoint:point fromView:view];
+    point = [to convertPoint:point fromWindow:from];
+    point = [self convertPoint:point fromView:to];
+    return point;
+}
+
+- (CGRect)yy_convertRect:(CGRect)rect toViewOrWindow:(UIView *)view {
+    if (!view) {
+        if ([self isKindOfClass:[UIWindow class]]) {
+            return [((UIWindow *)self) convertRect:rect toWindow:nil];
+        } else {
+            return [self convertRect:rect toView:nil];
+        }
+    }
+    
+    UIWindow *from = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window;
+    UIWindow *to = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
+    if (!from || !to) return [self convertRect:rect toView:view];
+    if (from == to) return [self convertRect:rect toView:view];
+    rect = [self convertRect:rect toView:from];
+    rect = [to convertRect:rect fromWindow:from];
+    rect = [view convertRect:rect fromView:to];
+    return rect;
+}
+
+- (CGRect)yy_convertRect:(CGRect)rect fromViewOrWindow:(UIView *)view {
+    if (!view) {
+        if ([self isKindOfClass:[UIWindow class]]) {
+            return [((UIWindow *)self) convertRect:rect fromWindow:nil];
+        } else {
+            return [self convertRect:rect fromView:nil];
+        }
+    }
+    
+    UIWindow *from = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
+    UIWindow *to = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window;
+    if ((!from || !to) || (from == to)) return [self convertRect:rect fromView:view];
+    rect = [from convertRect:rect fromView:view];
+    rect = [to convertRect:rect fromWindow:from];
+    rect = [self convertRect:rect fromView:to];
+    return rect;
+}
+
+@end

+ 380 - 0
Demo/Objective_C_Demo/YYText/YYLabel.h

@@ -0,0 +1,380 @@
+//
+//  YYLabel.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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(<YYText/YYText.h>)
+#import <YYText/YYTextParser.h>
+#import <YYText/YYTextLayout.h>
+#import <YYText/YYTextAttribute.h>
+#else
+#import "YYTextParser.h"
+#import "YYTextLayout.h"
+#import "YYTextAttribute.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+#if !TARGET_INTERFACE_BUILDER
+
+/**
+ The YYLabel 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 NSAttributedString+YYText.h for more convenience methods to set the attributes.
+ See YYTextAttribute.h and YYTextLayout.h for more information.
+ */
+@interface YYLabel : UIView <NSCoding>
+
+#pragma mark - Accessing the Text Attributes
+///=============================================================================
+/// @name Accessing the Text Attributes
+///=============================================================================
+
+/**
+ 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`.
+ */
+@property (nullable, nonatomic, copy) NSString *text;
+
+/**
+ 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`.
+ */
+@property (null_resettable, nonatomic, strong) UIFont *font;
+
+/**
+ 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`.
+ */
+@property (null_resettable, nonatomic, strong) UIColor *textColor;
+
+/**
+ 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`.
+ */
+@property (nullable, nonatomic, strong) UIColor *shadowColor;
+
+/**
+ 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`.
+ */
+@property (nonatomic) CGSize shadowOffset;
+
+/**
+ 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`.
+ */
+@property (nonatomic) CGFloat shadowBlurRadius;
+
+/**
+ The technique to use for aligning the text. Default is NSLeftTextAlignment.
+ 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`.
+ */
+@property (nonatomic) NSTextAlignment textAlignment;
+
+/**
+ The text vertical aligmnent in container. Default is YYTextVerticalAlignmentCenter.
+ */
+@property (nonatomic) YYTextVerticalAlignment textVerticalAlignment;
+
+/**
+ 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 YYTextAttribute.
+ See `NSAttributedString+YYText` for more convenience methods to set the attributes.
+ */
+@property (nullable, nonatomic, copy) NSAttributedString *attributedText;
+
+/**
+ The technique to use for wrapping and truncating the label's text.
+ Default is NSLineBreakByTruncatingTail.
+ */
+@property (nonatomic) NSLineBreakMode lineBreakMode;
+
+/**
+ The truncation token string used when text is truncated. Default is nil.
+ When the value is nil, the label use "…" as default truncation token.
+ */
+@property (nullable, nonatomic, copy) NSAttributedString *truncationToken;
+
+/**
+ The maximum number of lines to use for rendering text. Default value is 1.
+ 0 means no limit.
+ */
+@property (nonatomic) NSUInteger 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 `YYTextParser` protocol for more information.
+ */
+@property (nullable, nonatomic, strong) id<YYTextParser> textParser;
+
+/**
+ 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.
+ */
+@property (nullable, nonatomic, strong) YYTextLayout *textLayout;
+
+
+#pragma mark - Configuring the Text Container
+///=============================================================================
+/// @name Configuring the Text Container
+///=============================================================================
+
+/**
+ A UIBezierPath object that specifies the shape of the text frame. Default value is nil.
+ */
+@property (nullable, nonatomic, copy) UIBezierPath *textContainerPath;
+
+/**
+ An array of UIBezierPath objects representing the exclusion paths inside the
+ receiver's bounding rectangle. Default value is nil.
+ */
+@property (nullable, nonatomic, copy) NSArray<UIBezierPath *> *exclusionPaths;
+
+/**
+ The inset of the text container's layout area within the text view's content area.
+ Default value is UIEdgeInsetsZero.
+ */
+@property (nonatomic) UIEdgeInsets textContainerInset;
+
+/**
+ Whether the receiver's layout orientation is vertical form. Default is NO.
+ It may used to display CJK text.
+ */
+@property (nonatomic, getter=isVerticalForm) BOOL verticalForm;
+
+/**
+ The text line position modifier used to modify the lines' position in layout.
+ Default value is nil.
+ See `YYTextLinePositionModifier` protocol for more information.
+ */
+@property (nullable, nonatomic, copy) id<YYTextLinePositionModifier> linePositionModifier;
+
+/**
+ The debug option to display CoreText layout result.
+ The default value is [YYTextDebugOption sharedDebugOption].
+ */
+@property (nullable, nonatomic, copy) YYTextDebugOption *debugOption;
+
+
+#pragma 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.
+ */
+@property (nonatomic) CGFloat preferredMaxLayoutWidth;
+
+
+#pragma 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.
+ */
+@property (nullable, nonatomic, copy) YYTextAction textTapAction;
+
+/**
+ When user long press the label, this action will be called (similar to long press gesture).
+ The default value is nil.
+ */
+@property (nullable, nonatomic, copy) YYTextAction textLongPressAction;
+
+/**
+ When user tap the highlight range of text, this action will be called.
+ The default value is nil.
+ */
+@property (nullable, nonatomic, copy) YYTextAction highlightTapAction;
+
+/**
+ When user long press the highlight range of text, this action will be called.
+ The default value is nil. 
+ */
+@property (nullable, nonatomic, copy) YYTextAction highlightLongPressAction;
+
+
+#pragma 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 `NO`.
+ */
+@property (nonatomic) BOOL displaysAsynchronously;
+
+/**
+ If the value is YES, and the layer is rendered asynchronously, then it will
+ set label.layer.contents to nil before display. 
+ 
+ The default value is `YES`.
+ 
+ @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 YES.
+ */
+@property (nonatomic) BOOL clearContentsBeforeAsynchronouslyDisplay;
+
+/**
+ If the value is YES, 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 `YES`.
+ */
+@property (nonatomic) BOOL fadeOnAsynchronouslyDisplay;
+
+/**
+ If the value is YES, then it will add a fade animation on layer when some range
+ of text become highlighted. 
+ 
+ The default value is `YES`.
+ */
+@property (nonatomic) BOOL fadeOnHighlight;
+
+/**
+ Ignore common properties (such as text, font, textColor, attributedText...) and
+ only use "textLayout" to display content. 
+ 
+ The default value is `NO`.
+ 
+ @discussion If you control the label content only through "textLayout", then
+ you may set this value to YES for higher performance.
+ */
+@property (nonatomic) BOOL ignoreCommonProperties;
+
+/*
+ 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 YES.
+ 
+ 3. If you want to get the highest performance, you should do text layout with
+    `YYTextLayout` class in background thread. Here's an example:
+    
+    YYLabel *label = [YYLabel new];
+    label.displaysAsynchronously = YES;
+    label.ignoreCommonProperties = YES;
+    
+    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ 
+        // Create attributed string.
+        NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text"];
+        text.yy_font = [UIFont systemFontOfSize:16];
+        text.yy_color = [UIColor grayColor];
+        [text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];
+ 
+        // Create text container
+        YYTextContainer *container = [YYTextContainer new];
+        container.size = CGSizeMake(100, CGFLOAT_MAX);
+        container.maximumNumberOfRows = 0;
+        
+        // Generate a text layout.
+        YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text];
+        
+        dispatch_async(dispatch_get_main_queue(), ^{
+            label.size = layout.textBoundingSize;
+            label.textLayout = layout;
+        });
+    });
+ 
+ */
+
+@end
+
+
+#else // TARGET_INTERFACE_BUILDER
+IB_DESIGNABLE
+@interface YYLabel : UIView <NSCoding>
+@property (nullable, nonatomic, copy) IBInspectable NSString *text;
+@property (null_resettable, nonatomic, strong) IBInspectable UIColor *textColor;
+@property (nullable, nonatomic, strong) IBInspectable NSString *fontName_;
+@property (nonatomic) IBInspectable CGFloat fontSize_;
+@property (nonatomic) IBInspectable BOOL fontIsBold_;
+@property (nonatomic) IBInspectable NSUInteger numberOfLines;
+@property (nonatomic) IBInspectable NSInteger lineBreakMode;
+@property (nonatomic) IBInspectable CGFloat preferredMaxLayoutWidth;
+@property (nonatomic, getter=isVerticalForm) IBInspectable BOOL verticalForm;
+@property (nonatomic) IBInspectable NSInteger textAlignment;
+@property (nonatomic) IBInspectable NSInteger textVerticalAlignment;
+@property (nullable, nonatomic, strong) IBInspectable UIColor *shadowColor;
+@property (nonatomic) IBInspectable CGPoint shadowOffset;
+@property (nonatomic) IBInspectable CGFloat shadowBlurRadius;
+@property (nullable, nonatomic, copy) IBInspectable NSAttributedString *attributedText;
+@property (nonatomic) IBInspectable CGFloat insetTop_;
+@property (nonatomic) IBInspectable CGFloat insetBottom_;
+@property (nonatomic) IBInspectable CGFloat insetLeft_;
+@property (nonatomic) IBInspectable CGFloat insetRight_;
+@property (nonatomic) IBInspectable BOOL debugEnabled_;
+
+@property (null_resettable, nonatomic, strong) UIFont *font;
+@property (nullable, nonatomic, copy) NSAttributedString *truncationToken;
+@property (nullable, nonatomic, strong) id<YYTextParser> textParser;
+@property (nullable, nonatomic, strong) YYTextLayout *textLayout;
+@property (nullable, nonatomic, copy) UIBezierPath *textContainerPath;
+@property (nullable, nonatomic, copy) NSArray<UIBezierPath*> *exclusionPaths;
+@property (nonatomic) UIEdgeInsets textContainerInset;
+@property (nullable, nonatomic, copy) id<YYTextLinePositionModifier> linePositionModifier;
+@property (nonnull, nonatomic, copy) YYTextDebugOption *debugOption;
+@property (nullable, nonatomic, copy) YYTextAction textTapAction;
+@property (nullable, nonatomic, copy) YYTextAction textLongPressAction;
+@property (nullable, nonatomic, copy) YYTextAction highlightTapAction;
+@property (nullable, nonatomic, copy) YYTextAction highlightLongPressAction;
+@property (nonatomic) BOOL displaysAsynchronously;
+@property (nonatomic) BOOL clearContentsBeforeAsynchronouslyDisplay;
+@property (nonatomic) BOOL fadeOnAsynchronouslyDisplay;
+@property (nonatomic) BOOL fadeOnHighlight;
+@property (nonatomic) BOOL ignoreCommonProperties;
+@end
+#endif // !TARGET_INTERFACE_BUILDER
+
+NS_ASSUME_NONNULL_END

+ 1305 - 0
Demo/Objective_C_Demo/YYText/YYLabel.m

@@ -0,0 +1,1305 @@
+//
+//  YYLabel.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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 "YYLabel.h"
+#import "YYTextAsyncLayer.h"
+#import "YYTextWeakProxy.h"
+#import "YYTextUtilities.h"
+#import "NSAttributedString+YYText.h"
+#import <libkern/OSAtomic.h>
+
+
+static dispatch_queue_t YYLabelGetReleaseQueue() {
+    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
+}
+
+
+#define kLongPressMinimumDuration 0.5 // Time in seconds the fingers must be held down for long press gesture.
+#define kLongPressAllowableMovement 9.0 // Maximum movement in points allowed before the long press fails.
+#define kHighlightFadeDuration 0.15 // Time in seconds for highlight fadeout animation.
+#define kAsyncFadeDuration 0.08 // Time in seconds for async display fadeout animation.
+
+
+@interface YYLabel() <YYTextDebugTarget, YYTextAsyncLayerDelegate> {
+    NSMutableAttributedString *_innerText; ///< nonnull
+    YYTextLayout *_innerLayout;
+    YYTextContainer *_innerContainer; ///< nonnull
+    
+    NSMutableArray *_attachmentViews;
+    NSMutableArray *_attachmentLayers;
+    
+    NSRange _highlightRange; ///< current highlight range
+    YYTextHighlight *_highlight; ///< highlight attribute in `_highlightRange`
+    YYTextLayout *_highlightLayout; ///< when _state.showingHighlight=YES, this layout should be displayed
+    
+    YYTextLayout *_shrinkInnerLayout;
+    YYTextLayout *_shrinkHighlightLayout;
+    
+    NSTimer *_longPressTimer;
+    CGPoint _touchBeganPoint;
+    
+    struct {
+        unsigned int layoutNeedUpdate : 1;
+        unsigned int showingHighlight : 1;
+        
+        unsigned int trackingTouch : 1;
+        unsigned int swallowTouch : 1;
+        unsigned int touchMoved : 1;
+        
+        unsigned int hasTapAction : 1;
+        unsigned int hasLongPressAction : 1;
+        
+        unsigned int contentsNeedFade : 1;
+    } _state;
+}
+@end
+
+
+@implementation YYLabel
+
+#pragma mark - Private
+
+- (void)_updateIfNeeded {
+    if (_state.layoutNeedUpdate) {
+        _state.layoutNeedUpdate = NO;
+        [self _updateLayout];
+        [self.layer setNeedsDisplay];
+    }
+}
+
+- (void)_updateLayout {
+    _innerLayout = [YYTextLayout layoutWithContainer:_innerContainer text:_innerText];
+    _shrinkInnerLayout = [YYLabel _shrinkLayoutWithLayout:_innerLayout];
+}
+
+- (void)_setLayoutNeedUpdate {
+    _state.layoutNeedUpdate = YES;
+    [self _clearInnerLayout];
+    [self _setLayoutNeedRedraw];
+}
+
+- (void)_setLayoutNeedRedraw {
+    [self.layer setNeedsDisplay];
+}
+
+- (void)_clearInnerLayout {
+    if (!_innerLayout) return;
+    YYTextLayout *layout = _innerLayout;
+    _innerLayout = nil;
+    _shrinkInnerLayout = nil;
+    dispatch_async(YYLabelGetReleaseQueue(), ^{
+        NSAttributedString *text = [layout text]; // capture to block and release in background
+        if (layout.attachments.count) {
+            dispatch_async(dispatch_get_main_queue(), ^{
+                [text length]; // capture to block and release in main thread (maybe there's UIView/CALayer attachments).
+            });
+        }
+    });
+}
+
+- (YYTextLayout *)_innerLayout {
+    return _shrinkInnerLayout ? _shrinkInnerLayout : _innerLayout;
+}
+
+- (YYTextLayout *)_highlightLayout {
+    return _shrinkHighlightLayout ? _shrinkHighlightLayout : _highlightLayout;
+}
+
++ (YYTextLayout *)_shrinkLayoutWithLayout:(YYTextLayout *)layout {
+    if (layout.text.length && layout.lines.count == 0) {
+        YYTextContainer *container = layout.container.copy;
+        container.maximumNumberOfRows = 1;
+        CGSize containerSize = container.size;
+        if (!container.verticalForm) {
+            containerSize.height = YYTextContainerMaxSize.height;
+        } else {
+            containerSize.width = YYTextContainerMaxSize.width;
+        }
+        container.size = containerSize;
+        return [YYTextLayout layoutWithContainer:container text:layout.text];
+    } else {
+        return nil;
+    }
+}
+
+- (void)_startLongPressTimer {
+    [_longPressTimer invalidate];
+    _longPressTimer = [NSTimer timerWithTimeInterval:kLongPressMinimumDuration
+                                              target:[YYTextWeakProxy proxyWithTarget:self]
+                                            selector:@selector(_trackDidLongPress)
+                                            userInfo:nil
+                                             repeats:NO];
+    [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes];
+}
+
+- (void)_endLongPressTimer {
+    [_longPressTimer invalidate];
+    _longPressTimer = nil;
+}
+
+- (void)_trackDidLongPress {
+    [self _endLongPressTimer];
+    if (_state.hasLongPressAction && _textLongPressAction) {
+        NSRange range = NSMakeRange(NSNotFound, 0);
+        CGRect rect = CGRectNull;
+        CGPoint point = [self _convertPointToLayout:_touchBeganPoint];
+        YYTextRange *textRange = [self._innerLayout textRangeAtPoint:point];
+        CGRect textRect = [self._innerLayout rectForRange:textRange];
+        textRect = [self _convertRectFromLayout:textRect];
+        if (textRange) {
+            range = textRange.asRange;
+            rect = textRect;
+        }
+        _textLongPressAction(self, _innerText, range, rect);
+    }
+    if (_highlight) {
+        YYTextAction longPressAction = _highlight.longPressAction ? _highlight.longPressAction : _highlightLongPressAction;
+        if (longPressAction) {
+            YYTextPosition *start = [YYTextPosition positionWithOffset:_highlightRange.location];
+            YYTextPosition *end = [YYTextPosition positionWithOffset:_highlightRange.location + _highlightRange.length affinity:YYTextAffinityBackward];
+            YYTextRange *range = [YYTextRange rangeWithStart:start end:end];
+            CGRect rect = [self._innerLayout rectForRange:range];
+            rect = [self _convertRectFromLayout:rect];
+            longPressAction(self, _innerText, _highlightRange, rect);
+            [self _removeHighlightAnimated:YES];
+            _state.trackingTouch = NO;
+        }
+    }
+}
+
+- (YYTextHighlight *)_getHighlightAtPoint:(CGPoint)point range:(NSRangePointer)range {
+    if (!self._innerLayout.containsHighlight) return nil;
+    point = [self _convertPointToLayout:point];
+    YYTextRange *textRange = [self._innerLayout textRangeAtPoint:point];
+    if (!textRange) return nil;
+    
+    NSUInteger startIndex = textRange.start.offset;
+    if (startIndex == _innerText.length) {
+        if (startIndex > 0) {
+            startIndex--;
+        }
+    }
+    NSRange highlightRange = {0};
+    YYTextHighlight *highlight = [_innerText attribute:YYTextHighlightAttributeName
+                                               atIndex:startIndex
+                                 longestEffectiveRange:&highlightRange
+                                               inRange:NSMakeRange(0, _innerText.length)];
+    
+    if (!highlight) return nil;
+    if (range) *range = highlightRange;
+    return highlight;
+}
+
+- (void)_showHighlightAnimated:(BOOL)animated {
+    if (!_highlight) return;
+    if (!_highlightLayout) {
+        NSMutableAttributedString *hiText = _innerText.mutableCopy;
+        NSDictionary *newAttrs = _highlight.attributes;
+        [newAttrs enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+            [hiText yy_setAttribute:key value:value range:_highlightRange];
+        }];
+        _highlightLayout = [YYTextLayout layoutWithContainer:_innerContainer text:hiText];
+        _shrinkHighlightLayout = [YYLabel _shrinkLayoutWithLayout:_highlightLayout];
+        if (!_highlightLayout) _highlight = nil;
+    }
+    
+    if (_highlightLayout && !_state.showingHighlight) {
+        _state.showingHighlight = YES;
+        _state.contentsNeedFade = animated;
+        [self _setLayoutNeedRedraw];
+    }
+}
+
+- (void)_hideHighlightAnimated:(BOOL)animated {
+    if (_state.showingHighlight) {
+        _state.showingHighlight = NO;
+        _state.contentsNeedFade = animated;
+        [self _setLayoutNeedRedraw];
+    }
+}
+
+- (void)_removeHighlightAnimated:(BOOL)animated {
+    [self _hideHighlightAnimated:animated];
+    _highlight = nil;
+    _highlightLayout = nil;
+    _shrinkHighlightLayout = nil;
+}
+
+- (void)_endTouch {
+    [self _endLongPressTimer];
+    [self _removeHighlightAnimated:YES];
+    _state.trackingTouch = NO;
+}
+
+- (CGPoint)_convertPointToLayout:(CGPoint)point {
+    CGSize boundingSize = self._innerLayout.textBoundingSize;
+    if (self._innerLayout.container.isVerticalForm) {
+        CGFloat w = self._innerLayout.textBoundingSize.width;
+        if (w < self.bounds.size.width) w = self.bounds.size.width;
+        point.x += self._innerLayout.container.size.width - w;
+        if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+            point.x += (self.bounds.size.width - boundingSize.width) * 0.5;
+        } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+            point.x += (self.bounds.size.width - boundingSize.width);
+        }
+        return point;
+    } else {
+        if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+            point.y -= (self.bounds.size.height - boundingSize.height) * 0.5;
+        } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+            point.y -= (self.bounds.size.height - boundingSize.height);
+        }
+        return point;
+    }
+}
+
+- (CGPoint)_convertPointFromLayout:(CGPoint)point {
+    CGSize boundingSize = self._innerLayout.textBoundingSize;
+    if (self._innerLayout.container.isVerticalForm) {
+        CGFloat w = self._innerLayout.textBoundingSize.width;
+        if (w < self.bounds.size.width) w = self.bounds.size.width;
+        point.x -= self._innerLayout.container.size.width - w;
+        if (boundingSize.width < self.bounds.size.width) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.x -= (self.bounds.size.width - boundingSize.width) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.x -= (self.bounds.size.width - boundingSize.width);
+            }
+        }
+        return point;
+    } else {
+        if (boundingSize.height < self.bounds.size.height) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.y += (self.bounds.size.height - boundingSize.height) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.y += (self.bounds.size.height - boundingSize.height);
+            }
+        }
+        return point;
+    }
+}
+
+- (CGRect)_convertRectToLayout:(CGRect)rect {
+    rect.origin = [self _convertPointToLayout:rect.origin];
+    return rect;
+}
+
+- (CGRect)_convertRectFromLayout:(CGRect)rect {
+    rect.origin = [self _convertPointFromLayout:rect.origin];
+    return rect;
+}
+
+- (UIFont *)_defaultFont {
+    return [UIFont systemFontOfSize:17];
+}
+
+- (NSShadow *)_shadowFromProperties {
+    if (!_shadowColor || _shadowBlurRadius < 0) return nil;
+    NSShadow *shadow = [NSShadow new];
+    shadow.shadowColor = _shadowColor;
+#if !TARGET_INTERFACE_BUILDER
+    shadow.shadowOffset = _shadowOffset;
+#else
+    shadow.shadowOffset = CGSizeMake(_shadowOffset.x, _shadowOffset.y);
+#endif
+    shadow.shadowBlurRadius = _shadowBlurRadius;
+    return shadow;
+}
+
+- (void)_updateOuterLineBreakMode {
+    if (_innerContainer.truncationType) {
+        switch (_innerContainer.truncationType) {
+            case YYTextTruncationTypeStart: {
+                _lineBreakMode = NSLineBreakByTruncatingHead;
+            } break;
+            case YYTextTruncationTypeEnd: {
+                _lineBreakMode = NSLineBreakByTruncatingTail;
+            } break;
+            case YYTextTruncationTypeMiddle: {
+                _lineBreakMode = NSLineBreakByTruncatingMiddle;
+            } break;
+            default:break;
+        }
+    } else {
+        _lineBreakMode = _innerText.yy_lineBreakMode;
+    }
+}
+
+- (void)_updateOuterTextProperties {
+    _text = [_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)];
+    _font = _innerText.yy_font;
+    if (!_font) _font = [self _defaultFont];
+    _textColor = _innerText.yy_color;
+    if (!_textColor) _textColor = [UIColor blackColor];
+    _textAlignment = _innerText.yy_alignment;
+    _lineBreakMode = _innerText.yy_lineBreakMode;
+    NSShadow *shadow = _innerText.yy_shadow;
+    _shadowColor = shadow.shadowColor;
+#if !TARGET_INTERFACE_BUILDER
+    _shadowOffset = shadow.shadowOffset;
+#else
+    _shadowOffset = CGPointMake(shadow.shadowOffset.width, shadow.shadowOffset.height);
+#endif
+    
+    _shadowBlurRadius = shadow.shadowBlurRadius;
+    _attributedText = _innerText;
+    [self _updateOuterLineBreakMode];
+}
+
+- (void)_updateOuterContainerProperties {
+    _truncationToken = _innerContainer.truncationToken;
+    _numberOfLines = _innerContainer.maximumNumberOfRows;
+    _textContainerPath = _innerContainer.path;
+    _exclusionPaths = _innerContainer.exclusionPaths;
+    _textContainerInset = _innerContainer.insets;
+    _verticalForm = _innerContainer.verticalForm;
+    _linePositionModifier = _innerContainer.linePositionModifier;
+    [self _updateOuterLineBreakMode];
+}
+
+- (void)_clearContents {
+    CGImageRef image = (__bridge_retained CGImageRef)(self.layer.contents);
+    self.layer.contents = nil;
+    if (image) {
+        dispatch_async(YYLabelGetReleaseQueue(), ^{
+            CFRelease(image);
+        });
+    }
+}
+
+- (void)_initLabel {
+    ((YYTextAsyncLayer *)self.layer).displaysAsynchronously = NO;
+    self.layer.contentsScale = [UIScreen mainScreen].scale;
+    self.contentMode = UIViewContentModeRedraw;
+    
+    _attachmentViews = [NSMutableArray new];
+    _attachmentLayers = [NSMutableArray new];
+    
+    _debugOption = [YYTextDebugOption sharedDebugOption];
+    [YYTextDebugOption addDebugTarget:self];
+    
+    _font = [self _defaultFont];
+    _textColor = [UIColor blackColor];
+    _textVerticalAlignment = YYTextVerticalAlignmentCenter;
+    _numberOfLines = 1;
+    _lineBreakMode = NSLineBreakByTruncatingTail;
+    _innerText = [NSMutableAttributedString new];
+    _innerContainer = [YYTextContainer new];
+    _innerContainer.truncationType = YYTextTruncationTypeEnd;
+    _innerContainer.maximumNumberOfRows = _numberOfLines;
+    _clearContentsBeforeAsynchronouslyDisplay = YES;
+    _fadeOnAsynchronouslyDisplay = YES;
+    _fadeOnHighlight = YES;
+    
+    self.isAccessibilityElement = YES;
+}
+
+#pragma mark - Override
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:CGRectZero];
+    if (!self) return nil;
+    self.backgroundColor = [UIColor clearColor];
+    self.opaque = NO;
+    [self _initLabel];
+    self.frame = frame;
+    return self;
+}
+
+- (void)dealloc {
+    [YYTextDebugOption removeDebugTarget:self];
+    [_longPressTimer invalidate];
+}
+
++ (Class)layerClass {
+    return [YYTextAsyncLayer class];
+}
+
+- (void)setFrame:(CGRect)frame {
+    CGSize oldSize = self.bounds.size;
+    [super setFrame:frame];
+    CGSize newSize = self.bounds.size;
+    if (!CGSizeEqualToSize(oldSize, newSize)) {
+        _innerContainer.size = self.bounds.size;
+        if (!_ignoreCommonProperties) {
+            _state.layoutNeedUpdate = YES;
+        }
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedRedraw];
+    }
+}
+
+- (void)setBounds:(CGRect)bounds {
+    CGSize oldSize = self.bounds.size;
+    [super setBounds:bounds];
+    CGSize newSize = self.bounds.size;
+    if (!CGSizeEqualToSize(oldSize, newSize)) {
+        _innerContainer.size = self.bounds.size;
+        if (!_ignoreCommonProperties) {
+            _state.layoutNeedUpdate = YES;
+        }
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedRedraw];
+    }
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    if (_ignoreCommonProperties) {
+        return _innerLayout.textBoundingSize;
+    }
+    
+    if (!_verticalForm && size.width <= 0) size.width = YYTextContainerMaxSize.width;
+    if (_verticalForm && size.height <= 0) size.height = YYTextContainerMaxSize.height;
+    
+    if ((!_verticalForm && size.width == self.bounds.size.width) ||
+        (_verticalForm && size.height == self.bounds.size.height)) {
+        [self _updateIfNeeded];
+        YYTextLayout *layout = self._innerLayout;
+        BOOL contains = NO;
+        if (layout.container.maximumNumberOfRows == 0) {
+            if (layout.truncatedLine == nil) {
+                contains = YES;
+            }
+        } else {
+            if (layout.rowCount <= layout.container.maximumNumberOfRows) {
+                contains = YES;
+            }
+        }
+        if (contains) {
+            return layout.textBoundingSize;
+        }
+    }
+    
+    if (!_verticalForm) {
+        size.height = YYTextContainerMaxSize.height;
+    } else {
+        size.width = YYTextContainerMaxSize.width;
+    }
+    
+    YYTextContainer *container = [_innerContainer copy];
+    container.size = size;
+    
+    YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
+    return layout.textBoundingSize;
+}
+
+- (NSString *)accessibilityLabel {
+    return [_innerLayout.text yy_plainTextForRange:_innerLayout.text.yy_rangeOfAll];
+}
+
+#pragma mark - NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [super encodeWithCoder:aCoder];
+    [aCoder encodeObject:_attributedText forKey:@"attributedText"];
+    [aCoder encodeObject:_innerContainer forKey:@"innerContainer"];
+}
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder {
+    self = [super initWithCoder:aDecoder];
+    [self _initLabel];
+    YYTextContainer *innerContainer = [aDecoder decodeObjectForKey:@"innerContainer"];
+    if (innerContainer) {
+        _innerContainer = innerContainer;
+    } else {
+        _innerContainer.size = self.bounds.size;
+    }
+    [self _updateOuterContainerProperties];
+    self.attributedText = [aDecoder decodeObjectForKey:@"attributedText"];
+    [self _setLayoutNeedUpdate];
+    return self;
+}
+
+#pragma mark - Touches
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _updateIfNeeded];
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:self];
+    
+    _highlight = [self _getHighlightAtPoint:point range:&_highlightRange];
+    _highlightLayout = nil;
+    _shrinkHighlightLayout = nil;
+    _state.hasTapAction = _textTapAction != nil;
+    _state.hasLongPressAction = _textLongPressAction != nil;
+    
+    if (_highlight || _textTapAction || _textLongPressAction) {
+        _touchBeganPoint = point;
+        _state.trackingTouch = YES;
+        _state.swallowTouch = YES;
+        _state.touchMoved = NO;
+        [self _startLongPressTimer];
+        if (_highlight) [self _showHighlightAnimated:NO];
+    } else {
+        _state.trackingTouch = NO;
+        _state.swallowTouch = NO;
+        _state.touchMoved = NO;
+    }
+    if (!_state.swallowTouch) {
+        [super touchesBegan:touches withEvent:event];
+    }
+}
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _updateIfNeeded];
+    
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:self];
+    
+    if (_state.trackingTouch) {
+        if (!_state.touchMoved) {
+            CGFloat moveH = point.x - _touchBeganPoint.x;
+            CGFloat moveV = point.y - _touchBeganPoint.y;
+            if (fabs(moveH) > fabs(moveV)) {
+                if (fabs(moveH) > kLongPressAllowableMovement) _state.touchMoved = YES;
+            } else {
+                if (fabs(moveV) > kLongPressAllowableMovement) _state.touchMoved = YES;
+            }
+            if (_state.touchMoved) {
+                [self _endLongPressTimer];
+            }
+        }
+        if (_state.touchMoved && _highlight) {
+            YYTextHighlight *highlight = [self _getHighlightAtPoint:point range:NULL];
+            if (highlight == _highlight) {
+                [self _showHighlightAnimated:_fadeOnHighlight];
+            } else {
+                [self _hideHighlightAnimated:_fadeOnHighlight];
+            }
+        }
+    }
+    
+    if (!_state.swallowTouch) {
+        [super touchesMoved:touches withEvent:event];
+    }
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:self];
+    
+    if (_state.trackingTouch) {
+        [self _endLongPressTimer];
+        if (!_state.touchMoved && _textTapAction) {
+            NSRange range = NSMakeRange(NSNotFound, 0);
+            CGRect rect = CGRectNull;
+            CGPoint point = [self _convertPointToLayout:_touchBeganPoint];
+            YYTextRange *textRange = [self._innerLayout textRangeAtPoint:point];
+            CGRect textRect = [self._innerLayout rectForRange:textRange];
+            textRect = [self _convertRectFromLayout:textRect];
+            if (textRange) {
+                range = textRange.asRange;
+                rect = textRect;
+            }
+            _textTapAction(self, _innerText, range, rect);
+        }
+        
+        if (_highlight) {
+            if (!_state.touchMoved || [self _getHighlightAtPoint:point range:NULL] == _highlight) {
+                YYTextAction tapAction = _highlight.tapAction ? _highlight.tapAction : _highlightTapAction;
+                if (tapAction) {
+                    YYTextPosition *start = [YYTextPosition positionWithOffset:_highlightRange.location];
+                    YYTextPosition *end = [YYTextPosition positionWithOffset:_highlightRange.location + _highlightRange.length affinity:YYTextAffinityBackward];
+                    YYTextRange *range = [YYTextRange rangeWithStart:start end:end];
+                    CGRect rect = [self._innerLayout rectForRange:range];
+                    rect = [self _convertRectFromLayout:rect];
+                    tapAction(self, _innerText, _highlightRange, rect);
+                }
+            }
+            [self _removeHighlightAnimated:_fadeOnHighlight];
+        }
+    }
+    
+    if (!_state.swallowTouch) {
+        [super touchesEnded:touches withEvent:event];
+    }
+}
+
+- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _endTouch];
+    if (!_state.swallowTouch) [super touchesCancelled:touches withEvent:event];
+}
+
+#pragma mark - Properties
+
+- (void)setText:(NSString *)text {
+    if (_text == text || [_text isEqualToString:text]) return;
+    _text = text.copy;
+    BOOL needAddAttributes = _innerText.length == 0 && text.length > 0;
+    [_innerText replaceCharactersInRange:NSMakeRange(0, _innerText.length) withString:text ? text : @""];
+    [_innerText yy_removeDiscontinuousAttributesInRange:NSMakeRange(0, _innerText.length)];
+    if (needAddAttributes) {
+        _innerText.yy_font = _font;
+        _innerText.yy_color = _textColor;
+        _innerText.yy_shadow = [self _shadowFromProperties];
+        _innerText.yy_alignment = _textAlignment;
+        switch (_lineBreakMode) {
+            case NSLineBreakByWordWrapping:
+            case NSLineBreakByCharWrapping:
+            case NSLineBreakByClipping: {
+                _innerText.yy_lineBreakMode = _lineBreakMode;
+            } break;
+            case NSLineBreakByTruncatingHead:
+            case NSLineBreakByTruncatingTail:
+            case NSLineBreakByTruncatingMiddle: {
+                _innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
+            } break;
+            default: break;
+        }
+    }
+    if ([_textParser parseText:_innerText selectedRange:NULL]) {
+        [self _updateOuterTextProperties];
+    }
+    if (!_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setFont:(UIFont *)font {
+    if (!font) {
+        font = [self _defaultFont];
+    }
+    if (_font == font || [_font isEqual:font]) return;
+    _font = font;
+    _innerText.yy_font = _font;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setTextColor:(UIColor *)textColor {
+    if (!textColor) {
+        textColor = [UIColor blackColor];
+    }
+    if (_textColor == textColor || [_textColor isEqual:textColor]) return;
+    _textColor = textColor;
+    _innerText.yy_color = textColor;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+    }
+}
+
+- (void)setShadowColor:(UIColor *)shadowColor {
+    if (_shadowColor == shadowColor || [_shadowColor isEqual:shadowColor]) return;
+    _shadowColor = shadowColor;
+    _innerText.yy_shadow = [self _shadowFromProperties];
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+    }
+}
+
+#if !TARGET_INTERFACE_BUILDER
+- (void)setShadowOffset:(CGSize)shadowOffset {
+    if (CGSizeEqualToSize(_shadowOffset, shadowOffset)) return;
+    _shadowOffset = shadowOffset;
+    _innerText.yy_shadow = [self _shadowFromProperties];
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+    }
+}
+#else
+- (void)setShadowOffset:(CGPoint)shadowOffset {
+    if (CGPointEqualToPoint(_shadowOffset, shadowOffset)) return;
+    _shadowOffset = shadowOffset;
+    _innerText.yy_shadow = [self _shadowFromProperties];
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+    }
+}
+#endif
+
+- (void)setShadowBlurRadius:(CGFloat)shadowBlurRadius {
+    if (_shadowBlurRadius == shadowBlurRadius) return;
+    _shadowBlurRadius = shadowBlurRadius;
+    _innerText.yy_shadow = [self _shadowFromProperties];
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+    }
+}
+
+- (void)setTextAlignment:(NSTextAlignment)textAlignment {
+    if (_textAlignment == textAlignment) return;
+    _textAlignment = textAlignment;
+    _innerText.yy_alignment = textAlignment;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode {
+    if (_lineBreakMode == lineBreakMode) return;
+    _lineBreakMode = lineBreakMode;
+    _innerText.yy_lineBreakMode = lineBreakMode;
+    // allow multi-line break
+    switch (lineBreakMode) {
+        case NSLineBreakByWordWrapping:
+        case NSLineBreakByCharWrapping:
+        case NSLineBreakByClipping: {
+            _innerContainer.truncationType = YYTextTruncationTypeNone;
+            _innerText.yy_lineBreakMode = lineBreakMode;
+        } break;
+        case NSLineBreakByTruncatingHead:{
+            _innerContainer.truncationType = YYTextTruncationTypeStart;
+            _innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
+        } break;
+        case NSLineBreakByTruncatingTail:{
+            _innerContainer.truncationType = YYTextTruncationTypeEnd;
+            _innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
+        } break;
+        case NSLineBreakByTruncatingMiddle: {
+            _innerContainer.truncationType = YYTextTruncationTypeMiddle;
+            _innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
+        } break;
+        default: break;
+    }
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setTextVerticalAlignment:(YYTextVerticalAlignment)textVerticalAlignment {
+    if (_textVerticalAlignment == textVerticalAlignment) return;
+    _textVerticalAlignment = textVerticalAlignment;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setTruncationToken:(NSAttributedString *)truncationToken {
+    if (_truncationToken == truncationToken || [_truncationToken isEqual:truncationToken]) return;
+    _truncationToken = truncationToken.copy;
+    _innerContainer.truncationToken = truncationToken;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setNumberOfLines:(NSUInteger)numberOfLines {
+    if (_numberOfLines == numberOfLines) return;
+    _numberOfLines = numberOfLines;
+    _innerContainer.maximumNumberOfRows = numberOfLines;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setAttributedText:(NSAttributedString *)attributedText {
+    if (attributedText.length > 0) {
+        _innerText = attributedText.mutableCopy;
+        switch (_lineBreakMode) {
+            case NSLineBreakByWordWrapping:
+            case NSLineBreakByCharWrapping:
+            case NSLineBreakByClipping: {
+                _innerText.yy_lineBreakMode = _lineBreakMode;
+            } break;
+            case NSLineBreakByTruncatingHead:
+            case NSLineBreakByTruncatingTail:
+            case NSLineBreakByTruncatingMiddle: {
+                _innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
+            } break;
+            default: break;
+        }
+    } else {
+        _innerText = [NSMutableAttributedString new];
+    }
+    [_textParser parseText:_innerText selectedRange:NULL];
+    if (!_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _updateOuterTextProperties];
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setTextContainerPath:(UIBezierPath *)textContainerPath {
+    if (_textContainerPath == textContainerPath || [_textContainerPath isEqual:textContainerPath]) return;
+    _textContainerPath = textContainerPath.copy;
+    _innerContainer.path = textContainerPath;
+    if (!_textContainerPath) {
+        _innerContainer.size = self.bounds.size;
+        _innerContainer.insets = _textContainerInset;
+    }
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setExclusionPaths:(NSArray *)exclusionPaths {
+    if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return;
+    _exclusionPaths = exclusionPaths.copy;
+    _innerContainer.exclusionPaths = exclusionPaths;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset {
+    if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return;
+    _textContainerInset = textContainerInset;
+    _innerContainer.insets = textContainerInset;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setVerticalForm:(BOOL)verticalForm {
+    if (_verticalForm == verticalForm) return;
+    _verticalForm = verticalForm;
+    _innerContainer.verticalForm = verticalForm;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
+    if (_linePositionModifier == linePositionModifier) return;
+    _linePositionModifier = linePositionModifier;
+    _innerContainer.linePositionModifier = linePositionModifier;
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+        [self _endTouch];
+        [self invalidateIntrinsicContentSize];
+    }
+}
+
+- (void)setTextParser:(id<YYTextParser>)textParser {
+    if (_textParser == textParser || [_textParser isEqual:textParser]) return;
+    _textParser = textParser;
+    if ([_textParser parseText:_innerText selectedRange:NULL]) {
+        [self _updateOuterTextProperties];
+        if (!_ignoreCommonProperties) {
+            if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+                [self _clearContents];
+            }
+            [self _setLayoutNeedUpdate];
+            [self _endTouch];
+            [self invalidateIntrinsicContentSize];
+        }
+    }
+}
+
+- (void)setTextLayout:(YYTextLayout *)textLayout {
+    _innerLayout = textLayout;
+    _shrinkInnerLayout = nil;
+    
+    if (_ignoreCommonProperties) {
+        _innerText = (NSMutableAttributedString *)textLayout.text;
+        _innerContainer = textLayout.container.copy;
+    } else {
+        _innerText = textLayout.text.mutableCopy;
+        if (!_innerText) {
+            _innerText = [NSMutableAttributedString new];
+        }
+        [self _updateOuterTextProperties];
+        
+        _innerContainer = textLayout.container.copy;
+        if (!_innerContainer) {
+            _innerContainer = [YYTextContainer new];
+            _innerContainer.size = self.bounds.size;
+            _innerContainer.insets = self.textContainerInset;
+        }
+        [self _updateOuterContainerProperties];
+    }
+    
+    if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+        [self _clearContents];
+    }
+    _state.layoutNeedUpdate = NO;
+    [self _setLayoutNeedRedraw];
+    [self _endTouch];
+    [self invalidateIntrinsicContentSize];
+}
+
+- (YYTextLayout *)textLayout {
+    [self _updateIfNeeded];
+    return _innerLayout;
+}
+
+- (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously {
+    _displaysAsynchronously = displaysAsynchronously;
+    ((YYTextAsyncLayer *)self.layer).displaysAsynchronously = displaysAsynchronously;
+}
+
+#pragma mark - AutoLayout
+
+- (void)setPreferredMaxLayoutWidth:(CGFloat)preferredMaxLayoutWidth {
+    if (_preferredMaxLayoutWidth == preferredMaxLayoutWidth) return;
+    _preferredMaxLayoutWidth = preferredMaxLayoutWidth;
+    [self invalidateIntrinsicContentSize];
+}
+
+- (CGSize)intrinsicContentSize {
+    if (_preferredMaxLayoutWidth == 0) {
+        YYTextContainer *container = [_innerContainer copy];
+        container.size = YYTextContainerMaxSize;
+        
+        YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
+        return layout.textBoundingSize;
+    }
+    
+    CGSize containerSize = _innerContainer.size;
+    if (!_verticalForm) {
+        containerSize.height = YYTextContainerMaxSize.height;
+        containerSize.width = _preferredMaxLayoutWidth;
+        if (containerSize.width == 0) containerSize.width = self.bounds.size.width;
+    } else {
+        containerSize.width = YYTextContainerMaxSize.width;
+        containerSize.height = _preferredMaxLayoutWidth;
+        if (containerSize.height == 0) containerSize.height = self.bounds.size.height;
+    }
+    
+    YYTextContainer *container = [_innerContainer copy];
+    container.size = containerSize;
+    
+    YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
+    return layout.textBoundingSize;
+}
+
+#pragma mark - YYTextDebugTarget
+
+- (void)setDebugOption:(YYTextDebugOption *)debugOption {
+    BOOL needDraw = _debugOption.needDrawDebug;
+    _debugOption = debugOption.copy;
+    if (_debugOption.needDrawDebug != needDraw) {
+        [self _setLayoutNeedRedraw];
+    }
+}
+
+#pragma mark - YYTextAsyncLayerDelegate
+
+- (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask {
+    
+    // capture current context
+    BOOL contentsNeedFade = _state.contentsNeedFade;
+    NSAttributedString *text = _innerText;
+    YYTextContainer *container = _innerContainer;
+    YYTextVerticalAlignment verticalAlignment = _textVerticalAlignment;
+    YYTextDebugOption *debug = _debugOption;
+    NSMutableArray *attachmentViews = _attachmentViews;
+    NSMutableArray *attachmentLayers = _attachmentLayers;
+    BOOL layoutNeedUpdate = _state.layoutNeedUpdate;
+    BOOL fadeForAsync = _displaysAsynchronously && _fadeOnAsynchronouslyDisplay;
+    __block YYTextLayout *layout = (_state.showingHighlight && _highlightLayout) ? self._highlightLayout : self._innerLayout;
+    __block YYTextLayout *shrinkLayout = nil;
+    __block BOOL layoutUpdated = NO;
+    if (layoutNeedUpdate) {
+        text = text.copy;
+        container = container.copy;
+    }
+    
+    // create display task
+    YYTextAsyncLayerDisplayTask *task = [YYTextAsyncLayerDisplayTask new];
+    
+    task.willDisplay = ^(CALayer *layer) {
+        [layer removeAnimationForKey:@"contents"];
+        
+        // If the attachment is not in new layout, or we don't know the new layout currently,
+        // the attachment should be removed.
+        for (UIView *view in attachmentViews) {
+            if (layoutNeedUpdate || ![layout.attachmentContentsSet containsObject:view]) {
+                if (view.superview == self) {
+                    [view removeFromSuperview];
+                }
+            }
+        }
+        for (CALayer *layer in attachmentLayers) {
+            if (layoutNeedUpdate || ![layout.attachmentContentsSet containsObject:layer]) {
+                if (layer.superlayer == self.layer) {
+                    [layer removeFromSuperlayer];
+                }
+            }
+        }
+        [attachmentViews removeAllObjects];
+        [attachmentLayers removeAllObjects];
+    };
+
+    task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
+        if (isCancelled()) return;
+        if (text.length == 0) return;
+        
+        YYTextLayout *drawLayout = layout;
+        if (layoutNeedUpdate) {
+            layout = [YYTextLayout layoutWithContainer:container text:text];
+            shrinkLayout = [YYLabel _shrinkLayoutWithLayout:layout];
+            if (isCancelled()) return;
+            layoutUpdated = YES;
+            drawLayout = shrinkLayout ? shrinkLayout : layout;
+        }
+        
+        CGSize boundingSize = drawLayout.textBoundingSize;
+        CGPoint point = CGPointZero;
+        if (verticalAlignment == YYTextVerticalAlignmentCenter) {
+            if (drawLayout.container.isVerticalForm) {
+                point.x = -(size.width - boundingSize.width) * 0.5;
+            } else {
+                point.y = (size.height - boundingSize.height) * 0.5;
+            }
+        } else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
+            if (drawLayout.container.isVerticalForm) {
+                point.x = -(size.width - boundingSize.width);
+            } else {
+                point.y = (size.height - boundingSize.height);
+            }
+        }
+        point = YYTextCGPointPixelRound(point);
+        [drawLayout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled];
+    };
+
+    task.didDisplay = ^(CALayer *layer, BOOL finished) {
+        YYTextLayout *drawLayout = layout;
+        if (layoutUpdated && shrinkLayout) {
+            drawLayout = shrinkLayout;
+        }
+        if (!finished) {
+            // If the display task is cancelled, we should clear the attachments.
+            for (YYTextAttachment *a in drawLayout.attachments) {
+                if ([a.content isKindOfClass:[UIView class]]) {
+                    if (((UIView *)a.content).superview == layer.delegate) {
+                        [((UIView *)a.content) removeFromSuperview];
+                    }
+                } else if ([a.content isKindOfClass:[CALayer class]]) {
+                    if (((CALayer *)a.content).superlayer == layer) {
+                        [((CALayer *)a.content) removeFromSuperlayer];
+                    }
+                }
+            }
+            return;
+        }
+        [layer removeAnimationForKey:@"contents"];
+        
+        YYLabel *view = layer.delegate;
+        if (!view) return;
+        if (view->_state.layoutNeedUpdate && layoutUpdated) {
+            view->_innerLayout = layout;
+            view->_shrinkInnerLayout = shrinkLayout;
+            view->_state.layoutNeedUpdate = NO;
+        }
+        
+        CGSize size = layer.bounds.size;
+        CGSize boundingSize = drawLayout.textBoundingSize;
+        CGPoint point = CGPointZero;
+        if (verticalAlignment == YYTextVerticalAlignmentCenter) {
+            if (drawLayout.container.isVerticalForm) {
+                point.x = -(size.width - boundingSize.width) * 0.5;
+            } else {
+                point.y = (size.height - boundingSize.height) * 0.5;
+            }
+        } else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
+            if (drawLayout.container.isVerticalForm) {
+                point.x = -(size.width - boundingSize.width);
+            } else {
+                point.y = (size.height - boundingSize.height);
+            }
+        }
+        point = YYTextCGPointPixelRound(point);
+        [drawLayout drawInContext:nil size:size point:point view:view layer:layer debug:nil cancel:NULL];
+        for (YYTextAttachment *a in drawLayout.attachments) {
+            if ([a.content isKindOfClass:[UIView class]]) [attachmentViews addObject:a.content];
+            else if ([a.content isKindOfClass:[CALayer class]]) [attachmentLayers addObject:a.content];
+        }
+        
+        if (contentsNeedFade) {
+            CATransition *transition = [CATransition animation];
+            transition.duration = kHighlightFadeDuration;
+            transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+            transition.type = kCATransitionFade;
+            [layer addAnimation:transition forKey:@"contents"];
+        } else if (fadeForAsync) {
+            CATransition *transition = [CATransition animation];
+            transition.duration = kAsyncFadeDuration;
+            transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+            transition.type = kCATransitionFade;
+            [layer addAnimation:transition forKey:@"contents"];
+        }
+    };
+    
+    return task;
+}
+
+@end
+
+
+
+@interface YYLabel(IBInspectableProperties)
+@end
+
+@implementation YYLabel (IBInspectableProperties)
+
+- (BOOL)fontIsBold_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return NO;
+    return (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) > 0;
+}
+
+- (UIFont *)boldFont_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize];
+}
+
+- (UIFont *)normalFont_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:0] size:font.pointSize];
+}
+
+- (void)setFontName_:(NSString *)fontName {
+    if (!fontName) return;
+    UIFont *font = self.font;
+    if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
+        font = [UIFont systemFontOfSize:font.pointSize];
+    } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
+        font = [UIFont boldSystemFontOfSize:font.pointSize];
+    } else {
+        if ([self fontIsBold_:font] && ![fontName.lowercaseString containsString:@"bold"]) {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+            font = [self boldFont_:font];
+        } else {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+        }
+    }
+    if (font) self.font = font;
+}
+
+- (void)setFontSize_:(CGFloat)fontSize {
+    if (fontSize <= 0) return;
+    UIFont *font = self.font;
+    font = [font fontWithSize:fontSize];
+    if (font) self.font = font;
+}
+
+- (void)setFontIsBold_:(BOOL)fontBold {
+    UIFont *font = self.font;
+    if ([self fontIsBold_:font] == fontBold) return;
+    if (fontBold) {
+        font = [self boldFont_:font];
+    } else {
+        font = [self normalFont_:font];
+    }
+    if (font) self.font = font;
+}
+
+- (void)setInsetTop_:(CGFloat)textInsetTop {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.top = textInsetTop;
+    self.textContainerInset = insets;
+}
+
+- (void)setInsetBottom_:(CGFloat)textInsetBottom {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.bottom = textInsetBottom;
+    self.textContainerInset = insets;
+}
+
+- (void)setInsetLeft_:(CGFloat)textInsetLeft {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.left = textInsetLeft;
+    self.textContainerInset = insets;
+    
+}
+
+- (void)setInsetRight_:(CGFloat)textInsetRight {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.right = textInsetRight;
+    self.textContainerInset = insets;
+}
+
+- (void)setDebugEnabled_:(BOOL)enabled {
+    if (!enabled) {
+        self.debugOption = nil;
+    } else {
+        YYTextDebugOption *debugOption = [YYTextDebugOption new];
+        debugOption.baselineColor = [UIColor redColor];
+        debugOption.CTFrameBorderColor = [UIColor redColor];
+        debugOption.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180];
+        debugOption.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200];
+        self.debugOption = debugOption;
+    }
+}
+
+@end

+ 50 - 0
Demo/Objective_C_Demo/YYText/YYText.h

@@ -0,0 +1,50 @@
+//
+//  YYText.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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(<YYText/YYText.h>)
+FOUNDATION_EXPORT double YYTextVersionNumber;
+FOUNDATION_EXPORT const unsigned char YYTextVersionString[];
+#import <YYText/YYLabel.h>
+#import <YYText/YYTextView.h>
+#import <YYText/YYTextAttribute.h>
+#import <YYText/YYTextArchiver.h>
+#import <YYText/YYTextParser.h>
+#import <YYText/YYTextRunDelegate.h>
+#import <YYText/YYTextRubyAnnotation.h>
+#import <YYText/YYTextLayout.h>
+#import <YYText/YYTextLine.h>
+#import <YYText/YYTextInput.h>
+#import <YYText/YYTextDebugOption.h>
+#import <YYText/YYTextKeyboardManager.h>
+#import <YYText/YYTextUtilities.h>
+#import <YYText/NSAttributedString+YYText.h>
+#import <YYText/NSParagraphStyle+YYText.h>
+#import <YYText/UIPasteboard+YYText.h>
+#else
+#import "YYLabel.h"
+#import "YYTextView.h"
+#import "YYTextAttribute.h"
+#import "YYTextArchiver.h"
+#import "YYTextParser.h"
+#import "YYTextRunDelegate.h"
+#import "YYTextRubyAnnotation.h"
+#import "YYTextLayout.h"
+#import "YYTextLine.h"
+#import "YYTextInput.h"
+#import "YYTextDebugOption.h"
+#import "YYTextKeyboardManager.h"
+#import "YYTextUtilities.h"
+#import "NSAttributedString+YYText.h"
+#import "NSParagraphStyle+YYText.h"
+#import "UIPasteboard+YYText.h"
+#endif

+ 33 - 0
Demo/Objective_C_Demo/YYText/YYTextArchiver.h

@@ -0,0 +1,33 @@
+//
+//  YYTextArchiver.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/3/16.
+//  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
+
+/**
+ A subclass of `NSKeyedArchiver` which implement `NSKeyedArchiverDelegate` protocol.
+ 
+ The archiver can encode the object which contains
+ CGColor/CGImage/CTRunDelegateRef/.. (such as NSAttributedString).
+ */
+@interface YYTextArchiver : NSKeyedArchiver <NSKeyedArchiverDelegate>
+@end
+
+/**
+ A subclass of `NSKeyedUnarchiver` which implement `NSKeyedUnarchiverDelegate` 
+ protocol. The unarchiver can decode the data which is encoded by 
+ `YYTextArchiver` or `NSKeyedArchiver`.
+ */
+@interface YYTextUnarchiver : NSKeyedUnarchiver <NSKeyedUnarchiverDelegate>
+@end
+
+NS_ASSUME_NONNULL_END

+ 252 - 0
Demo/Objective_C_Demo/YYText/YYTextArchiver.m

@@ -0,0 +1,252 @@
+//
+//  YYTextArchiver.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/3/16.
+//  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 "YYTextArchiver.h"
+#import "YYTextRunDelegate.h"
+#import "YYTextRubyAnnotation.h"
+
+/**
+ When call CTRunDelegateGetTypeID() on some devices (runs iOS6), I got the error:
+ "dyld: lazy symbol binding failed: Symbol not found: _CTRunDelegateGetTypeID"
+ 
+ Here's a workaround for this issue.
+ */
+static CFTypeID CTRunDelegateTypeID() {
+    static CFTypeID typeID;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        /*
+        if ((long)CTRunDelegateGetTypeID + 1 > 1) { //avoid compiler optimization
+            typeID = CTRunDelegateGetTypeID();
+        }
+         */
+        YYTextRunDelegate *delegate = [YYTextRunDelegate new];
+        CTRunDelegateRef ref = delegate.CTRunDelegate;
+        typeID = CFGetTypeID(ref);
+        CFRelease(ref);
+    });
+    return typeID;
+}
+
+static CFTypeID CTRubyAnnotationTypeID() {
+    static CFTypeID typeID;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        if ((long)CTRubyAnnotationGetTypeID + 1 > 1) { //avoid compiler optimization
+            typeID = CTRunDelegateGetTypeID();
+        } else {
+            typeID = kCFNotFound;
+        }
+    });
+    return typeID;
+}
+
+/**
+ A wrapper for CGColorRef. Used for Archive/Unarchive/Copy.
+ */
+@interface _YYCGColor : NSObject <NSCopying, NSCoding>
+@property (nonatomic, assign) CGColorRef CGColor;
++ (instancetype)colorWithCGColor:(CGColorRef)CGColor;
+@end
+
+@implementation _YYCGColor
+
++ (instancetype)colorWithCGColor:(CGColorRef)CGColor {
+    _YYCGColor *color = [self new];
+    color.CGColor = CGColor;
+    return color;
+}
+
+- (void)setCGColor:(CGColorRef)CGColor {
+    if (_CGColor != CGColor) {
+        if (CGColor) CGColor = (CGColorRef)CFRetain(CGColor);
+        if (_CGColor) CFRelease(_CGColor);
+        _CGColor = CGColor;
+    }
+}
+
+- (void)dealloc {
+    if (_CGColor) CFRelease(_CGColor);
+    _CGColor = NULL;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    _YYCGColor *color = [self.class new];
+    color.CGColor = self.CGColor;
+    return color;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    UIColor *color = [UIColor colorWithCGColor:_CGColor];
+    [aCoder encodeObject:color forKey:@"color"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [self init];
+    UIColor *color = [aDecoder decodeObjectForKey:@"color"];
+    self.CGColor = color.CGColor;
+    return self;
+}
+
+@end
+
+/**
+ A wrapper for CGImageRef. Used for Archive/Unarchive/Copy.
+ */
+@interface _YYCGImage : NSObject <NSCoding, NSCopying>
+@property (nonatomic, assign) CGImageRef CGImage;
++ (instancetype)imageWithCGImage:(CGImageRef)CGImage;
+@end
+
+@implementation _YYCGImage
+
++ (instancetype)imageWithCGImage:(CGImageRef)CGImage {
+    _YYCGImage *image = [self new];
+    image.CGImage = CGImage;
+    return image;
+}
+
+- (void)setCGImage:(CGImageRef)CGImage {
+    if (_CGImage != CGImage) {
+        if (CGImage) CGImage = (CGImageRef)CFRetain(CGImage);
+        if (_CGImage) CFRelease(_CGImage);
+        _CGImage = CGImage;
+    }
+}
+
+- (void)dealloc {
+    if (_CGImage) CFRelease(_CGImage);
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    _YYCGImage *image = [self.class new];
+    image.CGImage = self.CGImage;
+    return image;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    UIImage *image = [UIImage imageWithCGImage:_CGImage];
+    [aCoder encodeObject:image forKey:@"image"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [self init];
+    UIImage *image = [aDecoder decodeObjectForKey:@"image"];
+    self.CGImage = image.CGImage;
+    return self;
+}
+
+@end
+
+
+@implementation YYTextArchiver
+
++ (NSData *)archivedDataWithRootObject:(id)rootObject {
+    if (!rootObject) return nil;
+    NSMutableData *data = [NSMutableData data];
+    YYTextArchiver *archiver = [[[self class] alloc] initForWritingWithMutableData:data];
+    [archiver encodeRootObject:rootObject];
+    [archiver finishEncoding];
+    return data;
+}
+
++ (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path {
+    NSData *data = [self archivedDataWithRootObject:rootObject];
+    if (!data) return NO;
+    return [data writeToFile:path atomically:YES];
+}
+
+- (instancetype)init {
+    self = [super init];
+    self.delegate = self;
+    return self;
+}
+
+- (instancetype)initForWritingWithMutableData:(NSMutableData *)data {
+    self = [super initForWritingWithMutableData:data];
+    self.delegate = self;
+    return self;
+}
+
+- (id)archiver:(NSKeyedArchiver *)archiver willEncodeObject:(id)object {
+    CFTypeID typeID = CFGetTypeID((CFTypeRef)object);
+    if (typeID == CTRunDelegateTypeID()) {
+        CTRunDelegateRef runDelegate = (__bridge CFTypeRef)(object);
+        id ref = CTRunDelegateGetRefCon(runDelegate);
+        if (ref) return ref;
+    } else if (typeID == CTRubyAnnotationTypeID()) {
+        CTRubyAnnotationRef ctRuby = (__bridge CFTypeRef)(object);
+        YYTextRubyAnnotation *ruby = [YYTextRubyAnnotation rubyWithCTRubyRef:ctRuby];
+        if (ruby) return ruby;
+    } else if (typeID == CGColorGetTypeID()) {
+        return [_YYCGColor colorWithCGColor:(CGColorRef)object];
+    } else if (typeID == CGImageGetTypeID()) {
+        return [_YYCGImage imageWithCGImage:(CGImageRef)object];
+    }
+    return object;
+}
+
+@end
+
+
+@implementation YYTextUnarchiver
+
++ (id)unarchiveObjectWithData:(NSData *)data {
+    if (data.length == 0) return nil;
+    YYTextUnarchiver *unarchiver = [[self alloc] initForReadingWithData:data];
+    return [unarchiver decodeObject];
+}
+
++ (id)unarchiveObjectWithFile:(NSString *)path {
+    NSData *data = [NSData dataWithContentsOfFile:path];
+    return [self unarchiveObjectWithData:data];
+}
+
+- (instancetype)init {
+    self = [super init];
+    self.delegate = self;
+    return self;
+}
+
+- (instancetype)initForReadingWithData:(NSData *)data {
+    self = [super initForReadingWithData:data];
+    self.delegate = self;
+    return self;
+}
+
+- (id)unarchiver:(NSKeyedUnarchiver *)unarchiver didDecodeObject:(id) NS_RELEASES_ARGUMENT object NS_RETURNS_RETAINED {
+    if ([object class] == [YYTextRunDelegate class]) {
+        YYTextRunDelegate *runDelegate = object;
+        CTRunDelegateRef ct = runDelegate.CTRunDelegate;
+        id ctObj = (__bridge id)ct;
+        if (ct) CFRelease(ct);
+        return ctObj;
+    } else if ([object class] == [YYTextRubyAnnotation class]) {
+        YYTextRubyAnnotation *ruby = object;
+        if ([UIDevice currentDevice].systemVersion.floatValue >= 8) {
+            CTRubyAnnotationRef ct = ruby.CTRubyAnnotation;
+            id ctObj = (__bridge id)(ct);
+            if (ct) CFRelease(ct);
+            return ctObj;
+        } else {
+            return object;
+        }
+    } else if ([object class] == [_YYCGColor class]) {
+        _YYCGColor *color = object;
+        return (id)color.CGColor;
+    } else if ([object class] == [_YYCGImage class]) {
+        _YYCGImage *image = object;
+        return (id)image.CGImage;
+    }
+    return object;
+}
+
+@end

+ 79 - 0
Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.h

@@ -0,0 +1,79 @@
+//
+//  YYTextAsyncLayer.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/11.
+//  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>
+#import <QuartzCore/QuartzCore.h>
+
+@class YYTextAsyncLayerDisplayTask;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ The YYTextAsyncLayer 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.
+ */
+@interface YYTextAsyncLayer : CALayer
+/// Whether the render code is executed in background. Default is YES.
+@property BOOL displaysAsynchronously;
+@end
+
+
+/**
+ The YYTextAsyncLayer's delegate protocol. The delegate of the YYTextAsyncLayer (typically a UIView)
+ must implements the method in this protocol.
+ */
+@protocol YYTextAsyncLayerDelegate <NSObject>
+@required
+/// This method is called to return a new display task when the layer's contents need update.
+- (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask;
+@end
+
+
+/**
+ A display task used by YYTextAsyncLayer to render the contents in background queue.
+ */
+@interface YYTextAsyncLayerDisplayTask : NSObject
+
+/**
+ This block will be called before the asynchronous drawing begins.
+ It will be called on the main thread.
+ 
+ @param layer  The layer.
+ */
+@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);
+
+/**
+ 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.
+ 
+ @param context      A new bitmap content created by layer.
+ @param size         The content size (typically same as layer's bound size).
+ @param isCancelled  If this block returns `YES`, the method should cancel the
+ drawing process and return as quickly as possible.
+ */
+@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));
+
+/**
+ This block will be called after the asynchronous drawing finished.
+ It will be called on the main thread.
+ 
+ @param layer  The layer.
+ @param finished  If the draw process is cancelled, it's `NO`, otherwise it's `YES`;
+ */
+@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 235 - 0
Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.m

@@ -0,0 +1,235 @@
+//
+//  YYTextAsyncLayer.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/11.
+//  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 "YYTextAsyncLayer.h"
+#import <libkern/OSAtomic.h>
+
+
+/// Global display queue, used for content rendering.
+static dispatch_queue_t YYTextAsyncLayerGetDisplayQueue() {
+#define MAX_QUEUE_COUNT 16
+    static int queueCount;
+    static dispatch_queue_t queues[MAX_QUEUE_COUNT];
+    static dispatch_once_t onceToken;
+    static int32_t counter = 0;
+    dispatch_once(&onceToken, ^{
+        queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
+        queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
+        if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
+            for (NSUInteger i = 0; i < queueCount; i++) {
+                dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
+                queues[i] = dispatch_queue_create("com.ibireme.text.render", attr);
+            }
+        } else {
+            for (NSUInteger i = 0; i < queueCount; i++) {
+                queues[i] = dispatch_queue_create("com.ibireme.text.render", DISPATCH_QUEUE_SERIAL);
+                dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
+            }
+        }
+    });
+    int32_t cur = OSAtomicIncrement32(&counter);
+    if (cur < 0) cur = -cur;
+    return queues[(cur) % queueCount];
+#undef MAX_QUEUE_COUNT
+}
+
+static dispatch_queue_t YYTextAsyncLayerGetReleaseQueue() {
+#ifdef YYDispatchQueuePool_h
+    return YYDispatchQueueGetForQOS(NSQualityOfServiceDefault);
+#else
+    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
+#endif
+}
+
+
+/// a thread safe incrementing counter.
+@interface _YYTextSentinel : NSObject
+/// Returns the current value of the counter.
+@property (atomic, readonly) int32_t value;
+/// Increase the value atomically. @return The new value.
+- (int32_t)increase;
+@end
+
+@implementation _YYTextSentinel {
+    int32_t _value;
+}
+- (int32_t)value {
+    return _value;
+}
+- (int32_t)increase {
+    return OSAtomicIncrement32(&_value);
+}
+@end
+
+
+@implementation YYTextAsyncLayerDisplayTask
+@end
+
+
+@implementation YYTextAsyncLayer {
+    _YYTextSentinel *_sentinel;
+}
+
+#pragma mark - Override
+
++ (id)defaultValueForKey:(NSString *)key {
+    if ([key isEqualToString:@"displaysAsynchronously"]) {
+        return @(YES);
+    } else {
+        return [super defaultValueForKey:key];
+    }
+}
+
+- (instancetype)init {
+    self = [super init];
+    static CGFloat scale; //global
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        scale = [UIScreen mainScreen].scale;
+    });
+    self.contentsScale = scale;
+    _sentinel = [_YYTextSentinel new];
+    _displaysAsynchronously = YES;
+    return self;
+}
+
+- (void)dealloc {
+    [_sentinel increase];
+}
+
+- (void)setNeedsDisplay {
+    [self _cancelAsyncDisplay];
+    [super setNeedsDisplay];
+}
+
+- (void)display {
+    super.contents = super.contents;
+    [self _displayAsync:_displaysAsynchronously];
+}
+
+#pragma mark - Private
+
+- (void)_displayAsync:(BOOL)async {
+    __strong id<YYTextAsyncLayerDelegate> delegate = self.delegate;
+    YYTextAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
+    if (!task.display) {
+        if (task.willDisplay) task.willDisplay(self);
+        self.contents = nil;
+        if (task.didDisplay) task.didDisplay(self, YES);
+        return;
+    }
+    
+    if (async) {
+        if (task.willDisplay) task.willDisplay(self);
+        _YYTextSentinel *sentinel = _sentinel;
+        int32_t value = sentinel.value;
+        BOOL (^isCancelled)() = ^BOOL() {
+            return value != sentinel.value;
+        };
+        CGSize size = self.bounds.size;
+        BOOL opaque = self.opaque;
+        CGFloat scale = self.contentsScale;
+        CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
+        if (size.width < 1 || size.height < 1) {
+            CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
+            self.contents = nil;
+            if (image) {
+                dispatch_async(YYTextAsyncLayerGetReleaseQueue(), ^{
+                    CFRelease(image);
+                });
+            }
+            if (task.didDisplay) task.didDisplay(self, YES);
+            CGColorRelease(backgroundColor);
+            return;
+        }
+        
+        dispatch_async(YYTextAsyncLayerGetDisplayQueue(), ^{
+            if (isCancelled()) {
+                CGColorRelease(backgroundColor);
+                return;
+            }
+            UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
+            CGContextRef context = UIGraphicsGetCurrentContext();
+            if (opaque) {
+                CGContextSaveGState(context); {
+                    if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
+                        CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
+                        CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
+                        CGContextFillPath(context);
+                    }
+                    if (backgroundColor) {
+                        CGContextSetFillColorWithColor(context, backgroundColor);
+                        CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
+                        CGContextFillPath(context);
+                    }
+                } CGContextRestoreGState(context);
+                CGColorRelease(backgroundColor);
+            }
+            task.display(context, size, isCancelled);
+            if (isCancelled()) {
+                UIGraphicsEndImageContext();
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    if (task.didDisplay) task.didDisplay(self, NO);
+                });
+                return;
+            }
+            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+            UIGraphicsEndImageContext();
+            if (isCancelled()) {
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    if (task.didDisplay) task.didDisplay(self, NO);
+                });
+                return;
+            }
+            dispatch_async(dispatch_get_main_queue(), ^{
+                if (isCancelled()) {
+                    if (task.didDisplay) task.didDisplay(self, NO);
+                } else {
+                    self.contents = (__bridge id)(image.CGImage);
+                    if (task.didDisplay) task.didDisplay(self, YES);
+                }
+            });
+        });
+    } else {
+        [_sentinel increase];
+        if (task.willDisplay) task.willDisplay(self);
+        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
+        CGContextRef context = UIGraphicsGetCurrentContext();
+        if (self.opaque) {
+            CGSize size = self.bounds.size;
+            size.width *= self.contentsScale;
+            size.height *= self.contentsScale;
+            CGContextSaveGState(context); {
+                if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {
+                    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
+                    CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
+                    CGContextFillPath(context);
+                }
+                if (self.backgroundColor) {
+                    CGContextSetFillColorWithColor(context, self.backgroundColor);
+                    CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
+                    CGContextFillPath(context);
+                }
+            } CGContextRestoreGState(context);
+        }
+        task.display(context, self.bounds.size, ^{return NO;});
+        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+        UIGraphicsEndImageContext();
+        self.contents = (__bridge id)(image.CGImage);
+        if (task.didDisplay) task.didDisplay(self, YES);
+    }
+}
+
+- (void)_cancelAsyncDisplay {
+    [_sentinel increase];
+}
+
+@end

+ 347 - 0
Demo/Objective_C_Demo/YYText/YYTextAttribute.h

@@ -0,0 +1,347 @@
+//
+//  YYTextAttribute.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/26.
+//  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
+
+#pragma mark - Enum Define
+
+/// The attribute type
+typedef NS_OPTIONS(NSInteger, YYTextAttributeType) {
+    YYTextAttributeTypeNone     = 0,
+    YYTextAttributeTypeUIKit    = 1 << 0, ///< UIKit attributes, such as UILabel/UITextField/drawInRect.
+    YYTextAttributeTypeCoreText = 1 << 1, ///< CoreText attributes, used by CoreText.
+    YYTextAttributeTypeYYText   = 1 << 2, ///< YYText attributes, used by YYText.
+};
+
+/// Get the attribute type from an attribute name.
+extern YYTextAttributeType YYTextAttributeGetType(NSString *attributeName);
+
+/**
+ Line style in YYText (similar to NSUnderlineStyle).
+ */
+typedef NS_OPTIONS (NSInteger, YYTextLineStyle) {
+    // basic style (bitmask:0xFF)
+    YYTextLineStyleNone       = 0x00, ///< (        ) Do not draw a line (Default).
+    YYTextLineStyleSingle     = 0x01, ///< (──────) Draw a single line.
+    YYTextLineStyleThick      = 0x02, ///< (━━━━━━━) Draw a thick line.
+    YYTextLineStyleDouble     = 0x09, ///< (══════) Draw a double line.
+    
+    // style pattern (bitmask:0xF00)
+    YYTextLineStylePatternSolid      = 0x000, ///< (────────) Draw a solid line (Default).
+    YYTextLineStylePatternDot        = 0x100, ///< (‑ ‑ ‑ ‑ ‑ ‑) Draw a line of dots.
+    YYTextLineStylePatternDash       = 0x200, ///< (— — — —) Draw a line of dashes.
+    YYTextLineStylePatternDashDot    = 0x300, ///< (— ‑ — ‑ — ‑) Draw a line of alternating dashes and dots.
+    YYTextLineStylePatternDashDotDot = 0x400, ///< (— ‑ ‑ — ‑ ‑) Draw a line of alternating dashes and two dots.
+    YYTextLineStylePatternCircleDot  = 0x900, ///< (••••••••••••) Draw a line of small circle dots.
+};
+
+/**
+ Text vertical alignment.
+ */
+typedef NS_ENUM(NSInteger, YYTextVerticalAlignment) {
+    YYTextVerticalAlignmentTop =    0, ///< Top alignment.
+    YYTextVerticalAlignmentCenter = 1, ///< Center alignment.
+    YYTextVerticalAlignmentBottom = 2, ///< Bottom alignment.
+};
+
+/**
+ The direction define in YYText.
+ */
+typedef NS_OPTIONS(NSUInteger, YYTextDirection) {
+    YYTextDirectionNone   = 0,
+    YYTextDirectionTop    = 1 << 0,
+    YYTextDirectionRight  = 1 << 1,
+    YYTextDirectionBottom = 1 << 2,
+    YYTextDirectionLeft   = 1 << 3,
+};
+
+/**
+ The trunction type, tells the truncation engine which type of truncation is being requested.
+ */
+typedef NS_ENUM (NSUInteger, YYTextTruncationType) {
+    /// No truncate.
+    YYTextTruncationTypeNone   = 0,
+    
+    /// Truncate at the beginning of the line, leaving the end portion visible.
+    YYTextTruncationTypeStart  = 1,
+    
+    /// Truncate at the end of the line, leaving the start portion visible.
+    YYTextTruncationTypeEnd    = 2,
+    
+    /// Truncate in the middle of the line, leaving both the start and the end portions visible.
+    YYTextTruncationTypeMiddle = 3,
+};
+
+
+
+#pragma mark - Attribute Name Defined in YYText
+
+/// The value of this attribute is a `YYTextBackedString` object.
+/// Use this attribute to store the original plain text if it is replaced by something else (such as attachment).
+UIKIT_EXTERN NSString *const YYTextBackedStringAttributeName;
+
+/// The value of this attribute is a `YYTextBinding` object.
+/// Use this attribute to bind a range of text together, as if it was a single charactor.
+UIKIT_EXTERN NSString *const YYTextBindingAttributeName;
+
+/// The value of this attribute is a `YYTextShadow` object.
+/// Use this attribute to add shadow to a range of text.
+/// Shadow will be drawn below text glyphs. Use YYTextShadow.subShadow to add multi-shadow.
+UIKIT_EXTERN NSString *const YYTextShadowAttributeName;
+
+/// The value of this attribute is a `YYTextShadow` object.
+/// Use this attribute to add inner shadow to a range of text.
+/// Inner shadow will be drawn above text glyphs. Use YYTextShadow.subShadow to add multi-shadow.
+UIKIT_EXTERN NSString *const YYTextInnerShadowAttributeName;
+
+/// The value of this attribute is a `YYTextDecoration` object.
+/// Use this attribute to add underline to a range of text.
+/// The underline will be drawn below text glyphs.
+UIKIT_EXTERN NSString *const YYTextUnderlineAttributeName;
+
+/// The value of this attribute is a `YYTextDecoration` object.
+/// Use this attribute to add strikethrough (delete line) to a range of text.
+/// The strikethrough will be drawn above text glyphs.
+UIKIT_EXTERN NSString *const YYTextStrikethroughAttributeName;
+
+/// The value of this attribute is a `YYTextBorder` 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.
+UIKIT_EXTERN NSString *const YYTextBorderAttributeName;
+
+/// The value of this attribute is a `YYTextBorder` 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.
+UIKIT_EXTERN NSString *const YYTextBackgroundBorderAttributeName;
+
+/// The value of this attribute is a `YYTextBorder` 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.
+UIKIT_EXTERN NSString *const YYTextBlockBorderAttributeName;
+
+/// The value of this attribute is a `YYTextAttachment` object.
+/// Use this attribute to add attachment to text.
+/// It should be used in conjunction with a CTRunDelegate.
+UIKIT_EXTERN NSString *const YYTextAttachmentAttributeName;
+
+/// The value of this attribute is a `YYTextHighlight` object.
+/// Use this attribute to add a touchable highlight state to a range of text.
+UIKIT_EXTERN NSString *const YYTextHighlightAttributeName;
+
+/// 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.
+UIKIT_EXTERN NSString *const YYTextGlyphTransformAttributeName;
+
+
+
+#pragma mark - String Token Define
+
+UIKIT_EXTERN NSString *const YYTextAttachmentToken; ///< Object replacement character (U+FFFC), used for text attachment.
+UIKIT_EXTERN NSString *const YYTextTruncationToken; ///< Horizontal ellipsis (U+2026), used for text truncation  "…".
+
+
+
+#pragma mark - Attribute Value Define
+
+/**
+ The tap/long press action callback defined in YYText.
+ 
+ @param containerView The text container view (such as YYLabel/YYTextView).
+ @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(^YYTextAction)(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect);
+
+
+/**
+ YYTextBackedString 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 YYTextBackedStringAttributeName).
+ 
+ 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 @":)".
+ */
+@interface YYTextBackedString : NSObject <NSCoding, NSCopying>
++ (instancetype)stringWithString:(nullable NSString *)string;
+@property (nullable, nonatomic, copy) NSString *string; ///< backed string
+@end
+
+
+/**
+ YYTextBinding objects are used by the NSAttributedString class cluster
+ as the values for shadow attributes (stored in the attributed string under
+ the key named YYTextBindingAttributeName).
+ 
+ Add this to a range of text will make the specified characters 'binding together'.
+ YYTextView will treat the range of text as a single character during text 
+ selection and edit.
+ */
+@interface YYTextBinding : NSObject <NSCoding, NSCopying>
++ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm;
+@property (nonatomic) BOOL deleteConfirm; ///< confirm the range when delete in YYTextView
+@end
+
+
+/**
+ YYTextShadow objects are used by the NSAttributedString class cluster
+ as the values for shadow attributes (stored in the attributed string under
+ the key named YYTextShadowAttributeName or YYTextInnerShadowAttributeName).
+ 
+ It's similar to `NSShadow`, but offers more options.
+ */
+@interface YYTextShadow : NSObject <NSCoding, NSCopying>
++ (instancetype)shadowWithColor:(nullable UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius;
+
+@property (nullable, nonatomic, strong) UIColor *color; ///< shadow color
+@property (nonatomic) CGSize offset;                    ///< shadow offset
+@property (nonatomic) CGFloat radius;                   ///< shadow blur radius
+@property (nonatomic) CGBlendMode blendMode;            ///< shadow blend mode
+@property (nullable, nonatomic, strong) YYTextShadow *subShadow;  ///< a sub shadow which will be added above the parent shadow
+
++ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow; ///< convert NSShadow to YYTextShadow
+- (NSShadow *)nsShadow; ///< convert YYTextShadow to NSShadow
+@end
+
+
+/**
+ YYTextDecorationLine objects are used by the NSAttributedString class cluster
+ as the values for decoration line attributes (stored in the attributed string under
+ the key named YYTextUnderlineAttributeName or YYTextStrikethroughAttributeName).
+ 
+ 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.
+ */
+@interface YYTextDecoration : NSObject <NSCoding, NSCopying>
++ (instancetype)decorationWithStyle:(YYTextLineStyle)style;
++ (instancetype)decorationWithStyle:(YYTextLineStyle)style width:(nullable NSNumber *)width color:(nullable UIColor *)color;
+@property (nonatomic) YYTextLineStyle style;                   ///< line style
+@property (nullable, nonatomic, strong) NSNumber *width;       ///< line width (nil means automatic width)
+@property (nullable, nonatomic, strong) UIColor *color;        ///< line color (nil means automatic color)
+@property (nullable, nonatomic, strong) YYTextShadow *shadow;  ///< line shadow
+@end
+
+
+/**
+ YYTextBorder objects are used by the NSAttributedString class cluster
+ as the values for border attributes (stored in the attributed string under
+ the key named YYTextBorderAttributeName or YYTextBackgroundBorderAttributeName).
+ 
+ It can be used to draw a border around a range of text, or draw a background
+ to a range of text.
+ 
+ Example:
+    ╭──────╮
+    │ Text │
+    ╰──────╯
+ */
+@interface YYTextBorder : NSObject <NSCoding, NSCopying>
++ (instancetype)borderWithLineStyle:(YYTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(nullable UIColor *)color;
++ (instancetype)borderWithFillColor:(nullable UIColor *)color cornerRadius:(CGFloat)cornerRadius;
+@property (nonatomic) YYTextLineStyle lineStyle;              ///< border line style
+@property (nonatomic) CGFloat strokeWidth;                    ///< border line width
+@property (nullable, nonatomic, strong) UIColor *strokeColor; ///< border line color
+@property (nonatomic) CGLineJoin lineJoin;                    ///< border line join
+@property (nonatomic) UIEdgeInsets insets;                    ///< border insets for text bounds
+@property (nonatomic) CGFloat cornerRadius;                   ///< border corder radius
+@property (nullable, nonatomic, strong) YYTextShadow *shadow; ///< border shadow
+@property (nullable, nonatomic, strong) UIColor *fillColor;   ///< inner fill color
+@end
+
+
+/**
+ YYTextAttachment objects are used by the NSAttributedString class cluster 
+ as the values for attachment attributes (stored in the attributed string under 
+ the key named YYTextAttachmentAttributeName).
+ 
+ When display an attributed string which contains `YYTextAttachment` 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.
+ */
+@interface YYTextAttachment : NSObject<NSCoding, NSCopying>
++ (instancetype)attachmentWithContent:(nullable id)content;
+@property (nullable, nonatomic, strong) id content;             ///< Supported type: UIImage, UIView, CALayer
+@property (nonatomic) UIViewContentMode contentMode;            ///< Content display mode.
+@property (nonatomic) UIEdgeInsets contentInsets;               ///< The insets when drawing content.
+@property (nullable, nonatomic, strong) NSDictionary *userInfo; ///< The user information dictionary.
+@end
+
+
+/**
+ YYTextHighlight objects are used by the NSAttributedString class cluster
+ as the values for touchable highlight attributes (stored in the attributed string
+ under the key named YYTextHighlightAttributeName).
+ 
+ When display an attributed string in `YYLabel` or `YYTextView`, the range of 
+ highlight text can be toucheds down by users. If a range of text is turned into 
+ highlighted state, the `attributes` in `YYTextHighlight` will be used to modify 
+ (set or remove) the original attributes in the range for display.
+ */
+@interface YYTextHighlight : NSObject <NSCoding, NSCopying>
+
+/**
+ Attributes that you can apply to text in an attributed string when highlight.
+ Key:   Same as CoreText/YYText Attribute Name.
+ Value: Modify attribute value when highlight (NSNull for remove attribute).
+ */
+@property (nullable, nonatomic, copy) NSDictionary<NSString *, id> *attributes;
+
+/**
+ 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.
+ */
++ (instancetype)highlightWithAttributes:(nullable NSDictionary<NSString *, id> *)attributes;
+
+/**
+ Convenience methods to create a default highlight with the specifeid background color.
+ 
+ @param color The background border color.
+ */
++ (instancetype)highlightWithBackgroundColor:(nullable UIColor *)color;
+
+// Convenience methods below to set the `attributes`.
+- (void)setFont:(nullable UIFont *)font;
+- (void)setColor:(nullable UIColor *)color;
+- (void)setStrokeWidth:(nullable NSNumber *)width;
+- (void)setStrokeColor:(nullable UIColor *)color;
+- (void)setShadow:(nullable YYTextShadow *)shadow;
+- (void)setInnerShadow:(nullable YYTextShadow *)shadow;
+- (void)setUnderline:(nullable YYTextDecoration *)underline;
+- (void)setStrikethrough:(nullable YYTextDecoration *)strikethrough;
+- (void)setBackgroundBorder:(nullable YYTextBorder *)border;
+- (void)setBorder:(nullable YYTextBorder *)border;
+- (void)setAttachment:(nullable YYTextAttachment *)attachment;
+
+/**
+ The user information dictionary, default is nil.
+ */
+@property (nullable, nonatomic, copy) NSDictionary *userInfo;
+
+/**
+ Tap action when user tap the highlight, default is nil.
+ If the value is nil, YYTextView or YYLabel will ask it's delegate to handle the tap action.
+ */
+@property (nullable, nonatomic, copy) YYTextAction tapAction;
+
+/**
+ Long press action when user long press the highlight, default is nil.
+ If the value is nil, YYTextView or YYLabel will ask it's delegate to handle the long press action.
+ */
+@property (nullable, nonatomic, copy) YYTextAction longPressAction;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 524 - 0
Demo/Objective_C_Demo/YYText/YYTextAttribute.m

@@ -0,0 +1,524 @@
+//
+//  YYTextAttribute.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/26.
+//  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 "YYTextAttribute.h"
+#import <UIKit/UIKit.h>
+#import <CoreText/CoreText.h>
+#import "NSAttributedString+YYText.h"
+#import "YYTextArchiver.h"
+
+
+static double _YYDeviceSystemVersion() {
+    static double version;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        version = [UIDevice currentDevice].systemVersion.doubleValue;
+    });
+    return version;
+}
+
+
+NSString *const YYTextBackedStringAttributeName = @"YYTextBackedString";
+NSString *const YYTextBindingAttributeName = @"YYTextBinding";
+NSString *const YYTextShadowAttributeName = @"YYTextShadow";
+NSString *const YYTextInnerShadowAttributeName = @"YYTextInnerShadow";
+NSString *const YYTextUnderlineAttributeName = @"YYTextUnderline";
+NSString *const YYTextStrikethroughAttributeName = @"YYTextStrikethrough";
+NSString *const YYTextBorderAttributeName = @"YYTextBorder";
+NSString *const YYTextBackgroundBorderAttributeName = @"YYTextBackgroundBorder";
+NSString *const YYTextBlockBorderAttributeName = @"YYTextBlockBorder";
+NSString *const YYTextAttachmentAttributeName = @"YYTextAttachment";
+NSString *const YYTextHighlightAttributeName = @"YYTextHighlight";
+NSString *const YYTextGlyphTransformAttributeName = @"YYTextGlyphTransform";
+
+NSString *const YYTextAttachmentToken = @"\uFFFC";
+NSString *const YYTextTruncationToken = @"\u2026";
+
+
+YYTextAttributeType YYTextAttributeGetType(NSString *name){
+    if (name.length == 0) return YYTextAttributeTypeNone;
+    
+    static NSMutableDictionary *dic;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        dic = [NSMutableDictionary new];
+        NSNumber *All = @(YYTextAttributeTypeUIKit | YYTextAttributeTypeCoreText | YYTextAttributeTypeYYText);
+        NSNumber *CoreText_YYText = @(YYTextAttributeTypeCoreText | YYTextAttributeTypeYYText);
+        NSNumber *UIKit_YYText = @(YYTextAttributeTypeUIKit | YYTextAttributeTypeYYText);
+        NSNumber *UIKit_CoreText = @(YYTextAttributeTypeUIKit | YYTextAttributeTypeCoreText);
+        NSNumber *UIKit = @(YYTextAttributeTypeUIKit);
+        NSNumber *CoreText = @(YYTextAttributeTypeCoreText);
+        NSNumber *YYText = @(YYTextAttributeTypeYYText);
+        
+        dic[NSFontAttributeName] = All;
+        dic[NSKernAttributeName] = All;
+        dic[NSForegroundColorAttributeName] = UIKit;
+        dic[(id)kCTForegroundColorAttributeName] = CoreText;
+        dic[(id)kCTForegroundColorFromContextAttributeName] = CoreText;
+        dic[NSBackgroundColorAttributeName] = UIKit;
+        dic[NSStrokeWidthAttributeName] = All;
+        dic[NSStrokeColorAttributeName] = UIKit;
+        dic[(id)kCTStrokeColorAttributeName] = CoreText_YYText;
+        dic[NSShadowAttributeName] = UIKit_YYText;
+        dic[NSStrikethroughStyleAttributeName] = UIKit;
+        dic[NSUnderlineStyleAttributeName] = UIKit_CoreText;
+        dic[(id)kCTUnderlineColorAttributeName] = CoreText;
+        dic[NSLigatureAttributeName] = All;
+        dic[(id)kCTSuperscriptAttributeName] = UIKit; //it's a CoreText attrubite, but only supported by UIKit...
+        dic[NSVerticalGlyphFormAttributeName] = All;
+        dic[(id)kCTGlyphInfoAttributeName] = CoreText_YYText;
+        dic[(id)kCTCharacterShapeAttributeName] = CoreText_YYText;
+        dic[(id)kCTRunDelegateAttributeName] = CoreText_YYText;
+        dic[(id)kCTBaselineClassAttributeName] = CoreText_YYText;
+        dic[(id)kCTBaselineInfoAttributeName] = CoreText_YYText;
+        dic[(id)kCTBaselineReferenceInfoAttributeName] = CoreText_YYText;
+        dic[(id)kCTWritingDirectionAttributeName] = CoreText_YYText;
+        dic[NSParagraphStyleAttributeName] = All;
+        
+        if (_YYDeviceSystemVersion() >= 7) {
+            dic[NSStrikethroughColorAttributeName] = UIKit;
+            dic[NSUnderlineColorAttributeName] = UIKit;
+            dic[NSTextEffectAttributeName] = UIKit;
+            dic[NSObliquenessAttributeName] = UIKit;
+            dic[NSExpansionAttributeName] = UIKit;
+            dic[(id)kCTLanguageAttributeName] = CoreText_YYText;
+            dic[NSBaselineOffsetAttributeName] = UIKit;
+            dic[NSWritingDirectionAttributeName] = All;
+            dic[NSAttachmentAttributeName] = UIKit;
+            dic[NSLinkAttributeName] = UIKit;
+        }
+        if (_YYDeviceSystemVersion() >= 8) {
+            dic[(id)kCTRubyAnnotationAttributeName] = CoreText;
+        }
+        
+        dic[YYTextBackedStringAttributeName] = YYText;
+        dic[YYTextBindingAttributeName] = YYText;
+        dic[YYTextShadowAttributeName] = YYText;
+        dic[YYTextInnerShadowAttributeName] = YYText;
+        dic[YYTextUnderlineAttributeName] = YYText;
+        dic[YYTextStrikethroughAttributeName] = YYText;
+        dic[YYTextBorderAttributeName] = YYText;
+        dic[YYTextBackgroundBorderAttributeName] = YYText;
+        dic[YYTextBlockBorderAttributeName] = YYText;
+        dic[YYTextAttachmentAttributeName] = YYText;
+        dic[YYTextHighlightAttributeName] = YYText;
+        dic[YYTextGlyphTransformAttributeName] = YYText;
+    });
+    NSNumber *num = dic[name];
+    if (num) return num.integerValue;
+    return YYTextAttributeTypeNone;
+}
+
+
+@implementation YYTextBackedString
+
++ (instancetype)stringWithString:(NSString *)string {
+    YYTextBackedString *one = [self new];
+    one.string = string;
+    return one;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:self.string forKey:@"string"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    _string = [aDecoder decodeObjectForKey:@"string"];
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    typeof(self) one = [self.class new];
+    one.string = self.string;
+    return one;
+}
+
+@end
+
+
+@implementation YYTextBinding
+
++ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm {
+    YYTextBinding *one = [self new];
+    one.deleteConfirm = deleteConfirm;
+    return one;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:@(self.deleteConfirm) forKey:@"deleteConfirm"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    _deleteConfirm = ((NSNumber *)[aDecoder decodeObjectForKey:@"deleteConfirm"]).boolValue;
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    typeof(self) one = [self.class new];
+    one.deleteConfirm = self.deleteConfirm;
+    return one;
+}
+
+@end
+
+
+@implementation YYTextShadow
+
++ (instancetype)shadowWithColor:(UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius {
+    YYTextShadow *one = [self new];
+    one.color = color;
+    one.offset = offset;
+    one.radius = radius;
+    return one;
+}
+
++ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow {
+    if (!nsShadow) return nil;
+    YYTextShadow *shadow = [self new];
+    shadow.offset = nsShadow.shadowOffset;
+    shadow.radius = nsShadow.shadowBlurRadius;
+    id color = nsShadow.shadowColor;
+    if (color) {
+        if (CGColorGetTypeID() == CFGetTypeID((__bridge CFTypeRef)(color))) {
+            color = [UIColor colorWithCGColor:(__bridge CGColorRef)(color)];
+        }
+        if ([color isKindOfClass:[UIColor class]]) {
+            shadow.color = color;
+        }
+    }
+    return shadow;
+}
+
+- (NSShadow *)nsShadow {
+    NSShadow *shadow = [NSShadow new];
+    shadow.shadowOffset = self.offset;
+    shadow.shadowBlurRadius = self.radius;
+    shadow.shadowColor = self.color;
+    return shadow;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:self.color forKey:@"color"];
+    [aCoder encodeObject:@(self.radius) forKey:@"radius"];
+    [aCoder encodeObject:[NSValue valueWithCGSize:self.offset] forKey:@"offset"];
+    [aCoder encodeObject:self.subShadow forKey:@"subShadow"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    _color = [aDecoder decodeObjectForKey:@"color"];
+    _radius = ((NSNumber *)[aDecoder decodeObjectForKey:@"radius"]).floatValue;
+    _offset = ((NSValue *)[aDecoder decodeObjectForKey:@"offset"]).CGSizeValue;
+    _subShadow = [aDecoder decodeObjectForKey:@"subShadow"];
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    typeof(self) one = [self.class new];
+    one.color = self.color;
+    one.radius = self.radius;
+    one.offset = self.offset;
+    one.subShadow = self.subShadow.copy;
+    return one;
+}
+
+@end
+
+
+@implementation YYTextDecoration
+
+- (instancetype)init {
+    self = [super init];
+    _style = YYTextLineStyleSingle;
+    return self;
+}
+
++ (instancetype)decorationWithStyle:(YYTextLineStyle)style {
+    YYTextDecoration *one = [self new];
+    one.style = style;
+    return one;
+}
++ (instancetype)decorationWithStyle:(YYTextLineStyle)style width:(NSNumber *)width color:(UIColor *)color {
+    YYTextDecoration *one = [self new];
+    one.style = style;
+    one.width = width;
+    one.color = color;
+    return one;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:@(self.style) forKey:@"style"];
+    [aCoder encodeObject:self.width forKey:@"width"];
+    [aCoder encodeObject:self.color forKey:@"color"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    self.style = ((NSNumber *)[aDecoder decodeObjectForKey:@"style"]).unsignedIntegerValue;
+    self.width = [aDecoder decodeObjectForKey:@"width"];
+    self.color = [aDecoder decodeObjectForKey:@"color"];
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    typeof(self) one = [self.class new];
+    one.style = self.style;
+    one.width = self.width;
+    one.color = self.color;
+    return one;
+}
+
+@end
+
+
+@implementation YYTextBorder
+
++ (instancetype)borderWithLineStyle:(YYTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(UIColor *)color {
+    YYTextBorder *one = [self new];
+    one.lineStyle = lineStyle;
+    one.strokeWidth = width;
+    one.strokeColor = color;
+    return one;
+}
+
++ (instancetype)borderWithFillColor:(UIColor *)color cornerRadius:(CGFloat)cornerRadius {
+    YYTextBorder *one = [self new];
+    one.fillColor = color;
+    one.cornerRadius = cornerRadius;
+    one.insets = UIEdgeInsetsMake(-2, 0, 0, -2);
+    return one;
+}
+
+- (instancetype)init {
+    self = [super init];
+    self.lineStyle = YYTextLineStyleSingle;
+    return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:@(self.lineStyle) forKey:@"lineStyle"];
+    [aCoder encodeObject:@(self.strokeWidth) forKey:@"strokeWidth"];
+    [aCoder encodeObject:self.strokeColor forKey:@"strokeColor"];
+    [aCoder encodeObject:@(self.lineJoin) forKey:@"lineJoin"];
+    [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:self.insets] forKey:@"insets"];
+    [aCoder encodeObject:@(self.cornerRadius) forKey:@"cornerRadius"];
+    [aCoder encodeObject:self.shadow forKey:@"shadow"];
+    [aCoder encodeObject:self.fillColor forKey:@"fillColor"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    _lineStyle = ((NSNumber *)[aDecoder decodeObjectForKey:@"lineStyle"]).unsignedIntegerValue;
+    _strokeWidth = ((NSNumber *)[aDecoder decodeObjectForKey:@"strokeWidth"]).doubleValue;
+    _strokeColor = [aDecoder decodeObjectForKey:@"strokeColor"];
+    _lineJoin = (CGLineJoin)((NSNumber *)[aDecoder decodeObjectForKey:@"join"]).unsignedIntegerValue;
+    _insets = ((NSValue *)[aDecoder decodeObjectForKey:@"insets"]).UIEdgeInsetsValue;
+    _cornerRadius = ((NSNumber *)[aDecoder decodeObjectForKey:@"cornerRadius"]).doubleValue;
+    _shadow = [aDecoder decodeObjectForKey:@"shadow"];
+    _fillColor = [aDecoder decodeObjectForKey:@"fillColor"];
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    typeof(self) one = [self.class new];
+    one.lineStyle = self.lineStyle;
+    one.strokeWidth = self.strokeWidth;
+    one.strokeColor = self.strokeColor;
+    one.lineJoin = self.lineJoin;
+    one.insets = self.insets;
+    one.cornerRadius = self.cornerRadius;
+    one.shadow = self.shadow.copy;
+    one.fillColor = self.fillColor;
+    return one;
+}
+
+@end
+
+
+@implementation YYTextAttachment
+
++ (instancetype)attachmentWithContent:(id)content {
+    YYTextAttachment *one = [self new];
+    one.content = content;
+    return one;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:self.content forKey:@"content"];
+    [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:self.contentInsets] forKey:@"contentInsets"];
+    [aCoder encodeObject:self.userInfo forKey:@"userInfo"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    _content = [aDecoder decodeObjectForKey:@"content"];
+    _contentInsets = ((NSValue *)[aDecoder decodeObjectForKey:@"contentInsets"]).UIEdgeInsetsValue;
+    _userInfo = [aDecoder decodeObjectForKey:@"userInfo"];
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    typeof(self) one = [self.class new];
+    if ([self.content respondsToSelector:@selector(copy)]) {
+        one.content = [self.content copy];
+    } else {
+        one.content = self.content;
+    }
+    one.contentInsets = self.contentInsets;
+    one.userInfo = self.userInfo.copy;
+    return one;
+}
+
+@end
+
+
+@implementation YYTextHighlight
+
++ (instancetype)highlightWithAttributes:(NSDictionary *)attributes {
+    YYTextHighlight *one = [self new];
+    one.attributes = attributes;
+    return one;
+}
+
++ (instancetype)highlightWithBackgroundColor:(UIColor *)color {
+    YYTextBorder *highlightBorder = [YYTextBorder new];
+    highlightBorder.insets = UIEdgeInsetsMake(-2, -1, -2, -1);
+    highlightBorder.cornerRadius = 3;
+    highlightBorder.fillColor = color;
+    
+    YYTextHighlight *one = [self new];
+    [one setBackgroundBorder:highlightBorder];
+    return one;
+}
+
+- (void)setAttributes:(NSDictionary *)attributes {
+    _attributes = attributes.mutableCopy;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    NSData *data = nil;
+    @try {
+        data = [YYTextArchiver archivedDataWithRootObject:self.attributes];
+    }
+    @catch (NSException *exception) {
+        NSLog(@"%@",exception);
+    }
+    [aCoder encodeObject:data forKey:@"attributes"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    NSData *data = [aDecoder decodeObjectForKey:@"attributes"];
+    @try {
+        _attributes = [YYTextUnarchiver unarchiveObjectWithData:data];
+    }
+    @catch (NSException *exception) {
+        NSLog(@"%@",exception);
+    }
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    typeof(self) one = [self.class new];
+    one.attributes = self.attributes.mutableCopy;
+    return one;
+}
+
+- (void)_makeMutableAttributes {
+    if (!_attributes) {
+        _attributes = [NSMutableDictionary new];
+    } else if (![_attributes isKindOfClass:[NSMutableDictionary class]]) {
+        _attributes = _attributes.mutableCopy;
+    }
+}
+
+- (void)setFont:(UIFont *)font {
+    [self _makeMutableAttributes];
+    if (font == (id)[NSNull null] || font == nil) {
+        ((NSMutableDictionary *)_attributes)[(id)kCTFontAttributeName] = [NSNull null];
+    } else {
+        CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
+        if (ctFont) {
+            ((NSMutableDictionary *)_attributes)[(id)kCTFontAttributeName] = (__bridge id)(ctFont);
+            CFRelease(ctFont);
+        }
+    }
+}
+
+- (void)setColor:(UIColor *)color {
+    [self _makeMutableAttributes];
+    if (color == (id)[NSNull null] || color == nil) {
+        ((NSMutableDictionary *)_attributes)[(id)kCTForegroundColorAttributeName] = [NSNull null];
+        ((NSMutableDictionary *)_attributes)[NSForegroundColorAttributeName] = [NSNull null];
+    } else {
+        ((NSMutableDictionary *)_attributes)[(id)kCTForegroundColorAttributeName] = (__bridge id)(color.CGColor);
+        ((NSMutableDictionary *)_attributes)[NSForegroundColorAttributeName] = color;
+    }
+}
+
+- (void)setStrokeWidth:(NSNumber *)width {
+    [self _makeMutableAttributes];
+    if (width == (id)[NSNull null] || width == nil) {
+        ((NSMutableDictionary *)_attributes)[(id)kCTStrokeWidthAttributeName] = [NSNull null];
+    } else {
+        ((NSMutableDictionary *)_attributes)[(id)kCTStrokeWidthAttributeName] = width;
+    }
+}
+
+- (void)setStrokeColor:(UIColor *)color {
+    [self _makeMutableAttributes];
+    if (color == (id)[NSNull null] || color == nil) {
+        ((NSMutableDictionary *)_attributes)[(id)kCTStrokeColorAttributeName] = [NSNull null];
+        ((NSMutableDictionary *)_attributes)[NSStrokeColorAttributeName] = [NSNull null];
+    } else {
+        ((NSMutableDictionary *)_attributes)[(id)kCTStrokeColorAttributeName] = (__bridge id)(color.CGColor);
+        ((NSMutableDictionary *)_attributes)[NSStrokeColorAttributeName] = color;
+    }
+}
+
+- (void)setTextAttribute:(NSString *)attribute value:(id)value {
+    [self _makeMutableAttributes];
+    if (value == nil) value = [NSNull null];
+    ((NSMutableDictionary *)_attributes)[attribute] = value;
+}
+
+- (void)setShadow:(YYTextShadow *)shadow {
+    [self setTextAttribute:YYTextShadowAttributeName value:shadow];
+}
+
+- (void)setInnerShadow:(YYTextShadow *)shadow {
+    [self setTextAttribute:YYTextInnerShadowAttributeName value:shadow];
+}
+
+- (void)setUnderline:(YYTextDecoration *)underline {
+    [self setTextAttribute:YYTextUnderlineAttributeName value:underline];
+}
+
+- (void)setStrikethrough:(YYTextDecoration *)strikethrough {
+    [self setTextAttribute:YYTextStrikethroughAttributeName value:strikethrough];
+}
+
+- (void)setBackgroundBorder:(YYTextBorder *)border {
+    [self setTextAttribute:YYTextBackgroundBorderAttributeName value:border];
+}
+
+- (void)setBorder:(YYTextBorder *)border {
+    [self setTextAttribute:YYTextBorderAttributeName value:border];
+}
+
+- (void)setAttachment:(YYTextAttachment *)attachment {
+    [self setTextAttribute:YYTextAttachmentAttributeName value:attachment];
+}
+
+@end
+

+ 55 - 0
Demo/Objective_C_Demo/YYText/YYTextContainerView.h

@@ -0,0 +1,55 @@
+//
+//  YYTextContainerView.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/21.
+//  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(<YYText/YYText.h>)
+#import <YYText/YYTextLayout.h>
+#else
+#import "YYTextLayout.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ A simple view to diaplay `YYTextLayout`.
+
+ @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.
+ */
+@interface YYTextContainerView : UIView
+
+/// First responder's aciton will forward to this view.
+@property (nullable, nonatomic, weak) UIView *hostView;
+
+/// Debug option for layout debug. Set this property will let the view redraw it's contents.
+@property (nullable, nonatomic, copy) YYTextDebugOption *debugOption;
+
+/// Text vertical alignment.
+@property (nonatomic) YYTextVerticalAlignment textVerticalAlignment;
+
+/// Text layout. Set this property will let the view redraw it's contents.
+@property (nullable, nonatomic, strong) YYTextLayout *layout;
+
+/// The contents fade animation duration when the layout's contents changed. Default is 0 (no animation).
+@property (nonatomic) NSTimeInterval contentsFadeDuration;
+
+/// Convenience method to set `layout` and `contentsFadeDuration`.
+/// @param layout  Same as `layout` property.
+/// @param fadeDuration  Same as `contentsFadeDuration` property.
+- (void)setLayout:(nullable YYTextLayout *)layout withFadeDuration:(NSTimeInterval)fadeDuration;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 144 - 0
Demo/Objective_C_Demo/YYText/YYTextContainerView.m

@@ -0,0 +1,144 @@
+//
+//  YYTextContainerView.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/21.
+//  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 "YYTextContainerView.h"
+
+@implementation YYTextContainerView {
+    BOOL _attachmentChanged;
+    NSMutableArray *_attachmentViews;
+    NSMutableArray *_attachmentLayers;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    self.backgroundColor = [UIColor clearColor];
+    _attachmentViews = [NSMutableArray array];
+    _attachmentLayers = [NSMutableArray array];
+    return self;
+}
+
+- (void)setDebugOption:(YYTextDebugOption *)debugOption {
+    BOOL needDraw = _debugOption.needDrawDebug;
+    _debugOption = debugOption.copy;
+    if (_debugOption.needDrawDebug != needDraw) {
+        [self setNeedsDisplay];
+    }
+}
+
+- (void)setTextVerticalAlignment:(YYTextVerticalAlignment)textVerticalAlignment {
+    if (_textVerticalAlignment == textVerticalAlignment) return;
+    _textVerticalAlignment = textVerticalAlignment;
+    [self setNeedsDisplay];
+}
+
+- (void)setContentsFadeDuration:(NSTimeInterval)contentsFadeDuration {
+    if (_contentsFadeDuration == contentsFadeDuration) return;
+    _contentsFadeDuration = contentsFadeDuration;
+    if (contentsFadeDuration <= 0) {
+        [self.layer removeAnimationForKey:@"contents"];
+    }
+}
+
+- (void)setLayout:(YYTextLayout *)layout {
+    if (_layout == layout) return;
+    _layout = layout;
+    _attachmentChanged = YES;
+    [self setNeedsDisplay];
+}
+
+- (void)setLayout:(YYTextLayout *)layout withFadeDuration:(NSTimeInterval)fadeDuration {
+    self.contentsFadeDuration = fadeDuration;
+    self.layout = layout;
+}
+
+- (void)drawRect:(CGRect)rect {
+    // fade content
+    [self.layer removeAnimationForKey:@"contents"];
+    if (_contentsFadeDuration > 0) {
+        CATransition *transition = [CATransition animation];
+        transition.duration = _contentsFadeDuration;
+        transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+        transition.type = kCATransitionFade;
+        [self.layer addAnimation:transition forKey:@"contents"];
+    }
+    
+    // update attachment
+    if (_attachmentChanged) {
+        for (UIView *view in _attachmentViews) {
+            if (view.superview == self) [view removeFromSuperview];
+        }
+        for (CALayer *layer in _attachmentLayers) {
+            if (layer.superlayer == self.layer) [layer removeFromSuperlayer];
+        }
+        [_attachmentViews removeAllObjects];
+        [_attachmentLayers removeAllObjects];
+    }
+    
+    // draw layout
+    CGSize boundingSize = _layout.textBoundingSize;
+    CGPoint point = CGPointZero;
+    if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+        if (_layout.container.isVerticalForm) {
+            point.x = -(self.bounds.size.width - boundingSize.width) * 0.5;
+        } else {
+            point.y = (self.bounds.size.height - boundingSize.height) * 0.5;
+        }
+    } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+        if (_layout.container.isVerticalForm) {
+            point.x = -(self.bounds.size.width - boundingSize.width);
+        } else {
+            point.y = (self.bounds.size.height - boundingSize.height);
+        }
+    }
+    [_layout drawInContext:UIGraphicsGetCurrentContext() size:self.bounds.size point:point view:self layer:self.layer debug:_debugOption cancel:nil];
+    
+    // update attachment
+    if (_attachmentChanged) {
+        _attachmentChanged = NO;
+        for (YYTextAttachment *a in _layout.attachments) {
+            if ([a.content isKindOfClass:[UIView class]]) [_attachmentViews addObject:a.content];
+            if ([a.content isKindOfClass:[CALayer class]]) [_attachmentLayers addObject:a.content];
+        }
+    }
+}
+
+- (void)setFrame:(CGRect)frame {
+    CGSize oldSize = self.bounds.size;
+    [super setFrame:frame];
+    if (!CGSizeEqualToSize(oldSize, self.bounds.size)) {
+        [self setNeedsLayout];
+    }
+}
+
+- (void)setBounds:(CGRect)bounds {
+    CGSize oldSize = self.bounds.size;
+    [super setBounds:bounds];
+    if (!CGSizeEqualToSize(oldSize, self.bounds.size)) {
+        [self setNeedsLayout];
+    }
+}
+
+#pragma mark - UIResponder forward
+
+- (BOOL)canBecomeFirstResponder {
+    return YES;
+}
+
+- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
+    return [self.hostView canPerformAction:action withSender:sender];
+}
+
+- (id)forwardingTargetForSelector:(SEL)aSelector {
+    return self.hostView;
+}
+
+@end

+ 95 - 0
Demo/Objective_C_Demo/YYText/YYTextDebugOption.h

@@ -0,0 +1,95 @@
+//
+//  YYTextDebugOption.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/8.
+//  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>
+
+@class YYTextDebugOption;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ The YYTextDebugTarget 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.
+ */
+@protocol YYTextDebugTarget <NSObject>
+
+@required
+/**
+ 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.
+ 
+ @param option  The shared debug option.
+ */
+- (void)setDebugOption:(nullable YYTextDebugOption *)option;
+@end
+
+
+
+/**
+ The debug option for YYText.
+ */
+@interface YYTextDebugOption : NSObject <NSCopying>
+@property (nullable, nonatomic, strong) UIColor *baselineColor;      ///< baseline color
+@property (nullable, nonatomic, strong) UIColor *CTFrameBorderColor; ///< CTFrame path border color
+@property (nullable, nonatomic, strong) UIColor *CTFrameFillColor;   ///< CTFrame path fill color
+@property (nullable, nonatomic, strong) UIColor *CTLineBorderColor;  ///< CTLine bounds border color
+@property (nullable, nonatomic, strong) UIColor *CTLineFillColor;    ///< CTLine bounds fill color
+@property (nullable, nonatomic, strong) UIColor *CTLineNumberColor;  ///< CTLine line number color
+@property (nullable, nonatomic, strong) UIColor *CTRunBorderColor;   ///< CTRun bounds border color
+@property (nullable, nonatomic, strong) UIColor *CTRunFillColor;     ///< CTRun bounds fill color
+@property (nullable, nonatomic, strong) UIColor *CTRunNumberColor;   ///< CTRun number color
+@property (nullable, nonatomic, strong) UIColor *CGGlyphBorderColor; ///< CGGlyph bounds border color
+@property (nullable, nonatomic, strong) UIColor *CGGlyphFillColor;   ///< CGGlyph bounds fill color
+
+- (BOOL)needDrawDebug; ///< `YES`: at least one debug color is visible. `NO`: all debug color is invisible/nil.
+- (void)clear; ///< Set all debug color to 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.
+ */
++ (void)addDebugTarget:(id<YYTextDebugTarget>)target;
+
+/**
+ Remove a debug target which is added by `addDebugTarget:`.
+ 
+ @param target A debug target.
+ */
++ (void)removeDebugTarget:(id<YYTextDebugTarget>)target;
+
+/**
+ Returns the shared debug option.
+ 
+ @return The shared debug option, default is nil.
+ */
++ (nullable YYTextDebugOption *)sharedDebugOption;
+
+/**
+ 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).
+ */
++ (void)setSharedDebugOption:(nullable YYTextDebugOption *)option;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 140 - 0
Demo/Objective_C_Demo/YYText/YYTextDebugOption.m

@@ -0,0 +1,140 @@
+//
+//  YYTextDebugOption.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/8.
+//  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 "YYTextDebugOption.h"
+#import "YYTextWeakProxy.h"
+#import <libkern/OSAtomic.h>
+#import <pthread.h>
+
+static pthread_mutex_t _sharedDebugLock;
+static CFMutableSetRef _sharedDebugTargets = nil;
+static YYTextDebugOption *_sharedDebugOption = nil;
+
+static const void* _sharedDebugSetRetain(CFAllocatorRef allocator, const void *value) {
+    return value;
+}
+
+static void _sharedDebugSetRelease(CFAllocatorRef allocator, const void *value) {
+}
+
+void _sharedDebugSetFunction(const void *value, void *context) {
+    id<YYTextDebugTarget> target = (__bridge id<YYTextDebugTarget>)(value);
+    [target setDebugOption:_sharedDebugOption];
+}
+
+static void _initSharedDebug() {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        pthread_mutex_init(&_sharedDebugLock, NULL);
+        CFSetCallBacks callbacks = kCFTypeSetCallBacks;
+        callbacks.retain = _sharedDebugSetRetain;
+        callbacks.release = _sharedDebugSetRelease;
+        _sharedDebugTargets = CFSetCreateMutable(CFAllocatorGetDefault(), 0, &callbacks);
+    });
+}
+
+static void _setSharedDebugOption(YYTextDebugOption *option) {
+    _initSharedDebug();
+    pthread_mutex_lock(&_sharedDebugLock);
+    _sharedDebugOption = option.copy;
+    CFSetApplyFunction(_sharedDebugTargets, _sharedDebugSetFunction, NULL);
+    pthread_mutex_unlock(&_sharedDebugLock);
+}
+
+static YYTextDebugOption *_getSharedDebugOption() {
+    _initSharedDebug();
+    pthread_mutex_lock(&_sharedDebugLock);
+    YYTextDebugOption *op = _sharedDebugOption;
+    pthread_mutex_unlock(&_sharedDebugLock);
+    return op;
+}
+
+static void _addDebugTarget(id<YYTextDebugTarget> target) {
+    _initSharedDebug();
+    pthread_mutex_lock(&_sharedDebugLock);
+    CFSetAddValue(_sharedDebugTargets, (__bridge const void *)(target));
+    pthread_mutex_unlock(&_sharedDebugLock);
+}
+
+static void _removeDebugTarget(id<YYTextDebugTarget> target) {
+    _initSharedDebug();
+    pthread_mutex_lock(&_sharedDebugLock);
+    CFSetRemoveValue(_sharedDebugTargets, (__bridge const void *)(target));
+    pthread_mutex_unlock(&_sharedDebugLock);
+}
+
+
+@implementation YYTextDebugOption
+
+- (id)copyWithZone:(NSZone *)zone {
+    YYTextDebugOption *op = [self.class new];
+    op.baselineColor = self.baselineColor;
+    op.CTFrameBorderColor = self.CTFrameBorderColor;
+    op.CTFrameFillColor = self.CTFrameFillColor;
+    op.CTLineBorderColor = self.CTLineBorderColor;
+    op.CTLineFillColor = self.CTLineFillColor;
+    op.CTLineNumberColor = self.CTLineNumberColor;
+    op.CTRunBorderColor = self.CTRunBorderColor;
+    op.CTRunFillColor = self.CTRunFillColor;
+    op.CTRunNumberColor = self.CTRunNumberColor;
+    op.CGGlyphBorderColor = self.CGGlyphBorderColor;
+    op.CGGlyphFillColor = self.CGGlyphFillColor;
+    return op;
+}
+
+- (BOOL)needDrawDebug {
+    if (self.baselineColor ||
+        self.CTFrameBorderColor ||
+        self.CTFrameFillColor ||
+        self.CTLineBorderColor ||
+        self.CTLineFillColor ||
+        self.CTLineNumberColor ||
+        self.CTRunBorderColor ||
+        self.CTRunFillColor ||
+        self.CTRunNumberColor ||
+        self.CGGlyphBorderColor ||
+        self.CGGlyphFillColor) return YES;
+    return NO;
+}
+
+- (void)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;
+}
+
++ (void)addDebugTarget:(id<YYTextDebugTarget>)target {
+    if (target) _addDebugTarget(target);
+}
+
++ (void)removeDebugTarget:(id<YYTextDebugTarget>)target {
+    if (target) _removeDebugTarget(target);
+}
+
++ (YYTextDebugOption *)sharedDebugOption {
+    return _getSharedDebugOption();
+}
+
++ (void)setSharedDebugOption:(YYTextDebugOption *)option {
+    NSAssert([NSThread isMainThread], @"This method must be called on the main thread");
+    _setSharedDebugOption(option);
+}
+
+@end
+

+ 52 - 0
Demo/Objective_C_Demo/YYText/YYTextEffectWindow.h

@@ -0,0 +1,52 @@
+//
+//  YYTextEffectWindow.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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(<YYText/YYText.h>)
+#import <YYText/YYTextMagnifier.h>
+#import <YYtext/YYTextSelectionView.h>
+#else
+#import "YYTextMagnifier.h"
+#import "YYTextSelectionView.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ 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.
+ */
+@interface YYTextEffectWindow : UIWindow
+
+/// Returns the shared instance (returns nil in App Extension).
++ (nullable instancetype)sharedWindow;
+
+/// Show the magnifier in this window with a 'popup' animation. @param mag A magnifier.
+- (void)showMagnifier:(YYTextMagnifier *)mag;
+/// Update the magnifier content and position. @param mag A magnifier.
+- (void)moveMagnifier:(YYTextMagnifier *)mag;
+/// Remove the magnifier from this window with a 'shrink' animation. @param mag A magnifier.
+- (void)hideMagnifier:(YYTextMagnifier *)mag;
+
+
+/// Show the selection dot in this window if the dot is clipped by the selection view.
+/// @param selection A selection view.
+- (void)showSelectionDot:(YYTextSelectionView *)selection;
+/// Remove the selection dot from this window.
+/// @param selection A selection view.
+- (void)hideSelectionDot:(YYTextSelectionView *)selection;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 420 - 0
Demo/Objective_C_Demo/YYText/YYTextEffectWindow.m

@@ -0,0 +1,420 @@
+//
+//  YYTextEffectWindow.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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 "YYTextEffectWindow.h"
+#import "YYTextKeyboardManager.h"
+#import "YYTextUtilities.h"
+#import "UIView+YYText.h"
+
+
+@implementation YYTextEffectWindow
+
++ (instancetype)sharedWindow {
+    static YYTextEffectWindow *one = nil;
+    if (one == nil) {
+        // iOS 9 compatible
+        NSString *mode = [NSRunLoop currentRunLoop].currentMode;
+        if (mode.length == 27 &&
+            [mode hasPrefix:@"UI"] &&
+            [mode hasSuffix:@"InitializationRunLoopMode"]) {
+            return nil;
+        }
+    }
+    
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        if (!YYTextIsAppExtension()) {
+            one = [self new];
+            one.frame = (CGRect){.size = YYTextScreenSize()};
+            one.userInteractionEnabled = NO;
+            one.windowLevel = UIWindowLevelStatusBar + 1;
+            one.hidden = NO;
+            
+            // for iOS 9:
+            one.opaque = NO;
+            one.backgroundColor = [UIColor clearColor];
+            one.layer.backgroundColor = [UIColor clearColor].CGColor;
+        }
+    });
+    return one;
+}
+
+- (UIViewController *)rootViewController {
+    for (UIWindow *window in [YYTextSharedApplication() windows]) {
+        if (self == window) continue;
+        if (window.hidden) continue;
+        UIViewController *topViewController = window.rootViewController;
+        if (topViewController) return topViewController;
+    }
+    UIViewController *viewController = [super rootViewController];
+    if (!viewController) {
+        viewController = [UIViewController new];
+        [super setRootViewController:viewController];
+    }
+    return viewController;
+}
+
+// Bring self to front
+- (void)_updateWindowLevel {
+    UIApplication *app = YYTextSharedApplication();
+    if (!app) return;
+    
+    UIWindow *top = app.windows.lastObject;
+    UIWindow *key = app.keyWindow;
+    if (key && key.windowLevel > top.windowLevel) top = key;
+    if (top == self) return;
+    self.windowLevel = top.windowLevel + 1;
+}
+
+- (YYTextDirection)_keyboardDirection {
+    CGRect keyboardFrame = [YYTextKeyboardManager defaultManager].keyboardFrame;
+    keyboardFrame = [[YYTextKeyboardManager defaultManager] convertRect:keyboardFrame toView:self];
+    if (CGRectIsNull(keyboardFrame) || CGRectIsEmpty(keyboardFrame)) return YYTextDirectionNone;
+    
+    if (CGRectGetMinY(keyboardFrame) == 0 &&
+        CGRectGetMinX(keyboardFrame) == 0 &&
+        CGRectGetMaxX(keyboardFrame) == CGRectGetWidth(self.frame))
+        return YYTextDirectionTop;
+    
+    if (CGRectGetMaxX(keyboardFrame) == CGRectGetWidth(self.frame) &&
+        CGRectGetMinY(keyboardFrame) == 0 &&
+        CGRectGetMaxY(keyboardFrame) == CGRectGetHeight(self.frame))
+        return YYTextDirectionRight;
+    
+    if (CGRectGetMaxY(keyboardFrame) == CGRectGetHeight(self.frame) &&
+        CGRectGetMinX(keyboardFrame) == 0 &&
+        CGRectGetMaxX(keyboardFrame) == CGRectGetWidth(self.frame))
+        return YYTextDirectionBottom;
+    
+    if (CGRectGetMinX(keyboardFrame) == 0 &&
+        CGRectGetMinY(keyboardFrame) == 0 &&
+        CGRectGetMaxY(keyboardFrame) == CGRectGetHeight(self.frame))
+        return YYTextDirectionLeft;
+    
+    return YYTextDirectionNone;
+}
+
+- (CGPoint)_correctedCaptureCenter:(CGPoint)center{
+    CGRect keyboardFrame = [YYTextKeyboardManager defaultManager].keyboardFrame;
+    keyboardFrame = [[YYTextKeyboardManager defaultManager] convertRect:keyboardFrame toView:self];
+    if (!CGRectIsNull(keyboardFrame) && !CGRectIsEmpty(keyboardFrame)) {
+        YYTextDirection direction = [self _keyboardDirection];
+        switch (direction) {
+            case YYTextDirectionTop: {
+                if (center.y < CGRectGetMaxY(keyboardFrame)) center.y = CGRectGetMaxY(keyboardFrame);
+            } break;
+            case YYTextDirectionRight: {
+                if (center.x > CGRectGetMinX(keyboardFrame)) center.x = CGRectGetMinX(keyboardFrame);
+            } break;
+            case YYTextDirectionBottom: {
+                if (center.y > CGRectGetMinY(keyboardFrame)) center.y = CGRectGetMinY(keyboardFrame);
+            } break;
+            case YYTextDirectionLeft: {
+                if (center.x < CGRectGetMaxX(keyboardFrame)) center.x = CGRectGetMaxX(keyboardFrame);
+            } break;
+            default: break;
+        }
+    }
+    return center;
+}
+
+- (CGPoint)_correctedCenter:(CGPoint)center forMagnifier:(YYTextMagnifier *)mag rotation:(CGFloat)rotation {
+    CGFloat degree = YYTextRadiansToDegrees(rotation);
+    
+    degree /= 45.0;
+    if (degree < 0) degree += (int)(-degree/8.0 + 1) * 8;
+    if (degree > 8) degree -= (int)(degree/8.0) * 8;
+    
+    CGFloat caretExt = 10;
+    if (degree <= 1 || degree >= 7) { //top
+        if (mag.type == YYTextMagnifierTypeCaret) {
+            if (center.y < caretExt)
+                center.y = caretExt;
+        } else if (mag.type == YYTextMagnifierTypeRanged) {
+            if (center.y < mag.bounds.size.height)
+                center.y = mag.bounds.size.height;
+        }
+    } else if (1 < degree && degree < 3) { // right
+        if (mag.type == YYTextMagnifierTypeCaret) {
+            if (center.x > self.bounds.size.width - caretExt)
+                center.x = self.bounds.size.width - caretExt;
+        } else if (mag.type == YYTextMagnifierTypeRanged) {
+            if (center.x > self.bounds.size.width - mag.bounds.size.height)
+                center.x = self.bounds.size.width - mag.bounds.size.height;
+        }
+    } else if (3 <= degree && degree <= 5) { // bottom
+        if (mag.type == YYTextMagnifierTypeCaret) {
+            if (center.y > self.bounds.size.height - caretExt)
+                center.y = self.bounds.size.height - caretExt;
+        } else if (mag.type == YYTextMagnifierTypeRanged) {
+            if (center.y > mag.bounds.size.height)
+                center.y = mag.bounds.size.height;
+        }
+    } else if (5 < degree && degree < 7) { // left
+        if (mag.type == YYTextMagnifierTypeCaret) {
+            if (center.x < caretExt)
+                center.x = caretExt;
+        } else if (mag.type == YYTextMagnifierTypeRanged) {
+            if (center.x < mag.bounds.size.height)
+                center.x = mag.bounds.size.height;
+        }
+    }
+
+    
+    CGRect keyboardFrame = [YYTextKeyboardManager defaultManager].keyboardFrame;
+    keyboardFrame = [[YYTextKeyboardManager defaultManager] convertRect:keyboardFrame toView:self];
+    if (!CGRectIsNull(keyboardFrame) && !CGRectIsEmpty(keyboardFrame)) {
+        YYTextDirection direction = [self _keyboardDirection];
+        switch (direction) {
+            case YYTextDirectionTop: {
+                if (mag.type == YYTextMagnifierTypeCaret) {
+                    if (center.y - mag.bounds.size.height / 2 < CGRectGetMaxY(keyboardFrame))
+                        center.y = CGRectGetMaxY(keyboardFrame) + mag.bounds.size.height / 2;
+                } else if (mag.type == YYTextMagnifierTypeRanged) {
+                    if (center.y < CGRectGetMaxY(keyboardFrame)) center.y = CGRectGetMaxY(keyboardFrame);
+                }
+            } break;
+            case YYTextDirectionRight: {
+                if (mag.type == YYTextMagnifierTypeCaret) {
+                    if (center.x + mag.bounds.size.height / 2 > CGRectGetMinX(keyboardFrame))
+                        center.x = CGRectGetMinX(keyboardFrame) - mag.bounds.size.width / 2;
+                } else if (mag.type == YYTextMagnifierTypeRanged) {
+                    if (center.x > CGRectGetMinX(keyboardFrame)) center.x = CGRectGetMinX(keyboardFrame);
+                }
+            } break;
+            case YYTextDirectionBottom: {
+                if (mag.type == YYTextMagnifierTypeCaret) {
+                    if (center.y + mag.bounds.size.height / 2 > CGRectGetMinY(keyboardFrame))
+                        center.y = CGRectGetMinY(keyboardFrame) - mag.bounds.size.height / 2;
+                } else if (mag.type == YYTextMagnifierTypeRanged) {
+                    if (center.y > CGRectGetMinY(keyboardFrame)) center.y = CGRectGetMinY(keyboardFrame);
+                }
+            } break;
+            case YYTextDirectionLeft: {
+                if (mag.type == YYTextMagnifierTypeCaret) {
+                    if (center.x - mag.bounds.size.height / 2 < CGRectGetMaxX(keyboardFrame))
+                        center.x = CGRectGetMaxX(keyboardFrame) + mag.bounds.size.width / 2;
+                } else if (mag.type == YYTextMagnifierTypeRanged) {
+                    if (center.x < CGRectGetMaxX(keyboardFrame)) center.x = CGRectGetMaxX(keyboardFrame);
+                }
+            } break;
+            default: break;
+        }
+    }
+    
+    return center;
+}
+
+/**
+ Capture screen snapshot and set it to magnifier.
+ @return Magnifier rotation radius.
+ */
+- (CGFloat)_updateMagnifier:(YYTextMagnifier *)mag {
+    UIApplication *app = YYTextSharedApplication();
+    if (!app) return 0;
+    
+    UIView *hostView = mag.hostView;
+    UIWindow *hostWindow = [hostView isKindOfClass:[UIWindow class]] ? (id)hostView : hostView.window;
+    if (!hostView || !hostWindow) return 0;
+    CGPoint captureCenter = [self yy_convertPoint:mag.hostCaptureCenter fromViewOrWindow:hostView];
+    captureCenter = [self _correctedCaptureCenter:captureCenter];
+    CGRect captureRect = {.size = mag.snapshotSize};
+    captureRect.origin.x = captureCenter.x - captureRect.size.width / 2;
+    captureRect.origin.y = captureCenter.y - captureRect.size.height / 2;
+    
+    CGAffineTransform trans = YYTextCGAffineTransformGetFromViews(hostView, self);
+    CGFloat rotation = YYTextCGAffineTransformGetRotation(trans);
+    
+    if (mag.captureDisabled) {
+        if (!mag.snapshot || mag.snapshot.size.width > 1) {
+            static UIImage *placeholder;
+            static dispatch_once_t onceToken;
+            dispatch_once(&onceToken, ^{
+                CGRect rect = mag.bounds;
+                rect.origin = CGPointZero;
+                UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0);
+                CGContextRef context = UIGraphicsGetCurrentContext();
+                [[UIColor colorWithWhite:1 alpha:0.8] set];
+                CGContextFillRect(context, rect);
+                placeholder = UIGraphicsGetImageFromCurrentImageContext();
+                UIGraphicsEndImageContext();
+            });
+            mag.captureFadeAnimation = YES;
+            mag.snapshot = placeholder;
+            mag.captureFadeAnimation = NO;
+        }
+        return rotation;
+    }
+    
+    UIGraphicsBeginImageContextWithOptions(captureRect.size, NO, 0);
+    CGContextRef context = UIGraphicsGetCurrentContext();
+    if (!context) return rotation;
+    
+    CGPoint tp = CGPointMake(captureRect.size.width / 2, captureRect.size.height / 2);
+    tp = CGPointApplyAffineTransform(tp, CGAffineTransformMakeRotation(rotation));
+    CGContextRotateCTM(context, -rotation);
+    CGContextTranslateCTM(context, tp.x - captureCenter.x, tp.y - captureCenter.y);
+    
+    NSMutableArray *windows = app.windows.mutableCopy;
+    UIWindow *keyWindow = app.keyWindow;
+    if (![windows containsObject:keyWindow]) [windows addObject:keyWindow];
+    [windows sortUsingComparator:^NSComparisonResult(UIWindow *w1, UIWindow *w2) {
+        if (w1.windowLevel < w2.windowLevel) return NSOrderedAscending;
+        else if (w1.windowLevel > w2.windowLevel) return NSOrderedDescending;
+        return NSOrderedSame;
+    }];
+    UIScreen *mainScreen = [UIScreen mainScreen];
+    for (UIWindow *window in windows) {
+        if (window.hidden || window.alpha <= 0.01) continue;
+        if (window.screen != mainScreen) continue;
+        if ([window isKindOfClass:self.class]) break; //don't capture window above self
+        CGContextSaveGState(context);
+        CGContextConcatCTM(context, YYTextCGAffineTransformGetFromViews(window, self));
+        [window.layer renderInContext:context]; //render
+        //[window drawViewHierarchyInRect:window.bounds afterScreenUpdates:NO]; //slower when capture whole window
+        CGContextRestoreGState(context);
+    }
+    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+    UIGraphicsEndImageContext();
+    
+    if (mag.snapshot.size.width == 1) {
+        mag.captureFadeAnimation = YES;
+    }
+    mag.snapshot = image;
+    mag.captureFadeAnimation = NO;
+    return rotation;
+}
+
+- (void)showMagnifier:(YYTextMagnifier *)mag {
+    if (!mag) return;
+    if (mag.superview != self) [self addSubview:mag];
+    [self _updateWindowLevel];
+    CGFloat rotation = [self _updateMagnifier:mag];
+    CGPoint center = [self yy_convertPoint:mag.hostPopoverCenter fromViewOrWindow:mag.hostView];
+    CGAffineTransform trans = CGAffineTransformMakeRotation(rotation);
+    trans = CGAffineTransformScale(trans, 0.3, 0.3);
+    mag.transform = trans;
+    mag.center = center;
+    if (mag.type == YYTextMagnifierTypeRanged) {
+        mag.alpha = 0;
+    }
+    NSTimeInterval time = mag.type == YYTextMagnifierTypeCaret ? 0.08 : 0.1;
+    [UIView animateWithDuration:time delay:0 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState animations:^{
+        if (mag.type == YYTextMagnifierTypeCaret) {
+            CGPoint newCenter = CGPointMake(0, -mag.fitSize.height / 2);
+            newCenter = CGPointApplyAffineTransform(newCenter, CGAffineTransformMakeRotation(rotation));
+            newCenter.x += center.x;
+            newCenter.y += center.y;
+            mag.center = [self _correctedCenter:newCenter forMagnifier:mag rotation:rotation];
+        } else {
+            mag.center = [self _correctedCenter:center forMagnifier:mag rotation:rotation];
+        }
+        mag.transform = CGAffineTransformMakeRotation(rotation);
+        mag.alpha = 1;
+    } completion:^(BOOL finished) {
+        
+    }];
+}
+
+- (void)moveMagnifier:(YYTextMagnifier *)mag {
+    if (!mag) return;
+    [self _updateWindowLevel];
+    CGFloat rotation = [self _updateMagnifier:mag];
+    CGPoint center = [self yy_convertPoint:mag.hostPopoverCenter fromViewOrWindow:mag.hostView];
+    if (mag.type == YYTextMagnifierTypeCaret) {
+        CGPoint newCenter = CGPointMake(0, -mag.fitSize.height / 2);
+        newCenter = CGPointApplyAffineTransform(newCenter, CGAffineTransformMakeRotation(rotation));
+        newCenter.x += center.x;
+        newCenter.y += center.y;
+        mag.center = [self _correctedCenter:newCenter forMagnifier:mag rotation:rotation];
+    } else {
+        mag.center = [self _correctedCenter:center forMagnifier:mag rotation:rotation];
+    }
+    mag.transform = CGAffineTransformMakeRotation(rotation);
+}
+
+- (void)hideMagnifier:(YYTextMagnifier *)mag {
+    if (!mag) return;
+    if (mag.superview != self) return;
+    CGFloat rotation = [self _updateMagnifier:mag];
+    CGPoint center = [self yy_convertPoint:mag.hostPopoverCenter fromViewOrWindow:mag.hostView];
+    NSTimeInterval time = mag.type == YYTextMagnifierTypeCaret ? 0.20 : 0.15;
+    [UIView animateWithDuration:time delay:0 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState animations:^{
+        
+        CGAffineTransform trans = CGAffineTransformMakeRotation(rotation);
+        trans = CGAffineTransformScale(trans, 0.01, 0.01);
+        mag.transform = trans;
+        
+        if (mag.type == YYTextMagnifierTypeCaret) {
+            CGPoint newCenter = CGPointMake(0, -mag.fitSize.height / 2);
+            newCenter = CGPointApplyAffineTransform(newCenter, CGAffineTransformMakeRotation(rotation));
+            newCenter.x += center.x;
+            newCenter.y += center.y;
+            mag.center = [self _correctedCenter:newCenter forMagnifier:mag rotation:rotation];
+        } else {
+            mag.center = [self _correctedCenter:center forMagnifier:mag rotation:rotation];
+            mag.alpha = 0;
+        }
+        
+    } completion:^(BOOL finished) {
+        if (finished) {
+            [mag removeFromSuperview];
+            mag.transform = CGAffineTransformIdentity;
+            mag.alpha = 1;
+        }
+    }];
+}
+
+- (void)_updateSelectionGrabberDot:(YYSelectionGrabberDot *)dot selection:(YYTextSelectionView *)selection{
+    dot.mirror.hidden = YES;
+    if (selection.hostView.clipsToBounds == YES && dot.yy_visibleAlpha > 0.1) {
+        CGRect dotRect = [dot yy_convertRect:dot.bounds toViewOrWindow:self];
+        BOOL dotInKeyboard = NO;
+        
+        CGRect keyboardFrame = [YYTextKeyboardManager defaultManager].keyboardFrame;
+        keyboardFrame = [[YYTextKeyboardManager defaultManager] convertRect:keyboardFrame toView:self];
+        if (!CGRectIsNull(keyboardFrame) && !CGRectIsEmpty(keyboardFrame)) {
+            CGRect inter = CGRectIntersection(dotRect, keyboardFrame);
+            if (!CGRectIsNull(inter) && (inter.size.width > 1 || inter.size.height > 1)) {
+                dotInKeyboard = YES;
+            }
+        }
+        if (!dotInKeyboard) {
+            CGRect hostRect = [selection.hostView convertRect:selection.hostView.bounds toView:self];
+            CGRect intersection = CGRectIntersection(dotRect, hostRect);
+            if (YYTextCGRectGetArea(intersection) < YYTextCGRectGetArea(dotRect)) {
+                CGFloat dist = YYTextCGPointGetDistanceToRect(YYTextCGRectGetCenter(dotRect), hostRect);
+                if (dist < CGRectGetWidth(dot.frame) * 0.55) {
+                    dot.mirror.hidden = NO;
+                }
+            }
+        }
+    }
+    CGPoint center = [dot yy_convertPoint:CGPointMake(CGRectGetWidth(dot.frame) / 2, CGRectGetHeight(dot.frame) / 2) toViewOrWindow:self];
+    dot.mirror.center = center;
+}
+
+- (void)showSelectionDot:(YYTextSelectionView *)selection {
+    if (!selection) return;
+    [self _updateWindowLevel];
+    [self insertSubview:selection.startGrabber.dot.mirror atIndex:0];
+    [self insertSubview:selection.endGrabber.dot.mirror atIndex:0];
+    [self _updateSelectionGrabberDot:selection.startGrabber.dot selection:selection];
+    [self _updateSelectionGrabberDot:selection.endGrabber.dot selection:selection];
+}
+
+- (void)hideSelectionDot:(YYTextSelectionView *)selection {
+    if (!selection) return;
+    [selection.startGrabber.dot.mirror removeFromSuperview];
+    [selection.endGrabber.dot.mirror removeFromSuperview];
+}
+
+@end

+ 87 - 0
Demo/Objective_C_Demo/YYText/YYTextInput.h

@@ -0,0 +1,87 @@
+//
+//  YYTextInput.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/17.
+//  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
+
+/**
+ 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.
+ */
+typedef NS_ENUM(NSInteger, YYTextAffinity) {
+    YYTextAffinityForward  = 0, ///< offset appears before the character
+    YYTextAffinityBackward = 1, ///< offset appears after the character
+};
+
+
+/**
+ A YYTextPosition object represents a position in a text container; in other words, 
+ it is an index into the backing string in a text-displaying view.
+ 
+ YYTextPosition has the same API as Apple's implementation in UITextView/UITextField,
+ so you can alse use it to interact with UITextView/UITextField.
+ */
+@interface YYTextPosition : UITextPosition <NSCopying>
+
+@property (nonatomic, readonly) NSInteger offset;
+@property (nonatomic, readonly) YYTextAffinity affinity;
+
++ (instancetype)positionWithOffset:(NSInteger)offset;
++ (instancetype)positionWithOffset:(NSInteger)offset affinity:(YYTextAffinity) affinity;
+
+- (NSComparisonResult)compare:(id)otherPosition;
+
+@end
+
+
+/**
+ A YYTextRange 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.
+ 
+ YYTextRange has the same API as Apple's implementation in UITextView/UITextField,
+ so you can alse use it to interact with UITextView/UITextField.
+ */
+@interface YYTextRange : UITextRange <NSCopying>
+
+@property (nonatomic, readonly) YYTextPosition *start;
+@property (nonatomic, readonly) YYTextPosition *end;
+@property (nonatomic, readonly, getter=isEmpty) BOOL empty;
+
++ (instancetype)rangeWithRange:(NSRange)range;
++ (instancetype)rangeWithRange:(NSRange)range affinity:(YYTextAffinity) affinity;
++ (instancetype)rangeWithStart:(YYTextPosition *)start end:(YYTextPosition *)end;
++ (instancetype)defaultRange; ///< <{0,0} Forward>
+
+- (NSRange)asRange;
+
+@end
+
+
+/**
+ A YYTextSelectionRect object encapsulates information about a selected range of 
+ text in a text-displaying view.
+ 
+ YYTextSelectionRect has the same API as Apple's implementation in UITextView/UITextField,
+ so you can alse use it to interact with UITextView/UITextField.
+ */
+@interface YYTextSelectionRect : UITextSelectionRect <NSCopying>
+
+@property (nonatomic, readwrite) CGRect rect;
+@property (nonatomic, readwrite) UITextWritingDirection writingDirection;
+@property (nonatomic, readwrite) BOOL containsStart;
+@property (nonatomic, readwrite) BOOL containsEnd;
+@property (nonatomic, readwrite) BOOL isVertical;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 152 - 0
Demo/Objective_C_Demo/YYText/YYTextInput.m

@@ -0,0 +1,152 @@
+//
+//  YYTextInput.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/17.
+//  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 "YYTextInput.h"
+#import "YYTextUtilities.h"
+
+
+@implementation YYTextPosition
+
++ (instancetype)positionWithOffset:(NSInteger)offset {
+    return [self positionWithOffset:offset affinity:YYTextAffinityForward];
+}
+
++ (instancetype)positionWithOffset:(NSInteger)offset affinity:(YYTextAffinity)affinity {
+    YYTextPosition *p = [self new];
+    p->_offset = offset;
+    p->_affinity = affinity;
+    return p;
+}
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+    return [self.class positionWithOffset:_offset affinity:_affinity];
+}
+
+- (NSString *)description {
+    return [NSString stringWithFormat:@"<%@: %p> (%@%@)", self.class, self, @(_offset), _affinity == YYTextAffinityForward ? @"F":@"B"];
+}
+
+- (NSUInteger)hash {
+    return _offset * 2 + (_affinity == YYTextAffinityForward ? 1 : 0);
+}
+
+- (BOOL)isEqual:(YYTextPosition *)object {
+    if (!object) return NO;
+    return _offset == object.offset && _affinity == object.affinity;
+}
+
+- (NSComparisonResult)compare:(YYTextPosition *)otherPosition {
+    if (!otherPosition) return NSOrderedAscending;
+    if (_offset < otherPosition.offset) return NSOrderedAscending;
+    if (_offset > otherPosition.offset) return NSOrderedDescending;
+    if (_affinity == YYTextAffinityBackward && otherPosition.affinity == YYTextAffinityForward) return NSOrderedAscending;
+    if (_affinity == YYTextAffinityForward && otherPosition.affinity == YYTextAffinityBackward) return NSOrderedDescending;
+    return NSOrderedSame;
+}
+
+@end
+
+
+
+@implementation YYTextRange {
+    YYTextPosition *_start;
+    YYTextPosition *_end;
+}
+
+- (instancetype)init {
+    self = [super init];
+    if (!self) return nil;
+    _start = [YYTextPosition positionWithOffset:0];
+    _end = [YYTextPosition positionWithOffset:0];
+    return self;
+}
+
+- (YYTextPosition *)start {
+    return _start;
+}
+
+- (YYTextPosition *)end {
+    return _end;
+}
+
+- (BOOL)isEmpty {
+    return _start.offset == _end.offset;
+}
+
+- (NSRange)asRange {
+    return NSMakeRange(_start.offset, _end.offset - _start.offset);
+}
+
++ (instancetype)rangeWithRange:(NSRange)range {
+    return [self rangeWithRange:range affinity:YYTextAffinityForward];
+}
+
++ (instancetype)rangeWithRange:(NSRange)range affinity:(YYTextAffinity)affinity {
+    YYTextPosition *start = [YYTextPosition positionWithOffset:range.location affinity:affinity];
+    YYTextPosition *end = [YYTextPosition positionWithOffset:range.location + range.length affinity:affinity];
+    return [self rangeWithStart:start end:end];
+}
+
++ (instancetype)rangeWithStart:(YYTextPosition *)start end:(YYTextPosition *)end {
+    if (!start || !end) return nil;
+    if ([start compare:end] == NSOrderedDescending) {
+        YYTEXT_SWAP(start, end);
+    }
+    YYTextRange *range = [YYTextRange new];
+    range->_start = start;
+    range->_end = end;
+    return range;
+}
+
++ (instancetype)defaultRange {
+    return [self new];
+}
+
+- (instancetype)copyWithZone:(NSZone *)zone {
+    return [self.class rangeWithStart:_start end:_end];
+}
+
+- (NSString *)description {
+    return [NSString stringWithFormat:@"<%@: %p> (%@, %@)%@", self.class, self, @(_start.offset), @(_end.offset - _start.offset), _end.affinity == YYTextAffinityForward ? @"F":@"B"];
+}
+
+- (NSUInteger)hash {
+    return (sizeof(NSUInteger) == 8 ? OSSwapInt64(_start.hash) : OSSwapInt32(_start.hash)) + _end.hash;
+}
+
+- (BOOL)isEqual:(YYTextRange *)object {
+    if (!object) return NO;
+    return [_start isEqual:object.start] && [_end isEqual:object.end];
+}
+
+@end
+
+
+
+@implementation YYTextSelectionRect
+
+@synthesize rect = _rect;
+@synthesize writingDirection = _writingDirection;
+@synthesize containsStart = _containsStart;
+@synthesize containsEnd = _containsEnd;
+@synthesize isVertical = _isVertical;
+
+- (id)copyWithZone:(NSZone *)zone {
+    YYTextSelectionRect *one = [self.class new];
+    one.rect = _rect;
+    one.writingDirection = _writingDirection;
+    one.containsStart = _containsStart;
+    one.containsEnd = _containsEnd;
+    one.isVertical = _isVertical;
+    return one;
+}
+
+@end

+ 98 - 0
Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.h

@@ -0,0 +1,98 @@
+//
+//  YYTextKeyboardManager.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/6/3.
+//  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
+
+/**
+ System keyboard transition information.
+ Use -[YYTextKeyboardManager convertRect:toView:] to convert frame to specified view.
+ */
+typedef struct {
+    BOOL fromVisible; ///< Keyboard visible before transition.
+    BOOL toVisible;   ///< Keyboard visible after transition.
+    CGRect fromFrame; ///< Keyboard frame before transition.
+    CGRect toFrame;   ///< Keyboard frame after transition.
+    NSTimeInterval animationDuration;       ///< Keyboard transition animation duration.
+    UIViewAnimationCurve animationCurve;    ///< Keyboard transition animation curve.
+    UIViewAnimationOptions animationOption; ///< Keybaord transition animation option.
+} YYTextKeyboardTransition;
+
+
+/**
+ The YYTextKeyboardObserver protocol defines the method you can use
+ to receive system keyboard change information.
+ */
+@protocol YYTextKeyboardObserver <NSObject>
+@optional
+- (void)keyboardChangedWithTransition:(YYTextKeyboardTransition)transition;
+@end
+
+
+/**
+ A YYTextKeyboardManager object lets you get the system keyboard information,
+ and track the keyboard visible/frame/transition.
+ 
+ @discussion You should access this class in main thread.
+ Compatible: iPhone/iPad with iOS6/7/8/9.
+ */
+@interface YYTextKeyboardManager : NSObject
+
+- (instancetype)init UNAVAILABLE_ATTRIBUTE;
++ (instancetype)new UNAVAILABLE_ATTRIBUTE;
+
+/// Get the default manager (returns nil in App Extension).
++ (nullable instancetype)defaultManager;
+
+/// Get the keyboard window. nil if there's no keyboard window.
+@property (nullable, nonatomic, readonly) UIWindow *keyboardWindow;
+
+/// Get the keyboard view. nil if there's no keyboard view.
+@property (nullable, nonatomic, readonly) UIView *keyboardView;
+
+/// Whether the keyboard is visible.
+@property (nonatomic, readonly, getter=isKeyboardVisible) BOOL keyboardVisible;
+
+/// Get the keyboard frame. CGRectNull if there's no keyboard view.
+/// Use convertRect:toView: to convert frame to specified view.
+@property (nonatomic, readonly) CGRect keyboardFrame;
+
+
+/**
+ 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.
+ */
+- (void)addObserver:(id<YYTextKeyboardObserver>)observer;
+
+/**
+ Remove an observer from manager.
+ 
+ @param observer An observer.
+ This method will do nothing if the observer is nil, or not in manager.
+ */
+- (void)removeObserver:(id<YYTextKeyboardObserver>)observer;
+
+/**
+ 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.
+ */
+- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 521 - 0
Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.m

@@ -0,0 +1,521 @@
+//
+//  YYTextKeyboardManager.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/6/3.
+//  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 "YYTextKeyboardManager.h"
+#import "YYTextUtilities.h"
+#import <objc/runtime.h>
+
+
+static int _YYTextKeyboardViewFrameObserverKey;
+
+/// Observer for view's frame/bounds/center/transform
+@interface _YYTextKeyboardViewFrameObserver : NSObject
+@property (nonatomic, copy) void (^notifyBlock)(UIView *keyboard);
+- (void)addToKeyboardView:(UIView *)keyboardView;
++ (instancetype)observerForView:(UIView *)keyboardView;
+@end
+
+
+@implementation _YYTextKeyboardViewFrameObserver {
+    __unsafe_unretained UIView *_keyboardView;
+}
+- (void)addToKeyboardView:(UIView *)keyboardView {
+    if (_keyboardView == keyboardView) return;
+    if (_keyboardView) {
+        [self removeFrameObserver];
+        objc_setAssociatedObject(_keyboardView, &_YYTextKeyboardViewFrameObserverKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+    }
+    _keyboardView = keyboardView;
+    if (keyboardView) {
+        [self addFrameObserver];
+    }
+    objc_setAssociatedObject(keyboardView, &_YYTextKeyboardViewFrameObserverKey, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (void)removeFrameObserver {
+    [_keyboardView removeObserver:self forKeyPath:@"frame"];
+    [_keyboardView removeObserver:self forKeyPath:@"center"];
+    [_keyboardView removeObserver:self forKeyPath:@"bounds"];
+    [_keyboardView removeObserver:self forKeyPath:@"transform"];
+    _keyboardView = nil;
+}
+
+- (void)addFrameObserver {
+    if (!_keyboardView) return;
+    [_keyboardView addObserver:self forKeyPath:@"frame" options:kNilOptions context:NULL];
+    [_keyboardView addObserver:self forKeyPath:@"center" options:kNilOptions context:NULL];
+    [_keyboardView addObserver:self forKeyPath:@"bounds" options:kNilOptions context:NULL];
+    [_keyboardView addObserver:self forKeyPath:@"transform" options:kNilOptions context:NULL];
+}
+
+- (void)dealloc {
+    [self removeFrameObserver];
+}
+
++ (instancetype)observerForView:(UIView *)keyboardView {
+    if (!keyboardView) return nil;
+    return objc_getAssociatedObject(keyboardView, &_YYTextKeyboardViewFrameObserverKey);
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
+    
+    BOOL isPrior = [[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];
+    if (isPrior) return;
+    
+    NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];
+    if (changeKind != NSKeyValueChangeSetting) return;
+    
+    id newVal = [change objectForKey:NSKeyValueChangeNewKey];
+    if (newVal == [NSNull null]) newVal = nil;
+    
+    if (_notifyBlock) {
+        _notifyBlock(_keyboardView);
+    }
+}
+
+@end
+
+
+
+@implementation YYTextKeyboardManager {
+    NSHashTable *_observers;
+    
+    CGRect _fromFrame;
+    BOOL _fromVisible;
+    UIInterfaceOrientation _fromOrientation;
+    
+    CGRect _notificationFromFrame;
+    CGRect _notificationToFrame;
+    NSTimeInterval _notificationDuration;
+    UIViewAnimationCurve _notificationCurve;
+    BOOL _hasNotification;
+    
+    CGRect _observedToFrame;
+    BOOL _hasObservedChange;
+    
+    BOOL _lastIsNotification;
+}
+
+- (instancetype)init {
+    @throw [NSException exceptionWithName:@"YYTextKeyboardManager init error" reason:@"Use 'defaultManager' to get instance." userInfo:nil];
+    return [super init];
+}
+
+- (instancetype)_init {
+    self = [super init];
+    _observers = [[NSHashTable alloc] initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(_keyboardFrameWillChangeNotification:)
+                                                 name:UIKeyboardWillChangeFrameNotification
+                                               object:nil];
+    // for iPad (iOS 9)
+    if ([UIDevice currentDevice].systemVersion.floatValue >= 9) {
+        [[NSNotificationCenter defaultCenter] addObserver:self
+                                                 selector:@selector(_keyboardFrameDidChangeNotification:)
+                                                     name:UIKeyboardDidChangeFrameNotification
+                                                   object:nil];
+    }
+    return self;
+}
+
+- (void)_initFrameObserver {
+    UIView *keyboardView = self.keyboardView;
+    if (!keyboardView) return;
+    __weak typeof(self) _self = self;
+    _YYTextKeyboardViewFrameObserver *observer = [_YYTextKeyboardViewFrameObserver observerForView:keyboardView];
+    if (!observer) {
+        observer = [_YYTextKeyboardViewFrameObserver new];
+        observer.notifyBlock = ^(UIView *keyboard) {
+            [_self _keyboardFrameChanged:keyboard];
+        };
+        [observer addToKeyboardView:keyboardView];
+    }
+}
+
+- (void)dealloc {
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
++ (instancetype)defaultManager {
+    static YYTextKeyboardManager *mgr = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        if (!YYTextIsAppExtension()) {
+            mgr = [[self alloc] _init];
+        }
+    });
+    return mgr;
+}
+
++ (void)load {
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+        [self defaultManager];
+    });
+}
+
+- (void)addObserver:(id<YYTextKeyboardObserver>)observer {
+    if (!observer) return;
+    [_observers addObject:observer];
+}
+
+- (void)removeObserver:(id<YYTextKeyboardObserver>)observer {
+    if (!observer) return;
+    [_observers removeObject:observer];
+}
+
+- (UIWindow *)keyboardWindow {
+    UIApplication *app = YYTextSharedApplication();
+    if (!app) return nil;
+    
+    UIWindow *window = nil;
+    for (window in app.windows) {
+        if ([self _getKeyboardViewFromWindow:window]) return window;
+    }
+    window = app.keyWindow;
+    if ([self _getKeyboardViewFromWindow:window]) return window;
+    
+    NSMutableArray *kbWindows = nil;
+    for (window in app.windows) {
+        NSString *windowName = NSStringFromClass(window.class);
+        if ([self _systemVersion] < 9) {
+            // UITextEffectsWindow
+            if (windowName.length == 19 &&
+                [windowName hasPrefix:@"UI"] &&
+                [windowName hasSuffix:@"TextEffectsWindow"]) {
+                if (!kbWindows) kbWindows = [NSMutableArray new];
+                [kbWindows addObject:window];
+            }
+        } else {
+            // UIRemoteKeyboardWindow
+            if (windowName.length == 22 &&
+                [windowName hasPrefix:@"UI"] &&
+                [windowName hasSuffix:@"RemoteKeyboardWindow"]) {
+                if (!kbWindows) kbWindows = [NSMutableArray new];
+                [kbWindows addObject:window];
+            }
+        }
+    }
+    
+    if (kbWindows.count == 1) {
+        return kbWindows.firstObject;
+    }
+    return nil;
+}
+
+- (UIView *)keyboardView {
+    UIApplication *app = YYTextSharedApplication();
+    if (!app) return nil;
+    
+    UIWindow *window = nil;
+    UIView *view = nil;
+    for (window in app.windows) {
+        view = [self _getKeyboardViewFromWindow:window];
+        if (view) return view;
+    }
+    window = app.keyWindow;
+    view = [self _getKeyboardViewFromWindow:window];
+    if (view) return view;
+    return nil;
+}
+
+- (BOOL)isKeyboardVisible {
+    UIWindow *window = self.keyboardWindow;
+    if (!window) return NO;
+    UIView *view = self.keyboardView;
+    if (!view) return NO;
+    CGRect rect = CGRectIntersection(window.bounds, view.frame);
+    if (CGRectIsNull(rect)) return NO;
+    if (CGRectIsInfinite(rect)) return NO;
+    return rect.size.width > 0 && rect.size.height > 0;
+}
+
+- (CGRect)keyboardFrame {
+    UIView *keyboard = [self keyboardView];
+    if (!keyboard) return CGRectNull;
+    
+    CGRect frame = CGRectNull;
+    UIWindow *window = keyboard.window;
+    if (window) {
+        frame = [window convertRect:keyboard.frame toWindow:nil];
+    } else {
+        frame = keyboard.frame;
+    }
+    return frame;
+}
+
+#pragma mark - private
+
+- (double)_systemVersion {
+    static double v;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        v = [UIDevice currentDevice].systemVersion.doubleValue;
+    });
+    return v;
+}
+
+- (UIView *)_getKeyboardViewFromWindow:(UIWindow *)window {
+    /*
+     iOS 6/7:
+     UITextEffectsWindow
+        UIPeripheralHostView << keyboard
+     
+     iOS 8:
+     UITextEffectsWindow
+        UIInputSetContainerView
+            UIInputSetHostView << keyboard
+     
+     iOS 9:
+     UIRemoteKeyboardWindow
+        UIInputSetContainerView
+            UIInputSetHostView << keyboard
+     */
+    if (!window) return nil;
+    
+    // Get the window
+    NSString *windowName = NSStringFromClass(window.class);
+    if ([self _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
+    if ([self _systemVersion] < 8) {
+        // UIPeripheralHostView
+        for (UIView *view in window.subviews) {
+            NSString *viewName = NSStringFromClass(view.class);
+            if (viewName.length != 20) continue;
+            if (![viewName hasPrefix:@"UI"]) continue;
+            if (![viewName hasSuffix:@"PeripheralHostView"]) continue;
+            return view;
+        }
+    } else {
+        // UIInputSetContainerView
+        for (UIView *view in window.subviews) {
+            NSString *viewName = NSStringFromClass(view.class);
+            if (viewName.length != 23) continue;
+            if (![viewName hasPrefix:@"UI"]) continue;
+            if (![viewName hasSuffix:@"InputSetContainerView"]) continue;
+            // UIInputSetHostView
+            for (UIView *subView in view.subviews) {
+                NSString *subViewName = NSStringFromClass(subView.class);
+                if (subViewName.length != 18) continue;
+                if (![subViewName hasPrefix:@"UI"]) continue;
+                if (![subViewName hasSuffix:@"InputSetHostView"]) continue;
+                return subView;
+            }
+        }
+    }
+    
+    return nil;
+}
+
+- (void)_keyboardFrameWillChangeNotification:(NSNotification *)notif {
+    if (![notif.name isEqualToString:UIKeyboardWillChangeFrameNotification]) return;
+    NSDictionary *info = notif.userInfo;
+    if (!info) return;
+    
+    [self _initFrameObserver];
+    
+    NSValue *beforeValue = info[UIKeyboardFrameBeginUserInfoKey];
+    NSValue *afterValue = info[UIKeyboardFrameEndUserInfoKey];
+    NSNumber *curveNumber = info[UIKeyboardAnimationCurveUserInfoKey];
+    NSNumber *durationNumber = info[UIKeyboardAnimationDurationUserInfoKey];
+    
+    CGRect before = beforeValue.CGRectValue;
+    CGRect after = afterValue.CGRectValue;
+    UIViewAnimationCurve curve = curveNumber.integerValue;
+    NSTimeInterval duration = durationNumber.doubleValue;
+    
+    // ignore zero end frame
+    if (after.size.width <= 0 && after.size.height <= 0) return;
+    
+    _notificationFromFrame = before;
+    _notificationToFrame = after;
+    _notificationCurve = curve;
+    _notificationDuration = duration;
+    _hasNotification = YES;
+    _lastIsNotification = YES;
+    
+    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
+    if (duration == 0) {
+        [self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
+    } else {
+        [self _notifyAllObservers];
+    }
+}
+
+- (void)_keyboardFrameDidChangeNotification:(NSNotification *)notif {
+    if (![notif.name isEqualToString:UIKeyboardDidChangeFrameNotification]) return;
+    NSDictionary *info = notif.userInfo;
+    if (!info) return;
+    
+    [self _initFrameObserver];
+    
+    NSValue *afterValue = info[UIKeyboardFrameEndUserInfoKey];
+    CGRect after = afterValue.CGRectValue;
+    
+    // ignore zero end frame
+    if (after.size.width <= 0 && after.size.height <= 0) return;
+    
+    _notificationToFrame = after;
+    _notificationCurve = UIViewAnimationCurveEaseInOut;
+    _notificationDuration = 0;
+    _hasNotification = YES;
+    _lastIsNotification = YES;
+    
+    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
+    [self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
+}
+
+- (void)_keyboardFrameChanged:(UIView *)keyboard {
+    if (keyboard != self.keyboardView) return;
+    
+    UIWindow *window = keyboard.window;
+    if (window) {
+        _observedToFrame = [window convertRect:keyboard.frame toWindow:nil];
+    } else {
+        _observedToFrame = keyboard.frame;
+    }
+    _hasObservedChange = YES;
+    _lastIsNotification = NO;
+    
+    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
+    [self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
+}
+
+- (void)_notifyAllObservers {
+    UIApplication *app = YYTextSharedApplication();
+    if (!app) return;
+    
+    UIView *keyboard = self.keyboardView;
+    UIWindow *window = keyboard.window;
+    if (!window) {
+        window = app.keyWindow;
+    }
+    if (!window) {
+        window = app.windows.firstObject;
+    }
+    
+    YYTextKeyboardTransition trans = {0};
+    
+    // from
+    if (_fromFrame.size.width == 0 && _fromFrame.size.height == 0) { // first notify
+        _fromFrame.size.width = window.bounds.size.width;
+        _fromFrame.size.height = trans.toFrame.size.height;
+        _fromFrame.origin.x = trans.toFrame.origin.x;
+        _fromFrame.origin.y = window.bounds.size.height;
+    }
+    trans.fromFrame = _fromFrame;
+    trans.fromVisible = _fromVisible;
+    
+    // to
+    if (_lastIsNotification || (_hasObservedChange && CGRectEqualToRect(_observedToFrame, _notificationToFrame))) {
+        trans.toFrame = _notificationToFrame;
+        trans.animationDuration = _notificationDuration;
+        trans.animationCurve = _notificationCurve;
+        trans.animationOption = _notificationCurve << 16;
+        
+        // Fix iPad(iOS7) keyboard frame error after rorate device when the keyboard is not docked to bottom.
+        if (((int)[self _systemVersion]) == 7) {
+            UIInterfaceOrientation ori = app.statusBarOrientation;
+            if (_fromOrientation != UIInterfaceOrientationUnknown && _fromOrientation != ori) {
+                switch (ori) {
+                    case UIInterfaceOrientationPortrait: {
+                        if (CGRectGetMaxY(trans.toFrame) != window.frame.size.height) {
+                            trans.toFrame.origin.y -= trans.toFrame.size.height;
+                        }
+                    } break;
+                    case UIInterfaceOrientationPortraitUpsideDown: {
+                        if (CGRectGetMinY(trans.toFrame) != 0) {
+                            trans.toFrame.origin.y += trans.toFrame.size.height;
+                        }
+                    } break;
+                    case UIInterfaceOrientationLandscapeLeft: {
+                        if (CGRectGetMaxX(trans.toFrame) != window.frame.size.width) {
+                            trans.toFrame.origin.x -= trans.toFrame.size.width;
+                        }
+                    } break;
+                    case UIInterfaceOrientationLandscapeRight: {
+                        if (CGRectGetMinX(trans.toFrame) != 0) {
+                            trans.toFrame.origin.x += trans.toFrame.size.width;
+                        }
+                    } break;
+                    default: break;
+                }
+            }
+        }
+    } else {
+        trans.toFrame = _observedToFrame;
+    }
+    
+    if (window && trans.toFrame.size.width > 0 && trans.toFrame.size.height > 0) {
+        CGRect rect = CGRectIntersection(window.bounds, trans.toFrame);
+        if (!CGRectIsNull(rect) && !CGRectIsEmpty(rect)) {
+            trans.toVisible = YES;
+        }
+    }
+    
+    if (!CGRectEqualToRect(trans.toFrame, _fromFrame)) {
+        for (id<YYTextKeyboardObserver> observer in _observers.copy) {
+            if ([observer respondsToSelector:@selector(keyboardChangedWithTransition:)]) {
+                [observer keyboardChangedWithTransition:trans];
+            }
+        }
+    }
+    
+    _hasNotification = NO;
+    _hasObservedChange = NO;
+    _fromFrame = trans.toFrame;
+    _fromVisible = trans.toVisible;
+    _fromOrientation = app.statusBarOrientation;
+}
+
+- (CGRect)convertRect:(CGRect)rect toView:(UIView *)view {
+    UIApplication *app = YYTextSharedApplication();
+    if (!app) return CGRectZero;
+    
+    if (CGRectIsNull(rect)) return rect;
+    if (CGRectIsInfinite(rect)) return rect;
+    
+    UIWindow *mainWindow = app.keyWindow;
+    if (!mainWindow) mainWindow = app.windows.firstObject;
+    if (!mainWindow) { // no window ?!
+        if (view) {
+            [view convertRect:rect fromView:nil];
+        } else {
+            return rect;
+        }
+    }
+    
+    rect = [mainWindow convertRect:rect fromWindow:nil];
+    if (!view) return [mainWindow convertRect:rect toWindow:nil];
+    if (view == mainWindow) return rect;
+    
+    UIWindow *toWindow = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
+    if (!mainWindow || !toWindow) return [mainWindow convertRect:rect toView:view];
+    if (mainWindow == toWindow) return [mainWindow convertRect:rect toView:view];
+    
+    // in different window
+    rect = [mainWindow convertRect:rect toView:mainWindow];
+    rect = [toWindow convertRect:rect fromWindow:mainWindow];
+    rect = [view convertRect:rect fromView:toWindow];
+    return rect;
+}
+
+@end

+ 571 - 0
Demo/Objective_C_Demo/YYText/YYTextLayout.h

@@ -0,0 +1,571 @@
+//
+//  YYTextLayout.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/3/3.
+//  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>
+#import <CoreText/CoreText.h>
+
+#if __has_include(<YYText/YYText.h>)
+#import <YYText/YYTextDebugOption.h>
+#import <YYText/YYTextLine.h>
+#import <YYText/YYTextInput.h>
+#else
+#import "YYTextDebugOption.h"
+#import "YYTextLine.h"
+#import "YYTextInput.h"
+#endif
+
+@protocol YYTextLinePositionModifier;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ The max text container size in layout.
+ */
+extern const CGSize YYTextContainerMaxSize;
+
+/**
+ The YYTextContainer class defines a region in which text is laid out.
+ YYTextLayout class uses one or more YYTextContainer objects to generate layouts.
+ 
+ A YYTextContainer 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    │
+     │                             │
+     └─────────────────────────────┘
+ */
+@interface YYTextContainer : NSObject <NSCoding, NSCopying>
+
+/// Creates a container with the specified size. @param size The size.
++ (instancetype)containerWithSize:(CGSize)size;
+
+/// Creates a container with the specified size and insets. @param size The size. @param insets The text insets.
++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets;
+
+/// Creates a container with the specified path. @param size The path.
++ (instancetype)containerWithPath:(nullable UIBezierPath *)path;
+
+/// The constrained size. (if the size is larger than YYTextContainerMaxSize, it will be clipped)
+@property CGSize size;
+
+/// The insets for constrained size. The inset value should not be negative. Default is UIEdgeInsetsZero.
+@property UIEdgeInsets insets;
+
+/// Custom constrained path. Set this property to ignore `size` and `insets`. Default is nil.
+@property (nullable, copy) UIBezierPath *path;
+
+/// An array of `UIBezierPath` for path exclusion. Default is nil.
+@property (nullable, copy) NSArray<UIBezierPath *> *exclusionPaths;
+
+/// Path line width. Default is 0;
+@property CGFloat pathLineWidth;
+
+/// 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;
+@property (getter=isPathFillEvenOdd) BOOL pathFillEvenOdd;
+
+/// Whether the text is vertical form (may used for CJK text layout). Default is NO.
+@property (getter=isVerticalForm) BOOL verticalForm;
+
+/// Maximum number of rows, 0 means no limit. Default is 0.
+@property NSUInteger maximumNumberOfRows;
+
+/// The line truncation type, default is none.
+@property YYTextTruncationType truncationType;
+
+/// The truncation token. If nil, the layout will use "…" instead. Default is nil.
+@property (nullable, copy) NSAttributedString *truncationToken;
+
+/// This modifier is applied to the lines before the layout is completed,
+/// give you a chance to modify the line position. Default is nil.
+@property (nullable, copy) id<YYTextLinePositionModifier> linePositionModifier;
+@end
+
+
+/**
+ The YYTextLinePositionModifier protocol declares the required method to modify
+ the line position in text layout progress. See `YYTextLinePositionSimpleModifier` for example.
+ */
+@protocol YYTextLinePositionModifier <NSObject, NSCopying>
+@required
+/**
+ This method will called before layout is completed. The method should be thread-safe.
+ @param lines     An array of YYTextLine.
+ @param text      The full text.
+ @param container The layout container.
+ */
+- (void)modifyLines:(NSArray<YYTextLine *> *)lines fromText:(NSAttributedString *)text inContainer:(YYTextContainer *)container;
+@end
+
+
+/**
+ A simple implementation of `YYTextLinePositionModifier`. It can fix each line's position
+ to a specified value, lets each line of height be the same.
+ */
+@interface YYTextLinePositionSimpleModifier : NSObject <YYTextLinePositionModifier>
+@property (assign) CGFloat fixedLineHeight; ///< The fixed line height (distance between two baseline).
+@end
+
+
+
+/**
+ YYTextLayout 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
+     └──────────────────────────┘
+ */
+@interface YYTextLayout : NSObject <NSCoding>
+
+
+#pragma mark - Generate text layout
+///=============================================================================
+/// @name Generate text layout
+///=============================================================================
+
+/**
+ Generate a layout with the given container size and text.
+
+ @param size The text container's size
+ @param text The text (if nil, returns nil).
+ @return A new layout, or nil when an error occurs.
+*/
++ (nullable YYTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)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.
+ */
++ (nullable YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)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).
+ @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.
+ */
++ (nullable YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range;
+
+/**
+ Generate layouts with the given containers and text.
+ 
+ @param containers An array of YYTextContainer object (if nil, returns nil).
+ @param text       The text (if nil, returns nil).
+ @return An array of YYTextLayout object (the count is same as containers),
+    or nil when an error occurs.
+ */
++ (nullable NSArray<YYTextLayout *> *)layoutWithContainers:(NSArray<YYTextContainer *> *)containers
+                                                      text:(NSAttributedString *)text;
+
+/**
+ Generate layouts with the given containers and text.
+ 
+ @param containers An array of YYTextContainer object (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 An array of YYTextLayout object (the count is same as containers),
+    or nil when an error occurs.
+ */
++ (nullable NSArray<YYTextLayout *> *)layoutWithContainers:(NSArray<YYTextContainer *> *)containers
+                                                      text:(NSAttributedString *)text
+                                                     range:(NSRange)range;
+
+- (instancetype)init UNAVAILABLE_ATTRIBUTE;
++ (instancetype)new UNAVAILABLE_ATTRIBUTE;
+
+
+#pragma mark - Text layout attributes
+///=============================================================================
+/// @name Text layout attributes
+///=============================================================================
+
+///< The text container
+@property (nonatomic, strong, readonly) YYTextContainer *container;
+///< The full text
+@property (nonatomic, strong, readonly) NSAttributedString *text;
+///< The text range in full text
+@property (nonatomic, readonly) NSRange range;
+///< CTFrameSetter
+@property (nonatomic, readonly) CTFramesetterRef frameSetter;
+///< CTFrame
+@property (nonatomic, readonly) CTFrameRef frame;
+///< Array of `YYTextLine`, no truncated
+@property (nonatomic, strong, readonly) NSArray<YYTextLine *> *lines;
+///< YYTextLine with truncated token, or nil
+@property (nullable, nonatomic, strong, readonly) YYTextLine *truncatedLine;
+///< Array of `YYTextAttachment`
+@property (nullable, nonatomic, strong, readonly) NSArray<YYTextAttachment *> *attachments;
+///< Array of NSRange(wrapped by NSValue) in text
+@property (nullable, nonatomic, strong, readonly) NSArray<NSValue *> *attachmentRanges;
+///< Array of CGRect(wrapped by NSValue) in container
+@property (nullable, nonatomic, strong, readonly) NSArray<NSValue *> *attachmentRects;
+///< Set of Attachment (UIImage/UIView/CALayer)
+@property (nullable, nonatomic, strong, readonly) NSSet *attachmentContentsSet;
+///< Number of rows
+@property (nonatomic, readonly) NSUInteger rowCount;
+///< Visible text range
+@property (nonatomic, readonly) NSRange visibleRange;
+///< Bounding rect (glyphs)
+@property (nonatomic, readonly) CGRect textBoundingRect;
+///< Bounding size (glyphs and insets, ceil to pixel)
+@property (nonatomic, readonly) CGSize textBoundingSize;
+///< Has highlight attribute
+@property (nonatomic, readonly) BOOL containsHighlight;
+///< Has block border attribute
+@property (nonatomic, readonly) BOOL needDrawBlockBorder;
+///< Has background border attribute
+@property (nonatomic, readonly) BOOL needDrawBackgroundBorder;
+///< Has shadow attribute
+@property (nonatomic, readonly) BOOL needDrawShadow;
+///< Has underline attribute
+@property (nonatomic, readonly) BOOL needDrawUnderline;
+///< Has visible text
+@property (nonatomic, readonly) BOOL needDrawText;
+///< Has attachment attribute
+@property (nonatomic, readonly) BOOL needDrawAttachment;
+///< Has inner shadow attribute
+@property (nonatomic, readonly) BOOL needDrawInnerShadow;
+///< Has strickthrough attribute
+@property (nonatomic, readonly) BOOL needDrawStrikethrough;
+///< Has border attribute
+@property (nonatomic, readonly) BOOL needDrawBorder;
+
+
+#pragma 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.
+ */
+- (NSUInteger)lineIndexForRow:(NSUInteger)row;
+
+/**
+ The number of lines for row.
+ 
+ @param row  A row index.
+ @return The number of lines, or NSNotFound when an error occurs.
+ */
+- (NSUInteger)lineCountForRow:(NSUInteger)row;
+
+/**
+ The row index for line.
+ 
+ @param line A row index.
+ 
+ @return The row index, or NSNotFound if not found.
+ */
+- (NSUInteger)rowIndexForLine:(NSUInteger)line;
+
+/**
+ 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.
+ */
+- (NSUInteger)lineIndexForPoint:(CGPoint)point;
+
+/**
+ 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.
+ */
+- (NSUInteger)closestLineIndexForPoint:(CGPoint)point;
+
+/**
+ 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 position   The text position in string.
+ @param lineIndex  The line index.
+ @return The offset in container, or CGFLOAT_MAX if not found.
+ */
+- (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)lineIndex;
+
+/**
+ 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.
+ */
+- (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex;
+
+/**
+ 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.
+ */
+- (nullable YYTextPosition *)closestPositionToPoint:(CGPoint)point;
+
+/**
+ 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.
+ */
+- (nullable YYTextPosition *)positionForPoint:(CGPoint)point
+                                  oldPosition:(YYTextPosition *)oldPosition
+                                otherPosition:(YYTextPosition *)otherPosition;
+
+/**
+ 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.
+ */
+- (nullable YYTextRange *)textRangeAtPoint:(CGPoint)point;
+
+/**
+ 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.
+ */
+- (nullable YYTextRange *)closestTextRangeAtPoint:(CGPoint)point;
+
+/**
+ 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
+ */
+- (nullable YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)position;
+
+/**
+ 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.
+ */
+- (nullable YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)position
+                                           inDirection:(UITextLayoutDirection)direction
+                                                offset:(NSInteger)offset;
+
+/**
+ 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.
+ */
+- (NSUInteger)lineIndexForPosition:(YYTextPosition *)position;
+
+/**
+ 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.
+ */
+- (CGPoint)linePositionForPosition:(YYTextPosition *)position;
+
+/**
+ 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.
+ */
+- (CGRect)caretRectForPosition:(YYTextPosition *)position;
+
+/**
+ 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.
+ */
+- (CGRect)firstRectForRange:(YYTextRange *)range;
+
+/**
+ 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.
+ */
+- (CGRect)rectForRange:(YYTextRange *)range;
+
+/**
+ 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 `YYTextSelectionRect` objects that encompass the selection.
+ If not found, the array is empty.
+ */
+- (NSArray<YYTextSelectionRect *> *)selectionRectsForRange:(YYTextRange *)range;
+
+/**
+ 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 `YYTextSelectionRect` objects that encompass the selection.
+ If not found, the array is empty.
+ */
+- (NSArray<YYTextSelectionRect *> *)selectionRectsWithoutStartAndEndForRange:(YYTextRange *)range;
+
+/**
+ 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 `YYTextSelectionRect` objects contains the start and end to
+ the selection. If not found, the array is empty.
+ */
+- (NSArray<YYTextSelectionRect *> *)selectionRectsWithOnlyStartAndEndForRange:(YYTextRange *)range;
+
+
+#pragma 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.
+ */
+- (void)drawInContext:(nullable CGContextRef)context
+                 size:(CGSize)size
+                point:(CGPoint)point
+                 view:(nullable UIView *)view
+                layer:(nullable CALayer *)layer
+                debug:(nullable YYTextDebugOption *)debug
+               cancel:(nullable BOOL (^)(void))cancel;
+
+/**
+ 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.
+ */
+- (void)drawInContext:(nullable CGContextRef)context
+                 size:(CGSize)size
+                debug:(nullable YYTextDebugOption *)debug;
+
+/**
+ 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.
+ */
+- (void)addAttachmentToView:(nullable UIView *)view layer:(nullable CALayer *)layer;
+
+/**
+ Remove attachment views and layers from their super container.
+ 
+ @warning This method must be called on main thread.
+ */
+- (void)removeAttachmentFromViewAndLayer;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 3361 - 0
Demo/Objective_C_Demo/YYText/YYTextLayout.m

@@ -0,0 +1,3361 @@
+//
+//  YYTextLayout.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/3/3.
+//  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 "YYTextLayout.h"
+#import "YYTextUtilities.h"
+#import "YYTextAttribute.h"
+#import "YYTextArchiver.h"
+#import "NSAttributedString+YYText.h"
+
+const CGSize YYTextContainerMaxSize = (CGSize){0x100000, 0x100000};
+
+typedef struct {
+    CGFloat head;
+    CGFloat foot;
+} YYRowEdge;
+
+static inline CGSize YYTextClipCGSize(CGSize size) {
+    if (size.width > YYTextContainerMaxSize.width) size.width = YYTextContainerMaxSize.width;
+    if (size.height > YYTextContainerMaxSize.height) size.height = YYTextContainerMaxSize.height;
+    return size;
+}
+
+static inline UIEdgeInsets UIEdgeInsetRotateVertical(UIEdgeInsets insets) {
+    UIEdgeInsets one;
+    one.top = insets.left;
+    one.left = insets.bottom;
+    one.bottom = insets.right;
+    one.right = insets.top;
+    return one;
+}
+
+/**
+ Sometimes CoreText may convert CGColor to UIColor for `kCTForegroundColorAttributeName`
+ attribute in iOS7. This should be a bug of CoreText, and may cause crash. Here's a workaround.
+ */
+static CGColorRef YYTextGetCGColor(CGColorRef color) {
+    static UIColor *defaultColor;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        defaultColor = [UIColor blackColor];
+    });
+    if (!color) return defaultColor.CGColor;
+    if ([((__bridge NSObject *)color) respondsToSelector:@selector(CGColor)]) {
+        return ((__bridge UIColor *)color).CGColor;
+    }
+    return color;
+}
+
+@implementation YYTextLinePositionSimpleModifier
+- (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(YYTextContainer *)container {
+    if (container.verticalForm) {
+        for (NSUInteger i = 0, max = lines.count; i < max; i++) {
+            YYTextLine *line = lines[i];
+            CGPoint pos = line.position;
+            pos.x = container.size.width - container.insets.right - line.row * _fixedLineHeight - _fixedLineHeight * 0.9;
+            line.position = pos;
+        }
+    } else {
+        for (NSUInteger i = 0, max = lines.count; i < max; i++) {
+            YYTextLine *line = lines[i];
+            CGPoint pos = line.position;
+            pos.y = line.row * _fixedLineHeight + _fixedLineHeight * 0.9 + container.insets.top;
+            line.position = pos;
+        }
+    }
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    YYTextLinePositionSimpleModifier *one = [self.class new];
+    one.fixedLineHeight = _fixedLineHeight;
+    return one;
+}
+@end
+
+
+@implementation YYTextContainer {
+    @package
+    BOOL _readonly; ///< used only in YYTextLayout.implementation
+    dispatch_semaphore_t _lock;
+    
+    CGSize _size;
+    UIEdgeInsets _insets;
+    UIBezierPath *_path;
+    NSArray *_exclusionPaths;
+    BOOL _pathFillEvenOdd;
+    CGFloat _pathLineWidth;
+    BOOL _verticalForm;
+    NSUInteger _maximumNumberOfRows;
+    YYTextTruncationType _truncationType;
+    NSAttributedString *_truncationToken;
+    id<YYTextLinePositionModifier> _linePositionModifier;
+}
+
++ (instancetype)containerWithSize:(CGSize)size {
+    return [self containerWithSize:size insets:UIEdgeInsetsZero];
+}
+
++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets {
+    YYTextContainer *one = [self new];
+    one.size = YYTextClipCGSize(size);
+    one.insets = insets;
+    return one;
+}
+
++ (instancetype)containerWithPath:(UIBezierPath *)path {
+    if (!path) return nil;
+    YYTextContainer *one = [self new];
+    one.path = path;
+    return one;
+}
+
+- (instancetype)init {
+    self = [super init];
+    if (!self) return nil;
+    _lock = dispatch_semaphore_create(1);
+    _pathFillEvenOdd = YES;
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    YYTextContainer *one = [self.class new];
+    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
+    one->_size = _size;
+    one->_insets = _insets;
+    one->_path = _path;
+    one->_exclusionPaths = _exclusionPaths.copy;
+    one->_pathFillEvenOdd = _pathFillEvenOdd;
+    one->_pathLineWidth = _pathLineWidth;
+    one->_verticalForm = _verticalForm;
+    one->_maximumNumberOfRows = _maximumNumberOfRows;
+    one->_truncationType = _truncationType;
+    one->_truncationToken = _truncationToken.copy;
+    one->_linePositionModifier = [(NSObject *)_linePositionModifier copy];
+    dispatch_semaphore_signal(_lock);
+    return one;
+}
+
+- (id)mutableCopyWithZone:(nullable NSZone *)zone {
+    return [self copyWithZone:zone];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:[NSValue valueWithCGSize:_size] forKey:@"size"];
+    [aCoder encodeObject:[NSValue valueWithUIEdgeInsets:_insets] forKey:@"insets"];
+    [aCoder encodeObject:_path forKey:@"path"];
+    [aCoder encodeObject:_exclusionPaths forKey:@"exclusionPaths"];
+    [aCoder encodeBool:_pathFillEvenOdd forKey:@"pathFillEvenOdd"];
+    [aCoder encodeDouble:_pathLineWidth forKey:@"pathLineWidth"];
+    [aCoder encodeBool:_verticalForm forKey:@"verticalForm"];
+    [aCoder encodeInteger:_maximumNumberOfRows forKey:@"maximumNumberOfRows"];
+    [aCoder encodeInteger:_truncationType forKey:@"truncationType"];
+    [aCoder encodeObject:_truncationToken forKey:@"truncationToken"];
+    if ([_linePositionModifier respondsToSelector:@selector(encodeWithCoder:)] &&
+        [_linePositionModifier respondsToSelector:@selector(initWithCoder:)]) {
+        [aCoder encodeObject:_linePositionModifier forKey:@"linePositionModifier"];
+    }
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [self init];
+    _size = ((NSValue *)[aDecoder decodeObjectForKey:@"size"]).CGSizeValue;
+    _insets = ((NSValue *)[aDecoder decodeObjectForKey:@"insets"]).UIEdgeInsetsValue;
+    _path = [aDecoder decodeObjectForKey:@"path"];
+    _exclusionPaths = [aDecoder decodeObjectForKey:@"exclusionPaths"];
+    _pathFillEvenOdd = [aDecoder decodeBoolForKey:@"pathFillEvenOdd"];
+    _pathLineWidth = [aDecoder decodeDoubleForKey:@"pathLineWidth"];
+    _verticalForm = [aDecoder decodeBoolForKey:@"verticalForm"];
+    _maximumNumberOfRows = [aDecoder decodeIntegerForKey:@"maximumNumberOfRows"];
+    _truncationType = [aDecoder decodeIntegerForKey:@"truncationType"];
+    _truncationToken = [aDecoder decodeObjectForKey:@"truncationToken"];
+    _linePositionModifier = [aDecoder decodeObjectForKey:@"linePositionModifier"];
+    return self;
+}
+
+#define Getter(...) \
+dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
+__VA_ARGS__; \
+dispatch_semaphore_signal(_lock);
+
+#define Setter(...) \
+if (_readonly) { \
+@throw [NSException exceptionWithName:NSInternalInconsistencyException \
+reason:@"Cannot change the property of the 'container' in 'YYTextLayout'." userInfo:nil]; \
+return; \
+} \
+dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
+__VA_ARGS__; \
+dispatch_semaphore_signal(_lock);
+
+- (CGSize)size {
+    Getter(CGSize size = _size) return size;
+}
+
+- (void)setSize:(CGSize)size {
+    Setter(if(!_path) _size = YYTextClipCGSize(size));
+}
+
+- (UIEdgeInsets)insets {
+    Getter(UIEdgeInsets insets = _insets) return insets;
+}
+
+- (void)setInsets:(UIEdgeInsets)insets {
+    Setter(if(!_path){
+        if (insets.top < 0) insets.top = 0;
+        if (insets.left < 0) insets.left = 0;
+        if (insets.bottom < 0) insets.bottom = 0;
+        if (insets.right < 0) insets.right = 0;
+        _insets = insets;
+    });
+}
+
+- (UIBezierPath *)path {
+    Getter(UIBezierPath *path = _path) return path;
+}
+
+- (void)setPath:(UIBezierPath *)path {
+    Setter(
+           _path = path.copy;
+           if (_path) {
+               CGRect bounds = _path.bounds;
+               CGSize size = bounds.size;
+               UIEdgeInsets insets = UIEdgeInsetsZero;
+               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;
+           }
+    );
+}
+
+- (NSArray *)exclusionPaths {
+    Getter(NSArray *paths = _exclusionPaths) return paths;
+}
+
+- (void)setExclusionPaths:(NSArray *)exclusionPaths {
+    Setter(_exclusionPaths = exclusionPaths.copy);
+}
+
+- (BOOL)isPathFillEvenOdd {
+    Getter(BOOL is = _pathFillEvenOdd) return is;
+}
+
+- (void)setPathFillEvenOdd:(BOOL)pathFillEvenOdd {
+    Setter(_pathFillEvenOdd = pathFillEvenOdd);
+}
+
+- (CGFloat)pathLineWidth {
+    Getter(CGFloat width = _pathLineWidth) return width;
+}
+
+- (void)setPathLineWidth:(CGFloat)pathLineWidth {
+    Setter(_pathLineWidth = pathLineWidth);
+}
+
+- (BOOL)isVerticalForm {
+    Getter(BOOL v = _verticalForm) return v;
+}
+
+- (void)setVerticalForm:(BOOL)verticalForm {
+    Setter(_verticalForm = verticalForm);
+}
+
+- (NSUInteger)maximumNumberOfRows {
+    Getter(NSUInteger num = _maximumNumberOfRows) return num;
+}
+
+- (void)setMaximumNumberOfRows:(NSUInteger)maximumNumberOfRows {
+    Setter(_maximumNumberOfRows = maximumNumberOfRows);
+}
+
+- (YYTextTruncationType)truncationType {
+    Getter(YYTextTruncationType type = _truncationType) return type;
+}
+
+- (void)setTruncationType:(YYTextTruncationType)truncationType {
+    Setter(_truncationType = truncationType);
+}
+
+- (NSAttributedString *)truncationToken {
+    Getter(NSAttributedString *token = _truncationToken) return token;
+}
+
+- (void)setTruncationToken:(NSAttributedString *)truncationToken {
+    Setter(_truncationToken = truncationToken.copy);
+}
+
+- (void)setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
+    Setter(_linePositionModifier = [(NSObject *)linePositionModifier copy]);
+}
+
+- (id<YYTextLinePositionModifier>)linePositionModifier {
+    Getter(id<YYTextLinePositionModifier> m = _linePositionModifier) return m;
+}
+
+#undef Getter
+#undef Setter
+@end
+
+
+
+
+@interface YYTextLayout ()
+
+@property (nonatomic, readwrite) YYTextContainer *container;
+@property (nonatomic, readwrite) NSAttributedString *text;
+@property (nonatomic, readwrite) NSRange range;
+
+@property (nonatomic, readwrite) CTFramesetterRef frameSetter;
+@property (nonatomic, readwrite) CTFrameRef frame;
+@property (nonatomic, readwrite) NSArray *lines;
+@property (nonatomic, readwrite) YYTextLine *truncatedLine;
+@property (nonatomic, readwrite) NSArray *attachments;
+@property (nonatomic, readwrite) NSArray *attachmentRanges;
+@property (nonatomic, readwrite) NSArray *attachmentRects;
+@property (nonatomic, readwrite) NSSet *attachmentContentsSet;
+@property (nonatomic, readwrite) NSUInteger rowCount;
+@property (nonatomic, readwrite) NSRange visibleRange;
+@property (nonatomic, readwrite) CGRect textBoundingRect;
+@property (nonatomic, readwrite) CGSize textBoundingSize;
+
+@property (nonatomic, readwrite) BOOL containsHighlight;
+@property (nonatomic, readwrite) BOOL needDrawBlockBorder;
+@property (nonatomic, readwrite) BOOL needDrawBackgroundBorder;
+@property (nonatomic, readwrite) BOOL needDrawShadow;
+@property (nonatomic, readwrite) BOOL needDrawUnderline;
+@property (nonatomic, readwrite) BOOL needDrawText;
+@property (nonatomic, readwrite) BOOL needDrawAttachment;
+@property (nonatomic, readwrite) BOOL needDrawInnerShadow;
+@property (nonatomic, readwrite) BOOL needDrawStrikethrough;
+@property (nonatomic, readwrite) BOOL needDrawBorder;
+
+@property (nonatomic, assign) NSUInteger *lineRowsIndex;
+@property (nonatomic, assign) YYRowEdge *lineRowsEdge; ///< top-left origin
+
+@end
+
+
+
+@implementation YYTextLayout
+
+#pragma mark - Layout
+
+- (instancetype)_init {
+    self = [super init];
+    return self;
+}
+
++ (YYTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text {
+    YYTextContainer *container = [YYTextContainer containerWithSize:size];
+    return [self layoutWithContainer:container text:text];
+}
+
++ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text {
+    return [self layoutWithContainer:container text:text range:NSMakeRange(0, text.length)];
+}
+
++ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range {
+    YYTextLayout *layout = NULL;
+    CGPathRef cgPath = nil;
+    CGRect cgPathBox = {0};
+    BOOL isVerticalForm = NO;
+    BOOL rowMaySeparated = NO;
+    NSMutableDictionary *frameAttrs = nil;
+    CTFramesetterRef ctSetter = NULL;
+    CTFrameRef ctFrame = NULL;
+    CFArrayRef ctLines = nil;
+    CGPoint *lineOrigins = NULL;
+    NSUInteger lineCount = 0;
+    NSMutableArray *lines = nil;
+    NSMutableArray *attachments = nil;
+    NSMutableArray *attachmentRanges = nil;
+    NSMutableArray *attachmentRects = nil;
+    NSMutableSet *attachmentContentsSet = nil;
+    BOOL needTruncation = NO;
+    NSAttributedString *truncationToken = nil;
+    YYTextLine *truncatedLine = nil;
+    YYRowEdge *lineRowsEdge = NULL;
+    NSUInteger *lineRowsIndex = NULL;
+    NSRange visibleRange;
+    NSUInteger maximumNumberOfRows = 0;
+    
+    text = text.mutableCopy;
+    container = container.copy;
+    if (!text || !container) return nil;
+    if (range.location + range.length > text.length) return nil;
+    container->_readonly = YES;
+    maximumNumberOfRows = container.maximumNumberOfRows;
+    
+    // CoreText bug when draw joined emoji since iOS 8.3.
+    // See -[NSMutableAttributedString setClearColorToJoinedEmoji] for more information.
+    static BOOL needFixJoinedEmojiBug = NO;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        double systemVersionDouble = [UIDevice currentDevice].systemVersion.doubleValue;
+        if (8.3 <= systemVersionDouble && systemVersionDouble < 9) {
+            needFixJoinedEmojiBug = YES;
+        }
+    });
+    if (needFixJoinedEmojiBug) {
+        [((NSMutableAttributedString *)text) yy_setClearColorToJoinedEmoji];
+    }
+    
+    layout = [[YYTextLayout alloc] _init];
+    layout.text = text;
+    layout.container = container;
+    layout.range = range;
+    isVerticalForm = container.verticalForm;
+    
+    // set cgPath and cgPathBox
+    if (container.path == nil && container.exclusionPaths.count == 0) {
+        CGRect rect = (CGRect) {CGPointZero, container.size };
+        rect = UIEdgeInsetsInsetRect(rect, container.insets);
+        rect = CGRectStandardize(rect);
+        cgPathBox = rect;
+        rect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeScale(1, -1));
+        cgPath = CGPathCreateWithRect(rect, NULL); // let CGPathIsRect() returns true
+    } else if (container.path && CGPathIsRect(container.path.CGPath, &cgPathBox) && container.exclusionPaths.count == 0) {
+        CGRect rect = CGRectApplyAffineTransform(cgPathBox, CGAffineTransformMakeScale(1, -1));
+        cgPath = CGPathCreateWithRect(rect, NULL); // let CGPathIsRect() returns true
+    } else {
+        rowMaySeparated = YES;
+        CGMutablePathRef path = NULL;
+        if (container.path) {
+            path = CGPathCreateMutableCopy(container.path.CGPath);
+        } else {
+            CGRect rect = (CGRect) {CGPointZero, container.size };
+            rect = UIEdgeInsetsInsetRect(rect, container.insets);
+            CGPathRef rectPath = CGPathCreateWithRect(rect, NULL);
+            if (rectPath) {
+                path = CGPathCreateMutableCopy(rectPath);
+                CGPathRelease(rectPath);
+            }
+        }
+        if (path) {
+            [layout.container.exclusionPaths enumerateObjectsUsingBlock: ^(UIBezierPath *onePath, NSUInteger idx, BOOL *stop) {
+                CGPathAddPath(path, NULL, onePath.CGPath);
+            }];
+            
+            cgPathBox = CGPathGetPathBoundingBox(path);
+            CGAffineTransform trans = CGAffineTransformMakeScale(1, -1);
+            CGMutablePathRef transPath = CGPathCreateMutableCopyByTransformingPath(path, &trans);
+            CGPathRelease(path);
+            path = transPath;
+        }
+        cgPath = path;
+    }
+    if (!cgPath) goto fail;
+    
+    // frame setter config
+    frameAttrs = [NSMutableDictionary dictionary];
+    if (container.isPathFillEvenOdd == NO) {
+        frameAttrs[(id)kCTFramePathFillRuleAttributeName] = @(kCTFramePathFillWindingNumber);
+    }
+    if (container.pathLineWidth > 0) {
+        frameAttrs[(id)kCTFramePathWidthAttributeName] = @(container.pathLineWidth);
+    }
+    if (container.isVerticalForm == YES) {
+        frameAttrs[(id)kCTFrameProgressionAttributeName] = @(kCTFrameProgressionRightToLeft);
+    }
+    
+    // create CoreText objects
+    ctSetter = CTFramesetterCreateWithAttributedString((CFTypeRef)text);
+    if (!ctSetter) goto fail;
+    ctFrame = CTFramesetterCreateFrame(ctSetter, YYTextCFRangeFromNSRange(range), cgPath, (CFTypeRef)frameAttrs);
+    if (!ctFrame) goto fail;
+    lines = [NSMutableArray new];
+    ctLines = CTFrameGetLines(ctFrame);
+    lineCount = CFArrayGetCount(ctLines);
+    if (lineCount > 0) {
+        lineOrigins = malloc(lineCount * sizeof(CGPoint));
+        if (lineOrigins == NULL) goto fail;
+        CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, lineCount), lineOrigins);
+    }
+    
+    CGRect textBoundingRect = CGRectZero;
+    CGSize textBoundingSize = CGSizeZero;
+    NSInteger rowIdx = -1;
+    NSUInteger rowCount = 0;
+    CGRect lastRect = CGRectMake(0, -FLT_MAX, 0, 0);
+    CGPoint lastPosition = CGPointMake(0, -FLT_MAX);
+    if (isVerticalForm) {
+        lastRect = CGRectMake(FLT_MAX, 0, 0, 0);
+        lastPosition = CGPointMake(FLT_MAX, 0);
+    }
+    
+    // calculate line frame
+    NSUInteger lineCurrentIdx = 0;
+    for (NSUInteger i = 0; i < lineCount; i++) {
+        CTLineRef ctLine = CFArrayGetValueAtIndex(ctLines, i);
+        CFArrayRef ctRuns = CTLineGetGlyphRuns(ctLine);
+        if (!ctRuns || CFArrayGetCount(ctRuns) == 0) continue;
+        
+        // CoreText coordinate system
+        CGPoint ctLineOrigin = lineOrigins[i];
+        
+        // UIKit coordinate system
+        CGPoint position;
+        position.x = cgPathBox.origin.x + ctLineOrigin.x;
+        position.y = cgPathBox.size.height + cgPathBox.origin.y - ctLineOrigin.y;
+        
+        YYTextLine *line = [YYTextLine lineWithCTLine:ctLine position:position vertical:isVerticalForm];
+        CGRect rect = line.bounds;
+        BOOL newRow = YES;
+        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 = NO;
+                } else {
+                    if (lastRect.origin.x > position.x && position.x > lastRect.origin.x - lastRect.size.width) newRow = NO;
+                }
+            } else {
+                if (rect.size.height > lastRect.size.height) {
+                    if (rect.origin.y < lastPosition.y && lastPosition.y < rect.origin.y + rect.size.height) newRow = NO;
+                } else {
+                    if (lastRect.origin.y < position.y && position.y < lastRect.origin.y + lastRect.size.height) newRow = NO;
+                }
+            }
+        }
+        
+        if (newRow) rowIdx++;
+        lastRect = rect;
+        lastPosition = position;
+        
+        line.index = lineCurrentIdx;
+        line.row = rowIdx;
+        [lines addObject:line];
+        rowCount = rowIdx + 1;
+        lineCurrentIdx ++;
+        
+        if (i == 0) textBoundingRect = rect;
+        else {
+            if (maximumNumberOfRows == 0 || rowIdx < maximumNumberOfRows) {
+                textBoundingRect = CGRectUnion(textBoundingRect, rect);
+            }
+        }
+    }
+    
+    if (rowCount > 0) {
+        if (maximumNumberOfRows > 0) {
+            if (rowCount > maximumNumberOfRows) {
+                needTruncation = YES;
+                rowCount = maximumNumberOfRows;
+                do {
+                    YYTextLine *line = lines.lastObject;
+                    if (!line) break;
+                    if (line.row < rowCount) break;
+                    [lines removeLastObject];
+                } while (1);
+            }
+        }
+        YYTextLine *lastLine = lines.lastObject;
+        if (!needTruncation && lastLine.range.location + lastLine.range.length < text.length) {
+            needTruncation = YES;
+        }
+        
+        // Give user a chance to modify the line's position.
+        if (container.linePositionModifier) {
+            [container.linePositionModifier modifyLines:lines fromText:text inContainer:container];
+            textBoundingRect = CGRectZero;
+            for (NSUInteger i = 0, max = lines.count; i < max; i++) {
+                YYTextLine *line = lines[i];
+                if (i == 0) textBoundingRect = line.bounds;
+                else textBoundingRect = CGRectUnion(textBoundingRect, line.bounds);
+            }
+        }
+        
+        lineRowsEdge = calloc(rowCount, sizeof(YYRowEdge));
+        if (lineRowsEdge == NULL) goto fail;
+        lineRowsIndex = calloc(rowCount, sizeof(NSUInteger));
+        if (lineRowsIndex == NULL) goto fail;
+        NSInteger lastRowIdx = -1;
+        CGFloat lastHead = 0;
+        CGFloat lastFoot = 0;
+        for (NSUInteger i = 0, max = lines.count; i < max; i++) {
+            YYTextLine *line = lines[i];
+            CGRect rect = line.bounds;
+            if ((NSInteger)line.row != lastRowIdx) {
+                if (lastRowIdx >= 0) {
+                    lineRowsEdge[lastRowIdx] = (YYRowEdge) {.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);
+                }
+            }
+        }
+        lineRowsEdge[lastRowIdx] = (YYRowEdge) {.head = lastHead, .foot = lastFoot };
+        
+        for (NSUInteger i = 1; i < rowCount; i++) {
+            YYRowEdge v0 = lineRowsEdge[i - 1];
+            YYRowEdge v1 = lineRowsEdge[i];
+            lineRowsEdge[i - 1].foot = lineRowsEdge[i].head = (v0.foot + v1.head) * 0.5;
+        }
+    }
+    
+    { // calculate bounding size
+        CGRect rect = textBoundingRect;
+        if (container.path) {
+            if (container.pathLineWidth > 0) {
+                CGFloat inset = container.pathLineWidth / 2;
+                rect = CGRectInset(rect, -inset, -inset);
+            }
+        } else {
+            rect = UIEdgeInsetsInsetRect(rect,YYTextUIEdgeInsetsInvert(container.insets));
+        }
+        rect = CGRectStandardize(rect);
+        CGSize size = rect.size;
+        if (container.verticalForm) {
+            size.width += container.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 = YYTextCGFloatPixelCeil(size.width);
+        size.height = YYTextCGFloatPixelCeil(size.height);
+        textBoundingSize = size;
+    }
+    
+    visibleRange = YYTextNSRangeFromCFRange(CTFrameGetVisibleStringRange(ctFrame));
+    if (needTruncation) {
+        YYTextLine *lastLine = lines.lastObject;
+        NSRange lastRange = lastLine.range;
+        visibleRange.length = lastRange.location + lastRange.length - visibleRange.location;
+        
+        // create truncated line
+        if (container.truncationType != YYTextTruncationTypeNone) {
+            CTLineRef truncationTokenLine = NULL;
+            if (container.truncationToken) {
+                truncationToken = container.truncationToken;
+                truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationToken);
+            } else {
+                CFArrayRef runs = CTLineGetGlyphRuns(lastLine.CTLine);
+                NSUInteger runCount = CFArrayGetCount(runs);
+                NSMutableDictionary *attrs = nil;
+                if (runCount > 0) {
+                    CTRunRef run = CFArrayGetValueAtIndex(runs, runCount - 1);
+                    attrs = (id)CTRunGetAttributes(run);
+                    attrs = attrs.mutableCopy;
+                    [attrs removeObjectForKey:YYTextAttachmentAttributeName];
+                    CTFontRef font = (__bridge CFTypeRef)attrs[(id)kCTFontAttributeName];
+                    CGFloat fontSize = font ? CTFontGetSize(font) : 12.0;
+                    UIFont *uiFont = [UIFont systemFontOfSize:fontSize * 0.9];
+                    if (uiFont) {
+                        font = CTFontCreateWithName((__bridge CFStringRef)uiFont.fontName, uiFont.pointSize, NULL);
+                    } else {
+                        font = NULL;
+                    }
+                    if (font) {
+                        attrs[(id)kCTFontAttributeName] = (__bridge id)(font);
+                        uiFont = nil;
+                        CFRelease(font);
+                    }
+                    if (!attrs) attrs = [NSMutableDictionary new];
+                }
+                truncationToken = [[NSAttributedString alloc] initWithString:YYTextTruncationToken attributes:attrs];
+                truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationToken);
+            }
+            if (truncationTokenLine) {
+                CTLineTruncationType type = kCTLineTruncationEnd;
+                if (container.truncationType == YYTextTruncationTypeStart) {
+                    type = kCTLineTruncationStart;
+                } else if (container.truncationType == YYTextTruncationTypeMiddle) {
+                    type = kCTLineTruncationMiddle;
+                }
+                NSMutableAttributedString *lastLineText = [text attributedSubstringFromRange:lastLine.range].mutableCopy;
+                [lastLineText appendAttributedString:truncationToken];
+                CTLineRef ctLastLineExtend = CTLineCreateWithAttributedString((CFAttributedStringRef)lastLineText);
+                if (ctLastLineExtend) {
+                    CGFloat truncatedWidth = lastLine.width;
+                    CGRect cgPathRect = CGRectZero;
+                    if (CGPathIsRect(cgPath, &cgPathRect)) {
+                        if (isVerticalForm) {
+                            truncatedWidth = cgPathRect.size.height;
+                        } else {
+                            truncatedWidth = cgPathRect.size.width;
+                        }
+                    }
+                    CTLineRef ctTruncatedLine = CTLineCreateTruncatedLine(ctLastLineExtend, truncatedWidth, type, truncationTokenLine);
+                    CFRelease(ctLastLineExtend);
+                    if (ctTruncatedLine) {
+                        truncatedLine = [YYTextLine lineWithCTLine:ctTruncatedLine position:lastLine.position vertical:isVerticalForm];
+                        truncatedLine.index = lastLine.index;
+                        truncatedLine.row = lastLine.row;
+                        CFRelease(ctTruncatedLine);
+                    }
+                }
+                CFRelease(truncationTokenLine);
+            }
+        }
+    }
+    
+    if (isVerticalForm) {
+        NSCharacterSet *rotateCharset = YYTextVerticalFormRotateCharacterSet();
+        NSCharacterSet *rotateMoveCharset = YYTextVerticalFormRotateAndMoveCharacterSet();
+        
+        void (^lineBlock)(YYTextLine *) = ^(YYTextLine *line){
+            CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+            if (!runs) return;
+            NSUInteger runCount = CFArrayGetCount(runs);
+            if (runCount == 0) return;
+            NSMutableArray *lineRunRanges = [NSMutableArray new];
+            line.verticalRotateRange = lineRunRanges;
+            for (NSUInteger r = 0; r < runCount; r++) {
+                CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+                NSMutableArray *runRanges = [NSMutableArray new];
+                [lineRunRanges addObject:runRanges];
+                NSUInteger glyphCount = CTRunGetGlyphCount(run);
+                if (glyphCount == 0) continue;
+                
+                CFIndex runStrIdx[glyphCount + 1];
+                CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx);
+                CFRange runStrRange = CTRunGetStringRange(run);
+                runStrIdx[glyphCount] = runStrRange.location + runStrRange.length;
+                CFDictionaryRef runAttrs = CTRunGetAttributes(run);
+                CTFontRef font = CFDictionaryGetValue(runAttrs, kCTFontAttributeName);
+                BOOL isColorGlyph = YYTextCTFontContainsColorBitmapGlyphs(font);
+                
+                NSUInteger prevIdx = 0;
+                YYTextRunGlyphDrawMode prevMode = YYTextRunGlyphDrawModeHorizontal;
+                NSString *layoutStr = layout.text.string;
+                for (NSUInteger g = 0; g < glyphCount; g++) {
+                    BOOL glyphRotate = 0, glyphRotateMove = NO;
+                    CFIndex runStrLen = runStrIdx[g + 1] - runStrIdx[g];
+                    if (isColorGlyph) {
+                        glyphRotate = YES;
+                    } else if (runStrLen == 1) {
+                        unichar c = [layoutStr characterAtIndex:runStrIdx[g]];
+                        glyphRotate = [rotateCharset characterIsMember:c];
+                        if (glyphRotate) glyphRotateMove = [rotateMoveCharset characterIsMember:c];
+                    } else if (runStrLen > 1){
+                        NSString *glyphStr = [layoutStr substringWithRange:NSMakeRange(runStrIdx[g], runStrLen)];
+                        BOOL glyphRotate = [glyphStr rangeOfCharacterFromSet:rotateCharset].location != NSNotFound;
+                        if (glyphRotate) glyphRotateMove = [glyphStr rangeOfCharacterFromSet:rotateMoveCharset].location != NSNotFound;
+                    }
+                    
+                    YYTextRunGlyphDrawMode mode = glyphRotateMove ? YYTextRunGlyphDrawModeVerticalRotateMove : (glyphRotate ? YYTextRunGlyphDrawModeVerticalRotate : YYTextRunGlyphDrawModeHorizontal);
+                    if (g == 0) {
+                        prevMode = mode;
+                    } else if (mode != prevMode) {
+                        YYTextRunGlyphRange *aRange = [YYTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, g - prevIdx) drawMode:prevMode];
+                        [runRanges addObject:aRange];
+                        prevIdx = g;
+                        prevMode = mode;
+                    }
+                }
+                if (prevIdx < glyphCount) {
+                    YYTextRunGlyphRange *aRange = [YYTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, glyphCount - prevIdx) drawMode:prevMode];
+                    [runRanges addObject:aRange];
+                }
+                
+            }
+        };
+        for (YYTextLine *line in lines) {
+            lineBlock(line);
+        }
+        if (truncatedLine) lineBlock(truncatedLine);
+    }
+    
+    if (visibleRange.length > 0) {
+        layout.needDrawText = YES;
+        
+        void (^block)(NSDictionary *attrs, NSRange range, BOOL *stop) = ^(NSDictionary *attrs, NSRange range, BOOL *stop) {
+            if (attrs[YYTextHighlightAttributeName]) layout.containsHighlight = YES;
+            if (attrs[YYTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES;
+            if (attrs[YYTextBackgroundBorderAttributeName]) layout.needDrawBackgroundBorder = YES;
+            if (attrs[YYTextShadowAttributeName] || attrs[NSShadowAttributeName]) layout.needDrawShadow = YES;
+            if (attrs[YYTextUnderlineAttributeName]) layout.needDrawUnderline = YES;
+            if (attrs[YYTextAttachmentAttributeName]) layout.needDrawAttachment = YES;
+            if (attrs[YYTextInnerShadowAttributeName]) layout.needDrawInnerShadow = YES;
+            if (attrs[YYTextStrikethroughAttributeName]) layout.needDrawStrikethrough = YES;
+            if (attrs[YYTextBorderAttributeName]) layout.needDrawBorder = YES;
+        };
+        
+        [layout.text enumerateAttributesInRange:visibleRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block];
+        if (truncatedLine) {
+            [truncationToken enumerateAttributesInRange:NSMakeRange(0, truncationToken.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block];
+        }
+    }
+    
+    attachments = [NSMutableArray new];
+    attachmentRanges = [NSMutableArray new];
+    attachmentRects = [NSMutableArray new];
+    attachmentContentsSet = [NSMutableSet new];
+    for (NSUInteger i = 0, max = lines.count; i < max; i++) {
+        YYTextLine *line = lines[i];
+        if (truncatedLine && line.index == truncatedLine.index) line = truncatedLine;
+        if (line.attachments.count > 0) {
+            [attachments addObjectsFromArray:line.attachments];
+            [attachmentRanges addObjectsFromArray:line.attachmentRanges];
+            [attachmentRects addObjectsFromArray:line.attachmentRects];
+            for (YYTextAttachment *attachment in line.attachments) {
+                if (attachment.content) {
+                    [attachmentContentsSet addObject:attachment.content];
+                }
+            }
+        }
+    }
+    if (attachments.count == 0) {
+        attachments = attachmentRanges = attachmentRects = nil;
+    }
+    
+    layout.frameSetter = ctSetter;
+    layout.frame = ctFrame;
+    layout.lines = lines;
+    layout.truncatedLine = truncatedLine;
+    layout.attachments = attachments;
+    layout.attachmentRanges = attachmentRanges;
+    layout.attachmentRects = attachmentRects;
+    layout.attachmentContentsSet = attachmentContentsSet;
+    layout.rowCount = rowCount;
+    layout.visibleRange = visibleRange;
+    layout.textBoundingRect = textBoundingRect;
+    layout.textBoundingSize = textBoundingSize;
+    layout.lineRowsEdge = lineRowsEdge;
+    layout.lineRowsIndex = lineRowsIndex;
+    CFRelease(cgPath);
+    CFRelease(ctSetter);
+    CFRelease(ctFrame);
+    if (lineOrigins) free(lineOrigins);
+    return layout;
+    
+fail:
+    if (cgPath) CFRelease(cgPath);
+    if (ctSetter) CFRelease(ctSetter);
+    if (ctFrame) CFRelease(ctFrame);
+    if (lineOrigins) free(lineOrigins);
+    if (lineRowsEdge) free(lineRowsEdge);
+    if (lineRowsIndex) free(lineRowsIndex);
+    return nil;
+}
+
++ (NSArray *)layoutWithContainers:(NSArray *)containers text:(NSAttributedString *)text {
+    return [self layoutWithContainers:containers text:text range:NSMakeRange(0, text.length)];
+}
+
++ (NSArray *)layoutWithContainers:(NSArray *)containers text:(NSAttributedString *)text range:(NSRange)range {
+    if (!containers || !text) return nil;
+    if (range.location + range.length > text.length) return nil;
+    NSMutableArray *layouts = [NSMutableArray array];
+    for (NSUInteger i = 0, max = containers.count; i < max; i++) {
+        YYTextContainer *container = containers[i];
+        YYTextLayout *layout = [self layoutWithContainer:container text:text range:range];
+        if (!layout) return nil;
+        NSInteger length = (NSInteger)range.length - (NSInteger)layout.visibleRange.length;
+        if (length <= 0) {
+            range.length = 0;
+            range.location = text.length;
+        } else {
+            range.length = length;
+            range.location += layout.visibleRange.length;
+        }
+    }
+    return layouts;
+}
+
+- (void)setFrameSetter:(CTFramesetterRef)frameSetter {
+    if (_frameSetter != frameSetter) {
+        if (frameSetter) CFRetain(frameSetter);
+        if (_frameSetter) CFRelease(_frameSetter);
+        _frameSetter = frameSetter;
+    }
+}
+
+- (void)setFrame:(CTFrameRef)frame {
+    if (_frame != frame) {
+        if (frame) CFRetain(frame);
+        if (_frame) CFRelease(_frame);
+        _frame = frame;
+    }
+}
+
+- (void)dealloc {
+    if (_frameSetter) CFRelease(_frameSetter);
+    if (_frame) CFRelease(_frame);
+    if (_lineRowsIndex) free(_lineRowsIndex);
+    if (_lineRowsEdge) free(_lineRowsEdge);
+}
+
+#pragma mark - Coding
+
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    NSData *textData = [YYTextArchiver archivedDataWithRootObject:_text];
+    [aCoder encodeObject:textData forKey:@"text"];
+    [aCoder encodeObject:_container forKey:@"container"];
+    [aCoder encodeObject:[NSValue valueWithRange:_range] forKey:@"range"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    NSData *textData = [aDecoder decodeObjectForKey:@"text"];
+    NSAttributedString *text = [YYTextUnarchiver unarchiveObjectWithData:textData];
+    YYTextContainer *container = [aDecoder decodeObjectForKey:@"container"];
+    NSRange range = ((NSValue *)[aDecoder decodeObjectForKey:@"range"]).rangeValue;
+    self = [self.class layoutWithContainer:container text:text range:range];
+    return self;
+}
+
+#pragma mark - Copying
+
+- (id)copyWithZone:(NSZone *)zone {
+    return self; // readonly object
+}
+
+
+#pragma 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.
+ */
+- (NSUInteger)_rowIndexForEdge:(CGFloat)edge {
+    if (_rowCount == 0) return NSNotFound;
+    BOOL isVertical = _container.verticalForm;
+    NSUInteger lo = 0, hi = _rowCount - 1, mid = 0;
+    NSUInteger rowIdx = NSNotFound;
+    while (lo <= hi) {
+        mid = (lo + hi) / 2;
+        YYRowEdge 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.
+ */
+- (NSUInteger)_closestRowIndexForEdge:(CGFloat)edge {
+    if (_rowCount == 0) return NSNotFound;
+    NSUInteger rowIdx = [self _rowIndexForEdge:edge];
+    if (rowIdx == NSNotFound) {
+        if (_container.verticalForm) {
+            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).
+ */
+- (CTRunRef)_runForLine:(YYTextLine *)line position:(YYTextPosition *)position {
+    if (!line || !position) return NULL;
+    CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+    for (NSUInteger i = 0, max = CFArrayGetCount(runs); i < max; i++) {
+        CTRunRef run = CFArrayGetValueAtIndex(runs, i);
+        CFRange range = CTRunGetStringRange(run);
+        if (position.affinity == YYTextAffinityBackward) {
+            if (range.location < position.offset && position.offset <= range.location + range.length) {
+                return run;
+            }
+        } else {
+            if (range.location <= position.offset && position.offset < range.location + range.length) {
+                return run;
+            }
+        }
+    }
+    return NULL;
+}
+
+/**
+ 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
+ */
+- (BOOL)_insideComposedCharacterSequences:(YYTextLine *)line position:(NSUInteger)position block:(void (^)(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next))block {
+    NSRange range = line.range;
+    if (range.length == 0) return NO;
+    __block BOOL inside = NO;
+    __block NSUInteger _prev, _next;
+    [_text.string enumerateSubstringsInRange:range options:NSStringEnumerationByComposedCharacterSequences usingBlock: ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
+        NSUInteger prev = substringRange.location;
+        NSUInteger next = substringRange.location + substringRange.length;
+        if (prev == position || next == position) {
+            *stop = YES;
+        }
+        if (prev < position && position < next) {
+            inside = YES;
+            _prev = prev;
+            _next = next;
+            *stop = YES;
+        }
+    }];
+    if (inside && block) {
+        CGFloat left = [self offsetForTextPosition:_prev lineIndex:line.index];
+        CGFloat right = [self offsetForTextPosition:_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
+ */
+- (BOOL)_insideEmoji:(YYTextLine *)line position:(NSUInteger)position block:(void (^)(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next))block {
+    if (!line) return NO;
+    CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+    for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
+        CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+        NSUInteger glyphCount = CTRunGetGlyphCount(run);
+        if (glyphCount == 0) continue;
+        CFRange range = CTRunGetStringRange(run);
+        if (range.length <= 1) continue;
+        if (position <= range.location || position >= range.location + range.length) continue;
+        CFDictionaryRef attrs = CTRunGetAttributes(run);
+        CTFontRef font = CFDictionaryGetValue(attrs, kCTFontAttributeName);
+        if (!YYTextCTFontContainsColorBitmapGlyphs(font)) continue;
+        
+        // Here's Emoji runs (larger than 1 unichar), and position is inside the range.
+        CFIndex indices[glyphCount];
+        CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), indices);
+        for (NSUInteger g = 0; g < glyphCount; g++) {
+            CFIndex prev = indices[g];
+            CFIndex next = 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)
+                CGPoint pos = CGPointZero;
+                CGSize adv = CGSizeZero;
+                CTRunGetPositions(run, CFRangeMake(g, 1), &pos);
+                CTRunGetAdvances(run, CFRangeMake(g, 1), &adv);
+                if (block) {
+                    block(line.position.x + pos.x,
+                          line.position.x + pos.x + adv.width,
+                          prev, next);
+                }
+                return YES;
+            }
+        }
+    }
+    return NO;
+}
+/**
+ 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.
+ */
+- (BOOL)_isRightToLeftInLine:(YYTextLine *)line atPoint:(CGPoint)point {
+    if (!line) return NO;
+    // get write direction
+    BOOL RTL = NO;
+    CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+    for (NSUInteger r = 0, max = CFArrayGetCount(runs); r < max; r++) {
+        CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+        CGPoint glyphPosition;
+        CTRunGetPositions(run, CFRangeMake(0, 1), &glyphPosition);
+        if (_container.verticalForm) {
+            CGFloat runX = glyphPosition.x;
+            runX += line.position.y;
+            CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL);
+            if (runX <= point.y && point.y <= runX + runWidth) {
+                if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES;
+                break;
+            }
+        } else {
+            CGFloat runX = glyphPosition.x;
+            runX += line.position.x;
+            CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL);
+            if (runX <= point.x && point.x <= runX + runWidth) {
+                if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES;
+                break;
+            }
+        }
+    }
+    return RTL;
+}
+
+/**
+ Correct the range's edge.
+ */
+- (YYTextRange *)_correctedRangeWithEdge:(YYTextRange *)range {
+    NSRange visibleRange = self.visibleRange;
+    YYTextPosition *start = range.start;
+    YYTextPosition *end = range.end;
+    
+    if (start.offset == visibleRange.location && start.affinity == YYTextAffinityBackward) {
+        start = [YYTextPosition positionWithOffset:start.offset affinity:YYTextAffinityForward];
+    }
+    
+    if (end.offset == visibleRange.location + visibleRange.length && start.affinity == YYTextAffinityForward) {
+        end = [YYTextPosition positionWithOffset:end.offset affinity:YYTextAffinityBackward];
+    }
+    
+    if (start != range.start || end != range.end) {
+        range = [YYTextRange rangeWithStart:start end:end];
+    }
+    return range;
+}
+
+- (NSUInteger)lineIndexForRow:(NSUInteger)row {
+    if (row >= _rowCount) return NSNotFound;
+    return _lineRowsIndex[row];
+}
+
+- (NSUInteger)lineCountForRow:(NSUInteger)row {
+    if (row >= _rowCount) return NSNotFound;
+    if (row == _rowCount - 1) {
+        return _lines.count - _lineRowsIndex[row];
+    } else {
+        return _lineRowsIndex[row + 1] - _lineRowsIndex[row];
+    }
+}
+
+- (NSUInteger)rowIndexForLine:(NSUInteger)line {
+    if (line >= _lines.count) return NSNotFound;
+    return ((YYTextLine *)_lines[line]).row;
+}
+
+- (NSUInteger)lineIndexForPoint:(CGPoint)point {
+    if (_lines.count == 0 || _rowCount == 0) return NSNotFound;
+    NSUInteger rowIdx = [self _rowIndexForEdge:_container.verticalForm ? point.x : point.y];
+    if (rowIdx == NSNotFound) return NSNotFound;
+    
+    NSUInteger lineIdx0 = _lineRowsIndex[rowIdx];
+    NSUInteger lineIdx1 = rowIdx == _rowCount - 1 ? _lines.count - 1 : _lineRowsIndex[rowIdx + 1] - 1;
+    for (NSUInteger i = lineIdx0; i <= lineIdx1; i++) {
+        CGRect bounds = ((YYTextLine *)_lines[i]).bounds;
+        if (CGRectContainsPoint(bounds, point)) return i;
+    }
+    
+    return NSNotFound;
+}
+
+- (NSUInteger)closestLineIndexForPoint:(CGPoint)point {
+    BOOL isVertical = _container.verticalForm;
+    if (_lines.count == 0 || _rowCount == 0) return NSNotFound;
+    NSUInteger rowIdx = [self _closestRowIndexForEdge:isVertical ? point.x : point.y];
+    if (rowIdx == NSNotFound) return NSNotFound;
+    
+    NSUInteger lineIdx0 = _lineRowsIndex[rowIdx];
+    NSUInteger lineIdx1 = rowIdx == _rowCount - 1 ? _lines.count - 1 : _lineRowsIndex[rowIdx + 1] - 1;
+    if (lineIdx0 == lineIdx1) return lineIdx0;
+    
+    CGFloat minDistance = CGFLOAT_MAX;
+    NSUInteger minIndex = lineIdx0;
+    for (NSUInteger i = lineIdx0; i <= lineIdx1; i++) {
+        CGRect bounds = ((YYTextLine *)_lines[i]).bounds;
+        if (isVertical) {
+            if (bounds.origin.y <= point.y && point.y <= bounds.origin.y + bounds.size.height) return i;
+            CGFloat distance;
+            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;
+            CGFloat distance;
+            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;
+}
+
+- (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)lineIndex {
+    if (lineIndex >= _lines.count) return CGFLOAT_MAX;
+    YYTextLine *line = _lines[lineIndex];
+    CFRange range = CTLineGetStringRange(line.CTLine);
+    if (position < range.location || position > range.location + range.length) return CGFLOAT_MAX;
+    
+    CGFloat offset = CTLineGetOffsetForStringIndex(line.CTLine, position, NULL);
+    return _container.verticalForm ? (offset + line.position.y) : (offset + line.position.x);
+}
+
+- (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex {
+    if (lineIndex >= _lines.count) return NSNotFound;
+    YYTextLine *line = _lines[lineIndex];
+    if (_container.verticalForm) {
+        point.x = point.y - line.position.y;
+        point.y = 0;
+    } else {
+        point.x -= line.position.x;
+        point.y = 0;
+    }
+    CFIndex idx = 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.
+     */
+    CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+    for (NSUInteger r = 0, max = CFArrayGetCount(runs); r < max; r++) {
+        CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+        CFRange range = CTRunGetStringRange(run);
+        if (range.location <= idx && idx < range.location + range.length) {
+            NSUInteger glyphCount = CTRunGetGlyphCount(run);
+            if (glyphCount == 0) break;
+            CFDictionaryRef attrs = CTRunGetAttributes(run);
+            CTFontRef font = CFDictionaryGetValue(attrs, kCTFontAttributeName);
+            if (!YYTextCTFontContainsColorBitmapGlyphs(font)) break;
+            
+            CFIndex indices[glyphCount];
+            CGPoint positions[glyphCount];
+            CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), indices);
+            CTRunGetPositions(run, CFRangeMake(0, glyphCount), positions);
+            for (NSUInteger g = 0; g < glyphCount; g++) {
+                NSUInteger gIdx = indices[g];
+                if (gIdx == idx && g + 1 < glyphCount) {
+                    CGFloat right = positions[g + 1].x;
+                    if (point.x < right) break;
+                    NSUInteger next = indices[g + 1];
+                    do {
+                        if (next == range.location + range.length) break;
+                        unichar c = [_text.string characterAtIndex:next];
+                        if ((c == 0xFE0E || c == 0xFE0F)) { // unicode variant form for emoji style
+                            next++;
+                        } else break;
+                    }
+                    while (1);
+                    if (next != indices[g + 1]) idx = next;
+                    break;
+                }
+            }
+            break;
+        }
+    }
+    return idx;
+}
+
+- (YYTextPosition *)closestPositionToPoint:(CGPoint)point {
+    BOOL isVertical = _container.verticalForm;
+    // 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;
+    
+    NSUInteger lineIndex = [self closestLineIndexForPoint:point];
+    if (lineIndex == NSNotFound) return nil;
+    YYTextLine *line = _lines[lineIndex];
+    __block NSUInteger position = [self textPositionForPoint:point lineIndex:lineIndex];
+    if (position == NSNotFound) position = line.range.location;
+    if (position <= _visibleRange.location) {
+        return [YYTextPosition positionWithOffset:_visibleRange.location affinity:YYTextAffinityForward];
+    } else if (position >= _visibleRange.location + _visibleRange.length) {
+        return [YYTextPosition positionWithOffset:_visibleRange.location + _visibleRange.length affinity:YYTextAffinityBackward];
+    }
+    
+    YYTextAffinity finalAffinity = YYTextAffinityForward;
+    BOOL finalAffinityDetected = NO;
+    
+    // binding range
+    NSRange bindingRange;
+    YYTextBinding *binding = [_text attribute:YYTextBindingAttributeName atIndex:position longestEffectiveRange:&bindingRange inRange:NSMakeRange(0, _text.length)];
+    if (binding && bindingRange.length > 0) {
+        NSUInteger headLineIdx = [self lineIndexForPosition:[YYTextPosition positionWithOffset:bindingRange.location]];
+        NSUInteger tailLineIdx = [self lineIndexForPosition:[YYTextPosition positionWithOffset:bindingRange.location + bindingRange.length affinity:YYTextAffinityBackward]];
+        if (headLineIdx == lineIndex && lineIndex == tailLineIdx) { // all in same line
+            CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:lineIndex];
+            CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:lineIndex];
+            if (left != CGFLOAT_MAX && right != CGFLOAT_MAX) {
+                if (_container.isVerticalForm) {
+                    if (fabs(point.y - left) < fabs(point.y - right)) {
+                        position = bindingRange.location;
+                        finalAffinity = YYTextAffinityForward;
+                    } else {
+                        position = bindingRange.location + bindingRange.length;
+                        finalAffinity = YYTextAffinityBackward;
+                    }
+                } else {
+                    if (fabs(point.x - left) < fabs(point.x - right)) {
+                        position = bindingRange.location;
+                        finalAffinity = YYTextAffinityForward;
+                    } else {
+                        position = bindingRange.location + bindingRange.length;
+                        finalAffinity = YYTextAffinityBackward;
+                    }
+                }
+            } else if (left != CGFLOAT_MAX) {
+                position = left;
+                finalAffinity = YYTextAffinityForward;
+            } else if (right != CGFLOAT_MAX) {
+                position = right;
+                finalAffinity = YYTextAffinityBackward;
+            }
+            finalAffinityDetected = YES;
+        } else if (headLineIdx == lineIndex) {
+            CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:lineIndex];
+            if (left != CGFLOAT_MAX) {
+                position = bindingRange.location;
+                finalAffinity = YYTextAffinityForward;
+                finalAffinityDetected = YES;
+            }
+        } else if (tailLineIdx == lineIndex) {
+            CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:lineIndex];
+            if (right != CGFLOAT_MAX) {
+                position = bindingRange.location + bindingRange.length;
+                finalAffinity = YYTextAffinityBackward;
+                finalAffinityDetected = YES;
+            }
+        } else {
+            BOOL onLeft = NO, onRight = NO;
+            if (headLineIdx != NSNotFound && tailLineIdx != NSNotFound) {
+                if (abs((int)headLineIdx - (int)lineIndex) < abs((int)tailLineIdx - (int)lineIndex)) onLeft = YES;
+                else onRight = YES;
+            } else if (headLineIdx != NSNotFound) {
+                onLeft = YES;
+            } else if (tailLineIdx != NSNotFound) {
+                onRight = YES;
+            }
+            
+            if (onLeft) {
+                CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:headLineIdx];
+                if (left != CGFLOAT_MAX) {
+                    lineIndex = headLineIdx;
+                    line = _lines[headLineIdx];
+                    position = bindingRange.location;
+                    finalAffinity = YYTextAffinityForward;
+                    finalAffinityDetected = YES;
+                }
+            } else if (onRight) {
+                CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:tailLineIdx];
+                if (right != CGFLOAT_MAX) {
+                    lineIndex = tailLineIdx;
+                    line = _lines[tailLineIdx];
+                    position = bindingRange.location + bindingRange.length;
+                    finalAffinity = YYTextAffinityBackward;
+                    finalAffinityDetected = YES;
+                }
+            }
+        }
+    }
+    
+    // empty line
+    if (line.range.length == 0) {
+        BOOL behind = (_lines.count > 1 && lineIndex == _lines.count - 1);  //end line
+        return [YYTextPosition positionWithOffset:line.range.location affinity:behind ? YYTextAffinityBackward:YYTextAffinityForward];
+    }
+    
+    // detect weather the line is a linebreak token
+    if (line.range.length <= 2) {
+        NSString *str = [_text.string substringWithRange:line.range];
+        if (YYTextIsLinebreakString(str)) { // an empty line ("\r", "\n", "\r\n")
+            return [YYTextPosition positionWithOffset:line.range.location];
+        }
+    }
+    
+    // above whole text frame
+    if (lineIndex == 0 && (isVertical ? (point.x > line.right) : (point.y < line.top))) {
+        position = 0;
+        finalAffinity = YYTextAffinityForward;
+        finalAffinityDetected = YES;
+    }
+    // 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 = YYTextAffinityBackward;
+        finalAffinityDetected = YES;
+    }
+    
+    // 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) {
+            unichar c1 = [_text.string characterAtIndex:position - 1];
+            if (YYTextIsLinebreakChar(c1)) {
+                position--;
+                if (position > line.range.location) {
+                    unichar c0 = [_text.string characterAtIndex:position - 1];
+                    if (YYTextIsLinebreakChar(c0)) {
+                        position--;
+                    }
+                }
+            }
+        }
+    }
+    if (position == line.range.location) {
+        return [YYTextPosition positionWithOffset:position];
+    }
+    if (position == line.range.location + line.range.length) {
+        return [YYTextPosition positionWithOffset:position affinity:YYTextAffinityBackward];
+    }
+    
+    [self _insideComposedCharacterSequences:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) {
+        if (isVertical) {
+            position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next);
+        } else {
+            position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next);
+        }
+    }];
+    
+    [self _insideEmoji:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) {
+        if (isVertical) {
+            position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next);
+        } else {
+            position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next);
+        }
+    }];
+    
+    if (position < _visibleRange.location) position = _visibleRange.location;
+    else if (position > _visibleRange.location + _visibleRange.length) position = _visibleRange.location + _visibleRange.length;
+    
+    if (!finalAffinityDetected) {
+        CGFloat ofs = [self offsetForTextPosition:position lineIndex:lineIndex];
+        if (ofs != CGFLOAT_MAX) {
+            BOOL RTL = [self _isRightToLeftInLine:line atPoint:point];
+            if (position >= line.range.location + line.range.length) {
+                finalAffinity = RTL ? YYTextAffinityForward : YYTextAffinityBackward;
+            } else if (position <= line.range.location) {
+                finalAffinity = RTL ? YYTextAffinityBackward : YYTextAffinityForward;
+            } else {
+                finalAffinity = (ofs < (isVertical ? point.y : point.x) && !RTL) ? YYTextAffinityForward : YYTextAffinityBackward;
+            }
+        }
+    }
+    
+    return [YYTextPosition positionWithOffset:position affinity:finalAffinity];
+}
+
+- (YYTextPosition *)positionForPoint:(CGPoint)point
+                         oldPosition:(YYTextPosition *)oldPosition
+                       otherPosition:(YYTextPosition *)otherPosition {
+    if (!oldPosition || !otherPosition) {
+        return oldPosition;
+    }
+    YYTextPosition *newPos = [self closestPositionToPoint:point];
+    if (!newPos) return oldPosition;
+    if ([newPos compare:otherPosition] == [oldPosition compare:otherPosition] &&
+        newPos.offset != otherPosition.offset) {
+        return newPos;
+    }
+    NSUInteger lineIndex = [self lineIndexForPosition:otherPosition];
+    if (lineIndex == NSNotFound) return oldPosition;
+    YYTextLine *line = _lines[lineIndex];
+    YYRowEdge vertical = _lineRowsEdge[line.row];
+    if (_container.verticalForm) {
+        point.x = (vertical.head + vertical.foot) * 0.5;
+    } else {
+        point.y = (vertical.head + vertical.foot) * 0.5;
+    }
+    newPos = [self closestPositionToPoint:point];
+    if ([newPos compare:otherPosition] == [oldPosition compare:otherPosition] &&
+        newPos.offset != otherPosition.offset) {
+        return newPos;
+    }
+    
+    if (_container.isVerticalForm) {
+        if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward
+            YYTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionUp offset:1];
+            if (range) return range.start;
+        } else { // search forward
+            YYTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionDown offset:1];
+            if (range) return range.end;
+        }
+    } else {
+        if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward
+            YYTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionLeft offset:1];
+            if (range) return range.start;
+        } else { // search forward
+            YYTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionRight offset:1];
+            if (range) return range.end;
+        }
+    }
+    
+    return oldPosition;
+}
+
+- (YYTextRange *)textRangeAtPoint:(CGPoint)point {
+    NSUInteger lineIndex = [self lineIndexForPoint:point];
+    if (lineIndex == NSNotFound) return nil;
+    NSUInteger textPosition = [self textPositionForPoint:point lineIndex:[self lineIndexForPoint:point]];
+    if (textPosition == NSNotFound) return nil;
+    YYTextPosition *pos = [self closestPositionToPoint:point];
+    if (!pos) return nil;
+    
+    // get write direction
+    BOOL RTL = [self _isRightToLeftInLine:_lines[lineIndex] atPoint:point];
+    CGRect rect = [self caretRectForPosition:pos];
+    if (CGRectIsNull(rect)) return nil;
+    
+    if (_container.verticalForm) {
+        YYTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown offset:1];
+        return range;
+    } else {
+        YYTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight offset:1];
+        return range;
+    }
+}
+
+- (YYTextRange *)closestTextRangeAtPoint:(CGPoint)point {
+    YYTextPosition *pos = [self closestPositionToPoint:point];
+    if (!pos) return nil;
+    NSUInteger lineIndex = [self lineIndexForPosition:pos];
+    if (lineIndex == NSNotFound) return nil;
+    YYTextLine *line = _lines[lineIndex];
+    BOOL RTL = [self _isRightToLeftInLine:line atPoint:point];
+    CGRect rect = [self caretRectForPosition:pos];
+    if (CGRectIsNull(rect)) return nil;
+    
+    UITextLayoutDirection direction = UITextLayoutDirectionRight;
+    if (pos.offset >= line.range.location + line.range.length) {
+        if (direction != RTL) {
+            direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft;
+        } else {
+            direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight;
+        }
+    } else if (pos.offset <= line.range.location) {
+        if (direction != RTL) {
+            direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight;
+        } else {
+            direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft;
+        }
+    } else {
+        if (_container.verticalForm) {
+            direction = (rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown;
+        } else {
+            direction = (rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight;
+        }
+    }
+    
+    YYTextRange *range = [self textRangeByExtendingPosition:pos inDirection:direction offset:1];
+    return range;
+}
+
+- (YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)position {
+    NSUInteger visibleStart = _visibleRange.location;
+    NSUInteger visibleEnd = _visibleRange.location + _visibleRange.length;
+    
+    if (!position) return nil;
+    if (position.offset < visibleStart || position.offset > visibleEnd) return nil;
+    
+    // head or tail, returns immediately
+    if (position.offset == visibleStart) {
+        return [YYTextRange rangeWithRange:NSMakeRange(position.offset, 0)];
+    } else if (position.offset == visibleEnd) {
+        return [YYTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:YYTextAffinityBackward];
+    }
+    
+    // binding range
+    NSRange tRange;
+    YYTextBinding *binding = [_text attribute:YYTextBindingAttributeName atIndex:position.offset longestEffectiveRange:&tRange inRange:_visibleRange];
+    if (binding && tRange.length > 0 && tRange.location < position.offset) {
+        return [YYTextRange rangeWithRange:tRange];
+    }
+    
+    // inside emoji or composed character sequences
+    NSUInteger lineIndex = [self lineIndexForPosition:position];
+    if (lineIndex != NSNotFound) {
+        __block NSUInteger _prev, _next;
+        BOOL emoji = NO, seq = NO;
+        
+        YYTextLine *line = _lines[lineIndex];
+        emoji = [self _insideEmoji:line position:position.offset block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) {
+            _prev = prev;
+            _next = next;
+        }];
+        if (!emoji) {
+            seq = [self _insideComposedCharacterSequences:line position:position.offset block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) {
+                _prev = prev;
+                _next = next;
+            }];
+        }
+        if (emoji || seq) {
+            return [YYTextRange rangeWithRange:NSMakeRange(_prev, _next - _prev)];
+        }
+    }
+    
+    // inside linebreak '\r\n'
+    if (position.offset > visibleStart && position.offset < visibleEnd) {
+        unichar c0 = [_text.string characterAtIndex:position.offset - 1];
+        if ((c0 == '\r') && position.offset < visibleEnd) {
+            unichar c1 = [_text.string characterAtIndex:position.offset];
+            if (c1 == '\n') {
+                return [YYTextRange rangeWithStart:[YYTextPosition positionWithOffset:position.offset - 1] end:[YYTextPosition positionWithOffset:position.offset + 1]];
+            }
+        }
+        if (YYTextIsLinebreakChar(c0) && position.affinity == YYTextAffinityBackward) {
+            NSString *str = [_text.string substringToIndex:position.offset];
+            NSUInteger len = YYTextLinebreakTailLength(str);
+            return [YYTextRange rangeWithStart:[YYTextPosition positionWithOffset:position.offset - len] end:[YYTextPosition positionWithOffset:position.offset]];
+        }
+    }
+    
+    return [YYTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:position.affinity];
+}
+
+- (YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)position
+                                  inDirection:(UITextLayoutDirection)direction
+                                       offset:(NSInteger)offset {
+    NSInteger visibleStart = _visibleRange.location;
+    NSInteger visibleEnd = _visibleRange.location + _visibleRange.length;
+    
+    if (!position) return nil;
+    if (position.offset < visibleStart || position.offset > visibleEnd) return nil;
+    if (offset == 0) return [self textRangeByExtendingPosition:position];
+    
+    BOOL isVerticalForm = _container.verticalForm;
+    BOOL verticalMove, forwardMove;
+    
+    if (isVerticalForm) {
+        verticalMove = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionRight;
+        forwardMove = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionDown;
+    } else {
+        verticalMove = direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown;
+        forwardMove = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight;
+    }
+    
+    if (offset < 0) {
+        forwardMove = !forwardMove;
+        offset = -offset;
+    }
+    
+    // head or tail, returns immediately
+    if (!forwardMove && position.offset == visibleStart) {
+        return [YYTextRange rangeWithRange:NSMakeRange(_visibleRange.location, 0)];
+    } else if (forwardMove && position.offset == visibleEnd) {
+        return [YYTextRange rangeWithRange:NSMakeRange(position.offset, 0) affinity:YYTextAffinityBackward];
+    }
+    
+    // extend from position
+    YYTextRange *fromRange = [self textRangeByExtendingPosition:position];
+    if (!fromRange) return nil;
+    YYTextRange *allForward = [YYTextRange rangeWithStart:fromRange.start end:[YYTextPosition positionWithOffset:visibleEnd]];
+    YYTextRange *allBackward = [YYTextRange rangeWithStart:[YYTextPosition positionWithOffset:visibleStart] end:fromRange.end];
+    
+    if (verticalMove) { // up/down in text layout
+        NSInteger lineIndex = [self lineIndexForPosition:position];
+        if (lineIndex == NSNotFound) return nil;
+        
+        YYTextLine *line = _lines[lineIndex];
+        NSInteger moveToRowIndex = (NSInteger)line.row + (forwardMove ? offset : -offset);
+        if (moveToRowIndex < 0) return allBackward;
+        else if (moveToRowIndex >= (NSInteger)_rowCount) return allForward;
+        
+        CGFloat ofs = [self offsetForTextPosition:position.offset lineIndex:lineIndex];
+        if (ofs == CGFLOAT_MAX) return nil;
+        
+        NSUInteger moveToLineFirstIndex = [self lineIndexForRow:moveToRowIndex];
+        NSUInteger moveToLineCount = [self lineCountForRow:moveToRowIndex];
+        if (moveToLineFirstIndex == NSNotFound || moveToLineCount == NSNotFound || moveToLineCount == 0) return nil;
+        CGFloat mostLeft = CGFLOAT_MAX, mostRight = -CGFLOAT_MAX;
+        YYTextLine *mostLeftLine = nil, *mostRightLine = nil;
+        NSUInteger insideIndex = NSNotFound;
+        for (NSUInteger i = 0; i < moveToLineCount; i++) {
+            NSUInteger lineIndex = moveToLineFirstIndex + i;
+            YYTextLine *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;
+                }
+            }
+        }
+        BOOL afinityEdge = NO;
+        if (insideIndex == NSNotFound) {
+            if (ofs <= mostLeft) {
+                insideIndex = mostLeftLine.index;
+            } else {
+                insideIndex = mostRightLine.index;
+            }
+            afinityEdge = YES;
+        }
+        YYTextLine *insideLine = _lines[insideIndex];
+        NSUInteger pos;
+        if (isVerticalForm) {
+            pos = [self textPositionForPoint:CGPointMake(insideLine.position.x, ofs) lineIndex:insideIndex];
+        } else {
+            pos = [self textPositionForPoint:CGPointMake(ofs, insideLine.position.y) lineIndex:insideIndex];
+        }
+        if (pos == NSNotFound) return nil;
+        YYTextPosition *extPos;
+        if (afinityEdge) {
+            if (pos == insideLine.range.location + insideLine.range.length) {
+                NSString *subStr = [_text.string substringWithRange:insideLine.range];
+                NSUInteger lineBreakLen = YYTextLinebreakTailLength(subStr);
+                extPos = [YYTextPosition positionWithOffset:pos - lineBreakLen];
+            } else {
+                extPos = [YYTextPosition positionWithOffset:pos];
+            }
+        } else {
+            extPos = [YYTextPosition positionWithOffset:pos];
+        }
+        YYTextRange *ext = [self textRangeByExtendingPosition:extPos];
+        if (!ext) return nil;
+        if (forwardMove) {
+            return [YYTextRange rangeWithStart:fromRange.start end:ext.end];
+        } else {
+            return [YYTextRange rangeWithStart:ext.start end:fromRange.end];
+        }
+        
+    } else { // left/right in text layout
+        YYTextPosition *toPosition = [YYTextPosition positionWithOffset:position.offset + (forwardMove ? offset : -offset)];
+        if (toPosition.offset <= visibleStart) return allBackward;
+        else if (toPosition.offset >= visibleEnd) return allForward;
+        
+        YYTextRange *toRange = [self textRangeByExtendingPosition:toPosition];
+        if (!toRange) return nil;
+        
+        NSInteger start = MIN(fromRange.start.offset, toRange.start.offset);
+        NSInteger end = MAX(fromRange.end.offset, toRange.end.offset);
+        return [YYTextRange rangeWithRange:NSMakeRange(start, end - start)];
+    }
+}
+
+- (NSUInteger)lineIndexForPosition:(YYTextPosition *)position {
+    if (!position) return NSNotFound;
+    if (_lines.count == 0) return NSNotFound;
+    NSUInteger location = position.offset;
+    NSInteger lo = 0, hi = _lines.count - 1, mid = 0;
+    if (position.affinity == YYTextAffinityBackward) {
+        while (lo <= hi) {
+            mid = (lo + hi) / 2;
+            YYTextLine *line = _lines[mid];
+            NSRange 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;
+            YYTextLine *line = _lines[mid];
+            NSRange 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;
+}
+
+- (CGPoint)linePositionForPosition:(YYTextPosition *)position {
+    NSUInteger lineIndex = [self lineIndexForPosition:position];
+    if (lineIndex == NSNotFound) return CGPointZero;
+    YYTextLine *line = _lines[lineIndex];
+    CGFloat offset = [self offsetForTextPosition:position.offset lineIndex:lineIndex];
+    if (offset == CGFLOAT_MAX) return CGPointZero;
+    if (_container.verticalForm) {
+        return CGPointMake(line.position.x, offset);
+    } else {
+        return CGPointMake(offset, line.position.y);
+    }
+}
+
+- (CGRect)caretRectForPosition:(YYTextPosition *)position {
+    NSUInteger lineIndex = [self lineIndexForPosition:position];
+    if (lineIndex == NSNotFound) return CGRectNull;
+    YYTextLine *line = _lines[lineIndex];
+    CGFloat offset = [self offsetForTextPosition:position.offset lineIndex:lineIndex];
+    if (offset == CGFLOAT_MAX) return CGRectNull;
+    if (_container.verticalForm) {
+        return CGRectMake(line.bounds.origin.x, offset, line.bounds.size.width, 0);
+    } else {
+        return CGRectMake(offset, line.bounds.origin.y, 0, line.bounds.size.height);
+    }
+}
+
+- (CGRect)firstRectForRange:(YYTextRange *)range {
+    range = [self _correctedRangeWithEdge:range];
+    
+    NSUInteger startLineIndex = [self lineIndexForPosition:range.start];
+    NSUInteger endLineIndex = [self lineIndexForPosition:range.end];
+    if (startLineIndex == NSNotFound || endLineIndex == NSNotFound) return CGRectNull;
+    if (startLineIndex > endLineIndex) return CGRectNull;
+    YYTextLine *startLine = _lines[startLineIndex];
+    YYTextLine *endLine = _lines[endLineIndex];
+    NSMutableArray *lines = [NSMutableArray new];
+    for (NSUInteger i = startLineIndex; i <= startLineIndex; i++) {
+        YYTextLine *line = _lines[i];
+        if (line.row != startLine.row) break;
+        [lines addObject:line];
+    }
+    if (_container.verticalForm) {
+        if (lines.count == 1) {
+            CGFloat top = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex];
+            CGFloat bottom;
+            if (startLine == endLine) {
+                bottom = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex];
+            } else {
+                bottom = startLine.bottom;
+            }
+            if (top == CGFLOAT_MAX || bottom == CGFLOAT_MAX) return CGRectNull;
+            if (top > bottom) YYTEXT_SWAP(top, bottom);
+            return CGRectMake(startLine.left, top, startLine.width, bottom - top);
+        } else {
+            CGFloat top = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex];
+            CGFloat bottom = startLine.bottom;
+            if (top == CGFLOAT_MAX || bottom == CGFLOAT_MAX) return CGRectNull;
+            if (top > bottom) YYTEXT_SWAP(top, bottom);
+            CGRect rect = CGRectMake(startLine.left, top, startLine.width, bottom - top);
+            for (NSUInteger i = 1; i < lines.count; i++) {
+                YYTextLine *line = lines[i];
+                rect = CGRectUnion(rect, line.bounds);
+            }
+            return rect;
+        }
+    } else {
+        if (lines.count == 1) {
+            CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex];
+            CGFloat right;
+            if (startLine == endLine) {
+                right = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex];
+            } else {
+                right = startLine.right;
+            }
+            if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull;
+            if (left > right) YYTEXT_SWAP(left, right);
+            return CGRectMake(left, startLine.top, right - left, startLine.height);
+        } else {
+            CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex];
+            CGFloat right = startLine.right;
+            if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull;
+            if (left > right) YYTEXT_SWAP(left, right);
+            CGRect rect = CGRectMake(left, startLine.top, right - left, startLine.height);
+            for (NSUInteger i = 1; i < lines.count; i++) {
+                YYTextLine *line = lines[i];
+                rect = CGRectUnion(rect, line.bounds);
+            }
+            return rect;
+        }
+    }
+}
+
+- (CGRect)rectForRange:(YYTextRange *)range {
+    NSArray *rects = [self selectionRectsForRange:range];
+    if (rects.count == 0) return CGRectNull;
+    CGRect rectUnion = ((YYTextSelectionRect *)rects.firstObject).rect;
+    for (NSUInteger i = 1; i < rects.count; i++) {
+        YYTextSelectionRect *rect = rects[i];
+        rectUnion = CGRectUnion(rectUnion, rect.rect);
+    }
+    return rectUnion;
+}
+
+- (NSArray *)selectionRectsForRange:(YYTextRange *)range {
+    range = [self _correctedRangeWithEdge:range];
+    
+    BOOL isVertical = _container.verticalForm;
+    NSMutableArray *rects = [NSMutableArray array];
+    if (!range) return rects;
+    
+    NSUInteger startLineIndex = [self lineIndexForPosition:range.start];
+    NSUInteger endLineIndex = [self lineIndexForPosition:range.end];
+    if (startLineIndex == NSNotFound || endLineIndex == NSNotFound) return rects;
+    if (startLineIndex > endLineIndex) YYTEXT_SWAP(startLineIndex, endLineIndex);
+    YYTextLine *startLine = _lines[startLineIndex];
+    YYTextLine *endLine = _lines[endLineIndex];
+    CGFloat offsetStart = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex];
+    CGFloat offsetEnd = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex];
+    
+    YYTextSelectionRect *start = [YYTextSelectionRect new];
+    if (isVertical) {
+        start.rect = CGRectMake(startLine.left, offsetStart, startLine.width, 0);
+    } else {
+        start.rect = CGRectMake(offsetStart, startLine.top, 0, startLine.height);
+    }
+    start.containsStart = YES;
+    start.isVertical = isVertical;
+    [rects addObject:start];
+    
+    YYTextSelectionRect *end = [YYTextSelectionRect new];
+    if (isVertical) {
+        end.rect = CGRectMake(endLine.left, offsetEnd, endLine.width, 0);
+    } else {
+        end.rect = CGRectMake(offsetEnd, endLine.top, 0, endLine.height);
+    }
+    end.containsEnd = YES;
+    end.isVertical = isVertical;
+    [rects addObject:end];
+    
+    if (startLine.row == endLine.row) { // same row
+        if (offsetStart > offsetEnd) YYTEXT_SWAP(offsetStart, offsetEnd);
+        YYTextSelectionRect *rect = [YYTextSelectionRect new];
+        if (isVertical) {
+            rect.rect = CGRectMake(startLine.bounds.origin.x, offsetStart, MAX(startLine.width, endLine.width), offsetEnd - offsetStart);
+        } else {
+            rect.rect = CGRectMake(offsetStart, startLine.bounds.origin.y, offsetEnd - offsetStart, MAX(startLine.height, endLine.height));
+        }
+        rect.isVertical = isVertical;
+        [rects addObject:rect];
+        
+    } else { // more than one row
+        
+        // start line select rect
+        YYTextSelectionRect *topRect = [YYTextSelectionRect new];
+        topRect.isVertical = isVertical;
+        CGFloat topOffset = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex];
+        CTRunRef topRun = [self _runForLine:startLine position:range.start];
+        if (topRun && (CTRunGetStatus(topRun) & kCTRunStatusRightToLeft)) {
+            if (isVertical) {
+                topRect.rect = CGRectMake(startLine.left, _container.path ? startLine.top : _container.insets.top, startLine.width, topOffset - startLine.top);
+            } else {
+                topRect.rect = CGRectMake(_container.path ? startLine.left : _container.insets.left, startLine.top, topOffset - startLine.left, startLine.height);
+            }
+            topRect.writingDirection = UITextWritingDirectionRightToLeft;
+        } else {
+            if (isVertical) {
+                topRect.rect = CGRectMake(startLine.left, topOffset, startLine.width, (_container.path ? startLine.bottom : _container.size.height - _container.insets.bottom) - topOffset);
+            } else {
+                topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset, startLine.height);
+            }
+        }
+        [rects addObject:topRect];
+        
+        // end line select rect
+        YYTextSelectionRect *bottomRect = [YYTextSelectionRect new];
+        bottomRect.isVertical = isVertical;
+        CGFloat bottomOffset = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex];
+        CTRunRef bottomRun = [self _runForLine:endLine position:range.end];
+        if (bottomRun && (CTRunGetStatus(bottomRun) & kCTRunStatusRightToLeft)) {
+            if (isVertical) {
+                bottomRect.rect = CGRectMake(endLine.left, bottomOffset, endLine.width, (_container.path ? endLine.bottom : _container.size.height - _container.insets.bottom) - bottomOffset);
+            } else {
+                bottomRect.rect = CGRectMake(bottomOffset, endLine.top, (_container.path ? endLine.right : _container.size.width - _container.insets.right) - bottomOffset, endLine.height);
+            }
+            bottomRect.writingDirection = UITextWritingDirectionRightToLeft;
+        } else {
+            if (isVertical) {
+                CGFloat top = _container.path ? endLine.top : _container.insets.top;
+                bottomRect.rect = CGRectMake(endLine.left, top, endLine.width, bottomOffset - top);
+            } else {
+                CGFloat left = _container.path ? endLine.left : _container.insets.left;
+                bottomRect.rect = CGRectMake(left, endLine.top, bottomOffset - left, endLine.height);
+            }
+        }
+        [rects addObject:bottomRect];
+        
+        if (endLineIndex - startLineIndex >= 2) {
+            CGRect r = CGRectZero;
+            BOOL startLineDetected = NO;
+            for (NSUInteger l = startLineIndex + 1; l < endLineIndex; l++) {
+                YYTextLine *line = _lines[l];
+                if (line.row == startLine.row || line.row == endLine.row) continue;
+                if (!startLineDetected) {
+                    r = line.bounds;
+                    startLineDetected = YES;
+                } else {
+                    r = CGRectUnion(r, line.bounds);
+                }
+            }
+            if (startLineDetected) {
+                if (isVertical) {
+                    if (!_container.path) {
+                        r.origin.y = _container.insets.top;
+                        r.size.height = _container.size.height - _container.insets.bottom - _container.insets.top;
+                    }
+                    r.size.width =  CGRectGetMinX(topRect.rect) - CGRectGetMaxX(bottomRect.rect);
+                    r.origin.x = CGRectGetMaxX(bottomRect.rect);
+                } else {
+                    if (!_container.path) {
+                        r.origin.x = _container.insets.left;
+                        r.size.width = _container.size.width - _container.insets.right - _container.insets.left;
+                    }
+                    r.origin.y = CGRectGetMaxY(topRect.rect);
+                    r.size.height = bottomRect.rect.origin.y - r.origin.y;
+                }
+                
+                YYTextSelectionRect *rect = [YYTextSelectionRect new];
+                rect.rect = r;
+                rect.isVertical = isVertical;
+                [rects addObject:rect];
+            }
+        } else {
+            if (isVertical) {
+                CGRect r0 = bottomRect.rect;
+                CGRect r1 = topRect.rect;
+                CGFloat mid = (CGRectGetMaxX(r0) + CGRectGetMinX(r1)) * 0.5;
+                r0.size.width = mid - r0.origin.x;
+                CGFloat r1ofs = r1.origin.x - mid;
+                r1.origin.x -= r1ofs;
+                r1.size.width += r1ofs;
+                topRect.rect = r1;
+                bottomRect.rect = r0;
+            } else {
+                CGRect r0 = topRect.rect;
+                CGRect r1 = bottomRect.rect;
+                CGFloat mid = (CGRectGetMaxY(r0) + CGRectGetMinY(r1)) * 0.5;
+                r0.size.height = mid - r0.origin.y;
+                CGFloat r1ofs = r1.origin.y - mid;
+                r1.origin.y -= r1ofs;
+                r1.size.height += r1ofs;
+                topRect.rect = r0;
+                bottomRect.rect = r1;
+            }
+        }
+    }
+    return rects;
+}
+
+- (NSArray *)selectionRectsWithoutStartAndEndForRange:(YYTextRange *)range {
+    NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy;
+    for (NSInteger i = 0, max = rects.count; i < max; i++) {
+        YYTextSelectionRect *rect = rects[i];
+        if (rect.containsStart || rect.containsEnd) {
+            [rects removeObjectAtIndex:i];
+            i--;
+            max--;
+        }
+    }
+    return rects;
+}
+
+- (NSArray *)selectionRectsWithOnlyStartAndEndForRange:(YYTextRange *)range {
+    NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy;
+    for (NSInteger i = 0, max = rects.count; i < max; i++) {
+        YYTextSelectionRect *rect = rects[i];
+        if (!rect.containsStart && !rect.containsEnd) {
+            [rects removeObjectAtIndex:i];
+            i--;
+            max--;
+        }
+    }
+    return rects;
+}
+
+
+#pragma mark - Draw
+
+
+typedef NS_OPTIONS(NSUInteger, YYTextDecorationType) {
+    YYTextDecorationTypeUnderline     = 1 << 0,
+    YYTextDecorationTypeStrikethrough = 1 << 1,
+};
+
+typedef NS_OPTIONS(NSUInteger, YYTextBorderType) {
+    YYTextBorderTypeBackgound = 1 << 0,
+    YYTextBorderTypeNormal    = 1 << 1,
+};
+
+static CGRect YYTextMergeRectInSameLine(CGRect rect1, CGRect rect2, BOOL isVertical) {
+    if (isVertical) {
+        CGFloat top = MIN(rect1.origin.y, rect2.origin.y);
+        CGFloat bottom = MAX(rect1.origin.y + rect1.size.height, rect2.origin.y + rect2.size.height);
+        CGFloat width = MAX(rect1.size.width, rect2.size.width);
+        return CGRectMake(rect1.origin.x, top, width, bottom - top);
+    } else {
+        CGFloat left = MIN(rect1.origin.x, rect2.origin.x);
+        CGFloat right = MAX(rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width);
+        CGFloat height = MAX(rect1.size.height, rect2.size.height);
+        return CGRectMake(left, rect1.origin.y, right - left, height);
+    }
+}
+
+static void YYTextGetRunsMaxMetric(CFArrayRef runs, CGFloat *xHeight, CGFloat *underlinePosition, CGFloat *lineThickness) {
+    CGFloat maxXHeight = 0;
+    CGFloat maxUnderlinePos = 0;
+    CGFloat maxLineThickness = 0;
+    for (NSUInteger i = 0, max = CFArrayGetCount(runs); i < max; i++) {
+        CTRunRef run = CFArrayGetValueAtIndex(runs, i);
+        CFDictionaryRef attrs = CTRunGetAttributes(run);
+        if (attrs) {
+            CTFontRef font = CFDictionaryGetValue(attrs, kCTFontAttributeName);
+            if (font) {
+                CGFloat xHeight = CTFontGetXHeight(font);
+                if (xHeight > maxXHeight) maxXHeight = xHeight;
+                CGFloat underlinePos = CTFontGetUnderlinePosition(font);
+                if (underlinePos < maxUnderlinePos) maxUnderlinePos = underlinePos;
+                CGFloat lineThickness = CTFontGetUnderlineThickness(font);
+                if (lineThickness > maxLineThickness) maxLineThickness = lineThickness;
+            }
+        }
+    }
+    if (xHeight) *xHeight = maxXHeight;
+    if (underlinePosition) *underlinePosition = maxUnderlinePos;
+    if (lineThickness) *lineThickness = maxLineThickness;
+}
+
+static void YYTextDrawRun(YYTextLine *line, CTRunRef run, CGContextRef context, CGSize size, BOOL isVertical, NSArray *runRanges, CGFloat verticalOffset) {
+    CGAffineTransform runTextMatrix = CTRunGetTextMatrix(run);
+    BOOL runTextMatrixIsID = CGAffineTransformIsIdentity(runTextMatrix);
+    
+    CFDictionaryRef runAttrs = CTRunGetAttributes(run);
+    NSValue *glyphTransformValue = CFDictionaryGetValue(runAttrs, (__bridge const void *)(YYTextGlyphTransformAttributeName));
+    if (!isVertical && !glyphTransformValue) { // draw run
+        if (!runTextMatrixIsID) {
+            CGContextSaveGState(context);
+            CGAffineTransform trans = CGContextGetTextMatrix(context);
+            CGContextSetTextMatrix(context, CGAffineTransformConcat(trans, runTextMatrix));
+        }
+        CTRunDraw(run, context, CFRangeMake(0, 0));
+        if (!runTextMatrixIsID) {
+            CGContextRestoreGState(context);
+        }
+    } else { // draw glyph
+        CTFontRef runFont = CFDictionaryGetValue(runAttrs, kCTFontAttributeName);
+        if (!runFont) return;
+        NSUInteger glyphCount = CTRunGetGlyphCount(run);
+        if (glyphCount <= 0) return;
+        
+        CGGlyph glyphs[glyphCount];
+        CGPoint glyphPositions[glyphCount];
+        CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs);
+        CTRunGetPositions(run, CFRangeMake(0, 0), glyphPositions);
+        
+        CGColorRef fillColor = (CGColorRef)CFDictionaryGetValue(runAttrs, kCTForegroundColorAttributeName);
+        fillColor = YYTextGetCGColor(fillColor);
+        NSNumber *strokeWidth = CFDictionaryGetValue(runAttrs, kCTStrokeWidthAttributeName);
+        
+        CGContextSaveGState(context); {
+            CGContextSetFillColorWithColor(context, fillColor);
+            if (!strokeWidth || strokeWidth.floatValue == 0) {
+                CGContextSetTextDrawingMode(context, kCGTextFill);
+            } else {
+                CGColorRef strokeColor = (CGColorRef)CFDictionaryGetValue(runAttrs, kCTStrokeColorAttributeName);
+                if (!strokeColor) strokeColor = fillColor;
+                CGContextSetStrokeColorWithColor(context, strokeColor);
+                CGContextSetLineWidth(context, CTFontGetSize(runFont) * fabs(strokeWidth.floatValue * 0.01));
+                if (strokeWidth.floatValue > 0) {
+                    CGContextSetTextDrawingMode(context, kCGTextStroke);
+                } else {
+                    CGContextSetTextDrawingMode(context, kCGTextFillStroke);
+                }
+            }
+            
+            if (isVertical) {
+                CFIndex runStrIdx[glyphCount + 1];
+                CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx);
+                CFRange runStrRange = CTRunGetStringRange(run);
+                runStrIdx[glyphCount] = runStrRange.location + runStrRange.length;
+                CGSize glyphAdvances[glyphCount];
+                CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances);
+                CGFloat ascent = CTFontGetAscent(runFont);
+                CGFloat descent = CTFontGetDescent(runFont);
+                CGAffineTransform glyphTransform = glyphTransformValue.CGAffineTransformValue;
+                CGPoint zeroPoint = CGPointZero;
+                
+                for (YYTextRunGlyphRange *oneRange in runRanges) {
+                    NSRange range = oneRange.glyphRangeInRun;
+                    NSUInteger rangeMax = range.location + range.length;
+                    YYTextRunGlyphDrawMode mode = oneRange.drawMode;
+                    
+                    for (NSUInteger g = range.location; g < rangeMax; g++) {
+                        CGContextSaveGState(context); {
+                            CGContextSetTextMatrix(context, CGAffineTransformIdentity);
+                            if (glyphTransformValue) {
+                                CGContextSetTextMatrix(context, glyphTransform);
+                            }
+                            if (mode) { // CJK glyph, need rotated
+                                CGFloat ofs = (ascent - descent) * 0.5;
+                                CGFloat w = glyphAdvances[g].width * 0.5;
+                                CGFloat x = x = line.position.x + verticalOffset + glyphPositions[g].y + (ofs - w);
+                                CGFloat y = -line.position.y + size.height - glyphPositions[g].x - (ofs + w);
+                                if (mode == YYTextRunGlyphDrawModeVerticalRotateMove) {
+                                    x += w;
+                                    y += w;
+                                }
+                                CGContextSetTextPosition(context, x, y);
+                            } else {
+                                CGContextRotateCTM(context, YYTextDegreesToRadians(-90));
+                                CGContextSetTextPosition(context,
+                                                         line.position.y - size.height + glyphPositions[g].x,
+                                                         line.position.x + verticalOffset + glyphPositions[g].y);
+                            }
+                            
+                            if (YYTextCTFontContainsColorBitmapGlyphs(runFont)) {
+                                CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context);
+                            } else {
+                                CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);
+                                CGContextSetFont(context, cgFont);
+                                CGContextSetFontSize(context, CTFontGetSize(runFont));
+                                CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1);
+                                CGFontRelease(cgFont);
+                            }
+                        } CGContextRestoreGState(context);
+                    }
+                }
+            } else { // not vertical
+                if (glyphTransformValue) {
+                    CFIndex runStrIdx[glyphCount + 1];
+                    CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx);
+                    CFRange runStrRange = CTRunGetStringRange(run);
+                    runStrIdx[glyphCount] = runStrRange.location + runStrRange.length;
+                    CGSize glyphAdvances[glyphCount];
+                    CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances);
+                    CGAffineTransform glyphTransform = glyphTransformValue.CGAffineTransformValue;
+                    CGPoint zeroPoint = CGPointZero;
+                    
+                    for (NSUInteger g = 0; g < glyphCount; g++) {
+                        CGContextSaveGState(context); {
+                            CGContextSetTextMatrix(context, CGAffineTransformIdentity);
+                            CGContextSetTextMatrix(context, glyphTransform);
+                            CGContextSetTextPosition(context,
+                                                     line.position.x + glyphPositions[g].x,
+                                                     size.height - (line.position.y + glyphPositions[g].y));
+                            
+                            if (YYTextCTFontContainsColorBitmapGlyphs(runFont)) {
+                                CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context);
+                            } else {
+                                CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);
+                                CGContextSetFont(context, cgFont);
+                                CGContextSetFontSize(context, CTFontGetSize(runFont));
+                                CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1);
+                                CGFontRelease(cgFont);
+                            }
+                        } CGContextRestoreGState(context);
+                    }
+                } else {
+                    if (YYTextCTFontContainsColorBitmapGlyphs(runFont)) {
+                        CTFontDrawGlyphs(runFont, glyphs, glyphPositions, glyphCount, context);
+                    } else {
+                        CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);
+                        CGContextSetFont(context, cgFont);
+                        CGContextSetFontSize(context, CTFontGetSize(runFont));
+                        CGContextShowGlyphsAtPositions(context, glyphs, glyphPositions, glyphCount);
+                        CGFontRelease(cgFont);
+                    }
+                }
+            }
+            
+        } CGContextRestoreGState(context);
+    }
+}
+
+static void YYTextSetLinePatternInContext(YYTextLineStyle style, CGFloat width, CGFloat phase, CGContextRef context){
+    CGContextSetLineWidth(context, width);
+    CGContextSetLineCap(context, kCGLineCapButt);
+    CGContextSetLineJoin(context, kCGLineJoinMiter);
+    
+    CGFloat dash = 12, dot = 5, space = 3;
+    NSUInteger pattern = style & 0xF00;
+    if (pattern == YYTextLineStylePatternSolid) {
+        CGContextSetLineDash(context, phase, NULL, 0);
+    } else if (pattern == YYTextLineStylePatternDot) {
+        CGFloat lengths[2] = {width * dot, width * space};
+        CGContextSetLineDash(context, phase, lengths, 2);
+    } else if (pattern == YYTextLineStylePatternDash) {
+        CGFloat lengths[2] = {width * dash, width * space};
+        CGContextSetLineDash(context, phase, lengths, 2);
+    } else if (pattern == YYTextLineStylePatternDashDot) {
+        CGFloat lengths[4] = {width * dash, width * space, width * dot, width * space};
+        CGContextSetLineDash(context, phase, lengths, 4);
+    } else if (pattern == YYTextLineStylePatternDashDotDot) {
+        CGFloat lengths[6] = {width * dash, width * space,width * dot, width * space, width * dot, width * space};
+        CGContextSetLineDash(context, phase, lengths, 6);
+    } else if (pattern == YYTextLineStylePatternCircleDot) {
+        CGFloat lengths[2] = {width * 0, width * 3};
+        CGContextSetLineDash(context, phase, lengths, 2);
+        CGContextSetLineCap(context, kCGLineCapRound);
+        CGContextSetLineJoin(context, kCGLineJoinRound);
+    }
+}
+
+
+static void YYTextDrawBorderRects(CGContextRef context, CGSize size, YYTextBorder *border, NSArray *rects, BOOL isVertical) {
+    if (rects.count == 0) return;
+    
+    YYTextShadow *shadow = border.shadow;
+    if (shadow.color) {
+        CGContextSaveGState(context);
+        CGContextSetShadowWithColor(context, shadow.offset, shadow.radius, shadow.color.CGColor);
+        CGContextBeginTransparencyLayer(context, NULL);
+    }
+    
+    NSMutableArray *paths = [NSMutableArray new];
+    for (NSValue *value in rects) {
+        CGRect rect = value.CGRectValue;
+        if (isVertical) {
+            rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(border.insets));
+        } else {
+            rect = UIEdgeInsetsInsetRect(rect, border.insets);
+        }
+        rect = YYTextCGRectPixelRound(rect);
+        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius];
+        [path closePath];
+        [paths addObject:path];
+    }
+    
+    if (border.fillColor) {
+        CGContextSaveGState(context);
+        CGContextSetFillColorWithColor(context, border.fillColor.CGColor);
+        for (UIBezierPath *path in paths) {
+            CGContextAddPath(context, path.CGPath);
+        }
+        CGContextFillPath(context);
+        CGContextRestoreGState(context);
+    }
+    
+    if (border.strokeColor && border.lineStyle > 0 && border.strokeWidth > 0) {
+        
+        //-------------------------- single line ------------------------------//
+        CGContextSaveGState(context);
+        for (UIBezierPath *path in paths) {
+            CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
+            CGContextAddPath(context, path.CGPath);
+            CGContextEOClip(context);
+        }
+        [border.strokeColor setStroke];
+        YYTextSetLinePatternInContext(border.lineStyle, border.strokeWidth, 0, context);
+        CGFloat inset = -border.strokeWidth * 0.5;
+        if ((border.lineStyle & 0xFF) == YYTextLineStyleThick) {
+            inset *= 2;
+            CGContextSetLineWidth(context, border.strokeWidth * 2);
+        }
+        CGFloat radiusDelta = -inset;
+        if (border.cornerRadius <= 0) {
+            radiusDelta = 0;
+        }
+        CGContextSetLineJoin(context, border.lineJoin);
+        for (NSValue *value in rects) {
+            CGRect rect = value.CGRectValue;
+            if (isVertical) {
+                rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(border.insets));
+            } else {
+                rect = UIEdgeInsetsInsetRect(rect, border.insets);
+            }
+            rect = CGRectInset(rect, inset, inset);
+            UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + radiusDelta];
+            [path closePath];
+            CGContextAddPath(context, path.CGPath);
+        }
+        CGContextStrokePath(context);
+        CGContextRestoreGState(context);
+        
+        //------------------------- second line ------------------------------//
+        if ((border.lineStyle & 0xFF) == YYTextLineStyleDouble) {
+            CGContextSaveGState(context);
+            CGFloat inset = -border.strokeWidth * 2;
+            for (NSValue *value in rects) {
+                CGRect rect = value.CGRectValue;
+                rect = UIEdgeInsetsInsetRect(rect, border.insets);
+                rect = CGRectInset(rect, inset, inset);
+                UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + 2 * border.strokeWidth];
+                [path closePath];
+                CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
+                CGContextAddPath(context, path.CGPath);
+                CGContextEOClip(context);
+            }
+            CGContextSetStrokeColorWithColor(context, border.strokeColor.CGColor);
+            YYTextSetLinePatternInContext(border.lineStyle, border.strokeWidth, 0, context);
+            CGContextSetLineJoin(context, border.lineJoin);
+            inset = -border.strokeWidth * 2.5;
+            radiusDelta = border.strokeWidth * 2;
+            if (border.cornerRadius <= 0) {
+                radiusDelta = 0;
+            }
+            for (NSValue *value in rects) {
+                CGRect rect = value.CGRectValue;
+                rect = UIEdgeInsetsInsetRect(rect, border.insets);
+                rect = CGRectInset(rect, inset, inset);
+                UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + radiusDelta];
+                [path closePath];
+                CGContextAddPath(context, path.CGPath);
+            }
+            CGContextStrokePath(context);
+            CGContextRestoreGState(context);
+        }
+    }
+    
+    if (shadow.color) {
+        CGContextEndTransparencyLayer(context);
+        CGContextRestoreGState(context);
+    }
+}
+
+static void YYTextDrawLineStyle(CGContextRef context, CGFloat length, CGFloat lineWidth, YYTextLineStyle style, CGPoint position, CGColorRef color, BOOL isVertical) {
+    NSUInteger styleBase = style & 0xFF;
+    if (styleBase == 0) return;
+    
+    CGContextSaveGState(context); {
+        if (isVertical) {
+            CGFloat x, y1, y2, w;
+            y1 = YYTextCGFloatPixelRound(position.y);
+            y2 = YYTextCGFloatPixelRound(position.y + length);
+            w = (styleBase == YYTextLineStyleThick ? lineWidth * 2 : lineWidth);
+            
+            CGFloat linePixel = YYTextCGFloatToPixel(w);
+            if (fabs(linePixel - floor(linePixel)) < 0.1) {
+                int iPixel = linePixel;
+                if (iPixel == 0 || (iPixel % 2)) { // odd line pixel
+                    x = YYTextCGFloatPixelHalf(position.x);
+                } else {
+                    x = YYTextCGFloatPixelFloor(position.x);
+                }
+            } else {
+                x = position.x;
+            }
+            
+            CGContextSetStrokeColorWithColor(context, color);
+            YYTextSetLinePatternInContext(style, lineWidth, position.y, context);
+            CGContextSetLineWidth(context, w);
+            if (styleBase == YYTextLineStyleSingle) {
+                CGContextMoveToPoint(context, x, y1);
+                CGContextAddLineToPoint(context, x, y2);
+                CGContextStrokePath(context);
+            } else if (styleBase == YYTextLineStyleThick) {
+                CGContextMoveToPoint(context, x, y1);
+                CGContextAddLineToPoint(context, x, y2);
+                CGContextStrokePath(context);
+            } else if (styleBase == YYTextLineStyleDouble) {
+                CGContextMoveToPoint(context, x - w, y1);
+                CGContextAddLineToPoint(context, x - w, y2);
+                CGContextStrokePath(context);
+                CGContextMoveToPoint(context, x + w, y1);
+                CGContextAddLineToPoint(context, x + w, y2);
+                CGContextStrokePath(context);
+            }
+        } else {
+            CGFloat x1, x2, y, w;
+            x1 = YYTextCGFloatPixelRound(position.x);
+            x2 = YYTextCGFloatPixelRound(position.x + length);
+            w = (styleBase == YYTextLineStyleThick ? lineWidth * 2 : lineWidth);
+            
+            CGFloat linePixel = YYTextCGFloatToPixel(w);
+            if (fabs(linePixel - floor(linePixel)) < 0.1) {
+                int iPixel = linePixel;
+                if (iPixel == 0 || (iPixel % 2)) { // odd line pixel
+                    y = YYTextCGFloatPixelHalf(position.y);
+                } else {
+                    y = YYTextCGFloatPixelFloor(position.y);
+                }
+            } else {
+                y = position.y;
+            }
+            
+            CGContextSetStrokeColorWithColor(context, color);
+            YYTextSetLinePatternInContext(style, lineWidth, position.x, context);
+            CGContextSetLineWidth(context, w);
+            if (styleBase == YYTextLineStyleSingle) {
+                CGContextMoveToPoint(context, x1, y);
+                CGContextAddLineToPoint(context, x2, y);
+                CGContextStrokePath(context);
+            } else if (styleBase == YYTextLineStyleThick) {
+                CGContextMoveToPoint(context, x1, y);
+                CGContextAddLineToPoint(context, x2, y);
+                CGContextStrokePath(context);
+            } else if (styleBase == YYTextLineStyleDouble) {
+                CGContextMoveToPoint(context, x1, y - w);
+                CGContextAddLineToPoint(context, x2, y - w);
+                CGContextStrokePath(context);
+                CGContextMoveToPoint(context, x1, y + w);
+                CGContextAddLineToPoint(context, x2, y + w);
+                CGContextStrokePath(context);
+            }
+        }
+    } CGContextRestoreGState(context);
+}
+
+static void YYTextDrawText(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) {
+    CGContextSaveGState(context); {
+        
+        CGContextTranslateCTM(context, point.x, point.y);
+        CGContextTranslateCTM(context, 0, size.height);
+        CGContextScaleCTM(context, 1, -1);
+        CGContextSetShadow(context, CGSizeZero, 0);
+        
+        BOOL isVertical = layout.container.verticalForm;
+        CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
+        
+        NSArray *lines = layout.lines;
+        for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) {
+            YYTextLine *line = lines[l];
+            if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
+            NSArray *lineRunRanges = line.verticalRotateRange;
+            CGContextSetTextMatrix(context, CGAffineTransformIdentity);
+            CGContextSetTextPosition(context, line.position.x + verticalOffset, size.height - line.position.y);
+            CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+            for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
+                CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+                YYTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset);
+            }
+            if (cancel && cancel()) break;
+        }
+        
+        // Use this to draw frame for test/debug.
+        // CGContextTranslateCTM(context, verticalOffset, size.height);
+        // CTFrameDraw(layout.frame, context);
+        
+    } CGContextRestoreGState(context);
+}
+
+static void YYTextDrawBlockBorder(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) {
+    CGContextSaveGState(context);
+    CGContextTranslateCTM(context, point.x, point.y);
+    
+    BOOL isVertical = layout.container.verticalForm;
+    CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
+    
+    NSArray *lines = layout.lines;
+    for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) {
+        if (cancel && cancel()) break;
+        
+        YYTextLine *line = lines[l];
+        if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
+        CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+        for (NSInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
+            CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+            CFIndex glyphCount = CTRunGetGlyphCount(run);
+            if (glyphCount == 0) continue;
+            NSDictionary *attrs = (id)CTRunGetAttributes(run);
+            YYTextBorder *border = attrs[YYTextBlockBorderAttributeName];
+            if (!border) continue;
+            
+            NSUInteger lineStartIndex = line.index;
+            while (lineStartIndex > 0) {
+                if (((YYTextLine *)lines[lineStartIndex - 1]).row == line.row) lineStartIndex--;
+                else break;
+            }
+            
+            CGRect unionRect = CGRectZero;
+            NSUInteger lineStartRow = ((YYTextLine *)lines[lineStartIndex]).row;
+            NSUInteger lineContinueIndex = lineStartIndex;
+            NSUInteger lineContinueRow = lineStartRow;
+            do {
+                YYTextLine *one = lines[lineContinueIndex];
+                if (lineContinueIndex == lineStartIndex) {
+                    unionRect = one.bounds;
+                } else {
+                    unionRect = CGRectUnion(unionRect, one.bounds);
+                }
+                if (lineContinueIndex + 1 == lMax) break;
+                YYTextLine *next = lines[lineContinueIndex + 1];
+                if (next.row != lineContinueRow) {
+                    YYTextBorder *nextBorder = [layout.text yy_attribute:YYTextBlockBorderAttributeName atIndex:next.range.location];
+                    if ([nextBorder isEqual:border]) {
+                        lineContinueRow++;
+                    } else {
+                        break;
+                    }
+                }
+                lineContinueIndex++;
+            } while (true);
+            
+            if (isVertical) {
+                UIEdgeInsets insets = layout.container.insets;
+                unionRect.origin.y = insets.top;
+                unionRect.size.height = layout.container.size.height -insets.top - insets.bottom;
+            } else {
+                UIEdgeInsets insets = layout.container.insets;
+                unionRect.origin.x = insets.left;
+                unionRect.size.width = layout.container.size.width -insets.left - insets.right;
+            }
+            unionRect.origin.x += verticalOffset;
+            YYTextDrawBorderRects(context, size, border, @[[NSValue valueWithCGRect:unionRect]], isVertical);
+            
+            l = lineContinueIndex;
+            break;
+        }
+    }
+    
+    
+    CGContextRestoreGState(context);
+}
+
+static void YYTextDrawBorder(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, YYTextBorderType type, BOOL (^cancel)(void)) {
+    CGContextSaveGState(context);
+    CGContextTranslateCTM(context, point.x, point.y);
+    
+    BOOL isVertical = layout.container.verticalForm;
+    CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
+    
+    NSArray *lines = layout.lines;
+    NSString *borderKey = (type == YYTextBorderTypeNormal ? YYTextBorderAttributeName : YYTextBackgroundBorderAttributeName);
+    
+    BOOL needJumpRun = NO;
+    NSUInteger jumpRunIndex = 0;
+    
+    for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) {
+        if (cancel && cancel()) break;
+        
+        YYTextLine *line = lines[l];
+        if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
+        CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+        for (NSInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
+            if (needJumpRun) {
+                needJumpRun = NO;
+                r = jumpRunIndex + 1;
+                if (r >= rMax) break;
+            }
+            
+            CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+            CFIndex glyphCount = CTRunGetGlyphCount(run);
+            if (glyphCount == 0) continue;
+            
+            NSDictionary *attrs = (id)CTRunGetAttributes(run);
+            YYTextBorder *border = attrs[borderKey];
+            if (!border) continue;
+            
+            CFRange runRange = CTRunGetStringRange(run);
+            if (runRange.location == kCFNotFound || runRange.length == 0) continue;
+            if (runRange.location + runRange.length > layout.text.length) continue;
+            
+            NSMutableArray *runRects = [NSMutableArray new];
+            NSInteger endLineIndex = l;
+            NSInteger endRunIndex = r;
+            BOOL endFound = NO;
+            for (NSInteger ll = l; ll < lMax; ll++) {
+                if (endFound) break;
+                YYTextLine *iLine = lines[ll];
+                CFArrayRef iRuns = CTLineGetGlyphRuns(iLine.CTLine);
+                
+                CGRect extLineRect = CGRectNull;
+                for (NSInteger rr = (ll == l) ? r : 0, rrMax = CFArrayGetCount(iRuns); rr < rrMax; rr++) {
+                    CTRunRef iRun = CFArrayGetValueAtIndex(iRuns, rr);
+                    NSDictionary *iAttrs = (id)CTRunGetAttributes(iRun);
+                    YYTextBorder *iBorder = iAttrs[borderKey];
+                    if (![border isEqual:iBorder]) {
+                        endFound = YES;
+                        break;
+                    }
+                    endLineIndex = ll;
+                    endRunIndex = rr;
+                    
+                    CGPoint iRunPosition = CGPointZero;
+                    CTRunGetPositions(iRun, CFRangeMake(0, 1), &iRunPosition);
+                    CGFloat ascent, descent;
+                    CGFloat iRunWidth = CTRunGetTypographicBounds(iRun, CFRangeMake(0, 0), &ascent, &descent, NULL);
+                    
+                    if (isVertical) {
+                        YYTEXT_SWAP(iRunPosition.x, iRunPosition.y);
+                        iRunPosition.y += iLine.position.y;
+                        CGRect iRect = CGRectMake(verticalOffset + line.position.x - descent, iRunPosition.y, ascent + descent, iRunWidth);
+                        if (CGRectIsNull(extLineRect)) {
+                            extLineRect = iRect;
+                        } else {
+                            extLineRect = CGRectUnion(extLineRect, iRect);
+                        }
+                    } else {
+                        iRunPosition.x += iLine.position.x;
+                        CGRect iRect = CGRectMake(iRunPosition.x, iLine.position.y - ascent, iRunWidth, ascent + descent);
+                        if (CGRectIsNull(extLineRect)) {
+                            extLineRect = iRect;
+                        } else {
+                            extLineRect = CGRectUnion(extLineRect, iRect);
+                        }
+                    }
+                }
+                
+                if (!CGRectIsNull(extLineRect)) {
+                    [runRects addObject:[NSValue valueWithCGRect:extLineRect]];
+                }
+            }
+            
+            NSMutableArray *drawRects = [NSMutableArray new];
+            CGRect curRect= ((NSValue *)[runRects firstObject]).CGRectValue;
+            for (NSInteger re = 0, reMax = runRects.count; re < reMax; re++) {
+                CGRect rect = ((NSValue *)runRects[re]).CGRectValue;
+                if (isVertical) {
+                    if (fabs(rect.origin.x - curRect.origin.x) < 1) {
+                        curRect = YYTextMergeRectInSameLine(rect, curRect, isVertical);
+                    } else {
+                        [drawRects addObject:[NSValue valueWithCGRect:curRect]];
+                        curRect = rect;
+                    }
+                } else {
+                    if (fabs(rect.origin.y - curRect.origin.y) < 1) {
+                        curRect = YYTextMergeRectInSameLine(rect, curRect, isVertical);
+                    } else {
+                        [drawRects addObject:[NSValue valueWithCGRect:curRect]];
+                        curRect = rect;
+                    }
+                }
+            }
+            if (!CGRectEqualToRect(curRect, CGRectZero)) {
+                [drawRects addObject:[NSValue valueWithCGRect:curRect]];
+            }
+            
+            YYTextDrawBorderRects(context, size, border, drawRects, isVertical);
+            
+            if (l == endLineIndex) {
+                r = endRunIndex;
+            } else {
+                l = endLineIndex - 1;
+                needJumpRun = YES;
+                jumpRunIndex = endRunIndex;
+                break;
+            }
+            
+        }
+    }
+    
+    CGContextRestoreGState(context);
+}
+
+static void YYTextDrawDecoration(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, YYTextDecorationType type, BOOL (^cancel)(void)) {
+    NSArray *lines = layout.lines;
+    
+    CGContextSaveGState(context);
+    CGContextTranslateCTM(context, point.x, point.y);
+    
+    BOOL isVertical = layout.container.verticalForm;
+    CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
+    CGContextTranslateCTM(context, verticalOffset, 0);
+    
+    for (NSUInteger l = 0, lMax = layout.lines.count; l < lMax; l++) {
+        if (cancel && cancel()) break;
+        
+        YYTextLine *line = lines[l];
+        if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
+        CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+        for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
+            CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+            CFIndex glyphCount = CTRunGetGlyphCount(run);
+            if (glyphCount == 0) continue;
+            
+            NSDictionary *attrs = (id)CTRunGetAttributes(run);
+            YYTextDecoration *underline = attrs[YYTextUnderlineAttributeName];
+            YYTextDecoration *strikethrough = attrs[YYTextStrikethroughAttributeName];
+            
+            BOOL needDrawUnderline = NO, needDrawStrikethrough = NO;
+            if ((type & YYTextDecorationTypeUnderline) && underline.style > 0) {
+                needDrawUnderline = YES;
+            }
+            if ((type & YYTextDecorationTypeStrikethrough) && strikethrough.style > 0) {
+                needDrawStrikethrough = YES;
+            }
+            if (!needDrawUnderline && !needDrawStrikethrough) continue;
+            
+            CFRange runRange = CTRunGetStringRange(run);
+            if (runRange.location == kCFNotFound || runRange.length == 0) continue;
+            if (runRange.location + runRange.length > layout.text.length) continue;
+            NSString *runStr = [layout.text attributedSubstringFromRange:NSMakeRange(runRange.location, runRange.length)].string;
+            if (YYTextIsLinebreakString(runStr)) continue; // may need more checks...
+            
+            CGFloat xHeight, underlinePosition, lineThickness;
+            YYTextGetRunsMaxMetric(runs, &xHeight, &underlinePosition, &lineThickness);
+            
+            CGPoint underlineStart, strikethroughStart;
+            CGFloat length;
+            
+            if (isVertical) {
+                underlineStart.x = line.position.x + underlinePosition;
+                strikethroughStart.x = line.position.x + xHeight / 2;
+                
+                CGPoint runPosition = CGPointZero;
+                CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition);
+                underlineStart.y = strikethroughStart.y = runPosition.x + line.position.y;
+                length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL);
+                
+            } else {
+                underlineStart.y = line.position.y - underlinePosition;
+                strikethroughStart.y = line.position.y - xHeight / 2;
+                
+                CGPoint runPosition = CGPointZero;
+                CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition);
+                underlineStart.x = strikethroughStart.x = runPosition.x + line.position.x;
+                length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL);
+            }
+            
+            if (needDrawUnderline) {
+                CGColorRef color = underline.color.CGColor;
+                if (!color) {
+                    color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]);
+                    color = YYTextGetCGColor(color);
+                }
+                CGFloat thickness = underline.width ? underline.width.floatValue : lineThickness;
+                YYTextShadow *shadow = underline.shadow;
+                while (shadow) {
+                    if (!shadow.color) {
+                        shadow = shadow.subShadow;
+                        continue;
+                    }
+                    CGFloat offsetAlterX = size.width + 0xFFFF;
+                    CGContextSaveGState(context); {
+                        CGSize offset = shadow.offset;
+                        offset.width -= offsetAlterX;
+                        CGContextSaveGState(context); {
+                            CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor);
+                            CGContextSetBlendMode(context, shadow.blendMode);
+                            CGContextTranslateCTM(context, offsetAlterX, 0);
+                            YYTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical);
+                        } CGContextRestoreGState(context);
+                    } CGContextRestoreGState(context);
+                    shadow = shadow.subShadow;
+                }
+                YYTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical);
+            }
+            
+            if (needDrawStrikethrough) {
+                CGColorRef color = strikethrough.color.CGColor;
+                if (!color) {
+                    color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]);
+                    color = YYTextGetCGColor(color);
+                }
+                CGFloat thickness = strikethrough.width ? strikethrough.width.floatValue : lineThickness;
+                YYTextShadow *shadow = underline.shadow;
+                while (shadow) {
+                    if (!shadow.color) {
+                        shadow = shadow.subShadow;
+                        continue;
+                    }
+                    CGFloat offsetAlterX = size.width + 0xFFFF;
+                    CGContextSaveGState(context); {
+                        CGSize offset = shadow.offset;
+                        offset.width -= offsetAlterX;
+                        CGContextSaveGState(context); {
+                            CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor);
+                            CGContextSetBlendMode(context, shadow.blendMode);
+                            CGContextTranslateCTM(context, offsetAlterX, 0);
+                            YYTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical);
+                        } CGContextRestoreGState(context);
+                    } CGContextRestoreGState(context);
+                    shadow = shadow.subShadow;
+                }
+                YYTextDrawLineStyle(context, length, thickness, strikethrough.style, strikethroughStart, color, isVertical);
+            }
+        }
+    }
+    CGContextRestoreGState(context);
+}
+
+static void YYTextDrawAttachment(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, UIView *targetView, CALayer *targetLayer, BOOL (^cancel)(void)) {
+    
+    BOOL isVertical = layout.container.verticalForm;
+    CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
+    
+    for (NSUInteger i = 0, max = layout.attachments.count; i < max; i++) {
+        YYTextAttachment *a = layout.attachments[i];
+        if (!a.content) continue;
+        
+        UIImage *image = nil;
+        UIView *view = nil;
+        CALayer *layer = nil;
+        if ([a.content isKindOfClass:[UIImage class]]) {
+            image = a.content;
+        } else if ([a.content isKindOfClass:[UIView class]]) {
+            view = a.content;
+        } else if ([a.content isKindOfClass:[CALayer class]]) {
+            layer = a.content;
+        }
+        if (!image && !view && !layer) continue;
+        if (image && !context) continue;
+        if (view && !targetView) continue;
+        if (layer && !targetLayer) continue;
+        if (cancel && cancel()) break;
+        
+        CGSize asize = image ? image.size : view ? view.frame.size : layer.frame.size;
+        CGRect rect = ((NSValue *)layout.attachmentRects[i]).CGRectValue;
+        if (isVertical) {
+            rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(a.contentInsets));
+        } else {
+            rect = UIEdgeInsetsInsetRect(rect, a.contentInsets);
+        }
+        rect = YYTextCGRectFitWithContentMode(rect, asize, a.contentMode);
+        rect = YYTextCGRectPixelRound(rect);
+        rect = CGRectStandardize(rect);
+        rect.origin.x += point.x + verticalOffset;
+        rect.origin.y += point.y;
+        if (image) {
+            CGImageRef ref = image.CGImage;
+            if (ref) {
+                CGContextSaveGState(context);
+                CGContextTranslateCTM(context, 0, CGRectGetMaxY(rect) + CGRectGetMinY(rect));
+                CGContextScaleCTM(context, 1, -1);
+                CGContextDrawImage(context, rect, ref);
+                CGContextRestoreGState(context);
+            }
+        } else if (view) {
+            view.frame = rect;
+            [targetView addSubview:view];
+        } else if (layer) {
+            layer.frame = rect;
+            [targetLayer addSublayer:layer];
+        }
+    }
+}
+
+static void YYTextDrawShadow(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) {
+    //move out of context. (0xFFFF is just a random large number)
+    CGFloat offsetAlterX = size.width + 0xFFFF;
+    
+    BOOL isVertical = layout.container.verticalForm;
+    CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
+    
+    CGContextSaveGState(context); {
+        CGContextTranslateCTM(context, point.x, point.y);
+        CGContextTranslateCTM(context, 0, size.height);
+        CGContextScaleCTM(context, 1, -1);
+        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
+        NSArray *lines = layout.lines;
+        for (NSUInteger l = 0, lMax = layout.lines.count; l < lMax; l++) {
+            if (cancel && cancel()) break;
+            
+            YYTextLine *line = lines[l];
+            if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
+            NSArray *lineRunRanges = line.verticalRotateRange;
+            CGContextSetTextPosition(context, line.position.x, size.height - line.position.y);
+            CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+            for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
+                CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+                NSDictionary *attrs = (id)CTRunGetAttributes(run);
+                YYTextShadow *shadow = attrs[YYTextShadowAttributeName];
+                YYTextShadow *nsShadow = [YYTextShadow shadowWithNSShadow:attrs[NSShadowAttributeName]]; // NSShadow compatible
+                if (nsShadow) {
+                    nsShadow.subShadow = shadow;
+                    shadow = nsShadow;
+                }
+                while (shadow) {
+                    if (!shadow.color) {
+                        shadow = shadow.subShadow;
+                        continue;
+                    }
+                    CGSize offset = shadow.offset;
+                    offset.width -= offsetAlterX;
+                    CGContextSaveGState(context); {
+                        CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor);
+                        CGContextSetBlendMode(context, shadow.blendMode);
+                        CGContextTranslateCTM(context, offsetAlterX, 0);
+                        YYTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset);
+                    } CGContextRestoreGState(context);
+                    shadow = shadow.subShadow;
+                }
+            }
+        }
+    } CGContextRestoreGState(context);
+}
+
+static void YYTextDrawInnerShadow(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) {
+    CGContextSaveGState(context);
+    CGContextTranslateCTM(context, point.x, point.y);
+    CGContextTranslateCTM(context, 0, size.height);
+    CGContextScaleCTM(context, 1, -1);
+    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
+    
+    BOOL isVertical = layout.container.verticalForm;
+    CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
+    
+    NSArray *lines = layout.lines;
+    for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) {
+        if (cancel && cancel()) break;
+        
+        YYTextLine *line = lines[l];
+        if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
+        NSArray *lineRunRanges = line.verticalRotateRange;
+        CGContextSetTextPosition(context, line.position.x, size.height - line.position.y);
+        CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+        for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
+            CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+            if (CTRunGetGlyphCount(run) == 0) continue;
+            NSDictionary *attrs = (id)CTRunGetAttributes(run);
+            YYTextShadow *shadow = attrs[YYTextInnerShadowAttributeName];
+            while (shadow) {
+                if (!shadow.color) {
+                    shadow = shadow.subShadow;
+                    continue;
+                }
+                CGPoint runPosition = CGPointZero;
+                CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition);
+                CGRect runImageBounds = CTRunGetImageBounds(run, context, CFRangeMake(0, 0));
+                runImageBounds.origin.x += runPosition.x;
+                if (runImageBounds.size.width < 0.1 || runImageBounds.size.height < 0.1) continue;
+                
+                CFDictionaryRef runAttrs = CTRunGetAttributes(run);
+                NSValue *glyphTransformValue = CFDictionaryGetValue(runAttrs, (__bridge const void *)(YYTextGlyphTransformAttributeName));
+                if (glyphTransformValue) {
+                    runImageBounds = CGRectMake(0, 0, size.width, size.height);
+                }
+                
+                // text inner shadow
+                CGContextSaveGState(context); {
+                    CGContextSetBlendMode(context, shadow.blendMode);
+                    CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL);
+                    CGContextSetAlpha(context, CGColorGetAlpha(shadow.color.CGColor));
+                    CGContextClipToRect(context, runImageBounds);
+                    CGContextBeginTransparencyLayer(context, NULL); {
+                        UIColor *opaqueShadowColor = [shadow.color colorWithAlphaComponent:1];
+                        CGContextSetShadowWithColor(context, shadow.offset, shadow.radius, opaqueShadowColor.CGColor);
+                        CGContextSetFillColorWithColor(context, opaqueShadowColor.CGColor);
+                        CGContextSetBlendMode(context, kCGBlendModeSourceOut);
+                        CGContextBeginTransparencyLayer(context, NULL); {
+                            CGContextFillRect(context, runImageBounds);
+                            CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
+                            CGContextBeginTransparencyLayer(context, NULL); {
+                                YYTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset);
+                            } CGContextEndTransparencyLayer(context);
+                        } CGContextEndTransparencyLayer(context);
+                    } CGContextEndTransparencyLayer(context);
+                } CGContextRestoreGState(context);
+                shadow = shadow.subShadow;
+            }
+        }
+    }
+    
+    CGContextRestoreGState(context);
+}
+
+static void YYTextDrawDebug(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, YYTextDebugOption *op) {
+    UIGraphicsPushContext(context);
+    CGContextSaveGState(context);
+    CGContextTranslateCTM(context, point.x, point.y);
+    CGContextSetLineWidth(context, 1.0 / YYTextScreenScale());
+    CGContextSetLineDash(context, 0, NULL, 0);
+    CGContextSetLineJoin(context, kCGLineJoinMiter);
+    CGContextSetLineCap(context, kCGLineCapButt);
+    
+    BOOL isVertical = layout.container.verticalForm;
+    CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0;
+    CGContextTranslateCTM(context, verticalOffset, 0);
+    
+    if (op.CTFrameBorderColor || op.CTFrameFillColor) {
+        UIBezierPath *path = layout.container.path;
+        if (!path) {
+            CGRect rect = (CGRect){CGPointZero, layout.container.size};
+            rect = UIEdgeInsetsInsetRect(rect, layout.container.insets);
+            if (op.CTFrameBorderColor) rect = YYTextCGRectPixelHalf(rect);
+            else rect = YYTextCGRectPixelRound(rect);
+            path = [UIBezierPath bezierPathWithRect:rect];
+        }
+        [path closePath];
+        
+        for (UIBezierPath *ex in layout.container.exclusionPaths) {
+            [path appendPath:ex];
+        }
+        if (op.CTFrameFillColor) {
+            [op.CTFrameFillColor setFill];
+            if (layout.container.pathLineWidth > 0) {
+                CGContextSaveGState(context); {
+                    CGContextBeginTransparencyLayer(context, NULL); {
+                        CGContextAddPath(context, path.CGPath);
+                        if (layout.container.pathFillEvenOdd) {
+                            CGContextEOFillPath(context);
+                        } else {
+                            CGContextFillPath(context);
+                        }
+                        CGContextSetBlendMode(context, kCGBlendModeDestinationOut);
+                        [[UIColor blackColor] setFill];
+                        CGPathRef cgPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, layout.container.pathLineWidth, kCGLineCapButt, kCGLineJoinMiter, 0);
+                        if (cgPath) {
+                            CGContextAddPath(context, cgPath);
+                            CGContextFillPath(context);
+                        }
+                        CGPathRelease(cgPath);
+                    } CGContextEndTransparencyLayer(context);
+                } CGContextRestoreGState(context);
+            } else {
+                CGContextAddPath(context, path.CGPath);
+                if (layout.container.pathFillEvenOdd) {
+                    CGContextEOFillPath(context);
+                } else {
+                    CGContextFillPath(context);
+                }
+            }
+        }
+        if (op.CTFrameBorderColor) {
+            CGContextSaveGState(context); {
+                if (layout.container.pathLineWidth > 0) {
+                    CGContextSetLineWidth(context, layout.container.pathLineWidth);
+                }
+                [op.CTFrameBorderColor setStroke];
+                CGContextAddPath(context, path.CGPath);
+                CGContextStrokePath(context);
+            } CGContextRestoreGState(context);
+        }
+    }
+    
+    NSArray *lines = layout.lines;
+    for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) {
+        YYTextLine *line = lines[l];
+        if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
+        CGRect lineBounds = line.bounds;
+        if (op.CTLineFillColor) {
+            [op.CTLineFillColor setFill];
+            CGContextAddRect(context, YYTextCGRectPixelRound(lineBounds));
+            CGContextFillPath(context);
+        }
+        if (op.CTLineBorderColor) {
+            [op.CTLineBorderColor setStroke];
+            CGContextAddRect(context, YYTextCGRectPixelHalf(lineBounds));
+            CGContextStrokePath(context);
+        }
+        if (op.baselineColor) {
+            [op.baselineColor setStroke];
+            if (isVertical) {
+                CGFloat x = YYTextCGFloatPixelHalf(line.position.x);
+                CGFloat y1 = YYTextCGFloatPixelHalf(line.top);
+                CGFloat y2 = YYTextCGFloatPixelHalf(line.bottom);
+                CGContextMoveToPoint(context, x, y1);
+                CGContextAddLineToPoint(context, x, y2);
+                CGContextStrokePath(context);
+            } else {
+                CGFloat x1 = YYTextCGFloatPixelHalf(lineBounds.origin.x);
+                CGFloat x2 = YYTextCGFloatPixelHalf(lineBounds.origin.x + lineBounds.size.width);
+                CGFloat y = YYTextCGFloatPixelHalf(line.position.y);
+                CGContextMoveToPoint(context, x1, y);
+                CGContextAddLineToPoint(context, x2, y);
+                CGContextStrokePath(context);
+            }
+        }
+        if (op.CTLineNumberColor) {
+            [op.CTLineNumberColor set];
+            NSMutableAttributedString *num = [[NSMutableAttributedString alloc] initWithString:@(l).description];
+            num.yy_color = op.CTLineNumberColor;
+            num.yy_font = [UIFont systemFontOfSize:6];
+            [num drawAtPoint:CGPointMake(line.position.x, line.position.y - (isVertical ? 1 : 6))];
+        }
+        if (op.CTRunFillColor || op.CTRunBorderColor || op.CTRunNumberColor || op.CGGlyphFillColor || op.CGGlyphBorderColor) {
+            CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
+            for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
+                CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+                CFIndex glyphCount = CTRunGetGlyphCount(run);
+                if (glyphCount == 0) continue;
+                
+                CGPoint glyphPositions[glyphCount];
+                CTRunGetPositions(run, CFRangeMake(0, glyphCount), glyphPositions);
+                
+                CGSize glyphAdvances[glyphCount];
+                CTRunGetAdvances(run, CFRangeMake(0, glyphCount), glyphAdvances);
+                
+                CGPoint runPosition = glyphPositions[0];
+                if (isVertical) {
+                    YYTEXT_SWAP(runPosition.x, 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;
+                }
+                
+                CGFloat ascent, descent, leading;
+                CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading);
+                CGRect runTypoBounds;
+                if (isVertical) {
+                    runTypoBounds = CGRectMake(runPosition.x - descent, runPosition.y, ascent + descent, width);
+                } else {
+                    runTypoBounds = CGRectMake(runPosition.x, line.position.y - ascent, width, ascent + descent);
+                }
+                
+                if (op.CTRunFillColor) {
+                    [op.CTRunFillColor setFill];
+                    CGContextAddRect(context, YYTextCGRectPixelRound(runTypoBounds));
+                    CGContextFillPath(context);
+                }
+                if (op.CTRunBorderColor) {
+                    [op.CTRunBorderColor setStroke];
+                    CGContextAddRect(context, YYTextCGRectPixelHalf(runTypoBounds));
+                    CGContextStrokePath(context);
+                }
+                if (op.CTRunNumberColor) {
+                    [op.CTRunNumberColor set];
+                    NSMutableAttributedString *num = [[NSMutableAttributedString alloc] initWithString:@(r).description];
+                    num.yy_color = op.CTRunNumberColor;
+                    num.yy_font = [UIFont systemFontOfSize:6];
+                    [num drawAtPoint:CGPointMake(runTypoBounds.origin.x, runTypoBounds.origin.y - 1)];
+                }
+                if (op.CGGlyphBorderColor || op.CGGlyphFillColor) {
+                    for (NSUInteger g = 0; g < glyphCount; g++) {
+                        CGPoint pos = glyphPositions[g];
+                        CGSize adv = glyphAdvances[g];
+                        CGRect rect;
+                        if (isVertical) {
+                            YYTEXT_SWAP(pos.x, pos.y);
+                            pos.x = runPosition.x;
+                            pos.y += line.position.y;
+                            rect = CGRectMake(pos.x - descent, pos.y, runTypoBounds.size.width, adv.width);
+                        } else {
+                            pos.x += line.position.x;
+                            pos.y = runPosition.y;
+                            rect = CGRectMake(pos.x, pos.y - ascent, adv.width, runTypoBounds.size.height);
+                        }
+                        if (op.CGGlyphFillColor) {
+                            [op.CGGlyphFillColor setFill];
+                            CGContextAddRect(context, YYTextCGRectPixelRound(rect));
+                            CGContextFillPath(context);
+                        }
+                        if (op.CGGlyphBorderColor) {
+                            [op.CGGlyphBorderColor setStroke];
+                            CGContextAddRect(context, YYTextCGRectPixelHalf(rect));
+                            CGContextStrokePath(context);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    CGContextRestoreGState(context);
+    UIGraphicsPopContext();
+}
+
+
+- (void)drawInContext:(CGContextRef)context
+                 size:(CGSize)size
+                point:(CGPoint)point
+                 view:(UIView *)view
+                layer:(CALayer *)layer
+                debug:(YYTextDebugOption *)debug
+                cancel:(BOOL (^)(void))cancel{
+    @autoreleasepool {
+        if (self.needDrawBlockBorder && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawBlockBorder(self, context, size, point, cancel);
+        }
+        if (self.needDrawBackgroundBorder && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawBorder(self, context, size, point, YYTextBorderTypeBackgound, cancel);
+        }
+        if (self.needDrawShadow && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawShadow(self, context, size, point, cancel);
+        }
+        if (self.needDrawUnderline && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawDecoration(self, context, size, point, YYTextDecorationTypeUnderline, cancel);
+        }
+        if (self.needDrawText && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawText(self, context, size, point, cancel);
+        }
+        if (self.needDrawAttachment && (context || view || layer)) {
+            if (cancel && cancel()) return;
+            YYTextDrawAttachment(self, context, size, point, view, layer, cancel);
+        }
+        if (self.needDrawInnerShadow && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawInnerShadow(self, context, size, point, cancel);
+        }
+        if (self.needDrawStrikethrough && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawDecoration(self, context, size, point, YYTextDecorationTypeStrikethrough, cancel);
+        }
+        if (self.needDrawBorder && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawBorder(self, context, size, point, YYTextBorderTypeNormal, cancel);
+        }
+        if (debug.needDrawDebug && context) {
+            if (cancel && cancel()) return;
+            YYTextDrawDebug(self, context, size, point, debug);
+        }
+    }
+}
+
+- (void)drawInContext:(CGContextRef)context
+                 size:(CGSize)size
+                debug:(YYTextDebugOption *)debug {
+    [self drawInContext:context size:size point:CGPointZero view:nil layer:nil debug:debug cancel:nil];
+}
+
+- (void)addAttachmentToView:(UIView *)view layer:(CALayer *)layer {
+    NSAssert([NSThread isMainThread], @"This method must be called on the main thread");
+    [self drawInContext:NULL size:CGSizeZero point:CGPointZero view:view layer:layer debug:nil cancel:nil];
+}
+
+- (void)removeAttachmentFromViewAndLayer {
+    NSAssert([NSThread isMainThread], @"This method must be called on the main thread");
+    for (YYTextAttachment *a in self.attachments) {
+        if ([a.content isKindOfClass:[UIView class]]) {
+            UIView *v = a.content;
+            [v removeFromSuperview];
+        } else if ([a.content isKindOfClass:[CALayer class]]) {
+            CALayer *l = a.content;
+            [l removeFromSuperlayer];
+        }
+    }
+}
+
+@end

+ 84 - 0
Demo/Objective_C_Demo/YYText/YYTextLine.h

@@ -0,0 +1,84 @@
+//
+//  YYTextLine.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/3/10.
+//  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>
+#import <CoreText/CoreText.h>
+
+#if __has_include(<YYText/YYText.h>)
+#import <YYText/YYTextAttribute.h>
+#else
+#import "YYTextAttribute.h"
+#endif
+
+@class YYTextRunGlyphRange;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ A text line object wrapped `CTLineRef`, see `YYTextLayout` for more.
+ */
+@interface YYTextLine : NSObject
+
++ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical;
+
+@property (nonatomic) NSUInteger index;     ///< line index
+@property (nonatomic) NSUInteger row;       ///< line row
+@property (nullable, nonatomic, strong) NSArray<NSArray<YYTextRunGlyphRange *> *> *verticalRotateRange; ///< Run rotate range
+
+@property (nonatomic, readonly) CTLineRef CTLine;   ///< CoreText line
+@property (nonatomic, readonly) NSRange range;      ///< string range
+@property (nonatomic, readonly) BOOL vertical;      ///< vertical form
+
+@property (nonatomic, readonly) CGRect bounds;      ///< bounds (ascent + descent)
+@property (nonatomic, readonly) CGSize size;        ///< bounds.size
+@property (nonatomic, readonly) CGFloat width;      ///< bounds.size.width
+@property (nonatomic, readonly) CGFloat height;     ///< bounds.size.height
+@property (nonatomic, readonly) CGFloat top;        ///< bounds.origin.y
+@property (nonatomic, readonly) CGFloat bottom;     ///< bounds.origin.y + bounds.size.height
+@property (nonatomic, readonly) CGFloat left;       ///< bounds.origin.x
+@property (nonatomic, readonly) CGFloat right;      ///< bounds.origin.x + bounds.size.width
+
+@property (nonatomic)   CGPoint position;   ///< baseline position
+@property (nonatomic, readonly) CGFloat ascent;     ///< line ascent
+@property (nonatomic, readonly) CGFloat descent;    ///< line descent
+@property (nonatomic, readonly) CGFloat leading;    ///< line leading
+@property (nonatomic, readonly) CGFloat lineWidth;  ///< line width
+@property (nonatomic, readonly) CGFloat trailingWhitespaceWidth;
+
+@property (nonatomic, readonly) NSArray<YYTextAttachment *> *attachments; ///< YYTextAttachment
+@property (nonatomic, readonly) NSArray<NSValue *> *attachmentRanges;     ///< NSRange(NSValue)
+@property (nonatomic, readonly) NSArray<NSValue *> *attachmentRects;      ///< CGRect(NSValue)
+
+@end
+
+
+typedef NS_ENUM(NSUInteger, YYTextRunGlyphDrawMode) {
+    /// No rotate.
+    YYTextRunGlyphDrawModeHorizontal = 0,
+    
+    /// Rotate vertical for single glyph.
+    YYTextRunGlyphDrawModeVerticalRotate = 1,
+    
+    /// Rotate vertical for single glyph, and move the glyph to a better position,
+    /// such as fullwidth punctuation.
+    YYTextRunGlyphDrawModeVerticalRotateMove = 2,
+};
+
+/**
+ A range in CTRun, used for vertical form.
+ */
+@interface YYTextRunGlyphRange : NSObject
+@property (nonatomic) NSRange glyphRangeInRun;
+@property (nonatomic) YYTextRunGlyphDrawMode drawMode;
++ (instancetype)rangeWithRange:(NSRange)range drawMode:(YYTextRunGlyphDrawMode)mode;
+@end
+
+NS_ASSUME_NONNULL_END

+ 167 - 0
Demo/Objective_C_Demo/YYText/YYTextLine.m

@@ -0,0 +1,167 @@
+//
+//  YYYTextLine.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/3/3.
+//  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 "YYTextLine.h"
+#import "YYTextUtilities.h"
+
+
+@implementation YYTextLine {
+    CGFloat _firstGlyphPos; // first glyph position for baseline, typically 0.
+}
+
++ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical {
+    if (!CTLine) return nil;
+    YYTextLine *line = [self new];
+    line->_position = position;
+    line->_vertical = isVertical;
+    [line setCTLine:CTLine];
+    return line;
+}
+
+- (void)dealloc {
+    if (_CTLine) CFRelease(_CTLine);
+}
+
+- (void)setCTLine:(CTLineRef)CTLine {
+    if (_CTLine != CTLine) {
+        if (CTLine) CFRetain(CTLine);
+        if (_CTLine) CFRelease(_CTLine);
+        _CTLine = CTLine;
+        if (_CTLine) {
+            _lineWidth = CTLineGetTypographicBounds(_CTLine, &_ascent, &_descent, &_leading);
+            CFRange range = CTLineGetStringRange(_CTLine);
+            _range = NSMakeRange(range.location, range.length);
+            if (CTLineGetGlyphCount(_CTLine) > 0) {
+                CFArrayRef runs = CTLineGetGlyphRuns(_CTLine);
+                CTRunRef run = CFArrayGetValueAtIndex(runs, 0);
+                CGPoint pos;
+                CTRunGetPositions(run, CFRangeMake(0, 1), &pos);
+                _firstGlyphPos = pos.x;
+            } else {
+                _firstGlyphPos = 0;
+            }
+            _trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(_CTLine);
+        } else {
+            _lineWidth = _ascent = _descent = _leading = _firstGlyphPos = _trailingWhitespaceWidth = 0;
+            _range = NSMakeRange(0, 0);
+        }
+        [self reloadBounds];
+    }
+}
+
+- (void)setPosition:(CGPoint)position {
+    _position = position;
+    [self reloadBounds];
+}
+
+- (void)reloadBounds {
+    if (_vertical) {
+        _bounds = CGRectMake(_position.x - _descent, _position.y, _ascent + _descent, _lineWidth);
+        _bounds.origin.y += _firstGlyphPos;
+    } else {
+        _bounds = CGRectMake(_position.x, _position.y - _ascent, _lineWidth, _ascent + _descent);
+        _bounds.origin.x += _firstGlyphPos;
+    }
+    
+    _attachments = nil;
+    _attachmentRanges = nil;
+    _attachmentRects = nil;
+    if (!_CTLine) return;
+    CFArrayRef runs = CTLineGetGlyphRuns(_CTLine);
+    NSUInteger runCount = CFArrayGetCount(runs);
+    if (runCount == 0) return;
+    
+    NSMutableArray *attachments = [NSMutableArray new];
+    NSMutableArray *attachmentRanges = [NSMutableArray new];
+    NSMutableArray *attachmentRects = [NSMutableArray new];
+    for (NSUInteger r = 0; r < runCount; r++) {
+        CTRunRef run = CFArrayGetValueAtIndex(runs, r);
+        CFIndex glyphCount = CTRunGetGlyphCount(run);
+        if (glyphCount == 0) continue;
+        NSDictionary *attrs = (id)CTRunGetAttributes(run);
+        YYTextAttachment *attachment = attrs[YYTextAttachmentAttributeName];
+        if (attachment) {
+            CGPoint runPosition = CGPointZero;
+            CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition);
+            
+            CGFloat ascent, descent, leading, runWidth;
+            CGRect runTypoBounds;
+            runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading);
+            
+            if (_vertical) {
+                YYTEXT_SWAP(runPosition.x, runPosition.y);
+                runPosition.y = _position.y + runPosition.y;
+                runTypoBounds = CGRectMake(_position.x + runPosition.x - descent, runPosition.y , ascent + descent, runWidth);
+            } else {
+                runPosition.x += _position.x;
+                runPosition.y = _position.y - runPosition.y;
+                runTypoBounds = CGRectMake(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent);
+            }
+            
+            NSRange runRange = YYTextNSRangeFromCFRange(CTRunGetStringRange(run));
+            [attachments addObject:attachment];
+            [attachmentRanges addObject:[NSValue valueWithRange:runRange]];
+            [attachmentRects addObject:[NSValue valueWithCGRect:runTypoBounds]];
+        }
+    }
+    _attachments = attachments.count ? attachments : nil;
+    _attachmentRanges = attachmentRanges.count ? attachmentRanges : nil;
+    _attachmentRects = attachmentRects.count ? attachmentRects : nil;
+}
+
+- (CGSize)size {
+    return _bounds.size;
+}
+
+- (CGFloat)width {
+    return CGRectGetWidth(_bounds);
+}
+
+- (CGFloat)height {
+    return CGRectGetHeight(_bounds);
+}
+
+- (CGFloat)top {
+    return CGRectGetMinY(_bounds);
+}
+
+- (CGFloat)bottom {
+    return CGRectGetMaxY(_bounds);
+}
+
+- (CGFloat)left {
+    return CGRectGetMinX(_bounds);
+}
+
+- (CGFloat)right {
+    return CGRectGetMaxX(_bounds);
+}
+
+- (NSString *)description {
+    NSMutableString *desc = @"".mutableCopy;
+    NSRange range = self.range;
+    [desc appendFormat:@"<YYTextLine: %p> row:%zd range:%tu,%tu",self, self.row, range.location, range.length];
+    [desc appendFormat:@" position:%@",NSStringFromCGPoint(self.position)];
+    [desc appendFormat:@" bounds:%@",NSStringFromCGRect(self.bounds)];
+    return desc;
+}
+
+@end
+
+
+@implementation YYTextRunGlyphRange
++ (instancetype)rangeWithRange:(NSRange)range drawMode:(YYTextRunGlyphDrawMode)mode {
+    YYTextRunGlyphRange *one = [self new];
+    one.glyphRangeInRun = range;
+    one.drawMode = mode;
+    return one;
+}
+@end

+ 52 - 0
Demo/Objective_C_Demo/YYText/YYTextMagnifier.h

@@ -0,0 +1,52 @@
+//
+//  YYTextMagnifier.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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(<YYText/YYText.h>)
+#import <YYText/YYTextAttribute.h>
+#else
+#import "YYTextAttribute.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Magnifier type
+typedef NS_ENUM(NSInteger, YYTextMagnifierType) {
+    YYTextMagnifierTypeCaret,  ///< Circular magnifier
+    YYTextMagnifierTypeRanged, ///< Round rectangle magnifier
+};
+
+/**
+ A magnifier view which can be displayed in `YYTextEffectWindow`.
+ 
+ @discussion Use `magnifierWithType:` to create instance.
+ Typically, you should not use this class directly.
+ */
+@interface YYTextMagnifier : UIView
+
+/// Create a mangifier with the specified type. @param type The magnifier type.
++ (id)magnifierWithType:(YYTextMagnifierType)type;
+
+@property (nonatomic, readonly) YYTextMagnifierType type;  ///< Type of magnifier
+@property (nonatomic, readonly) CGSize fitSize;            ///< The 'best' size for magnifier view.
+@property (nonatomic, readonly) CGSize snapshotSize;       ///< The 'best' snapshot image size for magnifier.
+@property (nullable, nonatomic, strong) UIImage *snapshot; ///< The image in magnifier (readwrite).
+
+@property (nullable, nonatomic, weak) UIView *hostView;   ///< The coordinate based view.
+@property (nonatomic) CGPoint hostCaptureCenter;          ///< The snapshot capture center in `hostView`.
+@property (nonatomic) CGPoint hostPopoverCenter;          ///< The popover center in `hostView`.
+@property (nonatomic) BOOL hostVerticalForm;              ///< The host view is vertical form.
+@property (nonatomic) BOOL captureDisabled;               ///< A hint for `YYTextEffectWindow` to disable capture.
+@property (nonatomic) BOOL captureFadeAnimation;          ///< Show fade animation when the snapshot image changed.
+@end
+
+NS_ASSUME_NONNULL_END

+ 355 - 0
Demo/Objective_C_Demo/YYText/YYTextMagnifier.m

@@ -0,0 +1,355 @@
+//
+//  YYTextMagnifier.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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 "YYTextMagnifier.h"
+#import "YYTextUtilities.h"
+
+#define kCaptureDisableFadeTime 0.1
+
+
+@interface _YYTextMagnifierCaret : YYTextMagnifier {
+    UIImageView *_contentView;
+    UIImageView *_coverView;
+}
+@end
+
+@implementation _YYTextMagnifierCaret
+
+#define kMultiple 1.2
+#define kDiameter 113.0
+#define kPadding 7.0
+#define kSize CGSizeMake(kDiameter + kPadding * 2, kDiameter + kPadding * 2)
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    _contentView = [UIImageView new];
+    _contentView.frame = CGRectMake(kPadding, kPadding, kDiameter, kDiameter);
+    _contentView.layer.cornerRadius = kDiameter / 2;
+    _contentView.clipsToBounds = YES;
+    [self addSubview:_contentView];
+    
+    _coverView = [UIImageView new];
+    _coverView.frame = (CGRect){.origin = CGPointZero, .size = kSize};
+    _coverView.image = [self.class coverImage];
+    [self addSubview:_coverView];
+    return self;
+}
+
+- (instancetype)init {
+    self = [self initWithFrame:CGRectZero];
+    self.frame = (CGRect){.size = [self sizeThatFits:CGSizeZero]};
+    return self;
+}
+
+- (YYTextMagnifierType)type {
+    return YYTextMagnifierTypeCaret;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    return kSize;
+}
+
+- (void)setSnapshot:(UIImage *)snapshot {
+    if (self.captureFadeAnimation) {
+        [_contentView.layer removeAnimationForKey:@"contents"];
+        CABasicAnimation *animation = [CABasicAnimation animation];
+        animation.duration = kCaptureDisableFadeTime;
+        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+        [_contentView.layer addAnimation:animation forKey:@"contents"];
+    }
+    _contentView.image = snapshot;
+}
+
+- (UIImage *)snapshot {
+    return _contentView.image;
+}
+
+- (CGSize)snapshotSize {
+    CGFloat length = floor(kDiameter / 1.2);
+    return CGSizeMake(length, length);
+}
+
+- (CGSize)fitSize {
+    return [self sizeThatFits:CGSizeZero];
+}
+
++ (UIImage *)coverImage {
+    static UIImage *image;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        CGSize size = kSize;
+        CGRect rect = (CGRect) {.size = size, .origin = CGPointZero};
+        rect = CGRectInset(rect, kPadding, kPadding);
+        
+        UIGraphicsBeginImageContextWithOptions(size, NO, 0);
+        CGContextRef context = UIGraphicsGetCurrentContext();
+        
+        CGPathRef boxPath = CGPathCreateWithRect(CGRectMake(0, 0, size.width, size.height), NULL);
+        CGPathRef fillPath = CGPathCreateWithEllipseInRect(rect, NULL);
+        CGPathRef strokePath = CGPathCreateWithEllipseInRect(YYTextCGRectPixelHalf(rect), NULL);
+        
+        // inner shadow
+        CGContextSaveGState(context); {
+            CGFloat blurRadius = 25;
+            CGSize offset = CGSizeMake(0, 15);
+            CGColorRef shadowColor = [UIColor colorWithWhite:0 alpha:0.16].CGColor;
+            CGColorRef opaqueShadowColor = CGColorCreateCopyWithAlpha(shadowColor, 1.0);
+            CGContextAddPath(context, fillPath);
+            CGContextClip(context);
+            CGContextSetAlpha(context, CGColorGetAlpha(shadowColor));
+            CGContextBeginTransparencyLayer(context, NULL); {
+                CGContextSetShadowWithColor(context, offset, blurRadius, opaqueShadowColor);
+                CGContextSetBlendMode(context, kCGBlendModeSourceOut);
+                CGContextSetFillColorWithColor(context, opaqueShadowColor);
+                CGContextAddPath(context, fillPath);
+                CGContextFillPath(context);
+            } CGContextEndTransparencyLayer(context);
+            CGColorRelease(opaqueShadowColor);
+        } CGContextRestoreGState(context);
+        
+        // outer shadow
+        CGContextSaveGState(context); {
+            CGContextAddPath(context, boxPath);
+            CGContextAddPath(context, fillPath);
+            CGContextEOClip(context);
+            CGColorRef shadowColor = [UIColor colorWithWhite:0 alpha:0.32].CGColor;
+            CGContextSetShadowWithColor(context, CGSizeMake(0, 1.5), 3, shadowColor);
+            CGContextBeginTransparencyLayer(context, NULL); {
+                CGContextAddPath(context, fillPath);
+                [[UIColor colorWithWhite:0.7 alpha:1.000] setFill];
+                CGContextFillPath(context);
+            } CGContextEndTransparencyLayer(context);
+        } CGContextRestoreGState(context);
+        
+        // stroke
+        CGContextSaveGState(context); {
+            CGContextAddPath(context, strokePath);
+            [[UIColor colorWithWhite:0.6 alpha:1] setStroke];
+            CGContextSetLineWidth(context, YYTextCGFloatFromPixel(1));
+            CGContextStrokePath(context);
+        } CGContextRestoreGState(context);
+        
+        CFRelease(boxPath);
+        CFRelease(fillPath);
+        CFRelease(strokePath);
+        
+        image = UIGraphicsGetImageFromCurrentImageContext();
+        UIGraphicsEndImageContext();
+        
+    });
+    return image;
+}
+
+
+#undef kMultiple
+#undef kDiameter
+#undef kPadding
+#undef kSize
+
+@end
+
+
+
+@interface _YYTextMagnifierRanged : YYTextMagnifier {
+    UIImageView *_contentView;
+    UIImageView *_coverView;
+}
+@end
+
+
+@implementation _YYTextMagnifierRanged
+#define kMultiple 1.2
+#define kSize CGSizeMake(141, 60)
+#define kPadding YYTextCGFloatPixelHalf(6.0)
+#define kRadius 6.0
+#define kHeight 32.0
+#define kArrow 14.0
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    _contentView = [UIImageView new];
+    _contentView.frame = CGRectMake(kPadding, kPadding, kSize.width - 2 * kPadding, kHeight);
+    _contentView.layer.cornerRadius = kRadius;
+    _contentView.clipsToBounds = YES;
+    [self addSubview:_contentView];
+    
+    _coverView = [UIImageView new];
+    _coverView.frame = (CGRect){.origin = CGPointZero, .size = kSize};
+    _coverView.image = [self.class coverImage];
+    [self addSubview:_coverView];
+    
+    self.layer.anchorPoint = CGPointMake(0.5, 1.2);
+    return self;
+}
+
+- (instancetype)init {
+    self = [self initWithFrame:CGRectZero];
+    self.frame = (CGRect){.size = [self sizeThatFits:CGSizeZero]};
+    return self;
+}
+
+- (YYTextMagnifierType)type {
+    return YYTextMagnifierTypeRanged;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    return kSize;
+}
+
+- (void)setSnapshot:(UIImage *)snapshot {
+    if (self.captureFadeAnimation) {
+        [_contentView.layer removeAnimationForKey:@"contents"];
+        CABasicAnimation *animation = [CABasicAnimation animation];
+        animation.duration = kCaptureDisableFadeTime;
+        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+        [_contentView.layer addAnimation:animation forKey:@"contents"];
+    }
+    _contentView.image = snapshot;
+}
+
+- (UIImage *)snapshot {
+    return _contentView.image;
+}
+
+- (CGSize)snapshotSize {
+    CGSize size;
+    size.width = floor((kSize.width - 2 * kPadding) / kMultiple);
+    size.height = floor(kHeight / kMultiple);
+    return size;
+}
+
+- (CGSize)fitSize {
+    return [self sizeThatFits:CGSizeZero];
+}
+
++ (UIImage *)coverImage {
+    static UIImage *image;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        CGSize size = kSize;
+        CGRect rect = (CGRect) {.size = size, .origin = CGPointZero};
+        
+        UIGraphicsBeginImageContextWithOptions(size, NO, 0);
+        CGContextRef context = UIGraphicsGetCurrentContext();
+        
+        CGPathRef boxPath = CGPathCreateWithRect(rect, NULL);
+        
+        CGMutablePathRef path = CGPathCreateMutable();
+        CGPathMoveToPoint(path, NULL, kPadding + kRadius, kPadding);
+        CGPathAddLineToPoint(path, NULL, size.width - kPadding - kRadius, kPadding);
+        CGPathAddQuadCurveToPoint(path, NULL, size.width - kPadding, kPadding, size.width - kPadding, kPadding + kRadius);
+        CGPathAddLineToPoint(path, NULL, size.width - kPadding, kHeight);
+        CGPathAddCurveToPoint(path, NULL, size.width - kPadding, kPadding + kHeight, size.width - kPadding - kRadius, kPadding + kHeight, size.width - kPadding - kRadius, kPadding + kHeight);
+        CGPathAddLineToPoint(path, NULL, size.width / 2 + kArrow, kPadding + kHeight);
+        CGPathAddLineToPoint(path, NULL, size.width / 2, kPadding + kHeight + kArrow);
+        CGPathAddLineToPoint(path, NULL, size.width / 2 - kArrow, kPadding + kHeight);
+        CGPathAddLineToPoint(path, NULL, kPadding + kRadius, kPadding + kHeight);
+        CGPathAddQuadCurveToPoint(path, NULL, kPadding, kPadding + kHeight, kPadding, kHeight);
+        CGPathAddLineToPoint(path, NULL, kPadding, kPadding + kRadius);
+        CGPathAddQuadCurveToPoint(path, NULL, kPadding, kPadding, kPadding + kRadius, kPadding);
+        CGPathCloseSubpath(path);
+        
+        CGMutablePathRef arrowPath = CGPathCreateMutable();
+        CGPathMoveToPoint(arrowPath, NULL, size.width / 2 - kArrow, YYTextCGFloatPixelFloor(kPadding) + kHeight);
+        CGPathAddLineToPoint(arrowPath, NULL, size.width / 2 + kArrow, YYTextCGFloatPixelFloor(kPadding) + kHeight);
+        CGPathAddLineToPoint(arrowPath, NULL, size.width / 2, kPadding + kHeight + kArrow);
+        CGPathCloseSubpath(arrowPath);
+        
+        // inner shadow
+        CGContextSaveGState(context); {
+            CGFloat blurRadius = 25;
+            CGSize offset = CGSizeMake(0, 15);
+            CGColorRef shadowColor = [UIColor colorWithWhite:0 alpha:0.16].CGColor;
+            CGColorRef opaqueShadowColor = CGColorCreateCopyWithAlpha(shadowColor, 1.0);
+            CGContextAddPath(context, path);
+            CGContextClip(context);
+            CGContextSetAlpha(context, CGColorGetAlpha(shadowColor));
+            CGContextBeginTransparencyLayer(context, NULL); {
+                CGContextSetShadowWithColor(context, offset, blurRadius, opaqueShadowColor);
+                CGContextSetBlendMode(context, kCGBlendModeSourceOut);
+                CGContextSetFillColorWithColor(context, opaqueShadowColor);
+                CGContextAddPath(context, path);
+                CGContextFillPath(context);
+            } CGContextEndTransparencyLayer(context);
+            CGColorRelease(opaqueShadowColor);
+        } CGContextRestoreGState(context);
+        
+        // outer shadow
+        CGContextSaveGState(context); {
+            CGContextAddPath(context, boxPath);
+            CGContextAddPath(context, path);
+            CGContextEOClip(context);
+            CGColorRef shadowColor = [UIColor colorWithWhite:0 alpha:0.32].CGColor;
+            CGContextSetShadowWithColor(context, CGSizeMake(0, 1.5), 3, shadowColor);
+            CGContextBeginTransparencyLayer(context, NULL); {
+                CGContextAddPath(context, path);
+                [[UIColor colorWithWhite:0.7 alpha:1.000] setFill];
+                CGContextFillPath(context);
+            } CGContextEndTransparencyLayer(context);
+        } CGContextRestoreGState(context);
+        
+        // arrow
+        CGContextSaveGState(context); {
+            CGContextAddPath(context, arrowPath);
+            [[UIColor colorWithWhite:1 alpha:0.95] set];
+            CGContextFillPath(context);
+        } CGContextRestoreGState(context);
+        
+        // stroke
+        CGContextSaveGState(context); {
+            CGContextAddPath(context, path);
+            [[UIColor colorWithWhite:0.6 alpha:1] setStroke];
+            CGContextSetLineWidth(context, YYTextCGFloatFromPixel(1));
+            CGContextStrokePath(context);
+        } CGContextRestoreGState(context);
+        
+        CFRelease(boxPath);
+        CFRelease(path);
+        CFRelease(arrowPath);
+        
+        image = UIGraphicsGetImageFromCurrentImageContext();
+        UIGraphicsEndImageContext();
+        
+    });
+    return image;
+}
+
+#undef kMultiple
+#undef kSize
+#undef kPadding
+#undef kRadius
+#undef kHeight
+#undef kArrow
+
+@end
+
+
+@implementation YYTextMagnifier
+
++ (id)magnifierWithType:(YYTextMagnifierType)type {
+    switch (type) {
+        case YYTextMagnifierTypeCaret :return [_YYTextMagnifierCaret new];
+        case YYTextMagnifierTypeRanged :return [_YYTextMagnifierRanged new];
+    }
+    return nil;
+}
+
+- (id)initWithFrame:(CGRect)frame {
+    // class cluster
+    if ([self isMemberOfClass:[YYTextMagnifier class]]) {
+        @throw [NSException exceptionWithName:NSStringFromClass([self class]) reason:@"Attempting to instantiate an abstract class. Use a concrete subclass instead." userInfo:nil];
+        return nil;
+    }
+    self = [super initWithFrame:frame];
+    return self;
+}
+
+@end

+ 91 - 0
Demo/Objective_C_Demo/YYText/YYTextParser.h

@@ -0,0 +1,91 @@
+//
+//  YYTextParser.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/3/6.
+//  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
+
+/**
+ The YYTextParser protocol declares the required method for YYTextView and YYLabel
+ to modify the text during editing.
+ 
+ You can implement this protocol to add code highlighting or emoticon replacement for
+ YYTextView and YYLabel. See `YYTextSimpleMarkdownParser` and `YYTextSimpleEmoticonParser` for example.
+ */
+@protocol YYTextParser <NSObject>
+@required
+/**
+ When text is changed in YYTextView or YYLabel, 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 YYLabel), this value is NULL.
+ 
+ @return If the 'text' is modified in this method, returns `YES`, otherwise returns `NO`.
+ */
+- (BOOL)parseText:(nullable NSMutableAttributedString *)text selectedRange:(nullable NSRangePointer)selectedRange;
+@end
+
+
+
+/**
+ 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.
+ */
+@interface YYTextSimpleMarkdownParser : NSObject <YYTextParser>
+@property (nonatomic) CGFloat fontSize;         ///< default is 14
+@property (nonatomic) CGFloat headerFontSize;   ///< default is 20
+
+@property (nullable, nonatomic, strong) UIColor *textColor;
+@property (nullable, nonatomic, strong) UIColor *controlTextColor;
+@property (nullable, nonatomic, strong) UIColor *headerTextColor;
+@property (nullable, nonatomic, strong) UIColor *inlineTextColor;
+@property (nullable, nonatomic, strong) UIColor *codeTextColor;
+@property (nullable, nonatomic, strong) UIColor *linkTextColor;
+
+- (void)setColorWithBrightTheme; ///< reset the color properties to pre-defined value.
+- (void)setColorWithDarkTheme;   ///< reset the color properties to pre-defined value.
+@end
+
+
+
+/**
+ 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".
+ */
+@interface YYTextSimpleEmoticonParser : NSObject <YYTextParser>
+
+/**
+ 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.
+ */
+@property (nullable, copy) NSDictionary<NSString *, __kindof UIImage *> *emoticonMapper;
+@end
+
+NS_ASSUME_NONNULL_END

+ 417 - 0
Demo/Objective_C_Demo/YYText/YYTextParser.m

@@ -0,0 +1,417 @@
+//
+//  YYTextParser.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/3/6.
+//  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 "YYTextParser.h"
+#import "YYTextUtilities.h"
+#import "YYTextAttribute.h"
+#import "NSAttributedString+YYText.h"
+#import "NSParagraphStyle+YYText.h"
+
+
+#pragma mark - Markdown Parser
+
+@implementation YYTextSimpleMarkdownParser {
+    UIFont *_font;
+    NSMutableArray *_headerFonts; ///< h1~h6
+    UIFont *_boldFont;
+    UIFont *_italicFont;
+    UIFont *_boldItalicFont;
+    UIFont *_monospaceFont;
+    YYTextBorder *_border;
+    
+    NSRegularExpression *_regexEscape;          ///< escape
+    NSRegularExpression *_regexHeader;          ///< #header
+    NSRegularExpression *_regexH1;              ///< header\n====
+    NSRegularExpression *_regexH2;              ///< header\n----
+    NSRegularExpression *_regexBreakline;       ///< ******
+    NSRegularExpression *_regexEmphasis;        ///< *text*  _text_
+    NSRegularExpression *_regexStrong;          ///< **text**
+    NSRegularExpression *_regexStrongEmphasis;  ///< ***text*** ___text___
+    NSRegularExpression *_regexUnderline;       ///< __text__
+    NSRegularExpression *_regexStrikethrough;   ///< ~~text~~
+    NSRegularExpression *_regexInlineCode;      ///< `text`
+    NSRegularExpression *_regexLink;            ///< [name](link)
+    NSRegularExpression *_regexLinkRefer;       ///< [ref]:
+    NSRegularExpression *_regexList;            ///< 1.text 2.text 3.text
+    NSRegularExpression *_regexBlockQuote;      ///< > quote
+    NSRegularExpression *_regexCodeBlock;       ///< \tcode \tcode
+    NSRegularExpression *_regexNotEmptyLine;
+}
+
+- (void)initRegex {
+#define regexp(reg, option) [NSRegularExpression regularExpressionWithPattern : @reg options : option error : NULL]
+    _regexEscape = regexp("(\\\\\\\\|\\\\\\`|\\\\\\*|\\\\\\_|\\\\\\(|\\\\\\)|\\\\\\[|\\\\\\]|\\\\#|\\\\\\+|\\\\\\-|\\\\\\!)", 0);
+    _regexHeader = regexp("^((\\#{1,6}[^#].*)|(\\#{6}.+))$", NSRegularExpressionAnchorsMatchLines);
+    _regexH1 = regexp("^[^=\\n][^\\n]*\\n=+$", NSRegularExpressionAnchorsMatchLines);
+    _regexH2 = regexp("^[^-\\n][^\\n]*\\n-+$", NSRegularExpressionAnchorsMatchLines);
+    _regexBreakline = regexp("^[ \\t]*([*-])[ \\t]*((\\1)[ \\t]*){2,}[ \\t]*$", NSRegularExpressionAnchorsMatchLines);
+    _regexEmphasis = regexp("((?<!\\*)\\*(?=[^ \\t*])(.+?)(?<=[^ \\t*])\\*(?!\\*)|(?<!_)_(?=[^ \\t_])(.+?)(?<=[^ \\t_])_(?!_))", 0);
+    _regexStrong = regexp("(?<!\\*)\\*{2}(?=[^ \\t*])(.+?)(?<=[^ \\t*])\\*{2}(?!\\*)", 0);
+    _regexStrongEmphasis =  regexp("((?<!\\*)\\*{3}(?=[^ \\t*])(.+?)(?<=[^ \\t*])\\*{3}(?!\\*)|(?<!_)_{3}(?=[^ \\t_])(.+?)(?<=[^ \\t_])_{3}(?!_))", 0);
+    _regexUnderline = regexp("(?<!_)__(?=[^ \\t_])(.+?)(?<=[^ \\t_])\\__(?!_)", 0);
+    _regexStrikethrough = regexp("(?<!~)~~(?=[^ \\t~])(.+?)(?<=[^ \\t~])\\~~(?!~)", 0);
+    _regexInlineCode = regexp("(?<!`)(`{1,3})([^`\n]+?)\\1(?!`)", 0);
+    _regexLink = regexp("!?\\[([^\\[\\]]+)\\](\\(([^\\(\\)]+)\\)|\\[([^\\[\\]]+)\\])", 0);
+    _regexLinkRefer = regexp("^[ \\t]*\\[[^\\[\\]]\\]:", NSRegularExpressionAnchorsMatchLines);
+    _regexList = regexp("^[ \\t]*([*+-]|\\d+[.])[ \\t]+", NSRegularExpressionAnchorsMatchLines);
+    _regexBlockQuote = regexp("^[ \\t]*>[ \\t>]*", NSRegularExpressionAnchorsMatchLines);
+    _regexCodeBlock = regexp("(^\\s*$\\n)((( {4}|\\t).*(\\n|\\z))|(^\\s*$\\n))+", NSRegularExpressionAnchorsMatchLines);
+    _regexNotEmptyLine = regexp("^[ \\t]*[^ \\t]+[ \\t]*$", NSRegularExpressionAnchorsMatchLines);
+#undef regexp
+}
+
+- (instancetype)init {
+    self = [super init];
+    _fontSize = 14;
+    _headerFontSize = 20;
+    [self _updateFonts];
+    [self setColorWithBrightTheme];
+    [self initRegex];
+    return self;
+}
+
+- (void)setFontSize:(CGFloat)fontSize {
+    if (fontSize < 1) fontSize = 12;
+    _fontSize = fontSize;
+    [self _updateFonts];
+}
+
+- (void)setHeaderFontSize:(CGFloat)headerFontSize {
+    if (headerFontSize < 1) headerFontSize = 20;
+    _headerFontSize = headerFontSize;
+    [self _updateFonts];
+}
+
+- (void)_updateFonts {
+    _font = [UIFont systemFontOfSize:_fontSize];
+    _headerFonts = [NSMutableArray new];
+    for (int i = 0; i < 6; i++) {
+        CGFloat size = _headerFontSize - (_headerFontSize - _fontSize) / 5.0 * i;
+        [_headerFonts addObject:[UIFont systemFontOfSize:size]];
+    }
+    _boldFont = YYTextFontWithBold(_font);
+    _italicFont = YYTextFontWithItalic(_font);
+    _boldItalicFont = YYTextFontWithBoldItalic(_font);
+    _monospaceFont = [UIFont fontWithName:@"Menlo" size:_fontSize]; // Since iOS 7
+    if (!_monospaceFont) _monospaceFont = [UIFont fontWithName:@"Courier" size:_fontSize]; // Since iOS 3
+}
+
+- (void)setColorWithBrightTheme {
+    _textColor = [UIColor blackColor];
+    _controlTextColor = [UIColor colorWithWhite:0.749 alpha:1.000];
+    _headerTextColor = [UIColor colorWithRed:1.000 green:0.502 blue:0.000 alpha:1.000];
+    _inlineTextColor = [UIColor colorWithWhite:0.150 alpha:1.000];
+    _codeTextColor = [UIColor colorWithWhite:0.150 alpha:1.000];
+    _linkTextColor = [UIColor colorWithRed:0.000 green:0.478 blue:0.962 alpha:1.000];
+    
+    _border = [YYTextBorder new];
+    _border.lineStyle = YYTextLineStyleSingle;
+    _border.fillColor = [UIColor colorWithWhite:0.184 alpha:0.090];
+    _border.strokeColor = [UIColor colorWithWhite:0.546 alpha:0.650];
+    _border.insets = UIEdgeInsetsMake(-1, 0, -1, 0);
+    _border.cornerRadius = 2;
+    _border.strokeWidth = YYTextCGFloatFromPixel(1);
+}
+
+- (void)setColorWithDarkTheme {
+    _textColor = [UIColor whiteColor];
+    _controlTextColor = [UIColor colorWithWhite:0.604 alpha:1.000];
+    _headerTextColor = [UIColor colorWithRed:0.558 green:1.000 blue:0.502 alpha:1.000];
+    _inlineTextColor = [UIColor colorWithRed:1.000 green:0.862 blue:0.387 alpha:1.000];
+    _codeTextColor = [UIColor colorWithWhite:0.906 alpha:1.000];
+    _linkTextColor = [UIColor colorWithRed:0.000 green:0.646 blue:1.000 alpha:1.000];
+    
+    _border = [YYTextBorder new];
+    _border.lineStyle = YYTextLineStyleSingle;
+    _border.fillColor = [UIColor colorWithWhite:0.820 alpha:0.130];
+    _border.strokeColor = [UIColor colorWithWhite:1.000 alpha:0.280];
+    _border.insets = UIEdgeInsetsMake(-1, 0, -1, 0);
+    _border.cornerRadius = 2;
+    _border.strokeWidth = YYTextCGFloatFromPixel(1);
+}
+
+- (NSUInteger)lenghOfBeginWhiteInString:(NSString *)str withRange:(NSRange)range{
+    for (NSUInteger i = 0; i < range.length; i++) {
+        unichar c = [str characterAtIndex:i + range.location];
+        if (c != ' ' && c != '\t' && c != '\n') return i;
+    }
+    return str.length;
+}
+
+- (NSUInteger)lenghOfEndWhiteInString:(NSString *)str withRange:(NSRange)range{
+    for (NSInteger i = range.length - 1; i >= 0; i--) {
+        unichar c = [str characterAtIndex:i + range.location];
+        if (c != ' ' && c != '\t' && c != '\n') return range.length - i;
+    }
+    return str.length;
+}
+
+- (NSUInteger)lenghOfBeginChar:(unichar)c inString:(NSString *)str withRange:(NSRange)range{
+    for (NSUInteger i = 0; i < range.length; i++) {
+        if ([str characterAtIndex:i + range.location] != c) return i;
+    }
+    return str.length;
+}
+
+- (BOOL)parseText:(NSMutableAttributedString *)text selectedRange:(NSRangePointer)range {
+    if (text.length == 0) return NO;
+    [text yy_removeAttributesInRange:NSMakeRange(0, text.length)];
+    text.yy_font = _font;
+    text.yy_color = _textColor;
+    
+    NSMutableString *str = text.string.mutableCopy;
+    [_regexEscape replaceMatchesInString:str options:kNilOptions range:NSMakeRange(0, str.length) withTemplate:@"@@"];
+    
+    [_regexHeader enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        NSUInteger whiteLen = [self lenghOfBeginWhiteInString:str withRange:r];
+        NSUInteger sharpLen = [self lenghOfBeginChar:'#' inString:str withRange:NSMakeRange(r.location + whiteLen, r.length - whiteLen)];
+        if (sharpLen > 6) sharpLen = 6;
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location, whiteLen + sharpLen)];
+        [text yy_setColor:_headerTextColor range:NSMakeRange(r.location + whiteLen + sharpLen, r.length - whiteLen - sharpLen)];
+        [text yy_setFont:_headerFonts[sharpLen - 1] range:result.range];
+    }];
+    
+    [_regexH1 enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        NSRange linebreak = [str rangeOfString:@"\n" options:0 range:result.range locale:nil];
+        if (linebreak.location != NSNotFound) {
+            [text yy_setColor:_headerTextColor range:NSMakeRange(r.location, linebreak.location - r.location)];
+            [text yy_setFont:_headerFonts[0] range:NSMakeRange(r.location, linebreak.location - r.location + 1)];
+            [text yy_setColor:_controlTextColor range:NSMakeRange(linebreak.location + linebreak.length, r.location + r.length - linebreak.location - linebreak.length)];
+        }
+    }];
+    
+    [_regexH2 enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        NSRange linebreak = [str rangeOfString:@"\n" options:0 range:result.range locale:nil];
+        if (linebreak.location != NSNotFound) {
+            [text yy_setColor:_headerTextColor range:NSMakeRange(r.location, linebreak.location - r.location)];
+            [text yy_setFont:_headerFonts[1] range:NSMakeRange(r.location, linebreak.location - r.location + 1)];
+            [text yy_setColor:_controlTextColor range:NSMakeRange(linebreak.location + linebreak.length, r.location + r.length - linebreak.location - linebreak.length)];
+        }
+    }];
+    
+    [_regexBreakline enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        [text yy_setColor:_controlTextColor range:result.range];
+    }];
+    
+    [_regexEmphasis enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location, 1)];
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location + r.length - 1, 1)];
+        [text yy_setFont:_italicFont range:NSMakeRange(r.location + 1, r.length - 2)];
+    }];
+    
+    [_regexStrong enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location, 2)];
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location + r.length - 2, 2)];
+        [text yy_setFont:_boldFont range:NSMakeRange(r.location + 2, r.length - 4)];
+    }];
+    
+    [_regexStrongEmphasis enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location, 3)];
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location + r.length - 3, 3)];
+        [text yy_setFont:_boldItalicFont range:NSMakeRange(r.location + 3, r.length - 6)];
+    }];
+    
+    [_regexUnderline enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location, 2)];
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location + r.length - 2, 2)];
+        [text yy_setTextUnderline:[YYTextDecoration decorationWithStyle:YYTextLineStyleSingle width:@1 color:nil] range:NSMakeRange(r.location + 2, r.length - 4)];
+    }];
+    
+    [_regexStrikethrough enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location, 2)];
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location + r.length - 2, 2)];
+        [text yy_setTextStrikethrough:[YYTextDecoration decorationWithStyle:YYTextLineStyleSingle width:@1 color:nil] range:NSMakeRange(r.location + 2, r.length - 4)];
+    }];
+    
+    [_regexInlineCode enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        NSUInteger len = [self lenghOfBeginChar:'`' inString:str withRange:r];
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location, len)];
+        [text yy_setColor:_controlTextColor range:NSMakeRange(r.location + r.length - len, len)];
+        [text yy_setColor:_inlineTextColor range:NSMakeRange(r.location + len, r.length - 2 * len)];
+        [text yy_setFont:_monospaceFont range:r];
+        [text yy_setTextBorder:_border.copy range:r];
+    }];
+    
+    [_regexLink enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_linkTextColor range:r];
+    }];
+    
+    [_regexLinkRefer enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_controlTextColor range:r];
+    }];
+    
+    [_regexList enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_controlTextColor range:r];
+    }];
+    
+    [_regexBlockQuote enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        [text yy_setColor:_controlTextColor range:r];
+    }];
+    
+    [_regexCodeBlock enumerateMatchesInString:str options:0 range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        NSRange r = result.range;
+        NSRange firstLineRange = [_regexNotEmptyLine rangeOfFirstMatchInString:str options:kNilOptions range:r];
+        NSUInteger lenStart = (firstLineRange.location != NSNotFound) ? firstLineRange.location - r.location : 0;
+        NSUInteger lenEnd = [self lenghOfEndWhiteInString:str withRange:r];
+        if (lenStart + lenEnd < r.length) {
+            NSRange codeR = NSMakeRange(r.location + lenStart, r.length - lenStart - lenEnd);
+            [text yy_setColor:_codeTextColor range:codeR];
+            [text yy_setFont:_monospaceFont range:codeR];
+            YYTextBorder *border = [YYTextBorder new];
+            border.lineStyle = YYTextLineStyleSingle;
+            border.fillColor = [UIColor colorWithWhite:0.184 alpha:0.090];
+            border.strokeColor = [UIColor colorWithWhite:0.200 alpha:0.300];
+            border.insets = UIEdgeInsetsMake(-1, 0, -1, 0);
+            border.cornerRadius = 3;
+            border.strokeWidth = YYTextCGFloatFromPixel(2);
+            [text yy_setTextBlockBorder:_border.copy range:codeR];
+        }
+    }];
+    
+    return YES;
+}
+
+
+@end
+
+
+
+#pragma mark - Emoticon Parser
+
+#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
+__VA_ARGS__; \
+dispatch_semaphore_signal(_lock);
+
+@implementation YYTextSimpleEmoticonParser {
+    NSRegularExpression *_regex;
+    NSDictionary *_mapper;
+    dispatch_semaphore_t _lock;
+}
+
+- (instancetype)init {
+    self = [super init];
+    _lock = dispatch_semaphore_create(1);
+    return self;
+}
+
+- (NSDictionary *)emoticonMapper {
+    LOCK(NSDictionary *mapper = _mapper); return mapper;
+}
+
+- (void)setEmoticonMapper:(NSDictionary *)emoticonMapper {
+    LOCK(
+         _mapper = emoticonMapper.copy;
+         if (_mapper.count == 0) {
+             _regex = nil;
+         } else {
+             NSMutableString *pattern = @"(".mutableCopy;
+             NSArray *allKeys = _mapper.allKeys;
+             NSCharacterSet *charset = [NSCharacterSet characterSetWithCharactersInString:@"$^?+*.,#|{}[]()\\"];
+             for (NSUInteger i = 0, max = allKeys.count; i < max; i++) {
+                 NSMutableString *one = [allKeys[i] mutableCopy];
+                 
+                 // escape regex characters
+                 for (NSUInteger ci = 0, cmax = one.length; ci < cmax; ci++) {
+                     unichar c = [one characterAtIndex:ci];
+                     if ([charset characterIsMember:c]) {
+                         [one insertString:@"\\" atIndex:ci];
+                         ci++;
+                         cmax++;
+                     }
+                 }
+                 
+                 [pattern appendString:one];
+                 if (i != max - 1) [pattern appendString:@"|"];
+             }
+             [pattern appendString:@")"];
+             _regex = [[NSRegularExpression alloc] initWithPattern:pattern options:kNilOptions error:nil];
+         }
+    );
+}
+
+// correct the selected range during text replacement
+- (NSRange)_replaceTextInRange:(NSRange)range withLength:(NSUInteger)length selectedRange:(NSRange)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;
+}
+
+- (BOOL)parseText:(NSMutableAttributedString *)text selectedRange:(NSRangePointer)range {
+    if (text.length == 0) return NO;
+    
+    NSDictionary *mapper;
+    NSRegularExpression *regex;
+    LOCK(mapper = _mapper; regex = _regex;);
+    if (mapper.count == 0 || regex == nil) return NO;
+    
+    NSArray *matches = [regex matchesInString:text.string options:kNilOptions range:NSMakeRange(0, text.length)];
+    if (matches.count == 0) return NO;
+    
+    NSRange selectedRange = range ? *range : NSMakeRange(0, 0);
+    NSUInteger cutLength = 0;
+    for (NSUInteger i = 0, max = matches.count; i < max; i++) {
+        NSTextCheckingResult *one = matches[i];
+        NSRange oneRange = one.range;
+        if (oneRange.length == 0) continue;
+        oneRange.location -= cutLength;
+        NSString *subStr = [text.string substringWithRange:oneRange];
+        UIImage *emoticon = mapper[subStr];
+        if (!emoticon) continue;
+        
+        CGFloat fontSize = 12; // CoreText default value
+        CTFontRef font = (__bridge CTFontRef)([text yy_attribute:NSFontAttributeName atIndex:oneRange.location]);
+        if (font) fontSize = CTFontGetSize(font);
+        NSMutableAttributedString *atr = [NSAttributedString yy_attachmentStringWithEmojiImage:emoticon fontSize:fontSize];
+        [atr yy_setTextBackedString:[YYTextBackedString stringWithString:subStr] range:NSMakeRange(0, atr.length)];
+        [text replaceCharactersInRange:oneRange withString:atr.string];
+        [text yy_removeDiscontinuousAttributesInRange:NSMakeRange(oneRange.location, atr.length)];
+        [text addAttributes:atr.yy_attributes range:NSMakeRange(oneRange.location, atr.length)];
+        selectedRange = [self _replaceTextInRange:oneRange withLength:atr.length selectedRange:selectedRange];
+        cutLength += oneRange.length - 1;
+    }
+    if (range) *range = selectedRange;
+    
+    return YES;
+}
+@end

+ 78 - 0
Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.h

@@ -0,0 +1,78 @@
+//
+//  YYTextRubyAnnotation.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/24.
+//  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>
+#import <CoreText/CoreText.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ Wrapper for CTRubyAnnotationRef.
+ 
+ Example:
+ 
+     YYTextRubyAnnotation *ruby = [YYTextRubyAnnotation new];
+     ruby.textBefore = @"zhù yīn";
+     CTRubyAnnotationRef ctRuby = ruby.CTRubyAnnotation;
+     if (ctRuby) {
+        /// add to attributed string
+        CFRelease(ctRuby);
+     }
+ 
+ */
+@interface YYTextRubyAnnotation : NSObject <NSCopying, NSCoding>
+
+/// Specifies how the ruby text and the base text should be aligned relative to each other.
+@property (nonatomic) CTRubyAlignment alignment;
+
+/// Specifies how the ruby text can overhang adjacent characters.
+@property (nonatomic) CTRubyOverhang overhang;
+
+/// Specifies the size of the annotation text as a percent of the size of the base text.
+@property (nonatomic) CGFloat sizeFactor;
+
+
+/// The ruby text is positioned before the base text;
+/// i.e. above horizontal text and to the right of vertical text.
+@property (nullable, nonatomic, copy) NSString *textBefore;
+
+/// The ruby text is positioned after the base text;
+/// i.e. below horizontal text and to the left of vertical text.
+@property (nullable, nonatomic, copy) NSString *textAfter;
+
+/// 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.
+@property (nullable, nonatomic, copy) NSString *textInterCharacter;
+
+/// The ruby text follows the base text with no special styling.
+@property (nullable, nonatomic, copy) NSString *textInline;
+
+
+/**
+ Create a ruby object from CTRuby object.
+ 
+ @param ctRuby  A CTRuby object.
+ 
+ @return A ruby object, or nil when an error occurs.
+ */
++ (instancetype)rubyWithCTRubyRef:(CTRubyAnnotationRef)ctRuby NS_AVAILABLE_IOS(8_0);
+
+/**
+ 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.
+ */
+- (nullable CTRubyAnnotationRef)CTRubyAnnotation CF_RETURNS_RETAINED NS_AVAILABLE_IOS(8_0);
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 83 - 0
Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.m

@@ -0,0 +1,83 @@
+//
+//  YYTextRubyAnnotation.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/24.
+//  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 "YYTextRubyAnnotation.h"
+
+@implementation YYTextRubyAnnotation
+
+- (instancetype)init {
+    self = super.init;
+    self.alignment = kCTRubyAlignmentAuto;
+    self.overhang = kCTRubyOverhangAuto;
+    self.sizeFactor = 0.5;
+    return self;
+}
+
++ (instancetype)rubyWithCTRubyRef:(CTRubyAnnotationRef)ctRuby {
+    if (!ctRuby) return nil;
+    YYTextRubyAnnotation *one = [self new];
+    one.alignment = CTRubyAnnotationGetAlignment(ctRuby);
+    one.overhang = CTRubyAnnotationGetOverhang(ctRuby);
+    one.sizeFactor = CTRubyAnnotationGetSizeFactor(ctRuby);
+    one.textBefore = (__bridge NSString *)(CTRubyAnnotationGetTextForPosition(ctRuby, kCTRubyPositionBefore));
+    one.textAfter = (__bridge NSString *)(CTRubyAnnotationGetTextForPosition(ctRuby, kCTRubyPositionAfter));
+    one.textInterCharacter = (__bridge NSString *)(CTRubyAnnotationGetTextForPosition(ctRuby, kCTRubyPositionInterCharacter));
+    one.textInline = (__bridge NSString *)(CTRubyAnnotationGetTextForPosition(ctRuby, kCTRubyPositionInline));
+    return one;
+}
+
+- (CTRubyAnnotationRef)CTRubyAnnotation CF_RETURNS_RETAINED {
+    if (((long)CTRubyAnnotationCreate + 1) == 1) return NULL; // system not support
+    
+    CFStringRef text[kCTRubyPositionCount];
+    text[kCTRubyPositionBefore] = (__bridge CFStringRef)(_textBefore);
+    text[kCTRubyPositionAfter] = (__bridge CFStringRef)(_textAfter);
+    text[kCTRubyPositionInterCharacter] = (__bridge CFStringRef)(_textInterCharacter);
+    text[kCTRubyPositionInline] = (__bridge CFStringRef)(_textInline);
+    CTRubyAnnotationRef ruby = CTRubyAnnotationCreate(_alignment, _overhang, _sizeFactor, text);
+    return ruby;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    YYTextRubyAnnotation *one = [self.class new];
+    one.alignment = _alignment;
+    one.overhang = _overhang;
+    one.sizeFactor = _sizeFactor;
+    one.textBefore = _textBefore;
+    one.textAfter = _textAfter;
+    one.textInterCharacter = _textInterCharacter;
+    one.textInline = _textInline;
+    return one;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:@(_alignment) forKey:@"alignment"];
+    [aCoder encodeObject:@(_overhang) forKey:@"overhang"];
+    [aCoder encodeObject:@(_sizeFactor) forKey:@"sizeFactor"];
+    [aCoder encodeObject:_textBefore forKey:@"textBefore"];
+    [aCoder encodeObject:_textAfter forKey:@"textAfter"];
+    [aCoder encodeObject:_textInterCharacter forKey:@"textInterCharacter"];
+    [aCoder encodeObject:_textInline forKey:@"textInline"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [self init];
+    _alignment = ((NSNumber *)[aDecoder decodeObjectForKey:@"alignment"]).intValue;
+    _overhang = ((NSNumber *)[aDecoder decodeObjectForKey:@"overhang"]).intValue;
+    _sizeFactor = ((NSNumber *)[aDecoder decodeObjectForKey:@"sizeFactor"]).intValue;
+    _textBefore = [aDecoder decodeObjectForKey:@"textBefore"];
+    _textAfter = [aDecoder decodeObjectForKey:@"textAfter"];
+    _textInterCharacter = [aDecoder decodeObjectForKey:@"textInterCharacter"];
+    _textInline = [aDecoder decodeObjectForKey:@"textInline"];
+    return self;
+}
+
+@end

+ 68 - 0
Demo/Objective_C_Demo/YYText/YYTextRunDelegate.h

@@ -0,0 +1,68 @@
+//
+//  YYTextRunDelegate.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/14.
+//  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>
+#import <CoreText/CoreText.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ Wrapper for CTRunDelegateRef.
+ 
+ Example:
+ 
+     YYTextRunDelegate *delegate = [YYTextRunDelegate new];
+     delegate.ascent = 20;
+     delegate.descent = 4;
+     delegate.width = 20;
+     CTRunDelegateRef ctRunDelegate = delegate.CTRunDelegate;
+     if (ctRunDelegate) {
+         /// add to attributed string
+         CFRelease(ctRunDelegate);
+     }
+ 
+ */
+@interface YYTextRunDelegate : NSObject <NSCopying, NSCoding>
+
+/**
+ Creates and returns the CTRunDelegate.
+ 
+ @discussion You need call CFRelease() after used.
+ The CTRunDelegateRef has a strong reference to this YYTextRunDelegate object.
+ In CoreText, use CTRunDelegateGetRefCon() to get this YYTextRunDelegate object.
+ 
+ @return The CTRunDelegate object.
+ */
+- (nullable CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED;
+
+/**
+ Additional information about the the run delegate.
+ */
+@property (nullable, nonatomic, strong) NSDictionary *userInfo;
+
+/**
+ The typographic ascent of glyphs in the run.
+ */
+@property (nonatomic) CGFloat ascent;
+
+/**
+ The typographic descent of glyphs in the run.
+ */
+@property (nonatomic) CGFloat descent;
+
+/**
+ The typographic width of glyphs in the run.
+ */
+@property (nonatomic) CGFloat width;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 71 - 0
Demo/Objective_C_Demo/YYText/YYTextRunDelegate.m

@@ -0,0 +1,71 @@
+//
+//  YYTextRunDelegate.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/14.
+//  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 "YYTextRunDelegate.h"
+
+static void DeallocCallback(void *ref) {
+    YYTextRunDelegate *self = (__bridge_transfer YYTextRunDelegate *)(ref);
+    self = nil; // release
+}
+
+static CGFloat GetAscentCallback(void *ref) {
+    YYTextRunDelegate *self = (__bridge YYTextRunDelegate *)(ref);
+    return self.ascent;
+}
+
+static CGFloat GetDecentCallback(void *ref) {
+    YYTextRunDelegate *self = (__bridge YYTextRunDelegate *)(ref);
+    return self.descent;
+}
+
+static CGFloat GetWidthCallback(void *ref) {
+    YYTextRunDelegate *self = (__bridge YYTextRunDelegate *)(ref);
+    return self.width;
+}
+
+@implementation YYTextRunDelegate
+
+- (CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED {
+    CTRunDelegateCallbacks callbacks;
+    callbacks.version = kCTRunDelegateCurrentVersion;
+    callbacks.dealloc = DeallocCallback;
+    callbacks.getAscent = GetAscentCallback;
+    callbacks.getDescent = GetDecentCallback;
+    callbacks.getWidth = GetWidthCallback;
+    return CTRunDelegateCreate(&callbacks, (__bridge_retained void *)(self.copy));
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:@(_ascent) forKey:@"ascent"];
+    [aCoder encodeObject:@(_descent) forKey:@"descent"];
+    [aCoder encodeObject:@(_width) forKey:@"width"];
+    [aCoder encodeObject:_userInfo forKey:@"userInfo"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    _ascent = ((NSNumber *)[aDecoder decodeObjectForKey:@"ascent"]).floatValue;
+    _descent = ((NSNumber *)[aDecoder decodeObjectForKey:@"descent"]).floatValue;
+    _width = ((NSNumber *)[aDecoder decodeObjectForKey:@"width"]).floatValue;
+    _userInfo = [aDecoder decodeObjectForKey:@"userInfo"];
+    return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+    typeof(self) one = [self.class new];
+    one.ascent = self.ascent;
+    one.descent = self.descent;
+    one.width = self.width;
+    one.userInfo = self.userInfo;
+    return one;
+}
+
+@end

+ 78 - 0
Demo/Objective_C_Demo/YYText/YYTextSelectionView.h

@@ -0,0 +1,78 @@
+//
+//  YYTextSelectionView.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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(<YYText/YYText.h>)
+#import <YYText/YYTextAttribute.h>
+#import <YYText/YYTextInput.h>
+#else
+#import "YYTextAttribute.h"
+#import "YYTextInput.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ A single dot view. The frame should be foursquare.
+ Change the background color for display.
+ 
+ @discussion Typically, you should not use this class directly.
+ */
+@interface YYSelectionGrabberDot : UIView
+/// Dont't access this property. It was used by `YYTextEffectWindow`.
+@property (nonatomic, strong) UIView *mirror;
+@end
+
+
+/**
+ A grabber (stick with a dot).
+ 
+ @discussion Typically, you should not use this class directly.
+ */
+@interface YYSelectionGrabber : UIView
+
+@property (nonatomic, readonly) YYSelectionGrabberDot *dot; ///< the dot view
+@property (nonatomic) YYTextDirection dotDirection;         ///< don't support composite direction
+@property (nullable, nonatomic, strong) UIColor *color;     ///< tint color, default is nil
+
+@end
+
+
+/**
+ The selection view for text edit and select.
+ 
+ @discussion Typically, you should not use this class directly.
+ */
+@interface YYTextSelectionView : UIView
+
+@property (nullable, nonatomic, weak) UIView *hostView; ///< the holder view
+@property (nullable, nonatomic, strong) UIColor *color; ///< the tint color
+@property (nonatomic, getter = isCaretBlinks) BOOL caretBlinks; ///< whether the caret is blinks
+@property (nonatomic, getter = isCaretVisible) BOOL caretVisible; ///< whether the caret is visible
+@property (nonatomic, getter = isVerticalForm) BOOL verticalForm; ///< weather the text view is vertical form
+
+@property (nonatomic) CGRect caretRect; ///< caret rect (width==0 or height==0)
+@property (nullable, nonatomic, copy) NSArray<YYTextSelectionRect *> *selectionRects; ///< default is nil
+
+@property (nonatomic, readonly) UIView *caretView;
+@property (nonatomic, readonly) YYSelectionGrabber *startGrabber;
+@property (nonatomic, readonly) YYSelectionGrabber *endGrabber;
+
+- (BOOL)isGrabberContainsPoint:(CGPoint)point;
+- (BOOL)isStartGrabberContainsPoint:(CGPoint)point;
+- (BOOL)isEndGrabberContainsPoint:(CGPoint)point;
+- (BOOL)isCaretContainsPoint:(CGPoint)point;
+- (BOOL)isSelectionRectsContainsPoint:(CGPoint)point;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 329 - 0
Demo/Objective_C_Demo/YYText/YYTextSelectionView.m

@@ -0,0 +1,329 @@
+//
+//  YYTextSelectionView.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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 "YYTextSelectionView.h"
+#import "YYTextUtilities.h"
+#import "YYTextWeakProxy.h"
+
+#define kMarkAlpha 0.2
+#define kLineWidth 2.0
+#define kBlinkDuration 0.5
+#define kBlinkFadeDuration 0.2
+#define kBlinkFirstDelay 0.1
+#define kTouchTestExtend 14.0
+#define kTouchDotExtend 7.0
+
+
+@implementation YYSelectionGrabberDot
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    self.userInteractionEnabled = NO;
+    self.mirror = [UIView new];
+    return self;
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+    CGFloat length = MIN(self.bounds.size.width, self.bounds.size.height);
+    self.layer.cornerRadius = length * 0.5;
+    self.mirror.bounds = self.bounds;
+    self.mirror.layer.cornerRadius = self.layer.cornerRadius;
+}
+
+- (void)setBackgroundColor:(UIColor *)backgroundColor {
+    [super setBackgroundColor:backgroundColor];
+    _mirror.backgroundColor = backgroundColor;    
+}
+
+@end
+
+
+
+@implementation YYSelectionGrabber
+
+- (instancetype) initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    _dot = [[YYSelectionGrabberDot alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
+    return self;
+}
+
+- (void)setDotDirection:(YYTextDirection)dotDirection {
+    _dotDirection = dotDirection;
+    [self addSubview:_dot];
+    CGRect frame = _dot.frame;
+    CGFloat ofs = 0.5;
+    if (dotDirection == YYTextDirectionTop) {
+        frame.origin.y = -frame.size.height + ofs;
+        frame.origin.x = (self.bounds.size.width - frame.size.width) / 2;
+    } else if (dotDirection == YYTextDirectionRight) {
+        frame.origin.x = self.bounds.size.width - ofs;
+        frame.origin.y = (self.bounds.size.height - frame.size.height) / 2;
+    } else if (dotDirection == YYTextDirectionBottom) {
+        frame.origin.y = self.bounds.size.height - ofs;
+        frame.origin.x = (self.bounds.size.width - frame.size.width) / 2;
+    } else if (dotDirection == YYTextDirectionLeft) {
+        frame.origin.x = -frame.size.width + ofs;
+        frame.origin.y = (self.bounds.size.height - frame.size.height) / 2;
+    } else {
+        [_dot removeFromSuperview];
+    }
+    _dot.frame = frame;
+}
+
+- (void)setColor:(UIColor *)color {
+    self.backgroundColor = color;
+    _dot.backgroundColor = color;
+    _color = color;
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+    [self setDotDirection:_dotDirection];
+}
+
+- (CGRect)touchRect {
+    CGRect rect = CGRectInset(self.frame, -kTouchTestExtend, -kTouchTestExtend);
+    UIEdgeInsets insets = {0};
+    if (_dotDirection == YYTextDirectionTop) {
+        insets.top = -kTouchDotExtend;
+    } else if (_dotDirection == YYTextDirectionRight) {
+        insets.right = -kTouchDotExtend;
+    } else if (_dotDirection == YYTextDirectionBottom) {
+        insets.bottom = -kTouchDotExtend;
+    } else if (_dotDirection == YYTextDirectionLeft) {
+        insets.left = -kTouchDotExtend;
+    }
+    rect = UIEdgeInsetsInsetRect(rect, insets);
+    return rect;
+}
+
+@end
+
+
+
+@interface YYTextSelectionView ()
+@property (nonatomic, strong) NSTimer *caretTimer;
+@property (nonatomic, strong) UIView *caretView;
+@property (nonatomic, strong) YYSelectionGrabber *startGrabber;
+@property (nonatomic, strong) YYSelectionGrabber *endGrabber;
+@property (nonatomic, strong) NSMutableArray *markViews;
+@end
+
+@implementation YYTextSelectionView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    
+    self.userInteractionEnabled = NO;
+    self.clipsToBounds = NO;
+    _markViews = [NSMutableArray array];
+    _caretView = [UIView new];
+    _caretView.hidden = YES;
+    _startGrabber = [YYSelectionGrabber new];
+    _startGrabber.dotDirection = YYTextDirectionTop;
+    _startGrabber.hidden = YES;
+    _endGrabber = [YYSelectionGrabber new];
+    _endGrabber.dotDirection = YYTextDirectionBottom;
+    _endGrabber.hidden = YES;
+    
+    [self addSubview:_startGrabber];
+    [self addSubview:_endGrabber];
+    [self addSubview:_caretView];
+    
+    return self;
+}
+
+- (void)dealloc {
+    [_caretTimer invalidate];
+}
+
+- (void)setColor:(UIColor *)color {
+    _color = color;
+    self.caretView.backgroundColor = color;
+    self.startGrabber.color = color;
+    self.endGrabber.color = color;
+    [self.markViews enumerateObjectsUsingBlock: ^(UIView *v, NSUInteger idx, BOOL *stop) {
+        v.backgroundColor = color;
+    }];
+}
+
+- (void)setCaretBlinks:(BOOL)caretBlinks {
+    if (_caretBlinks != caretBlinks) {
+        _caretView.alpha = 1;
+        [self.class cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startBlinks) object:nil];
+        if (caretBlinks) {
+            [self performSelector:@selector(_startBlinks) withObject:nil afterDelay:kBlinkFirstDelay];
+        } else {
+            [_caretTimer invalidate];
+            _caretTimer = nil;
+        }
+        _caretBlinks = caretBlinks;
+    }
+}
+
+- (void)_startBlinks {
+    [_caretTimer invalidate];
+    if (_caretVisible) {
+        _caretTimer = [NSTimer timerWithTimeInterval:kBlinkDuration target:[YYTextWeakProxy proxyWithTarget:self] selector:@selector(_doBlink) userInfo:nil repeats:YES];
+        [[NSRunLoop currentRunLoop] addTimer:_caretTimer forMode:NSDefaultRunLoopMode];
+    } else {
+        _caretView.alpha = 1;
+    }
+}
+
+- (void)_doBlink {
+    [UIView animateWithDuration:kBlinkFadeDuration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations: ^{
+        if (_caretView.alpha == 1) _caretView.alpha = 0;
+        else _caretView.alpha = 1;
+    } completion:NULL];
+}
+
+- (void)setCaretVisible:(BOOL)caretVisible {
+    _caretVisible = caretVisible;
+    self.caretView.hidden = !caretVisible;
+    _caretView.alpha = 1;
+    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startBlinks) object:nil];
+    if (_caretBlinks) {
+        [self performSelector:@selector(_startBlinks) withObject:nil afterDelay:kBlinkFirstDelay];
+    }
+}
+
+- (void)setVerticalForm:(BOOL)verticalForm {
+    if (_verticalForm != verticalForm) {
+        _verticalForm = verticalForm;
+        [self setCaretRect:_caretRect];
+        self.startGrabber.dotDirection = verticalForm ? YYTextDirectionRight : YYTextDirectionTop;
+        self.endGrabber.dotDirection = verticalForm ? YYTextDirectionLeft : YYTextDirectionBottom;
+    }
+}
+
+- (CGRect)_standardCaretRect:(CGRect)caretRect {
+    caretRect = CGRectStandardize(caretRect);
+    if (_verticalForm) {
+        if (caretRect.size.height == 0) {
+            caretRect.size.height = kLineWidth;
+            caretRect.origin.y -= kLineWidth * 0.5;
+        }
+        if (caretRect.origin.y < 0) {
+            caretRect.origin.y = 0;
+        } else if (caretRect.origin.y + caretRect.size.height > self.bounds.size.height) {
+            caretRect.origin.y = self.bounds.size.height - caretRect.size.height;
+        }
+    } else {
+        if (caretRect.size.width == 0) {
+            caretRect.size.width = kLineWidth;
+            caretRect.origin.x -= kLineWidth * 0.5;
+        }
+        if (caretRect.origin.x < 0) {
+            caretRect.origin.x = 0;
+        } else if (caretRect.origin.x + caretRect.size.width > self.bounds.size.width) {
+            caretRect.origin.x = self.bounds.size.width - caretRect.size.width;
+        }
+    }
+    caretRect = YYTextCGRectPixelRound(caretRect);
+    if (isnan(caretRect.origin.x) || isinf(caretRect.origin.x)) caretRect.origin.x = 0;
+    if (isnan(caretRect.origin.y) || isinf(caretRect.origin.y)) caretRect.origin.y = 0;
+    if (isnan(caretRect.size.width) || isinf(caretRect.size.width)) caretRect.size.width = 0;
+    if (isnan(caretRect.size.height) || isinf(caretRect.size.height)) caretRect.size.height = 0;
+    return caretRect;
+}
+
+- (void)setCaretRect:(CGRect)caretRect {
+    _caretRect = caretRect;
+    self.caretView.frame = [self _standardCaretRect:caretRect];
+    CGFloat minWidth = MIN(self.caretView.bounds.size.width, self.caretView.bounds.size.height);
+    self.caretView.layer.cornerRadius = minWidth / 2;
+}
+
+- (void)setSelectionRects:(NSArray *)selectionRects {
+    _selectionRects = selectionRects.copy;
+    [self.markViews enumerateObjectsUsingBlock: ^(UIView *v, NSUInteger idx, BOOL *stop) {
+        [v removeFromSuperview];
+    }];
+    [self.markViews removeAllObjects];
+    self.startGrabber.hidden = YES;
+    self.endGrabber.hidden = YES;
+    
+    [selectionRects enumerateObjectsUsingBlock: ^(YYTextSelectionRect *r, NSUInteger idx, BOOL *stop) {
+        CGRect rect = r.rect;
+        rect = CGRectStandardize(rect);
+        rect = YYTextCGRectPixelRound(rect);
+        if (r.containsStart || r.containsEnd) {
+            rect = [self _standardCaretRect:rect];
+            if (r.containsStart) {
+                self.startGrabber.hidden = NO;
+                self.startGrabber.frame = rect;
+            }
+            if (r.containsEnd) {
+                self.endGrabber.hidden = NO;
+                self.endGrabber.frame = rect;
+            }
+        } else {
+            if (rect.size.width > 0 && rect.size.height > 0) {
+                UIView *mark = [[UIView alloc] initWithFrame:rect];
+                mark.backgroundColor = _color;
+                mark.alpha = kMarkAlpha;
+                [self insertSubview:mark atIndex:0];
+                [self.markViews addObject:mark];
+            }
+        }
+    }];
+}
+
+- (BOOL)isGrabberContainsPoint:(CGPoint)point {
+    return [self isStartGrabberContainsPoint:point] || [self isEndGrabberContainsPoint:point];
+}
+
+- (BOOL)isStartGrabberContainsPoint:(CGPoint)point {
+    if (_startGrabber.hidden) return NO;
+    CGRect startRect = [_startGrabber touchRect];
+    CGRect endRect = [_endGrabber touchRect];
+    if (CGRectIntersectsRect(startRect, endRect)) {
+        CGFloat distStart = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(startRect));
+        CGFloat distEnd = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(endRect));
+        if (distEnd <= distStart) return NO;
+    }
+    return CGRectContainsPoint(startRect, point);
+}
+
+- (BOOL)isEndGrabberContainsPoint:(CGPoint)point {
+    if (_endGrabber.hidden) return NO;
+    CGRect startRect = [_startGrabber touchRect];
+    CGRect endRect = [_endGrabber touchRect];
+    if (CGRectIntersectsRect(startRect, endRect)) {
+        CGFloat distStart = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(startRect));
+        CGFloat distEnd = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(endRect));
+        if (distEnd > distStart) return NO;
+    }
+    return CGRectContainsPoint(endRect, point);
+}
+
+- (BOOL)isCaretContainsPoint:(CGPoint)point {
+    if (_caretVisible) {
+        CGRect rect = CGRectInset(_caretRect, -kTouchTestExtend, -kTouchTestExtend);
+        return CGRectContainsPoint(rect, point);
+    }
+    return NO;
+}
+
+- (BOOL)isSelectionRectsContainsPoint:(CGPoint)point {
+    if (_selectionRects.count == 0) return NO;
+    for (YYTextSelectionRect *rect in _selectionRects) {
+        if (CGRectContainsPoint(rect.rect, point)) return YES;
+    }
+    return NO;
+}
+
+@end

+ 42 - 0
Demo/Objective_C_Demo/YYText/YYTextTransaction.h

@@ -0,0 +1,42 @@
+//
+//  YYTextTransaction.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/18.
+//  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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ YYTextTransaction let you perform a selector once before current runloop sleep.
+ */
+@interface YYTextTransaction : NSObject
+
+/**
+ Creates and returns a transaction with a specified target and selector.
+ 
+ @param target    A specified target, the target is retained until runloop end.
+ @param selector  A selector for target.
+ 
+ @return A new transaction, or nil if an error occurs.
+ */
++ (YYTextTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;
+
+/**
+ Commit the trancaction to main runloop.
+ 
+ @discussion It will perform the selector on the target once before main runloop's
+ current loop sleep. If the same transaction (same target and same selector) has 
+ already commit to runloop in this loop, this method do nothing.
+ */
+- (void)commit;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 81 - 0
Demo/Objective_C_Demo/YYText/YYTextTransaction.m

@@ -0,0 +1,81 @@
+//
+//  YYTextTransaction.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/18.
+//  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 "YYTextTransaction.h"
+
+
+@interface YYTextTransaction()
+@property (nonatomic, strong) id target;
+@property (nonatomic, assign) SEL selector;
+@end
+
+static NSMutableSet *transactionSet = nil;
+
+static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
+    if (transactionSet.count == 0) return;
+    NSSet *currentSet = transactionSet;
+    transactionSet = [NSMutableSet new];
+    [currentSet enumerateObjectsUsingBlock:^(YYTextTransaction *transaction, BOOL *stop) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+        [transaction.target performSelector:transaction.selector];
+#pragma clang diagnostic pop
+    }];
+}
+
+static void YYTextTransactionSetup() {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        transactionSet = [NSMutableSet new];
+        CFRunLoopRef runloop = CFRunLoopGetMain();
+        CFRunLoopObserverRef observer;
+        
+        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
+                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,
+                                           true,      // repeat
+                                           0xFFFFFF,  // after CATransaction(2000000)
+                                           YYRunLoopObserverCallBack, NULL);
+        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
+        CFRelease(observer);
+    });
+}
+
+
+@implementation YYTextTransaction
+
++ (YYTextTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{
+    if (!target || !selector) return nil;
+    YYTextTransaction *t = [YYTextTransaction new];
+    t.target = target;
+    t.selector = selector;
+    return t;
+}
+
+- (void)commit {
+    if (!_target || !_selector) return;
+    YYTextTransactionSetup();
+    [transactionSet addObject:self];
+}
+
+- (NSUInteger)hash {
+    long v1 = (long)((void *)_selector);
+    long v2 = (long)_target;
+    return v1 ^ v2;
+}
+
+- (BOOL)isEqual:(id)object {
+    if (self == object) return YES;
+    if (![object isMemberOfClass:self.class]) return NO;
+    YYTextTransaction *other = object;
+    return other.selector == _selector && other.target == _target;
+}
+
+@end

+ 563 - 0
Demo/Objective_C_Demo/YYText/YYTextUtilities.h

@@ -0,0 +1,563 @@
+//
+//  YYTextUtilities.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/6.
+//  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>
+#import <QuartzCore/QuartzCore.h>
+#import <CoreText/CoreText.h>
+
+
+#ifndef YYTEXT_CLAMP // return the clamped value
+#define YYTEXT_CLAMP(_x_, _low_, _high_)  (((_x_) > (_high_)) ? (_high_) : (((_x_) < (_low_)) ? (_low_) : (_x_)))
+#endif
+
+#ifndef YYTEXT_SWAP // swap two value
+#define YYTEXT_SWAP(_a_, _b_)  do { __typeof__(_a_) _tmp_ = (_a_); (_a_) = (_b_); (_b_) = _tmp_; } while (0)
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ 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.
+ */
+static inline BOOL YYTextIsLinebreakChar(unichar c) {
+    switch (c) {
+        case 0x000D:
+        case 0x2028:
+        case 0x000A:
+        case 0x2029:
+            return YES;
+        default:
+            return NO;
+    }
+}
+
+/**
+ 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.
+ */
+static inline BOOL YYTextIsLinebreakString(NSString * _Nullable str) {
+    if (str.length > 2 || str.length == 0) return NO;
+    if (str.length == 1) {
+        unichar c = [str characterAtIndex:0];
+        return YYTextIsLinebreakChar(c);
+    } else {
+        return ([str characterAtIndex:0] == '\r') && ([str characterAtIndex: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.
+ */
+static inline NSUInteger YYTextLinebreakTailLength(NSString * _Nullable str) {
+    if (str.length >= 2) {
+        unichar c2 = [str characterAtIndex:str.length - 1];
+        if (YYTextIsLinebreakChar(c2)) {
+            unichar c1 = [str characterAtIndex:str.length - 2];
+            if (c1 == '\r' && c2 == '\n') return 2;
+            else return 1;
+        } else {
+            return 0;
+        }
+    } else if (str.length == 1) {
+        return YYTextIsLinebreakChar([str characterAtIndex:0]) ? 1 : 0;
+    } else {
+        return 0;
+    }
+}
+
+/**
+ Convert `UIDataDetectorTypes` to `NSTextCheckingType`.
+ 
+ @param types  The `UIDataDetectorTypes` type.
+ @return The `NSTextCheckingType` type.
+ */
+static inline NSTextCheckingType YYTextNSTextCheckingTypeFromUIDataDetectorType(UIDataDetectorTypes types) {
+    NSTextCheckingType t = 0;
+    if (types & UIDataDetectorTypePhoneNumber) t |= NSTextCheckingTypePhoneNumber;
+    if (types & UIDataDetectorTypeLink) t |= NSTextCheckingTypeLink;
+    if (types & UIDataDetectorTypeAddress) t |= NSTextCheckingTypeAddress;
+    if (types & UIDataDetectorTypeCalendarEvent) t |= NSTextCheckingTypeDate;
+    return t;
+}
+
+/**
+ Whether the font is `AppleColorEmoji` font.
+ 
+ @param font  A font.
+ @return YES: the font is Emoji, NO: the font is not Emoji.
+ */
+static inline BOOL YYTextUIFontIsEmoji(UIFont *font) {
+    return [font.fontName isEqualToString:@"AppleColorEmoji"];
+}
+
+/**
+ Whether the font is `AppleColorEmoji` font.
+ 
+ @param font  A font.
+ @return YES: the font is Emoji, NO: the font is not Emoji.
+ */
+static inline BOOL YYTextCTFontIsEmoji(CTFontRef font) {
+    BOOL isEmoji = NO;
+    CFStringRef name = CTFontCopyPostScriptName(font);
+    if (name && CFEqual(CFSTR("AppleColorEmoji"), name)) isEmoji = YES;
+    if (name) CFRelease(name);
+    return isEmoji;
+}
+
+/**
+ Whether the font is `AppleColorEmoji` font.
+ 
+ @param font  A font.
+ @return YES: the font is Emoji, NO: the font is not Emoji.
+ */
+static inline BOOL YYTextCGFontIsEmoji(CGFontRef font) {
+    BOOL isEmoji = NO;
+    CFStringRef name = CGFontCopyPostScriptName(font);
+    if (name && CFEqual(CFSTR("AppleColorEmoji"), name)) isEmoji = YES;
+    if (name) CFRelease(name);
+    return isEmoji;
+}
+
+/**
+ 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.
+ */
+static inline BOOL YYTextCTFontContainsColorBitmapGlyphs(CTFontRef font) {
+    return  (CTFontGetSymbolicTraits(font) & kCTFontTraitColorGlyphs) != 0;
+}
+
+/**
+ 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.
+ */
+static inline BOOL YYTextCGGlyphIsBitmap(CTFontRef font, CGGlyph glyph) {
+    if (!font && !glyph) return NO;
+    if (!YYTextCTFontContainsColorBitmapGlyphs(font)) return NO;
+    CGPathRef path = CTFontCreatePathForGlyph(font, glyph, NULL);
+    if (path) {
+        CFRelease(path);
+        return NO;
+    }
+    return YES;
+}
+
+/**
+ 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.
+ */
+static inline CGFloat YYTextEmojiGetAscentWithFontSize(CGFloat fontSize) {
+    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.
+ */
+static inline CGFloat YYTextEmojiGetDescentWithFontSize(CGFloat fontSize) {
+    if (fontSize < 16) {
+        return 0.390625 * fontSize;
+    } else if (16 <= fontSize && fontSize <= 24) {
+        return 0.15625 * fontSize + 3.75;
+    } else {
+        return 0.3125 * fontSize;
+    }
+    return 0;
+}
+
+/**
+ 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.
+ */
+static inline CGRect YYTextEmojiGetGlyphBoundingRectWithFontSize(CGFloat fontSize) {
+    CGRect rect;
+    rect.origin.x = 0.75;
+    rect.size.width = rect.size.height = YYTextEmojiGetAscentWithFontSize(fontSize);
+    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 in vertical form.
+ @return The shared character set.
+ */
+NSCharacterSet *YYTextVerticalFormRotateCharacterSet();
+
+/**
+ Get the character set which should rotate and move in vertical form.
+ @return The shared character set.
+ */
+NSCharacterSet *YYTextVerticalFormRotateAndMoveCharacterSet();
+
+
+
+/// Convert degrees to radians.
+static inline CGFloat YYTextDegreesToRadians(CGFloat degrees) {
+    return degrees * M_PI / 180;
+}
+
+/// Convert radians to degrees.
+static inline CGFloat YYTextRadiansToDegrees(CGFloat radians) {
+    return radians * 180 / M_PI;
+}
+
+
+
+/// Get the transform rotation.
+/// @return the rotation in radians [-PI,PI] ([-180°,180°])
+static inline CGFloat YYTextCGAffineTransformGetRotation(CGAffineTransform transform) {
+    return atan2(transform.b, transform.a);
+}
+
+/// Get the transform's scale.x
+static inline CGFloat YYTextCGAffineTransformGetScaleX(CGAffineTransform transform) {
+    return  sqrt(transform.a * transform.a + transform.c * transform.c);
+}
+
+/// Get the transform's scale.y
+static inline CGFloat YYTextCGAffineTransformGetScaleY(CGAffineTransform transform) {
+    return sqrt(transform.b * transform.b + transform.d * transform.d);
+}
+
+/// Get the transform's translate.x
+static inline CGFloat YYTextCGAffineTransformGetTranslateX(CGAffineTransform transform) {
+    return transform.tx;
+}
+
+/// Get the transform's translate.y
+static inline CGFloat YYTextCGAffineTransformGetTranslateY(CGAffineTransform transform) {
+    return transform.ty;
+}
+
+/**
+ 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
+ */
+CGAffineTransform YYTextCGAffineTransformGetFromPoints(CGPoint before[3], CGPoint after[3]);
+
+/// Get the transform which can converts a point from the coordinate system of a given view to another.
+CGAffineTransform YYTextCGAffineTransformGetFromViews(UIView *from, UIView *to);
+
+/// Create a skew transform.
+static inline CGAffineTransform YYTextCGAffineTransformMakeSkew(CGFloat x, CGFloat y){
+    CGAffineTransform transform = CGAffineTransformIdentity;
+    transform.c = -x;
+    transform.b = y;
+    return transform;
+}
+
+/// Negates/inverts a UIEdgeInsets.
+static inline UIEdgeInsets YYTextUIEdgeInsetsInvert(UIEdgeInsets insets) {
+    return UIEdgeInsetsMake(-insets.top, -insets.left, -insets.bottom, -insets.right);
+}
+
+/// Convert CALayer's gravity string to UIViewContentMode.
+UIViewContentMode YYTextCAGravityToUIViewContentMode(NSString *gravity);
+
+/// Convert UIViewContentMode to CALayer's gravity string.
+NSString *YYTextUIViewContentModeToCAGravity(UIViewContentMode contentMode);
+
+
+
+/**
+ Returns a rectangle to fit the @param rect with specified content mode.
+ 
+ @param rect The constrant rect
+ @param size The content size
+ @param mode The content mode
+ @return A rectangle for the given content mode.
+ @discussion UIViewContentModeRedraw is same as UIViewContentModeScaleToFill.
+ */
+CGRect YYTextCGRectFitWithContentMode(CGRect rect, CGSize size, UIViewContentMode mode);
+
+/// Returns the center for the rectangle.
+static inline CGPoint YYTextCGRectGetCenter(CGRect rect) {
+    return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
+}
+
+/// Returns the area of the rectangle.
+static inline CGFloat YYTextCGRectGetArea(CGRect rect) {
+    if (CGRectIsNull(rect)) return 0;
+    rect = CGRectStandardize(rect);
+    return rect.size.width * rect.size.height;
+}
+
+/// Returns the distance between two points.
+static inline CGFloat YYTextCGPointGetDistanceToPoint(CGPoint p1, CGPoint p2) {
+    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.
+static inline CGFloat YYTextCGPointGetDistanceToRect(CGPoint p, CGRect r) {
+    r = CGRectStandardize(r);
+    if (CGRectContainsPoint(r, p)) return 0;
+    CGFloat distV, distH;
+    if (CGRectGetMinY(r) <= p.y && p.y <= CGRectGetMaxY(r)) {
+        distV = 0;
+    } else {
+        distV = p.y < CGRectGetMinY(r) ? CGRectGetMinY(r) - p.y : p.y - CGRectGetMaxY(r);
+    }
+    if (CGRectGetMinX(r) <= p.x && p.x <= CGRectGetMaxX(r)) {
+        distH = 0;
+    } else {
+        distH = p.x < CGRectGetMinX(r) ? CGRectGetMinX(r) - p.x : p.x - CGRectGetMaxX(r);
+    }
+    return MAX(distV, distH);
+}
+
+
+/// Get main screen's scale.
+CGFloat YYTextScreenScale();
+
+/// Get main screen's size. Height is always larger than width.
+CGSize YYTextScreenSize();
+
+/// Convert point to pixel.
+static inline CGFloat YYTextCGFloatToPixel(CGFloat value) {
+    return value * YYTextScreenScale();
+}
+
+/// Convert pixel to point.
+static inline CGFloat YYTextCGFloatFromPixel(CGFloat value) {
+    return value / YYTextScreenScale();
+}
+
+/// floor point value for pixel-aligned
+static inline CGFloat YYTextCGFloatPixelFloor(CGFloat value) {
+    CGFloat scale = YYTextScreenScale();
+    return floor(value * scale) / scale;
+}
+
+/// round point value for pixel-aligned
+static inline CGFloat YYTextCGFloatPixelRound(CGFloat value) {
+    CGFloat scale = YYTextScreenScale();
+    return round(value * scale) / scale;
+}
+
+/// ceil point value for pixel-aligned
+static inline CGFloat YYTextCGFloatPixelCeil(CGFloat value) {
+    CGFloat scale = YYTextScreenScale();
+    return ceil(value * scale) / scale;
+}
+
+/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned)
+static inline CGFloat YYTextCGFloatPixelHalf(CGFloat value) {
+    CGFloat scale = YYTextScreenScale();
+    return (floor(value * scale) + 0.5) / scale;
+}
+
+/// floor point value for pixel-aligned
+static inline CGPoint YYTextCGPointPixelFloor(CGPoint point) {
+    CGFloat scale = YYTextScreenScale();
+    return CGPointMake(floor(point.x * scale) / scale,
+                       floor(point.y * scale) / scale);
+}
+
+/// round point value for pixel-aligned
+static inline CGPoint YYTextCGPointPixelRound(CGPoint point) {
+    CGFloat scale = YYTextScreenScale();
+    return CGPointMake(round(point.x * scale) / scale,
+                       round(point.y * scale) / scale);
+}
+
+/// ceil point value for pixel-aligned
+static inline CGPoint YYTextCGPointPixelCeil(CGPoint point) {
+    CGFloat scale = YYTextScreenScale();
+    return CGPointMake(ceil(point.x * scale) / scale,
+                       ceil(point.y * scale) / scale);
+}
+
+/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned)
+static inline CGPoint YYTextCGPointPixelHalf(CGPoint point) {
+    CGFloat scale = YYTextScreenScale();
+    return CGPointMake((floor(point.x * scale) + 0.5) / scale,
+                       (floor(point.y * scale) + 0.5) / scale);
+}
+
+
+
+/// floor point value for pixel-aligned
+static inline CGSize YYTextCGSizePixelFloor(CGSize size) {
+    CGFloat scale = YYTextScreenScale();
+    return CGSizeMake(floor(size.width * scale) / scale,
+                      floor(size.height * scale) / scale);
+}
+
+/// round point value for pixel-aligned
+static inline CGSize YYTextCGSizePixelRound(CGSize size) {
+    CGFloat scale = YYTextScreenScale();
+    return CGSizeMake(round(size.width * scale) / scale,
+                      round(size.height * scale) / scale);
+}
+
+/// ceil point value for pixel-aligned
+static inline CGSize YYTextCGSizePixelCeil(CGSize size) {
+    CGFloat scale = YYTextScreenScale();
+    return CGSizeMake(ceil(size.width * scale) / scale,
+                      ceil(size.height * scale) / scale);
+}
+
+/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned)
+static inline CGSize YYTextCGSizePixelHalf(CGSize size) {
+    CGFloat scale = YYTextScreenScale();
+    return CGSizeMake((floor(size.width * scale) + 0.5) / scale,
+                      (floor(size.height * scale) + 0.5) / scale);
+}
+
+
+
+/// floor point value for pixel-aligned
+static inline CGRect YYTextCGRectPixelFloor(CGRect rect) {
+    CGPoint origin = YYTextCGPointPixelCeil(rect.origin);
+    CGPoint corner = YYTextCGPointPixelFloor(CGPointMake(rect.origin.x + rect.size.width,
+                                                     rect.origin.y + rect.size.height));
+    CGRect ret = CGRectMake(origin.x, origin.y, corner.x - origin.x, 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
+static inline CGRect YYTextCGRectPixelRound(CGRect rect) {
+    CGPoint origin = YYTextCGPointPixelRound(rect.origin);
+    CGPoint corner = YYTextCGPointPixelRound(CGPointMake(rect.origin.x + rect.size.width,
+                                                     rect.origin.y + rect.size.height));
+    return CGRectMake(origin.x, origin.y, corner.x - origin.x, corner.y - origin.y);
+}
+
+/// ceil point value for pixel-aligned
+static inline CGRect YYTextCGRectPixelCeil(CGRect rect) {
+    CGPoint origin = YYTextCGPointPixelFloor(rect.origin);
+    CGPoint corner = YYTextCGPointPixelCeil(CGPointMake(rect.origin.x + rect.size.width,
+                                                    rect.origin.y + rect.size.height));
+    return CGRectMake(origin.x, origin.y, corner.x - origin.x, corner.y - origin.y);
+}
+
+/// round point value to .5 pixel for path stroke (odd pixel line width pixel-aligned)
+static inline CGRect YYTextCGRectPixelHalf(CGRect rect) {
+    CGPoint origin = YYTextCGPointPixelHalf(rect.origin);
+    CGPoint corner = YYTextCGPointPixelHalf(CGPointMake(rect.origin.x + rect.size.width,
+                                                    rect.origin.y + rect.size.height));
+    return CGRectMake(origin.x, origin.y, corner.x - origin.x, corner.y - origin.y);
+}
+
+
+
+/// floor UIEdgeInset for pixel-aligned
+static inline UIEdgeInsets YYTextUIEdgeInsetPixelFloor(UIEdgeInsets insets) {
+    insets.top = YYTextCGFloatPixelFloor(insets.top);
+    insets.left = YYTextCGFloatPixelFloor(insets.left);
+    insets.bottom = YYTextCGFloatPixelFloor(insets.bottom);
+    insets.right = YYTextCGFloatPixelFloor(insets.right);
+    return insets;
+}
+
+/// ceil UIEdgeInset for pixel-aligned
+static inline UIEdgeInsets YYTextUIEdgeInsetPixelCeil(UIEdgeInsets insets) {
+    insets.top = YYTextCGFloatPixelCeil(insets.top);
+    insets.left = YYTextCGFloatPixelCeil(insets.left);
+    insets.bottom = YYTextCGFloatPixelCeil(insets.bottom);
+    insets.right = YYTextCGFloatPixelCeil(insets.right);
+    return insets;
+}
+
+
+
+static inline UIFont * _Nullable YYTextFontWithBold(UIFont *font) {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize];
+}
+
+static inline UIFont * _Nullable YYTextFontWithItalic(UIFont *font) {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic] size:font.pointSize];
+}
+
+static inline UIFont * _Nullable YYTextFontWithBoldItalic(UIFont *font) {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold | UIFontDescriptorTraitItalic] size:font.pointSize];
+}
+
+
+
+/**
+ Convert CFRange to NSRange
+ @param range CFRange @return NSRange
+ */
+static inline NSRange YYTextNSRangeFromCFRange(CFRange range) {
+    return NSMakeRange(range.location, range.length);
+}
+
+/**
+ Convert NSRange to CFRange
+ @param range NSRange @return CFRange
+ */
+static inline CFRange YYTextCFRangeFromNSRange(NSRange range) {
+    return CFRangeMake(range.location, range.length);
+}
+
+
+/// Returns YES in App Extension.
+BOOL YYTextIsAppExtension();
+
+/// Returns nil in App Extension.
+UIApplication * _Nullable YYTextSharedApplication();
+
+NS_ASSUME_NONNULL_END

+ 309 - 0
Demo/Objective_C_Demo/YYText/YYTextUtilities.m

@@ -0,0 +1,309 @@
+//
+//  YYTextUtilities.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/4/6.
+//  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 "YYTextUtilities.h"
+#import <Accelerate/Accelerate.h>
+#import "UIView+YYText.h"
+
+NSCharacterSet *YYTextVerticalFormRotateCharacterSet() {
+    static NSMutableCharacterSet *set;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        set = [NSMutableCharacterSet new];
+        [set addCharactersInRange:NSMakeRange(0x1100, 256)]; // Hangul Jamo
+        [set addCharactersInRange:NSMakeRange(0x2460, 160)]; // Enclosed Alphanumerics
+        [set addCharactersInRange:NSMakeRange(0x2600, 256)]; // Miscellaneous Symbols
+        [set addCharactersInRange:NSMakeRange(0x2700, 192)]; // Dingbats
+        [set addCharactersInRange:NSMakeRange(0x2E80, 128)]; // CJK Radicals Supplement
+        [set addCharactersInRange:NSMakeRange(0x2F00, 224)]; // Kangxi Radicals
+        [set addCharactersInRange:NSMakeRange(0x2FF0, 16)]; // Ideographic Description Characters
+        [set addCharactersInRange:NSMakeRange(0x3000, 64)]; // CJK Symbols and Punctuation
+        [set removeCharactersInRange:NSMakeRange(0x3008, 10)];
+        [set removeCharactersInRange:NSMakeRange(0x3014, 12)];
+        [set addCharactersInRange:NSMakeRange(0x3040, 96)]; // Hiragana
+        [set addCharactersInRange:NSMakeRange(0x30A0, 96)]; // Katakana
+        [set addCharactersInRange:NSMakeRange(0x3100, 48)]; // Bopomofo
+        [set addCharactersInRange:NSMakeRange(0x3130, 96)]; // Hangul Compatibility Jamo
+        [set addCharactersInRange:NSMakeRange(0x3190, 16)]; // Kanbun
+        [set addCharactersInRange:NSMakeRange(0x31A0, 32)]; // Bopomofo Extended
+        [set addCharactersInRange:NSMakeRange(0x31C0, 48)]; // CJK Strokes
+        [set addCharactersInRange:NSMakeRange(0x31F0, 16)]; // Katakana Phonetic Extensions
+        [set addCharactersInRange:NSMakeRange(0x3200, 256)]; // Enclosed CJK Letters and Months
+        [set addCharactersInRange:NSMakeRange(0x3300, 256)]; // CJK Compatibility
+        [set addCharactersInRange:NSMakeRange(0x3400, 2582)]; // CJK Unified Ideographs Extension A
+        [set addCharactersInRange:NSMakeRange(0x4E00, 20941)]; // CJK Unified Ideographs
+        [set addCharactersInRange:NSMakeRange(0xAC00, 11172)]; // Hangul Syllables
+        [set addCharactersInRange:NSMakeRange(0xD7B0, 80)]; // Hangul Jamo Extended-B
+        [set addCharactersInString:@""]; // U+F8FF (Private Use Area)
+        [set addCharactersInRange:NSMakeRange(0xF900, 512)]; // CJK Compatibility Ideographs
+        [set addCharactersInRange:NSMakeRange(0xFE10, 16)]; // Vertical Forms
+        [set addCharactersInRange:NSMakeRange(0xFF00, 240)]; // Halfwidth and Fullwidth Forms
+        [set addCharactersInRange:NSMakeRange(0x1F200, 256)]; // Enclosed Ideographic Supplement
+        [set addCharactersInRange:NSMakeRange(0x1F300, 768)]; // Enclosed Ideographic Supplement
+        [set addCharactersInRange:NSMakeRange(0x1F600, 80)]; // Emoticons (Emoji)
+        [set addCharactersInRange:NSMakeRange(0x1F680, 128)]; // Transport and Map Symbols
+        
+        // See http://unicode-table.com/ for more information.
+    });
+    return set;
+}
+
+NSCharacterSet *YYTextVerticalFormRotateAndMoveCharacterSet() {
+    static NSMutableCharacterSet *set;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        set = [NSMutableCharacterSet new];
+        [set addCharactersInString:@",。、."];
+    });
+    return set;
+}
+
+// return 0 when succeed
+static int matrix_invert(__CLPK_integer N, double *matrix) {
+    __CLPK_integer error = 0;
+    __CLPK_integer pivot_tmp[6 * 6];
+    __CLPK_integer *pivot = pivot_tmp;
+    double workspace_tmp[6 * 6];
+    double *workspace = workspace_tmp;
+    bool need_free = false;
+    
+    if (N > 6) {
+        need_free = true;
+        pivot = malloc(N * N * sizeof(__CLPK_integer));
+        if (!pivot) return -1;
+        workspace = malloc(N * sizeof(double));
+        if (!workspace) {
+            free(pivot);
+            return -1;
+        }
+    }
+    
+    dgetrf_(&N, &N, matrix, &N, pivot, &error);
+    
+    if (error == 0) {
+        dgetri_(&N, matrix, &N, pivot, workspace, &N, &error);
+    }
+    
+    if (need_free) {
+        free(pivot);
+        free(workspace);
+    }
+    return error;
+}
+
+CGAffineTransform YYTextCGAffineTransformGetFromPoints(CGPoint before[3], CGPoint after[3]) {
+    if (before == NULL || after == NULL) return CGAffineTransformIdentity;
+    
+    CGPoint p1, p2, p3, q1, q2, q3;
+    p1 = before[0]; p2 = before[1]; p3 = before[2];
+    q1 =  after[0]; q2 =  after[1]; q3 =  after[2];
+    
+    double A[36];
+    A[ 0] = p1.x; A[ 1] = p1.y; A[ 2] = 0; A[ 3] = 0; A[ 4] = 1; A[ 5] = 0;
+    A[ 6] = 0; A[ 7] = 0; A[ 8] = p1.x; A[ 9] = p1.y; A[10] = 0; A[11] = 1;
+    A[12] = p2.x; A[13] = p2.y; A[14] = 0; A[15] = 0; A[16] = 1; A[17] = 0;
+    A[18] = 0; A[19] = 0; A[20] = p2.x; A[21] = p2.y; A[22] = 0; A[23] = 1;
+    A[24] = p3.x; A[25] = p3.y; A[26] = 0; A[27] = 0; A[28] = 1; A[29] = 0;
+    A[30] = 0; A[31] = 0; A[32] = p3.x; A[33] = p3.y; A[34] = 0; A[35] = 1;
+    
+    int error = matrix_invert(6, A);
+    if (error) return CGAffineTransformIdentity;
+    
+    double B[6];
+    B[0] = q1.x; B[1] = q1.y; B[2] = q2.x; B[3] = q2.y; B[4] = q3.x; B[5] = q3.y;
+    
+    double M[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];
+    
+    CGAffineTransform transform = CGAffineTransformMake(M[0], M[2], M[1], M[3], M[4], M[5]);
+    return transform;
+}
+
+CGAffineTransform YYTextCGAffineTransformGetFromViews(UIView *from, UIView *to) {
+    if (!from || !to) return CGAffineTransformIdentity;
+    
+    CGPoint before[3], after[3];
+    before[0] = CGPointMake(0, 0);
+    before[1] = CGPointMake(0, 1);
+    before[2] = CGPointMake(1, 0);
+    after[0] = [from yy_convertPoint:before[0] toViewOrWindow:to];
+    after[1] = [from yy_convertPoint:before[1] toViewOrWindow:to];
+    after[2] = [from yy_convertPoint:before[2] toViewOrWindow:to];
+    
+    return YYTextCGAffineTransformGetFromPoints(before, after);
+}
+
+UIViewContentMode YYTextCAGravityToUIViewContentMode(NSString *gravity) {
+    static NSDictionary *dic;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        dic = @{ kCAGravityCenter:@(UIViewContentModeCenter),
+                 kCAGravityTop:@(UIViewContentModeTop),
+                 kCAGravityBottom:@(UIViewContentModeBottom),
+                 kCAGravityLeft:@(UIViewContentModeLeft),
+                 kCAGravityRight:@(UIViewContentModeRight),
+                 kCAGravityTopLeft:@(UIViewContentModeTopLeft),
+                 kCAGravityTopRight:@(UIViewContentModeTopRight),
+                 kCAGravityBottomLeft:@(UIViewContentModeBottomLeft),
+                 kCAGravityBottomRight:@(UIViewContentModeBottomRight),
+                 kCAGravityResize:@(UIViewContentModeScaleToFill),
+                 kCAGravityResizeAspect:@(UIViewContentModeScaleAspectFit),
+                 kCAGravityResizeAspectFill:@(UIViewContentModeScaleAspectFill) };
+    });
+    if (!gravity) return UIViewContentModeScaleToFill;
+    return (UIViewContentMode)((NSNumber *)dic[gravity]).integerValue;
+}
+
+NSString *YYTextUIViewContentModeToCAGravity(UIViewContentMode contentMode) {
+    switch (contentMode) {
+        case UIViewContentModeScaleToFill: return kCAGravityResize;
+        case UIViewContentModeScaleAspectFit: return kCAGravityResizeAspect;
+        case UIViewContentModeScaleAspectFill: return kCAGravityResizeAspectFill;
+        case UIViewContentModeRedraw: return kCAGravityResize;
+        case UIViewContentModeCenter: return kCAGravityCenter;
+        case UIViewContentModeTop: return kCAGravityTop;
+        case UIViewContentModeBottom: return kCAGravityBottom;
+        case UIViewContentModeLeft: return kCAGravityLeft;
+        case UIViewContentModeRight: return kCAGravityRight;
+        case UIViewContentModeTopLeft: return kCAGravityTopLeft;
+        case UIViewContentModeTopRight: return kCAGravityTopRight;
+        case UIViewContentModeBottomLeft: return kCAGravityBottomLeft;
+        case UIViewContentModeBottomRight: return kCAGravityBottomRight;
+        default: return kCAGravityResize;
+    }
+}
+
+CGRect YYTextCGRectFitWithContentMode(CGRect rect, CGSize size, UIViewContentMode mode) {
+    rect = CGRectStandardize(rect);
+    size.width = size.width < 0 ? -size.width : size.width;
+    size.height = size.height < 0 ? -size.height : size.height;
+    CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
+    switch (mode) {
+        case UIViewContentModeScaleAspectFit:
+        case UIViewContentModeScaleAspectFill: {
+            if (rect.size.width < 0.01 || rect.size.height < 0.01 ||
+                size.width < 0.01 || size.height < 0.01) {
+                rect.origin = center;
+                rect.size = CGSizeZero;
+            } else {
+                CGFloat scale;
+                if (mode == UIViewContentModeScaleAspectFit) {
+                    if (size.width / size.height < rect.size.width / rect.size.height) {
+                        scale = rect.size.height / size.height;
+                    } else {
+                        scale = rect.size.width / size.width;
+                    }
+                } else {
+                    if (size.width / size.height < rect.size.width / rect.size.height) {
+                        scale = rect.size.width / size.width;
+                    } else {
+                        scale = rect.size.height / size.height;
+                    }
+                }
+                size.width *= scale;
+                size.height *= scale;
+                rect.size = size;
+                rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5);
+            }
+        } break;
+        case UIViewContentModeCenter: {
+            rect.size = size;
+            rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5);
+        } break;
+        case UIViewContentModeTop: {
+            rect.origin.x = center.x - size.width * 0.5;
+            rect.size = size;
+        } break;
+        case UIViewContentModeBottom: {
+            rect.origin.x = center.x - size.width * 0.5;
+            rect.origin.y += rect.size.height - size.height;
+            rect.size = size;
+        } break;
+        case UIViewContentModeLeft: {
+            rect.origin.y = center.y - size.height * 0.5;
+            rect.size = size;
+        } break;
+        case UIViewContentModeRight: {
+            rect.origin.y = center.y - size.height * 0.5;
+            rect.origin.x += rect.size.width - size.width;
+            rect.size = size;
+        } break;
+        case UIViewContentModeTopLeft: {
+            rect.size = size;
+        } break;
+        case UIViewContentModeTopRight: {
+            rect.origin.x += rect.size.width - size.width;
+            rect.size = size;
+        } break;
+        case UIViewContentModeBottomLeft: {
+            rect.origin.y += rect.size.height - size.height;
+            rect.size = size;
+        } break;
+        case UIViewContentModeBottomRight: {
+            rect.origin.x += rect.size.width - size.width;
+            rect.origin.y += rect.size.height - size.height;
+            rect.size = size;
+        } break;
+        case UIViewContentModeScaleToFill:
+        case UIViewContentModeRedraw:
+        default: {
+            rect = rect;
+        }
+    }
+    return rect;
+}
+
+CGFloat YYTextScreenScale() {
+    static CGFloat scale;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        scale = [UIScreen mainScreen].scale;
+    });
+    return scale;
+}
+
+CGSize YYTextScreenSize() {
+    static CGSize size;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        size = [UIScreen mainScreen].bounds.size;
+        if (size.height < size.width) {
+            CGFloat tmp = size.height;
+            size.height = size.width;
+            size.width = tmp;
+        }
+    });
+    return size;
+}
+
+
+BOOL YYTextIsAppExtension() {
+    static BOOL isAppExtension = NO;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        Class cls = NSClassFromString(@"UIApplication");
+        if(!cls || ![cls respondsToSelector:@selector(sharedApplication)]) isAppExtension = YES;
+        if ([[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]) isAppExtension = YES;
+    });
+    return isAppExtension;
+}
+
+UIApplication *YYTextSharedApplication() {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+    return YYTextIsAppExtension() ? nil : [UIApplication performSelector:@selector(sharedApplication)];
+#pragma clang diagnostic pop
+}

+ 410 - 0
Demo/Objective_C_Demo/YYText/YYTextView.h

@@ -0,0 +1,410 @@
+//
+//  YYTextView.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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(<YYText/YYText.h>)
+#import <YYText/YYTextParser.h>
+#import <YYText/YYTextLayout.h>
+#import <YYText/YYTextAttribute.h>
+#else
+#import "YYTextParser.h"
+#import "YYTextLayout.h"
+#import "YYTextAttribute.h"
+#endif
+
+@class YYTextView;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ The YYTextViewDelegate protocol defines a set of optional methods you can use
+ to receive editing-related messages for YYTextView objects. 
+ 
+ @discussion The API and behavior is similar to UITextViewDelegate,
+ see UITextViewDelegate's documentation for more information.
+ */
+@protocol YYTextViewDelegate <NSObject, UIScrollViewDelegate>
+@optional
+- (BOOL)textViewShouldBeginEditing:(YYTextView *)textView;
+- (BOOL)textViewShouldEndEditing:(YYTextView *)textView;
+- (void)textViewDidBeginEditing:(YYTextView *)textView;
+- (void)textViewDidEndEditing:(YYTextView *)textView;
+- (BOOL)textView:(YYTextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
+- (void)textViewDidChange:(YYTextView *)textView;
+- (void)textViewDidChangeSelection:(YYTextView *)textView;
+
+- (BOOL)textView:(YYTextView *)textView shouldTapHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange;
+- (void)textView:(YYTextView *)textView didTapHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect;
+- (BOOL)textView:(YYTextView *)textView shouldLongPressHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange;
+- (void)textView:(YYTextView *)textView didLongPressHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect;
+@end
+
+
+#if !TARGET_INTERFACE_BUILDER
+
+/**
+ The YYTextView class implements the behavior for a scrollable, multiline text region.
+ 
+ @discussion The API and behavior is similar to UITextView, but provides more features:
+ 
+ * 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 exclusion paths to control text container's shape.
+ * It supports vertical form layout to display and edit CJK text.
+ * It allows user to copy/paste image and attributed text from/to text view.
+ * It allows to set an attributed text as placeholder.
+ 
+ See NSAttributedString+YYText.h for more convenience methods to set the attributes.
+ See YYTextAttribute.h and YYTextLayout.h for more information.
+ */
+@interface YYTextView : UIScrollView <UITextInput>
+
+
+#pragma mark - Accessing the Delegate
+///=============================================================================
+/// @name Accessing the Delegate
+///=============================================================================
+
+@property (nullable, nonatomic, weak) id<YYTextViewDelegate> delegate;
+
+
+#pragma mark - Configuring the Text Attributes
+///=============================================================================
+/// @name Configuring the Text Attributes
+///=============================================================================
+
+/**
+ The text displayed by the text view.
+ Set a new value to this property also replaces the text in `attributedText`.
+ Get the value returns the plain text in `attributedText`.
+ */
+@property (null_resettable, nonatomic, copy) NSString *text;
+
+/**
+ The font of the text. Default is 12-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`.
+ */
+@property (nullable, nonatomic, strong) UIFont *font;
+
+/**
+ 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`.
+ */
+@property (nullable, nonatomic, strong) UIColor *textColor;
+
+/**
+ The technique to use for aligning the text. Default is NSLeftTextAlignment.
+ 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`.
+ */
+@property (nonatomic) NSTextAlignment textAlignment;
+
+/**
+ The text vertical aligmnent in container. Default is YYTextVerticalAlignmentTop.
+ */
+@property (nonatomic) YYTextVerticalAlignment textVerticalAlignment;
+
+/**
+ The types of data converted to clickable URLs in the text view. Default is UIDataDetectorTypeNone.
+ The tap or long press action should be handled by delegate.
+ */
+@property (nonatomic) UIDataDetectorTypes dataDetectorTypes;
+
+/**
+ The attributes to apply to links at normal state. Default is light blue color.
+ When a range of text is detected by the `dataDetectorTypes`, this value would be
+ used to modify the original attributes in the range.
+ */
+@property (nullable, nonatomic, copy) NSDictionary<NSString *, id> *linkTextAttributes;
+
+/**
+ The attributes to apply to links at highlight state. Default is a gray border.
+ When a range of text is detected by the `dataDetectorTypes` and the range was touched by user,
+ this value would be used to modify the original attributes in the range.
+ */
+@property (nullable, nonatomic, copy) NSDictionary<NSString *, id> *highlightTextAttributes;
+
+/**
+ The attributes to apply to new text being entered by the user.
+ When the text view's selection changes, this value is reset automatically.
+ */
+@property (nullable, nonatomic, copy) NSDictionary<NSString *, id> *typingAttributes;
+
+/**
+ The styled text displayed by the text view.
+ Set a new value to this property also replaces the value of the `text`, `font`, `textColor`,
+ `textAlignment` and other properties in text view.
+ 
+ @discussion It only support the attributes declared in CoreText and YYTextAttribute.
+ See `NSAttributedString+YYText` for more convenience methods to set the attributes.
+ */
+@property (nullable, nonatomic, copy) NSAttributedString *attributedText;
+
+/**
+ 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 `YYTextParser` protocol for more information.
+ */
+@property (nullable, nonatomic, strong) id<YYTextParser> textParser;
+
+/**
+ The current text layout in text view (readonly).
+ It can be used to query the text layout information.
+ */
+@property (nullable, nonatomic, strong, readonly) YYTextLayout *textLayout;
+
+
+#pragma mark - Configuring the Placeholder
+///=============================================================================
+/// @name Configuring the Placeholder
+///=============================================================================
+
+/**
+ The placeholder text displayed by the text view (when the text view is empty).
+ Set a new value to this property also replaces the text in `placeholderAttributedText`.
+ Get the value returns the plain text in `placeholderAttributedText`.
+ */
+@property (nullable, nonatomic, copy) NSString *placeholderText;
+
+/**
+ The font of the placeholder text. Default is same as `font` property.
+ Set a new value to this property also causes the new font to be applied to the entire `placeholderAttributedText`.
+ Get the value returns the font at the head of `placeholderAttributedText`.
+ */
+@property (nullable, nonatomic, strong) UIFont *placeholderFont;
+
+/**
+ The color of the placeholder text. Default is gray.
+ Set a new value to this property also causes the new color to be applied to the entire `placeholderAttributedText`.
+ Get the value returns the color at the head of `placeholderAttributedText`.
+ */
+@property (nullable, nonatomic, strong) UIColor *placeholderTextColor;
+
+/**
+ The styled placeholder text displayed by the text view (when the text view is empty).
+ Set a new value to this property also replaces the value of the `placeholderText`, 
+ `placeholderFont`, `placeholderTextColor`.
+ 
+ @discussion It only support the attributes declared in CoreText and YYTextAttribute.
+ See `NSAttributedString+YYText` for more convenience methods to set the attributes.
+ */
+@property (nullable, nonatomic, copy) NSAttributedString *placeholderAttributedText;
+
+
+#pragma mark - Configuring the Text Container
+///=============================================================================
+/// @name Configuring the Text Container
+///=============================================================================
+
+/**
+ The inset of the text container's layout area within the text view's content area.
+ */
+@property (nonatomic) UIEdgeInsets textContainerInset;
+
+/**
+ An array of UIBezierPath objects representing the exclusion paths inside the 
+ receiver's bounding rectangle. Default value is nil.
+ */
+@property (nullable, nonatomic, copy) NSArray<UIBezierPath *> *exclusionPaths;
+
+/**
+ Whether the receiver's layout orientation is vertical form. Default is NO.
+ It may used to edit/display CJK text.
+ */
+@property (nonatomic, getter=isVerticalForm) BOOL verticalForm;
+
+/**
+ The text line position modifier used to modify the lines' position in layout.
+ See `YYTextLinePositionModifier` protocol for more information.
+ */
+@property (nullable, nonatomic, copy) id<YYTextLinePositionModifier> linePositionModifier;
+
+/**
+ The debug option to display CoreText layout result.
+ The default value is [YYTextDebugOption sharedDebugOption].
+ */
+@property (nullable, nonatomic, copy) YYTextDebugOption *debugOption;
+
+
+#pragma mark - Working with the Selection and Menu
+///=============================================================================
+/// @name Working with the Selection and Menu
+///=============================================================================
+
+/**
+ Scrolls the receiver until the text in the specified range is visible.
+ */
+- (void)scrollRangeToVisible:(NSRange)range;
+
+/**
+ The current selection range of the receiver.
+ */
+@property (nonatomic) NSRange selectedRange;
+
+/**
+ A Boolean value indicating whether inserting text replaces the previous contents.
+ The default value is NO.
+ */
+@property (nonatomic) BOOL clearsOnInsertion;
+
+/**
+ A Boolean value indicating whether the receiver is selectable. Default is YES.
+ When the value of this property is NO, user cannot select content or edit text.
+ */
+@property (nonatomic, getter=isSelectable) BOOL selectable;
+
+/**
+ A Boolean value indicating whether the receiver is highlightable. Default is YES.
+ When the value of this property is NO, user cannot interact with the highlight range of text.
+ */
+@property (nonatomic, getter=isHighlightable) BOOL highlightable;
+
+/**
+ A Boolean value indicating whether the receiver is editable. Default is YES.
+ When the value of this property is NO, user cannot edit text.
+ */
+@property (nonatomic, getter=isEditable) BOOL editable;
+
+/**
+ A Boolean value indicating whether the receiver can paste image from pasteboard. Default is NO.
+ When the value of this property is YES, user can paste image from pasteboard via "paste" menu.
+ */
+@property (nonatomic) BOOL allowsPasteImage;
+
+/**
+ A Boolean value indicating whether the receiver can paste attributed text from pasteboard. Default is NO.
+ When the value of this property is YES, user can paste attributed text from pasteboard via "paste" menu.
+ */
+@property (nonatomic) BOOL allowsPasteAttributedString;
+
+/**
+ A Boolean value indicating whether the receiver can copy attributed text to pasteboard. Default is YES.
+ When the value of this property is YES, user can copy attributed text (with attachment image)
+ from text view to pasteboard via "copy" menu.
+ */
+@property (nonatomic) BOOL allowsCopyAttributedString;
+
+
+#pragma mark - Manage the undo and redo
+///=============================================================================
+/// @name Manage the undo and redo
+///=============================================================================
+
+/**
+ A Boolean value indicating whether the receiver can undo and redo typing with
+ shake gesture. The default value is YES.
+ */
+@property (nonatomic) BOOL allowsUndoAndRedo;
+
+/**
+ The maximum undo/redo level. The default value is 20.
+ */
+@property (nonatomic) NSUInteger maximumUndoLevel;
+
+
+#pragma mark - Replacing the System Input Views
+///=============================================================================
+/// @name Replacing the System Input Views
+///=============================================================================
+
+/**
+ The custom input view to display when the text view becomes the first responder.
+ It can be used to replace system keyboard.
+ 
+ @discussion If set the value while first responder, it will not take effect until 
+ 'reloadInputViews' is called.
+ */
+@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputView;
+
+/**
+ The custom accessory view to display when the text view becomes the first responder.
+ It can be used to add a toolbar at the top of keyboard.
+ 
+ @discussion If set the value while first responder, it will not take effect until
+ 'reloadInputViews' is called.
+ */
+@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputAccessoryView;
+
+/**
+ If you use an custom accessory view without "inputAccessoryView" property,
+ you may set the accessory view's height. It may used by auto scroll calculation.
+ */
+@property (nonatomic) CGFloat extraAccessoryViewHeight;
+
+@end
+
+
+#else // TARGET_INTERFACE_BUILDER
+//IB_DESIGNABLE
+//@interface YYTextView : UIScrollView <UITextInput>
+//@property (null_resettable, nonatomic, copy) IBInspectable NSString *text;
+//@property (nullable, nonatomic, strong) IBInspectable UIColor *textColor;
+//@property (nullable, nonatomic, strong) IBInspectable NSString *fontName_;
+//@property (nonatomic) IBInspectable CGFloat fontSize_;
+//@property (nonatomic) IBInspectable BOOL fontIsBold_;
+//@property (nonatomic) IBInspectable NSTextAlignment textAlignment;
+//@property (nonatomic) IBInspectable YYTextVerticalAlignment textVerticalAlignment;
+//@property (nullable, nonatomic, copy) IBInspectable NSString *placeholderText;
+//@property (nullable, nonatomic, strong) IBInspectable UIColor *placeholderTextColor;
+//@property (nullable, nonatomic, strong) IBInspectable NSString *placeholderFontName_;
+//@property (nonatomic) IBInspectable CGFloat placeholderFontSize_;
+//@property (nonatomic) IBInspectable BOOL placeholderFontIsBold_;
+//@property (nonatomic, getter=isVerticalForm) IBInspectable BOOL verticalForm;
+//@property (nonatomic) IBInspectable BOOL clearsOnInsertion;
+//@property (nonatomic, getter=isSelectable) IBInspectable BOOL selectable;
+//@property (nonatomic, getter=isHighlightable) IBInspectable BOOL highlightable;
+//@property (nonatomic, getter=isEditable) IBInspectable BOOL editable;
+//@property (nonatomic) IBInspectable BOOL allowsPasteImage;
+//@property (nonatomic) IBInspectable BOOL allowsPasteAttributedString;
+//@property (nonatomic) IBInspectable BOOL allowsCopyAttributedString;
+//@property (nonatomic) IBInspectable BOOL allowsUndoAndRedo;
+//@property (nonatomic) IBInspectable NSUInteger maximumUndoLevel;
+//@property (nonatomic) IBInspectable CGFloat insetTop_;
+//@property (nonatomic) IBInspectable CGFloat insetBottom_;
+//@property (nonatomic) IBInspectable CGFloat insetLeft_;
+//@property (nonatomic) IBInspectable CGFloat insetRight_;
+//@property (nonatomic) IBInspectable BOOL debugEnabled_;
+//@property (nullable, nonatomic, weak) id<YYTextViewDelegate> delegate;
+//@property (nullable, nonatomic, strong) UIFont *font;
+//@property (nonatomic) UIDataDetectorTypes dataDetectorTypes;
+//@property (nullable, nonatomic, copy) NSDictionary *linkTextAttributes;
+//@property (nullable, nonatomic, copy) NSDictionary *highlightTextAttributes;
+//@property (nullable, nonatomic, copy) NSDictionary *typingAttributes;
+//@property (nullable, nonatomic, copy) NSAttributedString *attributedText;
+//@property (nullable, nonatomic, strong) id<YYTextParser> textParser;
+//@property (nullable, nonatomic, strong, readonly) YYTextLayout *textLayout;
+//@property (nullable, nonatomic, strong) UIFont *placeholderFont;
+//@property (nullable, nonatomic, copy) NSAttributedString *placeholderAttributedText;
+//@property (nonatomic) UIEdgeInsets textContainerInset;
+//@property (nullable, nonatomic, copy) NSArray *exclusionPaths;
+//@property (nullable, nonatomic, copy) id<YYTextLinePositionModifier> linePositionModifier;
+//@property (nullable, nonatomic, copy) YYTextDebugOption *debugOption;
+//- (void)scrollRangeToVisible:(NSRange)range;
+//@property (nonatomic) NSRange selectedRange;
+//@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputView;
+//@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputAccessoryView;
+//@property (nonatomic) CGFloat extraAccessoryViewHeight;
+//@end
+#endif // !TARGET_INTERFACE_BUILDER
+
+
+// Notifications, see UITextView's documentation for more information.
+UIKIT_EXTERN NSString *const YYTextViewTextDidBeginEditingNotification;
+UIKIT_EXTERN NSString *const YYTextViewTextDidChangeNotification;
+UIKIT_EXTERN NSString *const YYTextViewTextDidEndEditingNotification;
+
+NS_ASSUME_NONNULL_END

+ 3832 - 0
Demo/Objective_C_Demo/YYText/YYTextView.m

@@ -0,0 +1,3832 @@
+//
+//  YYTextView.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  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 "YYTextView.h"
+#import "YYTextInput.h"
+#import "YYTextContainerView.h"
+#import "YYTextSelectionView.h"
+#import "YYTextMagnifier.h"
+#import "YYTextEffectWindow.h"
+#import "YYTextKeyboardManager.h"
+#import "YYTextUtilities.h"
+#import "YYTextTransaction.h"
+#import "YYTextWeakProxy.h"
+#import "NSAttributedString+YYText.h"
+#import "UIPasteboard+YYText.h"
+#import "UIView+YYText.h"
+
+
+static double _YYDeviceSystemVersion() {
+    static double version;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        version = [UIDevice currentDevice].systemVersion.doubleValue;
+    });
+    return version;
+}
+
+#ifndef kSystemVersion
+#define kSystemVersion _YYDeviceSystemVersion()
+#endif
+
+#ifndef kiOS6Later
+#define kiOS6Later (kSystemVersion >= 6)
+#endif
+
+#ifndef kiOS7Later
+#define kiOS7Later (kSystemVersion >= 7)
+#endif
+
+#ifndef kiOS8Later
+#define kiOS8Later (kSystemVersion >= 8)
+#endif
+
+#ifndef kiOS9Later
+#define kiOS9Later (kSystemVersion >= 9)
+#endif
+
+
+
+#define kDefaultUndoLevelMax 20 // Default maximum undo level
+
+#define kAutoScrollMinimumDuration 0.1 // Time in seconds to tick auto-scroll.
+#define kLongPressMinimumDuration 0.5 // Time in seconds the fingers must be held down for long press gesture.
+#define kLongPressAllowableMovement 10.0 // Maximum movement in points allowed before the long press fails.
+
+#define kMagnifierRangedTrackFix -6.0 // Magnifier ranged offset fix.
+#define kMagnifierRangedPopoverOffset 4.0 // Magnifier ranged popover offset.
+#define kMagnifierRangedCaptureOffset -6.0 // Magnifier ranged capture center offset.
+
+#define kHighlightFadeDuration 0.15 // Time in seconds for highlight fadeout animation.
+
+#define kDefaultInset UIEdgeInsetsMake(6, 4, 6, 4)
+#define kDefaultVerticalInset UIEdgeInsetsMake(4, 6, 4, 6)
+
+
+NSString *const YYTextViewTextDidBeginEditingNotification = @"YYTextViewTextDidBeginEditing";
+NSString *const YYTextViewTextDidChangeNotification = @"YYTextViewTextDidChange";
+NSString *const YYTextViewTextDidEndEditingNotification = @"YYTextViewTextDidEndEditing";
+
+
+typedef NS_ENUM (NSUInteger, YYTextGrabberDirection) {
+    kStart = 1,
+    kEnd   = 2,
+};
+
+typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
+    kLeft   = 1,
+    kTop    = 2,
+    kRight  = 3,
+    kBottom = 4,
+};
+
+
+/// An object that captures the state of the text view. Used for undo and redo.
+@interface _YYTextViewUndoObject : NSObject
+@property (nonatomic, strong) NSAttributedString *text;
+@property (nonatomic, assign) NSRange selectedRange;
+@end
+@implementation _YYTextViewUndoObject
++ (instancetype)objectWithText:(NSAttributedString *)text range:(NSRange)range {
+    _YYTextViewUndoObject *obj = [self new];
+    obj.text = text ? text : [NSAttributedString new];
+    obj.selectedRange = range;
+    return obj;
+}
+@end
+
+
+@interface YYTextView () <UIScrollViewDelegate, UIAlertViewDelegate, YYTextDebugTarget, YYTextKeyboardObserver> {
+    
+    YYTextRange *_selectedTextRange; /// nonnull
+    YYTextRange *_markedTextRange;
+    
+    __weak id<YYTextViewDelegate> _outerDelegate;
+    
+    UIImageView *_placeHolderView;
+    
+    NSMutableAttributedString *_innerText; ///< nonnull, inner attributed text
+    NSMutableAttributedString *_delectedText; ///< detected text for display
+    YYTextContainer *_innerContainer; ///< nonnull, inner text container
+    YYTextLayout *_innerLayout; ///< inner text layout, the text in this layout is longer than `_innerText` by appending '\n'
+    
+    YYTextContainerView *_containerView; ///< nonnull
+    YYTextSelectionView *_selectionView; ///< nonnull
+    YYTextMagnifier *_magnifierCaret; ///< nonnull
+    YYTextMagnifier *_magnifierRanged; ///< nonnull
+    
+    NSMutableAttributedString *_typingAttributesHolder; ///< nonnull, typing attributes
+    NSDataDetector *_dataDetector;
+    CGFloat _magnifierRangedOffset;
+    
+    NSRange _highlightRange; ///< current highlight range
+    YYTextHighlight *_highlight; ///< highlight attribute in `_highlightRange`
+    YYTextLayout *_highlightLayout; ///< when _state.showingHighlight=YES, this layout should be displayed
+    YYTextRange *_trackingRange; ///< the range in _innerLayout, may out of _innerText.
+    
+    BOOL _insetModifiedByKeyboard; ///< text is covered by keyboard, and the contentInset is modified
+    UIEdgeInsets _originalContentInset; ///< the original contentInset before modified
+    UIEdgeInsets _originalScrollIndicatorInsets; ///< the original scrollIndicatorInsets before modified
+    
+    NSTimer *_longPressTimer;
+    NSTimer *_autoScrollTimer;
+    CGFloat _autoScrollOffset; ///< current auto scroll offset which shoud add to scroll view
+    NSInteger _autoScrollAcceleration; ///< an acceleration coefficient for auto scroll
+    NSTimer *_selectionDotFixTimer; ///< fix the selection dot in window if the view is moved by parents
+    CGPoint _previousOriginInWindow;
+    
+    CGPoint _touchBeganPoint;
+    CGPoint _trackingPoint;
+    NSTimeInterval _touchBeganTime;
+    NSTimeInterval _trackingTime;
+    
+    NSMutableArray *_undoStack;
+    NSMutableArray *_redoStack;
+    NSRange _lastTypeRange;
+    
+    struct {
+        unsigned int trackingGrabber : 2;       ///< YYTextGrabberDirection, current tracking grabber
+        unsigned int trackingCaret : 1;         ///< track the caret
+        unsigned int trackingPreSelect : 1;     ///< track pre-select
+        unsigned int trackingTouch : 1;         ///< is in touch phase
+        unsigned int swallowTouch : 1;          ///< don't forward event to next responder
+        unsigned int touchMoved : 3;            ///< YYTextMoveDirection, move direction after touch began
+        unsigned int selectedWithoutEdit : 1;   ///< show selected range but not first responder
+        unsigned int deleteConfirm : 1;         ///< delete a binding text range
+        unsigned int ignoreFirstResponder : 1;  ///< ignore become first responder temporary
+        unsigned int ignoreTouchBegan : 1;      ///< ignore begin tracking touch temporary
+        
+        unsigned int showingMagnifierCaret : 1;
+        unsigned int showingMagnifierRanged : 1;
+        unsigned int showingMenu : 1;
+        unsigned int showingHighlight : 1;
+        
+        unsigned int typingAttributesOnce : 1;  ///< apply the typing attributes once
+        unsigned int clearsOnInsertionOnce : 1; ///< select all once when become first responder
+        unsigned int autoScrollTicked : 1;      ///< auto scroll did tick scroll at this timer period
+        unsigned int firstShowDot : 1;          ///< the selection grabber dot has displayed at least once
+        unsigned int needUpdate : 1;            ///< the layout or selection view is 'dirty' and need update
+        unsigned int placeholderNeedUpdate : 1; ///< the placeholder need update it's contents
+        
+        unsigned int insideUndoBlock : 1;
+        unsigned int firstResponderBeforeUndoAlert : 1;
+    } _state;
+}
+
+@end
+
+
+@implementation YYTextView
+
+#pragma mark - @protocol UITextInputTraits
+@synthesize autocapitalizationType = _autocapitalizationType;
+@synthesize autocorrectionType = _autocorrectionType;
+@synthesize spellCheckingType = _spellCheckingType;
+@synthesize keyboardType = _keyboardType;
+@synthesize keyboardAppearance = _keyboardAppearance;
+@synthesize returnKeyType = _returnKeyType;
+@synthesize enablesReturnKeyAutomatically = _enablesReturnKeyAutomatically;
+@synthesize secureTextEntry = _secureTextEntry;
+
+#pragma mark - @protocol UITextInput
+@synthesize selectedTextRange = _selectedTextRange;  //copy nonnull (YYTextRange*)
+@synthesize markedTextRange = _markedTextRange;      //readonly     (YYTextRange*)
+@synthesize markedTextStyle = _markedTextStyle;      //copy
+@synthesize inputDelegate = _inputDelegate;         //assign
+@synthesize tokenizer = _tokenizer;                 //readonly
+
+#pragma mark - @protocol UITextInput optional
+@synthesize selectionAffinity = _selectionAffinity;
+
+
+#pragma mark - Private
+
+/// Update layout and selection before runloop sleep/end.
+- (void)_commitUpdate {
+#if !TARGET_INTERFACE_BUILDER
+    _state.needUpdate = YES;
+    [[YYTextTransaction transactionWithTarget:self selector:@selector(_updateIfNeeded)] commit];
+#else
+    [self _update];
+#endif
+}
+
+/// Update layout and selection view if needed.
+- (void)_updateIfNeeded {
+    if (_state.needUpdate) {
+        [self _update];
+    }
+}
+
+/// Update layout and selection view immediately.
+- (void)_update {
+    _state.needUpdate = NO;
+    [self _updateLayout];
+    [self _updateSelectionView];
+}
+
+/// Update layout immediately.
+- (void)_updateLayout {
+    NSMutableAttributedString *text = _innerText.mutableCopy;
+    _placeHolderView.hidden = text.length > 0;
+    if ([self _detectText:text]) {
+        _delectedText = text;
+    } else {
+        _delectedText = nil;
+    }
+    [text replaceCharactersInRange:NSMakeRange(text.length, 0) withString:@"\r"]; // add for nextline caret
+    [text yy_removeDiscontinuousAttributesInRange:NSMakeRange(_innerText.length, 1)];
+    [text removeAttribute:YYTextBorderAttributeName range:NSMakeRange(_innerText.length, 1)];
+    [text removeAttribute:YYTextBackgroundBorderAttributeName range:NSMakeRange(_innerText.length, 1)];
+    if (_innerText.length == 0) {
+        [text yy_setAttributes:_typingAttributesHolder.yy_attributes]; // add for empty text caret
+    }
+    if (_selectedTextRange.end.offset == _innerText.length) {
+        [_typingAttributesHolder.yy_attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+            [text yy_setAttribute:key value:value range:NSMakeRange(_innerText.length, 1)];
+        }];
+    }
+    
+    _innerLayout = [YYTextLayout layoutWithContainer:_innerContainer text:text];
+    CGSize size = [_innerLayout textBoundingSize];
+    CGSize visibleSize = [self _getVisibleSize];
+    if (_innerContainer.isVerticalForm) {
+        size.height = visibleSize.height;
+        if (size.width < visibleSize.width) size.width = visibleSize.width;
+    } else {
+        size.width = visibleSize.width;
+    }
+    
+    [_containerView setLayout:_innerLayout withFadeDuration:0];
+    _containerView.frame = (CGRect){.size = size};
+    _state.showingHighlight = NO;
+    self.contentSize = size;
+}
+
+/// Update selection view immediately.
+/// This method should be called after "layout update" finished.
+- (void)_updateSelectionView {
+    _selectionView.frame = _containerView.frame;
+    _selectionView.caretBlinks = NO;
+    _selectionView.caretVisible = NO;
+    _selectionView.selectionRects = nil;
+    [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
+    if (!_innerLayout) return;
+    
+    NSMutableArray *allRects = [NSMutableArray new];
+    BOOL containsDot = NO;
+    
+    YYTextRange *selectedRange = _selectedTextRange;
+    if (_state.trackingTouch && _trackingRange) {
+        selectedRange = _trackingRange;
+    }
+    
+    if (_markedTextRange) {
+        NSArray *rects = [_innerLayout selectionRectsWithoutStartAndEndForRange:_markedTextRange];
+        if (rects) [allRects addObjectsFromArray:rects];
+        if (selectedRange.asRange.length > 0) {
+            rects = [_innerLayout selectionRectsWithOnlyStartAndEndForRange:selectedRange];
+            if (rects) [allRects addObjectsFromArray:rects];
+            containsDot = rects.count > 0;
+        } else {
+            CGRect rect = [_innerLayout caretRectForPosition:selectedRange.end];
+            _selectionView.caretRect = [self _convertRectFromLayout:rect];
+            _selectionView.caretVisible = YES;
+            _selectionView.caretBlinks = YES;
+        }
+    } else {
+        if (selectedRange.asRange.length == 0) { // only caret
+            if (self.isFirstResponder || _state.trackingPreSelect) {
+                CGRect rect = [_innerLayout caretRectForPosition:selectedRange.end];
+                _selectionView.caretRect = [self _convertRectFromLayout:rect];
+                _selectionView.caretVisible = YES;
+                if (!_state.trackingCaret && !_state.trackingPreSelect) {
+                    _selectionView.caretBlinks = YES;
+                }
+            }
+        } else { // range selected
+            if ((self.isFirstResponder && !_state.deleteConfirm) ||
+                (!self.isFirstResponder && _state.selectedWithoutEdit)) {
+                NSArray *rects = [_innerLayout selectionRectsForRange:selectedRange];
+                if (rects) [allRects addObjectsFromArray:rects];
+                containsDot = rects.count > 0;
+            } else if ((!self.isFirstResponder && _state.trackingPreSelect) ||
+                       (self.isFirstResponder && _state.deleteConfirm)){
+                NSArray *rects = [_innerLayout selectionRectsWithoutStartAndEndForRange:selectedRange];
+                if (rects) [allRects addObjectsFromArray:rects];
+            }
+        }
+    }
+    [allRects enumerateObjectsUsingBlock:^(YYTextSelectionRect *rect, NSUInteger idx, BOOL *stop) {
+        rect.rect = [self _convertRectFromLayout:rect.rect];
+    }];
+    _selectionView.selectionRects = allRects;
+    if (!_state.firstShowDot && containsDot) {
+        _state.firstShowDot = YES;
+        /*
+         The dot position may be wrong at the first time displayed.
+         I can't find the reason. Here's a workaround.
+         */
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.02 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+        });
+    }
+    [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+    
+    if (containsDot) {
+        [self _startSelectionDotFixTimer];
+    } else {
+        [self _endSelectionDotFixTimer];
+    }
+}
+
+/// Update inner contains's size.
+- (void)_updateInnerContainerSize {
+    CGSize size = [self _getVisibleSize];
+    if (_innerContainer.isVerticalForm) size.width = CGFLOAT_MAX;
+    else size.height = CGFLOAT_MAX;
+    _innerContainer.size = size;
+}
+
+/// Update placeholder before runloop sleep/end.
+- (void)_commitPlaceholderUpdate {
+#if !TARGET_INTERFACE_BUILDER
+    _state.placeholderNeedUpdate = YES;
+    [[YYTextTransaction transactionWithTarget:self selector:@selector(_updatePlaceholderIfNeeded)] commit];
+#else
+    [self _updatePlaceholder];
+#endif
+}
+
+/// Update placeholder if needed.
+- (void)_updatePlaceholderIfNeeded {
+    if (_state.placeholderNeedUpdate) {
+        _state.placeholderNeedUpdate = NO;
+        [self _updatePlaceholder];
+    }
+}
+
+/// Update placeholder immediately.
+- (void)_updatePlaceholder {
+    CGRect frame = CGRectZero;
+    _placeHolderView.image = nil;
+    _placeHolderView.frame = frame;
+    if (_placeholderAttributedText.length > 0) {
+        YYTextContainer *container = _innerContainer.copy;
+        container.size = self.bounds.size;
+        container.truncationType = YYTextTruncationTypeEnd;
+        container.truncationToken = nil;
+        YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_placeholderAttributedText];
+        CGSize size = [layout textBoundingSize];
+        BOOL needDraw = size.width > 1 && size.height > 1;
+        if (needDraw) {
+            UIGraphicsBeginImageContextWithOptions(size, NO, 0);
+            CGContextRef context = UIGraphicsGetCurrentContext();
+            [layout drawInContext:context size:size debug:self.debugOption];
+            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+            UIGraphicsEndImageContext();
+            _placeHolderView.image = image;
+            frame.size = image.size;
+            if (container.isVerticalForm) {
+                frame.origin.x = self.bounds.size.width - image.size.width;
+            } else {
+                frame.origin = CGPointZero;
+            }
+            _placeHolderView.frame = frame;
+        }
+    }
+}
+
+/// Update the `_selectedTextRange` to a single position by `_trackingPoint`.
+- (void)_updateTextRangeByTrackingCaret {
+    if (!_state.trackingTouch) return;
+    
+    CGPoint trackingPoint = [self _convertPointToLayout:_trackingPoint];
+    YYTextPosition *newPos = [_innerLayout closestPositionToPoint:trackingPoint];
+    if (newPos) {
+        newPos = [self _correctedTextPosition:newPos];
+        if (_markedTextRange) {
+            if ([newPos compare:_markedTextRange.start] == NSOrderedAscending) {
+                newPos = _markedTextRange.start;
+            } else if ([newPos compare:_markedTextRange.end] == NSOrderedDescending) {
+                newPos = _markedTextRange.end;
+            }
+        }
+        YYTextRange *newRange = [YYTextRange rangeWithRange:NSMakeRange(newPos.offset, 0) affinity:newPos.affinity];
+        _trackingRange = newRange;
+    }
+}
+
+/// Update the `_selectedTextRange` to a new range by `_trackingPoint` and `_state.trackingGrabber`.
+- (void)_updateTextRangeByTrackingGrabber {
+    if (!_state.trackingTouch || !_state.trackingGrabber) return;
+    
+    BOOL isStart = _state.trackingGrabber == kStart;
+    CGPoint magPoint = _trackingPoint;
+    magPoint.y += kMagnifierRangedTrackFix;
+    magPoint = [self _convertPointToLayout:magPoint];
+    YYTextPosition *position = [_innerLayout positionForPoint:magPoint
+                                                  oldPosition:(isStart ? _selectedTextRange.start : _selectedTextRange.end)
+                                                otherPosition:(isStart ? _selectedTextRange.end : _selectedTextRange.start)];
+    if (position) {
+        position = [self _correctedTextPosition:position];
+        if ((NSUInteger)position.offset > _innerText.length) {
+            position = [YYTextPosition positionWithOffset:_innerText.length];
+        }
+        YYTextRange *newRange = [YYTextRange rangeWithStart:(isStart ? position : _selectedTextRange.start)
+                                                        end:(isStart ? _selectedTextRange.end : position)];
+        _trackingRange = newRange;
+    }
+}
+
+/// Update the `_selectedTextRange` to a new range/position by `_trackingPoint`.
+- (void)_updateTextRangeByTrackingPreSelect {
+    if (!_state.trackingTouch) return;
+    YYTextRange *newRange = [self _getClosestTokenRangeAtPoint:_trackingPoint];
+    _trackingRange = newRange;
+}
+
+/// Show or update `_magnifierCaret` based on `_trackingPoint`, and hide `_magnifierRange`.
+- (void)_showMagnifierCaret {
+    if (YYTextIsAppExtension()) return;
+    
+    if (_state.showingMagnifierRanged) {
+        _state.showingMagnifierRanged = NO;
+        [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
+    }
+    
+    _magnifierCaret.hostPopoverCenter = _trackingPoint;
+    _magnifierCaret.hostCaptureCenter = _trackingPoint;
+    if (!_state.showingMagnifierCaret) {
+        _state.showingMagnifierCaret = YES;
+        [[YYTextEffectWindow sharedWindow] showMagnifier:_magnifierCaret];
+    } else {
+        [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret];
+    }
+}
+
+/// Show or update `_magnifierRanged` based on `_trackingPoint`, and hide `_magnifierCaret`.
+- (void)_showMagnifierRanged {
+    if (YYTextIsAppExtension()) return;
+    
+    if (_verticalForm) { // hack for vertical form...
+        [self _showMagnifierCaret];
+        return;
+    }
+    
+    if (_state.showingMagnifierCaret) {
+        _state.showingMagnifierCaret = NO;
+        [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
+    }
+    
+    CGPoint magPoint = _trackingPoint;
+    if (_verticalForm) {
+        magPoint.x += kMagnifierRangedTrackFix;
+    } else {
+        magPoint.y += kMagnifierRangedTrackFix;
+    }
+    
+    YYTextRange *selectedRange = _selectedTextRange;
+    if (_state.trackingTouch && _trackingRange) {
+        selectedRange = _trackingRange;
+    }
+    
+    YYTextPosition *position;
+    if (_markedTextRange) {
+        position = selectedRange.end;
+    } else {
+        position = [_innerLayout positionForPoint:[self _convertPointToLayout:magPoint]
+                                      oldPosition:(_state.trackingGrabber == kStart ? selectedRange.start : selectedRange.end)
+                                    otherPosition:(_state.trackingGrabber == kStart ? selectedRange.end : selectedRange.start)];
+    }
+    
+    NSUInteger lineIndex = [_innerLayout lineIndexForPosition:position];
+    if (lineIndex < _innerLayout.lines.count) {
+        YYTextLine *line = _innerLayout.lines[lineIndex];
+        CGRect lineRect = [self _convertRectFromLayout:line.bounds];
+        if (_verticalForm) {
+            magPoint.x = YYTEXT_CLAMP(magPoint.x, CGRectGetMinX(lineRect), CGRectGetMaxX(lineRect));
+        } else {
+            magPoint.y = YYTEXT_CLAMP(magPoint.y, CGRectGetMinY(lineRect), CGRectGetMaxY(lineRect));
+        }
+        CGPoint linePoint = [_innerLayout linePositionForPosition:position];
+        linePoint = [self _convertPointFromLayout:linePoint];
+        
+        CGPoint popoverPoint = linePoint;
+        if (_verticalForm) {
+            popoverPoint.x = linePoint.x + _magnifierRangedOffset;
+        } else {
+            popoverPoint.y = linePoint.y + _magnifierRangedOffset;
+        }
+        
+        CGPoint capturePoint;
+        if (_verticalForm) {
+            capturePoint.x = linePoint.x + kMagnifierRangedCaptureOffset;
+            capturePoint.y = linePoint.y;
+        } else {
+            capturePoint.x = linePoint.x;
+            capturePoint.y = linePoint.y + kMagnifierRangedCaptureOffset;
+        }
+        
+        _magnifierRanged.hostPopoverCenter = popoverPoint;
+        _magnifierRanged.hostCaptureCenter = capturePoint;
+        if (!_state.showingMagnifierRanged) {
+            _state.showingMagnifierRanged = YES;
+            [[YYTextEffectWindow sharedWindow] showMagnifier:_magnifierRanged];
+        } else {
+            [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged];
+        }
+    }
+}
+
+/// Update the showing magnifier.
+- (void)_updateMagnifier {
+    if (YYTextIsAppExtension()) return;
+    
+    if (_state.showingMagnifierCaret) {
+        [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret];
+    }
+    if (_state.showingMagnifierRanged) {
+        [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged];
+    }
+}
+
+/// Hide the `_magnifierCaret` and `_magnifierRanged`.
+- (void)_hideMagnifier {
+    if (YYTextIsAppExtension()) return;
+    
+    if (_state.showingMagnifierCaret || _state.showingMagnifierRanged) {
+        // disable touch began temporary to ignore caret animation overlap
+        _state.ignoreTouchBegan = YES;
+        __weak typeof(self) _self = self;
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            __strong typeof(_self) self = _self;
+            if (self) self->_state.ignoreTouchBegan = NO;
+        });
+    }
+    
+    if (_state.showingMagnifierCaret) {
+        _state.showingMagnifierCaret = NO;
+        [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
+    }
+    if (_state.showingMagnifierRanged) {
+        _state.showingMagnifierRanged = NO;
+        [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
+    }
+}
+
+/// Show and update the UIMenuController.
+- (void)_showMenu {
+    CGRect rect;
+    if (_selectionView.caretVisible) {
+        rect = _selectionView.caretView.frame;
+    } else if (_selectionView.selectionRects.count > 0) {
+        YYTextSelectionRect *sRect = _selectionView.selectionRects.firstObject;
+        rect = sRect.rect;
+        for (NSUInteger i = 1; i < _selectionView.selectionRects.count; i++) {
+            sRect = _selectionView.selectionRects[i];
+            rect = CGRectUnion(rect, sRect.rect);
+        }
+        
+        CGRect inter = CGRectIntersection(rect, self.bounds);
+        if (!CGRectIsNull(inter) && inter.size.height > 1) {
+            rect = inter; //clip to bounds
+        } else {
+            if (CGRectGetMinY(rect) < CGRectGetMinY(self.bounds)) {
+                rect.size.height = 1;
+                rect.origin.y = CGRectGetMinY(self.bounds);
+            } else {
+                rect.size.height = 1;
+                rect.origin.y = CGRectGetMaxY(self.bounds);
+            }
+        }
+        
+        YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
+        if (mgr.keyboardVisible) {
+            CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
+            CGRect kbInter = CGRectIntersection(rect, kbRect);
+            if (!CGRectIsNull(kbInter) && kbInter.size.height > 1 && kbInter.size.width > 1) {
+                // self is covered by keyboard
+                if (CGRectGetMinY(kbInter) > CGRectGetMinY(rect)) { // keyboard at bottom
+                    rect.size.height -= kbInter.size.height;
+                } else if (CGRectGetMaxY(kbInter) < CGRectGetMaxY(rect)) { // keyboard at top
+                    rect.origin.y += kbInter.size.height;
+                    rect.size.height -= kbInter.size.height;
+                }
+            }
+        }
+    } else {
+        rect = _selectionView.bounds;
+    }
+    
+    if (!self.isFirstResponder) {
+        if (!_containerView.isFirstResponder) {
+            [_containerView becomeFirstResponder];
+        }
+    }
+    
+    if (self.isFirstResponder || _containerView.isFirstResponder) {
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            UIMenuController *menu = [UIMenuController sharedMenuController];
+            [menu setTargetRect:CGRectStandardize(rect) inView:_selectionView];
+            [menu update];
+            if (!_state.showingMenu || !menu.menuVisible) {
+                _state.showingMenu = YES;
+                [menu setMenuVisible:YES animated:YES];
+            }
+        });
+    }
+}
+
+/// Hide the UIMenuController.
+- (void)_hideMenu {
+    if (_state.showingMenu) {
+        _state.showingMenu = NO;
+        UIMenuController *menu = [UIMenuController sharedMenuController];
+        [menu setMenuVisible:NO animated:YES];
+    }
+    if (_containerView.isFirstResponder) {
+        _state.ignoreFirstResponder = YES;
+        [_containerView resignFirstResponder]; // it will call [self becomeFirstResponder], ignore it temporary.
+        _state.ignoreFirstResponder = NO;
+    }
+}
+
+/// Show highlight layout based on `_highlight` and `_highlightRange`
+/// If the `_highlightLayout` is nil, try to create.
+- (void)_showHighlightAnimated:(BOOL)animated {
+    NSTimeInterval fadeDuration = animated ? kHighlightFadeDuration : 0;
+    if (!_highlight) return;
+    if (!_highlightLayout) {
+        NSMutableAttributedString *hiText = (_delectedText ? _delectedText : _innerText).mutableCopy;
+        NSDictionary *newAttrs = _highlight.attributes;
+        [newAttrs enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+            [hiText yy_setAttribute:key value:value range:_highlightRange];
+        }];
+        _highlightLayout = [YYTextLayout layoutWithContainer:_innerContainer text:hiText];
+        if (!_highlightLayout) _highlight = nil;
+    }
+    
+    if (_highlightLayout && !_state.showingHighlight) {
+        _state.showingHighlight = YES;
+        [_containerView setLayout:_highlightLayout withFadeDuration:fadeDuration];
+    }
+}
+
+/// Show `_innerLayout` instead of `_highlightLayout`.
+/// It does not destory the `_highlightLayout`.
+- (void)_hideHighlightAnimated:(BOOL)animated {
+    NSTimeInterval fadeDuration = animated ? kHighlightFadeDuration : 0;
+    if (_state.showingHighlight) {
+        _state.showingHighlight = NO;
+        [_containerView setLayout:_innerLayout withFadeDuration:fadeDuration];
+    }
+}
+
+/// Show `_innerLayout` and destory the `_highlight` and `_highlightLayout`.
+- (void)_removeHighlightAnimated:(BOOL)animated {
+    [self _hideHighlightAnimated:animated];
+    _highlight = nil;
+    _highlightLayout = nil;
+}
+
+/// Scroll current selected range to visible.
+- (void)_scrollSelectedRangeToVisible {
+    [self _scrollRangeToVisible:_selectedTextRange];
+}
+
+/// Scroll range to visible, take account into keyboard and insets.
+- (void)_scrollRangeToVisible:(YYTextRange *)range {
+    if (!range) return;
+    CGRect rect = [_innerLayout rectForRange:range];
+    if (CGRectIsNull(rect)) return;
+    rect = [self _convertRectFromLayout:rect];
+    rect = [_containerView convertRect:rect toView:self];
+    
+    if (rect.size.width < 1) rect.size.width = 1;
+    if (rect.size.height < 1) rect.size.height = 1;
+    CGFloat extend = 3;
+    
+    BOOL insetModified = NO;
+    YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
+    
+    if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) {
+        CGRect bounds = self.bounds;
+        bounds.origin = CGPointZero;
+        CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
+        kbRect.origin.y -= _extraAccessoryViewHeight;
+        kbRect.size.height += _extraAccessoryViewHeight;
+        
+        kbRect.origin.x -= self.contentOffset.x;
+        kbRect.origin.y -= self.contentOffset.y;
+        CGRect inter = CGRectIntersection(bounds, kbRect);
+        if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > extend) { // self is covered by keyboard
+            if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) { // keyboard below self.top
+                
+                UIEdgeInsets originalContentInset = self.contentInset;
+                UIEdgeInsets originalScrollIndicatorInsets = self.scrollIndicatorInsets;
+                if (_insetModifiedByKeyboard) {
+                    originalContentInset = _originalContentInset;
+                    originalScrollIndicatorInsets = _originalScrollIndicatorInsets;
+                }
+                
+                if (originalContentInset.bottom < inter.size.height + extend) {
+                    insetModified = YES;
+                    if (!_insetModifiedByKeyboard) {
+                        _insetModifiedByKeyboard = YES;
+                        _originalContentInset = self.contentInset;
+                        _originalScrollIndicatorInsets = self.scrollIndicatorInsets;
+                    }
+                    UIEdgeInsets newInset = originalContentInset;
+                    UIEdgeInsets newIndicatorInsets = originalScrollIndicatorInsets;
+                    newInset.bottom = inter.size.height + extend;
+                    newIndicatorInsets.bottom = newInset.bottom;
+                    UIViewAnimationOptions curve;
+                    if (kiOS7Later) {
+                        curve = 7 << 16;
+                    } else {
+                        curve = UIViewAnimationOptionCurveEaseInOut;
+                    }
+                    [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | curve animations:^{
+                        [super setContentInset:newInset];
+                        [super setScrollIndicatorInsets:newIndicatorInsets];
+                        [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO];
+                    } completion:NULL];
+                }
+            }
+        }
+    }
+    if (!insetModified) {
+        [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut animations:^{
+            [self _restoreInsetsAnimated:NO];
+            [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO];
+        } completion:NULL];
+    }
+}
+
+/// Restore contents insets if modified by keyboard.
+- (void)_restoreInsetsAnimated:(BOOL)animated {
+    if (_insetModifiedByKeyboard) {
+        _insetModifiedByKeyboard = NO;
+        if (animated) {
+            [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut  animations:^{
+                [super setContentInset:_originalContentInset];
+                [super setScrollIndicatorInsets:_originalScrollIndicatorInsets];
+            } completion:NULL];
+        } else {
+            [super setContentInset:_originalContentInset];
+            [super setScrollIndicatorInsets:_originalScrollIndicatorInsets];
+        }
+    }
+}
+
+/// Keyboard frame changed, scroll the caret to visible range, or modify the content insets.
+- (void)_keyboardChanged {
+    if (!self.isFirstResponder) return;
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+        if ([YYTextKeyboardManager defaultManager].keyboardVisible) {
+            [self _scrollRangeToVisible:_selectedTextRange];
+        } else {
+            [self _restoreInsetsAnimated:YES];
+        }
+        [self _updateMagnifier];
+        if (_state.showingMenu) {
+            [self _showMenu];
+        }
+    });
+}
+
+/// Start long press timer, used for 'highlight' range text action.
+- (void)_startLongPressTimer {
+    [_longPressTimer invalidate];
+    _longPressTimer = [NSTimer timerWithTimeInterval:kLongPressMinimumDuration
+                                              target:[YYTextWeakProxy proxyWithTarget:self]
+                                            selector:@selector(_trackDidLongPress)
+                                            userInfo:nil
+                                             repeats:NO];
+    [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes];
+}
+
+/// Invalidate the long press timer.
+- (void)_endLongPressTimer {
+    [_longPressTimer invalidate];
+    _longPressTimer = nil;
+}
+
+/// Long press detected.
+- (void)_trackDidLongPress {
+    [self _endLongPressTimer];
+    
+    BOOL dealLongPressAction = NO;
+    if (_state.showingHighlight) {
+        [self _hideMenu];
+        
+        if (_highlight.longPressAction) {
+            dealLongPressAction = YES;
+            CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
+            rect = [self _convertRectFromLayout:rect];
+            _highlight.longPressAction(self, _innerText, _highlightRange, rect);
+            [self _endTouchTracking];
+        } else {
+            BOOL shouldHighlight = YES;
+            if ([self.delegate respondsToSelector:@selector(textView:shouldLongPressHighlight:inRange:)]) {
+                shouldHighlight = [self.delegate textView:self shouldLongPressHighlight:_highlight inRange:_highlightRange];
+            }
+            if (shouldHighlight && [self.delegate respondsToSelector:@selector(textView:didLongPressHighlight:inRange:rect:)]) {
+                dealLongPressAction = YES;
+                CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
+                rect = [self _convertRectFromLayout:rect];
+                [self.delegate textView:self didLongPressHighlight:_highlight inRange:_highlightRange rect:rect];
+                [self _endTouchTracking];
+            }
+        }
+    }
+    
+    if (!dealLongPressAction){
+        [self _removeHighlightAnimated:NO];
+        if (_state.trackingTouch) {
+            if (_state.trackingGrabber) {
+                self.panGestureRecognizer.enabled = NO;
+                [self _hideMenu];
+                [self _showMagnifierRanged];
+            } else if (self.isFirstResponder){
+                self.panGestureRecognizer.enabled = NO;
+                _selectionView.caretBlinks = NO;
+                _state.trackingCaret = YES;
+                CGPoint trackingPoint = [self _convertPointToLayout:_trackingPoint];
+                YYTextPosition *newPos = [_innerLayout closestPositionToPoint:trackingPoint];
+                newPos = [self _correctedTextPosition:newPos];
+                if (newPos) {
+                    if (_markedTextRange) {
+                        if ([newPos compare:_markedTextRange.start] != NSOrderedDescending) {
+                            newPos = _markedTextRange.start;
+                        } else if ([newPos compare:_markedTextRange.end] != NSOrderedAscending) {
+                            newPos = _markedTextRange.end;
+                        }
+                    }
+                    _trackingRange = [YYTextRange rangeWithRange:NSMakeRange(newPos.offset, 0) affinity:newPos.affinity];
+                    [self _updateSelectionView];
+                }
+                [self _hideMenu];
+                
+                if (_markedTextRange) {
+                    [self _showMagnifierRanged];
+                } else {
+                    [self _showMagnifierCaret];
+                }
+            } else if (self.selectable) {
+                self.panGestureRecognizer.enabled = NO;
+                _state.trackingPreSelect = YES;
+                _state.selectedWithoutEdit = NO;
+                [self _updateTextRangeByTrackingPreSelect];
+                [self _updateSelectionView];
+                [self _showMagnifierCaret];
+            }
+        }
+    }
+}
+
+/// Start auto scroll timer, used for auto scroll tick.
+- (void)_startAutoScrollTimer {
+    if (!_autoScrollTimer) {
+        [_autoScrollTimer invalidate];
+        _autoScrollTimer = [NSTimer timerWithTimeInterval:kAutoScrollMinimumDuration
+                                                   target:[YYTextWeakProxy proxyWithTarget:self]
+                                                 selector:@selector(_trackDidTickAutoScroll)
+                                                 userInfo:nil
+                                                  repeats:YES];
+        [[NSRunLoop currentRunLoop] addTimer:_autoScrollTimer forMode:NSRunLoopCommonModes];
+    }
+}
+
+/// Invalidate the auto scroll, and restore the text view state.
+- (void)_endAutoScrollTimer {
+    if (_state.autoScrollTicked) [self flashScrollIndicators];
+    [_autoScrollTimer invalidate];
+    _autoScrollTimer = nil;
+    _autoScrollOffset = 0;
+    _autoScrollAcceleration = 0;
+    _state.autoScrollTicked = NO;
+    
+    if (_magnifierCaret.captureDisabled) {
+        _magnifierCaret.captureDisabled = NO;
+        if (_state.showingMagnifierCaret) {
+            [self _showMagnifierCaret];
+        }
+    }
+    if (_magnifierRanged.captureDisabled) {
+        _magnifierRanged.captureDisabled = NO;
+        if (_state.showingMagnifierRanged) {
+            [self _showMagnifierRanged];
+        }
+    }
+}
+
+/// Auto scroll ticked by timer.
+- (void)_trackDidTickAutoScroll {
+    if (_autoScrollOffset != 0) {
+        _magnifierCaret.captureDisabled = YES;
+        _magnifierRanged.captureDisabled = YES;
+        
+        CGPoint offset = self.contentOffset;
+        if (_verticalForm) {
+            offset.x += _autoScrollOffset;
+            
+            if (_autoScrollAcceleration > 0) {
+                offset.x += ((_autoScrollOffset > 0 ? 1 : -1) * _autoScrollAcceleration * _autoScrollAcceleration * 0.5);
+            }
+            _autoScrollAcceleration++;
+            offset.x = round(offset.x);
+            if (_autoScrollOffset < 0) {
+                if (offset.x < -self.contentInset.left) offset.x = -self.contentInset.left;
+            } else {
+                CGFloat maxOffsetX = self.contentSize.width - self.bounds.size.width + self.contentInset.right;
+                if (offset.x > maxOffsetX) offset.x = maxOffsetX;
+            }
+            if (offset.x < -self.contentInset.left) offset.x = -self.contentInset.left;
+        } else {
+            offset.y += _autoScrollOffset;
+            if (_autoScrollAcceleration > 0) {
+                offset.y += ((_autoScrollOffset > 0 ? 1 : -1) * _autoScrollAcceleration * _autoScrollAcceleration * 0.5);
+            }
+            _autoScrollAcceleration++;
+            offset.y = round(offset.y);
+            if (_autoScrollOffset < 0) {
+                if (offset.y < -self.contentInset.top) offset.y = -self.contentInset.top;
+            } else {
+                CGFloat maxOffsetY = self.contentSize.height - self.bounds.size.height + self.contentInset.bottom;
+                if (offset.y > maxOffsetY) offset.y = maxOffsetY;
+            }
+            if (offset.y < -self.contentInset.top) offset.y = -self.contentInset.top;
+        }
+        
+        BOOL shouldScroll;
+        if (_verticalForm) {
+            shouldScroll = fabs(offset.x -self.contentOffset.x) > 0.5;
+        } else {
+            shouldScroll = fabs(offset.y -self.contentOffset.y) > 0.5;
+        }
+        
+        if (shouldScroll) {
+            _state.autoScrollTicked = YES;
+            _trackingPoint.x += offset.x - self.contentOffset.x;
+            _trackingPoint.y += offset.y - self.contentOffset.y;
+            [UIView animateWithDuration:kAutoScrollMinimumDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear animations:^{
+                [self setContentOffset:offset];
+            } completion:^(BOOL finished) {
+                if (_state.trackingTouch) {
+                    if (_state.trackingGrabber) {
+                        [self _showMagnifierRanged];
+                        [self _updateTextRangeByTrackingGrabber];
+                    } else if (_state.trackingPreSelect) {
+                        [self _showMagnifierCaret];
+                        [self _updateTextRangeByTrackingPreSelect];
+                    } else if (_state.trackingCaret) {
+                        if (_markedTextRange) {
+                            [self _showMagnifierRanged];
+                        } else {
+                            [self _showMagnifierCaret];
+                        }
+                        [self _updateTextRangeByTrackingCaret];
+                    }
+                    [self _updateSelectionView];
+                }
+            }];
+        } else {
+            [self _endAutoScrollTimer];
+        }
+    } else {
+        [self _endAutoScrollTimer];
+    }
+}
+
+/// End current touch tracking (if is tracking now), and update the state.
+- (void)_endTouchTracking {
+    if (!_state.trackingTouch) return;
+    
+    _state.trackingTouch = NO;
+    _state.trackingGrabber = NO;
+    _state.trackingCaret = NO;
+    _state.trackingPreSelect = NO;
+    _state.touchMoved = NO;
+    _state.deleteConfirm = NO;
+    _state.clearsOnInsertionOnce = NO;
+    _trackingRange = nil;
+    _selectionView.caretBlinks = YES;
+    
+    [self _removeHighlightAnimated:YES];
+    [self _hideMagnifier];
+    [self _endLongPressTimer];
+    [self _endAutoScrollTimer];
+    [self _updateSelectionView];
+    
+    self.panGestureRecognizer.enabled = self.scrollEnabled;
+}
+
+/// Start a timer to fix the selection dot.
+- (void)_startSelectionDotFixTimer {
+    [_selectionDotFixTimer invalidate];
+    _longPressTimer = [NSTimer timerWithTimeInterval:1/15.0
+                                              target:[YYTextWeakProxy proxyWithTarget:self]
+                                            selector:@selector(_fixSelectionDot)
+                                            userInfo:nil
+                                             repeats:NO];
+    [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes];
+}
+
+/// End the timer.
+- (void)_endSelectionDotFixTimer {
+    [_selectionDotFixTimer invalidate];
+    _selectionDotFixTimer = nil;
+}
+
+/// If it shows selection grabber and this view was moved by super view,
+/// update the selection dot in window.
+- (void)_fixSelectionDot {
+    if (YYTextIsAppExtension()) return;
+    CGPoint origin = [self yy_convertPoint:CGPointZero toViewOrWindow:[YYTextEffectWindow sharedWindow]];
+    if (!CGPointEqualToPoint(origin, _previousOriginInWindow)) {
+        _previousOriginInWindow = origin;
+        [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
+        [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+    }
+}
+
+/// Try to get the character range/position with word granularity from the tokenizer.
+- (YYTextRange *)_getClosestTokenRangeAtPosition:(YYTextPosition *)position {
+    position = [self _correctedTextPosition:position];
+    if (!position) return nil;
+    YYTextRange *range = nil;
+    if (_tokenizer) {
+        range = (id)[_tokenizer rangeEnclosingPosition:position withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward];
+        if (range.asRange.length == 0) {
+            range = (id)[_tokenizer rangeEnclosingPosition:position withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward];
+        }
+    }
+    
+    if (range.asRange.length == 0) {
+        range = [_innerLayout textRangeByExtendingPosition:position inDirection:UITextLayoutDirectionRight offset:1];
+        range = [self _correctedTextRange:range];
+        if (range.asRange.length == 0) {
+            range = [_innerLayout textRangeByExtendingPosition:position inDirection:UITextLayoutDirectionLeft offset:1];
+            range = [self _correctedTextRange:range];
+        }
+    } else {
+        YYTextRange *extStart = [_innerLayout textRangeByExtendingPosition:range.start];
+        YYTextRange *extEnd = [_innerLayout textRangeByExtendingPosition:range.end];
+        if (extStart && extEnd) {
+            NSArray *arr = [@[extStart.start, extStart.end, extEnd.start, extEnd.end] sortedArrayUsingSelector:@selector(compare:)];
+            range = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
+        }
+    }
+    
+    range = [self _correctedTextRange:range];
+    if (range.asRange.length == 0) {
+        range = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+    }
+    
+    return [self _correctedTextRange:range];
+}
+
+/// Try to get the character range/position with word granularity from the tokenizer.
+- (YYTextRange *)_getClosestTokenRangeAtPoint:(CGPoint)point {
+    point = [self _convertPointToLayout:point];
+    YYTextRange *touchRange = [_innerLayout closestTextRangeAtPoint:point];
+    touchRange = [self _correctedTextRange:touchRange];
+    
+    if (_tokenizer && touchRange) {
+        YYTextRange *encEnd = (id)[_tokenizer rangeEnclosingPosition:touchRange.end withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward];
+        YYTextRange *encStart = (id)[_tokenizer rangeEnclosingPosition:touchRange.start withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward];
+        if (encEnd && encStart) {
+            NSArray *arr = [@[encEnd.start, encEnd.end, encStart.start, encStart.end] sortedArrayUsingSelector:@selector(compare:)];
+            touchRange = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
+        }
+    }
+    
+    if (touchRange) {
+        YYTextRange *extStart = [_innerLayout textRangeByExtendingPosition:touchRange.start];
+        YYTextRange *extEnd = [_innerLayout textRangeByExtendingPosition:touchRange.end];
+        if (extStart && extEnd) {
+            NSArray *arr = [@[extStart.start, extStart.end, extEnd.start, extEnd.end] sortedArrayUsingSelector:@selector(compare:)];
+            touchRange = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
+        }
+    }
+    
+    if (!touchRange) touchRange = [YYTextRange defaultRange];
+    
+    if (_innerText.length && touchRange.asRange.length == 0) {
+        touchRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+    }
+    
+    return touchRange;
+}
+
+/// Try to get the highlight property. If exist, the range will be returnd by the range pointer.
+/// If the delegate ignore the highlight, returns nil.
+- (YYTextHighlight *)_getHighlightAtPoint:(CGPoint)point range:(NSRangePointer)range {
+    if (!_highlightable || !_innerLayout.containsHighlight) return nil;
+    point = [self _convertPointToLayout:point];
+    YYTextRange *textRange = [_innerLayout textRangeAtPoint:point];
+    textRange = [self _correctedTextRange:textRange];
+    if (!textRange) return nil;
+    NSUInteger startIndex = textRange.start.offset;
+    if (startIndex == _innerText.length) {
+        if (startIndex == 0) return nil;
+        else startIndex--;
+    }
+    NSRange highlightRange = {0};
+    NSAttributedString *text = _delectedText ? _delectedText : _innerText;
+    YYTextHighlight *highlight = [text attribute:YYTextHighlightAttributeName
+                                         atIndex:startIndex
+                           longestEffectiveRange:&highlightRange
+                                         inRange:NSMakeRange(0, _innerText.length)];
+    
+    if (!highlight) return nil;
+    
+    BOOL shouldTap = YES, shouldLongPress = YES;
+    if (!highlight.tapAction && !highlight.longPressAction) {
+        if ([self.delegate respondsToSelector:@selector(textView:shouldTapHighlight:inRange:)]) {
+            shouldTap = [self.delegate textView:self shouldTapHighlight:highlight inRange:highlightRange];
+        }
+        if ([self.delegate respondsToSelector:@selector(textView:shouldLongPressHighlight:inRange:)]) {
+            shouldLongPress = [self.delegate textView:self shouldLongPressHighlight:highlight inRange:highlightRange];
+        }
+    }
+    if (!shouldTap && !shouldLongPress) return nil;
+    if (range) *range = highlightRange;
+    return highlight;
+}
+
+/// Return the ranged magnifier popover offset from the baseline, base on `_trackingPoint`.
+- (CGFloat)_getMagnifierRangedOffset {
+    CGPoint magPoint = _trackingPoint;
+    magPoint = [self _convertPointToLayout:magPoint];
+    if (_verticalForm) {
+        magPoint.x += kMagnifierRangedTrackFix;
+    } else {
+        magPoint.y += kMagnifierRangedTrackFix;
+    }
+    YYTextPosition *position = [_innerLayout closestPositionToPoint:magPoint];
+    NSUInteger lineIndex = [_innerLayout lineIndexForPosition:position];
+    if (lineIndex < _innerLayout.lines.count) {
+        YYTextLine *line = _innerLayout.lines[lineIndex];
+        if (_verticalForm) {
+            magPoint.x = YYTEXT_CLAMP(magPoint.x, line.left, line.right);
+            return magPoint.x - line.position.x + kMagnifierRangedPopoverOffset;
+        } else {
+            magPoint.y = YYTEXT_CLAMP(magPoint.y, line.top, line.bottom);
+            return magPoint.y - line.position.y + kMagnifierRangedPopoverOffset;
+        }
+    } else {
+        return 0;
+    }
+}
+
+/// Return a YYTextMoveDirection from `_touchBeganPoint` to `_trackingPoint`.
+- (unsigned int)_getMoveDirection {
+    CGFloat moveH = _trackingPoint.x - _touchBeganPoint.x;
+    CGFloat moveV = _trackingPoint.y - _touchBeganPoint.y;
+    if (fabs(moveH) > fabs(moveV)) {
+        if (fabs(moveH) > kLongPressAllowableMovement) {
+            return moveH > 0 ? kRight : kLeft;
+        }
+    } else {
+        if (fabs(moveV) > kLongPressAllowableMovement) {
+            return moveV > 0 ? kBottom : kTop;
+        }
+    }
+    return 0;
+}
+
+/// Get the auto scroll offset in one tick time.
+- (CGFloat)_getAutoscrollOffset {
+    if (!_state.trackingTouch) return 0;
+    
+    CGRect bounds = self.bounds;
+    bounds.origin = CGPointZero;
+    YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
+    if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) {
+        CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
+        kbRect.origin.y -= _extraAccessoryViewHeight;
+        kbRect.size.height += _extraAccessoryViewHeight;
+        
+        kbRect.origin.x -= self.contentOffset.x;
+        kbRect.origin.y -= self.contentOffset.y;
+        CGRect inter = CGRectIntersection(bounds, kbRect);
+        if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > 1) {
+            if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) {
+                bounds.size.height -= inter.size.height;
+            }
+        }
+    }
+    
+    CGPoint point = _trackingPoint;
+    point.x -= self.contentOffset.x;
+    point.y -= self.contentOffset.y;
+    
+    CGFloat maxOfs = 32; // a good value ~
+    CGFloat ofs = 0;
+    if (_verticalForm) {
+        if (point.x < self.contentInset.left) {
+            ofs = (point.x - self.contentInset.left - 5) * 0.5;
+            if (ofs < -maxOfs) ofs = -maxOfs;
+        } else if (point.x > bounds.size.width) {
+            ofs = ((point.x - bounds.size.width) + 5) * 0.5;
+            if (ofs > maxOfs) ofs = maxOfs;
+        }
+    } else {
+        if (point.y < self.contentInset.top) {
+            ofs = (point.y - self.contentInset.top - 5) * 0.5;
+            if (ofs < -maxOfs) ofs = -maxOfs;
+        } else if (point.y > bounds.size.height) {
+            ofs = ((point.y - bounds.size.height) + 5) * 0.5;
+            if (ofs > maxOfs) ofs = maxOfs;
+        }
+    }
+    return ofs;
+}
+
+/// Visible size based on bounds and insets
+- (CGSize)_getVisibleSize {
+    CGSize visibleSize = self.bounds.size;
+    visibleSize.width -= self.contentInset.left - self.contentInset.right;
+    visibleSize.height -= self.contentInset.top - self.contentInset.bottom;
+    if (visibleSize.width < 0) visibleSize.width = 0;
+    if (visibleSize.height < 0) visibleSize.height = 0;
+    return visibleSize;
+}
+
+/// Returns whether the text view can paste data from pastboard.
+- (BOOL)_isPasteboardContainsValidValue {
+    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
+    if (pasteboard.string.length > 0) {
+        return YES;
+    }
+    if (pasteboard.yy_AttributedString.length > 0) {
+        if (_allowsPasteAttributedString) {
+            return YES;
+        }
+    }
+    if (pasteboard.image || pasteboard.yy_ImageData.length > 0) {
+        if (_allowsPasteImage) {
+            return YES;
+        }
+    }
+    return NO;
+}
+
+/// Save current selected attributed text to pasteboard.
+- (void)_copySelectedTextToPasteboard {
+    if (_allowsCopyAttributedString) {
+        NSAttributedString *text = [_innerText attributedSubstringFromRange:_selectedTextRange.asRange];
+        if (text.length) {
+            [UIPasteboard generalPasteboard].yy_AttributedString = text;
+        }
+    } else {
+        NSString *string = [_innerText yy_plainTextForRange:_selectedTextRange.asRange];
+        if (string.length) {
+            [UIPasteboard generalPasteboard].string = string;
+        }
+    }
+}
+
+/// Update the text view state when pasteboard changed.
+- (void)_pasteboardChanged {
+    if (_state.showingMenu) {
+        UIMenuController *menu = [UIMenuController sharedMenuController];
+        [menu update];
+    }
+}
+
+/// Whether the position is valid (not out of bounds).
+- (BOOL)_isTextPositionValid:(YYTextPosition *)position {
+    if (!position) return NO;
+    if (position.offset < 0) return NO;
+    if (position.offset > _innerText.length) return NO;
+    if (position.offset == 0 && position.affinity == YYTextAffinityBackward) return NO;
+    if (position.offset == _innerText.length && position.affinity == YYTextAffinityBackward) return NO;
+    return YES;
+}
+
+/// Whether the range is valid (not out of bounds).
+- (BOOL)_isTextRangeValid:(YYTextRange *)range {
+    if (![self _isTextPositionValid:range.start]) return NO;
+    if (![self _isTextPositionValid:range.end]) return NO;
+    return YES;
+}
+
+/// Correct the position if it out of bounds.
+- (YYTextPosition *)_correctedTextPosition:(YYTextPosition *)position {
+    if (!position) return nil;
+    if ([self _isTextPositionValid:position]) return position;
+    if (position.offset < 0) {
+        return [YYTextPosition positionWithOffset:0];
+    }
+    if (position.offset > _innerText.length) {
+        return [YYTextPosition positionWithOffset:_innerText.length];
+    }
+    if (position.offset == 0 && position.affinity == YYTextAffinityBackward) {
+        return [YYTextPosition positionWithOffset:position.offset];
+    }
+    if (position.offset == _innerText.length && position.affinity == YYTextAffinityBackward) {
+        return [YYTextPosition positionWithOffset:position.offset];
+    }
+    return position;
+}
+
+/// Correct the range if it out of bounds.
+- (YYTextRange *)_correctedTextRange:(YYTextRange *)range {
+    if (!range) return nil;
+    if ([self _isTextRangeValid:range]) return range;
+    YYTextPosition *start = [self _correctedTextPosition:range.start];
+    YYTextPosition *end = [self _correctedTextPosition:range.end];
+    return [YYTextRange rangeWithStart:start end:end];
+}
+
+/// Convert the point from this view to text layout.
+- (CGPoint)_convertPointToLayout:(CGPoint)point {
+    CGSize boundingSize = _innerLayout.textBoundingSize;
+    if (_innerLayout.container.isVerticalForm) {
+        CGFloat w = _innerLayout.textBoundingSize.width;
+        if (w < self.bounds.size.width) w = self.bounds.size.width;
+        point.x += _innerLayout.container.size.width - w;
+        if (boundingSize.width < self.bounds.size.width) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.x += (self.bounds.size.width - boundingSize.width) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.x += (self.bounds.size.width - boundingSize.width);
+            }
+        }
+        return point;
+    } else {
+        if (boundingSize.height < self.bounds.size.height) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.y -= (self.bounds.size.height - boundingSize.height) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.y -= (self.bounds.size.height - boundingSize.height);
+            }
+        }
+        return point;
+    }
+}
+
+/// Convert the point from text layout to this view.
+- (CGPoint)_convertPointFromLayout:(CGPoint)point {
+    CGSize boundingSize = _innerLayout.textBoundingSize;
+    if (_innerLayout.container.isVerticalForm) {
+        CGFloat w = _innerLayout.textBoundingSize.width;
+        if (w < self.bounds.size.width) w = self.bounds.size.width;
+        point.x -= _innerLayout.container.size.width - w;
+        if (boundingSize.width < self.bounds.size.width) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.x -= (self.bounds.size.width - boundingSize.width) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.x -= (self.bounds.size.width - boundingSize.width);
+            }
+        }
+        return point;
+    } else {
+        if (boundingSize.height < self.bounds.size.height) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.y += (self.bounds.size.height - boundingSize.height) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.y += (self.bounds.size.height - boundingSize.height);
+            }
+        }
+        return point;
+    }
+}
+
+/// Convert the rect from this view to text layout.
+- (CGRect)_convertRectToLayout:(CGRect)rect {
+    rect.origin = [self _convertPointToLayout:rect.origin];
+    return rect;
+}
+
+/// Convert the rect from text layout to this view.
+- (CGRect)_convertRectFromLayout:(CGRect)rect {
+    rect.origin = [self _convertPointFromLayout:rect.origin];
+    return rect;
+}
+
+/// Replace the range with the text, and change the `_selectTextRange`.
+/// The caller should make sure the `range` and `text` are valid before call this method.
+- (void)_replaceRange:(YYTextRange *)range withText:(NSString *)text notifyToDelegate:(BOOL)notify{
+    if (NSEqualRanges(range.asRange, _selectedTextRange.asRange)) {
+        if (notify) [_inputDelegate selectionWillChange:self];
+        NSRange newRange = NSMakeRange(0, 0);
+        newRange.location = _selectedTextRange.start.offset + text.length;
+        _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+        if (notify) [_inputDelegate selectionDidChange:self];
+    } else {
+        if (range.asRange.length != text.length) {
+            if (notify) [_inputDelegate selectionWillChange:self];
+            NSRange unionRange = NSIntersectionRange(_selectedTextRange.asRange, range.asRange);
+            if (unionRange.length == 0) {
+                // no intersection
+                if (range.end.offset <= _selectedTextRange.start.offset) {
+                    NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length;
+                    NSRange newRange = _selectedTextRange.asRange;
+                    newRange.location += ofs;
+                    _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+                }
+            } else if (unionRange.length == _selectedTextRange.asRange.length) {
+                // target range contains selected range
+                _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(range.start.offset + text.length, 0)];
+            } else if (range.start.offset >= _selectedTextRange.start.offset &&
+                       range.end.offset <= _selectedTextRange.end.offset) {
+                // target range inside selected range
+                NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length;
+                NSRange newRange = _selectedTextRange.asRange;
+                newRange.length += ofs;
+                _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+            } else {
+                // interleaving
+                if (range.start.offset < _selectedTextRange.start.offset) {
+                    NSRange newRange = _selectedTextRange.asRange;
+                    newRange.location = range.start.offset + text.length;
+                    newRange.length -= unionRange.length;
+                    _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+                } else {
+                    NSRange newRange = _selectedTextRange.asRange;
+                    newRange.length -= unionRange.length;
+                    _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+                }
+            }
+            _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
+            if (notify) [_inputDelegate selectionDidChange:self];
+        }
+    }
+    if (notify) [_inputDelegate textWillChange:self];
+    NSRange newRange = NSMakeRange(range.asRange.location, text.length);
+    [_innerText replaceCharactersInRange:range.asRange withString:text];
+    [_innerText yy_removeDiscontinuousAttributesInRange:newRange];
+    if (notify) [_inputDelegate textDidChange:self];
+}
+
+/// Save current typing attributes to the attributes holder.
+- (void)_updateAttributesHolder {
+    if (_innerText.length > 0) {
+        NSUInteger index = _selectedTextRange.end.offset == 0 ? 0 : _selectedTextRange.end.offset - 1;
+        NSDictionary *attributes = [_innerText yy_attributesAtIndex:index];
+        if (!attributes) attributes = @{};
+        _typingAttributesHolder.yy_attributes = attributes;
+        [_typingAttributesHolder yy_removeDiscontinuousAttributesInRange:NSMakeRange(0, _typingAttributesHolder.length)];
+        [_typingAttributesHolder removeAttribute:YYTextBorderAttributeName range:NSMakeRange(0, _typingAttributesHolder.length)];
+        [_typingAttributesHolder removeAttribute:YYTextBackgroundBorderAttributeName range:NSMakeRange(0, _typingAttributesHolder.length)];
+    }
+}
+
+/// Update outer properties from current inner data.
+- (void)_updateOuterProperties {
+    [self _updateAttributesHolder];
+    NSParagraphStyle *style = _innerText.yy_paragraphStyle;
+    if (!style) style = _typingAttributesHolder.yy_paragraphStyle;
+    if (!style) style = [NSParagraphStyle defaultParagraphStyle];
+    
+    UIFont *font = _innerText.yy_font;
+    if (!font) font = _typingAttributesHolder.yy_font;
+    if (!font) font = [self _defaultFont];
+    
+    UIColor *color = _innerText.yy_color;
+    if (!color) color = _typingAttributesHolder.yy_color;
+    if (!color) color = [UIColor blackColor];
+    
+    [self _setText:[_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)]];
+    [self _setFont:font];
+    [self _setTextColor:color];
+    [self _setTextAlignment:style.alignment];
+    [self _setSelectedRange:_selectedTextRange.asRange];
+    [self _setTypingAttributes:_typingAttributesHolder.yy_attributes];
+    [self _setAttributedText:_innerText];
+}
+
+/// Parse text with `textParser` and update the _selectedTextRange.
+/// @return Whether changed (text or selection)
+- (BOOL)_parseText {
+    if (self.textParser) {
+        YYTextRange *oldTextRange = _selectedTextRange;
+        NSRange newRange = _selectedTextRange.asRange;
+        
+        [_inputDelegate textWillChange:self];
+        BOOL textChanged = [self.textParser parseText:_innerText selectedRange:&newRange];
+        [_inputDelegate textDidChange:self];
+        
+        YYTextRange *newTextRange = [YYTextRange rangeWithRange:newRange];
+        newTextRange = [self _correctedTextRange:newTextRange];
+        
+        if (![oldTextRange isEqual:newTextRange]) {
+            [_inputDelegate selectionWillChange:self];
+            _selectedTextRange = newTextRange;
+            [_inputDelegate selectionDidChange:self];
+        }
+        return textChanged;
+    }
+    return NO;
+}
+
+/// Returns whether the text should be detected by the data detector.
+- (BOOL)_shouldDetectText {
+    if (!_dataDetector) return NO;
+    if (!_highlightable) return NO;
+    if (_linkTextAttributes.count == 0 && _highlightTextAttributes.count == 0) return NO;
+    if (self.isFirstResponder || _containerView.isFirstResponder) return NO;
+    return YES;
+}
+
+/// Detect the data in text and add highlight to the data range.
+/// @return Whether detected.
+- (BOOL)_detectText:(NSMutableAttributedString *)text {
+    if (![self _shouldDetectText]) return NO;
+    if (text.length == 0) return NO;
+    __block BOOL detected = NO;
+    [_dataDetector enumerateMatchesInString:text.string options:kNilOptions range:NSMakeRange(0, text.length) usingBlock: ^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        switch (result.resultType) {
+            case NSTextCheckingTypeDate:
+            case NSTextCheckingTypeAddress:
+            case NSTextCheckingTypeLink:
+            case NSTextCheckingTypePhoneNumber: {
+                detected = YES;
+                if (_highlightTextAttributes.count) {
+                    YYTextHighlight *highlight = [YYTextHighlight highlightWithAttributes:_highlightTextAttributes];
+                    [text yy_setTextHighlight:highlight range:result.range];
+                }
+                if (_linkTextAttributes.count) {
+                    [_linkTextAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+                        [text yy_setAttribute:key value:obj range:result.range];
+                    }];
+                }
+            } break;
+            default:
+                break;
+        }
+    }];
+    return detected;
+}
+
+/// Returns the `root` view controller (returns nil if not found).
+- (UIViewController *)_getRootViewController {
+    UIViewController *ctrl = nil;
+    UIApplication *app = YYTextSharedApplication();
+    if (!ctrl) ctrl = app.keyWindow.rootViewController;
+    if (!ctrl) ctrl = [app.windows.firstObject rootViewController];
+    if (!ctrl) ctrl = self.yy_viewController;
+    if (!ctrl) return nil;
+    
+    while (!ctrl.view.window && ctrl.presentedViewController) {
+        ctrl = ctrl.presentedViewController;
+    }
+    if (!ctrl.view.window) return nil;
+    return ctrl;
+}
+
+/// Clear the undo and redo stack, and capture current state to undo stack.
+- (void)_resetUndoAndRedoStack {
+    [_undoStack removeAllObjects];
+    [_redoStack removeAllObjects];
+    _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
+    _lastTypeRange = _selectedTextRange.asRange;
+    [_undoStack addObject:object];
+}
+
+/// Clear the redo stack.
+- (void)_resetRedoStack {
+    [_redoStack removeAllObjects];
+}
+
+/// Capture current state to undo stack.
+- (void)_saveToUndoStack {
+    if (!_allowsUndoAndRedo) return;
+    _YYTextViewUndoObject *lastObject = _undoStack.lastObject;
+    if ([lastObject.text isEqualToAttributedString:self.attributedText]) return;
+    
+    _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
+    _lastTypeRange = _selectedTextRange.asRange;
+    [_undoStack addObject:object];
+    while (_undoStack.count > _maximumUndoLevel) {
+        [_undoStack removeObjectAtIndex:0];
+    }
+}
+
+/// Capture current state to redo stack.
+- (void)_saveToRedoStack {
+    if (!_allowsUndoAndRedo) return;
+    _YYTextViewUndoObject *lastObject = _redoStack.lastObject;
+    if ([lastObject.text isEqualToAttributedString:self.attributedText]) return;
+    
+    _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
+    [_redoStack addObject:object];
+    while (_redoStack.count > _maximumUndoLevel) {
+        [_redoStack removeObjectAtIndex:0];
+    }
+}
+
+- (BOOL)_canUndo {
+    if (_undoStack.count == 0) return NO;
+    _YYTextViewUndoObject *object = _undoStack.lastObject;
+    if ([object.text isEqualToAttributedString:_innerText]) return NO;
+    return YES;
+}
+
+- (BOOL)_canRedo {
+    if (_redoStack.count == 0) return NO;
+    _YYTextViewUndoObject *object = _undoStack.lastObject;
+    if ([object.text isEqualToAttributedString:_innerText]) return NO;
+    return YES;
+}
+
+- (void)_undo {
+    if (![self _canUndo]) return;
+    [self _saveToRedoStack];
+    _YYTextViewUndoObject *object = _undoStack.lastObject;
+    [_undoStack removeLastObject];
+    
+    _state.insideUndoBlock = YES;
+    self.attributedText = object.text;
+    self.selectedRange = object.selectedRange;
+    _state.insideUndoBlock = NO;
+}
+
+- (void)_redo {
+    if (![self _canRedo]) return;
+    [self _saveToUndoStack];
+    _YYTextViewUndoObject *object = _redoStack.lastObject;
+    [_redoStack removeLastObject];
+    
+    _state.insideUndoBlock = YES;
+    self.attributedText = object.text;
+    self.selectedRange = object.selectedRange;
+    _state.insideUndoBlock = NO;
+}
+
+- (void)_restoreFirstResponderAfterUndoAlert {
+    if (_state.firstResponderBeforeUndoAlert) {
+        [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
+    }
+}
+
+/// Show undo alert if it can undo or redo.
+#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
+- (void)_showUndoRedoAlert NS_EXTENSION_UNAVAILABLE_IOS(""){
+    _state.firstResponderBeforeUndoAlert = self.isFirstResponder;
+    __weak typeof(self) _self = self;
+    NSArray *strings = [self _localizedUndoStrings];
+    BOOL canUndo = [self _canUndo];
+    BOOL canRedo = [self _canRedo];
+    
+    UIViewController *ctrl = [self _getRootViewController];
+    
+    if (canUndo && canRedo) {
+        if (kiOS8Later) {
+            UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[4] message:@"" preferredStyle:UIAlertControllerStyleAlert];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[3] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+                [_self _undo];
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[2] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+                [_self _redo];
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [ctrl presentViewController:alert animated:YES completion:nil];
+        } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[4] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[3], strings[2], nil];
+            [alert show];
+#pragma clang diagnostic pop
+        }
+    } else if (canUndo) {
+        if (kiOS8Later) {
+            UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[4] message:@"" preferredStyle:UIAlertControllerStyleAlert];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[3] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+                [_self _undo];
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [ctrl presentViewController:alert animated:YES completion:nil];
+        } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[4] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[3], nil];
+            [alert show];
+#pragma clang diagnostic pop
+        }
+    } else if (canRedo) {
+        if (kiOS8Later) {
+            UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[2] message:@"" preferredStyle:UIAlertControllerStyleAlert];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[1] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+                [_self _redo];
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [ctrl presentViewController:alert animated:YES completion:nil];
+        } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[2] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[1], nil];
+            [alert show];
+#pragma clang diagnostic pop
+        }
+    }
+}
+#endif
+
+/// Get the localized undo alert strings based on app's main bundle.
+- (NSArray *)_localizedUndoStrings {
+    static NSArray *strings = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        NSDictionary *dic = @{
+            @"ar" : @[ @"إلغاء", @"إعادة", @"إعادة الكتابة", @"تراجع", @"تراجع عن الكتابة" ],
+            @"ca" : @[ @"Cancel·lar", @"Refer", @"Refer l’escriptura", @"Desfer", @"Desfer l’escriptura" ],
+            @"cs" : @[ @"Zrušit", @"Opakovat akci", @"Opakovat akci Psát", @"Odvolat akci", @"Odvolat akci Psát" ],
+            @"da" : @[ @"Annuller", @"Gentag", @"Gentag Indtastning", @"Fortryd", @"Fortryd Indtastning" ],
+            @"de" : @[ @"Abbrechen", @"Wiederholen", @"Eingabe wiederholen", @"Widerrufen", @"Eingabe widerrufen" ],
+            @"el" : @[ @"Ακύρωση", @"Επανάληψη", @"Επανάληψη πληκτρολόγησης", @"Αναίρεση", @"Αναίρεση πληκτρολόγησης" ],
+            @"en" : @[ @"Cancel", @"Redo", @"Redo Typing", @"Undo", @"Undo Typing" ],
+            @"es" : @[ @"Cancelar", @"Rehacer", @"Rehacer escritura", @"Deshacer", @"Deshacer escritura" ],
+            @"es_MX" : @[ @"Cancelar", @"Rehacer", @"Rehacer escritura", @"Deshacer", @"Deshacer escritura" ],
+            @"fi" : @[ @"Kumoa", @"Tee sittenkin", @"Kirjoita sittenkin", @"Peru", @"Peru kirjoitus" ],
+            @"fr" : @[ @"Annuler", @"Rétablir", @"Rétablir la saisie", @"Annuler", @"Annuler la saisie" ],
+            @"he" : @[ @"ביטול", @"חזור על הפעולה האחרונה", @"חזור על הקלדה", @"בטל", @"בטל הקלדה" ],
+            @"hr" : @[ @"Odustani", @"Ponovi", @"Ponovno upiši", @"Poništi", @"Poništi upisivanje" ],
+            @"hu" : @[ @"Mégsem", @"Ismétlés", @"Gépelés ismétlése", @"Visszavonás", @"Gépelés visszavonása" ],
+            @"id" : @[ @"Batalkan", @"Ulang", @"Ulang Pengetikan", @"Kembalikan", @"Batalkan Pengetikan" ],
+            @"it" : @[ @"Annulla", @"Ripristina originale", @"Ripristina Inserimento", @"Annulla", @"Annulla Inserimento" ],
+            @"ja" : @[ @"キャンセル", @"やり直す", @"やり直す - 入力", @"取り消す", @"取り消す - 入力" ],
+            @"ko" : @[ @"취소", @"실행 복귀", @"입력 복귀", @"실행 취소", @"입력 실행 취소" ],
+            @"ms" : @[ @"Batal", @"Buat semula", @"Ulang Penaipan", @"Buat asal", @"Buat asal Penaipan" ],
+            @"nb" : @[ @"Avbryt", @"Utfør likevel", @"Utfør skriving likevel", @"Angre", @"Angre skriving" ],
+            @"nl" : @[ @"Annuleer", @"Opnieuw", @"Opnieuw typen", @"Herstel", @"Herstel typen" ],
+            @"pl" : @[ @"Anuluj", @"Przywróć", @"Przywróć Wpisz", @"Cofnij", @"Cofnij Wpisz" ],
+            @"pt" : @[ @"Cancelar", @"Refazer", @"Refazer Digitação", @"Desfazer", @"Desfazer Digitação" ],
+            @"pt_PT" : @[ @"Cancelar", @"Refazer", @"Refazer digitar", @"Desfazer", @"Desfazer digitar" ],
+            @"ro" : @[ @"Renunță", @"Refă", @"Refă tastare", @"Anulează", @"Anulează tastare" ],
+            @"ru" : @[ @"Отменить", @"Повторить", @"Повторить набор на клавиатуре", @"Отменить", @"Отменить набор на клавиатуре" ],
+            @"sk" : @[ @"Zrušiť", @"Obnoviť", @"Obnoviť písanie", @"Odvolať", @"Odvolať písanie" ],
+            @"sv" : @[ @"Avbryt", @"Gör om", @"Gör om skriven text", @"Ångra", @"Ångra skriven text" ],
+            @"th" : @[ @"ยกเลิก", @"ทำกลับมาใหม่", @"ป้อนกลับมาใหม่", @"เลิกทำ", @"เลิกป้อน" ],
+            @"tr" : @[ @"Vazgeç", @"Yinele", @"Yazmayı Yinele", @"Geri Al", @"Yazmayı Geri Al" ],
+            @"uk" : @[ @"Скасувати", @"Повторити", @"Повторити введення", @"Відмінити", @"Відмінити введення" ],
+            @"vi" : @[ @"Hủy", @"Làm lại", @"Làm lại thao tác Nhập", @"Hoàn tác", @"Hoàn tác thao tác Nhập" ],
+            @"zh" : @[ @"取消", @"重做", @"重做键入", @"撤销", @"撤销键入" ],
+            @"zh_CN" : @[ @"取消", @"重做", @"重做键入", @"撤销", @"撤销键入" ],
+            @"zh_HK" : @[ @"取消", @"重做", @"重做輸入", @"還原", @"還原輸入" ],
+            @"zh_TW" : @[ @"取消", @"重做", @"重做輸入", @"還原", @"還原輸入" ]
+        };
+        NSString *preferred = [[NSBundle mainBundle] preferredLocalizations].firstObject;
+        if (preferred.length == 0) preferred = @"English";
+        NSString *canonical = [NSLocale canonicalLocaleIdentifierFromString:preferred];
+        if (canonical.length == 0) canonical = @"en";
+        strings = dic[canonical];
+        if (!strings  && [canonical containsString:@"_"]) {
+            NSString *prefix = [canonical componentsSeparatedByString:@"_"].firstObject;
+            if (prefix.length) strings = dic[prefix];
+        }
+        if (!strings) strings = dic[@"en"];
+    });
+    return strings;
+}
+
+/// Returns the default font for text view (same as CoreText).
+- (UIFont *)_defaultFont {
+    return [UIFont systemFontOfSize:12];
+}
+
+/// Returns the default tint color for text view (used for caret and select range background).
+- (UIColor *)_defaultTintColor {
+    return [UIColor colorWithRed:69/255.0 green:111/255.0 blue:238/255.0 alpha:1];
+}
+
+/// Returns the default placeholder color for text view (same as UITextField).
+- (UIColor *)_defaultPlaceholderColor {
+    return [UIColor colorWithRed:0 green:0 blue:25/255.0 alpha:44/255.0];
+}
+
+#pragma mark - Private Setter
+
+- (void)_setText:(NSString *)text {
+    if (_text == text || [_text isEqualToString:text]) return;
+    [self willChangeValueForKey:@"text"];
+    _text = text.copy;
+    if (!_text) _text = @"";
+    [self didChangeValueForKey:@"text"];
+    self.accessibilityLabel = _text;
+}
+
+- (void)_setFont:(UIFont *)font {
+    if (_font == font || [_font isEqual:font]) return;
+    [self willChangeValueForKey:@"font"];
+    _font = font;
+    [self didChangeValueForKey:@"font"];
+}
+
+- (void)_setTextColor:(UIColor *)textColor {
+    if (_textColor == textColor) return;
+    if (_textColor && textColor) {
+        if (CFGetTypeID(_textColor.CGColor) == CFGetTypeID(textColor.CGColor) &&
+            CFGetTypeID(_textColor.CGColor) == CGColorGetTypeID()) {
+            if ([_textColor isEqual:textColor]) {
+                return;
+            }
+        }
+    }
+    [self willChangeValueForKey:@"textColor"];
+    _textColor = textColor;
+    [self didChangeValueForKey:@"textColor"];
+}
+
+- (void)_setTextAlignment:(NSTextAlignment)textAlignment {
+    if (_textAlignment == textAlignment) return;
+    [self willChangeValueForKey:@"textAlignment"];
+    _textAlignment = textAlignment;
+    [self didChangeValueForKey:@"textAlignment"];
+}
+
+- (void)_setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes {
+    if (_dataDetectorTypes == dataDetectorTypes) return;
+    [self willChangeValueForKey:@"dataDetectorTypes"];
+    _dataDetectorTypes = dataDetectorTypes;
+    [self didChangeValueForKey:@"dataDetectorTypes"];
+}
+
+- (void)_setLinkTextAttributes:(NSDictionary *)linkTextAttributes {
+    if (_linkTextAttributes == linkTextAttributes || [_linkTextAttributes isEqual:linkTextAttributes]) return;
+    [self willChangeValueForKey:@"linkTextAttributes"];
+    _linkTextAttributes = linkTextAttributes.copy;
+    [self didChangeValueForKey:@"linkTextAttributes"];
+}
+
+- (void)_setHighlightTextAttributes:(NSDictionary *)highlightTextAttributes {
+    if (_highlightTextAttributes == highlightTextAttributes || [_highlightTextAttributes isEqual:highlightTextAttributes]) return;
+    [self willChangeValueForKey:@"highlightTextAttributes"];
+    _highlightTextAttributes = highlightTextAttributes.copy;
+    [self didChangeValueForKey:@"highlightTextAttributes"];
+}
+- (void)_setTextParser:(id<YYTextParser>)textParser {
+    if (_textParser == textParser || [_textParser isEqual:textParser]) return;
+    [self willChangeValueForKey:@"textParser"];
+    _textParser = textParser;
+    [self didChangeValueForKey:@"textParser"];
+}
+
+- (void)_setAttributedText:(NSAttributedString *)attributedText {
+    if (_attributedText == attributedText || [_attributedText isEqual:attributedText]) return;
+    [self willChangeValueForKey:@"attributedText"];
+    _attributedText = attributedText.copy;
+    if (!_attributedText) _attributedText = [NSAttributedString new];
+    [self didChangeValueForKey:@"attributedText"];
+}
+
+- (void)_setTextContainerInset:(UIEdgeInsets)textContainerInset {
+    if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return;
+    [self willChangeValueForKey:@"textContainerInset"];
+    _textContainerInset = textContainerInset;
+    [self didChangeValueForKey:@"textContainerInset"];
+}
+
+- (void)_setExclusionPaths:(NSArray *)exclusionPaths {
+    if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return;
+    [self willChangeValueForKey:@"exclusionPaths"];
+    _exclusionPaths = exclusionPaths.copy;
+    [self didChangeValueForKey:@"exclusionPaths"];
+}
+
+- (void)_setVerticalForm:(BOOL)verticalForm {
+    if (_verticalForm == verticalForm) return;
+    [self willChangeValueForKey:@"verticalForm"];
+    _verticalForm = verticalForm;
+    [self didChangeValueForKey:@"verticalForm"];
+}
+
+- (void)_setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
+    if (_linePositionModifier == linePositionModifier) return;
+    [self willChangeValueForKey:@"linePositionModifier"];
+    _linePositionModifier = [(NSObject *)linePositionModifier copy];
+    [self didChangeValueForKey:@"linePositionModifier"];
+}
+
+- (void)_setSelectedRange:(NSRange)selectedRange {
+    if (NSEqualRanges(_selectedRange, selectedRange)) return;
+    [self willChangeValueForKey:@"selectedRange"];
+    _selectedRange = selectedRange;
+    [self didChangeValueForKey:@"selectedRange"];
+    if ([self.delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) {
+        [self.delegate textViewDidChangeSelection:self];
+    }
+}
+
+- (void)_setTypingAttributes:(NSDictionary *)typingAttributes {
+    if (_typingAttributes == typingAttributes || [_typingAttributes isEqual:typingAttributes]) return;
+    [self willChangeValueForKey:@"typingAttributes"];
+    _typingAttributes = typingAttributes.copy;
+    [self didChangeValueForKey:@"typingAttributes"];
+}
+
+#pragma mark - Private Init
+
+- (void)_initTextView {
+    self.delaysContentTouches = NO;
+    self.canCancelContentTouches = YES;
+    self.multipleTouchEnabled = NO;
+    self.clipsToBounds = YES;
+    [super setDelegate:self];
+    
+    _text = @"";
+    _attributedText = [NSAttributedString new];
+    
+    // UITextInputTraits
+    _autocapitalizationType = UITextAutocapitalizationTypeSentences;
+    _autocorrectionType = UITextAutocorrectionTypeDefault;
+    _spellCheckingType = UITextSpellCheckingTypeDefault;
+    _keyboardType = UIKeyboardTypeDefault;
+    _keyboardAppearance = UIKeyboardAppearanceDefault;
+    _returnKeyType = UIReturnKeyDefault;
+    _enablesReturnKeyAutomatically = NO;
+    _secureTextEntry = NO;
+    
+    // UITextInput
+    _selectedTextRange = [YYTextRange defaultRange];
+    _markedTextRange = nil;
+    _markedTextStyle = nil;
+    _tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self];
+    
+    _editable = YES;
+    _selectable = YES;
+    _highlightable = YES;
+    _allowsCopyAttributedString = YES;
+    
+    _innerText = [NSMutableAttributedString new];
+    _innerContainer = [YYTextContainer new];
+    _innerContainer.insets = kDefaultInset;
+    _textContainerInset = kDefaultInset;
+    _typingAttributesHolder = [[NSMutableAttributedString alloc] initWithString:@" "];
+    _linkTextAttributes = @{NSForegroundColorAttributeName : [self _defaultTintColor],
+                            (id)kCTForegroundColorAttributeName : (id)[self _defaultTintColor].CGColor};
+    
+    YYTextHighlight *highlight = [YYTextHighlight new];
+    YYTextBorder * border = [YYTextBorder new];
+    border.insets = UIEdgeInsetsMake(-2, -2, -2, -2);
+    border.fillColor = [UIColor colorWithWhite:0.1 alpha:0.2];
+    border.cornerRadius = 3;
+    [highlight setBorder:border];
+    _highlightTextAttributes = highlight.attributes.copy;
+    
+    _placeHolderView = [UIImageView new];
+    _placeHolderView.userInteractionEnabled = NO;
+    _placeHolderView.hidden = YES;
+    
+    _containerView = [YYTextContainerView new];
+    _containerView.hostView = self;
+    
+    _selectionView = [YYTextSelectionView new];
+    _selectionView.userInteractionEnabled = NO;
+    _selectionView.hostView = self;
+    _selectionView.color = [self _defaultTintColor];
+    
+    _magnifierCaret = [YYTextMagnifier magnifierWithType:YYTextMagnifierTypeCaret];
+    _magnifierCaret.hostView = _containerView;
+    _magnifierRanged = [YYTextMagnifier magnifierWithType:YYTextMagnifierTypeRanged];
+    _magnifierRanged.hostView = _containerView;
+    
+    [self addSubview:_placeHolderView];
+    [self addSubview:_containerView];
+    [self addSubview:_selectionView];
+    
+    _undoStack = [NSMutableArray new];
+    _redoStack = [NSMutableArray new];
+    _allowsUndoAndRedo = YES;
+    _maximumUndoLevel = kDefaultUndoLevelMax;
+    
+    self.debugOption = [YYTextDebugOption sharedDebugOption];
+    [YYTextDebugOption addDebugTarget:self];
+    
+    [self _updateInnerContainerSize];
+    [self _update];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_pasteboardChanged) name:UIPasteboardChangedNotification object:nil];
+    [[YYTextKeyboardManager defaultManager] addObserver:self];
+    
+    self.isAccessibilityElement = YES;
+}
+
+#pragma mark - Public
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    [self _initTextView];
+    return self;
+}
+
+- (void)dealloc {
+    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIPasteboardChangedNotification object:nil];
+    [[YYTextKeyboardManager defaultManager] removeObserver:self];
+    
+    [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
+    [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
+    [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
+    
+    [YYTextDebugOption removeDebugTarget:self];
+    
+    [_longPressTimer invalidate];
+    [_autoScrollTimer invalidate];
+    [_selectionDotFixTimer invalidate];
+}
+
+- (void)scrollRangeToVisible:(NSRange)range {
+    YYTextRange *textRange = [YYTextRange rangeWithRange:range];
+    textRange = [self _correctedTextRange:textRange];
+    [self _scrollRangeToVisible:textRange];
+}
+
+#pragma mark - Property
+
+- (void)setText:(NSString *)text {
+    if (_text == text || [_text isEqualToString:text]) return;
+    [self _setText:text];
+    
+    _state.selectedWithoutEdit = NO;
+    _state.deleteConfirm = NO;
+    [self _endTouchTracking];
+    [self _hideMenu];
+    [self _resetUndoAndRedoStack];
+    [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:text];
+}
+
+- (void)setFont:(UIFont *)font {
+    if (_font == font || [_font isEqual:font]) return;
+    [self _setFont:font];
+    
+    _state.typingAttributesOnce = NO;
+    _typingAttributesHolder.yy_font = font;
+    _innerText.yy_font = font;
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setTextColor:(UIColor *)textColor {
+    if (_textColor == textColor || [_textColor isEqual:textColor]) return;
+    [self _setTextColor:textColor];
+    
+    _state.typingAttributesOnce = NO;
+    _typingAttributesHolder.yy_color = textColor;
+    _innerText.yy_color = textColor;
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setTextAlignment:(NSTextAlignment)textAlignment {
+    if (_textAlignment == textAlignment) return;
+    [self _setTextAlignment:textAlignment];
+    
+    _typingAttributesHolder.yy_alignment = textAlignment;
+    _innerText.yy_alignment = textAlignment;
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes {
+    if (_dataDetectorTypes == dataDetectorTypes) return;
+    [self _setDataDetectorTypes:dataDetectorTypes];
+    NSTextCheckingType type = YYTextNSTextCheckingTypeFromUIDataDetectorType(dataDetectorTypes);
+    _dataDetector = type ? [NSDataDetector dataDetectorWithTypes:type error:NULL] : nil;
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setLinkTextAttributes:(NSDictionary *)linkTextAttributes {
+    if (_linkTextAttributes == linkTextAttributes || [_linkTextAttributes isEqual:linkTextAttributes]) return;
+    [self _setLinkTextAttributes:linkTextAttributes];
+    if (_dataDetector) {
+        [self _commitUpdate];
+    }
+}
+
+- (void)setHighlightTextAttributes:(NSDictionary *)highlightTextAttributes {
+    if (_highlightTextAttributes == highlightTextAttributes || [_highlightTextAttributes isEqual:highlightTextAttributes]) return;
+    [self _setHighlightTextAttributes:highlightTextAttributes];
+    if (_dataDetector) {
+        [self _commitUpdate];
+    }
+}
+
+- (void)setTextParser:(id<YYTextParser>)textParser {
+    if (_textParser == textParser || [_textParser isEqual:textParser]) return;
+    [self _setTextParser:textParser];
+    if (textParser && _text.length) {
+        [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _text.length)] withText:_text];
+    }
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setTypingAttributes:(NSDictionary *)typingAttributes {
+    [self _setTypingAttributes:typingAttributes];
+    _state.typingAttributesOnce = YES;
+    [typingAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+        [_typingAttributesHolder yy_setAttribute:key value:obj];
+    }];
+    [self _commitUpdate];
+}
+
+- (void)setAttributedText:(NSAttributedString *)attributedText {
+    if (_attributedText == attributedText) return;
+    [self _setAttributedText:attributedText];
+    _state.typingAttributesOnce = NO;
+    
+    NSMutableAttributedString *text = attributedText.mutableCopy;
+    if (text.length == 0) {
+        [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:@""];
+        return;
+    }
+    if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
+        BOOL should = [self.delegate textView:self shouldChangeTextInRange:NSMakeRange(0, _innerText.length) replacementText:text.string];
+        if (!should) return;
+    }
+    
+    _state.selectedWithoutEdit = NO;
+    _state.deleteConfirm = NO;
+    [self _endTouchTracking];
+    [self _hideMenu];
+    
+    [_inputDelegate selectionWillChange:self];
+    [_inputDelegate textWillChange:self];
+     _innerText = text;
+    [self _parseText];
+    _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+    [_inputDelegate textDidChange:self];
+    [_inputDelegate selectionDidChange:self];
+    
+    [self _setAttributedText:text];
+    if (_innerText.length > 0) {
+        _typingAttributesHolder.yy_attributes = [_innerText yy_attributesAtIndex:_innerText.length - 1];
+    }
+    
+    [self _updateOuterProperties];
+    [self _updateLayout];
+    [self _updateSelectionView];
+    
+    if (self.isFirstResponder) {
+        [self _scrollRangeToVisible:_selectedTextRange];
+    }
+    
+    if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
+        [self.delegate textViewDidChange:self];
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
+    
+    if (!_state.insideUndoBlock) {
+        [self _resetUndoAndRedoStack];
+    }
+}
+
+- (void)setTextVerticalAlignment:(YYTextVerticalAlignment)textVerticalAlignment {
+    if (_textVerticalAlignment == textVerticalAlignment) return;
+    [self willChangeValueForKey:@"textVerticalAlignment"];
+    _textVerticalAlignment = textVerticalAlignment;
+    [self didChangeValueForKey:@"textVerticalAlignment"];
+    _containerView.textVerticalAlignment = textVerticalAlignment;
+    [self _commitUpdate];
+}
+
+- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset {
+    if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return;
+    [self _setTextContainerInset:textContainerInset];
+    _innerContainer.insets = textContainerInset;
+    [self _commitUpdate];
+}
+
+- (void)setExclusionPaths:(NSArray *)exclusionPaths {
+    if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return;
+    [self _setExclusionPaths:exclusionPaths];
+    _innerContainer.exclusionPaths = exclusionPaths;
+    if (_innerContainer.isVerticalForm) {
+        CGAffineTransform trans = CGAffineTransformMakeTranslation(_innerContainer.size.width - self.bounds.size.width, 0);
+        [_innerContainer.exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath *path, NSUInteger idx, BOOL *stop) {
+            [path applyTransform:trans];
+        }];
+    }
+    [self _commitUpdate];
+}
+
+- (void)setVerticalForm:(BOOL)verticalForm {
+    if (_verticalForm == verticalForm) return;
+    [self _setVerticalForm:verticalForm];
+    _innerContainer.verticalForm = verticalForm;
+    _selectionView.verticalForm = verticalForm;
+    
+    [self _updateInnerContainerSize];
+    
+    if (verticalForm) {
+        if (UIEdgeInsetsEqualToEdgeInsets(_innerContainer.insets, kDefaultInset)) {
+            _innerContainer.insets = kDefaultVerticalInset;
+            [self _setTextContainerInset:kDefaultVerticalInset];
+        }
+    } else {
+        if (UIEdgeInsetsEqualToEdgeInsets(_innerContainer.insets, kDefaultVerticalInset)) {
+            _innerContainer.insets = kDefaultInset;
+            [self _setTextContainerInset:kDefaultInset];
+        }
+    }
+    
+    _innerContainer.exclusionPaths = _exclusionPaths;
+    if (verticalForm) {
+        CGAffineTransform trans = CGAffineTransformMakeTranslation(_innerContainer.size.width - self.bounds.size.width, 0);
+        [_innerContainer.exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath *path, NSUInteger idx, BOOL *stop) {
+            [path applyTransform:trans];
+        }];
+    }
+    
+    [self _keyboardChanged];
+    [self _commitUpdate];
+}
+
+- (void)setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
+    if (_linePositionModifier == linePositionModifier) return;
+    [self _setLinePositionModifier:linePositionModifier];
+    _innerContainer.linePositionModifier = linePositionModifier;
+    [self _commitUpdate];
+}
+
+- (void)setSelectedRange:(NSRange)selectedRange {
+    if (NSEqualRanges(_selectedRange, selectedRange)) return;
+    if (_markedTextRange) return;
+    _state.typingAttributesOnce = NO;
+    
+    YYTextRange *range = [YYTextRange rangeWithRange:selectedRange];
+    range = [self _correctedTextRange:range];
+    [self _endTouchTracking];
+    _selectedTextRange = range;
+    [self _updateSelectionView];
+    
+    [self _setSelectedRange:range.asRange];
+    
+    if (!_state.insideUndoBlock) {
+        [self _resetUndoAndRedoStack];
+    }
+}
+
+- (void)setHighlightable:(BOOL)highlightable {
+    if (_highlightable == highlightable) return;
+    [self willChangeValueForKey:@"highlightable"];
+    _highlightable = highlightable;
+    [self didChangeValueForKey:@"highlightable"];
+    [self _commitUpdate];
+}
+
+- (void)setEditable:(BOOL)editable {
+    if (_editable == editable) return;
+    [self willChangeValueForKey:@"editable"];
+    _editable = editable;
+    [self didChangeValueForKey:@"editable"];
+    if (!editable) {
+        [self resignFirstResponder];
+    }
+}
+
+- (void)setSelectable:(BOOL)selectable {
+    if (_selectable == selectable) return;
+    [self willChangeValueForKey:@"selectable"];
+    _selectable = selectable;
+    [self didChangeValueForKey:@"selectable"];
+    if (!selectable) {
+        if (self.isFirstResponder) {
+            [self resignFirstResponder];
+        } else {
+            _state.selectedWithoutEdit = NO;
+            [self _endTouchTracking];
+            [self _hideMenu];
+            [self _updateSelectionView];
+        }
+    }
+}
+
+- (void)setClearsOnInsertion:(BOOL)clearsOnInsertion {
+    if (_clearsOnInsertion == clearsOnInsertion) return;
+    _clearsOnInsertion = clearsOnInsertion;
+    if (clearsOnInsertion) {
+        if (self.isFirstResponder) {
+            self.selectedRange = NSMakeRange(0, _attributedText.length);
+        } else {
+            _state.clearsOnInsertionOnce = YES;
+        }
+    }
+}
+
+- (void)setDebugOption:(YYTextDebugOption *)debugOption {
+    _containerView.debugOption = debugOption;
+}
+
+- (YYTextDebugOption *)debugOption {
+    return _containerView.debugOption;
+}
+
+- (YYTextLayout *)textLayout {
+    [self _updateIfNeeded];
+    return _innerLayout;
+}
+
+- (void)setPlaceholderText:(NSString *)placeholderText {
+    if (_placeholderAttributedText.length > 0) {
+        if (placeholderText.length > 0) {
+            [((NSMutableAttributedString *)_placeholderAttributedText) replaceCharactersInRange:NSMakeRange(0, _placeholderAttributedText.length) withString:placeholderText];
+        } else {
+            [((NSMutableAttributedString *)_placeholderAttributedText) replaceCharactersInRange:NSMakeRange(0, _placeholderAttributedText.length) withString:@""];
+        }
+        ((NSMutableAttributedString *)_placeholderAttributedText).yy_font = _placeholderFont;
+        ((NSMutableAttributedString *)_placeholderAttributedText).yy_color = _placeholderTextColor;
+    } else {
+        if (placeholderText.length > 0) {
+            NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:placeholderText];
+            if (!_placeholderFont) _placeholderFont = _font;
+            if (!_placeholderFont) _placeholderFont = [self _defaultFont];
+            if (!_placeholderTextColor) _placeholderTextColor = [self _defaultPlaceholderColor];
+            atr.yy_font = _placeholderFont;
+            atr.yy_color = _placeholderTextColor;
+            _placeholderAttributedText = atr;
+        }
+    }
+    [self _commitPlaceholderUpdate];
+}
+
+- (void)setPlaceholderFont:(UIFont *)placeholderFont {
+    _placeholderFont = placeholderFont;
+    ((NSMutableAttributedString *)_placeholderAttributedText).yy_font = _placeholderFont;
+    [self _commitPlaceholderUpdate];
+}
+
+- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor {
+    _placeholderTextColor = placeholderTextColor;
+    ((NSMutableAttributedString *)_placeholderAttributedText).yy_color = _placeholderTextColor;
+    [self _commitPlaceholderUpdate];
+}
+
+- (void)setPlaceholderAttributedText:(NSAttributedString *)placeholderAttributedText {
+    _placeholderAttributedText = placeholderAttributedText.mutableCopy;
+    _placeholderText = [_placeholderAttributedText yy_plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)];
+    _placeholderFont = _placeholderAttributedText.yy_font;
+    _placeholderTextColor = _placeholderAttributedText.yy_color;
+    [self _commitPlaceholderUpdate];
+}
+
+#pragma mark - Override For Protect
+
+- (void)setMultipleTouchEnabled:(BOOL)multipleTouchEnabled {
+    [super setMultipleTouchEnabled:NO]; // must not enabled
+}
+
+- (void)setContentInset:(UIEdgeInsets)contentInset {
+    UIEdgeInsets oldInsets = self.contentInset;
+    if (_insetModifiedByKeyboard) {
+        _originalContentInset = contentInset;
+    } else {
+        [super setContentInset:contentInset];
+        BOOL changed = !UIEdgeInsetsEqualToEdgeInsets(oldInsets, contentInset);
+        if (changed) {
+            [self _updateInnerContainerSize];
+            [self _commitUpdate];
+            [self _commitPlaceholderUpdate];
+        }
+    }
+}
+
+- (void)setScrollIndicatorInsets:(UIEdgeInsets)scrollIndicatorInsets {
+    if (_insetModifiedByKeyboard) {
+        _originalScrollIndicatorInsets = scrollIndicatorInsets;
+    } else {
+        [super setScrollIndicatorInsets:scrollIndicatorInsets];
+    }
+}
+
+- (void)setFrame:(CGRect)frame {
+    CGSize oldSize = self.bounds.size;
+    [super setFrame:frame];
+    CGSize newSize = self.bounds.size;
+    BOOL changed = _innerContainer.isVerticalForm ? (oldSize.height != newSize.height) : (oldSize.width != newSize.width);
+    if (changed) {
+        [self _updateInnerContainerSize];
+        [self _commitUpdate];
+    }
+    if (!CGSizeEqualToSize(oldSize, newSize)) {
+        [self _commitPlaceholderUpdate];
+    }
+}
+
+- (void)setBounds:(CGRect)bounds {
+    CGSize oldSize = self.bounds.size;
+    [super setBounds:bounds];
+    CGSize newSize = self.bounds.size;
+    BOOL changed = _innerContainer.isVerticalForm ? (oldSize.height != newSize.height) : (oldSize.width != newSize.width);
+    if (changed) {
+        [self _updateInnerContainerSize];
+        [self _commitUpdate];
+    }
+    if (!CGSizeEqualToSize(oldSize, newSize)) {
+        [self _commitPlaceholderUpdate];
+    }
+}
+
+- (void)tintColorDidChange {
+    if ([self respondsToSelector:@selector(tintColor)]) {
+        UIColor *color = self.tintColor;
+        NSMutableDictionary *attrs = _highlightTextAttributes.mutableCopy;
+        NSMutableDictionary *linkAttrs = _linkTextAttributes.mutableCopy;
+        if (!linkAttrs) linkAttrs = @{}.mutableCopy;
+        if (!color) {
+            [attrs removeObjectForKey:NSForegroundColorAttributeName];
+            [attrs removeObjectForKey:(id)kCTForegroundColorAttributeName];
+            [linkAttrs setObject:[self _defaultTintColor] forKey:NSForegroundColorAttributeName];
+            [linkAttrs setObject:(id)[self _defaultTintColor].CGColor forKey:(id)kCTForegroundColorAttributeName];
+        } else {
+            [attrs setObject:color forKey:NSForegroundColorAttributeName];
+            [attrs setObject:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName];
+            [linkAttrs setObject:color forKey:NSForegroundColorAttributeName];
+            [linkAttrs setObject:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName];
+        }
+        self.highlightTextAttributes = attrs;
+        _selectionView.color = color ? color : [self _defaultTintColor];
+        _linkTextAttributes = linkAttrs;
+        [self _commitUpdate];
+    }
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    if (!_verticalForm && size.width <= 0) size.width = YYTextContainerMaxSize.width;
+    if (_verticalForm && size.height <= 0) size.height = YYTextContainerMaxSize.height;
+    
+    if ((!_verticalForm && size.width == self.bounds.size.width) ||
+        (_verticalForm && size.height == self.bounds.size.height)) {
+        [self _updateIfNeeded];
+        if (!_verticalForm) {
+            if (_containerView.bounds.size.height <= size.height) {
+                return _containerView.bounds.size;
+            }
+        } else {
+            if (_containerView.bounds.size.width <= size.width) {
+                return _containerView.bounds.size;
+            }
+        }
+    }
+    
+    if (!_verticalForm) {
+        size.height = YYTextContainerMaxSize.height;
+    } else {
+        size.width = YYTextContainerMaxSize.width;
+    }
+    
+    YYTextContainer *container = [_innerContainer copy];
+    container.size = size;
+    
+    YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
+    return layout.textBoundingSize;
+}
+
+#pragma mark - Override UIResponder
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _updateIfNeeded];
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:_containerView];
+    
+    _touchBeganTime = _trackingTime = touch.timestamp;
+    _touchBeganPoint = _trackingPoint = point;
+    _trackingRange = _selectedTextRange;
+    
+    _state.trackingGrabber = NO;
+    _state.trackingCaret = NO;
+    _state.trackingPreSelect = NO;
+    _state.trackingTouch = YES;
+    _state.swallowTouch = YES;
+    _state.touchMoved = NO;
+    
+    if (!self.isFirstResponder && !_state.selectedWithoutEdit && self.highlightable) {
+        _highlight = [self _getHighlightAtPoint:point range:&_highlightRange];
+        _highlightLayout = nil;
+    }
+    
+    if ((!self.selectable && !_highlight) || _state.ignoreTouchBegan) {
+        _state.swallowTouch = NO;
+        _state.trackingTouch = NO;
+    }
+    
+    if (_state.trackingTouch) {
+        [self _startLongPressTimer];
+        if (_highlight) {
+            [self _showHighlightAnimated:NO];
+        } else {
+            if ([_selectionView isGrabberContainsPoint:point]) { // track grabber
+                self.panGestureRecognizer.enabled = NO; // disable scroll view
+                [self _hideMenu];
+                _state.trackingGrabber = [_selectionView isStartGrabberContainsPoint:point] ? kStart : kEnd;
+                _magnifierRangedOffset = [self _getMagnifierRangedOffset];
+            } else {
+                if (_selectedTextRange.asRange.length == 0 && self.isFirstResponder) {
+                    if ([_selectionView isCaretContainsPoint:point]) { // track caret
+                        _state.trackingCaret = YES;
+                        self.panGestureRecognizer.enabled = NO; // disable scroll view
+                    }
+                }
+            }
+        }
+        [self _updateSelectionView];
+    }
+    
+    if (!_state.swallowTouch) [super touchesBegan:touches withEvent:event];
+}
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _updateIfNeeded];
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:_containerView];
+    
+    _trackingTime = touch.timestamp;
+    _trackingPoint = point;
+    
+    if (!_state.touchMoved) {
+        _state.touchMoved = [self _getMoveDirection];
+        if (_state.touchMoved) [self _endLongPressTimer];
+    }
+    _state.clearsOnInsertionOnce = NO;
+    
+    if (_state.trackingTouch) {
+        BOOL showMagnifierCaret = NO;
+        BOOL showMagnifierRanged = NO;
+        
+        if (_highlight) {
+            
+            YYTextHighlight *highlight = [self _getHighlightAtPoint:_trackingPoint range:NULL];
+            if (highlight == _highlight) {
+                [self _showHighlightAnimated:YES];
+            } else {
+                [self _hideHighlightAnimated:YES];
+            }
+            
+        } else {
+            _trackingRange = _selectedTextRange;
+            if (_state.trackingGrabber) {
+                self.panGestureRecognizer.enabled = NO;
+                [self _hideMenu];
+                [self _updateTextRangeByTrackingGrabber];
+                showMagnifierRanged = YES;
+            } else if (_state.trackingPreSelect) {
+                [self _updateTextRangeByTrackingPreSelect];
+                showMagnifierCaret = YES;
+            } else if (_state.trackingCaret || _markedTextRange || self.isFirstResponder) {
+                if (_state.trackingCaret || _state.touchMoved) {
+                    _state.trackingCaret = YES;
+                    [self _hideMenu];
+                    if (_verticalForm) {
+                        if (_state.touchMoved == kTop || _state.touchMoved == kBottom) {
+                            self.panGestureRecognizer.enabled = NO;
+                        }
+                    } else {
+                        if (_state.touchMoved == kLeft || _state.touchMoved == kRight) {
+                            self.panGestureRecognizer.enabled = NO;
+                        }
+                    }
+                    [self _updateTextRangeByTrackingCaret];
+                    if (_markedTextRange) {
+                        showMagnifierRanged = YES;
+                    } else {
+                        showMagnifierCaret = YES;
+                    }
+                }
+            }
+        }
+        [self _updateSelectionView];
+        if (showMagnifierCaret) [self _showMagnifierCaret];
+        if (showMagnifierRanged) [self _showMagnifierRanged];
+    }
+    
+    CGFloat autoScrollOffset = [self _getAutoscrollOffset];
+    if (_autoScrollOffset != autoScrollOffset) {
+        if (fabs(autoScrollOffset) < fabs(_autoScrollOffset)) {
+            _autoScrollAcceleration *= 0.5;
+        }
+        _autoScrollOffset = autoScrollOffset;
+        if (_autoScrollOffset != 0 && _state.touchMoved) {
+            [self _startAutoScrollTimer];
+        }
+    }
+    
+    if (!_state.swallowTouch) [super touchesMoved:touches withEvent:event];
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _updateIfNeeded];
+    
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:_containerView];
+    
+    _trackingTime = touch.timestamp;
+    _trackingPoint = point;
+    
+    if (!_state.touchMoved) {
+        _state.touchMoved = [self _getMoveDirection];
+    }
+    if (_state.trackingTouch) {
+        [self _hideMagnifier];
+        
+        if (_highlight) {
+            if (_state.showingHighlight) {
+                if (_highlight.tapAction) {
+                    CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
+                    rect = [self _convertRectFromLayout:rect];
+                    _highlight.tapAction(self, _innerText, _highlightRange, rect);
+                } else {
+                    BOOL shouldTap = YES;
+                    if ([self.delegate respondsToSelector:@selector(textView:shouldTapHighlight:inRange:)]) {
+                        shouldTap = [self.delegate textView:self shouldTapHighlight:_highlight inRange:_highlightRange];
+                    }
+                    if (shouldTap && [self.delegate respondsToSelector:@selector(textView:didTapHighlight:inRange:rect:)]) {
+                        CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
+                        rect = [self _convertRectFromLayout:rect];
+                        [self.delegate textView:self didTapHighlight:_highlight inRange:_highlightRange rect:rect];
+                    }
+                }
+                [self _removeHighlightAnimated:YES];
+            }
+        } else {
+            if (_state.trackingCaret) {
+                if (_state.touchMoved) {
+                    [self _updateTextRangeByTrackingCaret];
+                    [self _showMenu];
+                } else {
+                    if (_state.showingMenu) [self _hideMenu];
+                    else [self _showMenu];
+                }
+            } else if (_state.trackingGrabber) {
+                [self _updateTextRangeByTrackingGrabber];
+                [self _showMenu];
+            } else if (_state.trackingPreSelect) {
+                [self _updateTextRangeByTrackingPreSelect];
+                if (_trackingRange.asRange.length > 0) {
+                    _state.selectedWithoutEdit = YES;
+                    [self _showMenu];
+                } else {
+                    [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
+                }
+            } else if (_state.deleteConfirm || _markedTextRange) {
+                [self _updateTextRangeByTrackingCaret];
+                [self _hideMenu];
+            } else {
+                if (!_state.touchMoved) {
+                    if (_state.selectedWithoutEdit) {
+                        _state.selectedWithoutEdit = NO;
+                        [self _hideMenu];
+                    } else {
+                        if (self.isFirstResponder) {
+                            YYTextRange *_oldRange = _trackingRange;
+                            [self _updateTextRangeByTrackingCaret];
+                            if ([_oldRange isEqual:_trackingRange]) {
+                                if (_state.showingMenu) [self _hideMenu];
+                                else [self _showMenu];
+                            } else {
+                                [self _hideMenu];
+                            }
+                        } else {
+                            [self _hideMenu];
+                            if (_state.clearsOnInsertionOnce) {
+                                _state.clearsOnInsertionOnce = NO;
+                                _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+                                [self _setSelectedRange:_selectedTextRange.asRange];
+                            } else {
+                                [self _updateTextRangeByTrackingCaret];
+                            }
+                            [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
+                        }
+                    }
+                }
+            }
+        }
+        
+        if (_trackingRange && (![_trackingRange isEqual:_selectedTextRange] || _state.trackingPreSelect)) {
+            if (![_trackingRange isEqual:_selectedTextRange]) {
+                [_inputDelegate selectionWillChange:self];
+                _selectedTextRange = _trackingRange;
+                [_inputDelegate selectionDidChange:self];
+                [self _updateAttributesHolder];
+                [self _updateOuterProperties];
+            }
+            if (!_state.trackingGrabber && !_state.trackingPreSelect) {
+                [self _scrollRangeToVisible:_selectedTextRange];
+            }
+        }
+        
+        [self _endTouchTracking];
+    }
+    
+    if (!_state.swallowTouch) [super touchesEnded:touches withEvent:event];
+}
+
+- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _endTouchTracking];
+    [self _hideMenu];
+
+    if (!_state.swallowTouch) [super touchesCancelled:touches withEvent:event];
+}
+
+- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
+    if (motion == UIEventSubtypeMotionShake && _allowsUndoAndRedo) {
+        if (!YYTextIsAppExtension()) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+            [self performSelector:@selector(_showUndoRedoAlert)];
+#pragma clang diagnostic pop
+        }
+    } else {
+        [super motionEnded:motion withEvent:event];
+    }
+}
+
+- (BOOL)canBecomeFirstResponder {
+    if (!self.isSelectable) return NO;
+    if (!self.isEditable) return NO;
+    if (_state.ignoreFirstResponder) return NO;
+    if ([self.delegate respondsToSelector:@selector(textViewShouldBeginEditing:)]) {
+        if (![self.delegate textViewShouldBeginEditing:self]) return NO;
+    }
+    return YES;
+}
+
+- (BOOL)becomeFirstResponder {
+    BOOL isFirstResponder = self.isFirstResponder;
+    if (isFirstResponder) return YES;
+    BOOL shouldDetectData = [self _shouldDetectText];
+    BOOL become = [super becomeFirstResponder];
+    if (!isFirstResponder && become) {
+        [self _endTouchTracking];
+        [self _hideMenu];
+        
+        _state.selectedWithoutEdit = NO;
+        if (shouldDetectData != [self _shouldDetectText]) {
+            [self _update];
+        }
+        [self _updateIfNeeded];
+        [self _updateSelectionView];
+        [self performSelector:@selector(_scrollSelectedRangeToVisible) withObject:nil afterDelay:0];
+        
+//        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+        
+            [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidBeginEditingNotification object:self];
+
+            if ([self.delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) {
+                [self.delegate textViewDidBeginEditing:self];
+            }
+//        });
+    }
+    return become;
+}
+
+- (BOOL)canResignFirstResponder {
+    if (!self.isFirstResponder) return YES;
+    if ([self.delegate respondsToSelector:@selector(textViewShouldEndEditing:)]) {
+        if (![self.delegate textViewShouldEndEditing:self]) return NO;
+    }
+    return YES;
+}
+
+- (BOOL)resignFirstResponder {
+    BOOL isFirstResponder = self.isFirstResponder;
+    if (!isFirstResponder) return YES;
+    BOOL resign = [super resignFirstResponder];
+    if (resign) {
+        if (_markedTextRange) {
+            _markedTextRange = nil;
+            [self _parseText];
+            [self _setText:[_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)]];
+        }
+        _state.selectedWithoutEdit = NO;
+        if ([self _shouldDetectText]) {
+            [self _update];
+        }
+        [self _endTouchTracking];
+        [self _hideMenu];
+        [self _updateIfNeeded];
+        [self _updateSelectionView];
+        [self _restoreInsetsAnimated:YES];
+        if ([self.delegate respondsToSelector:@selector(textViewDidEndEditing:)]) {
+            [self.delegate textViewDidEndEditing:self];
+        }
+        [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidEndEditingNotification object:self];
+    }
+    return resign;
+}
+
+- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
+    /*
+     ------------------------------------------------------
+     Default menu actions list:
+     cut:                                   Cut
+     copy:                                  Copy
+     select:                                Select
+     selectAll:                             Select All
+     paste:                                 Paste
+     delete:                                Delete
+     _promptForReplace:                     Replace...
+     _transliterateChinese:                 简⇄繁
+     _showTextStyleOptions:                 𝐁𝐼𝐔
+     _define:                               Define
+     _addShortcut:                          Add...
+     _accessibilitySpeak:                   Speak
+     _accessibilitySpeakLanguageSelection:  Speak...
+     _accessibilityPauseSpeaking:           Pause Speak
+     makeTextWritingDirectionRightToLeft:   ⇋
+     makeTextWritingDirectionLeftToRight:   ⇌
+     
+     ------------------------------------------------------
+     Default attribute modifier list:
+     toggleBoldface:
+     toggleItalics:
+     toggleUnderline:
+     increaseSize:
+     decreaseSize:
+     */
+    
+    if (_selectedTextRange.asRange.length == 0) {
+        if (action == @selector(select:) ||
+            action == @selector(selectAll:)) {
+            return _innerText.length > 0;
+        }
+        if (action == @selector(paste:)) {
+            return [self _isPasteboardContainsValidValue];
+        }
+    } else {
+        if (action == @selector(cut:)) {
+            return self.isFirstResponder && self.editable;
+        }
+        if (action == @selector(copy:)) {
+            return YES;
+        }
+        if (action == @selector(selectAll:)) {
+            return _selectedTextRange.asRange.length < _innerText.length;
+        }
+        if (action == @selector(paste:)) {
+            return self.isFirstResponder && self.editable && [self _isPasteboardContainsValidValue];
+        }
+        NSString *selString = NSStringFromSelector(action);
+        if ([selString hasSuffix:@"define:"] && [selString hasPrefix:@"_"]) {
+            return [self _getRootViewController] != nil;
+        }
+    }
+    return NO;
+}
+
+- (void)reloadInputViews {
+    [super reloadInputViews];
+    if (_markedTextRange) {
+        [self unmarkText];
+    }
+}
+
+#pragma mark - Override NSObject(UIResponderStandardEditActions)
+
+- (void)cut:(id)sender {
+    [self _endTouchTracking];
+    if (_selectedTextRange.asRange.length == 0) return;
+    
+    [self _copySelectedTextToPasteboard];
+    [self _saveToUndoStack];
+    [self _resetRedoStack];
+    [self replaceRange:_selectedTextRange withText:@""];
+}
+
+- (void)copy:(id)sender {
+    [self _endTouchTracking];
+    [self _copySelectedTextToPasteboard];
+}
+
+- (void)paste:(id)sender {
+    [self _endTouchTracking];
+    UIPasteboard *p = [UIPasteboard generalPasteboard];
+    NSAttributedString *atr = nil;
+    
+    if (_allowsPasteAttributedString) {
+        atr = p.yy_AttributedString;
+        if (atr.length == 0) atr = nil;
+    }
+    if (!atr && _allowsPasteImage) {
+        UIImage *img = nil;
+        
+        Class cls = NSClassFromString(@"YYImage");
+        if (cls) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+            if (p.yy_GIFData) {
+                img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_GIFData withObject:nil];
+            }
+            if (!img && p.yy_PNGData) {
+                img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_PNGData withObject:nil];
+            }
+            if (!img && p.yy_WEBPData) {
+                img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_WEBPData withObject:nil];
+            }
+#pragma clang diagnostic pop
+        }
+        
+        if (!img) {
+            img = p.image;
+        }
+        if (!img && p.yy_ImageData) {
+            img = [UIImage imageWithData:p.yy_ImageData scale:YYTextScreenScale()];
+        }
+        if (img && img.size.width > 1 && img.size.height > 1) {
+            id content = img;
+            
+            if (cls) {
+                if ([img conformsToProtocol:NSProtocolFromString(@"YYAnimatedImage")]) {
+                    NSNumber *frameCount = [img valueForKey:@"animatedImageFrameCount"];
+                    if (frameCount.integerValue > 1) {
+                        Class viewCls = NSClassFromString(@"YYAnimatedImageView");
+                        UIImageView *imgView = [(id)viewCls new];
+                        imgView.image = img;
+                        imgView.frame = CGRectMake(0, 0, img.size.width, img.size.height);
+                        if (imgView) {
+                            content = imgView;
+                        }
+                    }
+                }
+            }
+            
+            if ([content isKindOfClass:[UIImage class]] && img.images.count > 1) {
+                UIImageView *imgView = [UIImageView new];
+                imgView.image = img;
+                imgView.frame = CGRectMake(0, 0, img.size.width, img.size.height);
+                if (imgView) {
+                    content = imgView;
+                }
+            }
+            
+            NSMutableAttributedString *attText = [NSAttributedString yy_attachmentStringWithContent:content contentMode:UIViewContentModeScaleToFill width:img.size.width ascent:img.size.height descent:0];
+            NSDictionary *attrs = _typingAttributesHolder.yy_attributes;
+            if (attrs) [attText addAttributes:attrs range:NSMakeRange(0, attText.length)];
+            atr = attText;
+        }
+    }
+    
+    if (atr) {
+        NSUInteger endPosition = _selectedTextRange.start.offset + atr.length;
+        NSMutableAttributedString *text = _innerText.mutableCopy;
+        [text replaceCharactersInRange:_selectedTextRange.asRange withAttributedString:atr];
+        self.attributedText = text;
+        YYTextPosition *pos = [self _correctedTextPosition:[YYTextPosition positionWithOffset:endPosition]];
+        YYTextRange *range = [_innerLayout textRangeByExtendingPosition:pos];
+        range = [self _correctedTextRange:range];
+        if (range) {
+            self.selectedRange = NSMakeRange(range.end.offset, 0);
+        }
+    } else {
+        NSString *string = p.string;
+        if (string.length > 0) {
+            [self _saveToUndoStack];
+            [self _resetRedoStack];
+            [self replaceRange:_selectedTextRange withText:string];
+        }
+    }
+}
+
+- (void)select:(id)sender {
+    [self _endTouchTracking];
+    
+    if (_selectedTextRange.asRange.length > 0 || _innerText.length == 0) return;
+    YYTextRange *newRange = [self _getClosestTokenRangeAtPosition:_selectedTextRange.start];
+    if (newRange.asRange.length > 0) {
+        [_inputDelegate selectionWillChange:self];
+        _selectedTextRange = newRange;
+        [_inputDelegate selectionDidChange:self];
+    }
+    
+    [self _updateIfNeeded];
+    [self _updateOuterProperties];
+    [self _updateSelectionView];
+    [self _hideMenu];
+    [self _showMenu];
+}
+
+- (void)selectAll:(id)sender {
+    _trackingRange = nil;
+    [_inputDelegate selectionWillChange:self];
+    _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+    [_inputDelegate selectionDidChange:self];
+    
+    [self _updateIfNeeded];
+    [self _updateOuterProperties];
+    [self _updateSelectionView];
+    [self _hideMenu];
+    [self _showMenu];
+}
+
+- (void)_define:(id)sender {
+    [self _hideMenu];
+    
+    NSString *string = [_innerText yy_plainTextForRange:_selectedTextRange.asRange];
+    if (string.length == 0) return;
+    BOOL resign = [self resignFirstResponder];
+    if (!resign) return;
+    
+    UIReferenceLibraryViewController* ref = [[UIReferenceLibraryViewController alloc] initWithTerm:string];
+    ref.view.backgroundColor = [UIColor whiteColor];
+    [[self _getRootViewController] presentViewController:ref animated:YES completion:^{}];
+}
+
+
+#pragma mark - Overrice NSObject(NSKeyValueObservingCustomization)
+
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+    static NSSet *keys = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        keys = [NSSet setWithArray:@[
+            @"text",
+            @"font",
+            @"textColor",
+            @"textAlignment",
+            @"dataDetectorTypes",
+            @"linkTextAttributes",
+            @"highlightTextAttributes",
+            @"textParser",
+            @"attributedText",
+            @"textVerticalAlignment",
+            @"textContainerInset",
+            @"exclusionPaths",
+            @"verticalForm",
+            @"linePositionModifier",
+            @"selectedRange",
+            @"typingAttributes"
+        ]];
+    });
+    if ([keys containsObject:key]) {
+        return NO;
+    }
+    return [super automaticallyNotifiesObserversForKey:key];
+}
+
+#pragma mark - @protocol NSCoding
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder {
+    self = [super initWithCoder:aDecoder];
+    [self _initTextView];
+    self.attributedText = [aDecoder decodeObjectForKey:@"attributedText"];
+    self.selectedRange = ((NSValue *)[aDecoder decodeObjectForKey:@"selectedRange"]).rangeValue;
+    self.textVerticalAlignment = [aDecoder decodeIntegerForKey:@"textVerticalAlignment"];
+    self.dataDetectorTypes = [aDecoder decodeIntegerForKey:@"dataDetectorTypes"];
+    self.textContainerInset = ((NSValue *)[aDecoder decodeObjectForKey:@"textContainerInset"]).UIEdgeInsetsValue;
+    self.exclusionPaths = [aDecoder decodeObjectForKey:@"exclusionPaths"];
+    self.verticalForm = [aDecoder decodeBoolForKey:@"verticalForm"];
+    return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [super encodeWithCoder:aCoder];
+    [aCoder encodeObject:self.attributedText forKey:@"attributedText"];
+    [aCoder encodeObject:[NSValue valueWithRange:self.selectedRange] forKey:@"selectedRange"];
+    [aCoder encodeInteger:self.textVerticalAlignment forKey:@"textVerticalAlignment"];
+    [aCoder encodeInteger:self.dataDetectorTypes forKey:@"dataDetectorTypes"];
+    [aCoder encodeUIEdgeInsets:self.textContainerInset forKey:@"textContainerInset"];
+    [aCoder encodeObject:self.exclusionPaths forKey:@"exclusionPaths"];
+    [aCoder encodeBool:self.verticalForm forKey:@"verticalForm"];
+}
+
+#pragma mark - @protocol UIScrollViewDelegate
+
+- (id<YYTextViewDelegate>)delegate {
+    return _outerDelegate;
+}
+
+- (void)setDelegate:(id<YYTextViewDelegate>)delegate {
+    _outerDelegate = delegate;
+}
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
+    [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
+    
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidScroll:scrollView];
+    }
+}
+
+- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidZoom:scrollView];
+    }
+}
+
+- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewWillBeginDragging:scrollView];
+    }
+}
+
+- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
+    }
+}
+
+- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
+    if (!decelerate) {
+        [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+    }
+    
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
+    }
+}
+
+- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewWillBeginDecelerating:scrollView];
+    }
+}
+
+- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
+    [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+    
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidEndDecelerating:scrollView];
+    }
+}
+
+- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidEndScrollingAnimation:scrollView];
+    }
+}
+
+- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        return [_outerDelegate viewForZoomingInScrollView:scrollView];
+    } else {
+        return nil;
+    }
+}
+
+- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view{
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewWillBeginZooming:scrollView withView:view];
+    }
+}
+
+- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidEndZooming:scrollView withView:view atScale:scale];
+    }
+}
+
+- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        return [_outerDelegate scrollViewShouldScrollToTop:scrollView];
+    }
+    return YES;
+}
+
+- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidScrollToTop:scrollView];
+    }
+}
+
+#pragma mark - @protocol YYTextKeyboardObserver
+
+- (void)keyboardChangedWithTransition:(YYTextKeyboardTransition)transition {
+    [self _keyboardChanged];
+}
+
+#pragma mark - @protocol UIALertViewDelegate
+
+- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
+    NSString *title = [alertView buttonTitleAtIndex:buttonIndex];
+    if (title.length == 0) return;
+    NSArray *strings = [self _localizedUndoStrings];
+    if ([title isEqualToString:strings[1]] || [title isEqualToString:strings[2]]) {
+        [self _redo];
+    } else if ([title isEqualToString:strings[3]] || [title isEqualToString:strings[4]]) {
+        [self _undo];
+    }
+    [self _restoreFirstResponderAfterUndoAlert];
+}
+
+#pragma mark - @protocol UIKeyInput
+
+- (BOOL)hasText {
+    return _innerText.length > 0;
+}
+
+- (void)insertText:(NSString *)text {
+    if (text.length == 0) return;
+    if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
+        [self _saveToUndoStack];
+        [self _resetRedoStack];
+    }
+    [self replaceRange:_selectedTextRange withText:text];
+}
+
+- (void)deleteBackward {
+    [self _updateIfNeeded];
+    NSRange range = _selectedTextRange.asRange;
+    if (range.location == 0 && range.length == 0) return;
+    _state.typingAttributesOnce = NO;
+    
+    // test if there's 'TextBinding' before the caret
+    if (!_state.deleteConfirm && range.length == 0 && range.location > 0) {
+        NSRange effectiveRange;
+        YYTextBinding *binding = [_innerText attribute:YYTextBindingAttributeName atIndex:range.location - 1 longestEffectiveRange:&effectiveRange inRange:NSMakeRange(0, _innerText.length)];
+        if (binding && binding.deleteConfirm) {
+            _state.deleteConfirm = YES;
+            [_inputDelegate selectionWillChange:self];
+            _selectedTextRange = [YYTextRange rangeWithRange:effectiveRange];
+            _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
+            [_inputDelegate selectionDidChange:self];
+            
+            [self _updateOuterProperties];
+            [self _updateSelectionView];
+            return;
+        }
+    }
+    
+    _state.deleteConfirm = NO;
+    if (range.length == 0) {
+        YYTextRange *extendRange = [_innerLayout textRangeByExtendingPosition:_selectedTextRange.end inDirection:UITextLayoutDirectionLeft offset:1];
+        if ([self _isTextRangeValid:extendRange]) {
+            range = extendRange.asRange;
+        }
+    }
+    if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
+        [self _saveToUndoStack];
+        [self _resetRedoStack];
+    }
+    [self replaceRange:[YYTextRange rangeWithRange:range] withText:@""];
+}
+
+#pragma mark - @protocol UITextInput
+
+- (void)setInputDelegate:(id<UITextInputDelegate>)inputDelegate {
+    _inputDelegate = inputDelegate;
+}
+
+- (void)setSelectedTextRange:(YYTextRange *)selectedTextRange {
+    if (!selectedTextRange) return;
+    selectedTextRange = [self _correctedTextRange:selectedTextRange];
+    if ([selectedTextRange isEqual:_selectedTextRange]) return;
+    [self _updateIfNeeded];
+    [self _endTouchTracking];
+    [self _hideMenu];
+    _state.deleteConfirm = NO;
+    _state.typingAttributesOnce = NO;
+    
+    [_inputDelegate selectionWillChange:self];
+    _selectedTextRange = selectedTextRange;
+    _lastTypeRange = _selectedTextRange.asRange;
+    [_inputDelegate selectionDidChange:self];
+    
+    [self _updateOuterProperties];
+    [self _updateSelectionView];
+    
+    if (self.isFirstResponder) {
+        [self _scrollRangeToVisible:_selectedTextRange];
+    }
+}
+
+- (void)setMarkedTextStyle:(NSDictionary *)markedTextStyle {
+    _markedTextStyle = markedTextStyle.copy;
+}
+
+/*
+ Replace current markedText with the new markedText
+ @param markedText     New marked text.
+ @param selectedRange  The range from the '_markedTextRange'
+ */
+- (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange {
+    [self _updateIfNeeded];
+    [self _endTouchTracking];
+    [self _hideMenu];
+    
+    if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
+        NSRange range = _markedTextRange ? _markedTextRange.asRange : NSMakeRange(_selectedTextRange.end.offset, 0);
+        BOOL should = [self.delegate textView:self shouldChangeTextInRange:range replacementText:markedText];
+        if (!should) return;
+    }
+    
+    
+    if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
+        [self _saveToUndoStack];
+        [self _resetRedoStack];
+    }
+    
+    BOOL needApplyHolderAttribute = NO;
+    if (_innerText.length > 0 && _markedTextRange) {
+        [self _updateAttributesHolder];
+    } else {
+        needApplyHolderAttribute = YES;
+    }
+    
+    if (_selectedTextRange.asRange.length > 0) {
+        [self replaceRange:_selectedTextRange withText:@""];
+    }
+    
+    [_inputDelegate textWillChange:self];
+    [_inputDelegate selectionWillChange:self];
+    
+    if (!markedText) markedText = @"";
+    if (_markedTextRange == nil) {
+        _markedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.end.offset, markedText.length)];
+        [_innerText replaceCharactersInRange:NSMakeRange(_selectedTextRange.end.offset, 0) withString:markedText];
+        _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.start.offset + selectedRange.location, selectedRange.length)];
+    } else {
+        _markedTextRange = [self _correctedTextRange:_markedTextRange];
+        [_innerText replaceCharactersInRange:_markedTextRange.asRange withString:markedText];
+        _markedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_markedTextRange.start.offset, markedText.length)];
+        _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_markedTextRange.start.offset + selectedRange.location, selectedRange.length)];
+    }
+    
+    _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
+    _markedTextRange = [self _correctedTextRange:_markedTextRange];
+    if (_markedTextRange.asRange.length == 0) {
+        _markedTextRange = nil;
+    } else {
+        if (needApplyHolderAttribute) {
+            [_innerText setAttributes:_typingAttributesHolder.yy_attributes range:_markedTextRange.asRange];
+        }
+        [_innerText yy_removeDiscontinuousAttributesInRange:_markedTextRange.asRange];
+    }
+    
+    [_inputDelegate selectionDidChange:self];
+    [_inputDelegate textDidChange:self];
+    
+    [self _updateOuterProperties];
+    [self _updateLayout];
+    [self _updateSelectionView];
+    [self _scrollRangeToVisible:_selectedTextRange];
+    
+    if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
+        [self.delegate textViewDidChange:self];
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
+    
+    _lastTypeRange = _selectedTextRange.asRange;
+}
+
+- (void)unmarkText {
+    _markedTextRange = nil;
+    [self _endTouchTracking];
+    [self _hideMenu];
+    if ([self _parseText]) _state.needUpdate = YES;
+    
+    [self _updateIfNeeded];
+    [self _updateOuterProperties];
+    [self _updateSelectionView];
+    [self _scrollRangeToVisible:_selectedTextRange];
+}
+
+- (void)replaceRange:(YYTextRange *)range withText:(NSString *)text {
+    if (!range) return;
+    if (!text) text = @"";
+    if (range.asRange.length == 0 && text.length == 0) return;
+    range = [self _correctedTextRange:range];
+    
+    if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
+        BOOL should = [self.delegate textView:self shouldChangeTextInRange:range.asRange replacementText:text];
+        if (!should) return;
+    }
+    
+    BOOL useInnerAttributes = NO;
+    if (_innerText.length > 0) {
+        if (range.start.offset == 0 && range.end.offset == _innerText.length) {
+            if (text.length == 0) {
+                NSMutableDictionary *attrs = [_innerText yy_attributesAtIndex:0].mutableCopy;
+                [attrs removeObjectsForKeys:[NSMutableAttributedString yy_allDiscontinuousAttributeKeys]];
+                _typingAttributesHolder.yy_attributes = attrs;
+            }
+        }
+    } else { // no text
+        useInnerAttributes = YES;
+    }
+    BOOL applyTypingAttributes = NO;
+    if (_state.typingAttributesOnce) {
+        _state.typingAttributesOnce = NO;
+        if (!useInnerAttributes) {
+            if (range.asRange.length == 0 && text.length > 0) {
+                applyTypingAttributes = YES;
+            }
+        }
+    }
+    
+    _state.selectedWithoutEdit = NO;
+    _state.deleteConfirm = NO;
+    [self _endTouchTracking];
+    [self _hideMenu];
+    
+    [self _replaceRange:range withText:text notifyToDelegate:YES];
+    if (useInnerAttributes) {
+        [_innerText yy_setAttributes:_typingAttributesHolder.yy_attributes];
+    } else if (applyTypingAttributes) {
+        NSRange newRange = NSMakeRange(range.asRange.location, text.length);
+        [_typingAttributesHolder.yy_attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+            [_innerText yy_setAttribute:key value:obj range:newRange];
+        }];
+    }
+    [self _parseText];
+    [self _updateOuterProperties];
+    [self _update];
+    
+    if (self.isFirstResponder) {
+        [self _scrollRangeToVisible:_selectedTextRange];
+    }
+    
+    if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
+        [self.delegate textViewDidChange:self];
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
+    
+    _lastTypeRange = _selectedTextRange.asRange;
+}
+
+- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection forRange:(YYTextRange *)range {
+    if (!range) return;
+    range = [self _correctedTextRange:range];
+    [_innerText yy_setBaseWritingDirection:(NSWritingDirection)writingDirection range:range.asRange];
+    [self _commitUpdate];
+}
+
+- (NSString *)textInRange:(YYTextRange *)range {
+    range = [self _correctedTextRange:range];
+    if (!range) return @"";
+    return [_innerText.string substringWithRange:range.asRange];
+}
+
+- (UITextWritingDirection)baseWritingDirectionForPosition:(YYTextPosition *)position inDirection:(UITextStorageDirection)direction {
+    [self _updateIfNeeded];
+    position = [self _correctedTextPosition:position];
+    if (!position) return UITextWritingDirectionNatural;
+    if (_innerText.length == 0) return UITextWritingDirectionNatural;
+    NSUInteger idx = position.offset;
+    if (idx == _innerText.length) idx--;
+    
+    NSDictionary *attrs = [_innerText yy_attributesAtIndex:idx];
+    CTParagraphStyleRef paraStyle = (__bridge CFTypeRef)(attrs[NSParagraphStyleAttributeName]);
+    if (paraStyle) {
+        CTWritingDirection baseWritingDirection;
+        if (CTParagraphStyleGetValueForSpecifier(paraStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(CTWritingDirection), &baseWritingDirection)) {
+            return (UITextWritingDirection)baseWritingDirection;
+        }
+    }
+    
+    return UITextWritingDirectionNatural;
+}
+
+- (YYTextPosition *)beginningOfDocument {
+    return [YYTextPosition positionWithOffset:0];
+}
+
+- (YYTextPosition *)endOfDocument {
+    return [YYTextPosition positionWithOffset:_innerText.length];
+}
+
+- (YYTextPosition *)positionFromPosition:(YYTextPosition *)position offset:(NSInteger)offset {
+    if (offset == 0) return position;
+    
+    NSUInteger location = position.offset;
+    NSInteger newLocation = (NSInteger)location + offset;
+    if (newLocation < 0 || newLocation > _innerText.length) return nil;
+    
+    if (newLocation != 0 && newLocation != _innerText.length) {
+        // fix emoji
+        [self _updateIfNeeded];
+        YYTextRange *extendRange = [_innerLayout textRangeByExtendingPosition:[YYTextPosition positionWithOffset:newLocation]];
+        if (extendRange.asRange.length > 0) {
+            if (offset < 0) {
+                newLocation = extendRange.start.offset;
+            } else {
+                newLocation = extendRange.end.offset;
+            }
+        }
+    }
+    
+    YYTextPosition *p = [YYTextPosition positionWithOffset:newLocation];
+    return [self _correctedTextPosition:p];
+}
+
+- (YYTextPosition *)positionFromPosition:(YYTextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset {
+    [self _updateIfNeeded];
+    YYTextRange *range = [_innerLayout textRangeByExtendingPosition:position inDirection:direction offset:offset];
+    
+    BOOL forward;
+    if (_innerContainer.isVerticalForm) {
+        forward = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionDown;
+    } else {
+        forward = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight;
+    }
+    if (!forward && offset < 0) {
+        forward = -forward;
+    }
+    
+    YYTextPosition *newPosition = forward ? range.end : range.start;
+    if (newPosition.offset > _innerText.length) {
+        newPosition = [YYTextPosition positionWithOffset:_innerText.length affinity:YYTextAffinityBackward];
+    }
+    
+    return [self _correctedTextPosition:newPosition];
+}
+
+- (YYTextRange *)textRangeFromPosition:(YYTextPosition *)fromPosition toPosition:(YYTextPosition *)toPosition {
+    return [YYTextRange rangeWithStart:fromPosition end:toPosition];
+}
+
+- (NSComparisonResult)comparePosition:(YYTextPosition *)position toPosition:(YYTextPosition *)other {
+    return [position compare:other];
+}
+
+- (NSInteger)offsetFromPosition:(YYTextPosition *)from toPosition:(YYTextPosition *)toPosition {
+    return toPosition.offset - from.offset;
+}
+
+- (YYTextPosition *)positionWithinRange:(YYTextRange *)range farthestInDirection:(UITextLayoutDirection)direction {
+    NSRange nsRange = range.asRange;
+    if (direction == UITextLayoutDirectionLeft | direction == UITextLayoutDirectionUp) {
+        return [YYTextPosition positionWithOffset:nsRange.location];
+    } else {
+        return [YYTextPosition positionWithOffset:nsRange.location + nsRange.length affinity:YYTextAffinityBackward];
+    }
+}
+
+- (YYTextRange *)characterRangeByExtendingPosition:(YYTextPosition *)position inDirection:(UITextLayoutDirection)direction {
+    [self _updateIfNeeded];
+    YYTextRange *range = [_innerLayout textRangeByExtendingPosition:position inDirection:direction offset:1];
+    return [self _correctedTextRange:range];
+}
+
+- (YYTextPosition *)closestPositionToPoint:(CGPoint)point {
+    [self _updateIfNeeded];
+    point = [self _convertPointToLayout:point];
+    YYTextPosition *position = [_innerLayout closestPositionToPoint:point];
+    return [self _correctedTextPosition:position];
+}
+
+- (YYTextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(YYTextRange *)range {
+    YYTextPosition *pos = (id)[self closestPositionToPoint:point];
+    if (!pos) return nil;
+    
+    range = [self _correctedTextRange:range];
+    if ([pos compare:range.start] == NSOrderedAscending) {
+        pos = range.start;
+    } else if ([pos compare:range.end] == NSOrderedDescending) {
+        pos = range.end;
+    }
+    return pos;
+}
+
+- (YYTextRange *)characterRangeAtPoint:(CGPoint)point {
+    [self _updateIfNeeded];
+    point = [self _convertPointToLayout:point];
+    YYTextRange *r = [_innerLayout closestTextRangeAtPoint:point];
+    return [self _correctedTextRange:r];
+}
+
+- (CGRect)firstRectForRange:(YYTextRange *)range {
+    [self _updateIfNeeded];
+    CGRect rect = [_innerLayout firstRectForRange:range];
+    if (CGRectIsNull(rect)) rect = CGRectZero;
+    return [self _convertRectFromLayout:rect];
+}
+
+- (CGRect)caretRectForPosition:(YYTextPosition *)position {
+    [self _updateIfNeeded];
+    CGRect caretRect = [_innerLayout caretRectForPosition:position];
+    if (!CGRectIsNull(caretRect)) {
+        caretRect = [self _convertRectFromLayout:caretRect];
+        caretRect = CGRectStandardize(caretRect);
+        if (_verticalForm) {
+            if (caretRect.size.height == 0) {
+                caretRect.size.height = 2;
+                caretRect.origin.y -= 2 * 0.5;
+            }
+            if (caretRect.origin.y < 0) {
+                caretRect.origin.y = 0;
+            } else if (caretRect.origin.y + caretRect.size.height > self.bounds.size.height) {
+                caretRect.origin.y = self.bounds.size.height - caretRect.size.height;
+            }
+        } else {
+            if (caretRect.size.width == 0) {
+                caretRect.size.width = 2;
+                caretRect.origin.x -= 2 * 0.5;
+            }
+            if (caretRect.origin.x < 0) {
+                caretRect.origin.x = 0;
+            } else if (caretRect.origin.x + caretRect.size.width > self.bounds.size.width) {
+                caretRect.origin.x = self.bounds.size.width - caretRect.size.width;
+            }
+        }
+        return YYTextCGRectPixelRound(caretRect);
+    }
+    return CGRectZero;
+}
+
+- (NSArray *)selectionRectsForRange:(YYTextRange *)range {
+    [self _updateIfNeeded];
+    NSArray *rects = [_innerLayout selectionRectsForRange:range];
+    [rects enumerateObjectsUsingBlock:^(YYTextSelectionRect *rect, NSUInteger idx, BOOL *stop) {
+        rect.rect = [self _convertRectFromLayout:rect.rect];
+    }];
+    return rects;
+}
+
+#pragma mark - @protocol UITextInput optional
+
+- (UITextStorageDirection)selectionAffinity {
+    if (_selectedTextRange.end.affinity == YYTextAffinityForward) {
+        return UITextStorageDirectionForward;
+    } else {
+        return UITextStorageDirectionBackward;
+    }
+}
+
+- (void)setSelectionAffinity:(UITextStorageDirection)selectionAffinity {
+    _selectedTextRange = [YYTextRange rangeWithRange:_selectedTextRange.asRange affinity:selectionAffinity == UITextStorageDirectionForward ? YYTextAffinityForward : YYTextAffinityBackward];
+    [self _updateSelectionView];
+}
+
+- (NSDictionary *)textStylingAtPosition:(YYTextPosition *)position inDirection:(UITextStorageDirection)direction {
+    if (!position) return nil;
+    if (_innerText.length == 0) return _typingAttributesHolder.yy_attributes;
+    NSDictionary *attrs = nil;
+    if (0 <= position.offset  && position.offset <= _innerText.length) {
+        NSUInteger ofs = position.offset;
+        if (position.offset == _innerText.length ||
+            direction == UITextStorageDirectionBackward) {
+             ofs--;
+        }
+        attrs = [_innerText attributesAtIndex:ofs effectiveRange:NULL];
+    }
+    return attrs;
+}
+
+- (YYTextPosition *)positionWithinRange:(YYTextRange *)range atCharacterOffset:(NSInteger)offset {
+    if (!range) return nil;
+    if (offset < range.start.offset || offset > range.end.offset) return nil;
+    if (offset == range.start.offset) return range.start;
+    else if (offset == range.end.offset) return range.end;
+    else return [YYTextPosition positionWithOffset:offset];
+}
+
+- (NSInteger)characterOffsetOfPosition:(YYTextPosition *)position withinRange:(YYTextRange *)range {
+    return position ? position.offset : NSNotFound;
+}
+
+@end
+
+
+
+//@interface YYTextView(IBInspectableProperties)
+//@end
+//
+//@implementation YYTextView(IBInspectableProperties)
+//
+//- (BOOL)fontIsBold_:(UIFont *)font {
+//    if (![font respondsToSelector:@selector(fontDescriptor)]) return NO;
+//    return (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) > 0;
+//}
+//
+//- (UIFont *)boldFont_:(UIFont *)font {
+//    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+//    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize];
+//}
+//
+//- (UIFont *)normalFont_:(UIFont *)font {
+//    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+//    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:0] size:font.pointSize];
+//}
+//
+//- (void)setFontName_:(NSString *)fontName {
+//    if (!fontName) return;
+//    UIFont *font = self.font;
+//    if (!font) font = [self _defaultFont];
+//    if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
+//        font = [UIFont systemFontOfSize:font.pointSize];
+//    } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
+//        font = [UIFont boldSystemFontOfSize:font.pointSize];
+//    } else {
+//        if ([self fontIsBold_:font] && ![fontName.lowercaseString containsString:@"bold"]) {
+//            font = [UIFont fontWithName:fontName size:font.pointSize];
+//            font = [self boldFont_:font];
+//        } else {
+//            font = [UIFont fontWithName:fontName size:font.pointSize];
+//        }
+//    }
+//    if (font) self.font = font;
+//}
+//
+//- (void)setFontSize_:(CGFloat)fontSize {
+//    if (fontSize <= 0) return;
+//    UIFont *font = self.font;
+//    if (!font) font = [self _defaultFont];
+//    if (!font) font = [self _defaultFont];
+//    font = [font fontWithSize:fontSize];
+//    if (font) self.font = font;
+//}
+//
+//- (void)setFontIsBold_:(BOOL)fontBold {
+//    UIFont *font = self.font;
+//    if (!font) font = [self _defaultFont];
+//    if ([self fontIsBold_:font] == fontBold) return;
+//    if (fontBold) {
+//        font = [self boldFont_:font];
+//    } else {
+//        font = [self normalFont_:font];
+//    }
+//    if (font) self.font = font;
+//}
+//
+//- (void)setPlaceholderFontName_:(NSString *)fontName {
+//    if (!fontName) return;
+//    UIFont *font = self.placeholderFont;
+//    if (!font) font = [self _defaultFont];
+//    if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
+//        font = [UIFont systemFontOfSize:font.pointSize];
+//    } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
+//        font = [UIFont boldSystemFontOfSize:font.pointSize];
+//    } else {
+//        if ([self fontIsBold_:font] && ![fontName.lowercaseString containsString:@"bold"]) {
+//            font = [UIFont fontWithName:fontName size:font.pointSize];
+//            font = [self boldFont_:font];
+//        } else {
+//            font = [UIFont fontWithName:fontName size:font.pointSize];
+//        }
+//    }
+//    if (font) self.placeholderFont = font;
+//}
+//
+//- (void)setPlaceholderFontSize_:(CGFloat)fontSize {
+//    if (fontSize <= 0) return;
+//    UIFont *font = self.placeholderFont;
+//    if (!font) font = [self _defaultFont];
+//    font = [font fontWithSize:fontSize];
+//    if (font) self.placeholderFont = font;
+//}
+//
+//- (void)setPlaceholderFontIsBold_:(BOOL)fontBold {
+//    UIFont *font = self.placeholderFont;
+//    if (!font) font = [self _defaultFont];
+//    if ([self fontIsBold_:font] == fontBold) return;
+//    if (fontBold) {
+//        font = [self boldFont_:font];
+//    } else {
+//        font = [self normalFont_:font];
+//    }
+//    if (font) self.placeholderFont = font;
+//}
+//
+//- (void)setInsetTop_:(CGFloat)textInsetTop {
+//    UIEdgeInsets insets = self.textContainerInset;
+//    insets.top = textInsetTop;
+//    self.textContainerInset = insets;
+//}
+//
+//- (void)setInsetBottom_:(CGFloat)textInsetBottom {
+//    UIEdgeInsets insets = self.textContainerInset;
+//    insets.bottom = textInsetBottom;
+//    self.textContainerInset = insets;
+//}
+//
+//- (void)setInsetLeft_:(CGFloat)textInsetLeft {
+//    UIEdgeInsets insets = self.textContainerInset;
+//    insets.left = textInsetLeft;
+//    self.textContainerInset = insets;
+//    
+//}
+//
+//- (void)setInsetRight_:(CGFloat)textInsetRight {
+//    UIEdgeInsets insets = self.textContainerInset;
+//    insets.right = textInsetRight;
+//    self.textContainerInset = insets;
+//}
+//
+//- (void)setDebugEnabled_:(BOOL)enabled {
+//    if (!enabled) {
+//        self.debugOption = nil;
+//    } else {
+//        YYTextDebugOption *debugOption = [YYTextDebugOption new];
+//        debugOption.baselineColor = [UIColor redColor];
+//        debugOption.CTFrameBorderColor = [UIColor redColor];
+//        debugOption.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180];
+//        debugOption.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200];
+//        self.debugOption = debugOption;
+//    }
+//}
+//
+//@end

+ 61 - 0
Demo/Objective_C_Demo/YYText/YYTextWeakProxy.h

@@ -0,0 +1,61 @@
+//
+//  YYTextWeakProxy.h
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 14/10/18.
+//  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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ A proxy used to hold a weak object.
+ It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink.
+ 
+ sample code:
+ 
+     @implementation MyView {
+        NSTimer *_timer;
+     }
+     
+     - (void)initTimer {
+        YYTextWeakProxy *proxy = [YYTextWeakProxy proxyWithTarget:self];
+        _timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
+     }
+     
+     - (void)tick:(NSTimer *)timer {...}
+     @end
+ */
+@interface YYTextWeakProxy : NSProxy
+
+/**
+ The proxy target.
+ */
+@property (nullable, nonatomic, weak, readonly) id target;
+
+/**
+ Creates a new weak proxy for target.
+ 
+ @param target Target object.
+ 
+ @return A new proxy object.
+ */
+- (instancetype)initWithTarget:(id)target;
+
+/**
+ Creates a new weak proxy for target.
+ 
+ @param target Target object.
+ 
+ @return A new proxy object.
+ */
++ (instancetype)proxyWithTarget:(id)target;
+
+@end
+
+NS_ASSUME_NONNULL_END

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác