Repository: ibireme/YYText Branch: master Commit: 7bd2aa414147 Files: 129 Total size: 1.2 MB Directory structure: gitextract_s7a5jp6t/ ├── .gitignore ├── .travis.yml ├── Demo/ │ ├── YYTextDemo/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── CALayer+YYAdd.h │ │ ├── CALayer+YYAdd.m │ │ ├── EmoticonQQ.bundle/ │ │ │ └── info.plist │ │ ├── Info.plist │ │ ├── NSBundle+YYAdd.h │ │ ├── NSBundle+YYAdd.m │ │ ├── NSData+YYAdd.h │ │ ├── NSData+YYAdd.m │ │ ├── NSString+YYAdd.h │ │ ├── NSString+YYAdd.m │ │ ├── UIControl+YYAdd.h │ │ ├── UIControl+YYAdd.m │ │ ├── UIGestureRecognizer+YYAdd.h │ │ ├── UIGestureRecognizer+YYAdd.m │ │ ├── UIImage+YYWebImage.h │ │ ├── UIImage+YYWebImage.m │ │ ├── UIView+YYAdd.h │ │ ├── UIView+YYAdd.m │ │ ├── ViewController.h │ │ ├── ViewController.m │ │ ├── YYFPSLabel.h │ │ ├── YYFPSLabel.m │ │ ├── YYGestureRecognizer.h │ │ ├── YYGestureRecognizer.m │ │ ├── YYImage/ │ │ │ ├── Animated image support.txt │ │ │ ├── YYAnimatedImageView.h │ │ │ ├── YYAnimatedImageView.m │ │ │ ├── YYFrameImage.h │ │ │ ├── YYFrameImage.m │ │ │ ├── YYImage.h │ │ │ ├── YYImage.m │ │ │ ├── YYImageCoder.h │ │ │ ├── YYImageCoder.m │ │ │ ├── YYSpriteSheetImage.h │ │ │ └── YYSpriteSheetImage.m │ │ ├── YYTextAsyncExample.h │ │ ├── YYTextAsyncExample.m │ │ ├── YYTextAttachmentExample.h │ │ ├── YYTextAttachmentExample.m │ │ ├── YYTextAttributeExample.h │ │ ├── YYTextAttributeExample.m │ │ ├── YYTextBindingExample.h │ │ ├── YYTextBindingExample.m │ │ ├── YYTextCopyPasteExample.h │ │ ├── YYTextCopyPasteExample.m │ │ ├── YYTextEditExample.h │ │ ├── YYTextEditExample.m │ │ ├── YYTextEmoticonExample.h │ │ ├── YYTextEmoticonExample.m │ │ ├── YYTextExample.h │ │ ├── YYTextExample.m │ │ ├── YYTextExampleHelper.h │ │ ├── YYTextExampleHelper.m │ │ ├── YYTextMarkdownExample.h │ │ ├── YYTextMarkdownExample.m │ │ ├── YYTextRubyExample.h │ │ ├── YYTextRubyExample.m │ │ ├── YYTextTagExample.h │ │ ├── YYTextTagExample.m │ │ ├── YYTextUndoRedoExample.h │ │ ├── YYTextUndoRedoExample.m │ │ ├── YYWeakProxy.h │ │ ├── YYWeakProxy.m │ │ └── main.m │ └── YYTextDemo.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ └── contents.xcworkspacedata ├── Framework/ │ ├── Info.plist │ └── YYText.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ └── YYText.xcscheme ├── LICENSE ├── README.md ├── YYText/ │ ├── Component/ │ │ ├── YYTextContainerView.h │ │ ├── YYTextContainerView.m │ │ ├── YYTextDebugOption.h │ │ ├── YYTextDebugOption.m │ │ ├── YYTextEffectWindow.h │ │ ├── YYTextEffectWindow.m │ │ ├── YYTextInput.h │ │ ├── YYTextInput.m │ │ ├── YYTextKeyboardManager.h │ │ ├── YYTextKeyboardManager.m │ │ ├── YYTextLayout.h │ │ ├── YYTextLayout.m │ │ ├── YYTextLine.h │ │ ├── YYTextLine.m │ │ ├── YYTextMagnifier.h │ │ ├── YYTextMagnifier.m │ │ ├── YYTextSelectionView.h │ │ └── YYTextSelectionView.m │ ├── String/ │ │ ├── YYTextArchiver.h │ │ ├── YYTextArchiver.m │ │ ├── YYTextAttribute.h │ │ ├── YYTextAttribute.m │ │ ├── YYTextParser.h │ │ ├── YYTextParser.m │ │ ├── YYTextRubyAnnotation.h │ │ ├── YYTextRubyAnnotation.m │ │ ├── YYTextRunDelegate.h │ │ └── YYTextRunDelegate.m │ ├── Utility/ │ │ ├── NSAttributedString+YYText.h │ │ ├── NSAttributedString+YYText.m │ │ ├── NSParagraphStyle+YYText.h │ │ ├── NSParagraphStyle+YYText.m │ │ ├── UIPasteboard+YYText.h │ │ ├── UIPasteboard+YYText.m │ │ ├── UIView+YYText.h │ │ ├── UIView+YYText.m │ │ ├── YYTextAsyncLayer.h │ │ ├── YYTextAsyncLayer.m │ │ ├── YYTextTransaction.h │ │ ├── YYTextTransaction.m │ │ ├── YYTextUtilities.h │ │ ├── YYTextUtilities.m │ │ ├── YYTextWeakProxy.h │ │ └── YYTextWeakProxy.m │ ├── YYLabel.h │ ├── YYLabel.m │ ├── YYText.h │ ├── YYTextView.h │ └── YYTextView.m └── YYText.podspec ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # OS X .DS_Store # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## Build generated build/ DerivedData/ ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xccheckout *.xcuserstate *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output # Code Injection # # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ ================================================ FILE: .travis.yml ================================================ language: objective-c osx_image: xcode8 xcode_project: Framework/YYText.xcodeproj xcode_scheme: YYText before_install: - env - xcodebuild -version - xcodebuild -showsdks - xcpretty --version script: - set -o pipefail - xcodebuild clean build -project "$TRAVIS_XCODE_PROJECT" -scheme "$TRAVIS_XCODE_SCHEME" | xcpretty ================================================ FILE: Demo/YYTextDemo/AppDelegate.h ================================================ // // AppDelegate.h // YYTextDemo // // Created by ibireme on 15/10/17. // Copyright © 2015年 ibireme. All rights reserved. // #import @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end ================================================ FILE: Demo/YYTextDemo/AppDelegate.m ================================================ // // AppDelegate.m // YYTextDemo // // Created by ibireme on 15/10/17. // Copyright © 2015年 ibireme. All rights reserved. // #import "AppDelegate.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. return YES; } - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. } - (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } - (void)applicationWillEnterForeground:(UIApplication *)application { // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. } - (void)applicationDidBecomeActive:(UIApplication *)application { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } - (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } @end ================================================ FILE: Demo/YYTextDemo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "1x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "2x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "1x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "2x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "1x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "2x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "1x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "2x" }, { "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demo/YYTextDemo/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Demo/YYTextDemo/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Demo/YYTextDemo/CALayer+YYAdd.h ================================================ // // CALayer+YYAdd.h // YYKit // // Created by ibireme on 14/5/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 /** Provides extensions for `CALayer`. */ @interface CALayer (YYAdd) /** Take snapshot without transform, image's size equals to bounds. */ - (UIImage *)snapshotImage; /** Take snapshot without transform, PDF's page size equals to bounds. */ - (NSData *)snapshotPDF; /** Shortcut to set the layer's shadow @param color Shadow Color @param offset Shadow offset @param radius Shadow radius */ - (void)setLayerShadow:(UIColor*)color offset:(CGSize)offset radius:(CGFloat)radius; /** Remove all sublayers. */ - (void)removeAllSublayers; @property (nonatomic) CGFloat left; ///< Shortcut for frame.origin.x. @property (nonatomic) CGFloat top; ///< Shortcut for frame.origin.y @property (nonatomic) CGFloat right; ///< Shortcut for frame.origin.x + frame.size.width @property (nonatomic) CGFloat bottom; ///< Shortcut for frame.origin.y + frame.size.height @property (nonatomic) CGFloat width; ///< Shortcut for frame.size.width. @property (nonatomic) CGFloat height; ///< Shortcut for frame.size.height. @property (nonatomic) CGPoint center; ///< Shortcut for center. @property (nonatomic) CGFloat centerX; ///< Shortcut for center.x @property (nonatomic) CGFloat centerY; ///< Shortcut for center.y @property (nonatomic) CGPoint origin; ///< Shortcut for frame.origin. @property (nonatomic, getter=frameSize, setter=setFrameSize:) CGSize size; ///< Shortcut for frame.size. @property (nonatomic) CGFloat transformRotation; ///< key path "tranform.rotation" @property (nonatomic) CGFloat transformRotationX; ///< key path "tranform.rotation.x" @property (nonatomic) CGFloat transformRotationY; ///< key path "tranform.rotation.y" @property (nonatomic) CGFloat transformRotationZ; ///< key path "tranform.rotation.z" @property (nonatomic) CGFloat transformScale; ///< key path "tranform.scale" @property (nonatomic) CGFloat transformScaleX; ///< key path "tranform.scale.x" @property (nonatomic) CGFloat transformScaleY; ///< key path "tranform.scale.y" @property (nonatomic) CGFloat transformScaleZ; ///< key path "tranform.scale.z" @property (nonatomic) CGFloat transformTranslationX; ///< key path "tranform.translation.x" @property (nonatomic) CGFloat transformTranslationY; ///< key path "tranform.translation.y" @property (nonatomic) CGFloat transformTranslationZ; ///< key path "tranform.translation.z" /** Shortcut for transform.m34, -1/1000 is a good value. It should be set before other transform shortcut. */ @property (nonatomic, assign) CGFloat transformDepth; /** Add a fade animation to layer's contents when the contents is changed. @param duration Animation duration @param curve Animation curve. */ - (void)addFadeAnimationWithDuration:(NSTimeInterval)duration curve:(UIViewAnimationCurve)curve; /** Cancel fade animation which is added with "-addFadeAnimationWithDuration:curve:". */ - (void)removePreviousFadeAnimation; @end ================================================ FILE: Demo/YYTextDemo/CALayer+YYAdd.m ================================================ // // CALayer+YYAdd.m // YYKit // // Created by ibireme on 14/5/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 "CALayer+YYAdd.h" @implementation CALayer (YYAdd) - (UIImage *)snapshotImage { UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, 0); CGContextRef context = UIGraphicsGetCurrentContext(); [self renderInContext:context]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } - (NSData *)snapshotPDF { CGRect bounds = self.bounds; NSMutableData* data = [NSMutableData data]; CGDataConsumerRef consumer = CGDataConsumerCreateWithCFData((__bridge CFMutableDataRef)data); CGContextRef context = CGPDFContextCreate(consumer, &bounds, NULL); CGDataConsumerRelease(consumer); if (!context) return nil; CGPDFContextBeginPage(context, NULL); CGContextTranslateCTM(context, 0, bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); [self renderInContext:context]; CGPDFContextEndPage(context); CGPDFContextClose(context); CGContextRelease(context); return data; } - (void)setLayerShadow:(UIColor*)color offset:(CGSize)offset radius:(CGFloat)radius { self.shadowColor = color.CGColor; self.shadowOffset = offset; self.shadowRadius = radius; self.shadowOpacity = 1; self.shouldRasterize = YES; self.rasterizationScale = [UIScreen mainScreen].scale; } - (void)removeAllSublayers { while (self.sublayers.count) { [self.sublayers.lastObject removeFromSuperlayer]; } } - (CGFloat)left { return self.frame.origin.x; } - (void)setLeft:(CGFloat)x { CGRect frame = self.frame; frame.origin.x = x; self.frame = frame; } - (CGFloat)top { return self.frame.origin.y; } - (void)setTop:(CGFloat)y { CGRect frame = self.frame; frame.origin.y = y; self.frame = frame; } - (CGFloat)right { return self.frame.origin.x + self.frame.size.width; } - (void)setRight:(CGFloat)right { CGRect frame = self.frame; frame.origin.x = right - frame.size.width; self.frame = frame; } - (CGFloat)bottom { return self.frame.origin.y + self.frame.size.height; } - (void)setBottom:(CGFloat)bottom { CGRect frame = self.frame; frame.origin.y = bottom - frame.size.height; self.frame = frame; } - (CGFloat)width { return self.frame.size.width; } - (void)setWidth:(CGFloat)width { CGRect frame = self.frame; frame.size.width = width; self.frame = frame; } - (CGFloat)height { return self.frame.size.height; } - (void)setHeight:(CGFloat)height { CGRect frame = self.frame; frame.size.height = height; self.frame = frame; } - (CGPoint)center { return CGPointMake(self.frame.origin.x + self.frame.size.width * 0.5, self.frame.origin.y + self.frame.size.height * 0.5); } - (void)setCenter:(CGPoint)center { CGRect frame = self.frame; frame.origin.x = center.x - frame.size.width * 0.5; frame.origin.y = center.y - frame.size.height * 0.5; self.frame = frame; } - (CGFloat)centerX { return self.frame.origin.x + self.frame.size.width * 0.5; } - (void)setCenterX:(CGFloat)centerX { CGRect frame = self.frame; frame.origin.x = centerX - frame.size.width * 0.5; self.frame = frame; } - (CGFloat)centerY { return self.frame.origin.y + self.frame.size.height * 0.5; } - (void)setCenterY:(CGFloat)centerY { CGRect frame = self.frame; frame.origin.y = centerY - frame.size.height * 0.5; self.frame = frame; } - (CGPoint)origin { return self.frame.origin; } - (void)setOrigin:(CGPoint)origin { CGRect frame = self.frame; frame.origin = origin; self.frame = frame; } - (CGSize)frameSize { return self.frame.size; } - (void)setFrameSize:(CGSize)size { CGRect frame = self.frame; frame.size = size; self.frame = frame; } - (CGFloat)transformRotation { NSNumber *v = [self valueForKeyPath:@"transform.rotation"]; return v.doubleValue; } - (void)setTransformRotation:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.rotation"]; } - (CGFloat)transformRotationX { NSNumber *v = [self valueForKeyPath:@"transform.rotation.x"]; return v.doubleValue; } - (void)setTransformRotationX:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.rotation.x"]; } - (CGFloat)transformRotationY { NSNumber *v = [self valueForKeyPath:@"transform.rotation.y"]; return v.doubleValue; } - (void)setTransformRotationY:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.rotation.y"]; } - (CGFloat)transformRotationZ { NSNumber *v = [self valueForKeyPath:@"transform.rotation.z"]; return v.doubleValue; } - (void)setTransformRotationZ:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.rotation.z"]; } - (CGFloat)transformScaleX { NSNumber *v = [self valueForKeyPath:@"transform.scale.x"]; return v.doubleValue; } - (void)setTransformScaleX:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.scale.x"]; } - (CGFloat)transformScaleY { NSNumber *v = [self valueForKeyPath:@"transform.scale.y"]; return v.doubleValue; } - (void)setTransformScaleY:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.scale.y"]; } - (CGFloat)transformScaleZ { NSNumber *v = [self valueForKeyPath:@"transform.scale.z"]; return v.doubleValue; } - (void)setTransformScaleZ:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.scale.z"]; } - (CGFloat)transformScale { NSNumber *v = [self valueForKeyPath:@"transform.scale"]; return v.doubleValue; } - (void)setTransformScale:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.scale"]; } - (CGFloat)transformTranslationX { NSNumber *v = [self valueForKeyPath:@"transform.translation.x"]; return v.doubleValue; } - (void)setTransformTranslationX:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.translation.x"]; } - (CGFloat)transformTranslationY { NSNumber *v = [self valueForKeyPath:@"transform.translation.y"]; return v.doubleValue; } - (void)setTransformTranslationY:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.translation.y"]; } - (CGFloat)transformTranslationZ { NSNumber *v = [self valueForKeyPath:@"transform.translation.z"]; return v.doubleValue; } - (void)setTransformTranslationZ:(CGFloat)v { [self setValue:@(v) forKeyPath:@"transform.translation.z"]; } - (CGFloat)transformDepth { return self.transform.m34; } - (void)setTransformDepth:(CGFloat)v { CATransform3D d = self.transform; d.m34 = v; self.transform = d; } - (void)addFadeAnimationWithDuration:(NSTimeInterval)duration curve:(UIViewAnimationCurve)curve { if (duration <= 0) return; NSString *mediaFunction; switch (curve) { case UIViewAnimationCurveEaseInOut: { mediaFunction = kCAMediaTimingFunctionEaseOut; } break; case UIViewAnimationCurveEaseIn: { mediaFunction = kCAMediaTimingFunctionEaseIn; } break; case UIViewAnimationCurveEaseOut: { mediaFunction = kCAMediaTimingFunctionEaseInEaseOut; } break; case UIViewAnimationCurveLinear: { mediaFunction = kCAMediaTimingFunctionLinear; } break; default: { mediaFunction = kCAMediaTimingFunctionLinear; } break; } CATransition *transition = [CATransition animation]; transition.duration = duration; transition.timingFunction = [CAMediaTimingFunction functionWithName:mediaFunction]; transition.type = kCATransitionFade; [self addAnimation:transition forKey:@"yykit.fade"]; } - (void)removePreviousFadeAnimation { [self removeAnimationForKey:@"yykit.fade"]; } @end ================================================ FILE: Demo/YYTextDemo/EmoticonQQ.bundle/info.plist ================================================ /微笑 024 /撇嘴 041 /色 020 /发呆 044 /得意 022 /流泪 010 /害羞 021 /闭嘴 105 /睡 036 /大哭 011 /尴尬 026 /发怒 025 /调皮 002 /呲牙 001 /惊讶 034 /难过 033 /酷 013 /冷汗 028 /抓狂 014 /吐 023 /偷笑 004 /可爱 019 /白眼 031 /傲慢 032 /饥饿 080 /困 081 /惊恐 027 /流汗 003 /憨笑 038 /大兵 051 /奋斗 043 /咒骂 082 /疑问 035 /嘘 012 /晕 050 /折磨 014 /衰 040 /骷髅 077 /敲打 006 /再见 005 /擦汗 007 /抠鼻 084 /鼓掌 085 /糗大了 086 /坏笑 047 /左哼哼 087 /右哼哼 045 /哈欠 088 /鄙视 049 /委屈 015 /快哭了 089 /阴险 042 /亲亲 037 /吓 090 /可怜 052 /菜刀 018 /西瓜 061 /啤酒 062 /篮球 091 /乒乓 092 /咖啡 067 /饭 059 /猪头 008 /玫瑰 009 /凋谢 061 /示爱 030 /爱心 029 /心碎 073 /蛋糕 060 /闪电 079 /炸弹 017 /刀 069 /足球 076 /瓢虫 063 /便便 016 /月亮 068 /太阳 074 /礼物 075 /拥抱 046 /强 053 /弱 054 /握手 055 /胜利 056 /抱拳 057 /勾引 064 /拳头 072 /差劲 071 /爱你 066 /NO 093 /OK 065 /爱情 039 /飞吻 048 /跳跳 094 /发抖 070 /怄火 095 /转圈 096 /磕头 097 /回头 098 /跳绳 099 /挥手 078 /激动 100 /街舞 101 /献吻 102 /左太极 103 /右太极 104 /双喜 107 /鞭炮 108 /灯笼 109 /发财 110 /K歌 111 /购物 112 /邮件 113 /帅 114 /喝彩 115 /祈祷 116 /爆筋 117 /棒棒糖 118 /喝奶 119 下面 120 /香蕉 121 /飞机 112 /开车 123 /左车头 124 /车厢 125 /右车头 126 /多云 127 /下雨 128 /钞票 129 /熊猫 130 /灯泡 131 /风车 132 /闹钟 133 /打伞 134 /彩球 135 /钻戒 136 /沙发 137 /纸巾 138 /药 139 /手枪 140 /青蛙 141 ================================================ FILE: Demo/YYTextDemo/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleLocalizations en zh zh_TW ja fr de it ko CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Demo/YYTextDemo/NSBundle+YYAdd.h ================================================ // // NSBundle+YYAdd.h // YYCategories // // Created by ibireme on 14/10/20. // 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 /** Provides extensions for `NSBundle` to get resource by @2x or @3x... Example: ico.png, ico@2x.png, ico@3x.png. Call scaledResource:@"ico" ofType:@"png" on iPhone6 will return "ico@2x.png"'s path. */ @interface NSBundle (YYAdd) /** An array of NSNumber objects, shows the best order for path scale search. e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1] iPhone6 Plus:@[@3,@2,@1] */ + (NSArray *)preferredScales; /** Returns the full pathname for the resource file identified by the specified name and extension and residing in a given bundle directory. It first search the file with current screen's scale (such as @2x), then search from higher scale to lower scale. @param name The name of a resource file contained in the directory specified by bundlePath. @param ext If extension is an empty string or nil, the extension is assumed not to exist and the file is the first file encountered that exactly matches name. @param bundlePath The path of a top-level bundle directory. This must be a valid path. For example, to specify the bundle directory for a Mac app, you might specify the path /Applications/MyApp.app. @return The full pathname for the resource file or nil if the file could not be located. This method also returns nil if the bundle specified by the bundlePath parameter does not exist or is not a readable directory. */ + (NSString *)pathForScaledResource:(NSString *)name ofType:(NSString *)ext inDirectory:(NSString *)bundlePath; /** Returns the full pathname for the resource identified by the specified name and file extension. It first search the file with current screen's scale (such as @2x), then search from higher scale to lower scale. @param name The name of the resource file. If name is an empty string or nil, returns the first file encountered of the supplied type. @param ext If extension is an empty string or nil, the extension is assumed not to exist and the file is the first file encountered that exactly matches name. @return The full pathname for the resource file or nil if the file could not be located. */ - (NSString *)pathForScaledResource:(NSString *)name ofType:(NSString *)ext; /** Returns the full pathname for the resource identified by the specified name and file extension and located in the specified bundle subdirectory. It first search the file with current screen's scale (such as @2x), then search from higher scale to lower scale. @param name The name of the resource file. @param ext If extension is an empty string or nil, all the files in subpath and its subdirectories are returned. If an extension is provided the subdirectories are not searched. @param subpath The name of the bundle subdirectory. Can be nil. @return The full pathname for the resource file or nil if the file could not be located. */ - (NSString *)pathForScaledResource:(NSString *)name ofType:(NSString *)ext inDirectory:(NSString *)subpath; @end ================================================ FILE: Demo/YYTextDemo/NSBundle+YYAdd.m ================================================ // // NSBundle+YYAdd.m // YYCategories // // Created by ibireme on 14/10/20. // 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 "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" @implementation NSBundle (YYAdd) + (NSArray *)preferredScales { static NSArray *scales; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ CGFloat screenScale = [UIScreen mainScreen].scale; if (screenScale <= 1) { scales = @[@1,@2,@3]; } else if (screenScale <= 2) { scales = @[@2,@3,@1]; } else { scales = @[@3,@2,@1]; } }); return scales; } + (NSString *)pathForScaledResource:(NSString *)name ofType:(NSString *)ext inDirectory:(NSString *)bundlePath { if (name.length == 0) return nil; if ([name hasSuffix:@"/"]) return [self pathForResource:name ofType:ext inDirectory:bundlePath]; NSString *path = nil; NSArray *scales = [self preferredScales]; for (int s = 0; s < scales.count; s++) { CGFloat scale = ((NSNumber *)scales[s]).floatValue; NSString *scaledName = ext.length ? [name stringByAppendingNameScale:scale] : [name stringByAppendingPathScale:scale]; path = [self pathForResource:scaledName ofType:ext inDirectory:bundlePath]; if (path) break; } return path; } - (NSString *)pathForScaledResource:(NSString *)name ofType:(NSString *)ext { if (name.length == 0) return nil; if ([name hasSuffix:@"/"]) return [self pathForResource:name ofType:ext]; NSString *path = nil; NSArray *scales = [NSBundle preferredScales]; for (int s = 0; s < scales.count; s++) { CGFloat scale = ((NSNumber *)scales[s]).floatValue; NSString *scaledName = ext.length ? [name stringByAppendingNameScale:scale] : [name stringByAppendingPathScale:scale]; path = [self pathForResource:scaledName ofType:ext]; if (path) break; } return path; } - (NSString *)pathForScaledResource:(NSString *)name ofType:(NSString *)ext inDirectory:(NSString *)subpath { if (name.length == 0) return nil; if ([name hasSuffix:@"/"]) return [self pathForResource:name ofType:ext]; NSString *path = nil; NSArray *scales = [NSBundle preferredScales]; for (int s = 0; s < scales.count; s++) { CGFloat scale = ((NSNumber *)scales[s]).floatValue; NSString *scaledName = ext.length ? [name stringByAppendingNameScale:scale] : [name stringByAppendingPathScale:scale]; path = [self pathForResource:scaledName ofType:ext inDirectory:subpath]; if (path) break; } return path; } @end ================================================ FILE: Demo/YYTextDemo/NSData+YYAdd.h ================================================ // // NSData+YYAdd.h // YYKit // // Created by ibireme on 13/4/4. // 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 /** Provide hash, encrypt, encode and some common method for `NSData`. */ @interface NSData (YYAdd) #pragma mark - Others ///============================================================================= /// @name Others ///============================================================================= /** Create data from the file in main bundle (similar to [UIImage imageNamed:]). @param name The file name (in main bundle). @return A new data create from the file. */ + (NSData *)dataNamed:(NSString *)name; @end ================================================ FILE: Demo/YYTextDemo/NSData+YYAdd.m ================================================ // // NSData+YYAdd.m // YYKit // // Created by ibireme on 13/4/4. // 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 "NSData+YYAdd.h" @implementation NSData (YYAdd) + (NSData *)dataNamed:(NSString *)name { NSString *path = [[NSBundle mainBundle] pathForResource:name ofType:@""]; if (!path) return nil; NSData *data = [NSData dataWithContentsOfFile:path]; return data; } @end ================================================ FILE: Demo/YYTextDemo/NSString+YYAdd.h ================================================ // // NSString+YYAdd.h // YYCategories // // 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 /** Provide hash, encrypt, encode and some common method for 'NSString'. */ @interface NSString (YYAdd) #pragma mark - Drawing ///============================================================================= /// @name Drawing ///============================================================================= /** Returns the size of the string if it were rendered with the specified constraints. @param font The font to use for computing the string size. @param size The maximum acceptable size for the string. This value is used to calculate where line breaks and wrapping would occur. @param lineBreakMode The line break options for computing the size of the string. For a list of possible values, see NSLineBreakMode. @return The width and height of the resulting string's bounding box. These values may be rounded up to the nearest whole number. */ - (CGSize)sizeForFont:(UIFont *)font size:(CGSize)size mode:(NSLineBreakMode)lineBreakMode; /** Returns the width of the string if it were to be rendered with the specified font on a single line. @param font The font to use for computing the string width. @return The width of the resulting string's bounding box. These values may be rounded up to the nearest whole number. */ - (CGFloat)widthForFont:(UIFont *)font; /** Returns the height of the string if it were rendered with the specified constraints. @param font The font to use for computing the string size. @param width The maximum acceptable width for the string. This value is used to calculate where line breaks and wrapping would occur. @return The height of the resulting string's bounding box. These values may be rounded up to the nearest whole number. */ - (CGFloat)heightForFont:(UIFont *)font width:(CGFloat)width; #pragma mark - Regular Expression ///============================================================================= /// @name Regular Expression ///============================================================================= /** Whether it can match the regular expression @param regex The regular expression @param options The matching options to report. @return YES if can match the regex; otherwise, NO. */ - (BOOL)matchesRegex:(NSString *)regex options:(NSRegularExpressionOptions)options; /** Match the regular expression, and executes a given block using each object in the matches. @param regex The regular expression @param options The matching options to report. @param block The block to apply to elements in the array of matches. The block takes four arguments: match: The match substring. matchRange: The matching options. stop: A reference to a Boolean value. The block can set the value to YES to stop further processing of the array. The stop argument is an out-only argument. You should only ever set this Boolean to YES within the Block. */ - (void)enumerateRegexMatches:(NSString *)regex options:(NSRegularExpressionOptions)options usingBlock:(void (^)(NSString *match, NSRange matchRange, BOOL *stop))block; /** Returns a new string containing matching regular expressions replaced with the template string. @param regex The regular expression @param options The matching options to report. @param replacement The substitution template used when replacing matching instances. @return A string with matching regular expressions replaced by the template string. */ - (NSString *)stringByReplacingRegex:(NSString *)regex options:(NSRegularExpressionOptions)options withString:(NSString *)replacement; #pragma mark - Emoji ///============================================================================= /// @name Emoji ///============================================================================= /** Whether the receiver contains Apple Emoji (displayed in current version of iOS). */ - (BOOL)containsEmoji; - (BOOL)containsEmojiForSystemVersion:(double)systemVersion; #pragma mark - Utilities ///============================================================================= /// @name Utilities ///============================================================================= /** Returns a new UUID NSString e.g. "D1178E50-2A4D-4F1F-9BD3-F6AAB00E06B1" */ + (NSString *)stringWithUUID; /** Returns a string containing the characters in a given UTF32Char. @param char32 A UTF-32 character. @return A new string, or nil if the character is invalid. */ + (NSString *)stringWithUTF32Char:(UTF32Char)char32; /** Returns a string containing the characters in a given UTF32Char array. @param char32 An array of UTF-32 character. @param length The character count in array. @return A new string, or nil if an error occurs. */ + (NSString *)stringWithUTF32Chars:(const UTF32Char *)char32 length:(NSUInteger)length; /** Enumerates the unicode characters (UTF-32) in the specified range of the string. @param range The range within the string to enumerate substrings. @param block The block executed for the enumeration. The block takes four arguments: char32: The unicode character. range: The range in receiver. If the range.length is 1, the character is in BMP; otherwise (range.length is 2) the character is in none-BMP Plane and stored by a surrogate pair in the receiver. stop: A reference to a Boolean value that the block can use to stop the enumeration by setting *stop = YES; it should not touch *stop otherwise. */ - (void)enumerateUTF32CharInRange:(NSRange)range usingBlock:(void (^)(UTF32Char char32, NSRange range, BOOL *stop))block; /** Trim blank characters (space and newline) in head and tail. @return the trimmed string. */ - (NSString *)stringByTrim; /** Add scale modifier to the file name (without path extension), From @"name" to @"name@2x". e.g.
Before After(scale:2)
"icon" "icon@2x"
"icon " "icon @2x"
"icon.top" "icon.top@2x"
"/p/name" "/p/name@2x"
"/path/" "/path/"
@param scale Resource scale. @return String by add scale modifier, or just return if it's not end with file name. */ - (NSString *)stringByAppendingNameScale:(CGFloat)scale; /** Add scale modifier to the file path (with path extension), From @"name.png" to @"name@2x.png". e.g.
Before After(scale:2)
"icon.png" "icon@2x.png"
"icon..png""icon.@2x.png"
"icon" "icon@2x"
"icon " "icon @2x"
"icon." "icon.@2x"
"/p/name" "/p/name@2x"
"/path/" "/path/"
@param scale Resource scale. @return String by add scale modifier, or just return if it's not end with file name. */ - (NSString *)stringByAppendingPathScale:(CGFloat)scale; /** Return the path scale. e.g.
Path Scale
"icon.png" 1
"icon@2x.png" 2
"icon@2.5x.png" 2.5
"icon@2x" 1
"icon@2x..png" 1
"icon@2x.png/" 1
*/ - (CGFloat)pathScale; /** nil, @"", @" ", @"\n" will Returns NO; otherwise Returns YES. */ - (BOOL)isNotBlank; /** Returns YES if the target string is contained within the receiver. @param string A string to test the the receiver. @discussion Apple has implemented this method in iOS8. */ - (BOOL)containsString:(NSString *)string; /** Returns YES if the target CharacterSet is contained within the receiver. @param set A character set to test the the receiver. */ - (BOOL)containsCharacterSet:(NSCharacterSet *)set; /** Returns NSMakeRange(0, self.length). */ - (NSRange)rangeOfAll; /** Create a string from the file in main bundle (similar to [UIImage imageNamed:]). @param name The file name (in main bundle). @return A new string create from the file in UTF-8 character encoding. */ + (NSString *)stringNamed:(NSString *)name; @end ================================================ FILE: Demo/YYTextDemo/NSString+YYAdd.m ================================================ // // NSString+YYAdd.m // YYCategories // // 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 "NSString+YYAdd.h" @implementation NSString (YYAdd) - (CGSize)sizeForFont:(UIFont *)font size:(CGSize)size mode:(NSLineBreakMode)lineBreakMode { CGSize result; if (!font) font = [UIFont systemFontOfSize:12]; if ([self respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) { NSMutableDictionary *attr = [NSMutableDictionary new]; attr[NSFontAttributeName] = font; if (lineBreakMode != NSLineBreakByWordWrapping) { NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.lineBreakMode = lineBreakMode; attr[NSParagraphStyleAttributeName] = paragraphStyle; } CGRect rect = [self boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:attr context:nil]; result = rect.size; } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" result = [self sizeWithFont:font constrainedToSize:size lineBreakMode:lineBreakMode]; #pragma clang diagnostic pop } return result; } - (CGFloat)widthForFont:(UIFont *)font { CGSize size = [self sizeForFont:font size:CGSizeMake(HUGE, HUGE) mode:NSLineBreakByWordWrapping]; return size.width; } - (CGFloat)heightForFont:(UIFont *)font width:(CGFloat)width { CGSize size = [self sizeForFont:font size:CGSizeMake(width, HUGE) mode:NSLineBreakByWordWrapping]; return size.height; } - (BOOL)matchesRegex:(NSString *)regex options:(NSRegularExpressionOptions)options { NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:regex options:options error:NULL]; if (!pattern) return NO; return ([pattern numberOfMatchesInString:self options:0 range:NSMakeRange(0, self.length)] > 0); } - (void)enumerateRegexMatches:(NSString *)regex options:(NSRegularExpressionOptions)options usingBlock:(void (^)(NSString *match, NSRange matchRange, BOOL *stop))block { if (regex.length == 0 || !block) return; NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:regex options:options error:nil]; if (!regex) return; [pattern enumerateMatchesInString:self options:kNilOptions range:NSMakeRange(0, self.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { block([self substringWithRange:result.range], result.range, stop); }]; } - (NSString *)stringByReplacingRegex:(NSString *)regex options:(NSRegularExpressionOptions)options withString:(NSString *)replacement; { NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:regex options:options error:nil]; if (!pattern) return self; return [pattern stringByReplacingMatchesInString:self options:0 range:NSMakeRange(0, [self length]) withTemplate:replacement]; } - (BOOL)containsEmoji { return [self containsEmojiForSystemVersion:[UIDevice currentDevice].systemVersion.doubleValue]; } - (BOOL)containsEmojiForSystemVersion:(double)systemVersion { // If detected, it MUST contains emoji; otherwise it MAY not contains emoji. static NSMutableCharacterSet *minSet8_3, *minSetOld; // If detected, it may contains emoji; otherwise it MUST NOT contains emoji. static NSMutableCharacterSet *maxSet; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ minSetOld = [NSMutableCharacterSet new]; [minSetOld addCharactersInString:@"u2139\u2194\u2195\u2196\u2197\u2198\u2199\u21a9\u21aa\u231a\u231b\u23e9\u23ea\u23eb\u23ec\u23f0\u23f3\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb\u25fc\u25fd\u25fe\u2600\u2601\u260e\u2611\u2614\u2615\u261d\u261d\u263a\u2648\u2649\u264a\u264b\u264c\u264d\u264e\u264f\u2650\u2651\u2652\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2693\u26a0\u26a1\u26aa\u26ab\u26bd\u26be\u26c4\u26c5\u26ce\u26d4\u26ea\u26f2\u26f3\u26f5\u26fa\u26fd\u2702\u2705\u2708\u2709\u270a\u270b\u270c\u270c\u270f\u2712\u2714\u2716\u2728\u2733\u2734\u2744\u2747\u274c\u274e\u2753\u2754\u2755\u2757\u2764\u2795\u2796\u2797\u27a1\u27b0\u27bf\u2934\u2935\u2b05\u2b06\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\U0001f004\U0001f0cf\U0001f170\U0001f171\U0001f17e\U0001f17f\U0001f18e\U0001f191\U0001f192\U0001f193\U0001f194\U0001f195\U0001f196\U0001f197\U0001f198\U0001f199\U0001f19a\U0001f201\U0001f202\U0001f21a\U0001f22f\U0001f232\U0001f233\U0001f234\U0001f235\U0001f236\U0001f237\U0001f238\U0001f239\U0001f23a\U0001f250\U0001f251\U0001f300\U0001f301\U0001f302\U0001f303\U0001f304\U0001f305\U0001f306\U0001f307\U0001f308\U0001f309\U0001f30a\U0001f30b\U0001f30c\U0001f30d\U0001f30e\U0001f30f\U0001f310\U0001f311\U0001f312\U0001f313\U0001f314\U0001f315\U0001f316\U0001f317\U0001f318\U0001f319\U0001f31a\U0001f31b\U0001f31c\U0001f31d\U0001f31e\U0001f31f\U0001f320\U0001f330\U0001f331\U0001f332\U0001f333\U0001f334\U0001f335\U0001f337\U0001f338\U0001f339\U0001f33a\U0001f33b\U0001f33c\U0001f33d\U0001f33e\U0001f33f\U0001f340\U0001f341\U0001f342\U0001f343\U0001f344\U0001f345\U0001f346\U0001f347\U0001f348\U0001f349\U0001f34a\U0001f34b\U0001f34c\U0001f34d\U0001f34e\U0001f34f\U0001f350\U0001f351\U0001f352\U0001f353\U0001f354\U0001f355\U0001f356\U0001f357\U0001f358\U0001f359\U0001f35a\U0001f35b\U0001f35c\U0001f35d\U0001f35e\U0001f35f\U0001f360\U0001f361\U0001f362\U0001f363\U0001f364\U0001f365\U0001f366\U0001f367\U0001f368\U0001f369\U0001f36a\U0001f36b\U0001f36c\U0001f36d\U0001f36e\U0001f36f\U0001f370\U0001f371\U0001f372\U0001f373\U0001f374\U0001f375\U0001f376\U0001f377\U0001f378\U0001f379\U0001f37a\U0001f37b\U0001f37c\U0001f380\U0001f381\U0001f382\U0001f383\U0001f384\U0001f385\U0001f386\U0001f387\U0001f388\U0001f389\U0001f38a\U0001f38b\U0001f38c\U0001f38d\U0001f38e\U0001f38f\U0001f390\U0001f391\U0001f392\U0001f393\U0001f3a0\U0001f3a1\U0001f3a2\U0001f3a3\U0001f3a4\U0001f3a5\U0001f3a6\U0001f3a7\U0001f3a8\U0001f3a9\U0001f3aa\U0001f3ab\U0001f3ac\U0001f3ad\U0001f3ae\U0001f3af\U0001f3b0\U0001f3b1\U0001f3b2\U0001f3b3\U0001f3b4\U0001f3b5\U0001f3b6\U0001f3b7\U0001f3b8\U0001f3b9\U0001f3ba\U0001f3bb\U0001f3bc\U0001f3bd\U0001f3be\U0001f3bf\U0001f3c0\U0001f3c1\U0001f3c2\U0001f3c3\U0001f3c4\U0001f3c6\U0001f3c7\U0001f3c8\U0001f3c9\U0001f3ca\U0001f3e0\U0001f3e1\U0001f3e2\U0001f3e3\U0001f3e4\U0001f3e5\U0001f3e6\U0001f3e7\U0001f3e8\U0001f3e9\U0001f3ea\U0001f3eb\U0001f3ec\U0001f3ed\U0001f3ee\U0001f3ef\U0001f3f0\U0001f400\U0001f401\U0001f402\U0001f403\U0001f404\U0001f405\U0001f406\U0001f407\U0001f408\U0001f409\U0001f40a\U0001f40b\U0001f40c\U0001f40d\U0001f40e\U0001f40f\U0001f410\U0001f411\U0001f412\U0001f413\U0001f414\U0001f415\U0001f416\U0001f417\U0001f418\U0001f419\U0001f41a\U0001f41b\U0001f41c\U0001f41d\U0001f41e\U0001f41f\U0001f420\U0001f421\U0001f422\U0001f423\U0001f424\U0001f425\U0001f426\U0001f427\U0001f428\U0001f429\U0001f42a\U0001f42b\U0001f42c\U0001f42d\U0001f42e\U0001f42f\U0001f430\U0001f431\U0001f432\U0001f433\U0001f434\U0001f435\U0001f436\U0001f437\U0001f438\U0001f439\U0001f43a\U0001f43b\U0001f43c\U0001f43d\U0001f43e\U0001f440\U0001f442\U0001f443\U0001f444\U0001f445\U0001f446\U0001f447\U0001f448\U0001f449\U0001f44a\U0001f44b\U0001f44c\U0001f44d\U0001f44e\U0001f44f\U0001f450\U0001f451\U0001f452\U0001f453\U0001f454\U0001f455\U0001f456\U0001f457\U0001f458\U0001f459\U0001f45a\U0001f45b\U0001f45c\U0001f45d\U0001f45e\U0001f45f\U0001f460\U0001f461\U0001f462\U0001f463\U0001f464\U0001f465\U0001f466\U0001f467\U0001f468\U0001f469\U0001f46a\U0001f46b\U0001f46c\U0001f46d\U0001f46e\U0001f46f\U0001f470\U0001f471\U0001f472\U0001f473\U0001f474\U0001f475\U0001f476\U0001f477\U0001f478\U0001f479\U0001f47a\U0001f47b\U0001f47c\U0001f47d\U0001f47e\U0001f47f\U0001f480\U0001f481\U0001f482\U0001f483\U0001f484\U0001f485\U0001f486\U0001f487\U0001f488\U0001f489\U0001f48a\U0001f48b\U0001f48c\U0001f48d\U0001f48e\U0001f48f\U0001f490\U0001f491\U0001f492\U0001f493\U0001f494\U0001f495\U0001f496\U0001f497\U0001f498\U0001f499\U0001f49a\U0001f49b\U0001f49c\U0001f49d\U0001f49e\U0001f49f\U0001f4a0\U0001f4a1\U0001f4a2\U0001f4a3\U0001f4a4\U0001f4a5\U0001f4a6\U0001f4a7\U0001f4a8\U0001f4a9\U0001f4aa\U0001f4ab\U0001f4ac\U0001f4ad\U0001f4ae\U0001f4af\U0001f4b0\U0001f4b1\U0001f4b2\U0001f4b3\U0001f4b4\U0001f4b5\U0001f4b6\U0001f4b7\U0001f4b8\U0001f4b9\U0001f4ba\U0001f4bb\U0001f4bc\U0001f4bd\U0001f4be\U0001f4bf\U0001f4c0\U0001f4c1\U0001f4c2\U0001f4c3\U0001f4c4\U0001f4c5\U0001f4c6\U0001f4c7\U0001f4c8\U0001f4c9\U0001f4ca\U0001f4cb\U0001f4cc\U0001f4cd\U0001f4ce\U0001f4cf\U0001f4d0\U0001f4d1\U0001f4d2\U0001f4d3\U0001f4d4\U0001f4d5\U0001f4d6\U0001f4d7\U0001f4d8\U0001f4d9\U0001f4da\U0001f4db\U0001f4dc\U0001f4dd\U0001f4de\U0001f4df\U0001f4e0\U0001f4e1\U0001f4e2\U0001f4e3\U0001f4e4\U0001f4e5\U0001f4e6\U0001f4e7\U0001f4e8\U0001f4e9\U0001f4ea\U0001f4eb\U0001f4ec\U0001f4ed\U0001f4ee\U0001f4ef\U0001f4f0\U0001f4f1\U0001f4f2\U0001f4f3\U0001f4f4\U0001f4f5\U0001f4f6\U0001f4f7\U0001f4f9\U0001f4fa\U0001f4fb\U0001f4fc\U0001f500\U0001f501\U0001f502\U0001f503\U0001f504\U0001f505\U0001f506\U0001f507\U0001f508\U0001f509\U0001f50a\U0001f50b\U0001f50c\U0001f50d\U0001f50e\U0001f50f\U0001f510\U0001f511\U0001f512\U0001f513\U0001f514\U0001f515\U0001f516\U0001f517\U0001f518\U0001f519\U0001f51a\U0001f51b\U0001f51c\U0001f51d\U0001f51e\U0001f51f\U0001f520\U0001f521\U0001f522\U0001f523\U0001f524\U0001f525\U0001f526\U0001f527\U0001f528\U0001f529\U0001f52a\U0001f52b\U0001f52c\U0001f52d\U0001f52e\U0001f52f\U0001f530\U0001f531\U0001f532\U0001f533\U0001f534\U0001f535\U0001f536\U0001f537\U0001f538\U0001f539\U0001f53a\U0001f53b\U0001f53c\U0001f53d\U0001f550\U0001f551\U0001f552\U0001f553\U0001f554\U0001f555\U0001f556\U0001f557\U0001f558\U0001f559\U0001f55a\U0001f55b\U0001f55c\U0001f55d\U0001f55e\U0001f55f\U0001f560\U0001f561\U0001f562\U0001f563\U0001f564\U0001f565\U0001f566\U0001f567\U0001f5fb\U0001f5fc\U0001f5fd\U0001f5fe\U0001f5ff\U0001f600\U0001f601\U0001f602\U0001f603\U0001f604\U0001f605\U0001f606\U0001f607\U0001f608\U0001f609\U0001f60a\U0001f60b\U0001f60c\U0001f60d\U0001f60e\U0001f60f\U0001f610\U0001f611\U0001f612\U0001f613\U0001f614\U0001f615\U0001f616\U0001f617\U0001f618\U0001f619\U0001f61a\U0001f61b\U0001f61c\U0001f61d\U0001f61e\U0001f61f\U0001f620\U0001f621\U0001f622\U0001f623\U0001f624\U0001f625\U0001f626\U0001f627\U0001f628\U0001f629\U0001f62a\U0001f62b\U0001f62c\U0001f62d\U0001f62e\U0001f62f\U0001f630\U0001f631\U0001f632\U0001f633\U0001f634\U0001f635\U0001f636\U0001f637\U0001f638\U0001f639\U0001f63a\U0001f63b\U0001f63c\U0001f63d\U0001f63e\U0001f63f\U0001f640\U0001f645\U0001f646\U0001f647\U0001f648\U0001f649\U0001f64a\U0001f64b\U0001f64c\U0001f64d\U0001f64e\U0001f64f\U0001f680\U0001f681\U0001f682\U0001f683\U0001f684\U0001f685\U0001f686\U0001f687\U0001f688\U0001f689\U0001f68a\U0001f68b\U0001f68c\U0001f68d\U0001f68e\U0001f68f\U0001f690\U0001f691\U0001f692\U0001f693\U0001f694\U0001f695\U0001f696\U0001f697\U0001f698\U0001f699\U0001f69a\U0001f69b\U0001f69c\U0001f69d\U0001f69e\U0001f69f\U0001f6a0\U0001f6a1\U0001f6a2\U0001f6a3\U0001f6a4\U0001f6a5\U0001f6a6\U0001f6a7\U0001f6a8\U0001f6a9\U0001f6aa\U0001f6ab\U0001f6ac\U0001f6ad\U0001f6ae\U0001f6af\U0001f6b0\U0001f6b1\U0001f6b2\U0001f6b3\U0001f6b4\U0001f6b5\U0001f6b6\U0001f6b7\U0001f6b8\U0001f6b9\U0001f6ba\U0001f6bb\U0001f6bc\U0001f6bd\U0001f6be\U0001f6bf\U0001f6c0\U0001f6c1\U0001f6c2\U0001f6c3\U0001f6c4\U0001f6c5"]; maxSet = minSetOld.mutableCopy; [maxSet addCharactersInRange:NSMakeRange(0x20e3, 1)]; // Combining Enclosing Keycap (multi-face emoji) [maxSet addCharactersInRange:NSMakeRange(0xfe0f, 1)]; // Variation Selector [maxSet addCharactersInRange:NSMakeRange(0x1f1e6, 26)]; // Regional Indicator Symbol Letter minSet8_3 = minSetOld.mutableCopy; [minSet8_3 addCharactersInRange:NSMakeRange(0x1f3fb, 5)]; // Color of skin }); // 1. Most of string does not contains emoji, so test the maximum range of charset first. if ([self rangeOfCharacterFromSet:maxSet].location == NSNotFound) return NO; // 2. If the emoji can be detected by the minimum charset, return 'YES' directly. if ([self rangeOfCharacterFromSet:((systemVersion < 8.3) ? minSetOld : minSet8_3)].location != NSNotFound) return YES; // 3. The string contains some characters which may compose an emoji, but cannot detected with charset. // Use a regular expression to detect the emoji. It's slower than using charset. static NSRegularExpression *regexOld, *regex8_3, *regex9_0; static dispatch_once_t onceTokenRegex; dispatch_once(&onceTokenRegex, ^{ regexOld = [NSRegularExpression regularExpressionWithPattern:@"(©️|®️|™️|〰️|🇨🇳|🇩🇪|🇪🇸|🇫🇷|🇬🇧|🇮🇹|🇯🇵|🇰🇷|🇷🇺|🇺🇸)" options:kNilOptions error:nil]; regex8_3 = [NSRegularExpression regularExpressionWithPattern:@"(©️|®️|™️|〰️|🇦🇺|🇦🇹|🇧🇪|🇧🇷|🇨🇦|🇨🇱|🇨🇳|🇨🇴|🇩🇰|🇫🇮|🇫🇷|🇩🇪|🇭🇰|🇮🇳|🇮🇩|🇮🇪|🇮🇱|🇮🇹|🇯🇵|🇰🇷|🇲🇴|🇲🇾|🇲🇽|🇳🇱|🇳🇿|🇳🇴|🇵🇭|🇵🇱|🇵🇹|🇵🇷|🇷🇺|🇸🇦|🇸🇬|🇿🇦|🇪🇸|🇸🇪|🇨🇭|🇹🇷|🇬🇧|🇺🇸|🇦🇪|🇻🇳)" options:kNilOptions error:nil]; regex9_0 = [NSRegularExpression regularExpressionWithPattern:@"(©️|®️|™️|〰️|🇦🇫|🇦🇽|🇦🇱|🇩🇿|🇦🇸|🇦🇩|🇦🇴|🇦🇮|🇦🇶|🇦🇬|🇦🇷|🇦🇲|🇦🇼|🇦🇺|🇦🇹|🇦🇿|🇧🇸|🇧🇭|🇧🇩|🇧🇧|🇧🇾|🇧🇪|🇧🇿|🇧🇯|🇧🇲|🇧🇹|🇧🇴|🇧🇶|🇧🇦|🇧🇼|🇧🇻|🇧🇷|🇮🇴|🇻🇬|🇧🇳|🇧🇬|🇧🇫|🇧🇮|🇰🇭|🇨🇲|🇨🇦|🇨🇻|🇰🇾|🇨🇫|🇹🇩|🇨🇱|🇨🇳|🇨🇽|🇨🇨|🇨🇴|🇰🇲|🇨🇬|🇨🇩|🇨🇰|🇨🇷|🇨🇮|🇭🇷|🇨🇺|🇨🇼|🇨🇾|🇨🇿|🇩🇰|🇩🇯|🇩🇲|🇩🇴|🇪🇨|🇪🇬|🇸🇻|🇬🇶|🇪🇷|🇪🇪|🇪🇹|🇫🇰|🇫🇴|🇫🇯|🇫🇮|🇫🇷|🇬🇫|🇵🇫|🇹🇫|🇬🇦|🇬🇲|🇬🇪|🇩🇪|🇬🇭|🇬🇮|🇬🇷|🇬🇱|🇬🇩|🇬🇵|🇬🇺|🇬🇹|🇬🇬|🇬🇳|🇬🇼|🇬🇾|🇭🇹|🇭🇲|🇭🇳|🇭🇰|🇭🇺|🇮🇸|🇮🇳|🇮🇩|🇮🇷|🇮🇶|🇮🇪|🇮🇲|🇮🇱|🇮🇹|🇯🇲|🇯🇵|🇯🇪|🇯🇴|🇰🇿|🇰🇪|🇰🇮|🇰🇼|🇰🇬|🇱🇦|🇱🇻|🇱🇧|🇱🇸|🇱🇷|🇱🇾|🇱🇮|🇱🇹|🇱🇺|🇲🇴|🇲🇰|🇲🇬|🇲🇼|🇲🇾|🇲🇻|🇲🇱|🇲🇹|🇲🇭|🇲🇶|🇲🇷|🇲🇺|🇾🇹|🇲🇽|🇫🇲|🇲🇩|🇲🇨|🇲🇳|🇲🇪|🇲🇸|🇲🇦|🇲🇿|🇲🇲|🇳🇦|🇳🇷|🇳🇵|🇳🇱|🇳🇨|🇳🇿|🇳🇮|🇳🇪|🇳🇬|🇳🇺|🇳🇫|🇲🇵|🇰🇵|🇳🇴|🇴🇲|🇵🇰|🇵🇼|🇵🇸|🇵🇦|🇵🇬|🇵🇾|🇵🇪|🇵🇭|🇵🇳|🇵🇱|🇵🇹|🇵🇷|🇶🇦|🇷🇪|🇷🇴|🇷🇺|🇷🇼|🇧🇱|🇸🇭|🇰🇳|🇱🇨|🇲🇫|🇻🇨|🇼🇸|🇸🇲|🇸🇹|🇸🇦|🇸🇳|🇷🇸|🇸🇨|🇸🇱|🇸🇬|🇸🇰|🇸🇮|🇸🇧|🇸🇴|🇿🇦|🇬🇸|🇰🇷|🇸🇸|🇪🇸|🇱🇰|🇸🇩|🇸🇷|🇸🇯|🇸🇿|🇸🇪|🇨🇭|🇸🇾|🇹🇼|🇹🇯|🇹🇿|🇹🇭|🇹🇱|🇹🇬|🇹🇰|🇹🇴|🇹🇹|🇹🇳|🇹🇷|🇹🇲|🇹🇨|🇹🇻|🇺🇬|🇺🇦|🇦🇪|🇬🇧|🇺🇸|🇺🇲|🇻🇮|🇺🇾|🇺🇿|🇻🇺|🇻🇦|🇻🇪|🇻🇳|🇼🇫|🇪🇭|🇾🇪|🇿🇲|🇿🇼)" options:kNilOptions error:nil]; }); NSRange regexRange = [(systemVersion < 8.3 ? regexOld : systemVersion < 9.0 ? regex8_3 : regex9_0) rangeOfFirstMatchInString:self options:kNilOptions range:NSMakeRange(0, self.length)]; return regexRange.location != NSNotFound; } + (NSString *)stringWithUUID { CFUUIDRef uuid = CFUUIDCreate(NULL); CFStringRef string = CFUUIDCreateString(NULL, uuid); CFRelease(uuid); return (__bridge_transfer NSString *)string; } + (NSString *)stringWithUTF32Char:(UTF32Char)char32 { char32 = NSSwapHostIntToLittle(char32); return [[NSString alloc] initWithBytes:&char32 length:4 encoding:NSUTF32LittleEndianStringEncoding]; } + (NSString *)stringWithUTF32Chars:(const UTF32Char *)char32 length:(NSUInteger)length { return [[NSString alloc] initWithBytes:(const void *)char32 length:length * 4 encoding:NSUTF32LittleEndianStringEncoding]; } - (void)enumerateUTF32CharInRange:(NSRange)range usingBlock:(void (^)(UTF32Char char32, NSRange range, BOOL *stop))block { NSString *str = self; if (range.location != 0 || range.length != self.length) { str = [self substringWithRange:range]; } NSUInteger len = [str lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; UTF32Char *char32 = (UTF32Char *)[str cStringUsingEncoding:NSUTF32LittleEndianStringEncoding]; if (len == 0 || char32 == NULL) return; NSUInteger location = 0; BOOL stop = NO; NSRange subRange; UTF32Char oneChar; for (NSUInteger i = 0; i < len; i++) { oneChar = char32[i]; subRange = NSMakeRange(location, oneChar > 0xFFFF ? 2 : 1); block(oneChar, subRange, &stop); if (stop) return; location += subRange.length; } } - (NSString *)stringByTrim { NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet]; return [self stringByTrimmingCharactersInSet:set]; } - (NSString *)stringByAppendingNameScale:(CGFloat)scale { if (fabs(scale - 1) <= __FLT_EPSILON__ || self.length == 0 || [self hasSuffix:@"/"]) return self.copy; return [self stringByAppendingFormat:@"@%@x", @(scale)]; } - (NSString *)stringByAppendingPathScale:(CGFloat)scale { if (fabs(scale - 1) <= __FLT_EPSILON__ || self.length == 0 || [self hasSuffix:@"/"]) return self.copy; NSString *ext = self.pathExtension; NSRange extRange = NSMakeRange(self.length - ext.length, 0); if (ext.length > 0) extRange.location -= 1; NSString *scaleStr = [NSString stringWithFormat:@"@%@x", @(scale)]; return [self stringByReplacingCharactersInRange:extRange withString:scaleStr]; } - (CGFloat)pathScale { if (self.length == 0 || [self hasSuffix:@"/"]) return 1; NSString *name = self.stringByDeletingPathExtension; __block CGFloat scale = 1; [name enumerateRegexMatches:@"@[0-9]+\\.?[0-9]*x$" options:NSRegularExpressionAnchorsMatchLines usingBlock: ^(NSString *match, NSRange matchRange, BOOL *stop) { scale = [match substringWithRange:NSMakeRange(1, match.length - 2)].doubleValue; }]; return scale; } - (BOOL)isNotBlank { NSCharacterSet *blank = [NSCharacterSet whitespaceAndNewlineCharacterSet]; for (NSInteger i = 0; i < self.length; ++i) { unichar c = [self characterAtIndex:i]; if (![blank characterIsMember:c]) { return YES; } } return NO; } - (BOOL)containsString:(NSString *)string { if (string == nil) return NO; return [self rangeOfString:string].location != NSNotFound; } - (BOOL)containsCharacterSet:(NSCharacterSet *)set { if (set == nil) return NO; return [self rangeOfCharacterFromSet:set].location != NSNotFound; } - (NSRange)rangeOfAll { return NSMakeRange(0, self.length); } + (NSString *)stringNamed:(NSString *)name { NSString *path = [[NSBundle mainBundle] pathForResource:name ofType:@""]; NSString *str = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; if (!str) { path = [[NSBundle mainBundle] pathForResource:name ofType:@"txt"]; str = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; } return str; } @end ================================================ FILE: Demo/YYTextDemo/UIControl+YYAdd.h ================================================ // // UIControl+YYAdd.h // YYKit // // Created by ibireme on 13/4/5. // 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 /** Provides extensions for `UIControl`. */ @interface UIControl (YYAdd) /** Removes all targets and actions for a particular event (or events) from an internal dispatch table. */ - (void)removeAllTargets; /** Adds or replaces a target and action for a particular event (or events) to an internal dispatch table. @param target The target object—that is, the object to which the action message is sent. If this is nil, the responder chain is searched for an object willing to respond to the action message. @param action A selector identifying an action message. It cannot be NULL. @param controlEvents A bitmask specifying the control events for which the action message is sent. */ - (void)setTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents; /** Adds a block for a particular event (or events) to an internal dispatch table. It will cause a strong reference to @a block. @param block The block which is invoked then the action message is sent (cannot be nil). The block is retained. @param controlEvents A bitmask specifying the control events for which the action message is sent. */ - (void)addBlockForControlEvents:(UIControlEvents)controlEvents block:(void (^)(id sender))block; /** Adds or replaces a block for a particular event (or events) to an internal dispatch table. It will cause a strong reference to @a block. @param block The block which is invoked then the action message is sent (cannot be nil). The block is retained. @param controlEvents A bitmask specifying the control events for which the action message is sent. */ - (void)setBlockForControlEvents:(UIControlEvents)controlEvents block:(void (^)(id sender))block; /** Removes all blocks for a particular event (or events) from an internal dispatch table. @param controlEvents A bitmask specifying the control events for which the action message is sent. */ - (void)removeAllBlocksForControlEvents:(UIControlEvents)controlEvents; @end ================================================ FILE: Demo/YYTextDemo/UIControl+YYAdd.m ================================================ // // UIControl+YYAdd.m // YYKit // // Created by ibireme on 13/4/5. // 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 "UIControl+YYAdd.h" #import static const int block_key; @interface _YYUIControlBlockTarget : NSObject @property (nonatomic, copy) void (^block)(id sender); @property (nonatomic, assign) UIControlEvents events; - (id)initWithBlock:(void (^)(id sender))block events:(UIControlEvents)events; - (void)invoke:(id)sender; @end @implementation _YYUIControlBlockTarget - (id)initWithBlock:(void (^)(id sender))block events:(UIControlEvents)events { self = [super init]; if (self) { _block = [block copy]; _events = events; } return self; } - (void)invoke:(id)sender { if (_block) _block(sender); } @end @implementation UIControl (YYAdd) - (void)removeAllTargets { [[self allTargets] enumerateObjectsUsingBlock: ^(id object, BOOL *stop) { [self removeTarget:object action:NULL forControlEvents:UIControlEventAllEvents]; }]; } - (void)setTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents { NSSet *targets = [self allTargets]; for (id currentTarget in targets) { NSArray *actions = [self actionsForTarget:currentTarget forControlEvent:controlEvents]; for (NSString *currentAction in actions) { [self removeTarget:currentTarget action:NSSelectorFromString(currentAction) forControlEvents:controlEvents]; } } [self addTarget:target action:action forControlEvents:controlEvents]; } - (void)addBlockForControlEvents:(UIControlEvents)controlEvents block:(void (^)(id sender))block { _YYUIControlBlockTarget *target = [[_YYUIControlBlockTarget alloc] initWithBlock:block events:controlEvents]; [self addTarget:target action:@selector(invoke:) forControlEvents:controlEvents]; NSMutableArray *targets = [self _yy_allUIControlBlockTargets]; [targets addObject:target]; } - (void)setBlockForControlEvents:(UIControlEvents)controlEvents block:(void (^)(id sender))block { [self removeAllBlocksForControlEvents:controlEvents]; [self addBlockForControlEvents:controlEvents block:block]; } - (void)removeAllBlocksForControlEvents:(UIControlEvents)controlEvents { NSMutableArray *targets = [self _yy_allUIControlBlockTargets]; NSMutableArray *removes = [NSMutableArray array]; [targets enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) { _YYUIControlBlockTarget *target = (_YYUIControlBlockTarget *)obj; if (target.events == controlEvents) { [removes addObject:target]; [self removeTarget:target action:@selector(invoke:) forControlEvents:controlEvents]; } }]; [targets removeObjectsInArray:removes]; } - (NSMutableArray *)_yy_allUIControlBlockTargets { NSMutableArray *targets = objc_getAssociatedObject(self, &block_key); if (!targets) { targets = [NSMutableArray array]; objc_setAssociatedObject(self, &block_key, targets, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return targets; } @end ================================================ FILE: Demo/YYTextDemo/UIGestureRecognizer+YYAdd.h ================================================ // // UIGestureRecognizer+YYAdd.h // YYKit // // Created by ibireme on 13/10/13. // 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 /** Provides extensions for `UIGestureRecognizer`. */ @interface UIGestureRecognizer (YYAdd) /** Initializes an allocated gesture-recognizer object with a action block. @param block An action block that to handle the gesture recognized by the receiver. nil is invalid. It is retained by the gesture. @return An initialized instance of a concrete UIGestureRecognizer subclass or nil if an error occurred in the attempt to initialize the object. */ - (instancetype)initWithActionBlock:(void (^)(id sender))block; /** Adds an action block to a gesture-recognizer object. It is retained by the gesture. @param block A block invoked by the action message. nil is not a valid value. */ - (void)addActionBlock:(void (^)(id sender))block; /** Remove all action blocks. */ - (void)removeAllActionBlocks; @end ================================================ FILE: Demo/YYTextDemo/UIGestureRecognizer+YYAdd.m ================================================ // // UIGestureRecognizer+YYAdd.m // YYKit // // Created by ibireme on 13/10/13. // 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 "UIGestureRecognizer+YYAdd.h" #import static const int block_key; @interface _YYUIGestureRecognizerBlockTarget : NSObject @property (nonatomic, copy) void (^block)(id sender); - (id)initWithBlock:(void (^)(id sender))block; - (void)invoke:(id)sender; @end @implementation _YYUIGestureRecognizerBlockTarget - (id)initWithBlock:(void (^)(id sender))block{ self = [super init]; if (self) { _block = [block copy]; } return self; } - (void)invoke:(id)sender { if (_block) _block(sender); } @end @implementation UIGestureRecognizer (YYAdd) - (instancetype)initWithActionBlock:(void (^)(id sender))block { self = [self init]; [self addActionBlock:block]; return self; } - (void)addActionBlock:(void (^)(id sender))block { _YYUIGestureRecognizerBlockTarget *target = [[_YYUIGestureRecognizerBlockTarget alloc] initWithBlock:block]; [self addTarget:target action:@selector(invoke:)]; NSMutableArray *targets = [self _yy_allUIGestureRecognizerBlockTargets]; [targets addObject:target]; } - (void)removeAllActionBlocks{ NSMutableArray *targets = [self _yy_allUIGestureRecognizerBlockTargets]; [targets enumerateObjectsUsingBlock:^(id target, NSUInteger idx, BOOL *stop) { [self removeTarget:target action:@selector(invoke:)]; }]; [targets removeAllObjects]; } - (NSMutableArray *)_yy_allUIGestureRecognizerBlockTargets { NSMutableArray *targets = objc_getAssociatedObject(self, &block_key); if (!targets) { targets = [NSMutableArray array]; objc_setAssociatedObject(self, &block_key, targets, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return targets; } @end ================================================ FILE: Demo/YYTextDemo/UIImage+YYWebImage.h ================================================ // // UIImage+YYWebImage.h // YYWebImage // // Created by ibireme on 13/4/4. // 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 /** Provide some commen method for `UIImage`. Image process is based on CoreGraphic and vImage. */ @interface UIImage (YYWebImage) #pragma mark - Create image ///============================================================================= /// @name Create image ///============================================================================= /** Create an animated image with GIF data. After created, you can access the images via property '.images'. If the data is not animated gif, this function is same as [UIImage imageWithData:data scale:scale]; @discussion It has a better display performance, but costs more memory (width * height * frames Bytes). It only suited to display small gif such as animated emoticon. If you want to display large gif, see `YYImage`. @param data GIF data. @param scale The scale factor @return A new image created from GIF, or nil when an error occurs. */ + (UIImage *)yy_imageWithSmallGIFData:(NSData *)data scale:(CGFloat)scale; /** Create and return a 1x1 point size image with the given color. @param color The color. */ + (UIImage *)yy_imageWithColor:(UIColor *)color; /** Create and return a pure color image with the given color and size. @param color The color. @param size New image's type. */ + (UIImage *)yy_imageWithColor:(UIColor *)color size:(CGSize)size; /** Create and return an image with custom draw code. @param size The image size. @param drawBlock The draw block. @return The new image. */ + (UIImage *)yy_imageWithSize:(CGSize)size drawBlock:(void (^)(CGContextRef context))drawBlock; #pragma mark - Image Info ///============================================================================= /// @name Image Info ///============================================================================= /** Whether this image has alpha channel. */ - (BOOL)yy_hasAlphaChannel; #pragma mark - Modify Image ///============================================================================= /// @name Modify Image ///============================================================================= /** Draws the entire image in the specified rectangle, content changed with the contentMode. @discussion This method draws the entire image in the current graphics context, respecting the image's orientation setting. In the default coordinate system, images are situated down and to the right of the origin of the specified rectangle. This method respects any transforms applied to the current graphics context, however. @param rect The rectangle in which to draw the image. @param contentMode Draw content mode @param clips A Boolean value that determines whether content are confined to the rect. */ - (void)yy_drawInRect:(CGRect)rect withContentMode:(UIViewContentMode)contentMode clipsToBounds:(BOOL)clips; /** Returns a new image which is scaled from this image. The image will be stretched as needed. @param size The new size to be scaled, values should be positive. @return The new image with the given size. */ - (UIImage *)yy_imageByResizeToSize:(CGSize)size; /** Returns a new image which is scaled from this image. The image content will be changed with thencontentMode. @param size The new size to be scaled, values should be positive. @param contentMode The content mode for image content. @return The new image with the given size. */ - (UIImage *)yy_imageByResizeToSize:(CGSize)size contentMode:(UIViewContentMode)contentMode; /** Returns a new image which is cropped from this image. @param rect Image's inner rect. @return The new image, or nil if an error occurs. */ - (UIImage *)yy_imageByCropToRect:(CGRect)rect; /** Returns a new image which is edge inset from this image. @param insets Inset (positive) for each of the edges, values can be negative to 'outset'. @param color Extend edge's fill color, nil means clear color. @return The new image, or nil if an error occurs. */ - (UIImage *)yy_imageByInsetEdge:(UIEdgeInsets)insets withColor:(UIColor *)color; /** Rounds a new image with a given corner size. @param radius The radius of each corner oval. Values larger than half the rectangle's width or height are clamped appropriately to half the width or height. */ - (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius; /** Rounds a new image with a given corner size. @param radius The radius of each corner oval. Values larger than half the rectangle's width or height are clamped appropriately to half the width or height. @param corners A bitmask value that identifies the corners that you want rounded. You can use this parameter to round only a subset of the corners of the rectangle. @param borderWidth The inset border with clear color. Values larger than half the rectangle's width or height are clamped appropriately to half the width or height. */ - (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth; /** Returns a new rotated image (relative to the center). @param radians Rotated radians in counterclockwise.⟲ @param fitSize YES: new image's size is extend to fit all content. NO: image's size will not change, content may be clipped. */ - (UIImage *)yy_imageByRotate:(CGFloat)radians fitSize:(BOOL)fitSize; /** Returns a new image rotated counterclockwise by a quarter‑turn (90°). ⤺ The width and height will be exchanged. */ - (UIImage *)yy_imageByRotateLeft90; /** Returns a new image rotated clockwise by a quarter‑turn (90°). ⤼ The width and height will be exchanged. */ - (UIImage *)yy_imageByRotateRight90; /** Returns a new image rotated 180° . ↻ */ - (UIImage *)yy_imageByRotate180; /** Returns a vertically flipped image. ⥯ */ - (UIImage *)yy_imageByFlipVertical; /** Returns a horizontally flipped image. ⇋ */ - (UIImage *)yy_imageByFlipHorizontal; #pragma mark - Image Effect ///============================================================================= /// @name Image Effect ///============================================================================= /** Tint the image in alpha channel with the given color. @param color The color. */ - (UIImage *)yy_imageByTintColor:(UIColor *)color; /** Returns a grayscaled image. */ - (UIImage *)yy_imageByGrayscale; /** Applies a blur effect to this image. Suitable for blur any content. */ - (UIImage *)yy_imageByBlurSoft; /** Applies a blur effect to this image. Suitable for blur any content except pure white. (same as iOS Control Panel) */ - (UIImage *)yy_imageByBlurLight; /** Applies a blur effect to this image. Suitable for displaying black text. (same as iOS Navigation Bar White) */ - (UIImage *)yy_imageByBlurExtraLight; /** Applies a blur effect to this image. Suitable for displaying white text. (same as iOS Notification Center) */ - (UIImage *)yy_imageByBlurDark; /** Applies a blur and tint color to this image. @param tintColor The tint color. */ - (UIImage *)yy_imageByBlurWithTint:(UIColor *)tintColor; /** Applies a blur, tint color, and saturation adjustment to this image, optionally within the area specified by @a maskImage. @param blurRadius The radius of the blur in points, 0 means no blur effect. @param tintColor An optional UIColor object that is uniformly blended with the result of the blur and saturation operations. The alpha channel of this color determines how strong the tint is. nil means no tint. @param tintBlendMode The @a tintColor blend mode. Default is kCGBlendModeNormal (0). @param saturation A value of 1.0 produces no change in the resulting image. Values less than 1.0 will desaturation the resulting image while values greater than 1.0 will have the opposite effect. 0 means gray scale. @param maskImage If specified, @a inputImage is only modified in the area(s) defined by this mask. This must be an image mask or it must meet the requirements of the mask parameter of CGContextClipToMask. @return image with effect, or nil if an error occurs (e.g. no enough memory). */ - (UIImage *)yy_imageByBlurRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor tintMode:(CGBlendMode)tintBlendMode saturation:(CGFloat)saturation maskImage:(UIImage *)maskImage; @end ================================================ FILE: Demo/YYTextDemo/UIImage+YYWebImage.m ================================================ // // UIImage+YYWebImage.m // YYWebImage // // Created by ibireme on 13/4/4. // 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 "UIImage+YYWebImage.h" #import #import #import /// Convert degrees to radians. static inline CGFloat _DegreesToRadians(CGFloat degrees) { return degrees * M_PI / 180; } /** Resize rect to fit the size using a given contentMode. @param rect The draw rect @param size The content size @param mode The content mode @return A resized rect for the given content mode. @discussion UIViewContentModeRedraw is same as UIViewContentModeScaleToFill. */ static CGRect _YYCGRectFitWithContentMode(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 (size.width / size.height < rect.size.width / rect.size.height && mode == UIViewContentModeScaleAspectFit) { scale = rect.size.height / size.height; } else { scale = rect.size.width / size.width; } 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; } static NSTimeInterval _yy_CGImageSourceGetGIFFrameDelayAtIndex(CGImageSourceRef source, size_t index) { NSTimeInterval delay = 0; CFDictionaryRef dic = CGImageSourceCopyPropertiesAtIndex(source, index, NULL); if (dic) { CFDictionaryRef dicGIF = CFDictionaryGetValue(dic, kCGImagePropertyGIFDictionary); if (dicGIF) { NSNumber *num = CFDictionaryGetValue(dicGIF, kCGImagePropertyGIFUnclampedDelayTime); if (num.doubleValue <= __FLT_EPSILON__) { num = CFDictionaryGetValue(dicGIF, kCGImagePropertyGIFDelayTime); } delay = num.doubleValue; } CFRelease(dic); } // http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility if (delay < 0.02) delay = 0.1; return delay; } @implementation UIImage (YYWebImage) + (UIImage *)yy_imageWithSmallGIFData:(NSData *)data scale:(CGFloat)scale { CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFTypeRef)(data), NULL); if (!source) return nil; size_t count = CGImageSourceGetCount(source); if (count <= 1) { CFRelease(source); return [self.class imageWithData:data scale:scale]; } NSUInteger frames[count]; double oneFrameTime = 1 / 50.0; // 50 fps NSTimeInterval totalTime = 0; NSUInteger totalFrame = 0; NSUInteger gcdFrame = 0; for (size_t i = 0; i < count; i++) { NSTimeInterval delay = _yy_CGImageSourceGetGIFFrameDelayAtIndex(source, i); totalTime += delay; NSInteger frame = lrint(delay / oneFrameTime); if (frame < 1) frame = 1; frames[i] = frame; totalFrame += frames[i]; if (i == 0) gcdFrame = frames[i]; else { NSUInteger frame = frames[i], tmp; if (frame < gcdFrame) { tmp = frame; frame = gcdFrame; gcdFrame = tmp; } while (true) { tmp = frame % gcdFrame; if (tmp == 0) break; frame = gcdFrame; gcdFrame = tmp; } } } NSMutableArray *array = [NSMutableArray new]; for (size_t i = 0; i < count; i++) { CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL); if (!imageRef) { CFRelease(source); return nil; } size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); if (width == 0 || height == 0) { CFRelease(source); CFRelease(imageRef); return nil; } CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask; BOOL hasAlpha = NO; if (alphaInfo == kCGImageAlphaPremultipliedLast || alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaLast || alphaInfo == kCGImageAlphaFirst) { hasAlpha = YES; } // BGRA8888 (premultiplied) or BGRX8888 // same as UIGraphicsBeginImageContext() and -[UIView drawRect:] CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, space, bitmapInfo); CGColorSpaceRelease(space); if (!context) { CFRelease(source); CFRelease(imageRef); return nil; } CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode CGImageRef decoded = CGBitmapContextCreateImage(context); CFRelease(context); if (!decoded) { CFRelease(source); CFRelease(imageRef); return nil; } UIImage *image = image = [UIImage imageWithCGImage:decoded scale:scale orientation:UIImageOrientationUp]; CGImageRelease(imageRef); CGImageRelease(decoded); if (!image) { CFRelease(source); return nil; } for (size_t j = 0, max = frames[i] / gcdFrame; j < max; j++) { [array addObject:image]; } } CFRelease(source); UIImage *image = [self.class animatedImageWithImages:array duration:totalTime]; return image; } + (UIImage *)yy_imageWithColor:(UIColor *)color { return [self yy_imageWithColor:color size:CGSizeMake(1, 1)]; } + (UIImage *)yy_imageWithColor:(UIColor *)color size:(CGSize)size { if (!color || size.width <= 0 || size.height <= 0) return nil; CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height); UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, color.CGColor); CGContextFillRect(context, rect); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } + (UIImage *)yy_imageWithSize:(CGSize)size drawBlock:(void (^)(CGContextRef context))drawBlock { if (!drawBlock) return nil; UIGraphicsBeginImageContextWithOptions(size, NO, 0); CGContextRef context = UIGraphicsGetCurrentContext(); if (!context) return nil; drawBlock(context); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } - (BOOL)yy_hasAlphaChannel { if (self.CGImage == NULL) return NO; CGImageAlphaInfo alpha = CGImageGetAlphaInfo(self.CGImage) & kCGBitmapAlphaInfoMask; return (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); } - (void)yy_drawInRect:(CGRect)rect withContentMode:(UIViewContentMode)contentMode clipsToBounds:(BOOL)clips{ CGRect drawRect = _YYCGRectFitWithContentMode(rect, self.size, contentMode); if (drawRect.size.width == 0 || drawRect.size.height == 0) return; if (clips) { CGContextRef context = UIGraphicsGetCurrentContext(); if (context) { CGContextSaveGState(context); CGContextAddRect(context, rect); CGContextClip(context); [self drawInRect:drawRect]; CGContextRestoreGState(context); } } else { [self drawInRect:drawRect]; } } - (UIImage *)yy_imageByResizeToSize:(CGSize)size { if (size.width <= 0 || size.height <= 0) return nil; UIGraphicsBeginImageContextWithOptions(size, NO, self.scale); [self drawInRect:CGRectMake(0, 0, size.width, size.height)]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } - (UIImage *)yy_imageByResizeToSize:(CGSize)size contentMode:(UIViewContentMode)contentMode { if (size.width <= 0 || size.height <= 0) return nil; UIGraphicsBeginImageContextWithOptions(size, NO, self.scale); [self yy_drawInRect:CGRectMake(0, 0, size.width, size.height) withContentMode:contentMode clipsToBounds:NO]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } - (UIImage *)yy_imageByCropToRect:(CGRect)rect { rect.origin.x *= self.scale; rect.origin.y *= self.scale; rect.size.width *= self.scale; rect.size.height *= self.scale; if (rect.size.width <= 0 || rect.size.height <= 0) return nil; CGImageRef imageRef = CGImageCreateWithImageInRect(self.CGImage, rect); UIImage *image = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; CGImageRelease(imageRef); return image; } - (UIImage *)yy_imageByInsetEdge:(UIEdgeInsets)insets withColor:(UIColor *)color { CGSize size = self.size; size.width -= insets.left + insets.right; size.height -= insets.top + insets.bottom; if (size.width <= 0 || size.height <= 0) return nil; CGRect rect = CGRectMake(-insets.left, -insets.top, self.size.width, self.size.height); UIGraphicsBeginImageContextWithOptions(size, NO, self.scale); CGContextRef context = UIGraphicsGetCurrentContext(); if (color) { CGContextSetFillColorWithColor(context, color.CGColor); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height)); CGPathAddRect(path, NULL, rect); CGContextAddPath(context, path); CGContextEOFillPath(context); CGPathRelease(path); } [self drawInRect:rect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } - (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius { return [self yy_imageByRoundCornerRadius:radius corners:UIRectCornerAllCorners borderWidth:0]; } - (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth { UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); CGContextScaleCTM(context, 1, -1); CGContextTranslateCTM(context, 0, -rect.size.height); CGFloat minSize = MIN(self.size.width, self.size.height); if (borderWidth < minSize / 2) { [[UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)] addClip]; CGContextDrawImage(context, rect, self.CGImage); } UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } - (UIImage *)yy_imageByRotate:(CGFloat)radians fitSize:(BOOL)fitSize { size_t width = (size_t)CGImageGetWidth(self.CGImage); size_t height = (size_t)CGImageGetHeight(self.CGImage); CGRect newRect = CGRectApplyAffineTransform(CGRectMake(0., 0., width, height), fitSize ? CGAffineTransformMakeRotation(radians) : CGAffineTransformIdentity); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(NULL, (size_t)newRect.size.width, (size_t)newRect.size.height, 8, (size_t)newRect.size.width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (!context) return nil; CGContextSetShouldAntialias(context, true); CGContextSetAllowsAntialiasing(context, true); CGContextSetInterpolationQuality(context, kCGInterpolationHigh); CGContextTranslateCTM(context, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); CGContextRotateCTM(context, radians); CGContextDrawImage(context, CGRectMake(-(width * 0.5), -(height * 0.5), width, height), self.CGImage); CGImageRef imgRef = CGBitmapContextCreateImage(context); UIImage *img = [UIImage imageWithCGImage:imgRef scale:self.scale orientation:self.imageOrientation]; CGImageRelease(imgRef); CGContextRelease(context); return img; } - (UIImage *)_yy_flipHorizontal:(BOOL)horizontal vertical:(BOOL)vertical { if (!self.CGImage) return nil; size_t width = (size_t)CGImageGetWidth(self.CGImage); size_t height = (size_t)CGImageGetHeight(self.CGImage); size_t bytesPerRow = width * 4; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (!context) return nil; CGContextDrawImage(context, CGRectMake(0, 0, width, height), self.CGImage); UInt8 *data = (UInt8 *)CGBitmapContextGetData(context); if (!data) { CGContextRelease(context); return nil; } vImage_Buffer src = { data, height, width, bytesPerRow }; vImage_Buffer dest = { data, height, width, bytesPerRow }; if (vertical) { vImageVerticalReflect_ARGB8888(&src, &dest, kvImageBackgroundColorFill); } if (horizontal) { vImageHorizontalReflect_ARGB8888(&src, &dest, kvImageBackgroundColorFill); } CGImageRef imgRef = CGBitmapContextCreateImage(context); CGContextRelease(context); UIImage *img = [UIImage imageWithCGImage:imgRef scale:self.scale orientation:self.imageOrientation]; CGImageRelease(imgRef); return img; } - (UIImage *)yy_imageByRotateLeft90 { return [self yy_imageByRotate:_DegreesToRadians(90) fitSize:YES]; } - (UIImage *)yy_imageByRotateRight90 { return [self yy_imageByRotate:_DegreesToRadians(-90) fitSize:YES]; } - (UIImage *)yy_imageByRotate180 { return [self _yy_flipHorizontal:YES vertical:YES]; } - (UIImage *)yy_imageByFlipVertical { return [self _yy_flipHorizontal:NO vertical:YES]; } - (UIImage *)yy_imageByFlipHorizontal { return [self _yy_flipHorizontal:YES vertical:NO]; } - (UIImage *)yy_imageByTintColor:(UIColor *)color { UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); [color set]; UIRectFill(rect); [self drawAtPoint:CGPointMake(0, 0) blendMode:kCGBlendModeDestinationIn alpha:1]; UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return newImage; } - (UIImage *)yy_imageByGrayscale { return [self yy_imageByBlurRadius:0 tintColor:nil tintMode:0 saturation:0 maskImage:nil]; } - (UIImage *)yy_imageByBlurSoft { return [self yy_imageByBlurRadius:60 tintColor:[UIColor colorWithWhite:0.84 alpha:0.36] tintMode:kCGBlendModeNormal saturation:1.8 maskImage:nil]; } - (UIImage *)yy_imageByBlurLight { return [self yy_imageByBlurRadius:60 tintColor:[UIColor colorWithWhite:1.0 alpha:0.3] tintMode:kCGBlendModeNormal saturation:1.8 maskImage:nil]; } - (UIImage *)yy_imageByBlurExtraLight { return [self yy_imageByBlurRadius:40 tintColor:[UIColor colorWithWhite:0.97 alpha:0.82] tintMode:kCGBlendModeNormal saturation:1.8 maskImage:nil]; } - (UIImage *)yy_imageByBlurDark { return [self yy_imageByBlurRadius:40 tintColor:[UIColor colorWithWhite:0.11 alpha:0.73] tintMode:kCGBlendModeNormal saturation:1.8 maskImage:nil]; } - (UIImage *)yy_imageByBlurWithTint:(UIColor *)tintColor { const CGFloat EffectColorAlpha = 0.6; UIColor *effectColor = tintColor; size_t componentCount = CGColorGetNumberOfComponents(tintColor.CGColor); if (componentCount == 2) { CGFloat b; if ([tintColor getWhite:&b alpha:NULL]) { effectColor = [UIColor colorWithWhite:b alpha:EffectColorAlpha]; } } else { CGFloat r, g, b; if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) { effectColor = [UIColor colorWithRed:r green:g blue:b alpha:EffectColorAlpha]; } } return [self yy_imageByBlurRadius:20 tintColor:effectColor tintMode:kCGBlendModeNormal saturation:-1.0 maskImage:nil]; } - (UIImage *)yy_imageByBlurRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor tintMode:(CGBlendMode)tintBlendMode saturation:(CGFloat)saturation maskImage:(UIImage *)maskImage { if (self.size.width < 1 || self.size.height < 1) { NSLog(@"UIImage+YYAdd error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self); return nil; } if (!self.CGImage) { NSLog(@"UIImage+YYAdd error: inputImage must be backed by a CGImage: %@", self); return nil; } if (maskImage && !maskImage.CGImage) { NSLog(@"UIImage+YYAdd error: effectMaskImage must be backed by a CGImage: %@", maskImage); return nil; } // iOS7 and above can use new func. BOOL hasNewFunc = (long)vImageBuffer_InitWithCGImage != 0 && (long)vImageCreateCGImageFromBuffer != 0; BOOL hasBlur = blurRadius > __FLT_EPSILON__; BOOL hasSaturation = fabs(saturation - 1.0) > __FLT_EPSILON__; CGSize size = self.size; CGRect rect = { CGPointZero, size }; CGFloat scale = self.scale; CGImageRef imageRef = self.CGImage; BOOL opaque = NO; if (!hasBlur && !hasSaturation) { return [self _yy_mergeImageRef:imageRef tintColor:tintColor tintBlendMode:tintBlendMode maskImage:maskImage opaque:opaque]; } vImage_Buffer effect = { 0 }, scratch = { 0 }; vImage_Buffer *input = NULL, *output = NULL; vImage_CGImageFormat format = { .bitsPerComponent = 8, .bitsPerPixel = 32, .colorSpace = NULL, .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little, //requests a BGRA buffer. .version = 0, .decode = NULL, .renderingIntent = kCGRenderingIntentDefault }; if (hasNewFunc) { vImage_Error err; err = vImageBuffer_InitWithCGImage(&effect, &format, NULL, imageRef, kvImagePrintDiagnosticsToConsole); if (err != kvImageNoError) { NSLog(@"UIImage+YYAdd error: vImageBuffer_InitWithCGImage returned error code %zi for inputImage: %@", err, self); return nil; } err = vImageBuffer_Init(&scratch, effect.height, effect.width, format.bitsPerPixel, kvImageNoFlags); if (err != kvImageNoError) { NSLog(@"UIImage+YYAdd error: vImageBuffer_Init returned error code %zi for inputImage: %@", err, self); return nil; } } else { UIGraphicsBeginImageContextWithOptions(size, opaque, scale); CGContextRef effectCtx = UIGraphicsGetCurrentContext(); CGContextScaleCTM(effectCtx, 1.0, -1.0); CGContextTranslateCTM(effectCtx, 0, -size.height); CGContextDrawImage(effectCtx, rect, imageRef); effect.data = CGBitmapContextGetData(effectCtx); effect.width = CGBitmapContextGetWidth(effectCtx); effect.height = CGBitmapContextGetHeight(effectCtx); effect.rowBytes = CGBitmapContextGetBytesPerRow(effectCtx); UIGraphicsBeginImageContextWithOptions(size, opaque, scale); CGContextRef scratchCtx = UIGraphicsGetCurrentContext(); scratch.data = CGBitmapContextGetData(scratchCtx); scratch.width = CGBitmapContextGetWidth(scratchCtx); scratch.height = CGBitmapContextGetHeight(scratchCtx); scratch.rowBytes = CGBitmapContextGetBytesPerRow(scratchCtx); } input = &effect; output = &scratch; if (hasBlur) { // A description of how to compute the box kernel width from the Gaussian // radius (aka standard deviation) appears in the SVG spec: // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement // // For larger values of 's' (s >= 2.0), an approximation can be used: Three // successive box-blurs build a piece-wise quadratic convolution kernel, which // approximates the Gaussian kernel to within roughly 3%. // // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) // // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel. // CGFloat inputRadius = blurRadius * scale; if (inputRadius - 2.0 < __FLT_EPSILON__) inputRadius = 2.0; uint32_t radius = floor((inputRadius * 3.0 * sqrt(2 * M_PI) / 4 + 0.5) / 2); radius |= 1; // force radius to be odd so that the three box-blur methodology works. int iterations; if (blurRadius * scale < 0.5) iterations = 1; else if (blurRadius * scale < 1.5) iterations = 2; else iterations = 3; NSInteger tempSize = vImageBoxConvolve_ARGB8888(input, output, NULL, 0, 0, radius, radius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend); void *temp = malloc(tempSize); for (int i = 0; i < iterations; i++) { vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend); // swap vImage_Buffer *swap_tmp = input; input = output; output = swap_tmp; } free(temp); } if (hasSaturation) { // These values appear in the W3C Filter Effects spec: // https://dvcs.w3.org/hg/FXTF/raw-file/default/filters/Publish.html#grayscaleEquivalent CGFloat s = saturation; CGFloat matrixFloat[] = { 0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0, 0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0, 0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0, 0, 0, 0, 1, }; const int32_t divisor = 256; NSUInteger matrixSize = sizeof(matrixFloat) / sizeof(matrixFloat[0]); int16_t matrix[matrixSize]; for (NSUInteger i = 0; i < matrixSize; ++i) { matrix[i] = (int16_t)roundf(matrixFloat[i] * divisor); } vImageMatrixMultiply_ARGB8888(input, output, matrix, divisor, NULL, NULL, kvImageNoFlags); // swap vImage_Buffer *swap_tmp = input; input = output; output = swap_tmp; } UIImage *outputImage = nil; if (hasNewFunc) { CGImageRef effectCGImage = NULL; effectCGImage = vImageCreateCGImageFromBuffer(input, &format, &_yy_cleanupBuffer, NULL, kvImageNoAllocate, NULL); if (effectCGImage == NULL) { effectCGImage = vImageCreateCGImageFromBuffer(input, &format, NULL, NULL, kvImageNoFlags, NULL); free(input->data); } free(output->data); outputImage = [self _yy_mergeImageRef:effectCGImage tintColor:tintColor tintBlendMode:tintBlendMode maskImage:maskImage opaque:opaque]; CGImageRelease(effectCGImage); } else { CGImageRef effectCGImage; UIImage *effectImage; if (input != &effect) effectImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); if (input == &effect) effectImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); effectCGImage = effectImage.CGImage; outputImage = [self _yy_mergeImageRef:effectCGImage tintColor:tintColor tintBlendMode:tintBlendMode maskImage:maskImage opaque:opaque]; } return outputImage; } // Helper function to handle deferred cleanup of a buffer. static void _yy_cleanupBuffer(void *userData, void *buf_data) { free(buf_data); } // Helper function to add tint and mask. - (UIImage *)_yy_mergeImageRef:(CGImageRef)effectCGImage tintColor:(UIColor *)tintColor tintBlendMode:(CGBlendMode)tintBlendMode maskImage:(UIImage *)maskImage opaque:(BOOL)opaque { BOOL hasTint = tintColor != nil && CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__; BOOL hasMask = maskImage != nil; CGSize size = self.size; CGRect rect = { CGPointZero, size }; CGFloat scale = self.scale; if (!hasTint && !hasMask) { return [UIImage imageWithCGImage:effectCGImage]; } UIGraphicsBeginImageContextWithOptions(size, opaque, scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextScaleCTM(context, 1.0, -1.0); CGContextTranslateCTM(context, 0, -size.height); if (hasMask) { CGContextDrawImage(context, rect, self.CGImage); CGContextSaveGState(context); CGContextClipToMask(context, rect, maskImage.CGImage); } CGContextDrawImage(context, rect, effectCGImage); if (hasTint) { CGContextSaveGState(context); CGContextSetBlendMode(context, tintBlendMode); CGContextSetFillColorWithColor(context, tintColor.CGColor); CGContextFillRect(context, rect); CGContextRestoreGState(context); } if (hasMask) { CGContextRestoreGState(context); } UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return outputImage; } @end ================================================ FILE: Demo/YYTextDemo/UIView+YYAdd.h ================================================ // // UIView+YYAdd.h // YYCategories // // 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 /** Provides extensions for `UIView`. */ @interface UIView (YYAdd) /** Shortcut to set the view.layer's shadow @param color Shadow Color @param offset Shadow offset @param radius Shadow radius */ - (void)setLayerShadow:(UIColor*)color offset:(CGSize)offset radius:(CGFloat)radius; /** Remove all subviews. @warning Never call this method inside your view's drawRect: method. */ - (void)removeAllSubviews; /** Returns the view's view controller (may be nil). */ @property (nonatomic, readonly) UIViewController *viewController; @property (nonatomic) CGFloat left; ///< Shortcut for frame.origin.x. @property (nonatomic) CGFloat top; ///< Shortcut for frame.origin.y @property (nonatomic) CGFloat right; ///< Shortcut for frame.origin.x + frame.size.width @property (nonatomic) CGFloat bottom; ///< Shortcut for frame.origin.y + frame.size.height @property (nonatomic) CGFloat width; ///< Shortcut for frame.size.width. @property (nonatomic) CGFloat height; ///< Shortcut for frame.size.height. @property (nonatomic) CGFloat centerX; ///< Shortcut for center.x @property (nonatomic) CGFloat centerY; ///< Shortcut for center.y @property (nonatomic) CGPoint origin; ///< Shortcut for frame.origin. @property (nonatomic) CGSize size; ///< Shortcut for frame.size. @end double YYDeviceSystemVersion(); #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 CGSize YYDeviceScreenSize(); /// 屏幕宽度 #ifndef kScreenWidth #define kScreenWidth YYDeviceScreenSize().width #endif /// 屏幕高度 #ifndef kScreenHeight #define kScreenHeight YYDeviceScreenSize().height #endif /// 屏幕大小 #ifndef kScreenSize #define kScreenSize YYDeviceScreenSize() #endif /// 屏幕Scale #ifndef kScreenScale #define kScreenScale [UIScreen mainScreen].scale #endif /* Create UIColor with a hex string. Example: UIColorHex(0xF0F), UIColorHex(66ccff), UIColorHex(#66CCFF88) Valid format: #RGB #RGBA #RRGGBB #RRGGBBAA 0xRGB ... The `#` or "0x" sign is not required. */ #ifndef UIColorHex #define UIColorHex(_hex_) [UIColor colorWithHexString:((__bridge NSString *)CFSTR(#_hex_))] #endif @interface UIColor (YYAdd) /** Creates and returns a color object from hex string. @discussion: Valid format: #RGB #RGBA #RRGGBB #RRGGBBAA 0xRGB ... The `#` or "0x" sign is not required. The alpha will be set to 1.0 if there is no alpha component. It will return nil when an error occurs in parsing. Example: @"0xF0F", @"66ccff", @"#66CCFF88" @param hexStr The hex string value for the new color. @return An UIColor object from string, or nil if an error occurs. */ + (UIColor *)colorWithHexString:(NSString *)hexStr; /** Creates and returns a color object by add new color. @param add the color added @param blendMode add color blend mode */ - (UIColor *)colorByAddColor:(UIColor *)add blendMode:(CGBlendMode)blendMode; @end ================================================ FILE: Demo/YYTextDemo/UIView+YYAdd.m ================================================ // // UIView+YYAdd.m // YYCategories // // 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+YYAdd.h" #import @implementation UIView (YYAdd) - (void)setLayerShadow:(UIColor*)color offset:(CGSize)offset radius:(CGFloat)radius { self.layer.shadowColor = color.CGColor; self.layer.shadowOffset = offset; self.layer.shadowRadius = radius; self.layer.shadowOpacity = 1; self.layer.shouldRasterize = YES; self.layer.rasterizationScale = [UIScreen mainScreen].scale; } - (void)removeAllSubviews { //[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; while (self.subviews.count) { [self.subviews.lastObject removeFromSuperview]; } } - (UIViewController *)viewController { for (UIView *view = self; view; view = view.superview) { UIResponder *nextResponder = [view nextResponder]; if ([nextResponder isKindOfClass:[UIViewController class]]) { return (UIViewController *)nextResponder; } } return nil; } - (CGFloat)left { return self.frame.origin.x; } - (void)setLeft:(CGFloat)x { CGRect frame = self.frame; frame.origin.x = x; self.frame = frame; } - (CGFloat)top { return self.frame.origin.y; } - (void)setTop:(CGFloat)y { CGRect frame = self.frame; frame.origin.y = y; self.frame = frame; } - (CGFloat)right { return self.frame.origin.x + self.frame.size.width; } - (void)setRight:(CGFloat)right { CGRect frame = self.frame; frame.origin.x = right - frame.size.width; self.frame = frame; } - (CGFloat)bottom { return self.frame.origin.y + self.frame.size.height; } - (void)setBottom:(CGFloat)bottom { CGRect frame = self.frame; frame.origin.y = bottom - frame.size.height; self.frame = frame; } - (CGFloat)width { return self.frame.size.width; } - (void)setWidth:(CGFloat)width { CGRect frame = self.frame; frame.size.width = width; self.frame = frame; } - (CGFloat)height { return self.frame.size.height; } - (void)setHeight:(CGFloat)height { CGRect frame = self.frame; frame.size.height = height; self.frame = frame; } - (CGFloat)centerX { return self.center.x; } - (void)setCenterX:(CGFloat)centerX { self.center = CGPointMake(centerX, self.center.y); } - (CGFloat)centerY { return self.center.y; } - (void)setCenterY:(CGFloat)centerY { self.center = CGPointMake(self.center.x, centerY); } - (CGPoint)origin { return self.frame.origin; } - (void)setOrigin:(CGPoint)origin { CGRect frame = self.frame; frame.origin = origin; self.frame = frame; } - (CGSize)size { return self.frame.size; } - (void)setSize:(CGSize)size { CGRect frame = self.frame; frame.size = size; self.frame = frame; } @end double YYDeviceSystemVersion() { static double version; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ version = [UIDevice currentDevice].systemVersion.doubleValue; }); return version; } CGSize YYDeviceScreenSize() { 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; } @implementation UIColor (YYAdd) static inline NSUInteger hexStrToInt(NSString *str) { uint32_t result = 0; sscanf([str UTF8String], "%X", &result); return result; } static BOOL hexStrToRGBA(NSString *str, CGFloat *r, CGFloat *g, CGFloat *b, CGFloat *a) { str = [str uppercaseString]; if ([str hasPrefix:@"#"]) { str = [str substringFromIndex:1]; } else if ([str hasPrefix:@"0X"]) { str = [str substringFromIndex:2]; } NSUInteger length = [str length]; // RGB RGBA RRGGBB RRGGBBAA if (length != 3 && length != 4 && length != 6 && length != 8) { return NO; } //RGB,RGBA,RRGGBB,RRGGBBAA if (length < 5) { *r = hexStrToInt([str substringWithRange:NSMakeRange(0, 1)]) / 255.0f; *g = hexStrToInt([str substringWithRange:NSMakeRange(1, 1)]) / 255.0f; *b = hexStrToInt([str substringWithRange:NSMakeRange(2, 1)]) / 255.0f; if (length == 4) *a = hexStrToInt([str substringWithRange:NSMakeRange(3, 1)]) / 255.0f; else *a = 1; } else { *r = hexStrToInt([str substringWithRange:NSMakeRange(0, 2)]) / 255.0f; *g = hexStrToInt([str substringWithRange:NSMakeRange(2, 2)]) / 255.0f; *b = hexStrToInt([str substringWithRange:NSMakeRange(4, 2)]) / 255.0f; if (length == 8) *a = hexStrToInt([str substringWithRange:NSMakeRange(6, 2)]) / 255.0f; else *a = 1; } return YES; } + (instancetype)colorWithHexString:(NSString *)hexStr { CGFloat r, g, b, a; if (hexStrToRGBA(hexStr, &r, &g, &b, &a)) { return [UIColor colorWithRed:r green:g blue:b alpha:a]; } return nil; } - (UIColor *)colorByAddColor:(UIColor *)add blendMode:(CGBlendMode)blendMode { CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big; uint8_t pixel[4] = { 0 }; CGContextRef context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, colorSpace, bitmapInfo); CGContextSetFillColorWithColor(context, self.CGColor); CGContextFillRect(context, CGRectMake(0, 0, 1, 1)); CGContextSetBlendMode(context, blendMode); CGContextSetFillColorWithColor(context, add.CGColor); CGContextFillRect(context, CGRectMake(0, 0, 1, 1)); CGContextRelease(context); CGColorSpaceRelease(colorSpace); return [UIColor colorWithRed:pixel[0] / 255.0f green:pixel[1] / 255.0f blue:pixel[2] / 255.0f alpha:pixel[3] / 255.0f]; } @end ================================================ FILE: Demo/YYTextDemo/ViewController.h ================================================ // // ViewController.h // YYTextDemo // // Created by ibireme on 15/10/17. // Copyright © 2015年 ibireme. All rights reserved. // #import @interface ViewController : UINavigationController @end ================================================ FILE: Demo/YYTextDemo/ViewController.m ================================================ // // ViewController.m // YYTextDemo // // Created by ibireme on 15/10/17. // Copyright © 2015年 ibireme. All rights reserved. // #import "ViewController.h" #import "YYTextExample.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; YYTextExample *vc = [YYTextExample new]; [self pushViewController:vc animated:NO]; } @end ================================================ FILE: Demo/YYTextDemo/YYFPSLabel.h ================================================ // // YYFPSLabel.h // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import /** Show Screen FPS... The maximum fps in OSX/iOS Simulator is 60.00. The maximum fps on iPhone is 59.97. The maxmium fps on iPad is 60.0. */ @interface YYFPSLabel : UILabel @end ================================================ FILE: Demo/YYTextDemo/YYFPSLabel.m ================================================ // // YYFPSLabel.m // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYFPSLabel.h" //#import #import "YYText.h" #import "YYWeakProxy.h" #define kSize CGSizeMake(55, 20) @implementation YYFPSLabel { CADisplayLink *_link; NSUInteger _count; NSTimeInterval _lastTime; UIFont *_font; UIFont *_subFont; NSTimeInterval _llll; } - (instancetype)initWithFrame:(CGRect)frame { if (frame.size.width == 0 && frame.size.height == 0) { frame.size = kSize; } self = [super initWithFrame:frame]; self.layer.cornerRadius = 5; self.clipsToBounds = YES; self.textAlignment = NSTextAlignmentCenter; self.userInteractionEnabled = NO; self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700]; _font = [UIFont fontWithName:@"Menlo" size:14]; if (_font) { _subFont = [UIFont fontWithName:@"Menlo" size:4]; } else { _font = [UIFont fontWithName:@"Courier" size:14]; _subFont = [UIFont fontWithName:@"Courier" size:4]; } _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)]; [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; return self; } - (void)dealloc { [_link invalidate]; } - (CGSize)sizeThatFits:(CGSize)size { return kSize; } - (void)tick:(CADisplayLink *)link { if (_lastTime == 0) { _lastTime = link.timestamp; return; } _count++; NSTimeInterval delta = link.timestamp - _lastTime; if (delta < 1) return; _lastTime = link.timestamp; float fps = _count / delta; _count = 0; CGFloat progress = fps / 60.0; UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1]; NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]]; [text yy_setColor:color range:NSMakeRange(0, text.length - 3)]; [text yy_setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)]; text.yy_font = _font; [text yy_setFont:_subFont range:NSMakeRange(text.length - 4, 1)]; self.attributedText = text; } @end ================================================ FILE: Demo/YYTextDemo/YYGestureRecognizer.h ================================================ // // YYGestureRecognizer.h // YYKit // // 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 /// State of the gesture typedef NS_ENUM(NSUInteger, YYGestureRecognizerState) { YYGestureRecognizerStateBegan, ///< gesture start YYGestureRecognizerStateMoved, ///< gesture moved YYGestureRecognizerStateEnded, ///< gesture end YYGestureRecognizerStateCancelled, ///< gesture cancel }; /** A simple UIGestureRecognizer subclass for receive touch events. */ @interface YYGestureRecognizer : UIGestureRecognizer @property (nonatomic, readonly) CGPoint startPoint; ///< start point @property (nonatomic, readonly) CGPoint lastPoint; ///< last move point. @property (nonatomic, readonly) CGPoint currentPoint; ///< current move point. /// The action block invoked by every gesture event. @property (nonatomic, copy) void (^action)(YYGestureRecognizer *gesture, YYGestureRecognizerState state); /// Cancel the gesture for current touch. - (void)cancel; @end ================================================ FILE: Demo/YYTextDemo/YYGestureRecognizer.m ================================================ // // YYGestureRecognizer.m // YYKit // // 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 "YYGestureRecognizer.h" #import @implementation YYGestureRecognizer - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { self.state = UIGestureRecognizerStateBegan; _startPoint = [(UITouch *)[touches anyObject] locationInView:self.view]; _lastPoint = _currentPoint; _currentPoint = _startPoint; if (_action) _action(self, YYGestureRecognizerStateBegan); } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = (UITouch *)[touches anyObject]; CGPoint currentPoint = [touch locationInView:self.view]; self.state = UIGestureRecognizerStateChanged; _currentPoint = currentPoint; if (_action) _action(self, YYGestureRecognizerStateMoved); _lastPoint = _currentPoint; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { self.state = UIGestureRecognizerStateEnded; if (_action) _action(self, YYGestureRecognizerStateEnded); } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { self.state = UIGestureRecognizerStateCancelled; if (_action) _action(self, YYGestureRecognizerStateCancelled); } - (void)reset { self.state = UIGestureRecognizerStatePossible; } - (void)cancel { if (self.state == UIGestureRecognizerStateBegan || self.state == UIGestureRecognizerStateChanged) { self.state = UIGestureRecognizerStateCancelled; if (_action) _action(self, YYGestureRecognizerStateCancelled); } } @end ================================================ FILE: Demo/YYTextDemo/YYImage/Animated image support.txt ================================================ You may import YYImage (https://github.com/ibireme/YYImage) to support animated image copy and paste. ================================================ FILE: Demo/YYTextDemo/YYImage/YYAnimatedImageView.h ================================================ // // YYAnimatedImageView.h // YYImage // // Created by ibireme on 14/10/19. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import NS_ASSUME_NONNULL_BEGIN /** An image view for displaying animated image. @discussion It is a fully compatible `UIImageView` subclass. If the `image` or `highlightedImage` property adopt to the `YYAnimatedImage` protocol, then it can be used to play the multi-frame animation. The animation can also be controlled with the UIImageView methods `-startAnimating`, `-stopAnimating` and `-isAnimating`. This view request the frame data just in time. When the device has enough free memory, this view may cache some or all future frames in an inner buffer for lower CPU cost. Buffer size is dynamically adjusted based on the current state of the device memory. Sample Code: // ani@3x.gif YYImage *image = [YYImage imageNamed:@"ani"]; YYAnimatedImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image]; [view addSubView:imageView]; */ @interface YYAnimatedImageView : UIImageView /** If the image has more than one frame, set this value to `YES` will automatically play/stop the animation when the view become visible/invisible. The default value is `YES`. */ @property (nonatomic) BOOL autoPlayAnimatedImage; /** Index of the currently displayed frame (index from 0). Set a new value to this property will cause to display the new frame immediately. If the new value is invalid, this method has no effect. You can add an observer to this property to observe the playing status. */ @property (nonatomic) NSUInteger currentAnimatedImageIndex; /** Whether the image view is playing animation currently. You can add an observer to this property to observe the playing status. */ @property (nonatomic, readonly) BOOL currentIsPlayingAnimation; /** The animation timer's runloop mode, default is `NSRunLoopCommonModes`. Set this property to `NSDefaultRunLoopMode` will make the animation pause during UIScrollView scrolling. */ @property (nonatomic, copy) NSString *runloopMode; /** The max size (in bytes) for inner frame buffer size, default is 0 (dynamically). When the device has enough free memory, this view will request and decode some or all future frame image into an inner buffer. If this property's value is 0, then the max buffer size will be dynamically adjusted based on the current state of the device free memory. Otherwise, the buffer size will be limited by this value. When receive memory warning or app enter background, the buffer will be released immediately, and may grow back at the right time. */ @property (nonatomic) NSUInteger maxBufferSize; @end /** The YYAnimatedImage protocol declares the required methods for animated image display with YYAnimatedImageView. Subclass a UIImage and implement this protocol, so that instances of that class can be set to YYAnimatedImageView.image or YYAnimatedImageView.highlightedImage to display animation. See `YYImage` and `YYFrameImage` for example. */ @protocol YYAnimatedImage @required /// Total animated frame count. /// It the frame count is less than 1, then the methods below will be ignored. - (NSUInteger)animatedImageFrameCount; /// Animation loop count, 0 means infinite looping. - (NSUInteger)animatedImageLoopCount; /// Bytes per frame (in memory). It may used to optimize memory buffer size. - (NSUInteger)animatedImageBytesPerFrame; /// Returns the frame image from a specified index. /// This method may be called on background thread. /// @param index Frame index (zero based). - (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index; /// Returns the frames's duration from a specified index. /// @param index Frame index (zero based). - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index; @optional /// A rectangle in image coordinates defining the subrectangle of the image that /// will be displayed. The rectangle should not outside the image's bounds. /// It may used to display sprite animation with a single image (sprite sheet). - (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index; @end NS_ASSUME_NONNULL_END ================================================ FILE: Demo/YYTextDemo/YYImage/YYAnimatedImageView.m ================================================ // // YYAnimatedImageView.m // YYImage // // Created by ibireme on 14/10/19. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import "YYAnimatedImageView.h" #import "YYImageCoder.h" #import #import #define BUFFER_SIZE (10 * 1024 * 1024) // 10MB (minimum memory buffer size) #define LOCK(...) dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER); \ __VA_ARGS__; \ dispatch_semaphore_signal(self->_lock); #define LOCK_VIEW(...) dispatch_semaphore_wait(view->_lock, DISPATCH_TIME_FOREVER); \ __VA_ARGS__; \ dispatch_semaphore_signal(view->_lock); static int64_t _YYDeviceMemoryTotal() { int64_t mem = [[NSProcessInfo processInfo] physicalMemory]; if (mem < -1) mem = -1; return mem; } static int64_t _YYDeviceMemoryFree() { mach_port_t host_port = mach_host_self(); mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t); vm_size_t page_size; vm_statistics_data_t vm_stat; kern_return_t kern; kern = host_page_size(host_port, &page_size); if (kern != KERN_SUCCESS) return -1; kern = host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size); if (kern != KERN_SUCCESS) return -1; return vm_stat.free_count * page_size; } /** A proxy used to hold a weak object. It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink. */ @interface _YYImageWeakProxy : NSProxy @property (nonatomic, weak, readonly) id target; - (instancetype)initWithTarget:(id)target; + (instancetype)proxyWithTarget:(id)target; @end @implementation _YYImageWeakProxy - (instancetype)initWithTarget:(id)target { _target = target; return self; } + (instancetype)proxyWithTarget:(id)target { return [[_YYImageWeakProxy alloc] initWithTarget:target]; } - (id)forwardingTargetForSelector:(SEL)selector { return _target; } - (void)forwardInvocation:(NSInvocation *)invocation { void *null = NULL; [invocation setReturnValue:&null]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [NSObject instanceMethodSignatureForSelector:@selector(init)]; } - (BOOL)respondsToSelector:(SEL)aSelector { return [_target respondsToSelector:aSelector]; } - (BOOL)isEqual:(id)object { return [_target isEqual:object]; } - (NSUInteger)hash { return [_target hash]; } - (Class)superclass { return [_target superclass]; } - (Class)class { return [_target class]; } - (BOOL)isKindOfClass:(Class)aClass { return [_target isKindOfClass:aClass]; } - (BOOL)isMemberOfClass:(Class)aClass { return [_target isMemberOfClass:aClass]; } - (BOOL)conformsToProtocol:(Protocol *)aProtocol { return [_target conformsToProtocol:aProtocol]; } - (BOOL)isProxy { return YES; } - (NSString *)description { return [_target description]; } - (NSString *)debugDescription { return [_target debugDescription]; } @end typedef NS_ENUM(NSUInteger, YYAnimatedImageType) { YYAnimatedImageTypeNone = 0, YYAnimatedImageTypeImage, YYAnimatedImageTypeHighlightedImage, YYAnimatedImageTypeImages, YYAnimatedImageTypeHighlightedImages, }; @interface YYAnimatedImageView() { @package UIImage *_curAnimatedImage; dispatch_once_t _onceToken; dispatch_semaphore_t _lock; ///< lock for _buffer NSOperationQueue *_requestQueue; ///< image request queue, serial CADisplayLink *_link; ///< ticker for change frame NSTimeInterval _time; ///< time after last frame UIImage *_curFrame; ///< current frame to display NSUInteger _curIndex; ///< current frame index (from 0) NSUInteger _totalFrameCount; ///< total frame count BOOL _loopEnd; ///< whether the loop is end. NSUInteger _curLoop; ///< current loop count (from 0) NSUInteger _totalLoop; ///< total loop count, 0 means infinity NSMutableDictionary *_buffer; ///< frame buffer BOOL _bufferMiss; ///< whether miss frame on last opportunity NSUInteger _maxBufferCount; ///< maximum buffer count NSInteger _incrBufferCount; ///< current allowed buffer count (will increase by step) CGRect _curContentsRect; BOOL _curImageHasContentsRect; ///< image has implementated "animatedImageContentsRectAtIndex:" } @property (nonatomic, readwrite) BOOL currentIsPlayingAnimation; - (void)calcMaxBufferCount; @end /// An operation for image fetch @interface _YYAnimatedImageViewFetchOperation : NSOperation @property (nonatomic, weak) YYAnimatedImageView *view; @property (nonatomic, assign) NSUInteger nextIndex; @property (nonatomic, strong) UIImage *curImage; @end @implementation _YYAnimatedImageViewFetchOperation - (void)main { __strong YYAnimatedImageView *view = _view; if (!view) return; if ([self isCancelled]) return; view->_incrBufferCount++; if (view->_incrBufferCount == 0) [view calcMaxBufferCount]; if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) { view->_incrBufferCount = view->_maxBufferCount; } NSUInteger idx = _nextIndex; NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount; NSUInteger total = view->_totalFrameCount; view = nil; for (int i = 0; i < max; i++, idx++) { @autoreleasepool { if (idx >= total) idx = 0; if ([self isCancelled]) break; __strong YYAnimatedImageView *view = _view; if (!view) break; LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil)); if (miss) { UIImage *img = [_curImage animatedImageFrameAtIndex:idx]; img = img.yy_imageByDecoded; if ([self isCancelled]) break; LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]); view = nil; } } } } @end @implementation YYAnimatedImageView - (instancetype)init { self = [super init]; _runloopMode = NSRunLoopCommonModes; _autoPlayAnimatedImage = YES; return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; _runloopMode = NSRunLoopCommonModes; _autoPlayAnimatedImage = YES; return self; } - (instancetype)initWithImage:(UIImage *)image { self = [super init]; _runloopMode = NSRunLoopCommonModes; _autoPlayAnimatedImage = YES; self.frame = (CGRect) {CGPointZero, image.size }; self.image = image; return self; } - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage { self = [super init]; _runloopMode = NSRunLoopCommonModes; _autoPlayAnimatedImage = YES; CGSize size = image ? image.size : highlightedImage.size; self.frame = (CGRect) {CGPointZero, size }; self.image = image; self.highlightedImage = highlightedImage; return self; } // init the animated params. - (void)resetAnimated { dispatch_once(&_onceToken, ^{ _lock = dispatch_semaphore_create(1); _buffer = [NSMutableDictionary new]; _requestQueue = [[NSOperationQueue alloc] init]; _requestQueue.maxConcurrentOperationCount = 1; _link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)]; if (_runloopMode) { [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode]; } _link.paused = YES; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; }); [_requestQueue cancelAllOperations]; LOCK( if (_buffer.count) { NSMutableDictionary *holder = _buffer; _buffer = [NSMutableDictionary new]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ // Capture the dictionary to global queue, // release these images in background to avoid blocking UI thread. [holder class]; }); } ); _link.paused = YES; _time = 0; if (_curIndex != 0) { [self willChangeValueForKey:@"currentAnimatedImageIndex"]; _curIndex = 0; [self didChangeValueForKey:@"currentAnimatedImageIndex"]; } _curAnimatedImage = nil; _curFrame = nil; _curLoop = 0; _totalLoop = 0; _totalFrameCount = 1; _loopEnd = NO; _bufferMiss = NO; _incrBufferCount = 0; } - (void)setImage:(UIImage *)image { if (self.image == image) return; [self setImage:image withType:YYAnimatedImageTypeImage]; } - (void)setHighlightedImage:(UIImage *)highlightedImage { if (self.highlightedImage == highlightedImage) return; [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage]; } - (void)setAnimationImages:(NSArray *)animationImages { if (self.animationImages == animationImages) return; [self setImage:animationImages withType:YYAnimatedImageTypeImages]; } - (void)setHighlightedAnimationImages:(NSArray *)highlightedAnimationImages { if (self.highlightedAnimationImages == highlightedAnimationImages) return; [self setImage:highlightedAnimationImages withType:YYAnimatedImageTypeHighlightedImages]; } - (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; if (_link) [self resetAnimated]; [self imageChanged]; } - (id)imageForType:(YYAnimatedImageType)type { switch (type) { case YYAnimatedImageTypeNone: return nil; case YYAnimatedImageTypeImage: return self.image; case YYAnimatedImageTypeHighlightedImage: return self.highlightedImage; case YYAnimatedImageTypeImages: return self.animationImages; case YYAnimatedImageTypeHighlightedImages: return self.highlightedAnimationImages; } return nil; } - (YYAnimatedImageType)currentImageType { YYAnimatedImageType curType = YYAnimatedImageTypeNone; if (self.highlighted) { if (self.highlightedAnimationImages.count) curType = YYAnimatedImageTypeHighlightedImages; else if (self.highlightedImage) curType = YYAnimatedImageTypeHighlightedImage; } if (curType == YYAnimatedImageTypeNone) { if (self.animationImages.count) curType = YYAnimatedImageTypeImages; else if (self.image) curType = YYAnimatedImageTypeImage; } return curType; } - (void)setImage:(id)image withType:(YYAnimatedImageType)type { [self stopAnimating]; if (_link) [self resetAnimated]; _curFrame = nil; switch (type) { case YYAnimatedImageTypeNone: break; case YYAnimatedImageTypeImage: super.image = image; break; case YYAnimatedImageTypeHighlightedImage: super.highlightedImage = image; break; case YYAnimatedImageTypeImages: super.animationImages = image; break; case YYAnimatedImageTypeHighlightedImages: super.highlightedAnimationImages = image; break; } [self imageChanged]; } - (void)imageChanged { YYAnimatedImageType newType = [self currentImageType]; id newVisibleImage = [self imageForType:newType]; NSUInteger newImageFrameCount = 0; BOOL hasContentsRect = NO; if ([newVisibleImage isKindOfClass:[UIImage class]] && [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) { newImageFrameCount = ((UIImage *) newVisibleImage).animatedImageFrameCount; if (newImageFrameCount > 1) { hasContentsRect = [((UIImage *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)]; } } if (!hasContentsRect && _curImageHasContentsRect) { if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) { [CATransaction begin]; [CATransaction setDisableActions:YES]; self.layer.contentsRect = CGRectMake(0, 0, 1, 1); [CATransaction commit]; } } _curImageHasContentsRect = hasContentsRect; if (hasContentsRect) { CGRect rect = [((UIImage *) newVisibleImage) animatedImageContentsRectAtIndex:0]; [self setContentsRect:rect forImage:newVisibleImage]; } if (newImageFrameCount > 1) { [self resetAnimated]; _curAnimatedImage = newVisibleImage; _curFrame = newVisibleImage; _totalLoop = _curAnimatedImage.animatedImageLoopCount; _totalFrameCount = _curAnimatedImage.animatedImageFrameCount; [self calcMaxBufferCount]; } [self setNeedsDisplay]; [self didMoved]; } // dynamically adjust buffer size for current memory. - (void)calcMaxBufferCount { int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame; if (bytes == 0) bytes = 1024; int64_t total = _YYDeviceMemoryTotal(); int64_t free = _YYDeviceMemoryFree(); int64_t max = MIN(total * 0.2, free * 0.6); max = MAX(max, BUFFER_SIZE); if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max; double maxBufferCount = (double)max / (double)bytes; if (maxBufferCount < 1) maxBufferCount = 1; else if (maxBufferCount > 512) maxBufferCount = 512; _maxBufferCount = maxBufferCount; } - (void)dealloc { [_requestQueue cancelAllOperations]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; [_link invalidate]; } - (BOOL)isAnimating { return self.currentIsPlayingAnimation; } - (void)stopAnimating { [super stopAnimating]; [_requestQueue cancelAllOperations]; _link.paused = YES; self.currentIsPlayingAnimation = NO; } - (void)startAnimating { YYAnimatedImageType type = [self currentImageType]; if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) { NSArray *images = [self imageForType:type]; if (images.count > 0) { [super startAnimating]; self.currentIsPlayingAnimation = YES; } } else { if (_curAnimatedImage && _link.paused) { _curLoop = 0; _loopEnd = NO; _link.paused = NO; self.currentIsPlayingAnimation = YES; } } } - (void)didReceiveMemoryWarning:(NSNotification *)notification { [_requestQueue cancelAllOperations]; [_requestQueue addOperationWithBlock: ^{ _incrBufferCount = -60 - (int)(arc4random() % 120); // about 1~3 seconds to grow back.. NSNumber *next = @((_curIndex + 1) % _totalFrameCount); LOCK( NSArray * keys = _buffer.allKeys; for (NSNumber * key in keys) { if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation [_buffer removeObjectForKey:key]; } } )//LOCK }]; } - (void)didEnterBackground:(NSNotification *)notification { [_requestQueue cancelAllOperations]; NSNumber *next = @((_curIndex + 1) % _totalFrameCount); LOCK( NSArray * keys = _buffer.allKeys; for (NSNumber * key in keys) { if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation [_buffer removeObjectForKey:key]; } } )//LOCK } - (void)step:(CADisplayLink *)link { UIImage *image = _curAnimatedImage; NSMutableDictionary *buffer = _buffer; UIImage *bufferedImage = nil; NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount; BOOL bufferIsFull = NO; if (!image) return; if (_loopEnd) { // view will keep in last frame [self stopAnimating]; return; } NSTimeInterval delay = 0; if (!_bufferMiss) { _time += link.duration; delay = [image animatedImageDurationAtIndex:_curIndex]; if (_time < delay) return; _time -= delay; if (nextIndex == 0) { _curLoop++; if (_curLoop >= _totalLoop && _totalLoop != 0) { _loopEnd = YES; [self stopAnimating]; [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep return; // stop at last frame } } delay = [image animatedImageDurationAtIndex:nextIndex]; if (_time > delay) _time = delay; // do not jump over frame } LOCK( bufferedImage = buffer[@(nextIndex)]; if (bufferedImage) { if ((int)_incrBufferCount < _totalFrameCount) { [buffer removeObjectForKey:@(nextIndex)]; } [self willChangeValueForKey:@"currentAnimatedImageIndex"]; _curIndex = nextIndex; [self didChangeValueForKey:@"currentAnimatedImageIndex"]; _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage; if (_curImageHasContentsRect) { _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex]; [self setContentsRect:_curContentsRect forImage:_curFrame]; } nextIndex = (_curIndex + 1) % _totalFrameCount; _bufferMiss = NO; if (buffer.count == _totalFrameCount) { bufferIsFull = YES; } } else { _bufferMiss = YES; } )//LOCK if (!_bufferMiss) { [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep } if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new]; operation.view = self; operation.nextIndex = nextIndex; operation.curImage = image; [_requestQueue addOperation:operation]; } } - (void)displayLayer:(CALayer *)layer { if (_curFrame) { layer.contents = (__bridge id)_curFrame.CGImage; } } - (void)setContentsRect:(CGRect)rect forImage:(UIImage *)image{ CGRect layerRect = CGRectMake(0, 0, 1, 1); if (image) { CGSize imageSize = image.size; if (imageSize.width > 0.01 && imageSize.height > 0.01) { layerRect.origin.x = rect.origin.x / imageSize.width; layerRect.origin.y = rect.origin.y / imageSize.height; layerRect.size.width = rect.size.width / imageSize.width; layerRect.size.height = rect.size.height / imageSize.height; layerRect = CGRectIntersection(layerRect, CGRectMake(0, 0, 1, 1)); if (CGRectIsNull(layerRect) || CGRectIsEmpty(layerRect)) { layerRect = CGRectMake(0, 0, 1, 1); } } } [CATransaction begin]; [CATransaction setDisableActions:YES]; self.layer.contentsRect = layerRect; [CATransaction commit]; } - (void)didMoved { if (self.autoPlayAnimatedImage) { if(self.superview && self.window) { [self startAnimating]; } else { [self stopAnimating]; } } } - (void)didMoveToWindow { [super didMoveToWindow]; [self didMoved]; } - (void)didMoveToSuperview { [super didMoveToSuperview]; [self didMoved]; } - (void)setCurrentAnimatedImageIndex:(NSUInteger)currentAnimatedImageIndex { if (!_curAnimatedImage) return; if (currentAnimatedImageIndex >= _curAnimatedImage.animatedImageFrameCount) return; if (_curIndex == currentAnimatedImageIndex) return; void (^block)() = ^{ LOCK( [_requestQueue cancelAllOperations]; [_buffer removeAllObjects]; [self willChangeValueForKey:@"currentAnimatedImageIndex"]; _curIndex = currentAnimatedImageIndex; [self didChangeValueForKey:@"currentAnimatedImageIndex"]; _curFrame = [_curAnimatedImage animatedImageFrameAtIndex:_curIndex]; if (_curImageHasContentsRect) { _curContentsRect = [_curAnimatedImage animatedImageContentsRectAtIndex:_curIndex]; } _time = 0; _loopEnd = NO; _bufferMiss = NO; [self.layer setNeedsDisplay]; )//LOCK }; if (pthread_main_np()) { block(); } else { dispatch_async(dispatch_get_main_queue(), block); } } - (NSUInteger)currentAnimatedImageIndex { return _curIndex; } - (void)setRunloopMode:(NSString *)runloopMode { if ([_runloopMode isEqual:runloopMode]) return; if (_link) { if (_runloopMode) { [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode]; } if (runloopMode.length) { [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:runloopMode]; } } _runloopMode = runloopMode.copy; } #pragma mark - Overrice NSObject(NSKeyValueObservingCustomization) + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@"currentAnimatedImageIndex"]) { return NO; } return [super automaticallyNotifiesObserversForKey:key]; } #pragma mark - NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; _runloopMode = [aDecoder decodeObjectForKey:@"runloopMode"]; if (_runloopMode.length == 0) _runloopMode = NSRunLoopCommonModes; if ([aDecoder containsValueForKey:@"autoPlayAnimatedImage"]) { _autoPlayAnimatedImage = [aDecoder decodeBoolForKey:@"autoPlayAnimatedImage"]; } else { _autoPlayAnimatedImage = YES; } UIImage *image = [aDecoder decodeObjectForKey:@"YYAnimatedImage"]; UIImage *highlightedImage = [aDecoder decodeObjectForKey:@"YYHighlightedAnimatedImage"]; if (image) { self.image = image; [self setImage:image withType:YYAnimatedImageTypeImage]; } if (highlightedImage) { self.highlightedImage = highlightedImage; [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; [aCoder encodeObject:_runloopMode forKey:@"runloopMode"]; [aCoder encodeBool:_autoPlayAnimatedImage forKey:@"autoPlayAnimatedImage"]; BOOL ani, multi; ani = [self.image conformsToProtocol:@protocol(YYAnimatedImage)]; multi = (ani && ((UIImage *)self.image).animatedImageFrameCount > 1); if (multi) [aCoder encodeObject:self.image forKey:@"YYAnimatedImage"]; ani = [self.highlightedImage conformsToProtocol:@protocol(YYAnimatedImage)]; multi = (ani && ((UIImage *)self.highlightedImage).animatedImageFrameCount > 1); if (multi) [aCoder encodeObject:self.highlightedImage forKey:@"YYHighlightedAnimatedImage"]; } @end ================================================ FILE: Demo/YYTextDemo/YYImage/YYFrameImage.h ================================================ // // YYFrameImage.h // YYImage // // Created by ibireme on 14/12/9. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import #if __has_include() #import #elif __has_include() #import #else #import "YYAnimatedImageView.h" #endif NS_ASSUME_NONNULL_BEGIN /** An image to display frame-based animation. @discussion It is a fully compatible `UIImage` subclass. It only support system image format such as png and jpeg. The animation can be played by YYAnimatedImageView. Sample Code: NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"]; NSArray *times = @[@0.1, @0.2, @0.1]; YYFrameImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES]; YYAnimatedImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image]; [view addSubView:imageView]; */ @interface YYFrameImage : UIImage /** Create a frame animated image from files. @param paths An array of NSString objects, contains the full or partial path to each image file. e.g. @[@"/ani/1.png",@"/ani/2.png",@"/ani/3.png"] @param oneFrameDuration The duration (in seconds) per frame. @param loopCount The animation loop count, 0 means infinite. @return An initialized YYFrameImage object, or nil when an error occurs. */ - (nullable instancetype)initWithImagePaths:(NSArray *)paths oneFrameDuration:(NSTimeInterval)oneFrameDuration loopCount:(NSUInteger)loopCount; /** Create a frame animated image from files. @param paths An array of NSString objects, contains the full or partial path to each image file. e.g. @[@"/ani/frame1.png",@"/ani/frame2.png",@"/ani/frame3.png"] @param frameDurations An array of NSNumber objects, contains the duration (in seconds) per frame. e.g. @[@0.1, @0.2, @0.3]; @param loopCount The animation loop count, 0 means infinite. @return An initialized YYFrameImage object, or nil when an error occurs. */ - (nullable instancetype)initWithImagePaths:(NSArray *)paths frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount; /** Create a frame animated image from an array of data. @param dataArray An array of NSData objects. @param oneFrameDuration The duration (in seconds) per frame. @param loopCount The animation loop count, 0 means infinite. @return An initialized YYFrameImage object, or nil when an error occurs. */ - (nullable instancetype)initWithImageDataArray:(NSArray *)dataArray oneFrameDuration:(NSTimeInterval)oneFrameDuration loopCount:(NSUInteger)loopCount; /** Create a frame animated image from an array of data. @param dataArray An array of NSData objects. @param frameDurations An array of NSNumber objects, contains the duration (in seconds) per frame. e.g. @[@0.1, @0.2, @0.3]; @param loopCount The animation loop count, 0 means infinite. @return An initialized YYFrameImage object, or nil when an error occurs. */ - (nullable instancetype)initWithImageDataArray:(NSArray *)dataArray frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount; @end NS_ASSUME_NONNULL_END ================================================ FILE: Demo/YYTextDemo/YYImage/YYFrameImage.m ================================================ // // YYFrameImage.m // YYImage // // Created by ibireme on 14/12/9. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import "YYFrameImage.h" #import "YYImageCoder.h" /** Return the path scale. e.g.
Path Scale
"icon.png" 1
"icon@2x.png" 2
"icon@2.5x.png" 2.5
"icon@2x" 1
"icon@2x..png" 1
"icon@2x.png/" 1
*/ static CGFloat _NSStringPathScale(NSString *string) { if (string.length == 0 || [string hasSuffix:@"/"]) return 1; NSString *name = string.stringByDeletingPathExtension; __block CGFloat scale = 1; NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:@"@[0-9]+\\.?[0-9]*x$" options:NSRegularExpressionAnchorsMatchLines error:nil]; [pattern enumerateMatchesInString:name options:kNilOptions range:NSMakeRange(0, name.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { if (result.range.location >= 3) { scale = [string substringWithRange:NSMakeRange(result.range.location + 1, result.range.length - 2)].doubleValue; } }]; return scale; } @implementation YYFrameImage { NSUInteger _loopCount; NSUInteger _oneFrameBytes; NSArray *_imagePaths; NSArray *_imageDatas; NSArray *_frameDurations; } - (instancetype)initWithImagePaths:(NSArray *)paths oneFrameDuration:(NSTimeInterval)oneFrameDuration loopCount:(NSUInteger)loopCount { NSMutableArray *durations = [NSMutableArray new]; for (int i = 0, max = (int)paths.count; i < max; i++) { [durations addObject:@(oneFrameDuration)]; } return [self initWithImagePaths:paths frameDurations:durations loopCount:loopCount]; } - (instancetype)initWithImagePaths:(NSArray *)paths frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount { if (paths.count == 0) return nil; if (paths.count != frameDurations.count) return nil; NSString *firstPath = paths[0]; NSData *firstData = [NSData dataWithContentsOfFile:firstPath]; CGFloat scale = _NSStringPathScale(firstPath); UIImage *firstCG = [[[UIImage alloc] initWithData:firstData] yy_imageByDecoded]; self = [self initWithCGImage:firstCG.CGImage scale:scale orientation:UIImageOrientationUp]; if (!self) return nil; long frameByte = CGImageGetBytesPerRow(firstCG.CGImage) * CGImageGetHeight(firstCG.CGImage); _oneFrameBytes = (NSUInteger)frameByte; _imagePaths = paths.copy; _frameDurations = frameDurations.copy; _loopCount = loopCount; return self; } - (instancetype)initWithImageDataArray:(NSArray *)dataArray oneFrameDuration:(NSTimeInterval)oneFrameDuration loopCount:(NSUInteger)loopCount { NSMutableArray *durations = [NSMutableArray new]; for (int i = 0, max = (int)dataArray.count; i < max; i++) { [durations addObject:@(oneFrameDuration)]; } return [self initWithImageDataArray:dataArray frameDurations:durations loopCount:loopCount]; } - (instancetype)initWithImageDataArray:(NSArray *)dataArray frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount { if (dataArray.count == 0) return nil; if (dataArray.count != frameDurations.count) return nil; NSData *firstData = dataArray[0]; CGFloat scale = [UIScreen mainScreen].scale; UIImage *firstCG = [[[UIImage alloc] initWithData:firstData] yy_imageByDecoded]; self = [self initWithCGImage:firstCG.CGImage scale:scale orientation:UIImageOrientationUp]; if (!self) return nil; long frameByte = CGImageGetBytesPerRow(firstCG.CGImage) * CGImageGetHeight(firstCG.CGImage); _oneFrameBytes = (NSUInteger)frameByte; _imageDatas = dataArray.copy; _frameDurations = frameDurations.copy; _loopCount = loopCount; return self; } #pragma mark - YYAnimtedImage - (NSUInteger)animatedImageFrameCount { if (_imagePaths) { return _imagePaths.count; } else if (_imageDatas) { return _imageDatas.count; } else { return 1; } } - (NSUInteger)animatedImageLoopCount { return _loopCount; } - (NSUInteger)animatedImageBytesPerFrame { return _oneFrameBytes; } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { if (_imagePaths) { if (index >= _imagePaths.count) return nil; NSString *path = _imagePaths[index]; CGFloat scale = _NSStringPathScale(path); NSData *data = [NSData dataWithContentsOfFile:path]; return [[UIImage imageWithData:data scale:scale] yy_imageByDecoded]; } else if (_imageDatas) { if (index >= _imageDatas.count) return nil; NSData *data = _imageDatas[index]; return [[UIImage imageWithData:data scale:[UIScreen mainScreen].scale] yy_imageByDecoded]; } else { return index == 0 ? self : nil; } } - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index { if (index >= _frameDurations.count) return 0; NSNumber *num = _frameDurations[index]; return [num doubleValue]; } @end ================================================ FILE: Demo/YYTextDemo/YYImage/YYImage.h ================================================ // // YYImage.h // YYImage // // Created by ibireme on 14/10/20. // 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 YYImageVersionNumber; FOUNDATION_EXPORT const unsigned char YYImageVersionString[]; #import #import #import #import #elif __has_include() #import #import #import #import #else #import "YYFrameImage.h" #import "YYSpriteSheetImage.h" #import "YYImageCoder.h" #import "YYAnimatedImageView.h" #endif NS_ASSUME_NONNULL_BEGIN /** A YYImage object is a high-level way to display animated image data. @discussion It is a fully compatible `UIImage` subclass. It extends the UIImage to support animated WebP, APNG and GIF format image data decoding. It also support NSCoding protocol to archive and unarchive multi-frame image data. If the image is created from multi-frame image data, and you want to play the animation, try replace UIImageView with `YYAnimatedImageView`. Sample Code: // animation@3x.webp YYImage *image = [YYImage imageNamed:@"animation.webp"]; YYAnimatedImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image]; [view addSubView:imageView]; */ @interface YYImage : UIImage + (nullable YYImage *)imageNamed:(NSString *)name; // no cache! + (nullable YYImage *)imageWithContentsOfFile:(NSString *)path; + (nullable YYImage *)imageWithData:(NSData *)data; + (nullable YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale; /** If the image is created from data or file, then the value indicates the data type. */ @property (nonatomic, readonly) YYImageType animatedImageType; /** If the image is created from animated image data (multi-frame GIF/APNG/WebP), this property stores the original image data. */ @property (nullable, nonatomic, readonly) NSData *animatedImageData; /** The total memory usage (in bytes) if all frame images was loaded into memory. The value is 0 if the image is not created from a multi-frame image data. */ @property (nonatomic, readonly) NSUInteger animatedImageMemorySize; /** Preload all frame image to memory. @discussion Set this property to `YES` will block the calling thread to decode all animation frame image to memory, set to `NO` will release the preloaded frames. If the image is shared by lots of image views (such as emoticon), preload all frames will reduce the CPU cost. See `animatedImageMemorySize` for memory cost. */ @property (nonatomic) BOOL preloadAllAnimatedImageFrames; @end NS_ASSUME_NONNULL_END ================================================ FILE: Demo/YYTextDemo/YYImage/YYImage.m ================================================ // // YYImage.m // YYImage // // Created by ibireme on 14/10/20. // 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 "YYImage.h" /** An array of NSNumber objects, shows the best order for path scale search. e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1] iPhone6 Plus:@[@3,@2,@1] */ static NSArray *_NSBundlePreferredScales() { static NSArray *scales; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ CGFloat screenScale = [UIScreen mainScreen].scale; if (screenScale <= 1) { scales = @[@1,@2,@3]; } else if (screenScale <= 2) { scales = @[@2,@3,@1]; } else { scales = @[@3,@2,@1]; } }); return scales; } /** Add scale modifier to the file name (without path extension), From @"name" to @"name@2x". e.g.
Before After(scale:2)
"icon" "icon@2x"
"icon " "icon @2x"
"icon.top" "icon.top@2x"
"/p/name" "/p/name@2x"
"/path/" "/path/"
@param scale Resource scale. @return String by add scale modifier, or just return if it's not end with file name. */ static NSString *_NSStringByAppendingNameScale(NSString *string, CGFloat scale) { if (!string) return nil; if (fabs(scale - 1) <= __FLT_EPSILON__ || string.length == 0 || [string hasSuffix:@"/"]) return string.copy; return [string stringByAppendingFormat:@"@%@x", @(scale)]; } /** Return the path scale. e.g.
Path Scale
"icon.png" 1
"icon@2x.png" 2
"icon@2.5x.png" 2.5
"icon@2x" 1
"icon@2x..png" 1
"icon@2x.png/" 1
*/ static CGFloat _NSStringPathScale(NSString *string) { if (string.length == 0 || [string hasSuffix:@"/"]) return 1; NSString *name = string.stringByDeletingPathExtension; __block CGFloat scale = 1; NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:@"@[0-9]+\\.?[0-9]*x$" options:NSRegularExpressionAnchorsMatchLines error:nil]; [pattern enumerateMatchesInString:name options:kNilOptions range:NSMakeRange(0, name.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { if (result.range.location >= 3) { scale = [string substringWithRange:NSMakeRange(result.range.location + 1, result.range.length - 2)].doubleValue; } }]; return scale; } @implementation YYImage { YYImageDecoder *_decoder; NSArray *_preloadedFrames; dispatch_semaphore_t _preloadedLock; NSUInteger _bytesPerFrame; } + (YYImage *)imageNamed:(NSString *)name { if (name.length == 0) return nil; if ([name hasSuffix:@"/"]) return nil; NSString *res = name.stringByDeletingPathExtension; NSString *ext = name.pathExtension; NSString *path = nil; CGFloat scale = 1; // If no extension, guess by system supported (same as UIImage). NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"]; NSArray *scales = _NSBundlePreferredScales(); for (int s = 0; s < scales.count; s++) { scale = ((NSNumber *)scales[s]).floatValue; NSString *scaledName = _NSStringByAppendingNameScale(res, scale); for (NSString *e in exts) { path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e]; if (path) break; } if (path) break; } if (path.length == 0) return nil; NSData *data = [NSData dataWithContentsOfFile:path]; if (data.length == 0) return nil; return [[self alloc] initWithData:data scale:scale]; } + (YYImage *)imageWithContentsOfFile:(NSString *)path { return [[self alloc] initWithContentsOfFile:path]; } + (YYImage *)imageWithData:(NSData *)data { return [[self alloc] initWithData:data]; } + (YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale { return [[self alloc] initWithData:data scale:scale]; } - (instancetype)initWithContentsOfFile:(NSString *)path { NSData *data = [NSData dataWithContentsOfFile:path]; return [self initWithData:data scale:_NSStringPathScale(path)]; } - (instancetype)initWithData:(NSData *)data { return [self initWithData:data scale:1]; } - (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale { if (data.length == 0) return nil; if (scale <= 0) scale = [UIScreen mainScreen].scale; _preloadedLock = dispatch_semaphore_create(1); @autoreleasepool { YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale]; YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES]; UIImage *image = frame.image; if (!image) return nil; self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation]; if (!self) return nil; _animatedImageType = decoder.type; if (decoder.frameCount > 1) { _decoder = decoder; _bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage); _animatedImageMemorySize = _bytesPerFrame * decoder.frameCount; } self.yy_isDecodedForDisplay = YES; } return self; } - (NSData *)animatedImageData { return _decoder.data; } - (void)setPreloadAllAnimatedImageFrames:(BOOL)preloadAllAnimatedImageFrames { if (_preloadAllAnimatedImageFrames != preloadAllAnimatedImageFrames) { if (preloadAllAnimatedImageFrames && _decoder.frameCount > 0) { NSMutableArray *frames = [NSMutableArray new]; for (NSUInteger i = 0, max = _decoder.frameCount; i < max; i++) { UIImage *img = [self animatedImageFrameAtIndex:i]; if (img) { [frames addObject:img]; } else { [frames addObject:[NSNull null]]; } } dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER); _preloadedFrames = frames; dispatch_semaphore_signal(_preloadedLock); } else { dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER); _preloadedFrames = nil; dispatch_semaphore_signal(_preloadedLock); } } } #pragma mark - protocol NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { NSNumber *scale = [aDecoder decodeObjectForKey:@"YYImageScale"]; NSData *data = [aDecoder decodeObjectForKey:@"YYImageData"]; if (data.length) { self = [self initWithData:data scale:scale.doubleValue]; } else { self = [super initWithCoder:aDecoder]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { if (_decoder.data.length) { [aCoder encodeObject:@(self.scale) forKey:@"YYImageScale"]; [aCoder encodeObject:_decoder.data forKey:@"YYImageData"]; } else { [super encodeWithCoder:aCoder]; // Apple use UIImagePNGRepresentation() to encode UIImage. } } #pragma mark - protocol YYAnimatedImage - (NSUInteger)animatedImageFrameCount { return _decoder.frameCount; } - (NSUInteger)animatedImageLoopCount { return _decoder.loopCount; } - (NSUInteger)animatedImageBytesPerFrame { return _bytesPerFrame; } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { if (index >= _decoder.frameCount) return nil; dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER); UIImage *image = _preloadedFrames[index]; dispatch_semaphore_signal(_preloadedLock); if (image) return image == (id)[NSNull null] ? nil : image; return [_decoder frameAtIndex:index decodeForDisplay:YES].image; } - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index { NSTimeInterval duration = [_decoder frameDurationAtIndex:index]; /* http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp Many annoying ads specify a 0 duration to make an image flash as quickly as possible. We follow Safari and Firefox's behavior and use a duration of 100 ms for any frames that specify a duration of <= 10 ms. See and for more information. See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser. */ if (duration < 0.011f) return 0.100f; return duration; } @end ================================================ FILE: Demo/YYTextDemo/YYImage/YYImageCoder.h ================================================ // // YYImageCoder.h // YYImage // // Created by ibireme on 15/5/13. // 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 /** Image file type. */ typedef NS_ENUM(NSUInteger, YYImageType) { YYImageTypeUnknown = 0, ///< unknown YYImageTypeJPEG, ///< jpeg, jpg YYImageTypeJPEG2000, ///< jp2 YYImageTypeTIFF, ///< tiff, tif YYImageTypeBMP, ///< bmp YYImageTypeICO, ///< ico YYImageTypeICNS, ///< icns YYImageTypeGIF, ///< gif YYImageTypePNG, ///< png YYImageTypeWebP, ///< webp YYImageTypeOther, ///< other image format }; /** Dispose method specifies how the area used by the current frame is to be treated before rendering the next frame on the canvas. */ typedef NS_ENUM(NSUInteger, YYImageDisposeMethod) { /** No disposal is done on this frame before rendering the next; the contents of the canvas are left as is. */ YYImageDisposeNone = 0, /** The frame's region of the canvas is to be cleared to fully transparent black before rendering the next frame. */ YYImageDisposeBackground, /** The frame's region of the canvas is to be reverted to the previous contents before rendering the next frame. */ YYImageDisposePrevious, }; /** Blend operation specifies how transparent pixels of the current frame are blended with those of the previous canvas. */ typedef NS_ENUM(NSUInteger, YYImageBlendOperation) { /** All color components of the frame, including alpha, overwrite the current contents of the frame's canvas region. */ YYImageBlendNone = 0, /** The frame should be composited onto the output buffer based on its alpha. */ YYImageBlendOver, }; /** An image frame object. */ @interface YYImageFrame : NSObject @property (nonatomic) NSUInteger index; ///< Frame index (zero based) @property (nonatomic) NSUInteger width; ///< Frame width @property (nonatomic) NSUInteger height; ///< Frame height @property (nonatomic) NSUInteger offsetX; ///< Frame origin.x in canvas (left-bottom based) @property (nonatomic) NSUInteger offsetY; ///< Frame origin.y in canvas (left-bottom based) @property (nonatomic) NSTimeInterval duration; ///< Frame duration in seconds @property (nonatomic) YYImageDisposeMethod dispose; ///< Frame dispose method. @property (nonatomic) YYImageBlendOperation blend; ///< Frame blend operation. @property (nullable, nonatomic, strong) UIImage *image; ///< The image. + (instancetype)frameWithImage:(UIImage *)image; @end #pragma mark - Decoder /** An image decoder to decode image data. @discussion This class supports decoding animated WebP, APNG, GIF and system image format such as PNG, JPG, JP2, BMP, TIFF, PIC, ICNS and ICO. It can be used to decode complete image data, or to decode incremental image data during image download. This class is thread-safe. Example: // Decode single image: NSData *data = [NSData dataWithContentOfFile:@"/tmp/image.webp"]; YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0]; UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image; // Decode image during download: NSMutableData *data = [NSMutableData new]; YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0]; while(newDataArrived) { [data appendData:newData]; [decoder updateData:data final:NO]; if (decoder.frameCount > 0) { UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image; // progressive display... } } [decoder updateData:data final:YES]; UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image; // final display... */ @interface YYImageDecoder : NSObject @property (nullable, nonatomic, readonly) NSData *data; ///< Image data. @property (nonatomic, readonly) YYImageType type; ///< Image data type. @property (nonatomic, readonly) CGFloat scale; ///< Image scale. @property (nonatomic, readonly) NSUInteger frameCount; ///< Image frame count. @property (nonatomic, readonly) NSUInteger loopCount; ///< Image loop count, 0 means infinite. @property (nonatomic, readonly) NSUInteger width; ///< Image canvas width. @property (nonatomic, readonly) NSUInteger height; ///< Image canvas height. @property (nonatomic, readonly, getter=isFinalized) BOOL finalized; /** Creates an image decoder. @param scale Image's scale. @return An image decoder. */ - (instancetype)initWithScale:(CGFloat)scale NS_DESIGNATED_INITIALIZER; /** Updates the incremental image with new data. @discussion You can use this method to decode progressive/interlaced/baseline image when you do not have the complete image data. The `data` was retained by decoder, you should not modify the data in other thread during decoding. @param data The data to add to the image decoder. Each time you call this function, the 'data' parameter must contain all of the image file data accumulated so far. @param final A value that specifies whether the data is the final set. Pass YES if it is, NO otherwise. When the data is already finalized, you can not update the data anymore. @return Whether succeed. */ - (BOOL)updateData:(nullable NSData *)data final:(BOOL)final; /** Convenience method to create a decoder with specified data. @param data Image data. @param scale Image's scale. @return A new decoder, or nil if an error occurs. */ + (nullable instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale; /** Decodes and returns a frame from a specified index. @param index Frame image index (zero-based). @param decodeForDisplay Whether decode the image to memory bitmap for display. If NO, it will try to returns the original frame data without blend. @return A new frame with image, or nil if an error occurs. */ - (nullable YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay; /** Returns the frame duration from a specified index. @param index Frame image (zero-based). @return Duration in seconds. */ - (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index; /** Returns the frame's properties. See "CGImageProperties.h" in ImageIO.framework for more information. @param index Frame image index (zero-based). @return The ImageIO frame property. */ - (nullable NSDictionary *)framePropertiesAtIndex:(NSUInteger)index; /** Returns the image's properties. See "CGImageProperties.h" in ImageIO.framework for more information. */ - (nullable NSDictionary *)imageProperties; @end #pragma mark - Encoder /** An image encoder to encode image to data. @discussion It supports encoding single frame image with the type defined in YYImageType. It also supports encoding multi-frame image with GIF, APNG and WebP. Example: YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG]; jpegEncoder.quality = 0.9; [jpegEncoder addImage:image duration:0]; NSData jpegData = [jpegEncoder encode]; YYImageEncoder *gifEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeGIF]; gifEncoder.loopCount = 5; [gifEncoder addImage:image0 duration:0.1]; [gifEncoder addImage:image1 duration:0.15]; [gifEncoder addImage:image2 duration:0.2]; NSData gifData = [gifEncoder encode]; @warning It just pack the images together when encoding multi-frame image. If you want to reduce the image file size, try imagemagick/ffmpeg for GIF and WebP, and apngasm for APNG. */ @interface YYImageEncoder : NSObject @property (nonatomic, readonly) YYImageType type; ///< Image type. @property (nonatomic) NSUInteger loopCount; ///< Loop count, 0 means infinit, only available for GIF/APNG/WebP. @property (nonatomic) BOOL lossless; ///< Lossless, only available for WebP. @property (nonatomic) CGFloat quality; ///< Compress quality, 0.0~1.0, only available for JPG/JP2/WebP. - (instancetype)init UNAVAILABLE_ATTRIBUTE; + (instancetype)new UNAVAILABLE_ATTRIBUTE; /** Create an image encoder with a specified type. @param type Image type. @return A new encoder, or nil if an error occurs. */ - (nullable instancetype)initWithType:(YYImageType)type NS_DESIGNATED_INITIALIZER; /** Add an image to encoder. @param image Image. @param duration Image duration for animation. Pass 0 to ignore this parameter. */ - (void)addImage:(UIImage *)image duration:(NSTimeInterval)duration; /** Add an image with image data to encoder. @param data Image data. @param duration Image duration for animation. Pass 0 to ignore this parameter. */ - (void)addImageWithData:(NSData *)data duration:(NSTimeInterval)duration; /** Add an image from a file path to encoder. @param image Image file path. @param duration Image duration for animation. Pass 0 to ignore this parameter. */ - (void)addImageWithFile:(NSString *)path duration:(NSTimeInterval)duration; /** Encodes the image and returns the image data. @return The image data, or nil if an error occurs. */ - (nullable NSData *)encode; /** Encodes the image to a file. @param path The file path (overwrite if exist). @return Whether succeed. */ - (BOOL)encodeToFile:(NSString *)path; /** Convenience method to encode single frame image. @param image The image. @param type The destination image type. @param quality Image quality, 0.0~1.0. @return The image data, or nil if an error occurs. */ + (nullable NSData *)encodeImage:(UIImage *)image type:(YYImageType)type quality:(CGFloat)quality; /** Convenience method to encode image from a decoder. @param decoder The image decoder. @param type The destination image type; @param quality Image quality, 0.0~1.0. @return The image data, or nil if an error occurs. */ + (nullable NSData *)encodeImageWithDecoder:(YYImageDecoder *)decoder type:(YYImageType)type quality:(CGFloat)quality; @end #pragma mark - UIImage @interface UIImage (YYImageCoder) /** Decompress this image to bitmap, so when the image is displayed on screen, the main thread won't be blocked by additional decode. If the image has already been decoded or unable to decode, it just returns itself. @return an image decoded, or just return itself if no needed. @see yy_isDecodedForDisplay */ - (instancetype)yy_imageByDecoded; /** Wherher the image can be display on screen without additional decoding. @warning It just a hint for your code, change it has no other effect. */ @property (nonatomic) BOOL yy_isDecodedForDisplay; /** Saves this image to iOS Photos Album. @discussion This method attempts to save the original data to album if the image is created from an animated GIF/APNG, otherwise, it will save the image as JPEG or PNG (based on the alpha information). @param completionBlock The block invoked (in main thread) after the save operation completes. assetURL: An URL that identifies the saved image file. If the image is not saved, assetURL is nil. error: If the image is not saved, an error object that describes the reason for failure, otherwise nil. */ - (void)yy_saveToAlbumWithCompletionBlock:(nullable void(^)(NSURL * _Nullable assetURL, NSError * _Nullable error))completionBlock; /** Return a 'best' data representation for this image. @discussion The convertion based on these rule: 1. If the image is created from an animated GIF/APNG/WebP, it returns the original data. 2. It returns PNG or JPEG(0.9) representation based on the alpha information. @return Image data, or nil if an error occurs. */ - (nullable NSData *)yy_imageDataRepresentation; @end #pragma mark - Helper /// Detect a data's image type by reading the data's header 16 bytes (very fast). CG_EXTERN YYImageType YYImageDetectType(CFDataRef data); /// Convert YYImageType to UTI (such as kUTTypeJPEG). CG_EXTERN CFStringRef _Nullable YYImageTypeToUTType(YYImageType type); /// Convert UTI (such as kUTTypeJPEG) to YYImageType. CG_EXTERN YYImageType YYImageTypeFromUTType(CFStringRef uti); /// Get image type's file extension (such as @"jpg"). CG_EXTERN NSString *_Nullable YYImageTypeGetExtension(YYImageType type); /// Returns the shared DeviceRGB color space. CG_EXTERN CGColorSpaceRef YYCGColorSpaceGetDeviceRGB(); /// Returns the shared DeviceGray color space. CG_EXTERN CGColorSpaceRef YYCGColorSpaceGetDeviceGray(); /// Returns whether a color space is DeviceRGB. CG_EXTERN BOOL YYCGColorSpaceIsDeviceRGB(CGColorSpaceRef space); /// Returns whether a color space is DeviceGray. CG_EXTERN BOOL YYCGColorSpaceIsDeviceGray(CGColorSpaceRef space); /// Convert EXIF orientation value to UIImageOrientation. CG_EXTERN UIImageOrientation YYUIImageOrientationFromEXIFValue(NSInteger value); /// Convert UIImageOrientation to EXIF orientation value. CG_EXTERN NSInteger YYUIImageOrientationToEXIFValue(UIImageOrientation orientation); /** Create a decoded image. @discussion If the source image is created from a compressed image data (such as PNG or JPEG), you can use this method to decode the image. After decoded, you can access the decoded bytes with CGImageGetDataProvider() and CGDataProviderCopyData() without additional decode process. If the image has already decoded, this method just copy the decoded bytes to the new image. @param imageRef The source image. @param decodeForDisplay If YES, this method will decode the image and convert it to BGRA8888 (premultiplied) or BGRX8888 format for CALayer display. @return A decoded image, or NULL if an error occurs. */ CG_EXTERN CGImageRef _Nullable YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay); /** Create an image copy with an orientation. @param imageRef Source image @param orientation Image orientation which will applied to the image. @param destBitmapInfo Destimation image bitmap, only support 32bit format (such as ARGB8888). @return A new image, or NULL if an error occurs. */ CG_EXTERN CGImageRef _Nullable YYCGImageCreateCopyWithOrientation(CGImageRef imageRef, UIImageOrientation orientation, CGBitmapInfo destBitmapInfo); /** Create an image copy with CGAffineTransform. @param imageRef Source image. @param transform Transform applied to image (left-bottom based coordinate system). @param destSize Destination image size @param destBitmapInfo Destimation image bitmap, only support 32bit format (such as ARGB8888). @return A new image, or NULL if an error occurs. */ CG_EXTERN CGImageRef _Nullable YYCGImageCreateAffineTransformCopy(CGImageRef imageRef, CGAffineTransform transform, CGSize destSize, CGBitmapInfo destBitmapInfo); /** Encode an image to data with CGImageDestination. @param imageRef The image. @param type The image destination data type. @param quality The quality (0.0~1.0) @return A new image data, or nil if an error occurs. */ CG_EXTERN CFDataRef _Nullable YYCGImageCreateEncodedData(CGImageRef imageRef, YYImageType type, CGFloat quality); /** Whether WebP is available in YYImage. */ CG_EXTERN BOOL YYImageWebPAvailable(); /** Get a webp image frame count; @param webpData WebP data. @return Image frame count, or 0 if an error occurs. */ CG_EXTERN NSUInteger YYImageGetWebPFrameCount(CFDataRef webpData); /** Decode an image from WebP data, returns NULL if an error occurs. @param webpData The WebP data. @param decodeForDisplay If YES, this method will decode the image and convert it to BGRA8888 (premultiplied) format for CALayer display. @param useThreads YES to enable multi-thread decode. (speed up, but cost more CPU) @param bypassFiltering YES to skip the in-loop filtering. (speed up, but may lose some smooth) @param noFancyUpsampling YES to use faster pointwise upsampler. (speed down, and may lose some details). @return The decoded image, or NULL if an error occurs. */ CG_EXTERN CGImageRef _Nullable YYCGImageCreateWithWebPData(CFDataRef webpData, BOOL decodeForDisplay, BOOL useThreads, BOOL bypassFiltering, BOOL noFancyUpsampling); typedef NS_ENUM(NSUInteger, YYImagePreset) { YYImagePresetDefault = 0, ///< default preset. YYImagePresetPicture, ///< digital picture, like portrait, inner shot YYImagePresetPhoto, ///< outdoor photograph, with natural lighting YYImagePresetDrawing, ///< hand or line drawing, with high-contrast details YYImagePresetIcon, ///< small-sized colorful images YYImagePresetText ///< text-like }; /** Encode a CGImage to WebP data @param imageRef image @param lossless YES=lossless (similar to PNG), NO=lossy (similar to JPEG) @param quality 0.0~1.0 (0=smallest file, 1.0=biggest file) For lossless image, try the value near 1.0; for lossy, try the value near 0.8. @param compressLevel 0~6 (0=fast, 6=slower-better). Default is 4. @param preset Preset for different image type, default is YYImagePresetDefault. @return WebP data, or nil if an error occurs. */ CG_EXTERN CFDataRef _Nullable YYCGImageCreateEncodedWebPData(CGImageRef imageRef, BOOL lossless, CGFloat quality, int compressLevel, YYImagePreset preset); NS_ASSUME_NONNULL_END ================================================ FILE: Demo/YYTextDemo/YYImage/YYImageCoder.m ================================================ // // YYImageCoder.m // YYImage // // Created by ibireme on 15/5/13. // 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 "YYImageCoder.h" #import "YYImage.h" #import #import #import #import #import #import #import #import #import #ifndef YYIMAGE_WEBP_ENABLED #if __has_include() && __has_include() && \ __has_include() && __has_include() #define YYIMAGE_WEBP_ENABLED 1 #import #import #import #import #elif __has_include("webp/decode.h") && __has_include("webp/encode.h") && \ __has_include("webp/demux.h") && __has_include("webp/mux.h") #define YYIMAGE_WEBP_ENABLED 1 #import "webp/decode.h" #import "webp/encode.h" #import "webp/demux.h" #import "webp/mux.h" #else #define YYIMAGE_WEBP_ENABLED 0 #endif #endif //////////////////////////////////////////////////////////////////////////////// #pragma mark - Utility (for little endian platform) #define YY_FOUR_CC(c1,c2,c3,c4) ((uint32_t)(((c4) << 24) | ((c3) << 16) | ((c2) << 8) | (c1))) #define YY_TWO_CC(c1,c2) ((uint16_t)(((c2) << 8) | (c1))) static inline uint16_t yy_swap_endian_uint16(uint16_t value) { return (uint16_t) ((value & 0x00FF) << 8) | (uint16_t) ((value & 0xFF00) >> 8) ; } static inline uint32_t yy_swap_endian_uint32(uint32_t value) { return (uint32_t)((value & 0x000000FFU) << 24) | (uint32_t)((value & 0x0000FF00U) << 8) | (uint32_t)((value & 0x00FF0000U) >> 8) | (uint32_t)((value & 0xFF000000U) >> 24) ; } //////////////////////////////////////////////////////////////////////////////// #pragma mark - APNG /* PNG spec: http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html APNG spec: https://wiki.mozilla.org/APNG_Specification =============================================================================== PNG format: header (8): 89 50 4e 47 0d 0a 1a 0a chunk, chunk, chunk, ... =============================================================================== chunk format: length (4): uint32_t big endian fourcc (4): chunk type code data (length): data crc32 (4): uint32_t big endian crc32(fourcc + data) =============================================================================== PNG chunk define: IHDR (Image Header) required, must appear first, 13 bytes width (4) pixel count, should not be zero height (4) pixel count, should not be zero bit depth (1) expected: 1, 2, 4, 8, 16 color type (1) 1<<0 (palette used), 1<<1 (color used), 1<<2 (alpha channel used) compression method (1) 0 (deflate/inflate) filter method (1) 0 (adaptive filtering with five basic filter types) interlace method (1) 0 (no interlace) or 1 (Adam7 interlace) IDAT (Image Data) required, must appear consecutively if there's multiple 'IDAT' chunk IEND (End) required, must appear last, 0 bytes =============================================================================== APNG chunk define: acTL (Animation Control) required, must appear before 'IDAT', 8 bytes num frames (4) number of frames num plays (4) number of times to loop, 0 indicates infinite looping fcTL (Frame Control) required, must appear before the 'IDAT' or 'fdAT' chunks of the frame to which it applies, 26 bytes sequence number (4) sequence number of the animation chunk, starting from 0 width (4) width of the following frame height (4) height of the following frame x offset (4) x position at which to render the following frame y offset (4) y position at which to render the following frame delay num (2) frame delay fraction numerator delay den (2) frame delay fraction denominator dispose op (1) type of frame area disposal to be done after rendering this frame (0:none, 1:background 2:previous) blend op (1) type of frame area rendering for this frame (0:source, 1:over) fdAT (Frame Data) required sequence number (4) sequence number of the animation chunk frame data (x) frame data for this frame (same as 'IDAT') =============================================================================== `dispose_op` specifies how the output buffer should be changed at the end of the delay (before rendering the next frame). * NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. * BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. * PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. `blend_op` specifies whether the frame is to be alpha blended into the current output buffer content, or whether it should completely replace its region in the output buffer. * SOURCE: all color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. * OVER: the frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification */ typedef enum { YY_PNG_ALPHA_TYPE_PALEETE = 1 << 0, YY_PNG_ALPHA_TYPE_COLOR = 1 << 1, YY_PNG_ALPHA_TYPE_ALPHA = 1 << 2, } yy_png_alpha_type; typedef enum { YY_PNG_DISPOSE_OP_NONE = 0, YY_PNG_DISPOSE_OP_BACKGROUND = 1, YY_PNG_DISPOSE_OP_PREVIOUS = 2, } yy_png_dispose_op; typedef enum { YY_PNG_BLEND_OP_SOURCE = 0, YY_PNG_BLEND_OP_OVER = 1, } yy_png_blend_op; typedef struct { uint32_t width; ///< pixel count, should not be zero uint32_t height; ///< pixel count, should not be zero uint8_t bit_depth; ///< expected: 1, 2, 4, 8, 16 uint8_t color_type; ///< see yy_png_alpha_type uint8_t compression_method; ///< 0 (deflate/inflate) uint8_t filter_method; ///< 0 (adaptive filtering with five basic filter types) uint8_t interlace_method; ///< 0 (no interlace) or 1 (Adam7 interlace) } yy_png_chunk_IHDR; typedef struct { uint32_t sequence_number; ///< sequence number of the animation chunk, starting from 0 uint32_t width; ///< width of the following frame uint32_t height; ///< height of the following frame uint32_t x_offset; ///< x position at which to render the following frame uint32_t y_offset; ///< y position at which to render the following frame uint16_t delay_num; ///< frame delay fraction numerator uint16_t delay_den; ///< frame delay fraction denominator uint8_t dispose_op; ///< see yy_png_dispose_op uint8_t blend_op; ///< see yy_png_blend_op } yy_png_chunk_fcTL; typedef struct { uint32_t offset; ///< chunk offset in PNG data uint32_t fourcc; ///< chunk fourcc uint32_t length; ///< chunk data length uint32_t crc32; ///< chunk crc32 } yy_png_chunk_info; typedef struct { uint32_t chunk_index; ///< the first `fdAT`/`IDAT` chunk index uint32_t chunk_num; ///< the `fdAT`/`IDAT` chunk count uint32_t chunk_size; ///< the `fdAT`/`IDAT` chunk bytes yy_png_chunk_fcTL frame_control; } yy_png_frame_info; typedef struct { yy_png_chunk_IHDR header; ///< png header yy_png_chunk_info *chunks; ///< chunks uint32_t chunk_num; ///< count of chunks yy_png_frame_info *apng_frames; ///< frame info, NULL if not apng uint32_t apng_frame_num; ///< 0 if not apng uint32_t apng_loop_num; ///< 0 indicates infinite looping uint32_t *apng_shared_chunk_indexs; ///< shared chunk index uint32_t apng_shared_chunk_num; ///< shared chunk count uint32_t apng_shared_chunk_size; ///< shared chunk bytes uint32_t apng_shared_insert_index; ///< shared chunk insert index bool apng_first_frame_is_cover; ///< the first frame is same as png (cover) } yy_png_info; static void yy_png_chunk_IHDR_read(yy_png_chunk_IHDR *IHDR, const uint8_t *data) { IHDR->width = yy_swap_endian_uint32(*((uint32_t *)(data))); IHDR->height = yy_swap_endian_uint32(*((uint32_t *)(data + 4))); IHDR->bit_depth = data[8]; IHDR->color_type = data[9]; IHDR->compression_method = data[10]; IHDR->filter_method = data[11]; IHDR->interlace_method = data[12]; } static void yy_png_chunk_IHDR_write(yy_png_chunk_IHDR *IHDR, uint8_t *data) { *((uint32_t *)(data)) = yy_swap_endian_uint32(IHDR->width); *((uint32_t *)(data + 4)) = yy_swap_endian_uint32(IHDR->height); data[8] = IHDR->bit_depth; data[9] = IHDR->color_type; data[10] = IHDR->compression_method; data[11] = IHDR->filter_method; data[12] = IHDR->interlace_method; } static void yy_png_chunk_fcTL_read(yy_png_chunk_fcTL *fcTL, const uint8_t *data) { fcTL->sequence_number = yy_swap_endian_uint32(*((uint32_t *)(data))); fcTL->width = yy_swap_endian_uint32(*((uint32_t *)(data + 4))); fcTL->height = yy_swap_endian_uint32(*((uint32_t *)(data + 8))); fcTL->x_offset = yy_swap_endian_uint32(*((uint32_t *)(data + 12))); fcTL->y_offset = yy_swap_endian_uint32(*((uint32_t *)(data + 16))); fcTL->delay_num = yy_swap_endian_uint16(*((uint16_t *)(data + 20))); fcTL->delay_den = yy_swap_endian_uint16(*((uint16_t *)(data + 22))); fcTL->dispose_op = data[24]; fcTL->blend_op = data[25]; } static void yy_png_chunk_fcTL_write(yy_png_chunk_fcTL *fcTL, uint8_t *data) { *((uint32_t *)(data)) = yy_swap_endian_uint32(fcTL->sequence_number); *((uint32_t *)(data + 4)) = yy_swap_endian_uint32(fcTL->width); *((uint32_t *)(data + 8)) = yy_swap_endian_uint32(fcTL->height); *((uint32_t *)(data + 12)) = yy_swap_endian_uint32(fcTL->x_offset); *((uint32_t *)(data + 16)) = yy_swap_endian_uint32(fcTL->y_offset); *((uint16_t *)(data + 20)) = yy_swap_endian_uint16(fcTL->delay_num); *((uint16_t *)(data + 22)) = yy_swap_endian_uint16(fcTL->delay_den); data[24] = fcTL->dispose_op; data[25] = fcTL->blend_op; } // convert double value to fraction static void yy_png_delay_to_fraction(double duration, uint16_t *num, uint16_t *den) { if (duration >= 0xFF) { *num = 0xFF; *den = 1; } else if (duration <= 1.0 / (double)0xFF) { *num = 1; *den = 0xFF; } else { // Use continued fraction to calculate the num and den. long MAX = 10; double eps = (0.5 / (double)0xFF); long p[MAX], q[MAX], a[MAX], i, numl = 0, denl = 0; // The first two convergents are 0/1 and 1/0 p[0] = 0; q[0] = 1; p[1] = 1; q[1] = 0; // The rest of the convergents (and continued fraction) for (i = 2; i < MAX; i++) { a[i] = lrint(floor(duration)); p[i] = a[i] * p[i - 1] + p[i - 2]; q[i] = a[i] * q[i - 1] + q[i - 2]; if (p[i] <= 0xFF && q[i] <= 0xFF) { // uint16_t numl = p[i]; denl = q[i]; } else break; if (fabs(duration - a[i]) < eps) break; duration = 1.0 / (duration - a[i]); } if (numl != 0 && denl != 0) { *num = numl; *den = denl; } else { *num = 1; *den = 100; } } } // convert fraction to double value static double yy_png_delay_to_seconds(uint16_t num, uint16_t den) { if (den == 0) { return num / 100.0; } else { return (double)num / (double)den; } } static bool yy_png_validate_animation_chunk_order(yy_png_chunk_info *chunks, /* input */ uint32_t chunk_num, /* input */ uint32_t *first_idat_index, /* output */ bool *first_frame_is_cover /* output */) { /* PNG at least contains 3 chunks: IHDR, IDAT, IEND. `IHDR` must appear first. `IDAT` must appear consecutively. `IEND` must appear end. APNG must contains one `acTL` and at least one 'fcTL' and `fdAT`. `fdAT` must appear consecutively. `fcTL` must appear before `IDAT` or `fdAT`. */ if (chunk_num <= 2) return false; if (chunks->fourcc != YY_FOUR_CC('I', 'H', 'D', 'R')) return false; if ((chunks + chunk_num - 1)->fourcc != YY_FOUR_CC('I', 'E', 'N', 'D')) return false; uint32_t prev_fourcc = 0; uint32_t IHDR_num = 0; uint32_t IDAT_num = 0; uint32_t acTL_num = 0; uint32_t fcTL_num = 0; uint32_t first_IDAT = 0; bool first_frame_cover = false; for (uint32_t i = 0; i < chunk_num; i++) { yy_png_chunk_info *chunk = chunks + i; switch (chunk->fourcc) { case YY_FOUR_CC('I', 'H', 'D', 'R'): { // png header if (i != 0) return false; if (IHDR_num > 0) return false; IHDR_num++; } break; case YY_FOUR_CC('I', 'D', 'A', 'T'): { // png data if (prev_fourcc != YY_FOUR_CC('I', 'D', 'A', 'T')) { if (IDAT_num == 0) first_IDAT = i; else return false; } IDAT_num++; } break; case YY_FOUR_CC('a', 'c', 'T', 'L'): { // apng control if (acTL_num > 0) return false; acTL_num++; } break; case YY_FOUR_CC('f', 'c', 'T', 'L'): { // apng frame control if (i + 1 == chunk_num) return false; if ((chunk + 1)->fourcc != YY_FOUR_CC('f', 'd', 'A', 'T') && (chunk + 1)->fourcc != YY_FOUR_CC('I', 'D', 'A', 'T')) { return false; } if (fcTL_num == 0) { if ((chunk + 1)->fourcc == YY_FOUR_CC('I', 'D', 'A', 'T')) { first_frame_cover = true; } } fcTL_num++; } break; case YY_FOUR_CC('f', 'd', 'A', 'T'): { // apng data if (prev_fourcc != YY_FOUR_CC('f', 'd', 'A', 'T') && prev_fourcc != YY_FOUR_CC('f', 'c', 'T', 'L')) { return false; } } break; } prev_fourcc = chunk->fourcc; } if (IHDR_num != 1) return false; if (IDAT_num == 0) return false; if (acTL_num != 1) return false; if (fcTL_num < acTL_num) return false; *first_idat_index = first_IDAT; *first_frame_is_cover = first_frame_cover; return true; } static void yy_png_info_release(yy_png_info *info) { if (info) { if (info->chunks) free(info->chunks); if (info->apng_frames) free(info->apng_frames); if (info->apng_shared_chunk_indexs) free(info->apng_shared_chunk_indexs); free(info); } } /** Create a png info from a png file. See struct png_info for more information. @param data png/apng file data. @param length the data's length in bytes. @return A png info object, you may call yy_png_info_release() to release it. Returns NULL if an error occurs. */ static yy_png_info *yy_png_info_create(const uint8_t *data, uint32_t length) { if (length < 32) return NULL; if (*((uint32_t *)data) != YY_FOUR_CC(0x89, 0x50, 0x4E, 0x47)) return NULL; if (*((uint32_t *)(data + 4)) != YY_FOUR_CC(0x0D, 0x0A, 0x1A, 0x0A)) return NULL; uint32_t chunk_realloc_num = 16; yy_png_chunk_info *chunks = malloc(sizeof(yy_png_chunk_info) * chunk_realloc_num); if (!chunks) return NULL; // parse png chunks uint32_t offset = 8; uint32_t chunk_num = 0; uint32_t chunk_capacity = chunk_realloc_num; uint32_t apng_loop_num = 0; int32_t apng_sequence_index = -1; int32_t apng_frame_index = 0; int32_t apng_frame_number = -1; bool apng_chunk_error = false; do { if (chunk_num >= chunk_capacity) { yy_png_chunk_info *new_chunks = realloc(chunks, sizeof(yy_png_chunk_info) * (chunk_capacity + chunk_realloc_num)); if (!new_chunks) { free(chunks); return NULL; } chunks = new_chunks; chunk_capacity += chunk_realloc_num; } yy_png_chunk_info *chunk = chunks + chunk_num; const uint8_t *chunk_data = data + offset; chunk->offset = offset; chunk->length = yy_swap_endian_uint32(*((uint32_t *)chunk_data)); if ((uint64_t)chunk->offset + (uint64_t)chunk->length + 12 > length) { free(chunks); return NULL; } chunk->fourcc = *((uint32_t *)(chunk_data + 4)); if ((uint64_t)chunk->offset + 4 + chunk->length + 4 > (uint64_t)length) break; chunk->crc32 = yy_swap_endian_uint32(*((uint32_t *)(chunk_data + 8 + chunk->length))); chunk_num++; offset += 12 + chunk->length; switch (chunk->fourcc) { case YY_FOUR_CC('a', 'c', 'T', 'L') : { if (chunk->length == 8) { apng_frame_number = yy_swap_endian_uint32(*((uint32_t *)(chunk_data + 8))); apng_loop_num = yy_swap_endian_uint32(*((uint32_t *)(chunk_data + 12))); } else { apng_chunk_error = true; } } break; case YY_FOUR_CC('f', 'c', 'T', 'L') : case YY_FOUR_CC('f', 'd', 'A', 'T') : { if (chunk->fourcc == YY_FOUR_CC('f', 'c', 'T', 'L')) { if (chunk->length != 26) { apng_chunk_error = true; } else { apng_frame_index++; } } if (chunk->length > 4) { uint32_t sequence = yy_swap_endian_uint32(*((uint32_t *)(chunk_data + 8))); if (apng_sequence_index + 1 == sequence) { apng_sequence_index++; } else { apng_chunk_error = true; } } else { apng_chunk_error = true; } } break; case YY_FOUR_CC('I', 'E', 'N', 'D') : { offset = length; // end, break do-while loop } break; } } while (offset + 12 <= length); if (chunk_num < 3 || chunks->fourcc != YY_FOUR_CC('I', 'H', 'D', 'R') || chunks->length != 13) { free(chunks); return NULL; } // png info yy_png_info *info = calloc(1, sizeof(yy_png_info)); if (!info) { free(chunks); return NULL; } info->chunks = chunks; info->chunk_num = chunk_num; yy_png_chunk_IHDR_read(&info->header, data + chunks->offset + 8); // apng info if (!apng_chunk_error && apng_frame_number == apng_frame_index && apng_frame_number >= 1) { bool first_frame_is_cover = false; uint32_t first_IDAT_index = 0; if (!yy_png_validate_animation_chunk_order(info->chunks, info->chunk_num, &first_IDAT_index, &first_frame_is_cover)) { return info; // ignore apng chunk } info->apng_loop_num = apng_loop_num; info->apng_frame_num = apng_frame_number; info->apng_first_frame_is_cover = first_frame_is_cover; info->apng_shared_insert_index = first_IDAT_index; info->apng_frames = calloc(apng_frame_number, sizeof(yy_png_frame_info)); if (!info->apng_frames) { yy_png_info_release(info); return NULL; } info->apng_shared_chunk_indexs = calloc(info->chunk_num, sizeof(uint32_t)); if (!info->apng_shared_chunk_indexs) { yy_png_info_release(info); return NULL; } int32_t frame_index = -1; uint32_t *shared_chunk_index = info->apng_shared_chunk_indexs; for (int32_t i = 0; i < info->chunk_num; i++) { yy_png_chunk_info *chunk = info->chunks + i; switch (chunk->fourcc) { case YY_FOUR_CC('I', 'D', 'A', 'T'): { if (info->apng_shared_insert_index == 0) { info->apng_shared_insert_index = i; } if (first_frame_is_cover) { yy_png_frame_info *frame = info->apng_frames + frame_index; frame->chunk_num++; frame->chunk_size += chunk->length + 12; } } break; case YY_FOUR_CC('a', 'c', 'T', 'L'): { } break; case YY_FOUR_CC('f', 'c', 'T', 'L'): { frame_index++; yy_png_frame_info *frame = info->apng_frames + frame_index; frame->chunk_index = i + 1; yy_png_chunk_fcTL_read(&frame->frame_control, data + chunk->offset + 8); } break; case YY_FOUR_CC('f', 'd', 'A', 'T'): { yy_png_frame_info *frame = info->apng_frames + frame_index; frame->chunk_num++; frame->chunk_size += chunk->length + 12; } break; default: { *shared_chunk_index = i; shared_chunk_index++; info->apng_shared_chunk_size += chunk->length + 12; info->apng_shared_chunk_num++; } break; } } } return info; } /** Copy a png frame data from an apng file. @param data apng file data @param info png info @param index frame index (zero-based) @param size output, the size of the frame data @return A frame data (single-frame png file), call free() to release the data. Returns NULL if an error occurs. */ static uint8_t *yy_png_copy_frame_data_at_index(const uint8_t *data, const yy_png_info *info, const uint32_t index, uint32_t *size) { if (index >= info->apng_frame_num) return NULL; yy_png_frame_info *frame_info = info->apng_frames + index; uint32_t frame_remux_size = 8 /* PNG Header */ + info->apng_shared_chunk_size + frame_info->chunk_size; if (!(info->apng_first_frame_is_cover && index == 0)) { frame_remux_size -= frame_info->chunk_num * 4; // remove fdAT sequence number } uint8_t *frame_data = malloc(frame_remux_size); if (!frame_data) return NULL; *size = frame_remux_size; uint32_t data_offset = 0; bool inserted = false; memcpy(frame_data, data, 8); // PNG File Header data_offset += 8; for (uint32_t i = 0; i < info->apng_shared_chunk_num; i++) { uint32_t shared_chunk_index = info->apng_shared_chunk_indexs[i]; yy_png_chunk_info *shared_chunk_info = info->chunks + shared_chunk_index; if (shared_chunk_index >= info->apng_shared_insert_index && !inserted) { // replace IDAT with fdAT inserted = true; for (uint32_t c = 0; c < frame_info->chunk_num; c++) { yy_png_chunk_info *insert_chunk_info = info->chunks + frame_info->chunk_index + c; if (insert_chunk_info->fourcc == YY_FOUR_CC('f', 'd', 'A', 'T')) { *((uint32_t *)(frame_data + data_offset)) = yy_swap_endian_uint32(insert_chunk_info->length - 4); *((uint32_t *)(frame_data + data_offset + 4)) = YY_FOUR_CC('I', 'D', 'A', 'T'); memcpy(frame_data + data_offset + 8, data + insert_chunk_info->offset + 12, insert_chunk_info->length - 4); uint32_t crc = (uint32_t)crc32(0, frame_data + data_offset + 4, insert_chunk_info->length); *((uint32_t *)(frame_data + data_offset + insert_chunk_info->length + 4)) = yy_swap_endian_uint32(crc); data_offset += insert_chunk_info->length + 8; } else { // IDAT memcpy(frame_data + data_offset, data + insert_chunk_info->offset, insert_chunk_info->length + 12); data_offset += insert_chunk_info->length + 12; } } } if (shared_chunk_info->fourcc == YY_FOUR_CC('I', 'H', 'D', 'R')) { uint8_t tmp[25] = {0}; memcpy(tmp, data + shared_chunk_info->offset, 25); yy_png_chunk_IHDR IHDR = info->header; IHDR.width = frame_info->frame_control.width; IHDR.height = frame_info->frame_control.height; yy_png_chunk_IHDR_write(&IHDR, tmp + 8); *((uint32_t *)(tmp + 21)) = yy_swap_endian_uint32((uint32_t)crc32(0, tmp + 4, 17)); memcpy(frame_data + data_offset, tmp, 25); data_offset += 25; } else { memcpy(frame_data + data_offset, data + shared_chunk_info->offset, shared_chunk_info->length + 12); data_offset += shared_chunk_info->length + 12; } } return frame_data; } //////////////////////////////////////////////////////////////////////////////// #pragma mark - Helper /// Returns byte-aligned size. static inline size_t YYImageByteAlign(size_t size, size_t alignment) { return ((size + (alignment - 1)) / alignment) * alignment; } /// Convert degree to radians static inline CGFloat YYImageDegreesToRadians(CGFloat degrees) { return degrees * M_PI / 180; } CGColorSpaceRef YYCGColorSpaceGetDeviceRGB() { static CGColorSpaceRef space; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ space = CGColorSpaceCreateDeviceRGB(); }); return space; } CGColorSpaceRef YYCGColorSpaceGetDeviceGray() { static CGColorSpaceRef space; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ space = CGColorSpaceCreateDeviceGray(); }); return space; } BOOL YYCGColorSpaceIsDeviceRGB(CGColorSpaceRef space) { return space && CFEqual(space, YYCGColorSpaceGetDeviceRGB()); } BOOL YYCGColorSpaceIsDeviceGray(CGColorSpaceRef space) { return space && CFEqual(space, YYCGColorSpaceGetDeviceGray()); } /** A callback used in CGDataProviderCreateWithData() to release data. Example: void *data = malloc(size); CGDataProviderRef provider = CGDataProviderCreateWithData(data, data, size, YYCGDataProviderReleaseDataCallback); */ static void YYCGDataProviderReleaseDataCallback(void *info, const void *data, size_t size) { if (info) free(info); } /** Decode an image to bitmap buffer with the specified format. @param srcImage Source image. @param dest Destination buffer. It should be zero before call this method. If decode succeed, you should release the dest->data using free(). @param destFormat Destination bitmap format. @return Whether succeed. @warning This method support iOS7.0 and later. If call it on iOS6, it just returns NO. CG_AVAILABLE_STARTING(__MAC_10_9, __IPHONE_7_0) */ static BOOL YYCGImageDecodeToBitmapBufferWithAnyFormat(CGImageRef srcImage, vImage_Buffer *dest, vImage_CGImageFormat *destFormat) { if (!srcImage || (((long)vImageConvert_AnyToAny) + 1 == 1) || !destFormat || !dest) return NO; size_t width = CGImageGetWidth(srcImage); size_t height = CGImageGetHeight(srcImage); if (width == 0 || height == 0) return NO; dest->data = NULL; vImage_Error error = kvImageNoError; CFDataRef srcData = NULL; vImageConverterRef convertor = NULL; vImage_CGImageFormat srcFormat = {0}; srcFormat.bitsPerComponent = (uint32_t)CGImageGetBitsPerComponent(srcImage); srcFormat.bitsPerPixel = (uint32_t)CGImageGetBitsPerPixel(srcImage); srcFormat.colorSpace = CGImageGetColorSpace(srcImage); srcFormat.bitmapInfo = CGImageGetBitmapInfo(srcImage) | CGImageGetAlphaInfo(srcImage); convertor = vImageConverter_CreateWithCGImageFormat(&srcFormat, destFormat, NULL, kvImageNoFlags, NULL); if (!convertor) goto fail; CGDataProviderRef srcProvider = CGImageGetDataProvider(srcImage); srcData = srcProvider ? CGDataProviderCopyData(srcProvider) : NULL; // decode size_t srcLength = srcData ? CFDataGetLength(srcData) : 0; const void *srcBytes = srcData ? CFDataGetBytePtr(srcData) : NULL; if (srcLength == 0 || !srcBytes) goto fail; vImage_Buffer src = {0}; src.data = (void *)srcBytes; src.width = width; src.height = height; src.rowBytes = CGImageGetBytesPerRow(srcImage); error = vImageBuffer_Init(dest, height, width, 32, kvImageNoFlags); if (error != kvImageNoError) goto fail; error = vImageConvert_AnyToAny(convertor, &src, dest, NULL, kvImageNoFlags); // convert if (error != kvImageNoError) goto fail; CFRelease(convertor); CFRelease(srcData); return YES; fail: if (convertor) CFRelease(convertor); if (srcData) CFRelease(srcData); if (dest->data) free(dest->data); dest->data = NULL; return NO; } /** Decode an image to bitmap buffer with the 32bit format (such as ARGB8888). @param srcImage Source image. @param dest Destination buffer. It should be zero before call this method. If decode succeed, you should release the dest->data using free(). @param bitmapInfo Destination bitmap format. @return Whether succeed. */ static BOOL YYCGImageDecodeToBitmapBufferWith32BitFormat(CGImageRef srcImage, vImage_Buffer *dest, CGBitmapInfo bitmapInfo) { if (!srcImage || !dest) return NO; size_t width = CGImageGetWidth(srcImage); size_t height = CGImageGetHeight(srcImage); if (width == 0 || height == 0) return NO; BOOL hasAlpha = NO; BOOL alphaFirst = NO; BOOL alphaPremultiplied = NO; BOOL byteOrderNormal = NO; switch (bitmapInfo & kCGBitmapAlphaInfoMask) { case kCGImageAlphaPremultipliedLast: { hasAlpha = YES; alphaPremultiplied = YES; } break; case kCGImageAlphaPremultipliedFirst: { hasAlpha = YES; alphaPremultiplied = YES; alphaFirst = YES; } break; case kCGImageAlphaLast: { hasAlpha = YES; } break; case kCGImageAlphaFirst: { hasAlpha = YES; alphaFirst = YES; } break; case kCGImageAlphaNoneSkipLast: { } break; case kCGImageAlphaNoneSkipFirst: { alphaFirst = YES; } break; default: { return NO; } break; } switch (bitmapInfo & kCGBitmapByteOrderMask) { case kCGBitmapByteOrderDefault: { byteOrderNormal = YES; } break; case kCGBitmapByteOrder32Little: { } break; case kCGBitmapByteOrder32Big: { byteOrderNormal = YES; } break; default: { return NO; } break; } /* Try convert with vImageConvert_AnyToAny() (avaliable since iOS 7.0). If fail, try decode with CGContextDrawImage(). CGBitmapContext use a premultiplied alpha format, unpremultiply may lose precision. */ vImage_CGImageFormat destFormat = {0}; destFormat.bitsPerComponent = 8; destFormat.bitsPerPixel = 32; destFormat.colorSpace = YYCGColorSpaceGetDeviceRGB(); destFormat.bitmapInfo = bitmapInfo; dest->data = NULL; if (YYCGImageDecodeToBitmapBufferWithAnyFormat(srcImage, dest, &destFormat)) return YES; CGBitmapInfo contextBitmapInfo = bitmapInfo & kCGBitmapByteOrderMask; if (!hasAlpha || alphaPremultiplied) { contextBitmapInfo |= (bitmapInfo & kCGBitmapAlphaInfoMask); } else { contextBitmapInfo |= alphaFirst ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaPremultipliedLast; } CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 32, YYCGColorSpaceGetDeviceRGB(), contextBitmapInfo); if (!context) goto fail; CGContextDrawImage(context, CGRectMake(0, 0, width, height), srcImage); // decode and convert size_t bytesPerRow = CGBitmapContextGetBytesPerRow(context); size_t length = height * bytesPerRow; void *data = CGBitmapContextGetData(context); if (length == 0 || !data) goto fail; dest->data = malloc(length); dest->width = width; dest->height = height; dest->rowBytes = bytesPerRow; if (!dest->data) goto fail; if (hasAlpha && !alphaPremultiplied) { vImage_Buffer tmpSrc = {0}; tmpSrc.data = data; tmpSrc.width = width; tmpSrc.height = height; tmpSrc.rowBytes = bytesPerRow; vImage_Error error; if (alphaFirst && byteOrderNormal) { error = vImageUnpremultiplyData_ARGB8888(&tmpSrc, dest, kvImageNoFlags); } else { error = vImageUnpremultiplyData_RGBA8888(&tmpSrc, dest, kvImageNoFlags); } if (error != kvImageNoError) goto fail; } else { memcpy(dest->data, data, length); } CFRelease(context); return YES; fail: if (context) CFRelease(context); if (dest->data) free(dest->data); dest->data = NULL; return NO; return NO; } CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) { if (!imageRef) return NULL; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); if (width == 0 || height == 0) return NULL; if (decodeForDisplay) { //decode with redraw (may lose some precision) CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask; BOOL hasAlpha = NO; if (alphaInfo == kCGImageAlphaPremultipliedLast || alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaLast || alphaInfo == kCGImageAlphaFirst) { hasAlpha = YES; } // BGRA8888 (premultiplied) or BGRX8888 // same as UIGraphicsBeginImageContext() and -[UIView drawRect:] CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo); if (!context) return NULL; CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode CGImageRef newImage = CGBitmapContextCreateImage(context); CFRelease(context); return newImage; } else { CGColorSpaceRef space = CGImageGetColorSpace(imageRef); size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef); size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef); size_t bytesPerRow = CGImageGetBytesPerRow(imageRef); CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); if (bytesPerRow == 0 || width == 0 || height == 0) return NULL; CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef); if (!dataProvider) return NULL; CFDataRef data = CGDataProviderCopyData(dataProvider); // decode if (!data) return NULL; CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data); CFRelease(data); if (!newProvider) return NULL; CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault); CFRelease(newProvider); return newImage; } } CGImageRef YYCGImageCreateAffineTransformCopy(CGImageRef imageRef, CGAffineTransform transform, CGSize destSize, CGBitmapInfo destBitmapInfo) { if (!imageRef) return NULL; size_t srcWidth = CGImageGetWidth(imageRef); size_t srcHeight = CGImageGetHeight(imageRef); size_t destWidth = round(destSize.width); size_t destHeight = round(destSize.height); if (srcWidth == 0 || srcHeight == 0 || destWidth == 0 || destHeight == 0) return NULL; CGDataProviderRef tmpProvider = NULL, destProvider = NULL; CGImageRef tmpImage = NULL, destImage = NULL; vImage_Buffer src = {0}, tmp = {0}, dest = {0}; if(!YYCGImageDecodeToBitmapBufferWith32BitFormat(imageRef, &src, kCGImageAlphaFirst | kCGBitmapByteOrderDefault)) return NULL; size_t destBytesPerRow = YYImageByteAlign(destWidth * 4, 32); tmp.data = malloc(destHeight * destBytesPerRow); if (!tmp.data) goto fail; tmp.width = destWidth; tmp.height = destHeight; tmp.rowBytes = destBytesPerRow; vImage_CGAffineTransform vTransform = *((vImage_CGAffineTransform *)&transform); uint8_t backColor[4] = {0}; vImage_Error error = vImageAffineWarpCG_ARGB8888(&src, &tmp, NULL, &vTransform, backColor, kvImageBackgroundColorFill); if (error != kvImageNoError) goto fail; free(src.data); src.data = NULL; tmpProvider = CGDataProviderCreateWithData(tmp.data, tmp.data, destHeight * destBytesPerRow, YYCGDataProviderReleaseDataCallback); if (!tmpProvider) goto fail; tmp.data = NULL; // hold by provider tmpImage = CGImageCreate(destWidth, destHeight, 8, 32, destBytesPerRow, YYCGColorSpaceGetDeviceRGB(), kCGImageAlphaFirst | kCGBitmapByteOrderDefault, tmpProvider, NULL, false, kCGRenderingIntentDefault); if (!tmpImage) goto fail; CFRelease(tmpProvider); tmpProvider = NULL; if ((destBitmapInfo & kCGBitmapAlphaInfoMask) == kCGImageAlphaFirst && (destBitmapInfo & kCGBitmapByteOrderMask) != kCGBitmapByteOrder32Little) { return tmpImage; } if (!YYCGImageDecodeToBitmapBufferWith32BitFormat(tmpImage, &dest, destBitmapInfo)) goto fail; CFRelease(tmpImage); tmpImage = NULL; destProvider = CGDataProviderCreateWithData(dest.data, dest.data, destHeight * destBytesPerRow, YYCGDataProviderReleaseDataCallback); if (!destProvider) goto fail; dest.data = NULL; // hold by provider destImage = CGImageCreate(destWidth, destHeight, 8, 32, destBytesPerRow, YYCGColorSpaceGetDeviceRGB(), destBitmapInfo, destProvider, NULL, false, kCGRenderingIntentDefault); if (!destImage) goto fail; CFRelease(destProvider); destProvider = NULL; return destImage; fail: if (src.data) free(src.data); if (tmp.data) free(tmp.data); if (dest.data) free(dest.data); if (tmpProvider) CFRelease(tmpProvider); if (tmpImage) CFRelease(tmpImage); if (destProvider) CFRelease(destProvider); return NULL; } UIImageOrientation YYUIImageOrientationFromEXIFValue(NSInteger value) { switch (value) { case kCGImagePropertyOrientationUp: return UIImageOrientationUp; case kCGImagePropertyOrientationDown: return UIImageOrientationDown; case kCGImagePropertyOrientationLeft: return UIImageOrientationLeft; case kCGImagePropertyOrientationRight: return UIImageOrientationRight; case kCGImagePropertyOrientationUpMirrored: return UIImageOrientationUpMirrored; case kCGImagePropertyOrientationDownMirrored: return UIImageOrientationDownMirrored; case kCGImagePropertyOrientationLeftMirrored: return UIImageOrientationLeftMirrored; case kCGImagePropertyOrientationRightMirrored: return UIImageOrientationRightMirrored; default: return UIImageOrientationUp; } } NSInteger YYUIImageOrientationToEXIFValue(UIImageOrientation orientation) { switch (orientation) { case UIImageOrientationUp: return kCGImagePropertyOrientationUp; case UIImageOrientationDown: return kCGImagePropertyOrientationDown; case UIImageOrientationLeft: return kCGImagePropertyOrientationLeft; case UIImageOrientationRight: return kCGImagePropertyOrientationRight; case UIImageOrientationUpMirrored: return kCGImagePropertyOrientationUpMirrored; case UIImageOrientationDownMirrored: return kCGImagePropertyOrientationDownMirrored; case UIImageOrientationLeftMirrored: return kCGImagePropertyOrientationLeftMirrored; case UIImageOrientationRightMirrored: return kCGImagePropertyOrientationRightMirrored; default: return kCGImagePropertyOrientationUp; } } CGImageRef YYCGImageCreateCopyWithOrientation(CGImageRef imageRef, UIImageOrientation orientation, CGBitmapInfo destBitmapInfo) { if (!imageRef) return NULL; if (orientation == UIImageOrientationUp) return (CGImageRef)CFRetain(imageRef); size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); CGAffineTransform transform = CGAffineTransformIdentity; BOOL swapWidthAndHeight = NO; switch (orientation) { case UIImageOrientationDown: { transform = CGAffineTransformMakeRotation(YYImageDegreesToRadians(180)); transform = CGAffineTransformTranslate(transform, -(CGFloat)width, -(CGFloat)height); } break; case UIImageOrientationLeft: { transform = CGAffineTransformMakeRotation(YYImageDegreesToRadians(90)); transform = CGAffineTransformTranslate(transform, -(CGFloat)0, -(CGFloat)height); swapWidthAndHeight = YES; } break; case UIImageOrientationRight: { transform = CGAffineTransformMakeRotation(YYImageDegreesToRadians(-90)); transform = CGAffineTransformTranslate(transform, -(CGFloat)width, (CGFloat)0); swapWidthAndHeight = YES; } break; case UIImageOrientationUpMirrored: { transform = CGAffineTransformTranslate(transform, (CGFloat)width, 0); transform = CGAffineTransformScale(transform, -1, 1); } break; case UIImageOrientationDownMirrored: { transform = CGAffineTransformTranslate(transform, 0, (CGFloat)height); transform = CGAffineTransformScale(transform, 1, -1); } break; case UIImageOrientationLeftMirrored: { transform = CGAffineTransformMakeRotation(YYImageDegreesToRadians(-90)); transform = CGAffineTransformScale(transform, 1, -1); transform = CGAffineTransformTranslate(transform, -(CGFloat)width, -(CGFloat)height); swapWidthAndHeight = YES; } break; case UIImageOrientationRightMirrored: { transform = CGAffineTransformMakeRotation(YYImageDegreesToRadians(90)); transform = CGAffineTransformScale(transform, 1, -1); swapWidthAndHeight = YES; } break; default: break; } if (CGAffineTransformIsIdentity(transform)) return (CGImageRef)CFRetain(imageRef); CGSize destSize = {width, height}; if (swapWidthAndHeight) { destSize.width = height; destSize.height = width; } return YYCGImageCreateAffineTransformCopy(imageRef, transform, destSize, destBitmapInfo); } YYImageType YYImageDetectType(CFDataRef data) { if (!data) return YYImageTypeUnknown; uint64_t length = CFDataGetLength(data); if (length < 16) return YYImageTypeUnknown; const char *bytes = (char *)CFDataGetBytePtr(data); uint32_t magic4 = *((uint32_t *)bytes); switch (magic4) { case YY_FOUR_CC(0x4D, 0x4D, 0x00, 0x2A): { // big endian TIFF return YYImageTypeTIFF; } break; case YY_FOUR_CC(0x49, 0x49, 0x2A, 0x00): { // little endian TIFF return YYImageTypeTIFF; } break; case YY_FOUR_CC(0x00, 0x00, 0x01, 0x00): { // ICO return YYImageTypeICO; } break; case YY_FOUR_CC(0x00, 0x00, 0x02, 0x00): { // CUR return YYImageTypeICO; } break; case YY_FOUR_CC('i', 'c', 'n', 's'): { // ICNS return YYImageTypeICNS; } break; case YY_FOUR_CC('G', 'I', 'F', '8'): { // GIF return YYImageTypeGIF; } break; case YY_FOUR_CC(0x89, 'P', 'N', 'G'): { // PNG uint32_t tmp = *((uint32_t *)(bytes + 4)); if (tmp == YY_FOUR_CC('\r', '\n', 0x1A, '\n')) { return YYImageTypePNG; } } break; case YY_FOUR_CC('R', 'I', 'F', 'F'): { // WebP uint32_t tmp = *((uint32_t *)(bytes + 8)); if (tmp == YY_FOUR_CC('W', 'E', 'B', 'P')) { return YYImageTypeWebP; } } break; /* case YY_FOUR_CC('B', 'P', 'G', 0xFB): { // BPG return YYImageTypeBPG; } break; */ } uint16_t magic2 = *((uint16_t *)bytes); switch (magic2) { case YY_TWO_CC('B', 'A'): case YY_TWO_CC('B', 'M'): case YY_TWO_CC('I', 'C'): case YY_TWO_CC('P', 'I'): case YY_TWO_CC('C', 'I'): case YY_TWO_CC('C', 'P'): { // BMP return YYImageTypeBMP; } case YY_TWO_CC(0xFF, 0x4F): { // JPEG2000 return YYImageTypeJPEG2000; } } // JPG FF D8 FF if (memcmp(bytes,"\377\330\377",3) == 0) return YYImageTypeJPEG; // JP2 if (memcmp(bytes + 4, "\152\120\040\040\015", 5) == 0) return YYImageTypeJPEG2000; return YYImageTypeUnknown; } CFStringRef YYImageTypeToUTType(YYImageType type) { switch (type) { case YYImageTypeJPEG: return kUTTypeJPEG; case YYImageTypeJPEG2000: return kUTTypeJPEG2000; case YYImageTypeTIFF: return kUTTypeTIFF; case YYImageTypeBMP: return kUTTypeBMP; case YYImageTypeICO: return kUTTypeICO; case YYImageTypeICNS: return kUTTypeAppleICNS; case YYImageTypeGIF: return kUTTypeGIF; case YYImageTypePNG: return kUTTypePNG; default: return NULL; } } YYImageType YYImageTypeFromUTType(CFStringRef uti) { static NSDictionary *dic; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ dic = @{(id)kUTTypeJPEG : @(YYImageTypeJPEG), (id)kUTTypeJPEG2000 : @(YYImageTypeJPEG2000), (id)kUTTypeTIFF : @(YYImageTypeTIFF), (id)kUTTypeBMP : @(YYImageTypeBMP), (id)kUTTypeICO : @(YYImageTypeICO), (id)kUTTypeAppleICNS : @(YYImageTypeICNS), (id)kUTTypeGIF : @(YYImageTypeGIF), (id)kUTTypePNG : @(YYImageTypePNG)}; }); if (!uti) return YYImageTypeUnknown; NSNumber *num = dic[(__bridge __strong id)(uti)]; return num.unsignedIntegerValue; } NSString *YYImageTypeGetExtension(YYImageType type) { switch (type) { case YYImageTypeJPEG: return @"jpg"; case YYImageTypeJPEG2000: return @"jp2"; case YYImageTypeTIFF: return @"tiff"; case YYImageTypeBMP: return @"bmp"; case YYImageTypeICO: return @"ico"; case YYImageTypeICNS: return @"icns"; case YYImageTypeGIF: return @"gif"; case YYImageTypePNG: return @"png"; case YYImageTypeWebP: return @"webp"; default: return nil; } } CFDataRef YYCGImageCreateEncodedData(CGImageRef imageRef, YYImageType type, CGFloat quality) { if (!imageRef) return nil; quality = quality < 0 ? 0 : quality > 1 ? 1 : quality; if (type == YYImageTypeWebP) { #if YYIMAGE_WEBP_ENABLED if (quality == 1) { return YYCGImageCreateEncodedWebPData(imageRef, YES, quality, 4, YYImagePresetDefault); } else { return YYCGImageCreateEncodedWebPData(imageRef, NO, quality, 4, YYImagePresetDefault); } #else return NULL; #endif } CFStringRef uti = YYImageTypeToUTType(type); if (!uti) return nil; CFMutableDataRef data = CFDataCreateMutable(CFAllocatorGetDefault(), 0); if (!data) return NULL; CGImageDestinationRef dest = CGImageDestinationCreateWithData(data, uti, 1, NULL); if (!dest) { CFRelease(data); return NULL; } NSDictionary *options = @{(id)kCGImageDestinationLossyCompressionQuality : @(quality) }; CGImageDestinationAddImage(dest, imageRef, (CFDictionaryRef)options); if (!CGImageDestinationFinalize(dest)) { CFRelease(data); CFRelease(dest); return nil; } CFRelease(dest); if (CFDataGetLength(data) == 0) { CFRelease(data); return NULL; } return data; } #if YYIMAGE_WEBP_ENABLED BOOL YYImageWebPAvailable() { return YES; } CFDataRef YYCGImageCreateEncodedWebPData(CGImageRef imageRef, BOOL lossless, CGFloat quality, int compressLevel, YYImagePreset preset) { if (!imageRef) return nil; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); if (width == 0 || width > WEBP_MAX_DIMENSION) return nil; if (height == 0 || height > WEBP_MAX_DIMENSION) return nil; vImage_Buffer buffer = {0}; if(!YYCGImageDecodeToBitmapBufferWith32BitFormat(imageRef, &buffer, kCGImageAlphaLast | kCGBitmapByteOrderDefault)) return nil; WebPConfig config = {0}; WebPPicture picture = {0}; WebPMemoryWriter writer = {0}; CFDataRef webpData = NULL; BOOL pictureNeedFree = NO; quality = quality < 0 ? 0 : quality > 1 ? 1 : quality; preset = preset > YYImagePresetText ? YYImagePresetDefault : preset; compressLevel = compressLevel < 0 ? 0 : compressLevel > 6 ? 6 : compressLevel; if (!WebPConfigPreset(&config, (WebPPreset)preset, quality)) goto fail; config.quality = round(quality * 100.0); config.lossless = lossless; config.method = compressLevel; switch ((WebPPreset)preset) { case WEBP_PRESET_DEFAULT: { config.image_hint = WEBP_HINT_DEFAULT; } break; case WEBP_PRESET_PICTURE: { config.image_hint = WEBP_HINT_PICTURE; } break; case WEBP_PRESET_PHOTO: { config.image_hint = WEBP_HINT_PHOTO; } break; case WEBP_PRESET_DRAWING: case WEBP_PRESET_ICON: case WEBP_PRESET_TEXT: { config.image_hint = WEBP_HINT_GRAPH; } break; } if (!WebPValidateConfig(&config)) goto fail; if (!WebPPictureInit(&picture)) goto fail; pictureNeedFree = YES; picture.width = (int)buffer.width; picture.height = (int)buffer.height; picture.use_argb = lossless; if(!WebPPictureImportRGBA(&picture, buffer.data, (int)buffer.rowBytes)) goto fail; WebPMemoryWriterInit(&writer); picture.writer = WebPMemoryWrite; picture.custom_ptr = &writer; if(!WebPEncode(&config, &picture)) goto fail; webpData = CFDataCreate(CFAllocatorGetDefault(), writer.mem, writer.size); free(writer.mem); WebPPictureFree(&picture); free(buffer.data); return webpData; fail: if (buffer.data) free(buffer.data); if (pictureNeedFree) WebPPictureFree(&picture); return nil; } NSUInteger YYImageGetWebPFrameCount(CFDataRef webpData) { if (!webpData || CFDataGetLength(webpData) == 0) return 0; WebPData data = {CFDataGetBytePtr(webpData), CFDataGetLength(webpData)}; WebPDemuxer *demuxer = WebPDemux(&data); if (!demuxer) return 0; NSUInteger webpFrameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT); WebPDemuxDelete(demuxer); return webpFrameCount; } CGImageRef YYCGImageCreateWithWebPData(CFDataRef webpData, BOOL decodeForDisplay, BOOL useThreads, BOOL bypassFiltering, BOOL noFancyUpsampling) { /* Call WebPDecode() on a multi-frame webp data will get an error (VP8_STATUS_UNSUPPORTED_FEATURE). Use WebPDemuxer to unpack it first. */ WebPData data = {0}; WebPDemuxer *demuxer = NULL; int frameCount = 0, canvasWidth = 0, canvasHeight = 0; WebPIterator iter = {0}; BOOL iterInited = NO; const uint8_t *payload = NULL; size_t payloadSize = 0; WebPDecoderConfig config = {0}; BOOL hasAlpha = NO; size_t bitsPerComponent = 0, bitsPerPixel = 0, bytesPerRow = 0, destLength = 0; CGBitmapInfo bitmapInfo = 0; WEBP_CSP_MODE colorspace = 0; void *destBytes = NULL; CGDataProviderRef provider = NULL; CGImageRef imageRef = NULL; if (!webpData || CFDataGetLength(webpData) == 0) return NULL; data.bytes = CFDataGetBytePtr(webpData); data.size = CFDataGetLength(webpData); demuxer = WebPDemux(&data); if (!demuxer) goto fail; frameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT); if (frameCount == 0) { goto fail; } else if (frameCount == 1) { // single-frame payload = data.bytes; payloadSize = data.size; if (!WebPInitDecoderConfig(&config)) goto fail; if (WebPGetFeatures(payload , payloadSize, &config.input) != VP8_STATUS_OK) goto fail; canvasWidth = config.input.width; canvasHeight = config.input.height; } else { // multi-frame canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); if (canvasWidth < 1 || canvasHeight < 1) goto fail; if (!WebPDemuxGetFrame(demuxer, 1, &iter)) goto fail; iterInited = YES; if (iter.width > canvasWidth || iter.height > canvasHeight) goto fail; payload = iter.fragment.bytes; payloadSize = iter.fragment.size; if (!WebPInitDecoderConfig(&config)) goto fail; if (WebPGetFeatures(payload , payloadSize, &config.input) != VP8_STATUS_OK) goto fail; } if (payload == NULL || payloadSize == 0) goto fail; hasAlpha = config.input.has_alpha; bitsPerComponent = 8; bitsPerPixel = 32; bytesPerRow = YYImageByteAlign(bitsPerPixel / 8 * canvasWidth, 32); destLength = bytesPerRow * canvasHeight; if (decodeForDisplay) { bitmapInfo = kCGBitmapByteOrder32Host; bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; colorspace = MODE_bgrA; // small endian } else { bitmapInfo = kCGBitmapByteOrderDefault; bitmapInfo |= hasAlpha ? kCGImageAlphaLast : kCGImageAlphaNoneSkipLast; colorspace = MODE_RGBA; } destBytes = calloc(1, destLength); if (!destBytes) goto fail; config.options.use_threads = useThreads; //speed up 23% config.options.bypass_filtering = bypassFiltering; //speed up 11%, cause some banding config.options.no_fancy_upsampling = noFancyUpsampling; //speed down 16%, lose some details config.output.colorspace = colorspace; config.output.is_external_memory = 1; config.output.u.RGBA.rgba = destBytes; config.output.u.RGBA.stride = (int)bytesPerRow; config.output.u.RGBA.size = destLength; VP8StatusCode result = WebPDecode(payload, payloadSize, &config); if ((result != VP8_STATUS_OK) && (result != VP8_STATUS_NOT_ENOUGH_DATA)) goto fail; if (iter.x_offset != 0 || iter.y_offset != 0) { void *tmp = calloc(1, destLength); if (tmp) { vImage_Buffer src = {destBytes, canvasHeight, canvasWidth, bytesPerRow}; vImage_Buffer dest = {destBytes, canvasHeight, canvasWidth, bytesPerRow}; vImage_CGAffineTransform transform = {1, 0, 0, 1, iter.x_offset, -iter.y_offset}; uint8_t backColor[4] = {0}; vImageAffineWarpCG_ARGB8888(&src, &dest, NULL, &transform, backColor, kvImageBackgroundColorFill); memcpy(destBytes, tmp, destLength); free(tmp); } } provider = CGDataProviderCreateWithData(destBytes, destBytes, destLength, YYCGDataProviderReleaseDataCallback); if (!provider) goto fail; destBytes = NULL; // hold by provider imageRef = CGImageCreate(canvasWidth, canvasHeight, bitsPerComponent, bitsPerPixel, bytesPerRow, YYCGColorSpaceGetDeviceRGB(), bitmapInfo, provider, NULL, false, kCGRenderingIntentDefault); CFRelease(provider); if (iterInited) WebPDemuxReleaseIterator(&iter); WebPDemuxDelete(demuxer); return imageRef; fail: if (destBytes) free(destBytes); if (provider) CFRelease(provider); if (iterInited) WebPDemuxReleaseIterator(&iter); if (demuxer) WebPDemuxDelete(demuxer); return NULL; } #else BOOL YYImageWebPAvailable() { return NO; } CFDataRef YYCGImageCreateEncodedWebPData(CGImageRef imageRef, BOOL lossless, CGFloat quality, int compressLevel, YYImagePreset preset) { NSLog(@"WebP decoder is disabled"); return NULL; } NSUInteger YYImageGetWebPFrameCount(CFDataRef webpData) { NSLog(@"WebP decoder is disabled"); return 0; } CGImageRef YYCGImageCreateWithWebPData(CFDataRef webpData, BOOL decodeForDisplay, BOOL useThreads, BOOL bypassFiltering, BOOL noFancyUpsampling) { NSLog(@"WebP decoder is disabled"); return NULL; } #endif //////////////////////////////////////////////////////////////////////////////// #pragma mark - Decoder @implementation YYImageFrame + (instancetype)frameWithImage:(UIImage *)image { YYImageFrame *frame = [self new]; frame.image = image; return frame; } - (id)copyWithZone:(NSZone *)zone { YYImageFrame *frame = [self.class new]; frame.index = _index; frame.width = _width; frame.height = _height; frame.offsetX = _offsetX; frame.offsetY = _offsetY; frame.duration = _duration; frame.dispose = _dispose; frame.blend = _blend; frame.image = _image.copy; return frame; } @end // Internal frame object. @interface _YYImageDecoderFrame : YYImageFrame @property (nonatomic, assign) BOOL hasAlpha; ///< Whether frame has alpha. @property (nonatomic, assign) BOOL isFullSize; ///< Whether frame fill the canvas. @property (nonatomic, assign) NSUInteger blendFromIndex; ///< Blend from frame index to current frame. @end @implementation _YYImageDecoderFrame - (id)copyWithZone:(NSZone *)zone { _YYImageDecoderFrame *frame = [super copyWithZone:zone]; frame.hasAlpha = _hasAlpha; frame.isFullSize = _isFullSize; frame.blendFromIndex = _blendFromIndex; return frame; } @end @implementation YYImageDecoder { pthread_mutex_t _lock; // recursive lock BOOL _sourceTypeDetected; CGImageSourceRef _source; yy_png_info *_apngSource; #if YYIMAGE_WEBP_ENABLED WebPDemuxer *_webpSource; #endif UIImageOrientation _orientation; dispatch_semaphore_t _framesLock; NSArray *_frames; ///< Array, without image BOOL _needBlend; NSUInteger _blendFrameIndex; CGContextRef _blendCanvas; } - (void)dealloc { if (_source) CFRelease(_source); if (_apngSource) yy_png_info_release(_apngSource); #if YYIMAGE_WEBP_ENABLED if (_webpSource) WebPDemuxDelete(_webpSource); #endif if (_blendCanvas) CFRelease(_blendCanvas); } + (instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale { if (!data) return nil; YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:scale]; [decoder updateData:data final:YES]; if (decoder.frameCount == 0) return nil; return decoder; } - (instancetype)init { return [self initWithScale:[UIScreen mainScreen].scale]; } - (instancetype)initWithScale:(CGFloat)scale { self = [super init]; if (scale <= 0) scale = 1; _scale = scale; _framesLock = dispatch_semaphore_create(1); pthread_mutexattr_t attr; pthread_mutexattr_init (&attr); pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init (&_lock, &attr); pthread_mutexattr_destroy (&attr); return self; } - (BOOL)updateData:(NSData *)data final:(BOOL)final { BOOL result = NO; pthread_mutex_lock(&_lock); result = [self _updateData:data final:final]; pthread_mutex_unlock(&_lock); return result; } - (YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay { YYImageFrame *result = nil; pthread_mutex_lock(&_lock); result = [self _frameAtIndex:index decodeForDisplay:decodeForDisplay]; pthread_mutex_unlock(&_lock); return result; } - (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index { NSTimeInterval result = 0; dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); if (index < _frames.count) { result = ((_YYImageDecoderFrame *)_frames[index]).duration; } dispatch_semaphore_signal(_framesLock); return result; } - (NSDictionary *)framePropertiesAtIndex:(NSUInteger)index { NSDictionary *result = nil; pthread_mutex_lock(&_lock); result = [self _framePropertiesAtIndex:index]; pthread_mutex_unlock(&_lock); return result; } - (NSDictionary *)imageProperties { NSDictionary *result = nil; pthread_mutex_lock(&_lock); result = [self _imageProperties]; pthread_mutex_unlock(&_lock); return result; } #pragma private (wrap) - (BOOL)_updateData:(NSData *)data final:(BOOL)final { if (_finalized) return NO; if (data.length < _data.length) return NO; _finalized = final; _data = data; YYImageType type = YYImageDetectType((__bridge CFDataRef)data); if (_sourceTypeDetected) { if (_type != type) { return NO; } else { [self _updateSource]; } } else { if (_data.length > 16) { _type = type; _sourceTypeDetected = YES; [self _updateSource]; } } return YES; } - (YYImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay { if (index >= _frames.count) return 0; _YYImageDecoderFrame *frame = [(_YYImageDecoderFrame *)_frames[index] copy]; BOOL decoded = NO; BOOL extendToCanvas = NO; if (_type != YYImageTypeICO && decodeForDisplay) { // ICO contains multi-size frame and should not extend to canvas. extendToCanvas = YES; } if (!_needBlend) { CGImageRef imageRef = [self _newUnblendedImageAtIndex:index extendToCanvas:extendToCanvas decoded:&decoded]; if (!imageRef) return nil; if (decodeForDisplay && !decoded) { CGImageRef imageRefDecoded = YYCGImageCreateDecodedCopy(imageRef, YES); if (imageRefDecoded) { CFRelease(imageRef); imageRef = imageRefDecoded; decoded = YES; } } UIImage *image = [UIImage imageWithCGImage:imageRef scale:_scale orientation:_orientation]; CFRelease(imageRef); if (!image) return nil; image.yy_isDecodedForDisplay = decoded; frame.image = image; return frame; } // blend if (![self _createBlendContextIfNeeded]) return nil; CGImageRef imageRef = NULL; if (_blendFrameIndex + 1 == frame.index) { imageRef = [self _newBlendedImageWithFrame:frame]; _blendFrameIndex = index; } else { // should draw canvas from previous frame _blendFrameIndex = NSNotFound; CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height)); if (frame.blendFromIndex == frame.index) { CGImageRef unblendedImage = [self _newUnblendedImageAtIndex:index extendToCanvas:NO decoded:NULL]; if (unblendedImage) { CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendedImage); CFRelease(unblendedImage); } imageRef = CGBitmapContextCreateImage(_blendCanvas); if (frame.dispose == YYImageDisposeBackground) { CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); } _blendFrameIndex = index; } else { // canvas is not ready for (uint32_t i = (uint32_t)frame.blendFromIndex; i <= (uint32_t)frame.index; i++) { if (i == frame.index) { if (!imageRef) imageRef = [self _newBlendedImageWithFrame:frame]; } else { [self _blendImageWithFrame:_frames[i]]; } } _blendFrameIndex = index; } } if (!imageRef) return nil; UIImage *image = [UIImage imageWithCGImage:imageRef scale:_scale orientation:_orientation]; CFRelease(imageRef); if (!image) return nil; image.yy_isDecodedForDisplay = YES; frame.image = image; if (extendToCanvas) { frame.width = _width; frame.height = _height; frame.offsetX = 0; frame.offsetY = 0; frame.dispose = YYImageDisposeNone; frame.blend = YYImageBlendNone; } return frame; } - (NSDictionary *)_framePropertiesAtIndex:(NSUInteger)index { if (index >= _frames.count) return nil; if (!_source) return nil; CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_source, index, NULL); if (!properties) return nil; return CFBridgingRelease(properties); } - (NSDictionary *)_imageProperties { if (!_source) return nil; CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL); if (!properties) return nil; return CFBridgingRelease(properties); } #pragma private - (void)_updateSource { switch (_type) { case YYImageTypeWebP: { [self _updateSourceWebP]; } break; case YYImageTypePNG: { [self _updateSourceAPNG]; } break; default: { [self _updateSourceImageIO]; } break; } } - (void)_updateSourceWebP { #if YYIMAGE_WEBP_ENABLED _width = 0; _height = 0; _loopCount = 0; if (_webpSource) WebPDemuxDelete(_webpSource); _webpSource = NULL; dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); _frames = nil; dispatch_semaphore_signal(_framesLock); /* https://developers.google.com/speed/webp/docs/api The documentation said we can use WebPIDecoder to decode webp progressively, but currently it can only returns an empty image (not same as progressive jpegs), so we don't use progressive decoding. When using WebPDecode() to decode multi-frame webp, we will get the error "VP8_STATUS_UNSUPPORTED_FEATURE", so we first use WebPDemuxer to unpack it. */ WebPData webPData = {0}; webPData.bytes = _data.bytes; webPData.size = _data.length; WebPDemuxer *demuxer = WebPDemux(&webPData); if (!demuxer) return; uint32_t webpFrameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT); uint32_t webpLoopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT); uint32_t canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); uint32_t canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); if (webpFrameCount == 0 || canvasWidth < 1 || canvasHeight < 1) { WebPDemuxDelete(demuxer); return; } NSMutableArray *frames = [NSMutableArray new]; BOOL needBlend = NO; uint32_t iterIndex = 0; uint32_t lastBlendIndex = 0; WebPIterator iter = {0}; if (WebPDemuxGetFrame(demuxer, 1, &iter)) { // one-based index... do { _YYImageDecoderFrame *frame = [_YYImageDecoderFrame new]; [frames addObject:frame]; if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { frame.dispose = YYImageDisposeBackground; } if (iter.blend_method == WEBP_MUX_BLEND) { frame.blend = YYImageBlendOver; } int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); frame.index = iterIndex; frame.duration = iter.duration / 1000.0; frame.width = iter.width; frame.height = iter.height; frame.hasAlpha = iter.has_alpha; frame.blend = iter.blend_method == WEBP_MUX_BLEND; frame.offsetX = iter.x_offset; frame.offsetY = canvasHeight - iter.y_offset - iter.height; BOOL sizeEqualsToCanvas = (iter.width == canvasWidth && iter.height == canvasHeight); BOOL offsetIsZero = (iter.x_offset == 0 && iter.y_offset == 0); frame.isFullSize = (sizeEqualsToCanvas && offsetIsZero); if ((!frame.blend || !frame.hasAlpha) && frame.isFullSize) { frame.blendFromIndex = lastBlendIndex = iterIndex; } else { if (frame.dispose && frame.isFullSize) { frame.blendFromIndex = lastBlendIndex; lastBlendIndex = iterIndex + 1; } else { frame.blendFromIndex = lastBlendIndex; } } if (frame.index != frame.blendFromIndex) needBlend = YES; iterIndex++; } while (WebPDemuxNextFrame(&iter)); WebPDemuxReleaseIterator(&iter); } if (frames.count != webpFrameCount) { WebPDemuxDelete(demuxer); return; } _width = canvasWidth; _height = canvasHeight; _frameCount = frames.count; _loopCount = webpLoopCount; _needBlend = needBlend; _webpSource = demuxer; dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); _frames = frames; dispatch_semaphore_signal(_framesLock); #endif } - (void)_updateSourceAPNG { /* APNG extends PNG format to support animation, it was supported by ImageIO since iOS 8. We use a custom APNG decoder to make APNG available in old system, so we ignore the ImageIO's APNG frame info. Typically the custom decoder is a bit faster than ImageIO. */ yy_png_info_release(_apngSource); _apngSource = nil; [self _updateSourceImageIO]; // decode first frame if (_frameCount == 0) return; // png decode failed if (!_finalized) return; // ignore multi-frame before finalized yy_png_info *apng = yy_png_info_create(_data.bytes, (uint32_t)_data.length); if (!apng) return; // apng decode failed if (apng->apng_frame_num == 0 || (apng->apng_frame_num == 1 && apng->apng_first_frame_is_cover)) { yy_png_info_release(apng); return; // no animation } if (_source) { // apng decode succeed, no longer need image souce CFRelease(_source); _source = NULL; } uint32_t canvasWidth = apng->header.width; uint32_t canvasHeight = apng->header.height; NSMutableArray *frames = [NSMutableArray new]; BOOL needBlend = NO; uint32_t lastBlendIndex = 0; for (uint32_t i = 0; i < apng->apng_frame_num; i++) { _YYImageDecoderFrame *frame = [_YYImageDecoderFrame new]; [frames addObject:frame]; yy_png_frame_info *fi = apng->apng_frames + i; frame.index = i; frame.duration = yy_png_delay_to_seconds(fi->frame_control.delay_num, fi->frame_control.delay_den); frame.hasAlpha = YES; frame.width = fi->frame_control.width; frame.height = fi->frame_control.height; frame.offsetX = fi->frame_control.x_offset; frame.offsetY = canvasHeight - fi->frame_control.y_offset - fi->frame_control.height; BOOL sizeEqualsToCanvas = (frame.width == canvasWidth && frame.height == canvasHeight); BOOL offsetIsZero = (fi->frame_control.x_offset == 0 && fi->frame_control.y_offset == 0); frame.isFullSize = (sizeEqualsToCanvas && offsetIsZero); switch (fi->frame_control.dispose_op) { case YY_PNG_DISPOSE_OP_BACKGROUND: { frame.dispose = YYImageDisposeBackground; } break; case YY_PNG_DISPOSE_OP_PREVIOUS: { frame.dispose = YYImageDisposePrevious; } break; default: { frame.dispose = YYImageDisposeNone; } break; } switch (fi->frame_control.blend_op) { case YY_PNG_BLEND_OP_OVER: { frame.blend = YYImageBlendOver; } break; default: { frame.blend = YYImageBlendNone; } break; } if (frame.blend == YYImageBlendNone && frame.isFullSize) { frame.blendFromIndex = i; if (frame.dispose != YYImageDisposePrevious) lastBlendIndex = i; } else { if (frame.dispose == YYImageDisposeBackground && frame.isFullSize) { frame.blendFromIndex = lastBlendIndex; lastBlendIndex = i + 1; } else { frame.blendFromIndex = lastBlendIndex; } } if (frame.index != frame.blendFromIndex) needBlend = YES; } _width = canvasWidth; _height = canvasHeight; _frameCount = frames.count; _loopCount = apng->apng_loop_num; _needBlend = needBlend; _apngSource = apng; dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); _frames = frames; dispatch_semaphore_signal(_framesLock); } - (void)_updateSourceImageIO { _width = 0; _height = 0; _orientation = UIImageOrientationUp; _loopCount = 0; dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); _frames = nil; dispatch_semaphore_signal(_framesLock); if (!_source) { if (_finalized) { _source = CGImageSourceCreateWithData((__bridge CFDataRef)_data, NULL); } else { _source = CGImageSourceCreateIncremental(NULL); if (_source) CGImageSourceUpdateData(_source, (__bridge CFDataRef)_data, false); } } else { CGImageSourceUpdateData(_source, (__bridge CFDataRef)_data, _finalized); } if (!_source) return; _frameCount = CGImageSourceGetCount(_source); if (_frameCount == 0) return; if (!_finalized) { // ignore multi-frame before finalized _frameCount = 1; } else { if (_type == YYImageTypePNG) { // use custom apng decoder and ignore multi-frame _frameCount = 1; } if (_type == YYImageTypeGIF) { // get gif loop count CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL); if (properties) { CFTypeRef loop = CFDictionaryGetValue(properties, kCGImagePropertyGIFLoopCount); if (loop) CFNumberGetValue(loop, kCFNumberNSIntegerType, &_loopCount); CFRelease(properties); } } } /* ICO, GIF, APNG may contains multi-frame. */ NSMutableArray *frames = [NSMutableArray new]; for (NSUInteger i = 0; i < _frameCount; i++) { _YYImageDecoderFrame *frame = [_YYImageDecoderFrame new]; frame.index = i; frame.blendFromIndex = i; frame.hasAlpha = YES; frame.isFullSize = YES; [frames addObject:frame]; CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_source, i, NULL); if (properties) { NSTimeInterval duration = 0; NSInteger orientationValue = 0, width = 0, height = 0; CFTypeRef value = NULL; value = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &width); value = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &height); if (_type == YYImageTypeGIF) { CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary); if (gif) { // Use the unclamped frame delay if it exists. value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime); if (!value) { // Fall back to the clamped frame delay if the unclamped frame delay does not exist. value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime); } if (value) CFNumberGetValue(value, kCFNumberDoubleType, &duration); } } frame.width = width; frame.height = height; frame.duration = duration; if (i == 0 && _width + _height == 0) { // init first frame _width = width; _height = height; value = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); if (value) { CFNumberGetValue(value, kCFNumberNSIntegerType, &orientationValue); _orientation = YYUIImageOrientationFromEXIFValue(orientationValue); } } CFRelease(properties); } } dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); _frames = frames; dispatch_semaphore_signal(_framesLock); } - (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index extendToCanvas:(BOOL)extendToCanvas decoded:(BOOL *)decoded CF_RETURNS_RETAINED { if (!_finalized && index > 0) return NULL; if (_frames.count <= index) return NULL; _YYImageDecoderFrame *frame = _frames[index]; if (_source) { CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_source, index, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(YES)}); if (imageRef && extendToCanvas) { size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); if (width == _width && height == _height) { CGImageRef imageRefExtended = YYCGImageCreateDecodedCopy(imageRef, YES); if (imageRefExtended) { CFRelease(imageRef); imageRef = imageRefExtended; if (decoded) *decoded = YES; } } else { CGContextRef context = CGBitmapContextCreate(NULL, _width, _height, 8, 0, YYCGColorSpaceGetDeviceRGB(), kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); if (context) { CGContextDrawImage(context, CGRectMake(0, _height - height, width, height), imageRef); CGImageRef imageRefExtended = CGBitmapContextCreateImage(context); CFRelease(context); if (imageRefExtended) { CFRelease(imageRef); imageRef = imageRefExtended; if (decoded) *decoded = YES; } } } } return imageRef; } if (_apngSource) { uint32_t size = 0; uint8_t *bytes = yy_png_copy_frame_data_at_index(_data.bytes, _apngSource, (uint32_t)index, &size); if (!bytes) return NULL; CGDataProviderRef provider = CGDataProviderCreateWithData(bytes, bytes, size, YYCGDataProviderReleaseDataCallback); if (!provider) { free(bytes); return NULL; } bytes = NULL; // hold by provider CGImageSourceRef source = CGImageSourceCreateWithDataProvider(provider, NULL); if (!source) { CFRelease(provider); return NULL; } CFRelease(provider); if(CGImageSourceGetCount(source) < 1) { CFRelease(source); return NULL; } CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(YES)}); CFRelease(source); if (!imageRef) return NULL; if (extendToCanvas) { CGContextRef context = CGBitmapContextCreate(NULL, _width, _height, 8, 0, YYCGColorSpaceGetDeviceRGB(), kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); //bgrA if (context) { CGContextDrawImage(context, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), imageRef); CFRelease(imageRef); imageRef = CGBitmapContextCreateImage(context); CFRelease(context); if (decoded) *decoded = YES; } } return imageRef; } #if YYIMAGE_WEBP_ENABLED if (_webpSource) { WebPIterator iter; if (!WebPDemuxGetFrame(_webpSource, (int)(index + 1), &iter)) return NULL; // demux webp frame data // frame numbers are one-based in webp -----------^ int frameWidth = iter.width; int frameHeight = iter.height; if (frameWidth < 1 || frameHeight < 1) return NULL; int width = extendToCanvas ? (int)_width : frameWidth; int height = extendToCanvas ? (int)_height : frameHeight; if (width > _width || height > _height) return NULL; const uint8_t *payload = iter.fragment.bytes; size_t payloadSize = iter.fragment.size; WebPDecoderConfig config; if (!WebPInitDecoderConfig(&config)) { WebPDemuxReleaseIterator(&iter); return NULL; } if (WebPGetFeatures(payload , payloadSize, &config.input) != VP8_STATUS_OK) { WebPDemuxReleaseIterator(&iter); return NULL; } size_t bitsPerComponent = 8; size_t bitsPerPixel = 32; size_t bytesPerRow = YYImageByteAlign(bitsPerPixel / 8 * width, 32); size_t length = bytesPerRow * height; CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst; //bgrA void *pixels = calloc(1, length); if (!pixels) { WebPDemuxReleaseIterator(&iter); return NULL; } config.output.colorspace = MODE_bgrA; config.output.is_external_memory = 1; config.output.u.RGBA.rgba = pixels; config.output.u.RGBA.stride = (int)bytesPerRow; config.output.u.RGBA.size = length; VP8StatusCode result = WebPDecode(payload, payloadSize, &config); // decode if ((result != VP8_STATUS_OK) && (result != VP8_STATUS_NOT_ENOUGH_DATA)) { WebPDemuxReleaseIterator(&iter); free(pixels); return NULL; } WebPDemuxReleaseIterator(&iter); if (extendToCanvas && (iter.x_offset != 0 || iter.y_offset != 0)) { void *tmp = calloc(1, length); if (tmp) { vImage_Buffer src = {pixels, height, width, bytesPerRow}; vImage_Buffer dest = {tmp, height, width, bytesPerRow}; vImage_CGAffineTransform transform = {1, 0, 0, 1, iter.x_offset, -iter.y_offset}; uint8_t backColor[4] = {0}; vImage_Error error = vImageAffineWarpCG_ARGB8888(&src, &dest, NULL, &transform, backColor, kvImageBackgroundColorFill); if (error == kvImageNoError) { memcpy(pixels, tmp, length); } free(tmp); } } CGDataProviderRef provider = CGDataProviderCreateWithData(pixels, pixels, length, YYCGDataProviderReleaseDataCallback); if (!provider) { free(pixels); return NULL; } pixels = NULL; // hold by provider CGImageRef image = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, YYCGColorSpaceGetDeviceRGB(), bitmapInfo, provider, NULL, false, kCGRenderingIntentDefault); CFRelease(provider); if (decoded) *decoded = YES; return image; } #endif return NULL; } - (BOOL)_createBlendContextIfNeeded { if (!_blendCanvas) { _blendFrameIndex = NSNotFound; _blendCanvas = CGBitmapContextCreate(NULL, _width, _height, 8, 0, YYCGColorSpaceGetDeviceRGB(), kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); } BOOL suc = _blendCanvas != NULL; return suc; } - (void)_blendImageWithFrame:(_YYImageDecoderFrame *)frame { if (frame.dispose == YYImageDisposePrevious) { // nothing } else if (frame.dispose == YYImageDisposeBackground) { CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); } else { // no dispose if (frame.blend == YYImageBlendOver) { CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; if (unblendImage) { CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); CFRelease(unblendImage); } } else { CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; if (unblendImage) { CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); CFRelease(unblendImage); } } } } - (CGImageRef)_newBlendedImageWithFrame:(_YYImageDecoderFrame *)frame CF_RETURNS_RETAINED{ CGImageRef imageRef = NULL; if (frame.dispose == YYImageDisposePrevious) { if (frame.blend == YYImageBlendOver) { CGImageRef previousImage = CGBitmapContextCreateImage(_blendCanvas); CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; if (unblendImage) { CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); CFRelease(unblendImage); } imageRef = CGBitmapContextCreateImage(_blendCanvas); CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height)); if (previousImage) { CGContextDrawImage(_blendCanvas, CGRectMake(0, 0, _width, _height), previousImage); CFRelease(previousImage); } } else { CGImageRef previousImage = CGBitmapContextCreateImage(_blendCanvas); CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; if (unblendImage) { CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); CFRelease(unblendImage); } imageRef = CGBitmapContextCreateImage(_blendCanvas); CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height)); if (previousImage) { CGContextDrawImage(_blendCanvas, CGRectMake(0, 0, _width, _height), previousImage); CFRelease(previousImage); } } } else if (frame.dispose == YYImageDisposeBackground) { if (frame.blend == YYImageBlendOver) { CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; if (unblendImage) { CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); CFRelease(unblendImage); } imageRef = CGBitmapContextCreateImage(_blendCanvas); CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); } else { CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; if (unblendImage) { CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); CFRelease(unblendImage); } imageRef = CGBitmapContextCreateImage(_blendCanvas); CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); } } else { // no dispose if (frame.blend == YYImageBlendOver) { CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; if (unblendImage) { CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); CFRelease(unblendImage); } imageRef = CGBitmapContextCreateImage(_blendCanvas); } else { CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; if (unblendImage) { CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); CFRelease(unblendImage); } imageRef = CGBitmapContextCreateImage(_blendCanvas); } } return imageRef; } @end //////////////////////////////////////////////////////////////////////////////// #pragma mark - Encoder @implementation YYImageEncoder { NSMutableArray *_images; NSMutableArray *_durations; } - (instancetype)init { @throw [NSException exceptionWithName:@"YYImageEncoder init error" reason:@"YYImageEncoder must be initialized with a type. Use 'initWithType:' instead." userInfo:nil]; return [self initWithType:YYImageTypeUnknown]; } - (instancetype)initWithType:(YYImageType)type { if (type == YYImageTypeUnknown || type >= YYImageTypeOther) return nil; #if !YYIMAGE_WEBP_ENABLED if (type == YYImageTypeWebP) return nil; #endif self = [super init]; if (!self) return nil; _type = type; _images = [NSMutableArray new]; _durations = [NSMutableArray new]; switch (type) { case YYImageTypeJPEG: case YYImageTypeJPEG2000: { _quality = 0.9; } break; case YYImageTypeTIFF: case YYImageTypeBMP: case YYImageTypeGIF: case YYImageTypeICO: case YYImageTypeICNS: case YYImageTypePNG: { _quality = 1; _lossless = YES; } break; case YYImageTypeWebP: { _quality = 0.8; } break; default: break; } return self; } - (void)setQuality:(CGFloat)quality { _quality = quality < 0 ? 0 : quality > 1 ? 1 : quality; } - (void)addImage:(UIImage *)image duration:(NSTimeInterval)duration { if (!image.CGImage) return; duration = duration < 0 ? 0 : duration; [_images addObject:image]; [_durations addObject:@(duration)]; } - (void)addImageWithData:(NSData *)data duration:(NSTimeInterval)duration { if (data.length == 0) return; duration = duration < 0 ? 0 : duration; [_images addObject:data]; [_durations addObject:@(duration)]; } - (void)addImageWithFile:(NSString *)path duration:(NSTimeInterval)duration { if (path.length == 0) return; duration = duration < 0 ? 0 : duration; NSURL *url = [NSURL URLWithString:path]; if (!url) return; [_images addObject:url]; [_durations addObject:@(duration)]; } - (BOOL)_imageIOAvaliable { switch (_type) { case YYImageTypeJPEG: case YYImageTypeJPEG2000: case YYImageTypeTIFF: case YYImageTypeBMP: case YYImageTypeICO: case YYImageTypeICNS: case YYImageTypeGIF: { return _images.count > 0; } break; case YYImageTypePNG: { return _images.count == 1; } break; case YYImageTypeWebP: { return NO; } break; default: return NO; } } - (CGImageDestinationRef)_newImageDestination:(id)dest imageCount:(NSUInteger)count CF_RETURNS_RETAINED { if (!dest) return nil; CGImageDestinationRef destination = NULL; if ([dest isKindOfClass:[NSString class]]) { NSURL *url = [[NSURL alloc] initFileURLWithPath:dest]; if (url) { destination = CGImageDestinationCreateWithURL((CFURLRef)url, YYImageTypeToUTType(_type), count, NULL); } } else if ([dest isKindOfClass:[NSMutableData class]]) { destination = CGImageDestinationCreateWithData((CFMutableDataRef)dest, YYImageTypeToUTType(_type), count, NULL); } return destination; } - (void)_encodeImageWithDestination:(CGImageDestinationRef)destination imageCount:(NSUInteger)count { if (_type == YYImageTypeGIF) { NSDictionary *gifProperty = @{(__bridge id)kCGImagePropertyGIFDictionary: @{(__bridge id)kCGImagePropertyGIFLoopCount: @(_loopCount)}}; CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)gifProperty); } for (int i = 0; i < count; i++) { @autoreleasepool { id imageSrc = _images[i]; NSDictionary *frameProperty = NULL; if (_type == YYImageTypeGIF && count > 1) { frameProperty = @{(NSString *)kCGImagePropertyGIFDictionary : @{(NSString *) kCGImagePropertyGIFDelayTime:_durations[i]}}; } else { frameProperty = @{(id)kCGImageDestinationLossyCompressionQuality : @(_quality)}; } if ([imageSrc isKindOfClass:[UIImage class]]) { UIImage *image = imageSrc; if (image.imageOrientation != UIImageOrientationUp && image.CGImage) { CGBitmapInfo info = CGImageGetBitmapInfo(image.CGImage) | CGImageGetAlphaInfo(image.CGImage); CGImageRef rotated = YYCGImageCreateCopyWithOrientation(image.CGImage, image.imageOrientation, info); if (rotated) { image = [UIImage imageWithCGImage:rotated]; CFRelease(rotated); } } if (image.CGImage) CGImageDestinationAddImage(destination, ((UIImage *)imageSrc).CGImage, (CFDictionaryRef)frameProperty); } else if ([imageSrc isKindOfClass:[NSURL class]]) { CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)imageSrc, NULL); if (source) { CGImageDestinationAddImageFromSource(destination, source, i, (CFDictionaryRef)frameProperty); CFRelease(source); } } else if ([imageSrc isKindOfClass:[NSData class]]) { CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageSrc, NULL); if (source) { CGImageDestinationAddImageFromSource(destination, source, i, (CFDictionaryRef)frameProperty); CFRelease(source); } } } } } - (CGImageRef)_newCGImageFromIndex:(NSUInteger)index decoded:(BOOL)decoded CF_RETURNS_RETAINED { UIImage *image = nil; id imageSrc= _images[index]; if ([imageSrc isKindOfClass:[UIImage class]]) { image = imageSrc; } else if ([imageSrc isKindOfClass:[NSURL class]]) { image = [UIImage imageWithContentsOfFile:((NSURL *)imageSrc).absoluteString]; } else if ([imageSrc isKindOfClass:[NSData class]]) { image = [UIImage imageWithData:imageSrc]; } if (!image) return NULL; CGImageRef imageRef = image.CGImage; if (!imageRef) return NULL; if (image.imageOrientation != UIImageOrientationUp) { return YYCGImageCreateCopyWithOrientation(imageRef, image.imageOrientation, kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); } if (decoded) { return YYCGImageCreateDecodedCopy(imageRef, YES); } return (CGImageRef)CFRetain(imageRef); } - (NSData *)_encodeWithImageIO { NSMutableData *data = [NSMutableData new]; NSUInteger count = _type == YYImageTypeGIF ? _images.count : 1; CGImageDestinationRef destination = [self _newImageDestination:data imageCount:count]; BOOL suc = NO; if (destination) { [self _encodeImageWithDestination:destination imageCount:count]; suc = CGImageDestinationFinalize(destination); CFRelease(destination); } if (suc && data.length > 0) { return data; } else { return nil; } } - (BOOL)_encodeWithImageIO:(NSString *)path { NSUInteger count = _type == YYImageTypeGIF ? _images.count : 1; CGImageDestinationRef destination = [self _newImageDestination:path imageCount:count]; BOOL suc = NO; if (destination) { [self _encodeImageWithDestination:destination imageCount:count]; suc = CGImageDestinationFinalize(destination); CFRelease(destination); } return suc; } - (NSData *)_encodeAPNG { // encode APNG (ImageIO doesn't support APNG encoding, so we use a custom encoder) NSMutableArray *pngDatas = [NSMutableArray new]; NSMutableArray *pngSizes = [NSMutableArray new]; NSUInteger canvasWidth = 0, canvasHeight = 0; for (int i = 0; i < _images.count; i++) { CGImageRef decoded = [self _newCGImageFromIndex:i decoded:YES]; if (!decoded) return nil; CGSize size = CGSizeMake(CGImageGetWidth(decoded), CGImageGetHeight(decoded)); [pngSizes addObject:[NSValue valueWithCGSize:size]]; if (canvasWidth < size.width) canvasWidth = size.width; if (canvasHeight < size.height) canvasHeight = size.height; CFDataRef frameData = YYCGImageCreateEncodedData(decoded, YYImageTypePNG, 1); CFRelease(decoded); if (!frameData) return nil; [pngDatas addObject:(__bridge id)(frameData)]; CFRelease(frameData); if (size.width < 1 || size.height < 1) return nil; } CGSize firstFrameSize = [(NSValue *)[pngSizes firstObject] CGSizeValue]; if (firstFrameSize.width < canvasWidth || firstFrameSize.height < canvasHeight) { CGImageRef decoded = [self _newCGImageFromIndex:0 decoded:YES]; if (!decoded) return nil; CGContextRef context = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, YYCGColorSpaceGetDeviceRGB(), kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); if (!context) { CFRelease(decoded); return nil; } CGContextDrawImage(context, CGRectMake(0, canvasHeight - firstFrameSize.height, firstFrameSize.width, firstFrameSize.height), decoded); CFRelease(decoded); CGImageRef extendedImage = CGBitmapContextCreateImage(context); CFRelease(context); if (!extendedImage) return nil; CFDataRef frameData = YYCGImageCreateEncodedData(extendedImage, YYImageTypePNG, 1); if (!frameData) { CFRelease(extendedImage); return nil; } pngDatas[0] = (__bridge id)(frameData); CFRelease(frameData); } NSData *firstFrameData = pngDatas[0]; yy_png_info *info = yy_png_info_create(firstFrameData.bytes, (uint32_t)firstFrameData.length); if (!info) return nil; NSMutableData *result = [NSMutableData new]; BOOL insertBefore = NO, insertAfter = NO; uint32_t apngSequenceIndex = 0; uint32_t png_header[2]; png_header[0] = YY_FOUR_CC(0x89, 0x50, 0x4E, 0x47); png_header[1] = YY_FOUR_CC(0x0D, 0x0A, 0x1A, 0x0A); [result appendBytes:png_header length:8]; for (int i = 0; i < info->chunk_num; i++) { yy_png_chunk_info *chunk = info->chunks + i; if (!insertBefore && chunk->fourcc == YY_FOUR_CC('I', 'D', 'A', 'T')) { insertBefore = YES; // insert acTL (APNG Control) uint32_t acTL[5] = {0}; acTL[0] = yy_swap_endian_uint32(8); //length acTL[1] = YY_FOUR_CC('a', 'c', 'T', 'L'); // fourcc acTL[2] = yy_swap_endian_uint32((uint32_t)pngDatas.count); // num frames acTL[3] = yy_swap_endian_uint32((uint32_t)_loopCount); // num plays acTL[4] = yy_swap_endian_uint32((uint32_t)crc32(0, (const Bytef *)(acTL + 1), 12)); //crc32 [result appendBytes:acTL length:20]; // insert fcTL (first frame control) yy_png_chunk_fcTL chunk_fcTL = {0}; chunk_fcTL.sequence_number = apngSequenceIndex; chunk_fcTL.width = (uint32_t)firstFrameSize.width; chunk_fcTL.height = (uint32_t)firstFrameSize.height; yy_png_delay_to_fraction([(NSNumber *)_durations[0] doubleValue], &chunk_fcTL.delay_num, &chunk_fcTL.delay_den); chunk_fcTL.delay_num = chunk_fcTL.delay_num; chunk_fcTL.delay_den = chunk_fcTL.delay_den; chunk_fcTL.dispose_op = YY_PNG_DISPOSE_OP_BACKGROUND; chunk_fcTL.blend_op = YY_PNG_BLEND_OP_SOURCE; uint8_t fcTL[38] = {0}; *((uint32_t *)fcTL) = yy_swap_endian_uint32(26); //length *((uint32_t *)(fcTL + 4)) = YY_FOUR_CC('f', 'c', 'T', 'L'); // fourcc yy_png_chunk_fcTL_write(&chunk_fcTL, fcTL + 8); *((uint32_t *)(fcTL + 34)) = yy_swap_endian_uint32((uint32_t)crc32(0, (const Bytef *)(fcTL + 4), 30)); [result appendBytes:fcTL length:38]; apngSequenceIndex++; } if (!insertAfter && insertBefore && chunk->fourcc != YY_FOUR_CC('I', 'D', 'A', 'T')) { insertAfter = YES; // insert fcTL and fdAT (APNG frame control and data) for (int i = 1; i < pngDatas.count; i++) { NSData *frameData = pngDatas[i]; yy_png_info *frame = yy_png_info_create(frameData.bytes, (uint32_t)frameData.length); if (!frame) { yy_png_info_release(info); return nil; } // insert fcTL (first frame control) yy_png_chunk_fcTL chunk_fcTL = {0}; chunk_fcTL.sequence_number = apngSequenceIndex; chunk_fcTL.width = frame->header.width; chunk_fcTL.height = frame->header.height; yy_png_delay_to_fraction([(NSNumber *)_durations[0] doubleValue], &chunk_fcTL.delay_num, &chunk_fcTL.delay_den); chunk_fcTL.delay_num = chunk_fcTL.delay_num; chunk_fcTL.delay_den = chunk_fcTL.delay_den; chunk_fcTL.dispose_op = YY_PNG_DISPOSE_OP_BACKGROUND; chunk_fcTL.blend_op = YY_PNG_BLEND_OP_SOURCE; uint8_t fcTL[38] = {0}; *((uint32_t *)fcTL) = yy_swap_endian_uint32(26); //length *((uint32_t *)(fcTL + 4)) = YY_FOUR_CC('f', 'c', 'T', 'L'); // fourcc yy_png_chunk_fcTL_write(&chunk_fcTL, fcTL + 8); *((uint32_t *)(fcTL + 34)) = yy_swap_endian_uint32((uint32_t)crc32(0, (const Bytef *)(fcTL + 4), 30)); [result appendBytes:fcTL length:38]; apngSequenceIndex++; // insert fdAT (frame data) for (int d = 0; d < frame->chunk_num; d++) { yy_png_chunk_info *dchunk = frame->chunks + d; if (dchunk->fourcc == YY_FOUR_CC('I', 'D', 'A', 'T')) { uint32_t length = yy_swap_endian_uint32(dchunk->length + 4); [result appendBytes:&length length:4]; //length uint32_t fourcc = YY_FOUR_CC('f', 'd', 'A', 'T'); [result appendBytes:&fourcc length:4]; //fourcc uint32_t sq = yy_swap_endian_uint32(apngSequenceIndex); [result appendBytes:&sq length:4]; //data (sq) [result appendBytes:(((uint8_t *)frameData.bytes) + dchunk->offset + 8) length:dchunk->length]; //data uint8_t *bytes = ((uint8_t *)result.bytes) + result.length - dchunk->length - 8; uint32_t crc = yy_swap_endian_uint32((uint32_t)crc32(0, bytes, dchunk->length + 8)); [result appendBytes:&crc length:4]; //crc apngSequenceIndex++; } } yy_png_info_release(frame); } } [result appendBytes:((uint8_t *)firstFrameData.bytes) + chunk->offset length:chunk->length + 12]; } yy_png_info_release(info); return result; } - (NSData *)_encodeWebP { #if YYIMAGE_WEBP_ENABLED // encode webp NSMutableArray *webpDatas = [NSMutableArray new]; for (NSUInteger i = 0; i < _images.count; i++) { CGImageRef image = [self _newCGImageFromIndex:i decoded:NO]; if (!image) return nil; CFDataRef frameData = YYCGImageCreateEncodedWebPData(image, _lossless, _quality, 4, YYImagePresetDefault); CFRelease(image); if (!frameData) return nil; [webpDatas addObject:(__bridge id)frameData]; CFRelease(frameData); } if (webpDatas.count == 1) { return webpDatas.firstObject; } else { // multi-frame webp WebPMux *mux = WebPMuxNew(); if (!mux) return nil; for (NSUInteger i = 0; i < _images.count; i++) { NSData *data = webpDatas[i]; NSNumber *duration = _durations[i]; WebPMuxFrameInfo frame = {0}; frame.bitstream.bytes = data.bytes; frame.bitstream.size = data.length; frame.duration = (int)(duration.floatValue * 1000.0); frame.id = WEBP_CHUNK_ANMF; frame.dispose_method = WEBP_MUX_DISPOSE_BACKGROUND; frame.blend_method = WEBP_MUX_NO_BLEND; if (WebPMuxPushFrame(mux, &frame, 0) != WEBP_MUX_OK) { WebPMuxDelete(mux); return nil; } } WebPMuxAnimParams params = {(uint32_t)0, (int)_loopCount}; if (WebPMuxSetAnimationParams(mux, ¶ms) != WEBP_MUX_OK) { WebPMuxDelete(mux); return nil; } WebPData output_data; WebPMuxError error = WebPMuxAssemble(mux, &output_data); WebPMuxDelete(mux); if (error != WEBP_MUX_OK) { return nil; } NSData *result = [NSData dataWithBytes:output_data.bytes length:output_data.size]; WebPDataClear(&output_data); return result.length ? result : nil; } #else return nil; #endif } - (NSData *)encode { if (_images.count == 0) return nil; if ([self _imageIOAvaliable]) return [self _encodeWithImageIO]; if (_type == YYImageTypePNG) return [self _encodeAPNG]; if (_type == YYImageTypeWebP) return [self _encodeWebP]; return nil; } - (BOOL)encodeToFile:(NSString *)path { if (_images.count == 0 || path.length == 0) return NO; if ([self _imageIOAvaliable]) return [self _encodeWithImageIO:path]; NSData *data = [self encode]; if (!data) return NO; return [data writeToFile:path atomically:YES]; } + (NSData *)encodeImage:(UIImage *)image type:(YYImageType)type quality:(CGFloat)quality { YYImageEncoder *encoder = [[YYImageEncoder alloc] initWithType:type]; encoder.quality = quality; [encoder addImage:image duration:0]; return [encoder encode]; } + (NSData *)encodeImageWithDecoder:(YYImageDecoder *)decoder type:(YYImageType)type quality:(CGFloat)quality { if (!decoder || decoder.frameCount == 0) return nil; YYImageEncoder *encoder = [[YYImageEncoder alloc] initWithType:type]; encoder.quality = quality; for (int i = 0; i < decoder.frameCount; i++) { UIImage *frame = [decoder frameAtIndex:i decodeForDisplay:YES].image; [encoder addImageWithData:UIImagePNGRepresentation(frame) duration:[decoder frameDurationAtIndex:i]]; } return encoder.encode; } @end @implementation UIImage (YYImageCoder) - (instancetype)yy_imageByDecoded { if (self.yy_isDecodedForDisplay) return self; CGImageRef imageRef = self.CGImage; if (!imageRef) return self; CGImageRef newImageRef = YYCGImageCreateDecodedCopy(imageRef, YES); if (!newImageRef) return self; UIImage *newImage = [[self.class alloc] initWithCGImage:newImageRef scale:self.scale orientation:self.imageOrientation]; CGImageRelease(newImageRef); if (!newImage) newImage = self; // decode failed, return self. newImage.yy_isDecodedForDisplay = YES; return newImage; } - (BOOL)yy_isDecodedForDisplay { if (self.images.count > 1) return YES; NSNumber *num = objc_getAssociatedObject(self, @selector(yy_isDecodedForDisplay)); return [num boolValue]; } - (void)setYy_isDecodedForDisplay:(BOOL)isDecodedForDisplay { objc_setAssociatedObject(self, @selector(yy_isDecodedForDisplay), @(isDecodedForDisplay), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)yy_saveToAlbumWithCompletionBlock:(void(^)(NSURL *assetURL, NSError *error))completionBlock { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSData *data = [self _yy_dataRepresentationForSystem:YES]; ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; [library writeImageDataToSavedPhotosAlbum:data metadata:nil completionBlock:^(NSURL *assetURL, NSError *error){ if (!completionBlock) return; if (pthread_main_np()) { completionBlock(assetURL, error); } else { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(assetURL, error); }); } }]; }); } - (NSData *)yy_imageDataRepresentation { return [self _yy_dataRepresentationForSystem:NO]; } /// @param forSystem YES: used for system album (PNG/JPEG/GIF), NO: used for YYImage (PNG/JPEG/GIF/WebP) - (NSData *)_yy_dataRepresentationForSystem:(BOOL)forSystem { NSData *data = nil; if ([self isKindOfClass:[YYImage class]]) { YYImage *image = (id)self; if (image.animatedImageData) { if (forSystem) { // system only support GIF and PNG if (image.animatedImageType == YYImageTypeGIF || image.animatedImageType == YYImageTypePNG) { data = image.animatedImageData; } } else { data = image.animatedImageData; } } } if (!data) { CGImageRef imageRef = self.CGImage ? (CGImageRef)CFRetain(self.CGImage) : nil; if (imageRef) { CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask; BOOL hasAlpha = NO; if (alphaInfo == kCGImageAlphaPremultipliedLast || alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaLast || alphaInfo == kCGImageAlphaFirst) { hasAlpha = YES; } if (self.imageOrientation != UIImageOrientationUp) { CGImageRef rotated = YYCGImageCreateCopyWithOrientation(imageRef, self.imageOrientation, bitmapInfo | alphaInfo); if (rotated) { CFRelease(imageRef); imageRef = rotated; } } @autoreleasepool { UIImage *newImage = [UIImage imageWithCGImage:imageRef]; if (newImage) { if (hasAlpha) { data = UIImagePNGRepresentation([UIImage imageWithCGImage:imageRef]); } else { data = UIImageJPEGRepresentation([UIImage imageWithCGImage:imageRef], 0.9); // same as Apple's example } } } CFRelease(imageRef); } } if (!data) { data = UIImagePNGRepresentation(self); } return data; } @end ================================================ FILE: Demo/YYTextDemo/YYImage/YYSpriteSheetImage.h ================================================ // // YYSpriteImage.h // YYImage // // 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 #elif __has_include() #import #else #import "YYAnimatedImageView.h" #endif NS_ASSUME_NONNULL_BEGIN /** An image to display sprite sheet animation. @discussion It is a fully compatible `UIImage` subclass. The animation can be played by YYAnimatedImageView. Sample Code: // 8 * 12 sprites in a single sheet image UIImage *spriteSheet = [UIImage imageNamed:@"sprite-sheet"]; NSMutableArray *contentRects = [NSMutableArray new]; NSMutableArray *durations = [NSMutableArray new]; for (int j = 0; j < 12; j++) { for (int i = 0; i < 8; i++) { CGRect rect; rect.size = CGSizeMake(img.size.width / 8, img.size.height / 12); rect.origin.x = img.size.width / 8 * i; rect.origin.y = img.size.height / 12 * j; [contentRects addObject:[NSValue valueWithCGRect:rect]]; [durations addObject:@(1 / 60.0)]; } } YYSpriteSheetImage *sprite; sprite = [[YYSpriteSheetImage alloc] initWithSpriteSheetImage:img contentRects:contentRects frameDurations:durations loopCount:0]; YYAnimatedImageView *imgView = [YYAnimatedImageView new]; imgView.size = CGSizeMake(img.size.width / 8, img.size.height / 12); imgView.image = sprite; @discussion It can also be used to display single frame in sprite sheet image. Sample Code: YYSpriteSheetImage *sheet = ...; UIImageView *imageView = ...; imageView.image = sheet; imageView.layer.contentsRect = [sheet contentsRectForCALayerAtIndex:6]; */ @interface YYSpriteSheetImage : UIImage /** Creates and returns an image object. @param image The sprite sheet image (contains all frames). @param contentRects The sprite sheet image frame rects in the image coordinates. The rectangle should not outside the image's bounds. The objects in this array should be created with [NSValue valueWithCGRect:]. @param frameDurations The sprite sheet image frame's durations in seconds. The objects in this array should be NSNumber. @param loopCount Animation loop count, 0 means infinite looping. @return An image object, or nil if an error occurs. */ - (nullable instancetype)initWithSpriteSheetImage:(UIImage *)image contentRects:(NSArray *)contentRects frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount; @property (nonatomic, readonly) NSArray *contentRects; @property (nonatomic, readonly) NSArray *frameDurations; @property (nonatomic, readonly) NSUInteger loopCount; /** Get the contents rect for CALayer. See "contentsRect" property in CALayer for more information. @param index Index of frame. @return Contents Rect. */ - (CGRect)contentsRectForCALayerAtIndex:(NSUInteger)index; @end NS_ASSUME_NONNULL_END ================================================ FILE: Demo/YYTextDemo/YYImage/YYSpriteSheetImage.m ================================================ // // YYSpriteImage.m // YYImage // // 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 "YYSpriteSheetImage.h" @implementation YYSpriteSheetImage - (instancetype)initWithSpriteSheetImage:(UIImage *)image contentRects:(NSArray *)contentRects frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount { if (!image.CGImage) return nil; if (contentRects.count < 1 || frameDurations.count < 1) return nil; if (contentRects.count != frameDurations.count) return nil; self = [super initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation]; if (!self) return nil; _contentRects = contentRects.copy; _frameDurations = frameDurations.copy; _loopCount = loopCount; return self; } - (CGRect)contentsRectForCALayerAtIndex:(NSUInteger)index { CGRect layerRect = CGRectMake(0, 0, 1, 1); if (index >= _contentRects.count) return layerRect; CGSize imageSize = self.size; CGRect rect = [self animatedImageContentsRectAtIndex:index]; if (imageSize.width > 0.01 && imageSize.height > 0.01) { layerRect.origin.x = rect.origin.x / imageSize.width; layerRect.origin.y = rect.origin.y / imageSize.height; layerRect.size.width = rect.size.width / imageSize.width; layerRect.size.height = rect.size.height / imageSize.height; layerRect = CGRectIntersection(layerRect, CGRectMake(0, 0, 1, 1)); if (CGRectIsNull(layerRect) || CGRectIsEmpty(layerRect)) { layerRect = CGRectMake(0, 0, 1, 1); } } return layerRect; } #pragma mark @protocol YYAnimatedImage - (NSUInteger)animatedImageFrameCount { return _contentRects.count; } - (NSUInteger)animatedImageLoopCount { return _loopCount; } - (NSUInteger)animatedImageBytesPerFrame { return 0; } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { return self; } - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index { if (index >= _frameDurations.count) return 0; return ((NSNumber *)_frameDurations[index]).doubleValue; } - (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index { if (index >= _contentRects.count) return CGRectZero; return ((NSValue *)_contentRects[index]).CGRectValue; } @end ================================================ FILE: Demo/YYTextDemo/YYTextAsyncExample.h ================================================ // // YYTextAsyncExample.h // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextAsyncExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextAsyncExample.m ================================================ // // YYTextAsyncExample.m // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextAsyncExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "CALayer+YYAdd.h" #import "UIControl+YYAdd.h" #import "YYTextExampleHelper.h" #import "YYFPSLabel.h" #define kCellHeight 34 @interface YYTextAsyncExampleCell : UITableViewCell @property (nonatomic, assign) BOOL async; - (void)setAyncText:(NSAttributedString *)text; @end @implementation YYTextAsyncExampleCell { UILabel *_uiLabel; YYLabel *_yyLabel; } - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; _uiLabel = [UILabel new]; _uiLabel.font = [UIFont systemFontOfSize:8]; _uiLabel.numberOfLines = 0; _uiLabel.size = CGSizeMake(kScreenWidth, kCellHeight); _yyLabel = [YYLabel new]; _yyLabel.font = _uiLabel.font; _yyLabel.numberOfLines = _uiLabel.numberOfLines; _yyLabel.size = _uiLabel.size; _yyLabel.displaysAsynchronously = YES; /// enable async display _yyLabel.hidden = YES; [self.contentView addSubview:_uiLabel]; [self.contentView addSubview:_yyLabel]; return self; } - (void)setAsync:(BOOL)async { if (_async == async) return; _async = async; _uiLabel.hidden = async; _yyLabel.hidden = !async; } - (void)setAyncText:(id)text { if (_async) { _yyLabel.layer.contents = nil; _yyLabel.textLayout = text; } else { _uiLabel.attributedText = text; } } @end @interface YYTextAsyncExample () @property (nonatomic, assign) BOOL async; @property (nonatomic, strong) NSArray *strings; @property (nonatomic, strong) NSArray *layouts; @property (nonatomic, strong) UITableView *tableView; @end @implementation YYTextAsyncExample - (void)viewDidLoad { [super viewDidLoad]; self.tableView = [UITableView new]; self.tableView.frame = self.view.bounds; self.tableView.delegate = self; self.tableView.dataSource = self; [self.tableView registerClass:[YYTextAsyncExampleCell class] forCellReuseIdentifier:@"id"]; [self.view addSubview:self.tableView]; NSMutableArray *strings = [NSMutableArray new]; NSMutableArray *layouts = [NSMutableArray new]; for (int i = 0; i < 300; i++) { NSString *str = [NSString stringWithFormat:@"%d Async Display Test ✺◟(∗❛ัᴗ❛ั∗)◞✺ ✺◟(∗❛ัᴗ❛ั∗)◞✺ 😀😖😐😣😡🚖🚌🚋🎊💖💗💛💙🏨🏦🏫 Async Display Test ✺◟(∗❛ัᴗ❛ั∗)◞✺ ✺◟(∗❛ัᴗ❛ั∗)◞✺ 😀😖😐😣😡🚖🚌🚋🎊💖💗💛💙🏨🏦🏫",i]; NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:str]; text.yy_font = [UIFont systemFontOfSize:10]; text.yy_lineSpacing = 0; text.yy_strokeWidth = @(-3); text.yy_strokeColor = [UIColor redColor]; text.yy_lineHeightMultiple = 1; text.yy_maximumLineHeight = 12; text.yy_minimumLineHeight = 12; NSShadow *shadow = [NSShadow new]; shadow.shadowBlurRadius = 1; shadow.shadowColor = [UIColor redColor]; shadow.shadowOffset = CGSizeMake(0, 1); [strings addObject:text]; // it better to do layout in background queue... YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kScreenWidth, kCellHeight)]; YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text]; [layouts addObject:layout]; } self.strings = strings; self.layouts = layouts; UIView *toolbar; if ([UIVisualEffectView class]) { toolbar = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]]; } else { toolbar = [UIToolbar new]; } toolbar.size = CGSizeMake(kScreenWidth, 40); toolbar.top = kiOS7Later ? 64 : 0; [self.view addSubview:toolbar]; YYFPSLabel *fps = [YYFPSLabel new]; fps.centerY = toolbar.height / 2; fps.left = 5; [toolbar addSubview:fps]; UILabel *label = [UILabel new]; label.backgroundColor = [UIColor clearColor]; label.text = @"UILabel/YYLabel(Async): "; label.font = [UIFont systemFontOfSize:14]; [label sizeToFit]; label.centerY = toolbar.height / 2; label.left = fps.right + 10; [toolbar addSubview:label]; UISwitch *switcher = [UISwitch new]; [switcher sizeToFit]; switcher.centerY = toolbar.height / 2; switcher.left = label.right + (kiOS7Later ? 10 : -10); switcher.layer.transformScale = 0.7; __weak typeof(self) _self = self; [switcher addBlockForControlEvents:UIControlEventValueChanged block:^(UISwitch *switcher) { typeof(_self) self = _self; if (!self) return; [self setAsync:switcher.isOn]; }]; [toolbar addSubview:switcher]; } - (void)setAsync:(BOOL)async { _async = async; [self.tableView.visibleCells enumerateObjectsUsingBlock:^(YYTextAsyncExampleCell *cell, NSUInteger idx, BOOL *stop) { cell.async = async; NSIndexPath *indexPath = [self.tableView indexPathForCell:cell]; if (_async) { [cell setAyncText:_layouts[indexPath.row]]; } else { [cell setAyncText:_strings[indexPath.row]]; } }]; } #pragma mark - Table view data source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return _strings.count; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return kCellHeight; } - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath { return NO; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { YYTextAsyncExampleCell *cell = [tableView dequeueReusableCellWithIdentifier:@"id" forIndexPath:indexPath]; cell.async = _async; if (_async) { [cell setAyncText:_layouts[indexPath.row]]; } else { [cell setAyncText:_strings[indexPath.row]]; } return cell; } @end ================================================ FILE: Demo/YYTextDemo/YYTextAttachmentExample.h ================================================ // // YYTextAttachmentExample.h // YYKitExample // // Created by ibireme on 15/8/21. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextAttachmentExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextAttachmentExample.m ================================================ // // YYTextAttachmentExample.m // YYKitExample // // Created by ibireme on 15/8/21. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextAttachmentExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "YYTextExampleHelper.h" #import "YYGestureRecognizer.h" @interface YYTextAttachmentExample () @property (nonatomic, strong) YYLabel *label; @end @implementation YYTextAttachmentExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; [YYTextExampleHelper addDebugOptionToViewController:self]; NSMutableAttributedString *text = [NSMutableAttributedString new]; UIFont *font = [UIFont systemFontOfSize:16]; { NSString *title = @"This is UIImage attachment:"; [text appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:nil]]; UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"]; image = [UIImage imageWithCGImage:image.CGImage scale:2 orientation:UIImageOrientationUp]; NSMutableAttributedString *attachText = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString:attachText]; [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:nil]]; } { NSString *title = @"This is UIView attachment: "; [text appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:nil]]; UISwitch *switcher = [UISwitch new]; [switcher sizeToFit]; NSMutableAttributedString *attachText = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeCenter attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString:attachText]; [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:nil]]; } { NSString *title = @"This is Animated Image attachment:"; [text appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:nil]]; NSArray *names = @[@"001", @"022", @"019",@"056",@"085"]; for (NSString *name in names) { NSString *path = [[NSBundle mainBundle] pathForScaledResource:name ofType:@"gif" inDirectory:@"EmoticonQQ.bundle"]; NSData *data = [NSData dataWithContentsOfFile:path]; YYImage *image = [YYImage imageWithData:data scale:2]; image.preloadAllAnimatedImageFrames = YES; YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image]; NSMutableAttributedString *attachText = [NSMutableAttributedString yy_attachmentStringWithContent:imageView contentMode:UIViewContentModeCenter attachmentSize:imageView.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString:attachText]; } YYImage *image = [YYImage imageNamed:@"pia"]; image.preloadAllAnimatedImageFrames = YES; YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image]; imageView.autoPlayAnimatedImage = NO; [imageView startAnimating]; NSMutableAttributedString *attachText = [NSMutableAttributedString yy_attachmentStringWithContent:imageView contentMode:UIViewContentModeCenter attachmentSize:imageView.size alignToFont:font alignment:YYTextVerticalAlignmentBottom]; [text appendAttributedString:attachText]; [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:nil]]; } text.yy_font = font; _label = [YYLabel new]; _label.userInteractionEnabled = YES; _label.numberOfLines = 0; _label.textVerticalAlignment = YYTextVerticalAlignmentTop; _label.size = CGSizeMake(260, 260); _label.center = CGPointMake(self.view.width / 2, self.view.height / 2); _label.attributedText = text; [self addSeeMoreButton]; [self.view addSubview:_label]; _label.layer.borderWidth = 0.5; _label.layer.borderColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:1.000].CGColor; __weak typeof(_label) wlabel = _label; UIView *dot = [self newDotView]; dot.center = CGPointMake(_label.width, _label.height); dot.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin; [_label addSubview:dot]; YYGestureRecognizer *gesture = [YYGestureRecognizer new]; gesture.action = ^(YYGestureRecognizer *gesture, YYGestureRecognizerState state) { if (state != YYGestureRecognizerStateMoved) return; CGFloat width = gesture.currentPoint.x; CGFloat height = gesture.currentPoint.y; wlabel.width = width < 30 ? 30 : width; wlabel.height = height < 30 ? 30 : height; }; gesture.delegate = self; [_label addGestureRecognizer:gesture]; } - (void)addSeeMoreButton { __weak typeof(self) _self = self; NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"...more"]; YYTextHighlight *hi = [YYTextHighlight new]; [hi setColor:[UIColor colorWithRed:0.578 green:0.790 blue:1.000 alpha:1.000]]; hi.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { YYLabel *label = _self.label; [label sizeToFit]; }; [text yy_setColor:[UIColor colorWithRed:0.000 green:0.449 blue:1.000 alpha:1.000] range:[text.string rangeOfString:@"more"]]; [text yy_setTextHighlight:hi range:[text.string rangeOfString:@"more"]]; text.yy_font = _label.font; YYLabel *seeMore = [YYLabel new]; seeMore.attributedText = text; [seeMore sizeToFit]; NSAttributedString *truncationToken = [NSAttributedString yy_attachmentStringWithContent:seeMore contentMode:UIViewContentModeCenter attachmentSize:seeMore.size alignToFont:text.yy_font alignment:YYTextVerticalAlignmentCenter]; _label.truncationToken = truncationToken; } - (UIView *)newDotView { UIView *view = [UIView new]; view.size = CGSizeMake(50, 50); UIView *dot = [UIView new]; dot.size = CGSizeMake(10, 10); dot.backgroundColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:1.000]; dot.clipsToBounds = YES; dot.layer.cornerRadius = dot.height / 2; dot.center = CGPointMake(view.width / 2, view.height / 2); [view addSubview:dot]; return view; } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { CGPoint p = [gestureRecognizer locationInView:_label]; if (p.x < _label.width - 20) return NO; if (p.y < _label.height - 20) return NO; return YES; } @end ================================================ FILE: Demo/YYTextDemo/YYTextAttributeExample.h ================================================ // // YYTextAttributeExample.h // YYKitExample // // Created by ibireme on 15/8/19. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextAttributeExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextAttributeExample.m ================================================ // // YYTextAttributeExample.m // YYKitExample // // Created by ibireme on 15/8/19. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextAttributeExample.h" #import "YYText.h" #import "YYTextExampleHelper.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSString+YYAdd.h" @implementation YYTextAttributeExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; __weak typeof(self) _self = self; [YYTextExampleHelper addDebugOptionToViewController:self]; NSMutableAttributedString *text = [NSMutableAttributedString new]; { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Shadow"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; one.yy_color = [UIColor whiteColor]; YYTextShadow *shadow = [YYTextShadow new]; shadow.color = [UIColor colorWithWhite:0.000 alpha:0.490]; shadow.offset = CGSizeMake(0, 1); shadow.radius = 5; one.yy_textShadow = shadow; [text appendAttributedString:one]; [text appendAttributedString:[self padding]]; } { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Inner Shadow"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; one.yy_color = [UIColor whiteColor]; YYTextShadow *shadow = [YYTextShadow new]; shadow.color = [UIColor colorWithWhite:0.000 alpha:0.40]; shadow.offset = CGSizeMake(0, 1); shadow.radius = 1; one.yy_textInnerShadow = shadow; [text appendAttributedString:one]; [text appendAttributedString:[self padding]]; } { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Multiple Shadows"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; one.yy_color = [UIColor colorWithRed:1.000 green:0.795 blue:0.014 alpha:1.000]; YYTextShadow *shadow = [YYTextShadow new]; shadow.color = [UIColor colorWithWhite:0.000 alpha:0.20]; shadow.offset = CGSizeMake(0, -1); shadow.radius = 1.5; YYTextShadow *subShadow = [YYTextShadow new]; subShadow.color = [UIColor colorWithWhite:1 alpha:0.99]; subShadow.offset = CGSizeMake(0, 1); subShadow.radius = 1.5; shadow.subShadow = subShadow; one.yy_textShadow = shadow; YYTextShadow *innerShadow = [YYTextShadow new]; innerShadow.color = [UIColor colorWithRed:0.851 green:0.311 blue:0.000 alpha:0.780]; innerShadow.offset = CGSizeMake(0, 1); innerShadow.radius = 1; one.yy_textInnerShadow = innerShadow; [text appendAttributedString:one]; [text appendAttributedString:[self padding]]; } { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Background Image"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; one.yy_color = [UIColor colorWithRed:1.000 green:0.795 blue:0.014 alpha:1.000]; CGSize size = CGSizeMake(20, 20); UIImage *background = [UIImage yy_imageWithSize:size drawBlock:^(CGContextRef context) { UIColor *c0 = [UIColor colorWithRed:0.054 green:0.879 blue:0.000 alpha:1.000]; UIColor *c1 = [UIColor colorWithRed:0.869 green:1.000 blue:0.030 alpha:1.000]; [c0 setFill]; CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height)); [c1 setStroke]; CGContextSetLineWidth(context, 2); for (int i = 0; i < size.width * 2; i+= 4) { CGContextMoveToPoint(context, i, -2); CGContextAddLineToPoint(context, i - size.height, size.height + 2); } CGContextStrokePath(context); }]; one.yy_color = [UIColor colorWithPatternImage:background]; [text appendAttributedString:one]; [text appendAttributedString:[self padding]]; } { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Border"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; one.yy_color = [UIColor colorWithRed:1.000 green:0.029 blue:0.651 alpha:1.000]; YYTextBorder *border = [YYTextBorder new]; border.strokeColor = [UIColor colorWithRed:1.000 green:0.029 blue:0.651 alpha:1.000]; border.strokeWidth = 3; border.lineStyle = YYTextLineStylePatternCircleDot; border.cornerRadius = 3; border.insets = UIEdgeInsetsMake(0, -4, 0, -4); one.yy_textBackgroundBorder = border; [text appendAttributedString:[self padding]]; [text appendAttributedString:one]; [text appendAttributedString:[self padding]]; [text appendAttributedString:[self padding]]; [text appendAttributedString:[self padding]]; [text appendAttributedString:[self padding]]; } { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Link"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; one.yy_underlineStyle = NSUnderlineStyleSingle; /// 1. you can set a highlight with these code /* one.yy_color = [UIColor colorWithRed:0.093 green:0.492 blue:1.000 alpha:1.000]; YYTextBorder *border = [YYTextBorder new]; border.cornerRadius = 3; border.insets = UIEdgeInsetsMake(-2, -1, -2, -1); border.fillColor = [UIColor colorWithWhite:0.000 alpha:0.220]; YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setBorder:border]; highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { [_self showMessage:[NSString stringWithFormat:@"Tap: %@",[text.string substringWithRange:range]]]; }; [one yy_setTextHighlight:highlight range:one.yy_rangeOfAll]; */ /// 2. or you can use the convenience method [one yy_setTextHighlightRange:one.yy_rangeOfAll color:[UIColor colorWithRed:0.093 green:0.492 blue:1.000 alpha:1.000] backgroundColor:[UIColor colorWithWhite:0.000 alpha:0.220] tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){ [_self showMessage:[NSString stringWithFormat:@"Tap: %@",[text.string substringWithRange:range]]]; }]; [text appendAttributedString:one]; [text appendAttributedString:[self padding]]; } { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Another Link"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; one.yy_color = [UIColor redColor]; YYTextBorder *border = [YYTextBorder new]; border.cornerRadius = 50; border.insets = UIEdgeInsetsMake(0, -10, 0, -10); border.strokeWidth = 0.5; border.strokeColor = one.yy_color; border.lineStyle = YYTextLineStyleSingle; one.yy_textBackgroundBorder = border; YYTextBorder *highlightBorder = border.copy; highlightBorder.strokeWidth = 0; highlightBorder.strokeColor = one.yy_color; highlightBorder.fillColor = one.yy_color; YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setColor:[UIColor whiteColor]]; [highlight setBackgroundBorder:highlightBorder]; highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { [_self showMessage:[NSString stringWithFormat:@"Tap: %@",[text.string substringWithRange:range]]]; }; [one yy_setTextHighlight:highlight range:one.yy_rangeOfAll]; [text appendAttributedString:one]; [text appendAttributedString:[self padding]]; } { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Yet Another Link"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; one.yy_color = [UIColor whiteColor]; YYTextShadow *shadow = [YYTextShadow new]; shadow.color = [UIColor colorWithWhite:0.000 alpha:0.490]; shadow.offset = CGSizeMake(0, 1); shadow.radius = 5; one.yy_textShadow = shadow; YYTextShadow *shadow0 = [YYTextShadow new]; shadow0.color = [UIColor colorWithWhite:0.000 alpha:0.20]; shadow0.offset = CGSizeMake(0, -1); shadow0.radius = 1.5; YYTextShadow *shadow1 = [YYTextShadow new]; shadow1.color = [UIColor colorWithWhite:1 alpha:0.99]; shadow1.offset = CGSizeMake(0, 1); shadow1.radius = 1.5; shadow0.subShadow = shadow1; YYTextShadow *innerShadow0 = [YYTextShadow new]; innerShadow0.color = [UIColor colorWithRed:0.851 green:0.311 blue:0.000 alpha:0.780]; innerShadow0.offset = CGSizeMake(0, 1); innerShadow0.radius = 1; YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setColor:[UIColor colorWithRed:1.000 green:0.795 blue:0.014 alpha:1.000]]; [highlight setShadow:shadow0]; [highlight setInnerShadow:innerShadow0]; [one yy_setTextHighlight:highlight range:one.yy_rangeOfAll]; [text appendAttributedString:one]; } YYLabel *label = [YYLabel new]; label.attributedText = text; label.width = self.view.width; label.height = self.view.height - (kiOS7Later ? 64 : 44); label.top = (kiOS7Later ? 64 : 0); label.textAlignment = NSTextAlignmentCenter; label.textVerticalAlignment = YYTextVerticalAlignmentCenter; label.numberOfLines = 0; label.backgroundColor = [UIColor colorWithWhite:0.933 alpha:1.000]; [self.view addSubview:label]; /* If the 'highlight.tapAction' is not nil, the label will invoke 'highlight.tapAction' and ignore 'label.highlightTapAction'. If the 'highlight.tapAction' is nil, you can use 'highlightTapAction' to handle all tap action in this label. */ label.highlightTapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { [_self showMessage:[NSString stringWithFormat:@"Tap: %@",[text.string substringWithRange:range]]]; }; } - (NSAttributedString *)padding { NSMutableAttributedString *pad = [[NSMutableAttributedString alloc] initWithString:@"\n\n"]; pad.yy_font = [UIFont systemFontOfSize:4]; return pad; } - (void)showMessage:(NSString *)msg { CGFloat padding = 10; YYLabel *label = [YYLabel new]; label.text = msg; label.font = [UIFont systemFontOfSize:16]; label.textAlignment = NSTextAlignmentCenter; label.textColor = [UIColor whiteColor]; label.backgroundColor = [UIColor colorWithRed:0.033 green:0.685 blue:0.978 alpha:0.730]; label.width = self.view.width; label.textContainerInset = UIEdgeInsetsMake(padding, padding, padding, padding); label.height = [msg heightForFont:label.font width:label.width] + 2 * padding; label.bottom = (kiOS7Later ? 64 : 0); [self.view addSubview:label]; [UIView animateWithDuration:0.3 animations:^{ label.top = (kiOS7Later ? 64 : 0); } completion:^(BOOL finished) { [UIView animateWithDuration:0.2 delay:2 options:UIViewAnimationOptionCurveEaseInOut animations:^{ label.bottom = (kiOS7Later ? 64 : 0); } completion:^(BOOL finished) { [label removeFromSuperview]; }]; }]; } @end ================================================ FILE: Demo/YYTextDemo/YYTextBindingExample.h ================================================ // // YYTextBindingExample.h // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextBindingExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextBindingExample.m ================================================ // // YYTextBindingExample.m // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextBindingExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "YYTextExampleHelper.h" @interface YYTextExampleEmailBindingParser :NSObject @property (nonatomic, strong) NSRegularExpression *regex; @end @implementation YYTextExampleEmailBindingParser - (instancetype)init { self = [super init]; NSString *pattern = @"[-_a-zA-Z@\\.]+[ ,\\n]"; self.regex = [[NSRegularExpression alloc] initWithPattern:pattern options:kNilOptions error:nil]; return self; } - (BOOL)parseText:(NSMutableAttributedString *)text selectedRange:(NSRangePointer)range { __block BOOL changed = NO; [_regex enumerateMatchesInString:text.string options:NSMatchingWithoutAnchoringBounds range:text.yy_rangeOfAll usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { if (!result) return; NSRange range = result.range; if (range.location == NSNotFound || range.length < 1) return; if ([text attribute:YYTextBindingAttributeName atIndex:range.location effectiveRange:NULL]) return; NSRange bindlingRange = NSMakeRange(range.location, range.length - 1); YYTextBinding *binding = [YYTextBinding bindingWithDeleteConfirm:YES]; [text yy_setTextBinding:binding range:bindlingRange]; /// Text binding [text yy_setColor:[UIColor colorWithRed:0.000 green:0.519 blue:1.000 alpha:1.000] range:bindlingRange]; changed = YES; }]; return changed; } @end @interface YYTextBindingExample () @property (nonatomic, strong) YYTextView *textView; @property (nonatomic, assign) BOOL isInEdit; @end @implementation YYTextBindingExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { self.automaticallyAdjustsScrollViewInsets = NO; } NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"sjobs@apple.com, apple@apple.com, banana@banana.com, pear@pear.com "]; text.yy_font = [UIFont systemFontOfSize:17]; text.yy_lineSpacing = 5; text.yy_color = [UIColor blackColor]; YYTextView *textView = [YYTextView new]; textView.attributedText = text; textView.textParser = [YYTextExampleEmailBindingParser new]; textView.size = self.view.size; textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); textView.delegate = self; if (kiOS7Later) { textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; } textView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0); textView.scrollIndicatorInsets = textView.contentInset; [self.view addSubview:textView]; self.textView = textView; [self.textView becomeFirstResponder]; } - (void)edit:(UIBarButtonItem *)item { if (_textView.isFirstResponder) { [_textView resignFirstResponder]; } else { [_textView becomeFirstResponder]; } } - (void)textViewDidChange:(YYTextView *)textView { if (textView.text.length == 0) { textView.textColor = [UIColor blackColor]; } } - (void)textViewDidBeginEditing:(YYTextView *)textView { UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(edit:)]; self.navigationItem.rightBarButtonItem = buttonItem; } - (void)textViewDidEndEditing:(YYTextView *)textView { self.navigationItem.rightBarButtonItem = nil; } @end ================================================ FILE: Demo/YYTextDemo/YYTextCopyPasteExample.h ================================================ // // YYTextCopyPasteExample.h // YYKitExample // // Created by ibireme on 15/9/12. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextCopyPasteExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextCopyPasteExample.m ================================================ // // YYTextCopyPasteExample.m // YYKitExample // // Created by ibireme on 15/9/12. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextCopyPasteExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "YYTextExampleHelper.h" @interface YYTextCopyPasteExample () @property (nonatomic, assign) YYTextView *textView; @end @implementation YYTextCopyPasteExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { self.automaticallyAdjustsScrollViewInsets = NO; } NSString *text = @"You can copy image from browser or photo album and paste it to here. It support animated GIF and APNG. \n\nYou can also copy attributed string from other YYTextView.\n"; YYTextSimpleMarkdownParser *parser = [YYTextSimpleMarkdownParser new]; [parser setColorWithDarkTheme]; YYTextView *textView = [YYTextView new]; textView.text = text; textView.font = [UIFont systemFontOfSize:17]; textView.size = self.view.size; textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); textView.delegate = self; textView.allowsPasteImage = YES; /// Pasts image textView.allowsPasteAttributedString = YES; /// Paste attributed string if (kiOS7Later) { textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; } textView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0); textView.scrollIndicatorInsets = textView.contentInset; [self.view addSubview:textView]; self.textView = textView; textView.selectedRange = NSMakeRange(text.length, 0); [textView becomeFirstResponder]; } - (void)edit:(UIBarButtonItem *)item { if (_textView.isFirstResponder) { [_textView resignFirstResponder]; } else { [_textView becomeFirstResponder]; } } - (void)textViewDidBeginEditing:(YYTextView *)textView { UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(edit:)]; self.navigationItem.rightBarButtonItem = buttonItem; } - (void)textViewDidEndEditing:(YYTextView *)textView { self.navigationItem.rightBarButtonItem = nil; } @end ================================================ FILE: Demo/YYTextDemo/YYTextEditExample.h ================================================ // // YYTextEditExample.h // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextEditExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextEditExample.m ================================================ // // YYTextEditExample.m // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextEditExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "UIControl+YYAdd.h" #import "CALayer+YYAdd.h" #import "NSData+YYAdd.h" #import "UIGestureRecognizer+YYAdd.h" #import "YYTextExampleHelper.h" @interface YYTextEditExample () @property (nonatomic, assign) YYTextView *textView; @property (nonatomic, strong) UIImageView *imageView; @property (nonatomic, strong) UISwitch *verticalSwitch; @property (nonatomic, strong) UISwitch *debugSwitch; @property (nonatomic, strong) UISwitch *exclusionSwitch; @end @implementation YYTextEditExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { self.automaticallyAdjustsScrollViewInsets = NO; } [self initImageView]; __weak typeof(self) _self = self; UIView *toolbar; if ([UIVisualEffectView class]) { toolbar = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]]; } else { toolbar = [UIToolbar new]; } toolbar.size = CGSizeMake(kScreenWidth, 40); toolbar.top = kiOS7Later ? 64 : 0; [self.view addSubview:toolbar]; NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the season of light, it was the season of darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us. We were all going direct to heaven, we were all going direct the other way.\n\n这是最好的时代,这是最坏的时代;这是智慧的时代,这是愚蠢的时代;这是信仰的时期,这是怀疑的时期;这是光明的季节,这是黑暗的季节;这是希望之春,这是失望之冬;人们面前有着各样事物,人们面前一无所有;人们正在直登天堂,人们正在直下地狱。"]; text.yy_font = [UIFont fontWithName:@"Times New Roman" size:20]; text.yy_lineSpacing = 4; text.yy_firstLineHeadIndent = 20; YYTextView *textView = [YYTextView new]; textView.attributedText = text; textView.size = self.view.size; textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); textView.delegate = self; if (kiOS7Later) { textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; } else { textView.height -= 64; } textView.contentInset = UIEdgeInsetsMake(toolbar.bottom, 0, 0, 0); textView.scrollIndicatorInsets = textView.contentInset; textView.selectedRange = NSMakeRange(text.length, 0); [self.view insertSubview:textView belowSubview:toolbar]; self.textView = textView; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [textView becomeFirstResponder]; }); /*------------------------------ Toolbar ---------------------------------*/ UILabel *label; label = [UILabel new]; label.backgroundColor = [UIColor clearColor]; label.font = [UIFont systemFontOfSize:14]; label.text = @"Vertical:"; label.size = CGSizeMake([label.text widthForFont:label.font] + 2, toolbar.height); label.left = 10; [toolbar addSubview:label]; _verticalSwitch = [UISwitch new]; [_verticalSwitch sizeToFit]; _verticalSwitch.centerY = toolbar.height / 2; _verticalSwitch.left = label.right - 5; _verticalSwitch.layer.transformScale = 0.8; [_verticalSwitch addBlockForControlEvents:UIControlEventValueChanged block:^(UISwitch *switcher) { [_self.textView endEditing:YES]; if (switcher.isOn) { [_self setExclusionPathEnabled:NO]; _self.exclusionSwitch.on = NO; } _self.exclusionSwitch.enabled = !switcher.isOn; _self.textView.verticalForm = switcher.isOn; /// Set vertical form }]; [toolbar addSubview:_verticalSwitch]; label = [UILabel new]; label.backgroundColor = [UIColor clearColor]; label.font = [UIFont systemFontOfSize:14]; label.text = @"Debug:"; label.size = CGSizeMake([label.text widthForFont:label.font] + 2, toolbar.height); label.left = _verticalSwitch.right + 5; [toolbar addSubview:label]; _debugSwitch = [UISwitch new]; [_debugSwitch sizeToFit]; _debugSwitch.on = [YYTextExampleHelper isDebug]; _debugSwitch.centerY = toolbar.height / 2; _debugSwitch.left = label.right - 5; _debugSwitch.layer.transformScale = 0.8; [_debugSwitch addBlockForControlEvents:UIControlEventValueChanged block:^(UISwitch *switcher) { [YYTextExampleHelper setDebug:switcher.isOn]; }]; [toolbar addSubview:_debugSwitch]; label = [UILabel new]; label.backgroundColor = [UIColor clearColor]; label.font = [UIFont systemFontOfSize:14]; label.text = @"Exclusion:"; label.size = CGSizeMake([label.text widthForFont:label.font] + 2, toolbar.height); label.left = _debugSwitch.right + 5; [toolbar addSubview:label]; _exclusionSwitch = [UISwitch new]; [_exclusionSwitch sizeToFit]; _exclusionSwitch.centerY = toolbar.height / 2; _exclusionSwitch.left = label.right - 5; _exclusionSwitch.layer.transformScale = 0.8; [_exclusionSwitch addBlockForControlEvents:UIControlEventValueChanged block:^(UISwitch *switcher) { [_self setExclusionPathEnabled:switcher.isOn]; }]; [toolbar addSubview:_exclusionSwitch]; [[YYTextKeyboardManager defaultManager] addObserver:self]; } - (void)dealloc { [[YYTextKeyboardManager defaultManager] removeObserver:self]; } - (void)setExclusionPathEnabled:(BOOL)enabled { if (enabled) { [self.textView addSubview:self.imageView]; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.imageView.frame cornerRadius:self.imageView.layer.cornerRadius]; self.textView.exclusionPaths = @[path]; /// Set exclusion paths } else { [self.imageView removeFromSuperview]; self.textView.exclusionPaths = nil; } } - (void)initImageView { NSData *data = [NSData dataNamed:@"dribbble256_imageio.png"]; UIImage *image = [[YYImage alloc] initWithData:data scale:2]; UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image]; imageView.clipsToBounds = YES; imageView.userInteractionEnabled = YES; imageView.layer.cornerRadius = imageView.height / 2; imageView.center = CGPointMake(kScreenWidth / 2, kScreenWidth / 2); self.imageView = imageView; __weak typeof(self) _self = self; UIPanGestureRecognizer *g = [[UIPanGestureRecognizer alloc] initWithActionBlock:^(UIPanGestureRecognizer *g) { __strong typeof(_self) self = _self; if (!self) return; CGPoint p = [g locationInView:self.textView]; self.imageView.center = p; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.imageView.frame cornerRadius:self.imageView.layer.cornerRadius]; self.textView.exclusionPaths = @[path]; }]; [imageView addGestureRecognizer:g]; } - (void)edit:(UIBarButtonItem *)item { if (_textView.isFirstResponder) { [_textView resignFirstResponder]; } else { [_textView becomeFirstResponder]; } } #pragma mark text view - (void)textViewDidBeginEditing:(YYTextView *)textView { UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(edit:)]; self.navigationItem.rightBarButtonItem = buttonItem; } - (void)textViewDidEndEditing:(YYTextView *)textView { self.navigationItem.rightBarButtonItem = nil; } #pragma mark - keyboard - (void)keyboardChangedWithTransition:(YYTextKeyboardTransition)transition { BOOL clipped = NO; if (_textView.isVerticalForm && transition.toVisible) { CGRect rect = [[YYTextKeyboardManager defaultManager] convertRect:transition.toFrame toView:self.view]; if (CGRectGetMaxY(rect) == self.view.height) { CGRect textFrame = self.view.bounds; textFrame.size.height -= rect.size.height; _textView.frame = textFrame; clipped = YES; } } if (!clipped) { _textView.frame = self.view.bounds; } } @end ================================================ FILE: Demo/YYTextDemo/YYTextEmoticonExample.h ================================================ // // YYTextEmoticonExample.h // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextEmoticonExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextEmoticonExample.m ================================================ // // YYTextEmoticonExample.m // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextEmoticonExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "YYTextExampleHelper.h" @interface YYTextEmoticonExample () @property (nonatomic, strong) YYTextView *textView; @end @implementation YYTextEmoticonExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { self.automaticallyAdjustsScrollViewInsets = NO; } NSMutableDictionary *mapper = [NSMutableDictionary new]; mapper[@":smile:"] = [self imageWithName:@"002"]; mapper[@":cool:"] = [self imageWithName:@"013"]; mapper[@":biggrin:"] = [self imageWithName:@"047"]; mapper[@":arrow:"] = [self imageWithName:@"007"]; mapper[@":confused:"] = [self imageWithName:@"041"]; mapper[@":cry:"] = [self imageWithName:@"010"]; mapper[@":wink:"] = [self imageWithName:@"085"]; YYTextSimpleEmoticonParser *parser = [YYTextSimpleEmoticonParser new]; parser.emoticonMapper = mapper; YYTextLinePositionSimpleModifier *mod = [YYTextLinePositionSimpleModifier new]; mod.fixedLineHeight = 22; YYTextView *textView = [YYTextView new]; textView.text = @"Hahahah:smile:, it\'s emoticons::cool::arrow::cry::wink:\n\nYou can input \":\" + \"smile\" + \":\" to display smile emoticon, or you can copy and paste these emoticons.\n"; textView.font = [UIFont systemFontOfSize:17]; textView.textParser = parser; textView.size = self.view.size; textView.linePositionModifier = mod; textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); textView.delegate = self; if (kiOS7Later) { textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; } textView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0); textView.scrollIndicatorInsets = textView.contentInset; [self.view addSubview:textView]; self.textView = textView; [self.textView becomeFirstResponder]; } - (UIImage *)imageWithName:(NSString *)name { NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"EmoticonQQ" ofType:@"bundle"]]; NSString *path = [bundle pathForScaledResource:name ofType:@"gif"]; NSData *data = [NSData dataWithContentsOfFile:path]; YYImage *image = [YYImage imageWithData:data scale:2]; image.preloadAllAnimatedImageFrames = YES; return image; } - (void)edit:(UIBarButtonItem *)item { if (_textView.isFirstResponder) { [_textView resignFirstResponder]; } else { [_textView becomeFirstResponder]; } } - (void)textViewDidBeginEditing:(YYTextView *)textView { UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(edit:)]; self.navigationItem.rightBarButtonItem = buttonItem; } - (void)textViewDidEndEditing:(YYTextView *)textView { self.navigationItem.rightBarButtonItem = nil; } @end ================================================ FILE: Demo/YYTextDemo/YYTextExample.h ================================================ // // YYTextExample.h // YYKitExample // // Created by ibireme on 15/7/18. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextExample : UITableViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextExample.m ================================================ // // YYTextExample.m // YYKitExample // // Created by ibireme on 15/7/18. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextExample.h" #import #import "YYText.h" @interface YYTextExample() @property (nonatomic, strong) NSMutableArray *titles; @property (nonatomic, strong) NSMutableArray *classNames; @end @implementation YYTextExample - (void)viewDidLoad { [super viewDidLoad]; self.title = @"✎ YYText Demo ✎"; self.titles = @[].mutableCopy; self.classNames = @[].mutableCopy; [self addCell:@"Text Attributes 1" class:@"YYTextAttributeExample"]; [self addCell:@"Text Attributes 2" class:@"YYTextTagExample"]; [self addCell:@"Text Attachments" class:@"YYTextAttachmentExample"]; [self addCell:@"Text Edit" class:@"YYTextEditExample"]; [self addCell:@"Text Parser (Markdown)" class:@"YYTextMarkdownExample"]; [self addCell:@"Text Parser (Emoticon)" class:@"YYTextEmoticonExample"]; [self addCell:@"Text Binding" class:@"YYTextBindingExample"]; [self addCell:@"Copy and Paste" class:@"YYTextCopyPasteExample"]; [self addCell:@"Undo and Redo" class:@"YYTextUndoRedoExample"]; [self addCell:@"Ruby Annotation" class:@"YYTextRubyExample"]; [self addCell:@"Async Display" class:@"YYTextAsyncExample"]; [self.tableView reloadData]; } - (void)addCell:(NSString *)title class:(NSString *)className { [self.titles addObject:title]; [self.classNames addObject:className]; } #pragma mark - Table view data source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return _titles.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"YY"]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"YY"]; } cell.textLabel.text = _titles[indexPath.row]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSString *className = self.classNames[indexPath.row]; Class class = NSClassFromString(className); if (class) { UIViewController *ctrl = class.new; ctrl.title = _titles[indexPath.row]; [self.navigationController pushViewController:ctrl animated:YES]; } [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; } @end ================================================ FILE: Demo/YYTextDemo/YYTextExampleHelper.h ================================================ // // YYTextExampleHelper.h // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextExampleHelper : NSObject + (void)addDebugOptionToViewController:(UIViewController *)vc; + (void)setDebug:(BOOL)debug; + (BOOL)isDebug; @end ================================================ FILE: Demo/YYTextDemo/YYTextExampleHelper.m ================================================ // // YYTextExampleHelper.m // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextExampleHelper.h" #import "YYText.h" #import "UIControl+YYAdd.h" #import "UIView+YYAdd.h" static BOOL DebugEnabled = NO; @implementation YYTextExampleHelper + (void)addDebugOptionToViewController:(UIViewController *)vc { UISwitch *switcher = [UISwitch new]; [switcher.layer setValue:@(0.8) forKeyPath:@"transform.scale"]; [switcher setOn:DebugEnabled]; [switcher addBlockForControlEvents:UIControlEventValueChanged block:^(UISwitch *sender) { [self setDebug:sender.isOn]; }]; UIView *view = [UIView new]; view.size = CGSizeMake(40, 44); [view addSubview:switcher]; switcher.centerX = view.width / 2; switcher.centerY = view.height / 2; UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:view]; vc.navigationItem.rightBarButtonItem = item; } + (void)setDebug:(BOOL)debug { YYTextDebugOption *debugOptions = [YYTextDebugOption new]; if (debug) { debugOptions.baselineColor = [UIColor redColor]; debugOptions.CTFrameBorderColor = [UIColor redColor]; debugOptions.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180]; debugOptions.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200]; } else { [debugOptions clear]; } [YYTextDebugOption setSharedDebugOption:debugOptions]; DebugEnabled = debug; } + (BOOL)isDebug { return DebugEnabled; } @end ================================================ FILE: Demo/YYTextDemo/YYTextMarkdownExample.h ================================================ // // YYTextMarkdownExample.h // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextMarkdownExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextMarkdownExample.m ================================================ // // YYTextMarkdownExample.m // YYKitExample // // Created by ibireme on 15/9/3. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextMarkdownExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "YYTextExampleHelper.h" @interface YYTextMarkdownExample () @property (nonatomic, assign) YYTextView *textView; @end @implementation YYTextMarkdownExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { self.automaticallyAdjustsScrollViewInsets = NO; } NSString *text = @"#Markdown Editor\nThis is a simple markdown editor based on `YYTextView`.\n\n*********************************************\nIt\'s *italic* style.\n\nIt\'s also _italic_ style.\n\nIt\'s **bold** style.\n\nIt\'s ***italic and bold*** style.\n\nIt\'s __underline__ style.\n\nIt\'s ~~deleteline~~ style.\n\n\nHere is a link: [YYKit](https://github.com/ibireme/YYKit)\n\nHere is some code:\n\n\tif(a){\n\t\tif(b){\n\t\t\tif(c){\n\t\t\t\tprintf(\"haha\");\n\t\t\t}\n\t\t}\n\t}\n"; YYTextSimpleMarkdownParser *parser = [YYTextSimpleMarkdownParser new]; [parser setColorWithDarkTheme]; YYTextView *textView = [YYTextView new]; textView.text = text; textView.font = [UIFont systemFontOfSize:14]; textView.textParser = parser; textView.size = self.view.size; textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); textView.delegate = self; if (kiOS7Later) { textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; } textView.backgroundColor = [UIColor colorWithWhite:0.134 alpha:1.000]; textView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0); textView.scrollIndicatorInsets = textView.contentInset; textView.selectedRange = NSMakeRange(text.length, 0); [self.view addSubview:textView]; self.textView = textView; } - (void)edit:(UIBarButtonItem *)item { if (_textView.isFirstResponder) { [_textView resignFirstResponder]; } else { [_textView becomeFirstResponder]; } } - (void)textViewDidBeginEditing:(YYTextView *)textView { UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(edit:)]; self.navigationItem.rightBarButtonItem = buttonItem; } - (void)textViewDidEndEditing:(YYTextView *)textView { self.navigationItem.rightBarButtonItem = nil; } @end ================================================ FILE: Demo/YYTextDemo/YYTextRubyExample.h ================================================ // // YYTextRubyExample.h // YYKitExample // // Created by ibireme on 15/9/9. // Copyright (C) 2015 ibireme. All rights reserved. // #import @interface YYTextRubyExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextRubyExample.m ================================================ // // YYTextRubyExample.m // YYKitExample // // Created by ibireme on 15/9/9. // Copyright (C) 2015 ibireme. All rights reserved. // #import "YYTextRubyExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "YYTextExampleHelper.h" /* Ruby Annotation See: http://www.w3.org/TR/ruby/ */ @implementation YYTextRubyExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { self.automaticallyAdjustsScrollViewInsets = NO; } NSMutableAttributedString *text = [NSMutableAttributedString new]; if (kSystemVersion < 8) { [text yy_appendString:@"Only support iOS8 Later"]; text.yy_font = [UIFont systemFontOfSize:30]; } else { NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"这是用汉语写的一段文字。"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; YYTextRubyAnnotation *ruby; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"hàn yŭ"; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"汉语"]]; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"wén"; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"文"]]; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"zì"; ruby.alignment = kCTRubyAlignmentCenter; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"字"]]; [text appendAttributedString:one]; [text appendAttributedString:[self padding]]; one = [[NSMutableAttributedString alloc] initWithString:@"日本語で書いた作文です。"]; one.yy_font = [UIFont boldSystemFontOfSize:30]; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"に"; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"日"]]; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"ほん"; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"本"]]; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"ご"; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"語"]]; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"か"; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"書"]]; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"さく"; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"作"]]; ruby = [YYTextRubyAnnotation new]; ruby.textBefore = @"ぶん"; [one yy_setTextRubyAnnotation:ruby range:[one.string rangeOfString:@"文"]]; [text appendAttributedString:one]; } YYLabel *label = [YYLabel new]; label.attributedText = text; label.width = self.view.width - 60; label.centerX = self.view.width / 2; label.height = self.view.height - (kiOS7Later ? 64 : 44) - 60; label.top = (kiOS7Later ? 64 : 0) + 30; label.textAlignment = NSTextAlignmentCenter; label.textVerticalAlignment = YYTextVerticalAlignmentCenter; label.numberOfLines = 0; label.backgroundColor = [UIColor colorWithWhite:0.933 alpha:1.000]; [self.view addSubview:label]; } - (NSAttributedString *)padding { NSMutableAttributedString *pad = [[NSMutableAttributedString alloc] initWithString:@"\n\n"]; pad.yy_font = [UIFont systemFontOfSize:30]; return pad; } @end ================================================ FILE: Demo/YYTextDemo/YYTextTagExample.h ================================================ // // YYTextTagExample.h // YYKitExample // // Created by ibireme on 15/8/19. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextTagExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextTagExample.m ================================================ // // YYTextTagExample.m // YYKitExample // // Created by ibireme on 15/8/19. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextTagExample.h" #import "YYText.h" #import "UIView+YYAdd.h" #import "YYTextExampleHelper.h" @interface YYTextTagExample () @property (nonatomic, assign) YYTextView *textView; @end @implementation YYTextTagExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { self.automaticallyAdjustsScrollViewInsets = NO; } NSMutableAttributedString *text = [NSMutableAttributedString new]; NSArray *tags = @[@"◉red", @"◉orange", @"◉yellow", @"◉green", @"◉blue", @"◉purple", @"◉gray"]; NSArray *tagStrokeColors = @[ UIColorHex(fa3f39), UIColorHex(f48f25), UIColorHex(f1c02c), UIColorHex(54bc2e), UIColorHex(29a9ee), UIColorHex(c171d8), UIColorHex(818e91) ]; NSArray *tagFillColors = @[ UIColorHex(fb6560), UIColorHex(f6a550), UIColorHex(f3cc56), UIColorHex(76c957), UIColorHex(53baf1), UIColorHex(cd8ddf), UIColorHex(a4a4a7) ]; UIFont *font = [UIFont boldSystemFontOfSize:16]; for (int i = 0; i < tags.count; i++) { NSString *tag = tags[i]; UIColor *tagStrokeColor = tagStrokeColors[i]; UIColor *tagFillColor = tagFillColors[i]; NSMutableAttributedString *tagText = [[NSMutableAttributedString alloc] initWithString:tag]; [tagText yy_insertString:@"   " atIndex:0]; [tagText yy_appendString:@"   "]; tagText.yy_font = font; tagText.yy_color = [UIColor whiteColor]; [tagText yy_setTextBinding:[YYTextBinding bindingWithDeleteConfirm:NO] range:tagText.yy_rangeOfAll]; YYTextBorder *border = [YYTextBorder new]; border.strokeWidth = 1.5; border.strokeColor = tagStrokeColor; border.fillColor = tagFillColor; border.cornerRadius = 100; // a huge value border.lineJoin = kCGLineJoinBevel; border.insets = UIEdgeInsetsMake(-2, -5.5, -2, -8); [tagText yy_setTextBackgroundBorder:border range:[tagText.string rangeOfString:tag]]; [text appendAttributedString:tagText]; } text.yy_lineSpacing = 10; text.yy_lineBreakMode = NSLineBreakByWordWrapping; [text yy_appendString:@"\n"]; [text appendAttributedString:text]; // repeat for test YYTextView *textView = [YYTextView new]; textView.attributedText = text; textView.size = self.view.size; textView.textContainerInset = UIEdgeInsetsMake(10 + 64, 10, 10, 10); textView.allowsCopyAttributedString = YES; textView.allowsPasteAttributedString = YES; textView.delegate = self; if (kiOS7Later) { textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; } else { textView.height -= 64; } textView.scrollIndicatorInsets = textView.contentInset; textView.selectedRange = NSMakeRange(text.length, 0); [self.view addSubview:textView]; self.textView = textView; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [textView becomeFirstResponder]; }); } - (void)edit:(UIBarButtonItem *)item { if (_textView.isFirstResponder) { [_textView resignFirstResponder]; } else { [_textView becomeFirstResponder]; } } #pragma mark text view - (void)textViewDidBeginEditing:(YYTextView *)textView { UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(edit:)]; self.navigationItem.rightBarButtonItem = buttonItem; } - (void)textViewDidEndEditing:(YYTextView *)textView { self.navigationItem.rightBarButtonItem = nil; } @end ================================================ FILE: Demo/YYTextDemo/YYTextUndoRedoExample.h ================================================ // // YYTextUndoRedoExample.h // YYKitExample // // Created by ibireme on 15/9/12. // Copyright (c) 2015 ibireme. All rights reserved. // #import @interface YYTextUndoRedoExample : UIViewController @end ================================================ FILE: Demo/YYTextDemo/YYTextUndoRedoExample.m ================================================ // // YYTextUndoRedoExample.m // YYKitExample // // Created by ibireme on 15/9/12. // Copyright (c) 2015 ibireme. All rights reserved. // #import "YYTextUndoRedoExample.h" #import "YYText.h" #import "YYImage.h" #import "UIImage+YYWebImage.h" #import "UIView+YYAdd.h" #import "NSBundle+YYAdd.h" #import "NSString+YYAdd.h" #import "YYTextExampleHelper.h" @interface YYTextUndoRedoExample () @property (nonatomic, assign) YYTextView *textView; @end @implementation YYTextUndoRedoExample - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { self.automaticallyAdjustsScrollViewInsets = NO; } NSString *text = @"You can shake the device to undo and redo."; YYTextView *textView = [YYTextView new]; textView.text = text; textView.font = [UIFont systemFontOfSize:17]; textView.size = self.view.size; textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); textView.delegate = self; textView.allowsUndoAndRedo = YES; /// Undo and Redo textView.maximumUndoLevel = 10; /// Undo level if (kiOS7Later) { textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; } textView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0); textView.scrollIndicatorInsets = textView.contentInset; [self.view addSubview:textView]; self.textView = textView; textView.selectedRange = NSMakeRange(text.length, 0); [textView becomeFirstResponder]; } - (void)edit:(UIBarButtonItem *)item { if (_textView.isFirstResponder) { [_textView resignFirstResponder]; } else { [_textView becomeFirstResponder]; } } - (void)textViewDidBeginEditing:(YYTextView *)textView { UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(edit:)]; self.navigationItem.rightBarButtonItem = buttonItem; } - (void)textViewDidEndEditing:(YYTextView *)textView { self.navigationItem.rightBarButtonItem = nil; } @end ================================================ FILE: Demo/YYTextDemo/YYWeakProxy.h ================================================ // // YYWeakProxy.h // YYKit // // 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 /** 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 { YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self]; _timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES]; } - (void)tick:(NSTimer *)timer {...} @end */ @interface YYWeakProxy : NSProxy /** The proxy target. */ @property (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 ================================================ FILE: Demo/YYTextDemo/YYWeakProxy.m ================================================ // // YYWeakProxy.m // YYKit // // 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 "YYWeakProxy.h" @implementation YYWeakProxy - (instancetype)initWithTarget:(id)target { _target = target; return self; } + (instancetype)proxyWithTarget:(id)target { return [[YYWeakProxy 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 ================================================ FILE: Demo/YYTextDemo/main.m ================================================ // // main.m // YYTextDemo // // Created by ibireme on 15/10/17. // Copyright © 2015年 ibireme. All rights reserved. // #import #import "AppDelegate.h" int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: Demo/YYTextDemo.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 48; objects = { /* Begin PBXBuildFile section */ D91054861F3735D20007F224 /* Animated image support.txt in Resources */ = {isa = PBXBuildFile; fileRef = D910547B1F3735D20007F224 /* Animated image support.txt */; }; D91054871F3735D20007F224 /* YYAnimatedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = D910547D1F3735D20007F224 /* YYAnimatedImageView.m */; }; D91054881F3735D20007F224 /* YYFrameImage.m in Sources */ = {isa = PBXBuildFile; fileRef = D910547F1F3735D20007F224 /* YYFrameImage.m */; }; D91054891F3735D20007F224 /* YYImage.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054811F3735D20007F224 /* YYImage.m */; }; D910548A1F3735D20007F224 /* YYImageCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054831F3735D20007F224 /* YYImageCoder.m */; }; D910548B1F3735D20007F224 /* YYSpriteSheetImage.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054851F3735D20007F224 /* YYSpriteSheetImage.m */; }; D91054C11F3735E50007F224 /* YYTextContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D910548F1F3735E50007F224 /* YYTextContainerView.m */; }; D91054C21F3735E50007F224 /* YYTextDebugOption.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054911F3735E50007F224 /* YYTextDebugOption.m */; }; D91054C31F3735E50007F224 /* YYTextEffectWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054931F3735E50007F224 /* YYTextEffectWindow.m */; }; D91054C41F3735E50007F224 /* YYTextInput.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054951F3735E50007F224 /* YYTextInput.m */; }; D91054C51F3735E50007F224 /* YYTextKeyboardManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054971F3735E50007F224 /* YYTextKeyboardManager.m */; }; D91054C61F3735E50007F224 /* YYTextLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054991F3735E50007F224 /* YYTextLayout.m */; }; D91054C71F3735E50007F224 /* YYTextLine.m in Sources */ = {isa = PBXBuildFile; fileRef = D910549B1F3735E50007F224 /* YYTextLine.m */; }; D91054C81F3735E50007F224 /* YYTextMagnifier.m in Sources */ = {isa = PBXBuildFile; fileRef = D910549D1F3735E50007F224 /* YYTextMagnifier.m */; }; D91054C91F3735E50007F224 /* YYTextSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = D910549F1F3735E50007F224 /* YYTextSelectionView.m */; }; D91054CA1F3735E50007F224 /* YYTextArchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054A21F3735E50007F224 /* YYTextArchiver.m */; }; D91054CB1F3735E50007F224 /* YYTextAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054A41F3735E50007F224 /* YYTextAttribute.m */; }; D91054CC1F3735E50007F224 /* YYTextParser.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054A61F3735E50007F224 /* YYTextParser.m */; }; D91054CD1F3735E50007F224 /* YYTextRubyAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054A81F3735E50007F224 /* YYTextRubyAnnotation.m */; }; D91054CE1F3735E50007F224 /* YYTextRunDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054AA1F3735E50007F224 /* YYTextRunDelegate.m */; }; D91054CF1F3735E50007F224 /* NSAttributedString+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054AD1F3735E50007F224 /* NSAttributedString+YYText.m */; }; D91054D01F3735E50007F224 /* NSParagraphStyle+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054AF1F3735E50007F224 /* NSParagraphStyle+YYText.m */; }; D91054D11F3735E50007F224 /* UIPasteboard+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054B11F3735E50007F224 /* UIPasteboard+YYText.m */; }; D91054D21F3735E50007F224 /* UIView+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054B31F3735E50007F224 /* UIView+YYText.m */; }; D91054D31F3735E50007F224 /* YYTextAsyncLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054B51F3735E50007F224 /* YYTextAsyncLayer.m */; }; D91054D41F3735E50007F224 /* YYTextTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054B71F3735E50007F224 /* YYTextTransaction.m */; }; D91054D51F3735E50007F224 /* YYTextUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054B91F3735E50007F224 /* YYTextUtilities.m */; }; D91054D61F3735E50007F224 /* YYTextWeakProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054BB1F3735E50007F224 /* YYTextWeakProxy.m */; }; D91054D71F3735E50007F224 /* YYLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054BD1F3735E50007F224 /* YYLabel.m */; }; D91054D81F3735E50007F224 /* YYTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054C01F3735E50007F224 /* YYTextView.m */; }; D91054DD1F3736060007F224 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054DA1F3736060007F224 /* AppDelegate.m */; }; D91054DE1F3736060007F224 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054DC1F3736060007F224 /* ViewController.m */; }; D91054F61F3736700007F224 /* CALayer+YYAdd.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054E11F3736700007F224 /* CALayer+YYAdd.m */; }; D91054F71F3736700007F224 /* NSBundle+YYAdd.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054E31F3736700007F224 /* NSBundle+YYAdd.m */; }; D91054F81F3736700007F224 /* NSData+YYAdd.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054E51F3736700007F224 /* NSData+YYAdd.m */; }; D91054F91F3736700007F224 /* NSString+YYAdd.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054E71F3736700007F224 /* NSString+YYAdd.m */; }; D91054FA1F3736700007F224 /* UIControl+YYAdd.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054E91F3736700007F224 /* UIControl+YYAdd.m */; }; D91054FB1F3736700007F224 /* UIGestureRecognizer+YYAdd.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054EB1F3736700007F224 /* UIGestureRecognizer+YYAdd.m */; }; D91054FC1F3736700007F224 /* UIImage+YYWebImage.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054ED1F3736700007F224 /* UIImage+YYWebImage.m */; }; D91054FD1F3736700007F224 /* UIView+YYAdd.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054EF1F3736700007F224 /* UIView+YYAdd.m */; }; D91054FE1F3736700007F224 /* YYFPSLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054F11F3736700007F224 /* YYFPSLabel.m */; }; D91054FF1F3736700007F224 /* YYGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054F31F3736700007F224 /* YYGestureRecognizer.m */; }; D91055001F3736700007F224 /* YYWeakProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D91054F51F3736700007F224 /* YYWeakProxy.m */; }; D910551B1F37367B0007F224 /* YYTextAsyncExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055021F37367B0007F224 /* YYTextAsyncExample.m */; }; D910551C1F37367B0007F224 /* YYTextAttachmentExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055041F37367B0007F224 /* YYTextAttachmentExample.m */; }; D910551D1F37367B0007F224 /* YYTextAttributeExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055061F37367B0007F224 /* YYTextAttributeExample.m */; }; D910551E1F37367B0007F224 /* YYTextBindingExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055081F37367B0007F224 /* YYTextBindingExample.m */; }; D910551F1F37367B0007F224 /* YYTextCopyPasteExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D910550A1F37367B0007F224 /* YYTextCopyPasteExample.m */; }; D91055201F37367B0007F224 /* YYTextEditExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D910550C1F37367B0007F224 /* YYTextEditExample.m */; }; D91055211F37367B0007F224 /* YYTextEmoticonExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D910550E1F37367B0007F224 /* YYTextEmoticonExample.m */; }; D91055221F37367B0007F224 /* YYTextExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055101F37367B0007F224 /* YYTextExample.m */; }; D91055231F37367B0007F224 /* YYTextExampleHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055121F37367B0007F224 /* YYTextExampleHelper.m */; }; D91055241F37367B0007F224 /* YYTextMarkdownExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055141F37367B0007F224 /* YYTextMarkdownExample.m */; }; D91055251F37367B0007F224 /* YYTextRubyExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055161F37367B0007F224 /* YYTextRubyExample.m */; }; D91055261F37367B0007F224 /* YYTextTagExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D91055181F37367B0007F224 /* YYTextTagExample.m */; }; D91055271F37367B0007F224 /* YYTextUndoRedoExample.m in Sources */ = {isa = PBXBuildFile; fileRef = D910551A1F37367B0007F224 /* YYTextUndoRedoExample.m */; }; D910552C1F37368D0007F224 /* dribbble64_imageio.png in Resources */ = {isa = PBXBuildFile; fileRef = D91055281F37368D0007F224 /* dribbble64_imageio.png */; }; D910552D1F37368D0007F224 /* dribbble256_imageio.png in Resources */ = {isa = PBXBuildFile; fileRef = D91055291F37368D0007F224 /* dribbble256_imageio.png */; }; D910552E1F37368D0007F224 /* EmoticonQQ.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D910552A1F37368D0007F224 /* EmoticonQQ.bundle */; }; D910552F1F37368D0007F224 /* pia@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D910552B1F37368D0007F224 /* pia@2x.png */; }; D91055331F37374B0007F224 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D91055321F37374B0007F224 /* UIKit.framework */; }; D91055351F37374F0007F224 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D91055341F37374F0007F224 /* QuartzCore.framework */; }; D91055371F3737540007F224 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D91055361F3737540007F224 /* CoreText.framework */; }; D91055391F37375A0007F224 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D91055381F37375A0007F224 /* Accelerate.framework */; }; D910553B1F37375F0007F224 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D910553A1F37375F0007F224 /* ImageIO.framework */; }; D910553D1F3737640007F224 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D910553C1F3737640007F224 /* MobileCoreServices.framework */; }; D910553F1F3737670007F224 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D910553E1F3737670007F224 /* SystemConfiguration.framework */; }; D91055411F37376C0007F224 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D91055401F37376C0007F224 /* libz.tbd */; }; D94EE9961F37304200F37AD6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D94EE9941F37304200F37AD6 /* Main.storyboard */; }; D94EE9981F37304200F37AD6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D94EE9971F37304200F37AD6 /* Assets.xcassets */; }; D94EE99B1F37304200F37AD6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D94EE9991F37304200F37AD6 /* LaunchScreen.storyboard */; }; D94EE99E1F37304200F37AD6 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D94EE99D1F37304200F37AD6 /* main.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ D910547B1F3735D20007F224 /* Animated image support.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Animated image support.txt"; sourceTree = ""; }; D910547C1F3735D20007F224 /* YYAnimatedImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYAnimatedImageView.h; sourceTree = ""; }; D910547D1F3735D20007F224 /* YYAnimatedImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYAnimatedImageView.m; sourceTree = ""; }; D910547E1F3735D20007F224 /* YYFrameImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYFrameImage.h; sourceTree = ""; }; D910547F1F3735D20007F224 /* YYFrameImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYFrameImage.m; sourceTree = ""; }; D91054801F3735D20007F224 /* YYImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYImage.h; sourceTree = ""; }; D91054811F3735D20007F224 /* YYImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYImage.m; sourceTree = ""; }; D91054821F3735D20007F224 /* YYImageCoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYImageCoder.h; sourceTree = ""; }; D91054831F3735D20007F224 /* YYImageCoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYImageCoder.m; sourceTree = ""; }; D91054841F3735D20007F224 /* YYSpriteSheetImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYSpriteSheetImage.h; sourceTree = ""; }; D91054851F3735D20007F224 /* YYSpriteSheetImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYSpriteSheetImage.m; sourceTree = ""; }; D910548E1F3735E50007F224 /* YYTextContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextContainerView.h; sourceTree = ""; }; D910548F1F3735E50007F224 /* YYTextContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextContainerView.m; sourceTree = ""; }; D91054901F3735E50007F224 /* YYTextDebugOption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextDebugOption.h; sourceTree = ""; }; D91054911F3735E50007F224 /* YYTextDebugOption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextDebugOption.m; sourceTree = ""; }; D91054921F3735E50007F224 /* YYTextEffectWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextEffectWindow.h; sourceTree = ""; }; D91054931F3735E50007F224 /* YYTextEffectWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextEffectWindow.m; sourceTree = ""; }; D91054941F3735E50007F224 /* YYTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextInput.h; sourceTree = ""; }; D91054951F3735E50007F224 /* YYTextInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextInput.m; sourceTree = ""; }; D91054961F3735E50007F224 /* YYTextKeyboardManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextKeyboardManager.h; sourceTree = ""; }; D91054971F3735E50007F224 /* YYTextKeyboardManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextKeyboardManager.m; sourceTree = ""; }; D91054981F3735E50007F224 /* YYTextLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextLayout.h; sourceTree = ""; }; D91054991F3735E50007F224 /* YYTextLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextLayout.m; sourceTree = ""; }; D910549A1F3735E50007F224 /* YYTextLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextLine.h; sourceTree = ""; }; D910549B1F3735E50007F224 /* YYTextLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextLine.m; sourceTree = ""; }; D910549C1F3735E50007F224 /* YYTextMagnifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextMagnifier.h; sourceTree = ""; }; D910549D1F3735E50007F224 /* YYTextMagnifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextMagnifier.m; sourceTree = ""; }; D910549E1F3735E50007F224 /* YYTextSelectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextSelectionView.h; sourceTree = ""; }; D910549F1F3735E50007F224 /* YYTextSelectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextSelectionView.m; sourceTree = ""; }; D91054A11F3735E50007F224 /* YYTextArchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextArchiver.h; sourceTree = ""; }; D91054A21F3735E50007F224 /* YYTextArchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextArchiver.m; sourceTree = ""; }; D91054A31F3735E50007F224 /* YYTextAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAttribute.h; sourceTree = ""; }; D91054A41F3735E50007F224 /* YYTextAttribute.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAttribute.m; sourceTree = ""; }; D91054A51F3735E50007F224 /* YYTextParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextParser.h; sourceTree = ""; }; D91054A61F3735E50007F224 /* YYTextParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextParser.m; sourceTree = ""; }; D91054A71F3735E50007F224 /* YYTextRubyAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRubyAnnotation.h; sourceTree = ""; }; D91054A81F3735E50007F224 /* YYTextRubyAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRubyAnnotation.m; sourceTree = ""; }; D91054A91F3735E50007F224 /* YYTextRunDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRunDelegate.h; sourceTree = ""; }; D91054AA1F3735E50007F224 /* YYTextRunDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRunDelegate.m; sourceTree = ""; }; D91054AC1F3735E50007F224 /* NSAttributedString+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+YYText.h"; sourceTree = ""; }; D91054AD1F3735E50007F224 /* NSAttributedString+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+YYText.m"; sourceTree = ""; }; D91054AE1F3735E50007F224 /* NSParagraphStyle+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+YYText.h"; sourceTree = ""; }; D91054AF1F3735E50007F224 /* NSParagraphStyle+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSParagraphStyle+YYText.m"; sourceTree = ""; }; D91054B01F3735E50007F224 /* UIPasteboard+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIPasteboard+YYText.h"; sourceTree = ""; }; D91054B11F3735E50007F224 /* UIPasteboard+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIPasteboard+YYText.m"; sourceTree = ""; }; D91054B21F3735E50007F224 /* UIView+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+YYText.h"; sourceTree = ""; }; D91054B31F3735E50007F224 /* UIView+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+YYText.m"; sourceTree = ""; }; D91054B41F3735E50007F224 /* YYTextAsyncLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAsyncLayer.h; sourceTree = ""; }; D91054B51F3735E50007F224 /* YYTextAsyncLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAsyncLayer.m; sourceTree = ""; }; D91054B61F3735E50007F224 /* YYTextTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextTransaction.h; sourceTree = ""; }; D91054B71F3735E50007F224 /* YYTextTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextTransaction.m; sourceTree = ""; }; D91054B81F3735E50007F224 /* YYTextUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextUtilities.h; sourceTree = ""; }; D91054B91F3735E50007F224 /* YYTextUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextUtilities.m; sourceTree = ""; }; D91054BA1F3735E50007F224 /* YYTextWeakProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextWeakProxy.h; sourceTree = ""; }; D91054BB1F3735E50007F224 /* YYTextWeakProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextWeakProxy.m; sourceTree = ""; }; D91054BC1F3735E50007F224 /* YYLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYLabel.h; sourceTree = ""; }; D91054BD1F3735E50007F224 /* YYLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYLabel.m; sourceTree = ""; }; D91054BE1F3735E50007F224 /* YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYText.h; sourceTree = ""; }; D91054BF1F3735E50007F224 /* YYTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextView.h; sourceTree = ""; }; D91054C01F3735E50007F224 /* YYTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextView.m; sourceTree = ""; }; D91054D91F3736060007F224 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; D91054DA1F3736060007F224 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; D91054DB1F3736060007F224 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; D91054DC1F3736060007F224 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; D91054E01F3736700007F224 /* CALayer+YYAdd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CALayer+YYAdd.h"; sourceTree = ""; }; D91054E11F3736700007F224 /* CALayer+YYAdd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CALayer+YYAdd.m"; sourceTree = ""; }; D91054E21F3736700007F224 /* NSBundle+YYAdd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSBundle+YYAdd.h"; sourceTree = ""; }; D91054E31F3736700007F224 /* NSBundle+YYAdd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSBundle+YYAdd.m"; sourceTree = ""; }; D91054E41F3736700007F224 /* NSData+YYAdd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+YYAdd.h"; sourceTree = ""; }; D91054E51F3736700007F224 /* NSData+YYAdd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+YYAdd.m"; sourceTree = ""; }; D91054E61F3736700007F224 /* NSString+YYAdd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+YYAdd.h"; sourceTree = ""; }; D91054E71F3736700007F224 /* NSString+YYAdd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+YYAdd.m"; sourceTree = ""; }; D91054E81F3736700007F224 /* UIControl+YYAdd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIControl+YYAdd.h"; sourceTree = ""; }; D91054E91F3736700007F224 /* UIControl+YYAdd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIControl+YYAdd.m"; sourceTree = ""; }; D91054EA1F3736700007F224 /* UIGestureRecognizer+YYAdd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIGestureRecognizer+YYAdd.h"; sourceTree = ""; }; D91054EB1F3736700007F224 /* UIGestureRecognizer+YYAdd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIGestureRecognizer+YYAdd.m"; sourceTree = ""; }; D91054EC1F3736700007F224 /* UIImage+YYWebImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+YYWebImage.h"; sourceTree = ""; }; D91054ED1F3736700007F224 /* UIImage+YYWebImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+YYWebImage.m"; sourceTree = ""; }; D91054EE1F3736700007F224 /* UIView+YYAdd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+YYAdd.h"; sourceTree = ""; }; D91054EF1F3736700007F224 /* UIView+YYAdd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+YYAdd.m"; sourceTree = ""; }; D91054F01F3736700007F224 /* YYFPSLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYFPSLabel.h; sourceTree = ""; }; D91054F11F3736700007F224 /* YYFPSLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYFPSLabel.m; sourceTree = ""; }; D91054F21F3736700007F224 /* YYGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYGestureRecognizer.h; sourceTree = ""; }; D91054F31F3736700007F224 /* YYGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYGestureRecognizer.m; sourceTree = ""; }; D91054F41F3736700007F224 /* YYWeakProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYWeakProxy.h; sourceTree = ""; }; D91054F51F3736700007F224 /* YYWeakProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYWeakProxy.m; sourceTree = ""; }; D91055011F37367B0007F224 /* YYTextAsyncExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAsyncExample.h; sourceTree = ""; }; D91055021F37367B0007F224 /* YYTextAsyncExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAsyncExample.m; sourceTree = ""; }; D91055031F37367B0007F224 /* YYTextAttachmentExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAttachmentExample.h; sourceTree = ""; }; D91055041F37367B0007F224 /* YYTextAttachmentExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAttachmentExample.m; sourceTree = ""; }; D91055051F37367B0007F224 /* YYTextAttributeExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAttributeExample.h; sourceTree = ""; }; D91055061F37367B0007F224 /* YYTextAttributeExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAttributeExample.m; sourceTree = ""; }; D91055071F37367B0007F224 /* YYTextBindingExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextBindingExample.h; sourceTree = ""; }; D91055081F37367B0007F224 /* YYTextBindingExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextBindingExample.m; sourceTree = ""; }; D91055091F37367B0007F224 /* YYTextCopyPasteExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextCopyPasteExample.h; sourceTree = ""; }; D910550A1F37367B0007F224 /* YYTextCopyPasteExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextCopyPasteExample.m; sourceTree = ""; }; D910550B1F37367B0007F224 /* YYTextEditExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextEditExample.h; sourceTree = ""; }; D910550C1F37367B0007F224 /* YYTextEditExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextEditExample.m; sourceTree = ""; }; D910550D1F37367B0007F224 /* YYTextEmoticonExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextEmoticonExample.h; sourceTree = ""; }; D910550E1F37367B0007F224 /* YYTextEmoticonExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextEmoticonExample.m; sourceTree = ""; }; D910550F1F37367B0007F224 /* YYTextExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextExample.h; sourceTree = ""; }; D91055101F37367B0007F224 /* YYTextExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextExample.m; sourceTree = ""; }; D91055111F37367B0007F224 /* YYTextExampleHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextExampleHelper.h; sourceTree = ""; }; D91055121F37367B0007F224 /* YYTextExampleHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextExampleHelper.m; sourceTree = ""; }; D91055131F37367B0007F224 /* YYTextMarkdownExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextMarkdownExample.h; sourceTree = ""; }; D91055141F37367B0007F224 /* YYTextMarkdownExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextMarkdownExample.m; sourceTree = ""; }; D91055151F37367B0007F224 /* YYTextRubyExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRubyExample.h; sourceTree = ""; }; D91055161F37367B0007F224 /* YYTextRubyExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRubyExample.m; sourceTree = ""; }; D91055171F37367B0007F224 /* YYTextTagExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextTagExample.h; sourceTree = ""; }; D91055181F37367B0007F224 /* YYTextTagExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextTagExample.m; sourceTree = ""; }; D91055191F37367B0007F224 /* YYTextUndoRedoExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextUndoRedoExample.h; sourceTree = ""; }; D910551A1F37367B0007F224 /* YYTextUndoRedoExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextUndoRedoExample.m; sourceTree = ""; }; D91055281F37368D0007F224 /* dribbble64_imageio.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dribbble64_imageio.png; sourceTree = ""; }; D91055291F37368D0007F224 /* dribbble256_imageio.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dribbble256_imageio.png; sourceTree = ""; }; D910552A1F37368D0007F224 /* EmoticonQQ.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = EmoticonQQ.bundle; sourceTree = ""; }; D910552B1F37368D0007F224 /* pia@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "pia@2x.png"; sourceTree = ""; }; D91055321F37374B0007F224 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; D91055341F37374F0007F224 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; D91055361F3737540007F224 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; D91055381F37375A0007F224 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; D910553A1F37375F0007F224 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; D910553C1F3737640007F224 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; D910553E1F3737670007F224 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; D91055401F37376C0007F224 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; D94EE98B1F37304200F37AD6 /* YYTextDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = YYTextDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; D94EE9951F37304200F37AD6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; D94EE9971F37304200F37AD6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D94EE99A1F37304200F37AD6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; D94EE99C1F37304200F37AD6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D94EE99D1F37304200F37AD6 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ D94EE9881F37304200F37AD6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D91055411F37376C0007F224 /* libz.tbd in Frameworks */, D910553F1F3737670007F224 /* SystemConfiguration.framework in Frameworks */, D910553D1F3737640007F224 /* MobileCoreServices.framework in Frameworks */, D910553B1F37375F0007F224 /* ImageIO.framework in Frameworks */, D91055391F37375A0007F224 /* Accelerate.framework in Frameworks */, D91055371F3737540007F224 /* CoreText.framework in Frameworks */, D91055351F37374F0007F224 /* QuartzCore.framework in Frameworks */, D91055331F37374B0007F224 /* UIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ D910547A1F3735D20007F224 /* YYImage */ = { isa = PBXGroup; children = ( D910547B1F3735D20007F224 /* Animated image support.txt */, D910547C1F3735D20007F224 /* YYAnimatedImageView.h */, D910547D1F3735D20007F224 /* YYAnimatedImageView.m */, D910547E1F3735D20007F224 /* YYFrameImage.h */, D910547F1F3735D20007F224 /* YYFrameImage.m */, D91054801F3735D20007F224 /* YYImage.h */, D91054811F3735D20007F224 /* YYImage.m */, D91054821F3735D20007F224 /* YYImageCoder.h */, D91054831F3735D20007F224 /* YYImageCoder.m */, D91054841F3735D20007F224 /* YYSpriteSheetImage.h */, D91054851F3735D20007F224 /* YYSpriteSheetImage.m */, ); name = YYImage; path = YYTextDemo/YYImage; sourceTree = ""; }; D910548C1F3735E50007F224 /* YYText */ = { isa = PBXGroup; children = ( D910548D1F3735E50007F224 /* Component */, D91054A01F3735E50007F224 /* String */, D91054AB1F3735E50007F224 /* Utility */, D91054BC1F3735E50007F224 /* YYLabel.h */, D91054BD1F3735E50007F224 /* YYLabel.m */, D91054BE1F3735E50007F224 /* YYText.h */, D91054BF1F3735E50007F224 /* YYTextView.h */, D91054C01F3735E50007F224 /* YYTextView.m */, ); name = YYText; path = ../YYText; sourceTree = ""; }; D910548D1F3735E50007F224 /* Component */ = { isa = PBXGroup; children = ( D910548E1F3735E50007F224 /* YYTextContainerView.h */, D910548F1F3735E50007F224 /* YYTextContainerView.m */, D91054901F3735E50007F224 /* YYTextDebugOption.h */, D91054911F3735E50007F224 /* YYTextDebugOption.m */, D91054921F3735E50007F224 /* YYTextEffectWindow.h */, D91054931F3735E50007F224 /* YYTextEffectWindow.m */, D91054941F3735E50007F224 /* YYTextInput.h */, D91054951F3735E50007F224 /* YYTextInput.m */, D91054961F3735E50007F224 /* YYTextKeyboardManager.h */, D91054971F3735E50007F224 /* YYTextKeyboardManager.m */, D91054981F3735E50007F224 /* YYTextLayout.h */, D91054991F3735E50007F224 /* YYTextLayout.m */, D910549A1F3735E50007F224 /* YYTextLine.h */, D910549B1F3735E50007F224 /* YYTextLine.m */, D910549C1F3735E50007F224 /* YYTextMagnifier.h */, D910549D1F3735E50007F224 /* YYTextMagnifier.m */, D910549E1F3735E50007F224 /* YYTextSelectionView.h */, D910549F1F3735E50007F224 /* YYTextSelectionView.m */, ); path = Component; sourceTree = ""; }; D91054A01F3735E50007F224 /* String */ = { isa = PBXGroup; children = ( D91054A11F3735E50007F224 /* YYTextArchiver.h */, D91054A21F3735E50007F224 /* YYTextArchiver.m */, D91054A31F3735E50007F224 /* YYTextAttribute.h */, D91054A41F3735E50007F224 /* YYTextAttribute.m */, D91054A51F3735E50007F224 /* YYTextParser.h */, D91054A61F3735E50007F224 /* YYTextParser.m */, D91054A71F3735E50007F224 /* YYTextRubyAnnotation.h */, D91054A81F3735E50007F224 /* YYTextRubyAnnotation.m */, D91054A91F3735E50007F224 /* YYTextRunDelegate.h */, D91054AA1F3735E50007F224 /* YYTextRunDelegate.m */, ); path = String; sourceTree = ""; }; D91054AB1F3735E50007F224 /* Utility */ = { isa = PBXGroup; children = ( D91054AC1F3735E50007F224 /* NSAttributedString+YYText.h */, D91054AD1F3735E50007F224 /* NSAttributedString+YYText.m */, D91054AE1F3735E50007F224 /* NSParagraphStyle+YYText.h */, D91054AF1F3735E50007F224 /* NSParagraphStyle+YYText.m */, D91054B01F3735E50007F224 /* UIPasteboard+YYText.h */, D91054B11F3735E50007F224 /* UIPasteboard+YYText.m */, D91054B21F3735E50007F224 /* UIView+YYText.h */, D91054B31F3735E50007F224 /* UIView+YYText.m */, D91054B41F3735E50007F224 /* YYTextAsyncLayer.h */, D91054B51F3735E50007F224 /* YYTextAsyncLayer.m */, D91054B61F3735E50007F224 /* YYTextTransaction.h */, D91054B71F3735E50007F224 /* YYTextTransaction.m */, D91054B81F3735E50007F224 /* YYTextUtilities.h */, D91054B91F3735E50007F224 /* YYTextUtilities.m */, D91054BA1F3735E50007F224 /* YYTextWeakProxy.h */, D91054BB1F3735E50007F224 /* YYTextWeakProxy.m */, ); path = Utility; sourceTree = ""; }; D91054DF1F3736130007F224 /* Supporting Files */ = { isa = PBXGroup; children = ( D91054E01F3736700007F224 /* CALayer+YYAdd.h */, D91054E11F3736700007F224 /* CALayer+YYAdd.m */, D91054E21F3736700007F224 /* NSBundle+YYAdd.h */, D91054E31F3736700007F224 /* NSBundle+YYAdd.m */, D91054E41F3736700007F224 /* NSData+YYAdd.h */, D91054E51F3736700007F224 /* NSData+YYAdd.m */, D91054E61F3736700007F224 /* NSString+YYAdd.h */, D91054E71F3736700007F224 /* NSString+YYAdd.m */, D91054E81F3736700007F224 /* UIControl+YYAdd.h */, D91054E91F3736700007F224 /* UIControl+YYAdd.m */, D91054EA1F3736700007F224 /* UIGestureRecognizer+YYAdd.h */, D91054EB1F3736700007F224 /* UIGestureRecognizer+YYAdd.m */, D91054EC1F3736700007F224 /* UIImage+YYWebImage.h */, D91054ED1F3736700007F224 /* UIImage+YYWebImage.m */, D91054EE1F3736700007F224 /* UIView+YYAdd.h */, D91054EF1F3736700007F224 /* UIView+YYAdd.m */, D91054F01F3736700007F224 /* YYFPSLabel.h */, D91054F11F3736700007F224 /* YYFPSLabel.m */, D91054F21F3736700007F224 /* YYGestureRecognizer.h */, D91054F31F3736700007F224 /* YYGestureRecognizer.m */, D91054F41F3736700007F224 /* YYWeakProxy.h */, D91054F51F3736700007F224 /* YYWeakProxy.m */, D91054D91F3736060007F224 /* AppDelegate.h */, D91054DA1F3736060007F224 /* AppDelegate.m */, D91054DB1F3736060007F224 /* ViewController.h */, D91054DC1F3736060007F224 /* ViewController.m */, D94EE9941F37304200F37AD6 /* Main.storyboard */, D94EE9971F37304200F37AD6 /* Assets.xcassets */, D94EE9991F37304200F37AD6 /* LaunchScreen.storyboard */, D94EE99C1F37304200F37AD6 /* Info.plist */, D94EE99D1F37304200F37AD6 /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; D91055301F3736910007F224 /* Resources */ = { isa = PBXGroup; children = ( D910552A1F37368D0007F224 /* EmoticonQQ.bundle */, D91055281F37368D0007F224 /* dribbble64_imageio.png */, D91055291F37368D0007F224 /* dribbble256_imageio.png */, D910552B1F37368D0007F224 /* pia@2x.png */, ); name = Resources; sourceTree = ""; }; D91055311F37374B0007F224 /* Frameworks */ = { isa = PBXGroup; children = ( D91055401F37376C0007F224 /* libz.tbd */, D910553E1F3737670007F224 /* SystemConfiguration.framework */, D910553C1F3737640007F224 /* MobileCoreServices.framework */, D910553A1F37375F0007F224 /* ImageIO.framework */, D91055381F37375A0007F224 /* Accelerate.framework */, D91055361F3737540007F224 /* CoreText.framework */, D91055341F37374F0007F224 /* QuartzCore.framework */, D91055321F37374B0007F224 /* UIKit.framework */, ); name = Frameworks; sourceTree = ""; }; D94EE9821F37304200F37AD6 = { isa = PBXGroup; children = ( D910547A1F3735D20007F224 /* YYImage */, D910548C1F3735E50007F224 /* YYText */, D94EE98D1F37304200F37AD6 /* YYTextDemo */, D94EE98C1F37304200F37AD6 /* Products */, D91055311F37374B0007F224 /* Frameworks */, ); sourceTree = ""; }; D94EE98C1F37304200F37AD6 /* Products */ = { isa = PBXGroup; children = ( D94EE98B1F37304200F37AD6 /* YYTextDemo.app */, ); name = Products; sourceTree = ""; }; D94EE98D1F37304200F37AD6 /* YYTextDemo */ = { isa = PBXGroup; children = ( D910550F1F37367B0007F224 /* YYTextExample.h */, D91055101F37367B0007F224 /* YYTextExample.m */, D91055111F37367B0007F224 /* YYTextExampleHelper.h */, D91055121F37367B0007F224 /* YYTextExampleHelper.m */, D91055051F37367B0007F224 /* YYTextAttributeExample.h */, D91055061F37367B0007F224 /* YYTextAttributeExample.m */, D91055171F37367B0007F224 /* YYTextTagExample.h */, D91055181F37367B0007F224 /* YYTextTagExample.m */, D91055031F37367B0007F224 /* YYTextAttachmentExample.h */, D91055041F37367B0007F224 /* YYTextAttachmentExample.m */, D910550B1F37367B0007F224 /* YYTextEditExample.h */, D910550C1F37367B0007F224 /* YYTextEditExample.m */, D91055131F37367B0007F224 /* YYTextMarkdownExample.h */, D91055141F37367B0007F224 /* YYTextMarkdownExample.m */, D910550D1F37367B0007F224 /* YYTextEmoticonExample.h */, D910550E1F37367B0007F224 /* YYTextEmoticonExample.m */, D91055071F37367B0007F224 /* YYTextBindingExample.h */, D91055081F37367B0007F224 /* YYTextBindingExample.m */, D91055091F37367B0007F224 /* YYTextCopyPasteExample.h */, D910550A1F37367B0007F224 /* YYTextCopyPasteExample.m */, D91055191F37367B0007F224 /* YYTextUndoRedoExample.h */, D910551A1F37367B0007F224 /* YYTextUndoRedoExample.m */, D91055151F37367B0007F224 /* YYTextRubyExample.h */, D91055161F37367B0007F224 /* YYTextRubyExample.m */, D91055011F37367B0007F224 /* YYTextAsyncExample.h */, D91055021F37367B0007F224 /* YYTextAsyncExample.m */, D91055301F3736910007F224 /* Resources */, D91054DF1F3736130007F224 /* Supporting Files */, ); path = YYTextDemo; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ D94EE98A1F37304200F37AD6 /* YYTextDemo */ = { isa = PBXNativeTarget; buildConfigurationList = D94EE9A11F37304200F37AD6 /* Build configuration list for PBXNativeTarget "YYTextDemo" */; buildPhases = ( D94EE9871F37304200F37AD6 /* Sources */, D94EE9881F37304200F37AD6 /* Frameworks */, D94EE9891F37304200F37AD6 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = YYTextDemo; productName = YYTextDemo; productReference = D94EE98B1F37304200F37AD6 /* YYTextDemo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ D94EE9831F37304200F37AD6 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 0900; ORGANIZATIONNAME = ibireme; TargetAttributes = { D94EE98A1F37304200F37AD6 = { CreatedOnToolsVersion = 9.0; }; }; }; buildConfigurationList = D94EE9861F37304200F37AD6 /* Build configuration list for PBXProject "YYTextDemo" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = D94EE9821F37304200F37AD6; productRefGroup = D94EE98C1F37304200F37AD6 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( D94EE98A1F37304200F37AD6 /* YYTextDemo */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ D94EE9891F37304200F37AD6 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( D94EE99B1F37304200F37AD6 /* LaunchScreen.storyboard in Resources */, D910552F1F37368D0007F224 /* pia@2x.png in Resources */, D910552D1F37368D0007F224 /* dribbble256_imageio.png in Resources */, D94EE9981F37304200F37AD6 /* Assets.xcassets in Resources */, D94EE9961F37304200F37AD6 /* Main.storyboard in Resources */, D91054861F3735D20007F224 /* Animated image support.txt in Resources */, D910552C1F37368D0007F224 /* dribbble64_imageio.png in Resources */, D910552E1F37368D0007F224 /* EmoticonQQ.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ D94EE9871F37304200F37AD6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D91054CE1F3735E50007F224 /* YYTextRunDelegate.m in Sources */, D91054FE1F3736700007F224 /* YYFPSLabel.m in Sources */, D91054C11F3735E50007F224 /* YYTextContainerView.m in Sources */, D91055211F37367B0007F224 /* YYTextEmoticonExample.m in Sources */, D91054881F3735D20007F224 /* YYFrameImage.m in Sources */, D910551B1F37367B0007F224 /* YYTextAsyncExample.m in Sources */, D94EE99E1F37304200F37AD6 /* main.m in Sources */, D91055251F37367B0007F224 /* YYTextRubyExample.m in Sources */, D91054FC1F3736700007F224 /* UIImage+YYWebImage.m in Sources */, D91054D11F3735E50007F224 /* UIPasteboard+YYText.m in Sources */, D91054F61F3736700007F224 /* CALayer+YYAdd.m in Sources */, D91054D01F3735E50007F224 /* NSParagraphStyle+YYText.m in Sources */, D91054FD1F3736700007F224 /* UIView+YYAdd.m in Sources */, D91054D51F3735E50007F224 /* YYTextUtilities.m in Sources */, D91054D71F3735E50007F224 /* YYLabel.m in Sources */, D91054CC1F3735E50007F224 /* YYTextParser.m in Sources */, D91055221F37367B0007F224 /* YYTextExample.m in Sources */, D91054F81F3736700007F224 /* NSData+YYAdd.m in Sources */, D91054F71F3736700007F224 /* NSBundle+YYAdd.m in Sources */, D91054C81F3735E50007F224 /* YYTextMagnifier.m in Sources */, D91054DE1F3736060007F224 /* ViewController.m in Sources */, D910551D1F37367B0007F224 /* YYTextAttributeExample.m in Sources */, D91055261F37367B0007F224 /* YYTextTagExample.m in Sources */, D91054C21F3735E50007F224 /* YYTextDebugOption.m in Sources */, D91054C51F3735E50007F224 /* YYTextKeyboardManager.m in Sources */, D91054FF1F3736700007F224 /* YYGestureRecognizer.m in Sources */, D91055201F37367B0007F224 /* YYTextEditExample.m in Sources */, D91054CF1F3735E50007F224 /* NSAttributedString+YYText.m in Sources */, D91055001F3736700007F224 /* YYWeakProxy.m in Sources */, D91055231F37367B0007F224 /* YYTextExampleHelper.m in Sources */, D910551C1F37367B0007F224 /* YYTextAttachmentExample.m in Sources */, D91054C71F3735E50007F224 /* YYTextLine.m in Sources */, D91054CD1F3735E50007F224 /* YYTextRubyAnnotation.m in Sources */, D91055241F37367B0007F224 /* YYTextMarkdownExample.m in Sources */, D91054D41F3735E50007F224 /* YYTextTransaction.m in Sources */, D910548B1F3735D20007F224 /* YYSpriteSheetImage.m in Sources */, D91054CA1F3735E50007F224 /* YYTextArchiver.m in Sources */, D910551E1F37367B0007F224 /* YYTextBindingExample.m in Sources */, D91054D61F3735E50007F224 /* YYTextWeakProxy.m in Sources */, D91054C41F3735E50007F224 /* YYTextInput.m in Sources */, D91054871F3735D20007F224 /* YYAnimatedImageView.m in Sources */, D91054D21F3735E50007F224 /* UIView+YYText.m in Sources */, D91054C61F3735E50007F224 /* YYTextLayout.m in Sources */, D91054FA1F3736700007F224 /* UIControl+YYAdd.m in Sources */, D91054DD1F3736060007F224 /* AppDelegate.m in Sources */, D91054D81F3735E50007F224 /* YYTextView.m in Sources */, D91054F91F3736700007F224 /* NSString+YYAdd.m in Sources */, D910548A1F3735D20007F224 /* YYImageCoder.m in Sources */, D91054D31F3735E50007F224 /* YYTextAsyncLayer.m in Sources */, D91054C31F3735E50007F224 /* YYTextEffectWindow.m in Sources */, D91054891F3735D20007F224 /* YYImage.m in Sources */, D91054C91F3735E50007F224 /* YYTextSelectionView.m in Sources */, D91054CB1F3735E50007F224 /* YYTextAttribute.m in Sources */, D91055271F37367B0007F224 /* YYTextUndoRedoExample.m in Sources */, D910551F1F37367B0007F224 /* YYTextCopyPasteExample.m in Sources */, D91054FB1F3736700007F224 /* UIGestureRecognizer+YYAdd.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ D94EE9941F37304200F37AD6 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( D94EE9951F37304200F37AD6 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; D94EE9991F37304200F37AD6 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( D94EE99A1F37304200F37AD6 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ D94EE99F1F37304200F37AD6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; name = Debug; }; D94EE9A01F37304200F37AD6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; D94EE9A21F37304200F37AD6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMPRESS_PNG_FILES = NO; INFOPLIST_FILE = YYTextDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ibireme.YYTextDemo; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_PNG_TEXT = NO; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; D94EE9A31F37304200F37AD6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMPRESS_PNG_FILES = NO; INFOPLIST_FILE = YYTextDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ibireme.YYTextDemo; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_PNG_TEXT = NO; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ D94EE9861F37304200F37AD6 /* Build configuration list for PBXProject "YYTextDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( D94EE99F1F37304200F37AD6 /* Debug */, D94EE9A01F37304200F37AD6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D94EE9A11F37304200F37AD6 /* Build configuration list for PBXNativeTarget "YYTextDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( D94EE9A21F37304200F37AD6 /* Debug */, D94EE9A31F37304200F37AD6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = D94EE9831F37304200F37AD6 /* Project object */; } ================================================ FILE: Demo/YYTextDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Framework/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 1.0.7 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Framework/YYText.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 48; objects = { /* Begin PBXBuildFile section */ D995E6BA1F372EC800EBEE44 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D918BFE71F372EA000106E14 /* MobileCoreServices.framework */; }; D995E6BB1F372EC800EBEE44 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D918BFE61F372E9B00106E14 /* Accelerate.framework */; }; D995E6BC1F372EC800EBEE44 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D918BFE51F372E9800106E14 /* CoreText.framework */; }; D995E6BD1F372EC800EBEE44 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D918BFE41F372E7A00106E14 /* QuartzCore.framework */; }; D995E6BE1F372EC800EBEE44 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D918BFE31F372E7600106E14 /* UIKit.framework */; }; D995E6BF1F372EC800EBEE44 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D918BFE21F372E6800106E14 /* CoreFoundation.framework */; }; D995E6F51F372ED100EBEE44 /* YYTextContainerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6C21F372ED100EBEE44 /* YYTextContainerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E6F61F372ED100EBEE44 /* YYTextContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6C31F372ED100EBEE44 /* YYTextContainerView.m */; }; D995E6F71F372ED100EBEE44 /* YYTextDebugOption.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6C41F372ED100EBEE44 /* YYTextDebugOption.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E6F81F372ED100EBEE44 /* YYTextDebugOption.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6C51F372ED100EBEE44 /* YYTextDebugOption.m */; }; D995E6F91F372ED100EBEE44 /* YYTextEffectWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6C61F372ED100EBEE44 /* YYTextEffectWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E6FA1F372ED100EBEE44 /* YYTextEffectWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6C71F372ED100EBEE44 /* YYTextEffectWindow.m */; }; D995E6FB1F372ED100EBEE44 /* YYTextInput.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6C81F372ED100EBEE44 /* YYTextInput.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E6FC1F372ED100EBEE44 /* YYTextInput.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6C91F372ED100EBEE44 /* YYTextInput.m */; }; D995E6FD1F372ED100EBEE44 /* YYTextKeyboardManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6CA1F372ED100EBEE44 /* YYTextKeyboardManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E6FE1F372ED100EBEE44 /* YYTextKeyboardManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6CB1F372ED100EBEE44 /* YYTextKeyboardManager.m */; }; D995E6FF1F372ED100EBEE44 /* YYTextLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6CC1F372ED100EBEE44 /* YYTextLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7001F372ED100EBEE44 /* YYTextLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6CD1F372ED100EBEE44 /* YYTextLayout.m */; }; D995E7011F372ED100EBEE44 /* YYTextLine.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6CE1F372ED100EBEE44 /* YYTextLine.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7021F372ED100EBEE44 /* YYTextLine.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6CF1F372ED100EBEE44 /* YYTextLine.m */; }; D995E7031F372ED100EBEE44 /* YYTextMagnifier.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6D01F372ED100EBEE44 /* YYTextMagnifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7041F372ED100EBEE44 /* YYTextMagnifier.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6D11F372ED100EBEE44 /* YYTextMagnifier.m */; }; D995E7051F372ED100EBEE44 /* YYTextSelectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6D21F372ED100EBEE44 /* YYTextSelectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7061F372ED100EBEE44 /* YYTextSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6D31F372ED100EBEE44 /* YYTextSelectionView.m */; }; D995E7071F372ED100EBEE44 /* YYTextArchiver.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6D51F372ED100EBEE44 /* YYTextArchiver.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7081F372ED100EBEE44 /* YYTextArchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6D61F372ED100EBEE44 /* YYTextArchiver.m */; }; D995E7091F372ED100EBEE44 /* YYTextAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6D71F372ED100EBEE44 /* YYTextAttribute.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E70A1F372ED100EBEE44 /* YYTextAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6D81F372ED100EBEE44 /* YYTextAttribute.m */; }; D995E70B1F372ED100EBEE44 /* YYTextParser.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6D91F372ED100EBEE44 /* YYTextParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E70C1F372ED100EBEE44 /* YYTextParser.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6DA1F372ED100EBEE44 /* YYTextParser.m */; }; D995E70D1F372ED100EBEE44 /* YYTextRubyAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6DB1F372ED100EBEE44 /* YYTextRubyAnnotation.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E70E1F372ED100EBEE44 /* YYTextRubyAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6DC1F372ED100EBEE44 /* YYTextRubyAnnotation.m */; }; D995E70F1F372ED100EBEE44 /* YYTextRunDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6DD1F372ED100EBEE44 /* YYTextRunDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7101F372ED100EBEE44 /* YYTextRunDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6DE1F372ED100EBEE44 /* YYTextRunDelegate.m */; }; D995E7111F372ED100EBEE44 /* NSAttributedString+YYText.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6E01F372ED100EBEE44 /* NSAttributedString+YYText.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7121F372ED100EBEE44 /* NSAttributedString+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6E11F372ED100EBEE44 /* NSAttributedString+YYText.m */; }; D995E7131F372ED100EBEE44 /* NSParagraphStyle+YYText.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6E21F372ED100EBEE44 /* NSParagraphStyle+YYText.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7141F372ED100EBEE44 /* NSParagraphStyle+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6E31F372ED100EBEE44 /* NSParagraphStyle+YYText.m */; }; D995E7151F372ED100EBEE44 /* UIPasteboard+YYText.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6E41F372ED100EBEE44 /* UIPasteboard+YYText.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7161F372ED100EBEE44 /* UIPasteboard+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6E51F372ED100EBEE44 /* UIPasteboard+YYText.m */; }; D995E7171F372ED100EBEE44 /* UIView+YYText.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6E61F372ED100EBEE44 /* UIView+YYText.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7181F372ED100EBEE44 /* UIView+YYText.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6E71F372ED100EBEE44 /* UIView+YYText.m */; }; D995E7191F372ED100EBEE44 /* YYTextAsyncLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6E81F372ED100EBEE44 /* YYTextAsyncLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E71A1F372ED100EBEE44 /* YYTextAsyncLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6E91F372ED100EBEE44 /* YYTextAsyncLayer.m */; }; D995E71B1F372ED100EBEE44 /* YYTextTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6EA1F372ED100EBEE44 /* YYTextTransaction.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E71C1F372ED100EBEE44 /* YYTextTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6EB1F372ED100EBEE44 /* YYTextTransaction.m */; }; D995E71D1F372ED100EBEE44 /* YYTextUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6EC1F372ED100EBEE44 /* YYTextUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E71E1F372ED100EBEE44 /* YYTextUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6ED1F372ED100EBEE44 /* YYTextUtilities.m */; }; D995E71F1F372ED100EBEE44 /* YYTextWeakProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6EE1F372ED100EBEE44 /* YYTextWeakProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7201F372ED100EBEE44 /* YYTextWeakProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6EF1F372ED100EBEE44 /* YYTextWeakProxy.m */; }; D995E7211F372ED100EBEE44 /* YYLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6F01F372ED100EBEE44 /* YYLabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7221F372ED100EBEE44 /* YYLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6F11F372ED100EBEE44 /* YYLabel.m */; }; D995E7231F372ED100EBEE44 /* YYText.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6F21F372ED100EBEE44 /* YYText.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7241F372ED100EBEE44 /* YYTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = D995E6F31F372ED100EBEE44 /* YYTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; D995E7251F372ED100EBEE44 /* YYTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = D995E6F41F372ED100EBEE44 /* YYTextView.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ D918BFD61F372E3000106E14 /* YYText.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = YYText.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D918BFDA1F372E3000106E14 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D918BFE21F372E6800106E14 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; D918BFE31F372E7600106E14 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; D918BFE41F372E7A00106E14 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; D918BFE51F372E9800106E14 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; D918BFE61F372E9B00106E14 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; D918BFE71F372EA000106E14 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; D995E6C21F372ED100EBEE44 /* YYTextContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextContainerView.h; sourceTree = ""; }; D995E6C31F372ED100EBEE44 /* YYTextContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextContainerView.m; sourceTree = ""; }; D995E6C41F372ED100EBEE44 /* YYTextDebugOption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextDebugOption.h; sourceTree = ""; }; D995E6C51F372ED100EBEE44 /* YYTextDebugOption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextDebugOption.m; sourceTree = ""; }; D995E6C61F372ED100EBEE44 /* YYTextEffectWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextEffectWindow.h; sourceTree = ""; }; D995E6C71F372ED100EBEE44 /* YYTextEffectWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextEffectWindow.m; sourceTree = ""; }; D995E6C81F372ED100EBEE44 /* YYTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextInput.h; sourceTree = ""; }; D995E6C91F372ED100EBEE44 /* YYTextInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextInput.m; sourceTree = ""; }; D995E6CA1F372ED100EBEE44 /* YYTextKeyboardManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextKeyboardManager.h; sourceTree = ""; }; D995E6CB1F372ED100EBEE44 /* YYTextKeyboardManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextKeyboardManager.m; sourceTree = ""; }; D995E6CC1F372ED100EBEE44 /* YYTextLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextLayout.h; sourceTree = ""; }; D995E6CD1F372ED100EBEE44 /* YYTextLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextLayout.m; sourceTree = ""; }; D995E6CE1F372ED100EBEE44 /* YYTextLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextLine.h; sourceTree = ""; }; D995E6CF1F372ED100EBEE44 /* YYTextLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextLine.m; sourceTree = ""; }; D995E6D01F372ED100EBEE44 /* YYTextMagnifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextMagnifier.h; sourceTree = ""; }; D995E6D11F372ED100EBEE44 /* YYTextMagnifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextMagnifier.m; sourceTree = ""; }; D995E6D21F372ED100EBEE44 /* YYTextSelectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextSelectionView.h; sourceTree = ""; }; D995E6D31F372ED100EBEE44 /* YYTextSelectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextSelectionView.m; sourceTree = ""; }; D995E6D51F372ED100EBEE44 /* YYTextArchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextArchiver.h; sourceTree = ""; }; D995E6D61F372ED100EBEE44 /* YYTextArchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextArchiver.m; sourceTree = ""; }; D995E6D71F372ED100EBEE44 /* YYTextAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAttribute.h; sourceTree = ""; }; D995E6D81F372ED100EBEE44 /* YYTextAttribute.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAttribute.m; sourceTree = ""; }; D995E6D91F372ED100EBEE44 /* YYTextParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextParser.h; sourceTree = ""; }; D995E6DA1F372ED100EBEE44 /* YYTextParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextParser.m; sourceTree = ""; }; D995E6DB1F372ED100EBEE44 /* YYTextRubyAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRubyAnnotation.h; sourceTree = ""; }; D995E6DC1F372ED100EBEE44 /* YYTextRubyAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRubyAnnotation.m; sourceTree = ""; }; D995E6DD1F372ED100EBEE44 /* YYTextRunDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextRunDelegate.h; sourceTree = ""; }; D995E6DE1F372ED100EBEE44 /* YYTextRunDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextRunDelegate.m; sourceTree = ""; }; D995E6E01F372ED100EBEE44 /* NSAttributedString+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+YYText.h"; sourceTree = ""; }; D995E6E11F372ED100EBEE44 /* NSAttributedString+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+YYText.m"; sourceTree = ""; }; D995E6E21F372ED100EBEE44 /* NSParagraphStyle+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+YYText.h"; sourceTree = ""; }; D995E6E31F372ED100EBEE44 /* NSParagraphStyle+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSParagraphStyle+YYText.m"; sourceTree = ""; }; D995E6E41F372ED100EBEE44 /* UIPasteboard+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIPasteboard+YYText.h"; sourceTree = ""; }; D995E6E51F372ED100EBEE44 /* UIPasteboard+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIPasteboard+YYText.m"; sourceTree = ""; }; D995E6E61F372ED100EBEE44 /* UIView+YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+YYText.h"; sourceTree = ""; }; D995E6E71F372ED100EBEE44 /* UIView+YYText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+YYText.m"; sourceTree = ""; }; D995E6E81F372ED100EBEE44 /* YYTextAsyncLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextAsyncLayer.h; sourceTree = ""; }; D995E6E91F372ED100EBEE44 /* YYTextAsyncLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextAsyncLayer.m; sourceTree = ""; }; D995E6EA1F372ED100EBEE44 /* YYTextTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextTransaction.h; sourceTree = ""; }; D995E6EB1F372ED100EBEE44 /* YYTextTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextTransaction.m; sourceTree = ""; }; D995E6EC1F372ED100EBEE44 /* YYTextUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextUtilities.h; sourceTree = ""; }; D995E6ED1F372ED100EBEE44 /* YYTextUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextUtilities.m; sourceTree = ""; }; D995E6EE1F372ED100EBEE44 /* YYTextWeakProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextWeakProxy.h; sourceTree = ""; }; D995E6EF1F372ED100EBEE44 /* YYTextWeakProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextWeakProxy.m; sourceTree = ""; }; D995E6F01F372ED100EBEE44 /* YYLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYLabel.h; sourceTree = ""; }; D995E6F11F372ED100EBEE44 /* YYLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYLabel.m; sourceTree = ""; }; D995E6F21F372ED100EBEE44 /* YYText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYText.h; sourceTree = ""; }; D995E6F31F372ED100EBEE44 /* YYTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YYTextView.h; sourceTree = ""; }; D995E6F41F372ED100EBEE44 /* YYTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTextView.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ D918BFD21F372E3000106E14 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D995E6BA1F372EC800EBEE44 /* MobileCoreServices.framework in Frameworks */, D995E6BB1F372EC800EBEE44 /* Accelerate.framework in Frameworks */, D995E6BC1F372EC800EBEE44 /* CoreText.framework in Frameworks */, D995E6BD1F372EC800EBEE44 /* QuartzCore.framework in Frameworks */, D995E6BE1F372EC800EBEE44 /* UIKit.framework in Frameworks */, D995E6BF1F372EC800EBEE44 /* CoreFoundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ D918BFCC1F372E3000106E14 = { isa = PBXGroup; children = ( D995E6C01F372ED100EBEE44 /* YYText */, D918BFE11F372E6800106E14 /* Frameworks */, D918BFD71F372E3000106E14 /* Products */, ); sourceTree = ""; }; D918BFD71F372E3000106E14 /* Products */ = { isa = PBXGroup; children = ( D918BFD61F372E3000106E14 /* YYText.framework */, ); name = Products; sourceTree = ""; }; D918BFE11F372E6800106E14 /* Frameworks */ = { isa = PBXGroup; children = ( D918BFDA1F372E3000106E14 /* Info.plist */, D918BFE71F372EA000106E14 /* MobileCoreServices.framework */, D918BFE61F372E9B00106E14 /* Accelerate.framework */, D918BFE51F372E9800106E14 /* CoreText.framework */, D918BFE41F372E7A00106E14 /* QuartzCore.framework */, D918BFE31F372E7600106E14 /* UIKit.framework */, D918BFE21F372E6800106E14 /* CoreFoundation.framework */, ); name = Frameworks; sourceTree = ""; }; D995E6C01F372ED100EBEE44 /* YYText */ = { isa = PBXGroup; children = ( D995E6F21F372ED100EBEE44 /* YYText.h */, D995E6F01F372ED100EBEE44 /* YYLabel.h */, D995E6F11F372ED100EBEE44 /* YYLabel.m */, D995E6F31F372ED100EBEE44 /* YYTextView.h */, D995E6F41F372ED100EBEE44 /* YYTextView.m */, D995E6D41F372ED100EBEE44 /* String */, D995E6C11F372ED100EBEE44 /* Component */, D995E6DF1F372ED100EBEE44 /* Utility */, ); name = YYText; path = ../YYText; sourceTree = ""; }; D995E6C11F372ED100EBEE44 /* Component */ = { isa = PBXGroup; children = ( D995E6CC1F372ED100EBEE44 /* YYTextLayout.h */, D995E6CD1F372ED100EBEE44 /* YYTextLayout.m */, D995E6CE1F372ED100EBEE44 /* YYTextLine.h */, D995E6CF1F372ED100EBEE44 /* YYTextLine.m */, D995E6C81F372ED100EBEE44 /* YYTextInput.h */, D995E6C91F372ED100EBEE44 /* YYTextInput.m */, D995E6C21F372ED100EBEE44 /* YYTextContainerView.h */, D995E6C31F372ED100EBEE44 /* YYTextContainerView.m */, D995E6C61F372ED100EBEE44 /* YYTextEffectWindow.h */, D995E6C71F372ED100EBEE44 /* YYTextEffectWindow.m */, D995E6C41F372ED100EBEE44 /* YYTextDebugOption.h */, D995E6C51F372ED100EBEE44 /* YYTextDebugOption.m */, D995E6D01F372ED100EBEE44 /* YYTextMagnifier.h */, D995E6D11F372ED100EBEE44 /* YYTextMagnifier.m */, D995E6D21F372ED100EBEE44 /* YYTextSelectionView.h */, D995E6D31F372ED100EBEE44 /* YYTextSelectionView.m */, D995E6CA1F372ED100EBEE44 /* YYTextKeyboardManager.h */, D995E6CB1F372ED100EBEE44 /* YYTextKeyboardManager.m */, ); path = Component; sourceTree = ""; }; D995E6D41F372ED100EBEE44 /* String */ = { isa = PBXGroup; children = ( D995E6D71F372ED100EBEE44 /* YYTextAttribute.h */, D995E6D81F372ED100EBEE44 /* YYTextAttribute.m */, D995E6D91F372ED100EBEE44 /* YYTextParser.h */, D995E6DA1F372ED100EBEE44 /* YYTextParser.m */, D995E6D51F372ED100EBEE44 /* YYTextArchiver.h */, D995E6D61F372ED100EBEE44 /* YYTextArchiver.m */, D995E6DB1F372ED100EBEE44 /* YYTextRubyAnnotation.h */, D995E6DC1F372ED100EBEE44 /* YYTextRubyAnnotation.m */, D995E6DD1F372ED100EBEE44 /* YYTextRunDelegate.h */, D995E6DE1F372ED100EBEE44 /* YYTextRunDelegate.m */, ); path = String; sourceTree = ""; }; D995E6DF1F372ED100EBEE44 /* Utility */ = { isa = PBXGroup; children = ( D995E6E01F372ED100EBEE44 /* NSAttributedString+YYText.h */, D995E6E11F372ED100EBEE44 /* NSAttributedString+YYText.m */, D995E6E21F372ED100EBEE44 /* NSParagraphStyle+YYText.h */, D995E6E31F372ED100EBEE44 /* NSParagraphStyle+YYText.m */, D995E6E41F372ED100EBEE44 /* UIPasteboard+YYText.h */, D995E6E51F372ED100EBEE44 /* UIPasteboard+YYText.m */, D995E6E61F372ED100EBEE44 /* UIView+YYText.h */, D995E6E71F372ED100EBEE44 /* UIView+YYText.m */, D995E6E81F372ED100EBEE44 /* YYTextAsyncLayer.h */, D995E6E91F372ED100EBEE44 /* YYTextAsyncLayer.m */, D995E6EA1F372ED100EBEE44 /* YYTextTransaction.h */, D995E6EB1F372ED100EBEE44 /* YYTextTransaction.m */, D995E6EC1F372ED100EBEE44 /* YYTextUtilities.h */, D995E6ED1F372ED100EBEE44 /* YYTextUtilities.m */, D995E6EE1F372ED100EBEE44 /* YYTextWeakProxy.h */, D995E6EF1F372ED100EBEE44 /* YYTextWeakProxy.m */, ); path = Utility; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ D918BFD31F372E3000106E14 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( D995E70B1F372ED100EBEE44 /* YYTextParser.h in Headers */, D995E7111F372ED100EBEE44 /* NSAttributedString+YYText.h in Headers */, D995E71F1F372ED100EBEE44 /* YYTextWeakProxy.h in Headers */, D995E7031F372ED100EBEE44 /* YYTextMagnifier.h in Headers */, D995E70D1F372ED100EBEE44 /* YYTextRubyAnnotation.h in Headers */, D995E7231F372ED100EBEE44 /* YYText.h in Headers */, D995E7171F372ED100EBEE44 /* UIView+YYText.h in Headers */, D995E7051F372ED100EBEE44 /* YYTextSelectionView.h in Headers */, D995E6F71F372ED100EBEE44 /* YYTextDebugOption.h in Headers */, D995E7191F372ED100EBEE44 /* YYTextAsyncLayer.h in Headers */, D995E7091F372ED100EBEE44 /* YYTextAttribute.h in Headers */, D995E71D1F372ED100EBEE44 /* YYTextUtilities.h in Headers */, D995E7241F372ED100EBEE44 /* YYTextView.h in Headers */, D995E70F1F372ED100EBEE44 /* YYTextRunDelegate.h in Headers */, D995E6F91F372ED100EBEE44 /* YYTextEffectWindow.h in Headers */, D995E7151F372ED100EBEE44 /* UIPasteboard+YYText.h in Headers */, D995E7131F372ED100EBEE44 /* NSParagraphStyle+YYText.h in Headers */, D995E6FB1F372ED100EBEE44 /* YYTextInput.h in Headers */, D995E7211F372ED100EBEE44 /* YYLabel.h in Headers */, D995E71B1F372ED100EBEE44 /* YYTextTransaction.h in Headers */, D995E6FD1F372ED100EBEE44 /* YYTextKeyboardManager.h in Headers */, D995E7011F372ED100EBEE44 /* YYTextLine.h in Headers */, D995E6F51F372ED100EBEE44 /* YYTextContainerView.h in Headers */, D995E6FF1F372ED100EBEE44 /* YYTextLayout.h in Headers */, D995E7071F372ED100EBEE44 /* YYTextArchiver.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ D918BFD51F372E3000106E14 /* YYText */ = { isa = PBXNativeTarget; buildConfigurationList = D918BFDE1F372E3000106E14 /* Build configuration list for PBXNativeTarget "YYText" */; buildPhases = ( D918BFD11F372E3000106E14 /* Sources */, D918BFD21F372E3000106E14 /* Frameworks */, D918BFD31F372E3000106E14 /* Headers */, D918BFD41F372E3000106E14 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = YYText; productName = YYText; productReference = D918BFD61F372E3000106E14 /* YYText.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ D918BFCD1F372E3000106E14 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 0900; ORGANIZATIONNAME = ibireme; TargetAttributes = { D918BFD51F372E3000106E14 = { CreatedOnToolsVersion = 9.0; }; }; }; buildConfigurationList = D918BFD01F372E3000106E14 /* Build configuration list for PBXProject "YYText" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, ); mainGroup = D918BFCC1F372E3000106E14; productRefGroup = D918BFD71F372E3000106E14 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( D918BFD51F372E3000106E14 /* YYText */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ D918BFD41F372E3000106E14 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ D918BFD11F372E3000106E14 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D995E7021F372ED100EBEE44 /* YYTextLine.m in Sources */, D995E7251F372ED100EBEE44 /* YYTextView.m in Sources */, D995E7121F372ED100EBEE44 /* NSAttributedString+YYText.m in Sources */, D995E70C1F372ED100EBEE44 /* YYTextParser.m in Sources */, D995E7061F372ED100EBEE44 /* YYTextSelectionView.m in Sources */, D995E7161F372ED100EBEE44 /* UIPasteboard+YYText.m in Sources */, D995E7221F372ED100EBEE44 /* YYLabel.m in Sources */, D995E7001F372ED100EBEE44 /* YYTextLayout.m in Sources */, D995E71E1F372ED100EBEE44 /* YYTextUtilities.m in Sources */, D995E6F61F372ED100EBEE44 /* YYTextContainerView.m in Sources */, D995E70E1F372ED100EBEE44 /* YYTextRubyAnnotation.m in Sources */, D995E7201F372ED100EBEE44 /* YYTextWeakProxy.m in Sources */, D995E7081F372ED100EBEE44 /* YYTextArchiver.m in Sources */, D995E7101F372ED100EBEE44 /* YYTextRunDelegate.m in Sources */, D995E6F81F372ED100EBEE44 /* YYTextDebugOption.m in Sources */, D995E6FA1F372ED100EBEE44 /* YYTextEffectWindow.m in Sources */, D995E7041F372ED100EBEE44 /* YYTextMagnifier.m in Sources */, D995E7181F372ED100EBEE44 /* UIView+YYText.m in Sources */, D995E6FE1F372ED100EBEE44 /* YYTextKeyboardManager.m in Sources */, D995E6FC1F372ED100EBEE44 /* YYTextInput.m in Sources */, D995E7141F372ED100EBEE44 /* NSParagraphStyle+YYText.m in Sources */, D995E71A1F372ED100EBEE44 /* YYTextAsyncLayer.m in Sources */, D995E70A1F372ED100EBEE44 /* YYTextAttribute.m in Sources */, D995E71C1F372ED100EBEE44 /* YYTextTransaction.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ D918BFDC1F372E3000106E14 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; D918BFDD1F372E3000106E14 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; D918BFDF1F372E3000106E14 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ibireme.YYText; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; D918BFE01F372E3000106E14 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ibireme.YYText; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ D918BFD01F372E3000106E14 /* Build configuration list for PBXProject "YYText" */ = { isa = XCConfigurationList; buildConfigurations = ( D918BFDC1F372E3000106E14 /* Debug */, D918BFDD1F372E3000106E14 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D918BFDE1F372E3000106E14 /* Build configuration list for PBXNativeTarget "YYText" */ = { isa = XCConfigurationList; buildConfigurations = ( D918BFDF1F372E3000106E14 /* Debug */, D918BFE01F372E3000106E14 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = D918BFCD1F372E3000106E14 /* Project object */; } ================================================ FILE: Framework/YYText.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Framework/YYText.xcodeproj/xcshareddata/xcschemes/YYText.xcscheme ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 ibireme Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ YYText ============== [![License MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/ibireme/YYText/master/LICENSE)  [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)  [![CocoaPods](http://img.shields.io/cocoapods/v/YYText.svg?style=flat)](http://cocoapods.org/pods/YYText)  [![CocoaPods](http://img.shields.io/cocoapods/p/YYText.svg?style=flat)](http://cocoadocs.org/docsets/YYText)  [![Support](https://img.shields.io/badge/support-iOS%206%2B%20-blue.svg?style=flat)](https://www.apple.com/nl/ios/)  [![Build Status](https://travis-ci.org/ibireme/YYText.svg?branch=master)](https://travis-ci.org/ibireme/YYText) Powerful text framework for iOS to display and edit rich text.
(It's a component of [YYKit](https://github.com/ibireme/YYKit)) Features ============== - UILabel and UITextView API compatible - High performance asynchronous text layout and rendering - Extended CoreText attributes with more text effects - Text attachments with UIImage, UIView and CALayer - Custom highlight text range to allow user interact with - Text parser support (built in markdown/emoticon parser) - Text container path and exclusion paths support - Vertical form layout support (for CJK text) - Image and attributed text copy/paste support - Attributed text placeholder support - Custom keyboard view support - Undo and redo control - Attributed text archiver and unarchiver support - Multi-language and VoiceOver support - Interface Builder support - Fully documented Architecture ============== YYText vs TextKit Text Attributes ============== ### YYText supported attributes
Demo Attribute Name Class
TextAttachment YYTextAttachment
TextHighlight YYTextHighlight
TextBinding YYTextBinding
TextShadow
TextInnerShadow
YYTextShadow
TextBorder YYTextBorder
TextBackgroundBorder YYTextBorder
TextBlockBorder YYTextBorder
TextGlyphTransform NSValue(CGAffineTransform)
TextUnderline YYTextDecoration
TextStrickthrough YYTextDecoration
TextBackedString YYTextBackedString
### CoreText attributes which is supported by YYText
Demo Attribute Name Class
Font UIFont(CTFontRef)
Kern NSNumber
StrokeWidth NSNumber
StrokeColor CGColorRef
Shadow NSShadow
Ligature NSNumber
VerticalGlyphForm NSNumber(BOOL)
WritingDirection NSArray(NSNumber)
RunDelegate CTRunDelegateRef
TextAlignment NSParagraphStyle
(NSTextAlignment)
LineBreakMode NSParagraphStyle
(NSLineBreakMode)
LineSpacing NSParagraphStyle
(CGFloat)
ParagraphSpacing
ParagraphSpacingBefore
NSParagraphStyle
(CGFloat)
FirstLineHeadIndent NSParagraphStyle
(CGFloat)
HeadIndent NSParagraphStyle
(CGFloat)
TailIndent NSParagraphStyle
(CGFloat)
MinimumLineHeight NSParagraphStyle
(CGFloat)
MaximumLineHeight NSParagraphStyle
(CGFloat)
LineHeightMultiple NSParagraphStyle
(CGFloat)
BaseWritingDirection NSParagraphStyle
(NSWritingDirection)
DefaultTabInterval
TabStops
NSParagraphStyle
CGFloat/NSArray(NSTextTab)
Usage ============== ### Basic ```objc // YYLabel (similar to UILabel) YYLabel *label = [YYLabel new]; label.frame = ... label.font = ... label.textColor = ... label.textAlignment = ... label.lineBreakMode = ... label.numberOfLines = ... label.text = ... // YYTextView (similar to UITextView) YYTextView *textView = [YYTextView new]; textView.frame = ... textView.font = ... textView.textColor = ... textView.dataDetectorTypes = ... textView.placeHolderText = ... textView.placeHolderTextColor = ... textView.delegate = ... ``` ### Attributed text ```objc // 1. Create an attributed string. NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text, blabla..."]; // 2. Set attributes to text, you can use almost all CoreText attributes. text.yy_font = [UIFont boldSystemFontOfSize:30]; text.yy_color = [UIColor blueColor]; [text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)]; text.yy_lineSpacing = 10; // 3. Set to YYLabel or YYTextView. YYLabel *label = [YYLabel new]; label.frame = ... label.attributedString = text; YYTextView *textView = [YYTextView new]; textView.frame = ... textView.attributedString = text; ``` ### Text highlight You can use some convenience methods to set text highlight: ```objc [text yy_setTextHighlightRange:range color:[UIColor blueColor] backgroundColor:[UIColor grayColor] tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){ NSLog(@"tap text range:..."); }]; ``` Or set the text highlight with your custom config: ```objc // 1. Create a 'highlight' attribute for text. YYTextBorder *border = [YYTextBorder borderWithFillColor:[UIColor grayColor] cornerRadius:3]; YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setColor:[UIColor whiteColor]]; [highlight setBackgroundBorder:highlightBorder]; highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { NSLog(@"tap text range:..."); // you can also set the action handler to YYLabel or YYTextView. }; // 2. Add 'highlight' attribute to a range of text. [attributedText yy_setTextHighlight:highlight range:highlightRange]; // 3. Set text to label or text view. YYLabel *label = ... label.attributedText = attributedText YYTextView *textView = ... textView.attributedText = ... // 4. Receive user interactive action. label.highlightTapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { NSLog(@"tap text range:..."); }; label.highlightLongPressAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { NSLog(@"long press text range:..."); }; @UITextViewDelegate - (void)textView:(YYTextView *)textView didTapHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect { NSLog(@"tap text range:..."); } - (void)textView:(YYTextView *)textView didLongPressHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect { NSLog(@"long press text range:..."); } ``` ### Text attachments ```objc NSMutableAttributedString *text = [NSMutableAttributedString new]; UIFont *font = [UIFont systemFontOfSize:16]; NSMutableAttributedString *attachment = nil; // UIImage attachment UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"]; attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; // UIView attachment UISwitch *switcher = [UISwitch new]; [switcher sizeToFit]; attachment = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; // CALayer attachment CASharpLayer *layer = [CASharpLayer layer]; layer.path = ... attachment = [NSMutableAttributedString yy_attachmentStringWithContent:layer contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; ``` ### Text layout calculation ```objc NSAttributedString *text = ... CGSize size = CGSizeMake(100, CGFLOAT_MAX); YYTextLayout *layout = [YYTextLayout layoutWithContainerSize:size text:text]; // get text bounding layout.textBoundingRect; // get bounding rect layout.textBoundingSize; // get bounding size // query text layout [layout lineIndexForPoint:CGPointMake(10,10)]; [layout closestLineIndexForPoint:CGPointMake(10,10)]; [layout closestPositionToPoint:CGPointMake(10,10)]; [layout textRangeAtPoint:CGPointMake(10,10)]; [layout rectForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]]; [layout selectionRectsForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]]; // text layout display YYLabel *label = [YYLabel new]; label.size = layout.textBoundingSize; label.textLayout = layout; ``` ### Adjust text line position ```objc // Convenience methods: // 1. Create a text line position modifier, implements `YYTextLinePositionModifier` protocol. // 2. Set it to label or text view. YYTextLinePositionSimpleModifier *modifier = [YYTextLinePositionSimpleModifier new]; modifier.fixedLineHeight = 24; YYLabel *label = [YYLabel new]; label.linePositionModifier = modifier; // Fully control YYTextLinePositionSimpleModifier *modifier = [YYTextLinePositionSimpleModifier new]; modifier.fixedLineHeight = 24; YYTextContainer *container = [YYTextContainer new]; container.size = CGSizeMake(100, CGFLOAT_MAX); container.linePositionModifier = modifier; YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text]; YYLabel *label = [YYLabel new]; label.size = layout.textBoundingSize; label.textLayout = layout; ``` ### Asynchronous layout and rendering ```objc // If you have performance issues, // you may enable the asynchronous display mode. YYLabel *label = ... label.displaysAsynchronously = YES; // If you want to get the highest performance, you should do // text layout with `YYTextLayout` class in background thread. 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; }); }); ``` ### Text container control ```objc YYLabel *label = ... label.textContainerPath = [UIBezierPath bezierPathWith...]; label.exclusionPaths = @[[UIBezierPath bezierPathWith...];,...]; label.textContainerInset = UIEdgeInsetsMake(...); label.verticalForm = YES/NO; YYTextView *textView = ... textView.exclusionPaths = @[[UIBezierPath bezierPathWith...];,...]; textView.textContainerInset = UIEdgeInsetsMake(...); textView.verticalForm = YES/NO; ``` ### Text parser ```objc // 1. Create a text parser YYTextSimpleEmoticonParser *parser = [YYTextSimpleEmoticonParser new]; NSMutableDictionary *mapper = [NSMutableDictionary new]; mapper[@":smile:"] = [UIImage imageNamed:@"smile.png"]; mapper[@":cool:"] = [UIImage imageNamed:@"cool.png"]; mapper[@":cry:"] = [UIImage imageNamed:@"cry.png"]; mapper[@":wink:"] = [UIImage imageNamed:@"wink.png"]; parser.emoticonMapper = mapper; YYTextSimpleMarkdownParser *parser = [YYTextSimpleMarkdownParser new]; [parser setColorWithDarkTheme]; MyCustomParser *parser = ... // custom parser // 2. Attach parser to label or text view YYLabel *label = ... label.textParser = parser; YYTextView *textView = ... textView.textParser = parser; ``` ### Debug ```objc // Set a shared debug option to show text layout result. YYTextDebugOption *debugOptions = [YYTextDebugOption new]; debugOptions.baselineColor = [UIColor redColor]; debugOptions.CTFrameBorderColor = [UIColor redColor]; debugOptions.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180]; debugOptions.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200]; [YYTextDebugOption setSharedDebugOption:debugOptions]; ``` ### More examples See `Demo/YYTextDemo.xcodeproj` for more examples:

Installation ============== ### CocoaPods 1. Add `pod 'YYText'` to your Podfile. 2. Run `pod install` or `pod update`. 3. Import \. ### Carthage 1. Add `github "ibireme/YYText"` to your Cartfile. 2. Run `carthage update --platform ios` and add the framework to your project. 3. Import \. ### Manually 1. Download all the files in the `YYText` subdirectory. 2. Add the source files to your Xcode project. 3. Link with required frameworks: * UIKit * CoreFoundation * CoreText * QuartzCore * Accelerate * MobileCoreServices 4. Import `YYText.h`. ### Notice You may add [YYImage](https://github.com/ibireme/YYImage) or [YYWebImage](https://github.com/ibireme/YYWebImage) to your project if you want to support animated image (GIF/APNG/WebP). Documentation ============== Full API documentation is available on [CocoaDocs](http://cocoadocs.org/docsets/YYText/).
You can also install documentation locally using [appledoc](https://github.com/tomaz/appledoc). Requirements ============== This library requires `iOS 6.0+` and `Xcode 8.0+`. License ============== YYText is released under the MIT license. See LICENSE file for details.

--- 中文介绍 ============== 功能强大的 iOS 富文本编辑与显示框架。
(该项目是 [YYKit](https://github.com/ibireme/YYKit) 组件之一) 特性 ============== - API 兼容 UILabel 和 UITextView - 支持高性能的异步排版和渲染 - 扩展了 CoreText 的属性以支持更多文字效果 - 支持 UIImage、UIView、CALayer 作为图文混排元素 - 支持添加自定义样式的、可点击的文本高亮范围 - 支持自定义文本解析 (内置简单的 Markdown/表情解析) - 支持文本容器路径、内部留空路径的控制 - 支持文字竖排版,可用于编辑和显示中日韩文本 - 支持图片和富文本的复制粘贴 - 文本编辑时,支持富文本占位符 - 支持自定义键盘视图 - 撤销和重做次数的控制 - 富文本的序列化与反序列化支持 - 支持多语言,支持 VoiceOver - 支持 Interface Builder - 全部代码都有文档注释 架构 ============== YYText 和 TextKit 架构对比 文本属性 ============== ### YYText 原生支持的属性
Demo Attribute Name Class
TextAttachment YYTextAttachment
TextHighlight YYTextHighlight
TextBinding YYTextBinding
TextShadow
TextInnerShadow
YYTextShadow
TextBorder YYTextBorder
TextBackgroundBorder YYTextBorder
TextBlockBorder YYTextBorder
TextGlyphTransform NSValue(CGAffineTransform)
TextUnderline YYTextDecoration
TextStrickthrough YYTextDecoration
TextBackedString YYTextBackedString
### YYText 支持的 CoreText 属性
Demo Attribute Name Class
Font UIFont(CTFontRef)
Kern NSNumber
StrokeWidth NSNumber
StrokeColor CGColorRef
Shadow NSShadow
Ligature NSNumber
VerticalGlyphForm NSNumber(BOOL)
WritingDirection NSArray(NSNumber)
RunDelegate CTRunDelegateRef
TextAlignment NSParagraphStyle
(NSTextAlignment)
LineBreakMode NSParagraphStyle
(NSLineBreakMode)
LineSpacing NSParagraphStyle
(CGFloat)
ParagraphSpacing
ParagraphSpacingBefore
NSParagraphStyle
(CGFloat)
FirstLineHeadIndent NSParagraphStyle
(CGFloat)
HeadIndent NSParagraphStyle
(CGFloat)
TailIndent NSParagraphStyle
(CGFloat)
MinimumLineHeight NSParagraphStyle
(CGFloat)
MaximumLineHeight NSParagraphStyle
(CGFloat)
LineHeightMultiple NSParagraphStyle
(CGFloat)
BaseWritingDirection NSParagraphStyle
(NSWritingDirection)
DefaultTabInterval
TabStops
NSParagraphStyle
CGFloat/NSArray(NSTextTab)
用法 ============== ### 基本用法 ```objc // YYLabel (和 UILabel 用法一致) YYLabel *label = [YYLabel new]; label.frame = ... label.font = ... label.textColor = ... label.textAlignment = ... label.lineBreakMode = ... label.numberOfLines = ... label.text = ... // YYTextView (和 UITextView 用法一致) YYTextView *textView = [YYTextView new]; textView.frame = ... textView.font = ... textView.textColor = ... textView.dataDetectorTypes = ... textView.placeHolderText = ... textView.placeHolderTextColor = ... textView.delegate = ... ``` ### 属性文本 ```objc // 1. 创建一个属性文本 NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text, blabla..."]; // 2. 为文本设置属性 text.yy_font = [UIFont boldSystemFontOfSize:30]; text.yy_color = [UIColor blueColor]; [text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)]; text.yy_lineSpacing = 10; // 3. 赋值到 YYLabel 或 YYTextView YYLabel *label = [YYLabel new]; label.frame = ... label.attributedString = text; YYTextView *textView = [YYTextView new]; textView.frame = ... textView.attributedString = text; ``` ### 文本高亮 你可以用一些已经封装好的简便方法来设置文本高亮: ```objc [text yy_setTextHighlightRange:range color:[UIColor blueColor] backgroundColor:[UIColor grayColor] tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){ NSLog(@"tap text range:..."); }]; ``` 或者用更复杂的办法来调节文本高亮的细节: ```objc // 1. 创建一个"高亮"属性,当用户点击了高亮区域的文本时,"高亮"属性会替换掉原本的属性 YYTextBorder *border = [YYTextBorder borderWithFillColor:[UIColor grayColor] cornerRadius:3]; YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setColor:[UIColor whiteColor]]; [highlight setBackgroundBorder:highlightBorder]; highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { NSLog(@"tap text range:..."); // 你也可以把事件回调放到 YYLabel 和 YYTextView 来处理。 }; // 2. 把"高亮"属性设置到某个文本范围 [attributedText yy_setTextHighlight:highlight range:highlightRange]; // 3. 把属性文本设置到 YYLabel 或 YYTextView YYLabel *label = ... label.attributedText = attributedText YYTextView *textView = ... textView.attributedText = ... // 4. 接受事件回调 label.highlightTapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { NSLog(@"tap text range:..."); }; label.highlightLongPressAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) { NSLog(@"long press text range:..."); }; @UITextViewDelegate - (void)textView:(YYTextView *)textView didTapHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect { NSLog(@"tap text range:..."); } - (void)textView:(YYTextView *)textView didLongPressHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect { NSLog(@"long press text range:..."); } ``` ### 图文混排 ```objc NSMutableAttributedString *text = [NSMutableAttributedString new]; UIFont *font = [UIFont systemFontOfSize:16]; NSMutableAttributedString *attachment = nil; // 嵌入 UIImage UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"]; attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; // 嵌入 UIView UISwitch *switcher = [UISwitch new]; [switcher sizeToFit]; attachment = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; // 嵌入 CALayer CASharpLayer *layer = [CASharpLayer layer]; layer.path = ... attachment = [NSMutableAttributedString yy_attachmentStringWithContent:layer contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter]; [text appendAttributedString: attachment]; ``` ### 文本布局计算 ```objc NSAttributedString *text = ... CGSize size = CGSizeMake(100, CGFLOAT_MAX); YYTextLayout *layout = [YYTextLayout layoutWithContainerSize:size text:text]; // 获取文本显示位置和大小 layout.textBoundingRect; // get bounding rect layout.textBoundingSize; // get bounding size // 查询文本排版结果 [layout lineIndexForPoint:CGPointMake(10,10)]; [layout closestLineIndexForPoint:CGPointMake(10,10)]; [layout closestPositionToPoint:CGPointMake(10,10)]; [layout textRangeAtPoint:CGPointMake(10,10)]; [layout rectForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]]; [layout selectionRectsForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]]; // 显示文本排版结果 YYLabel *label = [YYLabel new]; label.size = layout.textBoundingSize; label.textLayout = layout; ``` ### 文本行位置调整 ```objc // 由于中文、英文、Emoji 等字体高度不一致,或者富文本中出现了不同字号的字体, // 可能会造成每行文字的高度不一致。这里可以添加一个修改器来实现固定行高,或者自定义文本行位置。 // 简单的方法: // 1. 创建一个文本行位置修改类,实现 `YYTextLinePositionModifier` 协议。 // 2. 设置到 Label 或 TextView。 YYTextLinePositionSimpleModifier *modifier = [YYTextLinePositionSimpleModifier new]; modifier.fixedLineHeight = 24; YYLabel *label = [YYLabel new]; label.linePositionModifier = modifier; // 完全控制: YYTextLinePositionSimpleModifier *modifier = [YYTextLinePositionSimpleModifier new]; modifier.fixedLineHeight = 24; YYTextContainer *container = [YYTextContainer new]; container.size = CGSizeMake(100, CGFLOAT_MAX); container.linePositionModifier = modifier; YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text]; YYLabel *label = [YYLabel new]; label.size = layout.textBoundingSize; label.textLayout = layout; ``` ### 异步排版和渲染 ```objc // 如果你在显示字符串时有性能问题,可以这样开启异步模式: YYLabel *label = ... label.displaysAsynchronously = YES; // 如果需要获得最高的性能,你可以在后台线程用 `YYTextLayout` 进行预排版: YYLabel *label = [YYLabel new]; label.displaysAsynchronously = YES; //开启异步绘制 label.ignoreCommonProperties = YES; //忽略除了 textLayout 之外的其他属性 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 创建属性字符串 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)]; // 创建文本容器 YYTextContainer *container = [YYTextContainer new]; container.size = CGSizeMake(100, CGFLOAT_MAX); container.maximumNumberOfRows = 0; // 生成排版结果 YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text]; dispatch_async(dispatch_get_main_queue(), ^{ label.size = layout.textBoundingSize; label.textLayout = layout; }); }); ``` ### 文本容器控制 ```objc YYLabel *label = ... label.textContainerPath = [UIBezierPath bezierPathWith...]; label.exclusionPaths = @[[UIBezierPath bezierPathWith...];,...]; label.textContainerInset = UIEdgeInsetsMake(...); label.verticalForm = YES/NO; YYTextView *textView = ... textView.exclusionPaths = @[[UIBezierPath bezierPathWith...];,...]; textView.textContainerInset = UIEdgeInsetsMake(...); textView.verticalForm = YES/NO; ``` ### 文本解析 ```objc // 1. 创建一个解析器 // 内置简单的表情解析 YYTextSimpleEmoticonParser *parser = [YYTextSimpleEmoticonParser new]; NSMutableDictionary *mapper = [NSMutableDictionary new]; mapper[@":smile:"] = [UIImage imageNamed:@"smile.png"]; mapper[@":cool:"] = [UIImage imageNamed:@"cool.png"]; mapper[@":cry:"] = [UIImage imageNamed:@"cry.png"]; mapper[@":wink:"] = [UIImage imageNamed:@"wink.png"]; parser.emoticonMapper = mapper; // 内置简单的 markdown 解析 YYTextSimpleMarkdownParser *parser = [YYTextSimpleMarkdownParser new]; [parser setColorWithDarkTheme]; // 实现 `YYTextParser` 协议的自定义解析器 MyCustomParser *parser = ... // 2. 把解析器添加到 YYLabel 或 YYTextView YYLabel *label = ... label.textParser = parser; YYTextView *textView = ... textView.textParser = parser; ``` ### Debug ```objc // 设置一个全局的 debug option 来显示排版结果。 YYTextDebugOption *debugOptions = [YYTextDebugOption new]; debugOptions.baselineColor = [UIColor redColor]; debugOptions.CTFrameBorderColor = [UIColor redColor]; debugOptions.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180]; debugOptions.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200]; [YYTextDebugOption setSharedDebugOption:debugOptions]; ``` ### 更多示例 查看演示工程 `Demo/YYTextDemo.xcodeproj`:

安装 ============== ### CocoaPods 1. 在 Podfile 中添加 `pod 'YYText'`。 2. 执行 `pod install` 或 `pod update`。 3. 导入 \。 ### Carthage 1. 在 Cartfile 中添加 `github "ibireme/YYText"`。 2. 执行 `carthage update --platform ios` 并将生成的 framework 添加到你的工程。 3. 导入 \。 ### 手动安装 1. 下载 YYText 文件夹内的所有内容。 2. 将 YYText 内的源文件添加(拖放)到你的工程。 3. 链接以下 frameworks: * UIKit * CoreFoundation * CoreText * QuartzCore * Accelerate * MobileCoreServices 4. 导入 `YYText.h`。 ### 注意 你可以添加 [YYImage](https://github.com/ibireme/YYImage) 或 [YYWebImage](https://github.com/ibireme/YYWebImage) 到你的工程,以支持动画格式(GIF/APNG/WebP)的图片。 文档 ============== 你可以在 [CocoaDocs](http://cocoadocs.org/docsets/YYText/) 查看在线 API 文档,也可以用 [appledoc](https://github.com/tomaz/appledoc) 本地生成文档。 系统要求 ============== 该项目最低支持 `iOS 6.0` 和 `Xcode 8.0`。 已知问题 ============== * YYText 并不能支持所有 CoreText/TextKit 的属性,比如 NSBackgroundColor、NSStrikethrough、NSUnderline、NSAttachment、NSLink 等,但 YYText 中基本都有对应属性作为替代。详情见上方表格。 * YYTextView 未实现局部刷新,所以在输入和编辑大量的文本(比如超过大概五千个汉字、或大概一万个英文字符)时会出现较明显的卡顿现象。 * 竖排版时,添加 exclusionPaths 在少数情况下可能会导致文本显示空白。 * 当添加了非矩形的 textContainerPath,并且有嵌入大于文本排版方向宽度的 RunDelegate 时,RunDelegate 之后的文字会无法显示。这是 CoreText 的 Bug(或者说是 Feature)。 许可证 ============== YYText 使用 MIT 许可证,详情见 LICENSE 文件。 ================================================ FILE: YYText/Component/YYTextContainerView.h ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextContainerView.m ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextDebugOption.h ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextDebugOption.m ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextEffectWindow.h ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextEffectWindow.m ================================================ // // 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; } // stop self from becoming the KeyWindow - (void)becomeKeyWindow { [[YYTextSharedApplication().delegate window] makeKeyWindow]; } - (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]; if (isnan(center.x) || isnan(center.y) || isinf(center.x) || isinf(center.y)) { dot.mirror.hidden = YES; } else { 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 ================================================ FILE: YYText/Component/YYTextInput.h ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextInput.m ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextKeyboardManager.h ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextKeyboardManager.m ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextLayout.h ================================================ // // 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 path 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 ================================================ FILE: YYText/Component/YYTextLayout.m ================================================ // // 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 { 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; BOOL constraintSizeIsExtended = NO; CGRect constraintRectBeforeExtended = {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; // It may use larger constraint size when create CTFrame with // CTFramesetterCreateFrame in iOS 10. static BOOL needFixLayoutSizeBug = NO; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ double systemVersionDouble = [UIDevice currentDevice].systemVersion.doubleValue; if (8.3 <= systemVersionDouble && systemVersionDouble < 9) { needFixJoinedEmojiBug = YES; } if (systemVersionDouble >= 10) { needFixLayoutSizeBug = 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) { if (container.size.width <= 0 || container.size.height <= 0) goto fail; CGRect rect = (CGRect) {CGPointZero, container.size }; if (needFixLayoutSizeBug) { constraintSizeIsExtended = YES; constraintRectBeforeExtended = UIEdgeInsetsInsetRect(rect, container.insets); constraintRectBeforeExtended = CGRectStandardize(constraintRectBeforeExtended); if (container.isVerticalForm) { rect.size.width = YYTextContainerMaxSize.width; } else { rect.size.height = YYTextContainerMaxSize.height; } } 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; if (constraintSizeIsExtended) { if (isVerticalForm) { if (rect.origin.x + rect.size.width > constraintRectBeforeExtended.origin.x + constraintRectBeforeExtended.size.width) break; } else { if (rect.origin.y + rect.size.height > constraintRectBeforeExtended.origin.y + constraintRectBeforeExtended.size.height) break; } } 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 = ceil(size.width); size.height = ceil(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 ? attrs.mutableCopy : [NSMutableArray new]; [attrs removeObjectsForKeys:[NSMutableAttributedString yy_allDiscontinuousAttributeKeys]]; 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); } CGColorRef color = (__bridge CGColorRef)(attrs[(id)kCTForegroundColorAttributeName]); if (color && CFGetTypeID(color) == CGColorGetTypeID() && CGColorGetAlpha(color) == 0) { // ignore clear color [attrs removeObjectForKey:(id)kCTForegroundColorAttributeName]; } 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 == nil || 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) { CGRect bounds = CGRectUnion(path.bounds, (CGRect){CGPointZero, size}); bounds = CGRectInset(bounds, -2 * border.strokeWidth, -2 * border.strokeWidth); CGContextAddRect(context, bounds); 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]; CGRect bounds = CGRectUnion(path.bounds, (CGRect){CGPointZero, size}); bounds = CGRectInset(bounds, -2 * border.strokeWidth, -2 * border.strokeWidth); CGContextAddRect(context, bounds); 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); 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; CGFloat posX = line.position.x + verticalOffset; CGFloat posY = 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); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextSetTextPosition(context, posX, posY); 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 != nil) ? 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 != nil) ? 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); 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; CGFloat linePosX = line.position.x; CGFloat linePosY = 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); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextSetTextPosition(context, linePosX, linePosY); 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; CGFloat linePosX = line.position.x; CGFloat linePosY = 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; CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextSetTextPosition(context, linePosX, linePosY); 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 ================================================ FILE: YYText/Component/YYTextLine.h ================================================ // // 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 (nullable, nonatomic, readonly) NSArray *attachments; ///< YYTextAttachment @property (nullable, nonatomic, readonly) NSArray *attachmentRanges; ///< NSRange(NSValue) @property (nullable, 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 ================================================ FILE: YYText/Component/YYTextLine.m ================================================ // // 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:(_Nonnull 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 ================================================ FILE: YYText/Component/YYTextMagnifier.h ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextMagnifier.m ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextSelectionView.h ================================================ // // 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 ================================================ FILE: YYText/Component/YYTextSelectionView.m ================================================ // // 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 ================================================ FILE: YYText/String/YYTextArchiver.h ================================================ // // 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 ================================================ FILE: YYText/String/YYTextArchiver.m ================================================ // // 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 ================================================ FILE: YYText/String/YYTextAttribute.h ================================================ // // 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 ================================================ FILE: YYText/String/YYTextAttribute.m ================================================ // // 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 != nil) 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 ================================================ FILE: YYText/String/YYTextParser.h ================================================ // // 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 ================================================ FILE: YYText/String/YYTextParser.m ================================================ // // 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 ================================================ FILE: YYText/String/YYTextRubyAnnotation.h ================================================ // // 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 ================================================ FILE: YYText/String/YYTextRubyAnnotation.m ================================================ // // 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 ================================================ FILE: YYText/String/YYTextRunDelegate.h ================================================ // // 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 ================================================ FILE: YYText/String/YYTextRunDelegate.m ================================================ // // 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 ================================================ FILE: YYText/Utility/NSAttributedString+YYText.h ================================================ // // 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 font The attachment will align to this font. @param alignment The attachment holder's alignment to text line. @return An attributed string, or nil if an error occurs. @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 ================================================ FILE: YYText/Utility/NSAttributedString+YYText.m ================================================ // // 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 (index > self.length || self.length == 0) return nil; 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 (index > self.length || 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_setStrikethroughColor: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 ================================================ FILE: YYText/Utility/NSParagraphStyle+YYText.h ================================================ // // 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 ================================================ FILE: YYText/Utility/NSParagraphStyle+YYText.m ================================================ // // 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]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CGFloat lineSpacing; if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing)) { style.lineSpacing = lineSpacing; } #pragma clang diagnostic pop 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; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CGFloat lineSpacing = self.lineSpacing; set[count].spec = kCTParagraphStyleSpecifierLineSpacing; set[count].valueSize = sizeof(CGFloat); set[count].value = &lineSpacing; count++; #pragma clang diagnostic pop 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 ================================================ FILE: YYText/Utility/UIPasteboard+YYText.h ================================================ // // 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 ================================================ FILE: YYText/Utility/UIPasteboard+YYText.m ================================================ // // 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 ================================================ FILE: YYText/Utility/UIView+YYText.h ================================================ // // 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 ================================================ FILE: YYText/Utility/UIView+YYText.m ================================================ // // 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 ================================================ FILE: YYText/Utility/YYTextAsyncLayer.h ================================================ // // 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. block 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. block param context: A new bitmap content created by layer. block param size: The content size (typically same as layer's bound size). block param isCancelled: If this block returns `YES`, the method should cancel the drawing process and return as quickly as possible. */ @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. block param layer: The layer. block 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 ================================================ FILE: YYText/Utility/YYTextAsyncLayer.m ================================================ // // 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)); } } }); uint32_t cur = (uint32_t)OSAtomicIncrement32(&counter); 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 = (id)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 && context) { 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 && context) { 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 ================================================ FILE: YYText/Utility/YYTextTransaction.h ================================================ // // 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 ================================================ FILE: YYText/Utility/YYTextTransaction.m ================================================ // // 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 ================================================ FILE: YYText/Utility/YYTextUtilities.h ================================================ // // 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[_Nullable 3], CGPoint after[_Nullable 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 `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 ================================================ FILE: YYText/Utility/YYTextUtilities.m ================================================ // // 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 } ================================================ FILE: YYText/Utility/YYTextWeakProxy.h ================================================ // // 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 ================================================ FILE: YYText/Utility/YYTextWeakProxy.m ================================================ // // 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 ================================================ FILE: YYText/YYLabel.h ================================================ // // 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 NSTextAlignmentNatural. Set a new value to this property also causes the new alignment to be applied to the entire `attributedText`. Get the value returns the alignment at the head of `attributedText`. */ @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 ================================================ FILE: YYText/YYLabel.m ================================================ // // 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; _textAlignment = NSTextAlignmentNatural; _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"]; __strong YYLabel *view = (YYLabel *)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 rangeOfString:@"bold"].location == NSNotFound)) { 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 ================================================ FILE: YYText/YYText.h ================================================ // // 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 ================================================ FILE: YYText/YYTextView.h ================================================ // // 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 NSTextAlignmentNatural. Set a new value to this property also causes the new alignment to be applied to the entire `attributedText`. Get the value returns the alignment at the head of `attributedText`. */ @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 ================================================ FILE: YYText/YYTextView.m ================================================ // // 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)]; }]; } [self willChangeValueForKey:@"textLayout"]; _innerLayout = [YYTextLayout layoutWithContainer:_innerContainer text:text]; [self didChangeValueForKey:@"textLayout"]; 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 || 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 rangeOfString:@"_"].location != NSNotFound)) { 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; _textAlignment = NSTextAlignmentNatural; _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; } } _placeholderText = [_placeholderAttributedText yy_plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)]; [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]; if ([self.delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) { [self.delegate textViewDidBeginEditing:self]; } [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidBeginEditingNotification object: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 rangeOfString:@"bold"].location == NSNotFound)) { 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 rangeOfString:@"bold"].location == NSNotFound)) { 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 ================================================ FILE: YYText.podspec ================================================ Pod::Spec.new do |s| s.name = 'YYText' s.summary = 'Powerful text framework for iOS to display and edit rich text.' s.version = '1.0.7' s.license = { :type => 'MIT', :file => 'LICENSE' } s.authors = { 'ibireme' => 'ibireme@gmail.com' } s.social_media_url = 'http://blog.ibireme.com' s.homepage = 'https://github.com/ibireme/YYText' s.platform = :ios, '6.0' s.ios.deployment_target = '6.0' s.source = { :git => 'https://github.com/ibireme/YYText.git', :tag => s.version.to_s } s.requires_arc = true s.source_files = 'YYText/**/*.{h,m}' s.public_header_files = 'YYText/**/*.{h}' s.frameworks = 'UIKit', 'CoreFoundation','CoreText', 'QuartzCore', 'Accelerate', 'MobileCoreServices' end