diff --git a/Demo.xcodeproj/project.pbxproj b/Demo.xcodeproj/project.pbxproj index f9fdc64..d6ee52c 100755 --- a/Demo.xcodeproj/project.pbxproj +++ b/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 = ""; }; 4CE611A11B98B7250020591A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4CE74F3F1CF0465C0093AC0C /* YYTextViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextViewController.h; sourceTree = ""; }; + 4CE74F401CF0465C0093AC0C /* YYTextViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextViewController.m; sourceTree = ""; }; + 4CE74F421CF047610093AC0C /* RefreshLayoutViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RefreshLayoutViewController.h; sourceTree = ""; }; + 4CE74F431CF047610093AC0C /* RefreshLayoutViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RefreshLayoutViewController.m; sourceTree = ""; }; + 4CE74F451CF0809C0093AC0C /* LayoutGuideViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutGuideViewController.h; sourceTree = ""; }; + 4CE74F461CF0809C0093AC0C /* LayoutGuideViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutGuideViewController.m; sourceTree = ""; }; + 4CE74F481CF080B60093AC0C /* ChatViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatViewController.h; sourceTree = ""; }; + 4CE74F491CF080B60093AC0C /* ChatViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatViewController.m; sourceTree = ""; }; + 4CE74F4B1CF080C10093AC0C /* TextViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextViewController.h; sourceTree = ""; }; + 4CE74F4C1CF080C10093AC0C /* TextViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextViewController.m; sourceTree = ""; }; + 4CE74F4F1CF185F70093AC0C /* ColorPickerTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ColorPickerTextField.h; sourceTree = ""; }; + 4CE74F501CF185F70093AC0C /* ColorPickerTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ColorPickerTextField.m; sourceTree = ""; }; + 4CE74F521CF185F70093AC0C /* HFColorButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HFColorButton.h; sourceTree = ""; }; + 4CE74F531CF185F70093AC0C /* HFColorButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HFColorButton.m; sourceTree = ""; }; + 4CE74F541CF185F70093AC0C /* HFColorPickerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HFColorPickerView.h; sourceTree = ""; }; + 4CE74F551CF185F70093AC0C /* HFColorPickerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HFColorPickerView.m; sourceTree = ""; }; + 4CE74F561CF185F70093AC0C /* UIColor+HexColors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+HexColors.h"; sourceTree = ""; }; + 4CE74F571CF185F70093AC0C /* UIColor+HexColors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+HexColors.m"; sourceTree = ""; }; + 4CE74F601CF1864E0093AC0C /* ColorTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ColorTableViewCell.h; sourceTree = ""; }; + 4CE74F611CF1864E0093AC0C /* ColorTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ColorTableViewCell.m; sourceTree = ""; }; + 4CE74F631CF186720093AC0C /* TextFieldTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextFieldTableViewCell.h; sourceTree = ""; }; + 4CE74F641CF186720093AC0C /* TextFieldTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextFieldTableViewCell.m; sourceTree = ""; }; + 4CE74F661CF1BBCC0093AC0C /* ImageSwitchTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageSwitchTableViewCell.h; sourceTree = ""; }; + 4CE74F671CF1BBCC0093AC0C /* ImageSwitchTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageSwitchTableViewCell.m; sourceTree = ""; }; + 4CE74F6A1CF206E40093AC0C /* NSAttributedString+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+YYText.h"; sourceTree = ""; }; + 4CE74F6B1CF206E40093AC0C /* NSAttributedString+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+YYText.m"; sourceTree = ""; }; + 4CE74F6C1CF206E40093AC0C /* NSParagraphStyle+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+YYText.h"; sourceTree = ""; }; + 4CE74F6D1CF206E40093AC0C /* NSParagraphStyle+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSParagraphStyle+YYText.m"; sourceTree = ""; }; + 4CE74F6E1CF206E40093AC0C /* UIPasteboard+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIPasteboard+YYText.h"; sourceTree = ""; }; + 4CE74F6F1CF206E40093AC0C /* UIPasteboard+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIPasteboard+YYText.m"; sourceTree = ""; }; + 4CE74F701CF206E40093AC0C /* UIView+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+YYText.h"; sourceTree = ""; }; + 4CE74F711CF206E40093AC0C /* UIView+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+YYText.m"; sourceTree = ""; }; + 4CE74F721CF206E40093AC0C /* YYLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYLabel.h; sourceTree = ""; }; + 4CE74F731CF206E40093AC0C /* YYLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYLabel.m; sourceTree = ""; }; + 4CE74F741CF206E40093AC0C /* YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYText.h; sourceTree = ""; }; + 4CE74F751CF206E40093AC0C /* YYTextArchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextArchiver.h; sourceTree = ""; }; + 4CE74F761CF206E40093AC0C /* YYTextArchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextArchiver.m; sourceTree = ""; }; + 4CE74F771CF206E40093AC0C /* YYTextAsyncLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAsyncLayer.h; sourceTree = ""; }; + 4CE74F781CF206E40093AC0C /* YYTextAsyncLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAsyncLayer.m; sourceTree = ""; }; + 4CE74F791CF206E40093AC0C /* YYTextAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAttribute.h; sourceTree = ""; }; + 4CE74F7A1CF206E40093AC0C /* YYTextAttribute.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAttribute.m; sourceTree = ""; }; + 4CE74F7B1CF206E40093AC0C /* YYTextContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextContainerView.h; sourceTree = ""; }; + 4CE74F7C1CF206E40093AC0C /* YYTextContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextContainerView.m; sourceTree = ""; }; + 4CE74F7D1CF206E40093AC0C /* YYTextDebugOption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextDebugOption.h; sourceTree = ""; }; + 4CE74F7E1CF206E40093AC0C /* YYTextDebugOption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextDebugOption.m; sourceTree = ""; }; + 4CE74F7F1CF206E40093AC0C /* YYTextEffectWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextEffectWindow.h; sourceTree = ""; }; + 4CE74F801CF206E40093AC0C /* YYTextEffectWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextEffectWindow.m; sourceTree = ""; }; + 4CE74F811CF206E40093AC0C /* YYTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextInput.h; sourceTree = ""; }; + 4CE74F821CF206E40093AC0C /* YYTextInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextInput.m; sourceTree = ""; }; + 4CE74F831CF206E40093AC0C /* YYTextKeyboardManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextKeyboardManager.h; sourceTree = ""; }; + 4CE74F841CF206E40093AC0C /* YYTextKeyboardManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextKeyboardManager.m; sourceTree = ""; }; + 4CE74F851CF206E40093AC0C /* YYTextLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextLayout.h; sourceTree = ""; }; + 4CE74F861CF206E40093AC0C /* YYTextLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextLayout.m; sourceTree = ""; }; + 4CE74F871CF206E40093AC0C /* YYTextLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextLine.h; sourceTree = ""; }; + 4CE74F881CF206E40093AC0C /* YYTextLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextLine.m; sourceTree = ""; }; + 4CE74F891CF206E40093AC0C /* YYTextMagnifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextMagnifier.h; sourceTree = ""; }; + 4CE74F8A1CF206E40093AC0C /* YYTextMagnifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextMagnifier.m; sourceTree = ""; }; + 4CE74F8B1CF206E40093AC0C /* YYTextParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextParser.h; sourceTree = ""; }; + 4CE74F8C1CF206E40093AC0C /* YYTextParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextParser.m; sourceTree = ""; }; + 4CE74F8D1CF206E40093AC0C /* YYTextRubyAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRubyAnnotation.h; sourceTree = ""; }; + 4CE74F8E1CF206E40093AC0C /* YYTextRubyAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRubyAnnotation.m; sourceTree = ""; }; + 4CE74F8F1CF206E40093AC0C /* YYTextRunDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRunDelegate.h; sourceTree = ""; }; + 4CE74F901CF206E40093AC0C /* YYTextRunDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRunDelegate.m; sourceTree = ""; }; + 4CE74F911CF206E40093AC0C /* YYTextSelectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextSelectionView.h; sourceTree = ""; }; + 4CE74F921CF206E40093AC0C /* YYTextSelectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextSelectionView.m; sourceTree = ""; }; + 4CE74F931CF206E40093AC0C /* YYTextTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextTransaction.h; sourceTree = ""; }; + 4CE74F941CF206E40093AC0C /* YYTextTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextTransaction.m; sourceTree = ""; }; + 4CE74F951CF206E40093AC0C /* YYTextUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextUtilities.h; sourceTree = ""; }; + 4CE74F961CF206E40093AC0C /* YYTextUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextUtilities.m; sourceTree = ""; }; + 4CE74F971CF206E40093AC0C /* YYTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextView.h; sourceTree = ""; }; + 4CE74F981CF206E40093AC0C /* YYTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextView.m; sourceTree = ""; }; + 4CE74F991CF206E40093AC0C /* YYTextWeakProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextWeakProxy.h; sourceTree = ""; }; + 4CE74F9A1CF206E40093AC0C /* YYTextWeakProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextWeakProxy.m; sourceTree = ""; }; + 4CE74FCB1CF2174E0093AC0C /* ChatTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatTableViewCell.h; sourceTree = ""; }; + 4CE74FCC1CF2174E0093AC0C /* ChatTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatTableViewCell.m; sourceTree = ""; }; 4CEC3A581B8CECDF00909DCA /* IQNSArray+Sort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IQNSArray+Sort.swift"; sourceTree = ""; }; 4CEC3A591B8CECDF00909DCA /* IQUITextFieldView+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IQUITextFieldView+Additions.swift"; sourceTree = ""; }; 4CEC3A5A1B8CECDF00909DCA /* IQUIView+Hierarchy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IQUIView+Hierarchy.swift"; sourceTree = ""; }; @@ -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 = ""; }; C0017B781BAD943400BD1D70 /* CustomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomViewController.swift; sourceTree = ""; }; C03C87F41B8DCBF100295DFA /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; C03C87F61B8DCC1400295DFA /* OptionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionsViewController.swift; sourceTree = ""; }; @@ -167,8 +303,6 @@ C0CB61A91B884D4100C33368 /* BottomBlankSpaceViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BottomBlankSpaceViewController.m; sourceTree = ""; }; C0CB61AA1B884D4100C33368 /* CollectionViewDemoController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CollectionViewDemoController.h; sourceTree = ""; }; C0CB61AB1B884D4100C33368 /* CollectionViewDemoController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CollectionViewDemoController.m; sourceTree = ""; }; - C0CB61AC1B884D4100C33368 /* CustomSubclassView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomSubclassView.h; sourceTree = ""; }; - C0CB61AD1B884D4100C33368 /* CustomSubclassView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSubclassView.m; sourceTree = ""; }; C0CB61AE1B884D4100C33368 /* CustomViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomViewController.h; sourceTree = ""; }; C0CB61AF1B884D4100C33368 /* CustomViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomViewController.m; sourceTree = ""; }; C0CB61B01B884D4100C33368 /* ExampleTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExampleTableViewController.h; sourceTree = ""; }; @@ -286,6 +420,86 @@ path = DemoObjCUITests; sourceTree = ""; }; + 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 = ""; + }; + 4CE74F511CF185F70093AC0C /* HFColorPicker */ = { + isa = PBXGroup; + children = ( + 4CE74F521CF185F70093AC0C /* HFColorButton.h */, + 4CE74F531CF185F70093AC0C /* HFColorButton.m */, + 4CE74F541CF185F70093AC0C /* HFColorPickerView.h */, + 4CE74F551CF185F70093AC0C /* HFColorPickerView.m */, + ); + path = HFColorPicker; + sourceTree = ""; + }; + 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 = ""; + }; 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 = ""; @@ -465,7 +687,9 @@ C0CB61A31B884D4100C33368 /* Third Party */ = { isa = PBXGroup; children = ( + 4CE74F4E1CF185F70093AC0C /* Color Picker TextField */, C0CB61A41B884D4100C33368 /* IQDropDownTextField */, + 4CE74F691CF206E40093AC0C /* YYText */, ); path = "Third Party"; sourceTree = ""; @@ -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 = ""; @@ -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"; diff --git a/Demo/Objective_C_Demo/Cell/ChatTableViewCell.h b/Demo/Objective_C_Demo/Cell/ChatTableViewCell.h new file mode 100644 index 0000000..dbb4f82 --- /dev/null +++ b/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 + +@interface ChatLabel : UILabel + +@end + +@interface ChatTableViewCell : UITableViewCell + +@property (strong, nonatomic) IBOutlet ChatLabel *chatLabel; + +@end diff --git a/Demo/Objective_C_Demo/Cell/ChatTableViewCell.m b/Demo/Objective_C_Demo/Cell/ChatTableViewCell.m new file mode 100644 index 0000000..e8d0060 --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/Cell/ColorTableViewCell.h b/Demo/Objective_C_Demo/Cell/ColorTableViewCell.h new file mode 100644 index 0000000..e7fe258 --- /dev/null +++ b/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 +#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 diff --git a/Demo/Objective_C_Demo/Cell/ColorTableViewCell.m b/Demo/Objective_C_Demo/Cell/ColorTableViewCell.m new file mode 100644 index 0000000..e86ac2b --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/Cell/ImageSwitchTableViewCell.h b/Demo/Objective_C_Demo/Cell/ImageSwitchTableViewCell.h new file mode 100644 index 0000000..5bb0a03 --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/Cell/ImageSwitchTableViewCell.m b/Demo/Objective_C_Demo/Cell/ImageSwitchTableViewCell.m new file mode 100644 index 0000000..898f42e --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/Cell/NavigationTableViewCell.m b/Demo/Objective_C_Demo/Cell/NavigationTableViewCell.m index c0e6333..0a69948 100644 --- a/Demo/Objective_C_Demo/Cell/NavigationTableViewCell.m +++ b/Demo/Objective_C_Demo/Cell/NavigationTableViewCell.m @@ -14,7 +14,6 @@ - (void)awakeFromNib { [super awakeFromNib]; - self.backgroundColor = [UIColor clearColor]; } @end diff --git a/Demo/Objective_C_Demo/Cell/OptionTableViewCell.m b/Demo/Objective_C_Demo/Cell/OptionTableViewCell.m index dd6cb91..b570f58 100644 --- a/Demo/Objective_C_Demo/Cell/OptionTableViewCell.m +++ b/Demo/Objective_C_Demo/Cell/OptionTableViewCell.m @@ -14,7 +14,6 @@ - (void)awakeFromNib { [super awakeFromNib]; - self.backgroundColor = [UIColor clearColor]; } @end diff --git a/Demo/Objective_C_Demo/Cell/StepperTableViewCell.m b/Demo/Objective_C_Demo/Cell/StepperTableViewCell.m index 97f8adb..8aa3524 100644 --- a/Demo/Objective_C_Demo/Cell/StepperTableViewCell.m +++ b/Demo/Objective_C_Demo/Cell/StepperTableViewCell.m @@ -15,7 +15,6 @@ - (void)awakeFromNib { [super awakeFromNib]; - self.backgroundColor = [UIColor clearColor]; } @end diff --git a/Demo/Objective_C_Demo/Cell/SwitchTableViewCell.m b/Demo/Objective_C_Demo/Cell/SwitchTableViewCell.m index d729f6b..ad514ca 100644 --- a/Demo/Objective_C_Demo/Cell/SwitchTableViewCell.m +++ b/Demo/Objective_C_Demo/Cell/SwitchTableViewCell.m @@ -14,7 +14,6 @@ - (void)awakeFromNib { [super awakeFromNib]; - self.backgroundColor = [UIColor clearColor]; } @end diff --git a/Demo/Objective_C_Demo/Cell/TextFieldTableViewCell.h b/Demo/Objective_C_Demo/Cell/TextFieldTableViewCell.h new file mode 100644 index 0000000..751b1c7 --- /dev/null +++ b/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 + +@interface TextFieldTableViewCell : UITableViewCell + +@property (strong, nonatomic) IBOutlet UILabel *labelTitle; +@property (strong, nonatomic) IBOutlet UILabel *labelSubtitle; + +@property (strong, nonatomic) IBOutlet UITextField *textField; + +@end diff --git a/Demo/Objective_C_Demo/Cell/TextFieldTableViewCell.m b/Demo/Objective_C_Demo/Cell/TextFieldTableViewCell.m new file mode 100644 index 0000000..1869dea --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/Resources/Info.plist b/Demo/Objective_C_Demo/Resources/Info.plist index 674f40b..dce5525 100755 --- a/Demo/Objective_C_Demo/Resources/Info.plist +++ b/Demo/Objective_C_Demo/Resources/Info.plist @@ -31,7 +31,7 @@ armv7 UIStatusBarStyle - UIStatusBarStyleLightContent + UIStatusBarStyleDefault UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/Demo/Objective_C_Demo/Storyboard/Main.storyboard b/Demo/Objective_C_Demo/Storyboard/Main.storyboard index dc8c51a..b9111b6 100644 --- a/Demo/Objective_C_Demo/Storyboard/Main.storyboard +++ b/Demo/Objective_C_Demo/Storyboard/Main.storyboard @@ -1,132 +1,288 @@ - + - + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - @@ -135,58 +291,79 @@ - + + + + + - + - - + + + + + + + - + + + + + + + - + + + + + - + - - + + + + @@ -195,7 +372,13 @@ - + + + + + + + @@ -207,13 +390,16 @@ + + + + - - - + + @@ -221,9 +407,8 @@ - - - + + @@ -231,9 +416,8 @@ - - - + + @@ -243,81 +427,149 @@ + + + + + + + + + + + + + + - + + + + + + + - + + + + + - - - + + + - - - + + - - - + + + + + + + + + - - - + + - - - + + + + + + + + + + + + + - - - - - 2 There are also nice optional features allowing you to customize the distance from the text field, add the next/previous done button as a keyboard UIToolbar, play sounds when the user navigations through the form and more. + + + + 2 There are also nice optional features allowing you to customize the distance from the text field, add the next/previous done button as a keyboard UIToolbar, play sounds when the user navigations through the form and more. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + @@ -328,182 +580,267 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + + + + + - - - + + - - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + - - - - + + + + + + - - - + + + - - - - + + + + + + - - - + + - - - + + - - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + @@ -518,83 +855,254 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + - - - + - + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - + + @@ -616,15 +1124,14 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - + - - + + @@ -646,166 +1153,44 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + - - - - - - - - - - - - - - - - - - - + + @@ -828,75 +1213,14 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -918,41 +1242,194 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - + - - + + + + + + - + - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -969,26 +1446,22 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - - - - - + - - + + + + + - - @@ -996,133 +1469,364 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + - - + - - + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + - + - - - - - + + - - - + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + - + - - - + + - - + + - + - - + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1140,35 +1844,38 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - - + + - + - + - + - + + + + + - - + @@ -1183,18 +1890,16 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + - - - + @@ -1205,13 +1910,19 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - - + + + + + + + + + @@ -1222,14 +1933,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - - - + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + @@ -1240,13 +1957,19 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - - + + + + + + + + + @@ -1257,14 +1980,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - - - + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + @@ -1275,13 +2004,19 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + + + + + + + @@ -1292,14 +2027,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - - + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + @@ -1310,13 +2051,19 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + + + + + + + @@ -1327,14 +2074,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - - + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + @@ -1345,13 +2098,19 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + + + + + + + @@ -1362,14 +2121,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - - + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + @@ -1380,13 +2145,19 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + + + + + + + @@ -1397,14 +2168,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - - + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + @@ -1415,13 +2192,19 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + + + + + + + @@ -1432,14 +2215,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - - + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + @@ -1450,13 +2239,19 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + + + + + + + @@ -1467,14 +2262,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - - + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + @@ -1486,260 +2287,187 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + + + + + + + - + - + + + + + - - - - - - - - - - - + + + - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - - + + + - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TextField enabled. Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextField enabled. Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, Long Text Test, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + @@ -1767,56 +2709,68 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + + + + + - - - - + + + - + + + + + + + - + + + + + + + - + - + + + + + - - - - - - `IQKeyboardManager` allows you to prevent issues of the keyboard sliding up and covering a text field without needing you to enter any code. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + `IQKeyboardManager` allows you to prevent issues of the keyboard sliding up and covering a text field without needing you to enter any code.

Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + One of the Speciality of this Library is `It Works Automatically`.

Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + `IQKeyboardManager` works on all orientations, and with the toolbar.

Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + `ZERO LINE OF CODE`, `No More imports`, `No More Subclasses`, `No More Manual Work`.

Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + To use `IQKeyboardManager` you simply need to add the framework to your project or add the source files to your project.

Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - + - + - + - - - - - @@ -1913,126 +2899,184 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + - + + + + + - - - - + + + - - - + + - - - - + + + + + + + + + + + + - + - - - - + + + + + + + + + - + + + + + + + + + + + + + - + + + + + + + - + + + + + - - - - - + + + - - - + + + - - + + + + + + + + + + + + + + + + + - + + + + + + + - + - + + + + + - + - - + @@ -2047,15 +3091,20 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - - + + + + + + + + @@ -2064,170 +3113,679 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + + + + + + + - + + + + + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + + - - - + + + + + + + + - - - + + - - - + + + + + + + + - - - + + - - - + + + + + + + + - - - + + - - - + + + + + + + + - - - + + - - - + + + + + + + + - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + - + + + + + + + - - + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + @@ -2238,6 +3796,6 @@ textField.inputAcessoryView = [[UIView alloc] init]; + - diff --git a/Demo/Objective_C_Demo/Third Party/Color Picker TextField/ColorPickerTextField.h b/Demo/Objective_C_Demo/Third Party/Color Picker TextField/ColorPickerTextField.h new file mode 100644 index 0000000..70bd151 --- /dev/null +++ b/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 + +@class ColorPickerTextField; + +@protocol ColorPickerTextFieldDelegate + +@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 delegate; + +@end diff --git a/Demo/Objective_C_Demo/Third Party/Color Picker TextField/ColorPickerTextField.m b/Demo/Objective_C_Demo/Third Party/Color Picker TextField/ColorPickerTextField.m new file mode 100644 index 0000000..7e79da2 --- /dev/null +++ b/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 () + +@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 diff --git a/Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorButton.h b/Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorButton.h new file mode 100755 index 0000000..d0d7562 --- /dev/null +++ b/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 + +@interface HFColorButton : UIButton + +@property (nonatomic, strong) UIColor* color; + +@end diff --git a/Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorButton.m b/Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorButton.m new file mode 100755 index 0000000..328e7c2 --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorPickerView.h b/Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorPickerView.h new file mode 100755 index 0000000..e1ee018 --- /dev/null +++ b/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 + +@protocol HFColorPickerViewDelegate; + +@interface HFColorPickerView : UIView + +@property (nonatomic, assign) IBOutlet id delegate; +@property (nonatomic, strong) NSArray* colors; +@property (nonatomic) CGFloat buttonDiameter; +@property (nonatomic) NSInteger selectedIndex; +@property (nonatomic) NSUInteger numberOfColorsPerRow; + +@end + + +@protocol HFColorPickerViewDelegate + +- (void)colorPicker:(HFColorPickerView*)colorPickerView selectedColor:(UIColor*)selectedColor; + +@end \ No newline at end of file diff --git a/Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorPickerView.m b/Demo/Objective_C_Demo/Third Party/Color Picker TextField/HFColorPicker/HFColorPickerView.m new file mode 100755 index 0000000..6a9f4d2 --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/Third Party/Color Picker TextField/UIColor+HexColors.h b/Demo/Objective_C_Demo/Third Party/Color Picker TextField/UIColor+HexColors.h new file mode 100755 index 0000000..aabba8c --- /dev/null +++ b/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 + +@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 diff --git a/Demo/Objective_C_Demo/Third Party/Color Picker TextField/UIColor+HexColors.m b/Demo/Objective_C_Demo/Third Party/Color Picker TextField/UIColor+HexColors.m new file mode 100755 index 0000000..e12811f --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/ViewController/BottomBlankSpaceViewController.m b/Demo/Objective_C_Demo/ViewController/BottomBlankSpaceViewController.m index ef41012..f017638 100644 --- a/Demo/Objective_C_Demo/ViewController/BottomBlankSpaceViewController.m +++ b/Demo/Objective_C_Demo/ViewController/BottomBlankSpaceViewController.m @@ -9,6 +9,10 @@ #import "BottomBlankSpaceViewController.h" #import "IQKeyboardManager.h" +@interface BottomBlankSpaceViewController () + +@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; diff --git a/Demo/Objective_C_Demo/ViewController/ChatViewController.h b/Demo/Objective_C_Demo/ViewController/ChatViewController.h new file mode 100644 index 0000000..ee97da5 --- /dev/null +++ b/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 + +@interface ChatViewController : UIViewController + +@end diff --git a/Demo/Objective_C_Demo/ViewController/ChatViewController.m b/Demo/Objective_C_Demo/ViewController/ChatViewController.m new file mode 100644 index 0000000..415c060 --- /dev/null +++ b/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 () + +@property (strong, nonatomic) IBOutlet UITableView *tableView; + +@end + +@implementation ChatViewController +{ + NSMutableArray *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 diff --git a/Demo/Objective_C_Demo/ViewController/CollectionViewDemoController.m b/Demo/Objective_C_Demo/ViewController/CollectionViewDemoController.m index 7efa1d1..ed52d67 100644 --- a/Demo/Objective_C_Demo/ViewController/CollectionViewDemoController.m +++ b/Demo/Objective_C_Demo/ViewController/CollectionViewDemoController.m @@ -8,7 +8,7 @@ #import "CollectionViewDemoController.h" -@interface CollectionViewDemoController () +@interface CollectionViewDemoController () @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 { diff --git a/Demo/Objective_C_Demo/ViewController/CustomSubclassView.h b/Demo/Objective_C_Demo/ViewController/CustomSubclassView.h deleted file mode 100644 index d347bd5..0000000 --- a/Demo/Objective_C_Demo/ViewController/CustomSubclassView.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// CustomSubclassView.h -// IQKeyboardManager -// -// Created by InfoEnum02 on 21/04/15. -// Copyright (c) 2015 Iftekhar. All rights reserved. -// - -#import - -@interface CustomSubclassView : UIView - -@end diff --git a/Demo/Objective_C_Demo/ViewController/CustomSubclassView.m b/Demo/Objective_C_Demo/ViewController/CustomSubclassView.m deleted file mode 100644 index a0a0137..0000000 --- a/Demo/Objective_C_Demo/ViewController/CustomSubclassView.m +++ /dev/null @@ -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 diff --git a/Demo/Objective_C_Demo/ViewController/CustomViewController.m b/Demo/Objective_C_Demo/ViewController/CustomViewController.m index e00da56..411c970 100644 --- a/Demo/Objective_C_Demo/ViewController/CustomViewController.m +++ b/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 () { + 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] toolbarPreviousNextAllowedClasses] addObject:[CustomSubclassView class]]; + [[[IQKeyboardManager sharedManager] enabledToolbarClasses] addObject:[self class]]; } else { - [[[IQKeyboardManager sharedManager] toolbarPreviousNextAllowedClasses] removeObject:[CustomSubclassView class]]; + [[[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] enabledTouchResignedClasses] addObject:[self class]]; + } + else + { + [[[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 diff --git a/Demo/Objective_C_Demo/ViewController/ExampleTableViewController.m b/Demo/Objective_C_Demo/ViewController/ExampleTableViewController.m index 1b54a8a..f7b90e1 100644 --- a/Demo/Objective_C_Demo/ViewController/ExampleTableViewController.m +++ b/Demo/Objective_C_Demo/ViewController/ExampleTableViewController.m @@ -8,7 +8,7 @@ #import "ExampleTableViewController.h" -@interface ExampleTableViewController () +@interface ExampleTableViewController () @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 diff --git a/Demo/Objective_C_Demo/ViewController/LayoutGuideViewController.h b/Demo/Objective_C_Demo/ViewController/LayoutGuideViewController.h new file mode 100644 index 0000000..c76877a --- /dev/null +++ b/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 + +@interface LayoutGuideViewController : UIViewController + +@end diff --git a/Demo/Objective_C_Demo/ViewController/LayoutGuideViewController.m b/Demo/Objective_C_Demo/ViewController/LayoutGuideViewController.m new file mode 100644 index 0000000..da17b22 --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/ViewController/ManualToolbarViewController.m b/Demo/Objective_C_Demo/ViewController/ManualToolbarViewController.m index 9e4ee33..e0225d8 100644 --- a/Demo/Objective_C_Demo/ViewController/ManualToolbarViewController.m +++ b/Demo/Objective_C_Demo/ViewController/ManualToolbarViewController.m @@ -10,7 +10,7 @@ #import "IQUIView+IQKeyboardToolbar.h" -@interface ManualToolbarViewController () +@interface ManualToolbarViewController () -(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 { diff --git a/Demo/Objective_C_Demo/ViewController/NavigationBarViewController.m b/Demo/Objective_C_Demo/ViewController/NavigationBarViewController.m index 08aece6..c41c0bd 100644 --- a/Demo/Objective_C_Demo/ViewController/NavigationBarViewController.m +++ b/Demo/Objective_C_Demo/ViewController/NavigationBarViewController.m @@ -3,27 +3,72 @@ // IQKeyboard #import "NavigationBarViewController.h" +#import "IQKeyboardReturnKeyHandler.h" +#import "IQUIView+IQKeyboardToolbar.h" -@interface NavigationBarViewController () +@interface NavigationBarViewController () @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]; +} - scrollView.contentSize = self.view.bounds.size; +- (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; + } +} + +- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller +{ + return UIModalPresentationNone; +} + +-(void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController +{ + [self.view endEditing:YES]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation diff --git a/Demo/Objective_C_Demo/ViewController/RefreshLayoutViewController.h b/Demo/Objective_C_Demo/ViewController/RefreshLayoutViewController.h new file mode 100644 index 0000000..ad733f5 --- /dev/null +++ b/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 + +@interface RefreshLayoutViewController : UIViewController + +@end diff --git a/Demo/Objective_C_Demo/ViewController/RefreshLayoutViewController.m b/Demo/Objective_C_Demo/ViewController/RefreshLayoutViewController.m new file mode 100644 index 0000000..2a289be --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/ViewController/ScrollViewController.m b/Demo/Objective_C_Demo/ViewController/ScrollViewController.m index 287fcbc..3787261 100644 --- a/Demo/Objective_C_Demo/ViewController/ScrollViewController.m +++ b/Demo/Objective_C_Demo/ViewController/ScrollViewController.m @@ -4,6 +4,10 @@ #import "ScrollViewController.h" +@interface ScrollViewController () + +@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 { diff --git a/Demo/Objective_C_Demo/ViewController/Settings/SettingsViewController.m b/Demo/Objective_C_Demo/ViewController/Settings/SettingsViewController.m index 4c5a2f8..b58a60f 100644 --- a/Demo/Objective_C_Demo/ViewController/Settings/SettingsViewController.m +++ b/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 () +@interface SettingsViewController () @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]; } diff --git a/Demo/Objective_C_Demo/ViewController/SpecialCaseViewController.m b/Demo/Objective_C_Demo/ViewController/SpecialCaseViewController.m index 2c69db8..32e1a91 100644 --- a/Demo/Objective_C_Demo/ViewController/SpecialCaseViewController.m +++ b/Demo/Objective_C_Demo/ViewController/SpecialCaseViewController.m @@ -7,7 +7,7 @@ #import "IQKeyboardManager.h" #import "IQUIView+IQKeyboardToolbar.h" -@interface SpecialCaseViewController () +@interface SpecialCaseViewController () -(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; diff --git a/Demo/Objective_C_Demo/ViewController/TableViewInContainerViewController.m b/Demo/Objective_C_Demo/ViewController/TableViewInContainerViewController.m index a5db193..a42c21d 100644 --- a/Demo/Objective_C_Demo/ViewController/TableViewInContainerViewController.m +++ b/Demo/Objective_C_Demo/ViewController/TableViewInContainerViewController.m @@ -8,6 +8,10 @@ #import "TableViewInContainerViewController.h" +@interface TableViewInContainerViewController () + +@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 { diff --git a/Demo/Objective_C_Demo/ViewController/TextFieldViewController.h b/Demo/Objective_C_Demo/ViewController/TextFieldViewController.h index f63a793..bc661ee 100644 --- a/Demo/Objective_C_Demo/ViewController/TextFieldViewController.h +++ b/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 diff --git a/Demo/Objective_C_Demo/ViewController/TextFieldViewController.m b/Demo/Objective_C_Demo/ViewController/TextFieldViewController.m index 8907779..4c1c1c2 100644 --- a/Demo/Objective_C_Demo/ViewController/TextFieldViewController.m +++ b/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 () @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"; + [textField3 setCustomPreviousTarget:self action:@selector(previousAction:)]; + [textField3 setCustomNextTarget:self action:@selector(nextAction:)]; + [textField3 setCustomDoneTarget:self action:@selector(doneAction:)]; - [dropDownTextField setCustomPreviousTarget:self action:@selector(previousAction:)]; - [dropDownTextField setCustomNextTarget:self action:@selector(nextAction:)]; - [dropDownTextField 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; diff --git a/Demo/Objective_C_Demo/ViewController/TextSelectionViewController.m b/Demo/Objective_C_Demo/ViewController/TextSelectionViewController.m index d5264c5..c099ab0 100755 --- a/Demo/Objective_C_Demo/ViewController/TextSelectionViewController.m +++ b/Demo/Objective_C_Demo/ViewController/TextSelectionViewController.m @@ -4,7 +4,7 @@ #import "TextSelectionViewController.h" -@interface TextSelectionViewController () +@interface TextSelectionViewController () @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; diff --git a/Demo/Objective_C_Demo/ViewController/TextViewController.h b/Demo/Objective_C_Demo/ViewController/TextViewController.h new file mode 100644 index 0000000..b755bbd --- /dev/null +++ b/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 + +@interface TextViewController : UIViewController + +@end diff --git a/Demo/Objective_C_Demo/ViewController/TextViewController.m b/Demo/Objective_C_Demo/ViewController/TextViewController.m new file mode 100644 index 0000000..4b2d3f8 --- /dev/null +++ b/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 diff --git a/Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.h b/Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.h index 264f0b4..a31d31c 100644 --- a/Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.h +++ b/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 diff --git a/Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.m b/Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.m index 724cb1e..5b50cd2 100644 --- a/Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.m +++ b/Demo/Objective_C_Demo/ViewController/TextViewSpecialCaseViewController.m @@ -5,28 +5,12 @@ #import "TextViewSpecialCaseViewController.h" #import "IQKeyboardManager.h" -@interface TextViewSpecialCaseViewController () - --(void)refreshUI; +@interface TextViewSpecialCaseViewController () @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; diff --git a/Demo/Objective_C_Demo/ViewController/ViewController.m b/Demo/Objective_C_Demo/ViewController/ViewController.m index a148746..e9c1fea 100644 --- a/Demo/Objective_C_Demo/ViewController/ViewController.m +++ b/Demo/Objective_C_Demo/ViewController/ViewController.m @@ -5,7 +5,7 @@ #import "ViewController.h" #import "IQKeyboardManager.h" -@interface ViewController () +@interface ViewController () @end @@ -37,18 +37,50 @@ [[IQKeyboardManager sharedManager] setToolbarManageBehaviour:IQAutoToolbarByPosition]; } +//- (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 +{ + 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)shouldAutorotate { return NO; } --(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; - cell.backgroundColor = [UIColor clearColor]; - return cell; -} - -(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { return (toInterfaceOrientation == UIInterfaceOrientationPortrait); diff --git a/Demo/Objective_C_Demo/ViewController/WebViewController.m b/Demo/Objective_C_Demo/ViewController/WebViewController.m index 5de7f76..2b7997b 100755 --- a/Demo/Objective_C_Demo/ViewController/WebViewController.m +++ b/Demo/Objective_C_Demo/ViewController/WebViewController.m @@ -4,6 +4,10 @@ #import "WebViewController.h" +@interface WebViewController () + +@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; diff --git a/Demo/Objective_C_Demo/ViewController/YYTextViewController.h b/Demo/Objective_C_Demo/ViewController/YYTextViewController.h new file mode 100644 index 0000000..4f773de --- /dev/null +++ b/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 + +@interface YYTextViewController : UIViewController + +@end diff --git a/Demo/Objective_C_Demo/ViewController/YYTextViewController.m b/Demo/Objective_C_Demo/ViewController/YYTextViewController.m new file mode 100644 index 0000000..ebdd9e7 --- /dev/null +++ b/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 () + +@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 diff --git a/Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.h b/Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.h new file mode 100755 index 0000000..8c794a9 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.h @@ -0,0 +1,1415 @@ +// +// NSAttributedString+YYText.h +// 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 +#import + +#if __has_include() +#import +#import +#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 *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 *)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 *yy_writingDirection; +- (nullable NSArray *)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 *yy_tabStops; +- (nullable NSArray *)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 *)attributes; +- (void)setYy_attributes:(nullable NSDictionary *)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 *yy_writingDirection; +- (void)yy_setWritingDirection:(nullable NSArray *)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 *yy_tabStops; +- (void)yy_setTabStops:(nullable NSArray *)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 *)yy_allDiscontinuousAttributeKeys; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.m b/Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.m new file mode 100755 index 0000000..4d492f1 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/NSAttributedString+YYText.m @@ -0,0 +1,1403 @@ +// +// NSAttributedString+YYText.m +// 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 + + +// 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 diff --git a/Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.h b/Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.h new file mode 100755 index 0000000..49b8acd --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.h @@ -0,0 +1,37 @@ +// +// NSParagraphStyle+YYText.h +// 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 + +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 diff --git a/Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.m b/Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.m new file mode 100755 index 0000000..0e59582 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/NSParagraphStyle+YYText.m @@ -0,0 +1,224 @@ +// +// NSParagraphStyle+YYText.m +// 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 + +// 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), ¶graphSpacing)) { + 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), ¶graphSpacingBefore)) { + 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 = ¶graphSpacing; + 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 = ¶LineBreak; + 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 = ¶WritingDirection; + 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 = ¶graphSpacingBefore; + 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 diff --git a/Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.h b/Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.h new file mode 100755 index 0000000..d516a3d --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.h @@ -0,0 +1,41 @@ +// +// UIPasteboard+YYText.h +// 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 + +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 diff --git a/Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.m b/Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.m new file mode 100755 index 0000000..29a86f2 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/UIPasteboard+YYText.m @@ -0,0 +1,146 @@ +// +// UIPasteboard+YYText.m +// 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 + + +#if __has_include("YYImage.h") +#import "YYImage.h" +#define YYTextAnimatedImageAvailable 1 +#elif __has_include() +#import +#define YYTextAnimatedImageAvailable 1 +#elif __has_include() +#import +#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 diff --git a/Demo/Objective_C_Demo/YYText/UIView+YYText.h b/Demo/Objective_C_Demo/YYText/UIView+YYText.h new file mode 100755 index 0000000..9472834 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/UIView+YYText.h @@ -0,0 +1,72 @@ +// +// UIView+YYText.h +// 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 + +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 diff --git a/Demo/Objective_C_Demo/YYText/UIView+YYText.m b/Demo/Objective_C_Demo/YYText/UIView+YYText.m new file mode 100755 index 0000000..f1c4c53 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/UIView+YYText.m @@ -0,0 +1,123 @@ +// +// UIView+YYText.m +// 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 \ No newline at end of file diff --git a/Demo/Objective_C_Demo/YYText/YYLabel.h b/Demo/Objective_C_Demo/YYText/YYLabel.h new file mode 100755 index 0000000..95563ff --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYLabel.h @@ -0,0 +1,380 @@ +// +// YYLabel.h +// 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 + +#if __has_include() +#import +#import +#import +#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 + +#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 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 *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 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 +@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 textParser; +@property (nullable, nonatomic, strong) YYTextLayout *textLayout; +@property (nullable, nonatomic, copy) UIBezierPath *textContainerPath; +@property (nullable, nonatomic, copy) NSArray *exclusionPaths; +@property (nonatomic) UIEdgeInsets textContainerInset; +@property (nullable, nonatomic, copy) id 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 diff --git a/Demo/Objective_C_Demo/YYText/YYLabel.m b/Demo/Objective_C_Demo/YYText/YYLabel.m new file mode 100755 index 0000000..4c906a9 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYLabel.m @@ -0,0 +1,1305 @@ +// +// YYLabel.m +// 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 + + +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() { + 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)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)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 diff --git a/Demo/Objective_C_Demo/YYText/YYText.h b/Demo/Objective_C_Demo/YYText/YYText.h new file mode 100755 index 0000000..0900d8b --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYText.h @@ -0,0 +1,50 @@ +// +// YYText.h +// 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 + +#if __has_include() +FOUNDATION_EXPORT double YYTextVersionNumber; +FOUNDATION_EXPORT const unsigned char YYTextVersionString[]; +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextArchiver.h b/Demo/Objective_C_Demo/YYText/YYTextArchiver.h new file mode 100755 index 0000000..cff8935 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextArchiver.h @@ -0,0 +1,33 @@ +// +// YYTextArchiver.h +// 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 + +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 +@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 +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Objective_C_Demo/YYText/YYTextArchiver.m b/Demo/Objective_C_Demo/YYText/YYTextArchiver.m new file mode 100755 index 0000000..e0f64dc --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextArchiver.m @@ -0,0 +1,252 @@ +// +// YYTextArchiver.m +// 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 +@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 +@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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.h b/Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.h new file mode 100755 index 0000000..9d00826 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.h @@ -0,0 +1,79 @@ +// +// YYTextAsyncLayer.h +// 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 +#import + +@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 +@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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.m b/Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.m new file mode 100755 index 0000000..4187f88 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextAsyncLayer.m @@ -0,0 +1,235 @@ +// +// YYTextAsyncLayer.m +// 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 + + +/// 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 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextAttribute.h b/Demo/Objective_C_Demo/YYText/YYTextAttribute.h new file mode 100755 index 0000000..c6bb774 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextAttribute.h @@ -0,0 +1,347 @@ +// +// YYTextAttribute.h +// 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 + +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 ++ (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 ++ (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 ++ (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 ++ (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 ++ (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 ++ (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 + +/** + 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 *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 *)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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextAttribute.m b/Demo/Objective_C_Demo/YYText/YYTextAttribute.m new file mode 100755 index 0000000..09fa05f --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextAttribute.m @@ -0,0 +1,524 @@ +// +// YYTextAttribute.m +// 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 +#import +#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 + diff --git a/Demo/Objective_C_Demo/YYText/YYTextContainerView.h b/Demo/Objective_C_Demo/YYText/YYTextContainerView.h new file mode 100755 index 0000000..66cd909 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextContainerView.h @@ -0,0 +1,55 @@ +// +// YYTextContainerView.h +// 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 + +#if __has_include() +#import +#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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextContainerView.m b/Demo/Objective_C_Demo/YYText/YYTextContainerView.m new file mode 100755 index 0000000..e3b7ee8 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextContainerView.m @@ -0,0 +1,144 @@ +// +// YYTextContainerView.m +// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextDebugOption.h b/Demo/Objective_C_Demo/YYText/YYTextDebugOption.h new file mode 100755 index 0000000..f706351 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextDebugOption.h @@ -0,0 +1,95 @@ +// +// YYTextDebugOption.h +// 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 + +@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 + +@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 +@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)target; + +/** + Remove a debug target which is added by `addDebugTarget:`. + + @param target A debug target. + */ ++ (void)removeDebugTarget:(id)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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextDebugOption.m b/Demo/Objective_C_Demo/YYText/YYTextDebugOption.m new file mode 100755 index 0000000..5eaaa73 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextDebugOption.m @@ -0,0 +1,140 @@ +// +// YYTextDebugOption.m +// 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 +#import + +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 target = (__bridge id)(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 target) { + _initSharedDebug(); + pthread_mutex_lock(&_sharedDebugLock); + CFSetAddValue(_sharedDebugTargets, (__bridge const void *)(target)); + pthread_mutex_unlock(&_sharedDebugLock); +} + +static void _removeDebugTarget(id 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)target { + if (target) _addDebugTarget(target); +} + ++ (void)removeDebugTarget:(id)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 + diff --git a/Demo/Objective_C_Demo/YYText/YYTextEffectWindow.h b/Demo/Objective_C_Demo/YYText/YYTextEffectWindow.h new file mode 100755 index 0000000..27cc4c1 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextEffectWindow.h @@ -0,0 +1,52 @@ +// +// YYTextEffectWindow.h +// 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 + +#if __has_include() +#import +#import +#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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextEffectWindow.m b/Demo/Objective_C_Demo/YYText/YYTextEffectWindow.m new file mode 100755 index 0000000..904a3d1 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextEffectWindow.m @@ -0,0 +1,420 @@ +// +// YYTextEffectWindow.m +// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextInput.h b/Demo/Objective_C_Demo/YYText/YYTextInput.h new file mode 100755 index 0000000..2e879c0 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextInput.h @@ -0,0 +1,87 @@ +// +// YYTextInput.h +// 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 + +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 + +@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 + +@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 + +@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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextInput.m b/Demo/Objective_C_Demo/YYText/YYTextInput.m new file mode 100755 index 0000000..0c4cb52 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextInput.m @@ -0,0 +1,152 @@ +// +// YYTextInput.m +// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.h b/Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.h new file mode 100755 index 0000000..5f28a55 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.h @@ -0,0 +1,98 @@ +// +// YYTextKeyboardManager.h +// 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 + +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 +@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)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)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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.m b/Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.m new file mode 100755 index 0000000..43467bb --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextKeyboardManager.m @@ -0,0 +1,521 @@ +// +// YYTextKeyboardManager.m +// 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 + + +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)observer { + if (!observer) return; + [_observers addObject:observer]; +} + +- (void)removeObserver:(id)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 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextLayout.h b/Demo/Objective_C_Demo/YYText/YYTextLayout.h new file mode 100755 index 0000000..24c0a62 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextLayout.h @@ -0,0 +1,571 @@ +// +// YYTextLayout.h +// 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 +#import + +#if __has_include() +#import +#import +#import +#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 + +/// 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 *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 linePositionModifier; +@end + + +/** + The YYTextLinePositionModifier protocol declares the required method to modify + the line position in text layout progress. See `YYTextLinePositionSimpleModifier` for example. + */ +@protocol YYTextLinePositionModifier +@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 *)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 +@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 + + +#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 *)layoutWithContainers:(NSArray *)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 *)layoutWithContainers:(NSArray *)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 *lines; +///< YYTextLine with truncated token, or nil +@property (nullable, nonatomic, strong, readonly) YYTextLine *truncatedLine; +///< Array of `YYTextAttachment` +@property (nullable, nonatomic, strong, readonly) NSArray *attachments; +///< Array of NSRange(wrapped by NSValue) in text +@property (nullable, nonatomic, strong, readonly) NSArray *attachmentRanges; +///< Array of CGRect(wrapped by NSValue) in container +@property (nullable, nonatomic, strong, readonly) NSArray *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 *)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 *)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 *)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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextLayout.m b/Demo/Objective_C_Demo/YYText/YYTextLayout.m new file mode 100755 index 0000000..fb01de9 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextLayout.m @@ -0,0 +1,3361 @@ +// +// YYTextLayout.m +// 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 _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)linePositionModifier { + Setter(_linePositionModifier = [(NSObject *)linePositionModifier copy]); +} + +- (id)linePositionModifier { + Getter(id 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextLine.h b/Demo/Objective_C_Demo/YYText/YYTextLine.h new file mode 100755 index 0000000..822a96c --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextLine.h @@ -0,0 +1,84 @@ +// +// YYTextLine.h +// 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 +#import + +#if __has_include() +#import +#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 *> *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 *attachments; ///< YYTextAttachment +@property (nonatomic, readonly) NSArray *attachmentRanges; ///< NSRange(NSValue) +@property (nonatomic, readonly) NSArray *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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextLine.m b/Demo/Objective_C_Demo/YYText/YYTextLine.m new file mode 100755 index 0000000..7c1d749 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextLine.m @@ -0,0 +1,167 @@ +// +// YYYTextLine.m +// 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:@" 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextMagnifier.h b/Demo/Objective_C_Demo/YYText/YYTextMagnifier.h new file mode 100755 index 0000000..adf131b --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextMagnifier.h @@ -0,0 +1,52 @@ +// +// YYTextMagnifier.h +// 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 + +#if __has_include() +#import +#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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextMagnifier.m b/Demo/Objective_C_Demo/YYText/YYTextMagnifier.m new file mode 100755 index 0000000..3932204 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextMagnifier.m @@ -0,0 +1,355 @@ +// +// YYTextMagnifier.m +// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextParser.h b/Demo/Objective_C_Demo/YYText/YYTextParser.h new file mode 100755 index 0000000..111064b --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextParser.h @@ -0,0 +1,91 @@ +// +// YYTextParser.h +// 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 + +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 +@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 +@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 + +/** + 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 *emoticonMapper; +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Objective_C_Demo/YYText/YYTextParser.m b/Demo/Objective_C_Demo/YYText/YYTextParser.m new file mode 100755 index 0000000..9455532 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextParser.m @@ -0,0 +1,417 @@ +// +// YYTextParser.m +// 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>]*", 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.h b/Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.h new file mode 100755 index 0000000..1a13843 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.h @@ -0,0 +1,78 @@ +// +// YYTextRubyAnnotation.h +// 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 +#import + +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 + +/// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.m b/Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.m new file mode 100755 index 0000000..f4d356c --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextRubyAnnotation.m @@ -0,0 +1,83 @@ +// +// YYTextRubyAnnotation.m +// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextRunDelegate.h b/Demo/Objective_C_Demo/YYText/YYTextRunDelegate.h new file mode 100755 index 0000000..ac73b06 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextRunDelegate.h @@ -0,0 +1,68 @@ +// +// YYTextRunDelegate.h +// 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 +#import + +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 + +/** + 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextRunDelegate.m b/Demo/Objective_C_Demo/YYText/YYTextRunDelegate.m new file mode 100755 index 0000000..3b858ff --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextRunDelegate.m @@ -0,0 +1,71 @@ +// +// YYTextRunDelegate.m +// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextSelectionView.h b/Demo/Objective_C_Demo/YYText/YYTextSelectionView.h new file mode 100755 index 0000000..be5f38e --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextSelectionView.h @@ -0,0 +1,78 @@ +// +// YYTextSelectionView.h +// 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 + +#if __has_include() +#import +#import +#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 *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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextSelectionView.m b/Demo/Objective_C_Demo/YYText/YYTextSelectionView.m new file mode 100755 index 0000000..6c3129f --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextSelectionView.m @@ -0,0 +1,329 @@ +// +// YYTextSelectionView.m +// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextTransaction.h b/Demo/Objective_C_Demo/YYText/YYTextTransaction.h new file mode 100755 index 0000000..22124f7 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextTransaction.h @@ -0,0 +1,42 @@ +// +// YYTextTransaction.h +// 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 + +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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextTransaction.m b/Demo/Objective_C_Demo/YYText/YYTextTransaction.m new file mode 100755 index 0000000..3ced249 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextTransaction.m @@ -0,0 +1,81 @@ +// +// YYTextTransaction.m +// 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextUtilities.h b/Demo/Objective_C_Demo/YYText/YYTextUtilities.h new file mode 100755 index 0000000..9410c9b --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextUtilities.h @@ -0,0 +1,563 @@ +// +// YYTextUtilities.h +// 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 +#import +#import + + +#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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextUtilities.m b/Demo/Objective_C_Demo/YYText/YYTextUtilities.m new file mode 100755 index 0000000..11eaefe --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextUtilities.m @@ -0,0 +1,309 @@ +// +// YYTextUtilities.m +// 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 +#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 +} diff --git a/Demo/Objective_C_Demo/YYText/YYTextView.h b/Demo/Objective_C_Demo/YYText/YYTextView.h new file mode 100755 index 0000000..f2c42a4 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextView.h @@ -0,0 +1,410 @@ +// +// YYTextView.h +// 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 + +#if __has_include() +#import +#import +#import +#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 +@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 + + +#pragma mark - Accessing the Delegate +///============================================================================= +/// @name Accessing the Delegate +///============================================================================= + +@property (nullable, nonatomic, weak) id 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 *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 *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 *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 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 *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 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 +//@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 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 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 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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextView.m b/Demo/Objective_C_Demo/YYText/YYTextView.m new file mode 100755 index 0000000..1a37e17 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextView.m @@ -0,0 +1,3832 @@ +// +// YYTextView.m +// 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 () { + + YYTextRange *_selectedTextRange; /// nonnull + YYTextRange *_markedTextRange; + + __weak id _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)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)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)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)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)delegate { + return _outerDelegate; +} + +- (void)setDelegate:(id)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)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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextWeakProxy.h b/Demo/Objective_C_Demo/YYText/YYTextWeakProxy.h new file mode 100755 index 0000000..0834302 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextWeakProxy.h @@ -0,0 +1,61 @@ +// +// YYTextWeakProxy.h +// 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 + +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 diff --git a/Demo/Objective_C_Demo/YYText/YYTextWeakProxy.m b/Demo/Objective_C_Demo/YYText/YYTextWeakProxy.m new file mode 100755 index 0000000..63ec141 --- /dev/null +++ b/Demo/Objective_C_Demo/YYText/YYTextWeakProxy.m @@ -0,0 +1,83 @@ +// +// YYTextWeakProxy.m +// 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 "YYTextWeakProxy.h" + + +@implementation YYTextWeakProxy + +- (instancetype)initWithTarget:(id)target { + _target = target; + return self; +} + ++ (instancetype)proxyWithTarget:(id)target { + return [[YYTextWeakProxy alloc] initWithTarget:target]; +} + +- (id)forwardingTargetForSelector:(SEL)selector { + return _target; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + void *null = NULL; + [invocation setReturnValue:&null]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + return [_target respondsToSelector:aSelector]; +} + +- (BOOL)isEqual:(id)object { + return [_target isEqual:object]; +} + +- (NSUInteger)hash { + return [_target hash]; +} + +- (Class)superclass { + return [_target superclass]; +} + +- (Class)class { + return [_target class]; +} + +- (BOOL)isKindOfClass:(Class)aClass { + return [_target isKindOfClass:aClass]; +} + +- (BOOL)isMemberOfClass:(Class)aClass { + return [_target isMemberOfClass:aClass]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { + return [_target conformsToProtocol:aProtocol]; +} + +- (BOOL)isProxy { + return YES; +} + +- (NSString *)description { + return [_target description]; +} + +- (NSString *)debugDescription { + return [_target debugDescription]; +} + +@end diff --git a/Demo/Resources/Images.xcassets/Contents.json b/Demo/Resources/Images.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Demo/Resources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/Contents.json b/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/Contents.json new file mode 100644 index 0000000..a732745 --- /dev/null +++ b/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "IQButtonBarArrowDown@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "IQButtonBarArrowDown@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/IQButtonBarArrowDown@2x.png b/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/IQButtonBarArrowDown@2x.png new file mode 100644 index 0000000..4218763 Binary files /dev/null and b/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/IQButtonBarArrowDown@2x.png differ diff --git a/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/IQButtonBarArrowDown@3x.png b/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/IQButtonBarArrowDown@3x.png new file mode 100644 index 0000000..a931cfe Binary files /dev/null and b/Demo/Resources/Images.xcassets/IQButtonBarArrowDown.imageset/IQButtonBarArrowDown@3x.png differ diff --git a/Demo/Swift_Demo/Cell/NavigationTableViewCell.swift b/Demo/Swift_Demo/Cell/NavigationTableViewCell.swift index fb448a8..24436cb 100644 --- a/Demo/Swift_Demo/Cell/NavigationTableViewCell.swift +++ b/Demo/Swift_Demo/Cell/NavigationTableViewCell.swift @@ -13,6 +13,5 @@ class NavigationTableViewCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() - self.backgroundColor = UIColor.clearColor() } } diff --git a/Demo/Swift_Demo/Cell/OptionTableViewCell.swift b/Demo/Swift_Demo/Cell/OptionTableViewCell.swift index fa60b22..4b6f8ef 100644 --- a/Demo/Swift_Demo/Cell/OptionTableViewCell.swift +++ b/Demo/Swift_Demo/Cell/OptionTableViewCell.swift @@ -13,6 +13,5 @@ class OptionTableViewCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() - self.backgroundColor = UIColor.clearColor() } } diff --git a/Demo/Swift_Demo/Cell/StepperTableViewCell.swift b/Demo/Swift_Demo/Cell/StepperTableViewCell.swift index 1c887bd..ee56b34 100644 --- a/Demo/Swift_Demo/Cell/StepperTableViewCell.swift +++ b/Demo/Swift_Demo/Cell/StepperTableViewCell.swift @@ -15,6 +15,5 @@ class StepperTableViewCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() - self.backgroundColor = UIColor.clearColor() } } diff --git a/Demo/Swift_Demo/Cell/SwitchTableViewCell.swift b/Demo/Swift_Demo/Cell/SwitchTableViewCell.swift index 55b6e6a..58a6e5c 100644 --- a/Demo/Swift_Demo/Cell/SwitchTableViewCell.swift +++ b/Demo/Swift_Demo/Cell/SwitchTableViewCell.swift @@ -14,6 +14,5 @@ class SwitchTableViewCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() - self.backgroundColor = UIColor.clearColor() } } diff --git a/Demo/Swift_Demo/Storyboard/MainSwift.storyboard b/Demo/Swift_Demo/Storyboard/MainSwift.storyboard index f27f83d..29f762d 100644 --- a/Demo/Swift_Demo/Storyboard/MainSwift.storyboard +++ b/Demo/Swift_Demo/Storyboard/MainSwift.storyboard @@ -118,15 +118,8 @@ - - - - - - - + - @@ -533,7 +526,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -562,28 +555,21 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - + + - @@ -593,7 +579,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -623,7 +609,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -653,7 +639,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -683,7 +669,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -713,7 +699,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -743,7 +729,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -773,7 +759,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -804,7 +790,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -835,7 +821,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -865,7 +851,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -895,7 +881,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -925,7 +911,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -1011,7 +997,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -1047,7 +1033,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -1096,7 +1082,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -1154,7 +1140,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; - + @@ -1874,15 +1860,8 @@ textField.inputAcessoryView = [[UIView alloc] init]; - - - - - - - + - @@ -2109,7 +2088,7 @@ textField.inputAcessoryView = [[UIView alloc] init]; -