├── .gitignore ├── Classes ├── Draft │ ├── DraftManager+Attachment.h │ ├── DraftManager+Attachment.m │ ├── DraftManager.h │ ├── DraftManager.m │ ├── DraftMetaDataModel.h │ ├── DraftMetaDataModel.m │ ├── NSTextStorage+Draft.h │ ├── NSTextStorage+Draft.m │ ├── RichTextView+Draft.h │ └── RichTextView+Draft.m ├── RichTextTools.h ├── RichTextView │ ├── RichTextView+TextAttachment.h │ ├── RichTextView+TextAttachment.m │ ├── RichTextView.h │ └── RichTextView.m ├── TextAttachment │ ├── AnyViewTextAttachment.h │ ├── AnyViewTextAttachment.m │ ├── AudioTextAttachment.h │ ├── AudioTextAttachment.m │ ├── CopyTextAttachmentProtocol.h │ ├── GifTextAttachment.h │ ├── GifTextAttachment.m │ ├── ImageTextAttachment.h │ ├── ImageTextAttachment.m │ ├── MediaTextAttachment.h │ ├── MediaTextAttachment.m │ ├── VideoTextAttachment.h │ └── VideoTextAttachment.m ├── TextAttachmentManager │ ├── AttachmentGestureRecognizerDelegate.h │ ├── AttachmentGestureRecognizerDelegate.m │ ├── AttachmentLayoutManager.h │ ├── AttachmentLayoutManager.m │ ├── TextAttachmentReusableView.h │ ├── TextAttachmentReusableView.m │ ├── TextAttachmentsManager.h │ ├── TextAttachmentsManager.m │ └── TextViewAttachmentDelegate.h ├── TextKit+ │ ├── NSLayoutManager+Attachment.h │ ├── NSLayoutManager+Attachment.m │ ├── NSTextStorage+Html.h │ ├── NSTextStorage+Html.m │ ├── NSTextStorage+ProcessEditing.h │ ├── NSTextStorage+ProcessEditing.m │ ├── NSTextStorage+Utils.h │ └── NSTextStorage+Utils.m ├── UITextView+ │ ├── UITextView+Binding.h │ ├── UITextView+Binding.m │ ├── UITextView+RichText.h │ ├── UITextView+RichText.m │ ├── UITextView+TextAttachmentUtils.h │ └── UITextView+TextAttachmentUtils.m ├── Utils │ ├── MediaCompressTool.h │ ├── MediaCompressTool.m │ ├── UIImage+Utils.h │ └── UIImage+Utils.m └── XFRichTextTool.bundle │ ├── movie_play@2x.png │ └── movie_play@3x.png ├── Podfile ├── Podfile.lock ├── README.md ├── TTRichTextView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── qyer.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── TTRichTextView.xcworkspace ├── contents.xcworkspacedata ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── qyer.xcuserdatad │ ├── IDEFindNavigatorScopes.plist │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist ├── TTRichTextView ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── cf_login_backBtn2.imageset │ │ ├── Contents.json │ │ ├── cf_login_backBtn2@2x.png │ │ └── cf_login_backBtn2@3x.png │ ├── cf_richText_tool_image.imageset │ │ ├── Contents.json │ │ ├── tupian_normal@2x.png │ │ └── tupian_normal@3x.png │ ├── cf_richText_tool_video_normal.imageset │ │ ├── Contents.json │ │ ├── 视频-icon@2x.png │ │ └── 视频-icon@3x.png │ └── rich_topic_icon.imageset │ │ ├── Contents.json │ │ ├── rich_topic_icon@2x.png │ │ └── rich_topic_icon@3x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Controller │ ├── DraftListViewController.h │ ├── DraftListViewController.m │ ├── PostRichEditViewController.h │ ├── PostRichEditViewController.m │ ├── RichTextViewController.h │ └── RichTextViewController.m ├── Info.plist ├── Utility │ ├── Define.h │ ├── NSObject+Swizzle.h │ ├── NSObject+Swizzle.m │ ├── UIViewController+Base.h │ ├── UIViewController+Base.m │ ├── UIViewController+XF.h │ └── UIViewController+XF.m ├── View │ ├── AudioAttachmentReusableView.h │ ├── AudioAttachmentReusableView.m │ ├── GifAttachmentReusableView.h │ ├── GifAttachmentReusableView.m │ ├── RichTextInputAccessoryView.h │ └── RichTextInputAccessoryView.m ├── ViewController.h ├── ViewController.m ├── main.m └── niconiconi@2x.gif ├── TTRichTextViewTests ├── Info.plist └── TTRichTextViewTests.m ├── richtext.gif └── richtext_empty.gif /.gitignore: -------------------------------------------------------------------------------- 1 | Pods/ 2 | Podfile.lock 3 | -------------------------------------------------------------------------------- /Classes/Draft/DraftManager+Attachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // DraftManager+Attachment.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/29. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "DraftManager.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface DraftManager (Attachment) 14 | 15 | 16 | 17 | 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /Classes/Draft/DraftManager+Attachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // DraftManager+Attachment.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/29. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "DraftManager+Attachment.h" 10 | 11 | @implementation DraftManager (Attachment) 12 | 13 | 14 | - (void)insertImgAttachmentInDraft:(NSURL *)photoFileURL photoUrl:(NSURL *)photoUrl{ 15 | DraftAttachmentDataModel *imgAttachment = [[DraftAttachmentDataModel alloc] init]; 16 | imgAttachment.draft_uuid = self.model.uuid; 17 | imgAttachment.diskPath = photoFileURL.absoluteString; 18 | } 19 | 20 | 21 | 22 | 23 | 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Classes/Draft/DraftManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // DraftManager.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/28. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DraftMetaDataModel.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | /// 草稿数据最大限制 15 | static NSInteger const maxCountDraftCache = 100; 16 | 17 | @interface DraftManager : NSObject 18 | 19 | @property (nonatomic,copy,readonly) NSString *draftId; 20 | 21 | @property (nonatomic,strong,readonly) DraftMetaDataModel *model; 22 | 23 | /// 启动调用当前方法,检测草稿编辑中是否异常退出 24 | + (void)isReopenEditingDraft; 25 | 26 | /// 获取所有草稿 27 | + (void)fetchAllDraft:(void(^)(NSArray * drafts))complate; 28 | 29 | /// 删除草稿 30 | - (BOOL)deleteDraft; 31 | + (BOOL)deleteDraft:(DraftMetaDataModel *)draftModel; 32 | 33 | /// 更新草稿状态 34 | - (void)updateDraftStatus:(DraftEditStatus)status; 35 | /// 更新草稿正文 36 | - (void)updateDraftText:(NSString *)contentText richText:(NSString *)richText richTextType:(RichTextType)richTextType; 37 | 38 | 39 | @end 40 | 41 | NS_ASSUME_NONNULL_END 42 | -------------------------------------------------------------------------------- /Classes/Draft/DraftManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // DraftManager.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/28. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "DraftManager.h" 10 | 11 | @interface DraftManager () 12 | 13 | @property (nonatomic,copy,readwrite) NSString *draftId; 14 | @property (nonatomic,copy) NSString *userId; 15 | @property (nonatomic,strong,readwrite) DraftMetaDataModel *model; 16 | 17 | 18 | @end 19 | 20 | @implementation DraftManager (limitCount) 21 | 22 | + (void)limitDraftCount{ 23 | /// 限制100条 24 | 25 | } 26 | 27 | @end 28 | 29 | 30 | @implementation DraftManager 31 | 32 | - (instancetype)initWith:(NSString *)draftId draftType:(DraftType)type 33 | { 34 | self = [super init]; 35 | if (self) { 36 | _draftId = draftId; 37 | /// search 38 | DraftMetaDataModel *draftModel = [DraftManager draftModelWith:draftId]; 39 | if (draftModel) { 40 | _model = draftModel; 41 | }else{ 42 | _model = [[DraftMetaDataModel alloc] init]; 43 | _model.type = type; 44 | if (![_model saveToDB]) { 45 | NSLog(@"DraftMetaDataModel SaveToDB ~~~~~~~~~ Error"); 46 | } 47 | _draftId = _model.uuid; 48 | } 49 | 50 | /// 重置草稿状态为编辑中.... 51 | [self updateDraftStatus:DraftEditing]; 52 | 53 | /// 限制草稿箱数量 54 | [DraftManager limitDraftCount]; 55 | } 56 | return self; 57 | } 58 | 59 | #pragma mark - 异常退出 重新打开 60 | 61 | + (void)isReopenEditingDraft{ 62 | /// 获取最后一次编辑中的草稿 63 | [[DraftMetaDataModel getUsingLKDBHelper] search:DraftMetaDataModel.class where:@{@"status":@(0)} orderBy:@"createTime desc" offset:0 count:NSIntegerMax callback:^(NSMutableArray * _Nullable array) { 64 | if (array.count == 0) { 65 | return ; 66 | } 67 | DraftMetaDataModel *draft = array.firstObject; 68 | 69 | dispatch_async(dispatch_get_main_queue(), ^{ 70 | UIViewController *topVC = [self getTopViewController]; 71 | 72 | UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"检测到你在编辑中异常退出,是否继续编辑" message:nil preferredStyle:UIAlertControllerStyleAlert]; 73 | UIAlertAction * cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 74 | /// 重置草稿状态 编辑完成 75 | draft.status = DraftEdited; 76 | if ([draft updateToDB]) { 77 | NSLog(@"~~~~编辑状态更新完成"); 78 | } 79 | }]; 80 | UIAlertAction * action = [UIAlertAction actionWithTitle:@"继续编辑" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 81 | //TODO: 弹出编辑器控制器 82 | NSAssert(NO, @"TODO: 弹出编辑器控制器"); 83 | }]; 84 | [alert addAction:cancelAction]; 85 | [alert addAction:action]; 86 | [topVC presentViewController:alert animated:YES completion:nil]; 87 | }); 88 | }]; 89 | } 90 | 91 | + (UIViewController *)getTopViewController { 92 | 93 | UIViewController *topViewController = [[[UIApplication sharedApplication].windows objectAtIndex:0] rootViewController]; 94 | while (topViewController.presentedViewController) { 95 | topViewController = topViewController.presentedViewController; 96 | } 97 | 98 | if ([topViewController isKindOfClass:[UINavigationController class]]) { 99 | UINavigationController *nav = (UINavigationController*)topViewController; 100 | topViewController = [nav.viewControllers lastObject]; 101 | } 102 | if ([topViewController isKindOfClass:[UITabBarController class]]) { 103 | UITabBarController *tab = (UITabBarController*)topViewController; 104 | topViewController = tab.selectedViewController; 105 | } 106 | 107 | return topViewController; 108 | } 109 | 110 | 111 | #pragma mark - Fetch draft 112 | 113 | /// 主键查询 114 | + (DraftMetaDataModel *)draftModelWith:(NSString *)draftId{ 115 | if (draftId.length == 0) { 116 | return nil; 117 | } 118 | return [DraftMetaDataModel searchSingleWithWhere:@{@"uuid":draftId} orderBy:nil];; 119 | } 120 | 121 | /// 获取所有草稿 122 | + (void)fetchAllDraft:(void(^)(NSArray * drafts))complate{ 123 | /// 时间倒序 124 | [[DraftMetaDataModel getUsingLKDBHelper] search:DraftMetaDataModel.class where:nil orderBy:@"createTime desc" offset:0 count:NSIntegerMax callback:^(NSMutableArray * _Nullable array) { 125 | 126 | /// 查询附件 127 | for (DraftMetaDataModel *draftModel in array) { 128 | /// TODO: 附件数据排序 129 | draftModel.dataModes = [self fetchDraftAttachments:draftModel orderBy:nil]; 130 | } 131 | 132 | if (complate) { 133 | complate(array); 134 | } 135 | }]; 136 | } 137 | 138 | /// 获取草稿对应的附件 139 | + (NSArray *)fetchDraftAttachments:(DraftMetaDataModel *)draft orderBy:(NSString *)orderBy{ 140 | return [DraftAttachmentDataModel searchWithWhere:@{@"draft_uuid":draft.uuid} orderBy:orderBy offset:0 count:NSIntegerMax]; 141 | } 142 | 143 | /// 获取草稿对应的其他内容 144 | 145 | #pragma mark - Delete draft 146 | 147 | /// 删除草稿 148 | - (BOOL)deleteDraft{ 149 | return [DraftManager deleteDraft:_model]; 150 | } 151 | 152 | + (BOOL)deleteDraft:(DraftMetaDataModel *)draftModel{ 153 | if (![DraftMetaDataModel isExistsWithModel:draftModel]) { 154 | return YES; 155 | } 156 | /// 查询附件 157 | NSArray * attachmentsInDB = [self fetchDraftAttachments:draftModel orderBy:nil]; 158 | [[DraftMetaDataModel getUsingLKDBHelper] executeForTransaction:^BOOL(LKDBHelper * _Nonnull helper) { 159 | /// 删除附件 160 | BOOL isSuc = [DraftManager deleteDraftAttachments:attachmentsInDB]; 161 | /// 删除草稿 162 | isSuc = [draftModel deleteToDB]; 163 | return isSuc; 164 | }]; 165 | 166 | /// 是否删除成功 167 | if ([DraftMetaDataModel isExistsWithModel:draftModel]) { 168 | return NO; 169 | } 170 | /// 在删除附件对应资源文件 171 | for (DraftAttachmentDataModel *attachment in attachmentsInDB) { 172 | [self clearSandboxResourceWithFileName:attachment.diskPath]; 173 | } 174 | return YES; 175 | } 176 | 177 | /// 删除附件 178 | + (BOOL)deleteDraftAttachments:(NSArray *) attachmentsInDB{ 179 | BOOL isSuc = YES; 180 | /// 先删除附件表数据 181 | for (DraftAttachmentDataModel *attachment in attachmentsInDB) { 182 | isSuc = [attachment deleteToDB]; 183 | if (!isSuc) { 184 | return NO; 185 | } 186 | } 187 | return isSuc; 188 | } 189 | 190 | + (BOOL)clearSandboxResourceWithFileName:(NSString *)fileName{ 191 | if (fileName.length == 0) { 192 | return NO; 193 | } 194 | NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 195 | NSString* documentsDirectory = [paths objectAtIndex:0]; 196 | NSString* fullPathToFile = [documentsDirectory stringByAppendingPathComponent:fileName]; 197 | if ([[NSFileManager defaultManager] fileExistsAtPath:fullPathToFile] == NO) { 198 | NSLog(@"~~~~%@!!!!!!!!文件不存在",fullPathToFile); 199 | return NO; 200 | } 201 | NSError *err; 202 | BOOL isRemoved = [[NSFileManager defaultManager] removeItemAtPath:fullPathToFile error:&err]; 203 | if (!isRemoved || err) { 204 | return NO; 205 | } 206 | return YES; 207 | } 208 | 209 | #pragma mark - Update draft 210 | 211 | /// 更新草稿状态 212 | - (void)updateDraftStatus:(DraftEditStatus)status{ 213 | if (status == _model.status) {//不用更新了 214 | return; 215 | } 216 | _model.status = status; 217 | if (status == DraftEdited) { 218 | /// 更新时间 219 | _model.createTime = [[NSDate date] timeIntervalSince1970]; 220 | } 221 | if (![_model updateToDB]) { 222 | NSLog(@"更新草稿状态失败~~~~~~"); 223 | } 224 | } 225 | 226 | /// 更新草稿正文 227 | - (void)updateDraftText:(NSString *)contentText richText:(NSString *)richText richTextType:(RichTextType)richTextType{ 228 | _model.textContent = contentText; 229 | _model.richTextType = richTextType; 230 | _model.richText = richText; 231 | if ([_model saveToDB]) { 232 | NSLog(@"~~~~~~~更新文本内容失败"); 233 | } 234 | } 235 | 236 | 237 | @end 238 | 239 | -------------------------------------------------------------------------------- /Classes/Draft/DraftMetaDataModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentMetaDataModel.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/22. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RichTextTools.h" 11 | #import 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | @class DraftAttachmentDataModel; 17 | 18 | @interface DraftMetaDataModel : NSObject 19 | 20 | @property (nonatomic,copy) NSString *uuid; 21 | 22 | @property (nonatomic,assign) DraftEditStatus status; 23 | 24 | @property (nonatomic,assign) RichTextType richTextType; 25 | 26 | /// 草稿类型 27 | @property (nonatomic,assign) DraftType type; 28 | 29 | /// 纯文本内容,用于草稿列表展示 30 | @property (nonatomic,copy) NSString *textContent; 31 | 32 | /// 富文本字符串 html、markdown 33 | @property (nonatomic,copy) NSString *richText; 34 | 35 | @property (nonatomic,strong) NSArray *dataModes; 36 | 37 | @property (nonatomic,assign) NSTimeInterval createTime; 38 | 39 | + (instancetype)defaultDraftModel; 40 | 41 | @end 42 | 43 | 44 | @interface DraftAttachmentDataModel : NSObject 45 | 46 | @property (nonatomic,copy) NSString *uuid; 47 | /// 对应草稿文章uuid 48 | @property (nonatomic,copy) NSString *draft_uuid; 49 | 50 | 51 | /// 附件显示尺寸 52 | @property (nonatomic,assign) CGFloat attachmentWidth; 53 | @property (nonatomic,assign) CGFloat attachmentHeight; 54 | 55 | @property (nonatomic,assign) AttachmentType attachmentType; 56 | 57 | @property (nonatomic,copy) NSString *diskPath; 58 | 59 | @property (nonatomic,copy) NSString *urlPath; 60 | 61 | @property (nonatomic,strong) NSDictionary *userInfo; 62 | 63 | @end 64 | 65 | 66 | NS_ASSUME_NONNULL_END 67 | -------------------------------------------------------------------------------- /Classes/Draft/DraftMetaDataModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentMetaDataModel.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/22. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "DraftMetaDataModel.h" 10 | #import "NSTextStorage+Draft.h" 11 | #import 12 | #import "RichTextTools.h" 13 | 14 | @implementation DraftMetaDataModel 15 | 16 | +(LKDBHelper *)getUsingLKDBHelper{ 17 | static LKDBHelper *helper; 18 | static dispatch_once_t onceToken; 19 | dispatch_once(&onceToken, ^{ 20 | helper = [[LKDBHelper alloc] initWithDBName:@"draft"]; 21 | NSLog(@"DBName====%@",[LKDBHelper performSelector:@selector(getDBPathWithDBName:) withObject:@"draft"]); 22 | }); 23 | return helper; 24 | } 25 | 26 | + (void)initialize{ 27 | [self removePropertyWithColumnNameArray:@[@"dataModes"]]; 28 | } 29 | 30 | +(NSString *)getTableName{ 31 | return NSStringFromClass([self class]); 32 | } 33 | 34 | + (BOOL)isContainParent{ 35 | return NO; 36 | } 37 | 38 | +(NSString *)getPrimaryKey{ 39 | return @"uuid"; 40 | } 41 | 42 | 43 | - (instancetype)init 44 | { 45 | self = [super init]; 46 | if (self) { 47 | _uuid = [NSUUID UUID].UUIDString; 48 | _createTime = [[NSDate date] timeIntervalSince1970]; 49 | } 50 | return self; 51 | } 52 | 53 | 54 | + (instancetype)defaultDraftModel{ 55 | DraftMetaDataModel *draftModel = [[DraftMetaDataModel alloc] init]; 56 | 57 | NSString *html = @"

Writing Swift code is interactive and fun, the syntax is concise yet expressive, and apps run lightning-fast. Swift is ready for your next project — or addition into your current app — because Swift code works side-by-side with Objective-C.

"; 58 | NSMutableString *string = [NSMutableString stringWithString:html]; 59 | NSMutableArray *datas = [NSMutableArray arrayWithCapacity:0]; 60 | CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; 61 | 62 | for (int i = 0; i < 200; i++) { 63 | if (i%2 == 0) { 64 | DraftAttachmentDataModel *gifDataModel = [[DraftAttachmentDataModel alloc] init]; 65 | gifDataModel.attachmentWidth = screenWidth - 20; 66 | gifDataModel.attachmentHeight = 200; 67 | gifDataModel.attachmentType = GifAttachmentType; 68 | 69 | [datas addObject:gifDataModel]; 70 | }else{ 71 | DraftAttachmentDataModel *audioDataModel = [[DraftAttachmentDataModel alloc] init]; 72 | audioDataModel.attachmentWidth = screenWidth - 20; 73 | audioDataModel.attachmentHeight = 50; 74 | 75 | audioDataModel.attachmentType = AudioAttachmentType; 76 | 77 | [datas addObject:audioDataModel]; 78 | } 79 | [string appendFormat:@"%@%@",kAttachmentDraftPlaceholder,html]; 80 | } 81 | 82 | draftModel.richTextType = RichTextHtml; 83 | draftModel.richText = string; 84 | draftModel.dataModes = datas; 85 | return draftModel; 86 | } 87 | 88 | @end 89 | 90 | 91 | 92 | 93 | 94 | @implementation DraftAttachmentDataModel 95 | 96 | - (instancetype)init 97 | { 98 | self = [super init]; 99 | if (self) { 100 | _uuid = [NSUUID UUID].UUIDString; 101 | } 102 | return self; 103 | } 104 | 105 | +(LKDBHelper *)getUsingLKDBHelper{ 106 | static LKDBHelper *helper; 107 | static dispatch_once_t onceToken; 108 | dispatch_once(&onceToken, ^{ 109 | helper = [[LKDBHelper alloc] initWithDBName:@"draft"]; 110 | }); 111 | return helper; 112 | } 113 | 114 | +(NSString *)getTableName{ 115 | return NSStringFromClass([self class]); 116 | } 117 | 118 | + (BOOL)isContainParent{ 119 | return NO; 120 | } 121 | 122 | +(NSString *)getPrimaryKey{ 123 | return @"uuid"; 124 | } 125 | 126 | 127 | @end 128 | -------------------------------------------------------------------------------- /Classes/Draft/NSTextStorage+Draft.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextStorage+Draft.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DraftMetaDataModel.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface NSTextStorage (Draft) 15 | 16 | + (NSTextStorage *)textStorageWithDraft:(DraftMetaDataModel *)draft typingAttributes:(NSDictionary *)typingAttributes; 17 | 18 | - (void)buildDraft:(RichTextType)richTextType complated:(void(^)(DraftMetaDataModel *draftModel))complated; 19 | 20 | - (void)serializationDraftToJSON:(DraftMetaDataModel *)draftModel; 21 | 22 | @end 23 | 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /Classes/Draft/NSTextStorage+Draft.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextStorage+Draft.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "NSTextStorage+Draft.h" 10 | #import "MediaTextAttachment.h" 11 | #import "AnyViewTextAttachment.h" 12 | #import "NSTextStorage+Utils.h" 13 | #import "NSTextStorage+Html.h" 14 | 15 | #define TTextAttachmentAttributeName (@"TTextAttachmentAttributeName") 16 | 17 | @implementation NSTextStorage (Draft) 18 | 19 | + (NSTextStorage *)textStorageWithDraft:(DraftMetaDataModel *)draft typingAttributes:(NSDictionary *)typingAttributes{ 20 | if (!draft || draft.richText.length == 0) { 21 | return [[NSTextStorage alloc] init]; 22 | } 23 | NSMutableAttributedString *textAttr = [NSTextStorage attributedStringForSimpleHtml:draft.richText typingAttributes:typingAttributes]; 24 | 25 | NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:kAttachmentDraftPlaceholderRegularExpression options:NSRegularExpressionCaseInsensitive error:nil]; 26 | __block NSInteger index = 0; 27 | NSRange allRange = NSMakeRange(0, textAttr.length); 28 | NSArray *resute = [expression matchesInString:textAttr.string options:0 range:allRange]; 29 | 30 | for (NSTextCheckingResult *match in resute) { 31 | if (!match || index >= draft.dataModes.count) break; 32 | 33 | /// 标记 34 | NSMutableAttributedString *attachmentAttr = [[NSMutableAttributedString alloc ] initWithString: [textAttr.string substringWithRange:match.range] ]; 35 | [attachmentAttr addAttribute:TTextAttachmentAttributeName value:@(1) range:NSMakeRange(0, attachmentAttr.length)]; 36 | 37 | [textAttr replaceCharactersInRange:match.range withAttributedString:attachmentAttr]; 38 | index = index + 1; 39 | } 40 | 41 | index = 0; 42 | [textAttr enumerateAttribute:TTextAttachmentAttributeName inRange:allRange options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 43 | if (!value || index >= draft.dataModes.count) { 44 | return ; 45 | } 46 | NSTextAttachment *attachment = [MediaTextAttachment attachmentForMetaData:draft.dataModes[index]]; 47 | NSAttributedString* attachmentAttr = [NSTextStorage attributedStringForAttachment:attachment typingAttributes:typingAttributes]; 48 | [textAttr replaceCharactersInRange:range withAttributedString:attachmentAttr]; 49 | }]; 50 | 51 | 52 | return [[NSTextStorage alloc] initWithAttributedString:textAttr]; 53 | } 54 | 55 | 56 | - (void)buildDraft:(RichTextType)richTextType complated:(void(^)(DraftMetaDataModel *draftModel))complated{ 57 | DraftMetaDataModel *draft = [[DraftMetaDataModel alloc] init]; 58 | NSMutableArray *dataModes = [NSMutableArray arrayWithCapacity:0]; 59 | 60 | NSString * textDraft = [self draftHtmlForAttributedString:^(NSTextAttachment * _Nonnull attachment) { 61 | DraftAttachmentDataModel *dataModel = [(MediaTextAttachment *)attachment attachmentDraftMetaData]; 62 | [dataModes addObject: dataModel]; 63 | }]; 64 | 65 | draft.richTextType = RichTextHtml; 66 | 67 | draft.richText = textDraft; 68 | draft.dataModes = dataModes; 69 | 70 | [self serializationDraftToJSON:draft]; 71 | 72 | if (complated) { 73 | complated(draft); 74 | } 75 | } 76 | 77 | - (void)serializationDraftToJSON:(DraftMetaDataModel *)draftModel{ 78 | if (!draftModel || draftModel.richText.length == 0) { 79 | return; 80 | } 81 | NSMutableArray *jsonArr = [NSMutableArray arrayWithCapacity:0]; 82 | NSArray *textArr = [NSTextStorage subTextArrFor:draftModel.richText spliteWith:kAttachmentDraftPlaceholder]; 83 | if (textArr.count == 1) { 84 | jsonArr = [NSMutableArray arrayWithArray:textArr]; 85 | }else{ 86 | NSInteger index = 0; 87 | for (NSString *text in textArr) { 88 | if ([text isEqualToString:kAttachmentDraftPlaceholder] && index < draftModel.dataModes.count) { 89 | /// 附件 90 | DraftAttachmentDataModel *attachmentDataModel = draftModel.dataModes[index]; 91 | [jsonArr addObject:attachmentDataModel]; 92 | index = index + 1; 93 | }else{ 94 | [jsonArr addObject:text]; 95 | } 96 | } 97 | } 98 | NSLog(@"~~~~~~~~~@!!!jsonArr!!!!!!!!!%@",jsonArr); 99 | } 100 | 101 | #pragma mark - Private 102 | 103 | /// 分割字符串 返回有序数组 ===> @[subText,placeholder,subText,placeholder....] 104 | + (NSArray *)subTextArrFor:(NSString *)text spliteWith:(NSString *)placeholder{ 105 | if (text.length == 0) { 106 | return @[]; 107 | } 108 | if (placeholder.length == 0) { 109 | return @[text]; 110 | } 111 | NSRange searchRange = NSMakeRange(0, text.length); 112 | NSMutableArray *textArrs = [NSMutableArray arrayWithCapacity:0]; 113 | while (true) { 114 | if (searchRange.location == NSNotFound || searchRange.length == 0 || searchRange.length + searchRange.location > text.length) { 115 | break; 116 | } 117 | NSRange placeholderRange = [text rangeOfString:placeholder options:NSCaseInsensitiveSearch range:searchRange locale:nil]; 118 | if (placeholderRange.length == 0 || placeholderRange.location == NSNotFound) { 119 | /// 没有了 120 | NSString *subText = [text substringWithRange:searchRange]; 121 | [textArrs addObject:subText]; 122 | break; 123 | } 124 | 125 | if (placeholderRange.location == searchRange.location) { 126 | NSString *placeholderText = [text substringWithRange:placeholderRange]; 127 | [textArrs addObject:placeholderText]; 128 | }else{ 129 | NSRange textRange = NSMakeRange(searchRange.location, placeholderRange.location - searchRange.location); 130 | NSString *subText = [text substringWithRange:textRange]; 131 | [textArrs addObject:subText]; 132 | 133 | NSString *placeholderText = [text substringWithRange:placeholderRange]; 134 | [textArrs addObject:placeholderText]; 135 | } 136 | 137 | searchRange.location = placeholderRange.location + placeholderRange.length; 138 | searchRange.length = text.length - searchRange.location; 139 | } 140 | return [textArrs copy]; 141 | } 142 | 143 | @end 144 | -------------------------------------------------------------------------------- /Classes/Draft/RichTextView+Draft.h: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextView+Draft.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/22. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "RichTextView.h" 10 | #import "DraftMetaDataModel.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface RichTextView (Draft) 15 | 16 | +(instancetype _Nullable )richTextViewWithDraft:(DraftMetaDataModel* _Nullable )draft 17 | typingAttributes:(NSDictionary *_Nullable)typingAttributes; 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /Classes/Draft/RichTextView+Draft.m: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextView+Draft.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/22. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "RichTextView+Draft.h" 10 | #import "AttachmentLayoutManager.h" 11 | #import "NSTextStorage+Draft.h" 12 | 13 | @implementation RichTextView (Draft) 14 | 15 | +(instancetype)richTextViewWithDraft:(DraftMetaDataModel *)draft typingAttributes:(NSDictionary *)typingAttributes{ 16 | NSTextStorage *storage = [NSTextStorage textStorageWithDraft:draft typingAttributes:typingAttributes]; 17 | NSTextContainer *container = [[NSTextContainer alloc] init]; 18 | AttachmentLayoutManager *layoutManager = [[AttachmentLayoutManager alloc] init]; 19 | 20 | container.heightTracksTextView = YES; 21 | container.widthTracksTextView = YES; 22 | [storage addLayoutManager:layoutManager]; 23 | [layoutManager addTextContainer:container]; 24 | 25 | RichTextView *textView = [[RichTextView alloc] initWithFrame:CGRectZero textContainer:container]; 26 | textView.layoutManager.allowsNonContiguousLayout = NO; 27 | textView.typingAttributes = typingAttributes; 28 | return textView; 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Classes/RichTextTools.h: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextTools.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #ifndef RichTextTools_h 10 | #define RichTextTools_h 11 | 12 | #define XFRichTextSrcName(file) ([@"XFRichTextTool.bundle" stringByAppendingPathComponent:file]) 13 | #define XFRichTextImage(file) ([UIImage imageNamed:XFRichTextSrcName(file)]) 14 | 15 | 16 | #define kHtmlExpression(tag) ([NSString stringWithFormat:@"\\<(%@)(?![\\w-])([^\\>\\/]*(?:\\/(?!\\>)[^\\>\\/]*)*?)(?:(\\/)\\>|\\>(?:([^\\<]*(?:\\<(?!\\/\\1\\>)[^\\<]*)*)(\\<\\/\\1\\>))?)",tag]) 17 | 18 | #define UnavailableMacro(msg) __attribute__((unavailable(msg))) 19 | 20 | /// 附件类型 21 | typedef NS_ENUM(NSUInteger, AttachmentType) { 22 | UndefinedAttachmentType, 23 | ImgAttachmentType, 24 | GifAttachmentType, 25 | AudioAttachmentType, 26 | VideoAttachmentType, 27 | }; 28 | 29 | /// 富文本存储类型 30 | typedef NS_ENUM(NSUInteger, RichTextType) { 31 | RichTextHtml 32 | }; 33 | 34 | /// 草稿类型 35 | typedef NS_ENUM(NSUInteger, DraftType) { 36 | DraftDefaultType 37 | }; 38 | 39 | /// 草稿状态 40 | typedef NS_ENUM(NSUInteger, DraftEditStatus) { 41 | DraftEditing, /// 编辑中 42 | DraftEdited, /// 编辑完成 43 | }; 44 | 45 | 46 | /// html 存储草稿: 附件占位符 47 | static NSString *const kAttachmentDraftPlaceholder = @"[Attachment]"; 48 | static NSString *const kAttachmentDraftPlaceholderRegularExpression = @"\\[Attachment\\]"; 49 | 50 | #endif /* RichTextTools_h */ 51 | -------------------------------------------------------------------------------- /Classes/RichTextView/RichTextView+TextAttachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextView+TextAttachment.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/27. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "RichTextView.h" 10 | #import "MediaTextAttachment.h" 11 | #import "ImageTextAttachment.h" 12 | #import "VideoTextAttachment.h" 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | @interface RichTextView (TextAttachment) 17 | 18 | /// Attachment 19 | - (NSInteger)imageAttachmentCount; 20 | - (NSInteger)videoAttachmentCount; 21 | 22 | // 获取所有未上传成功的附件 23 | - (NSArray *)allPendingUploadAttachments; 24 | 25 | // insert attachment 26 | - (ImageTextAttachment *)insertImgAttachment:(UIImage *)resizeImg paragraphStyle:(NSMutableParagraphStyle*)style; 27 | - (VideoTextAttachment *)insertVideoAttachment:(UIImage *)posterImg paragraphStyle:(NSMutableParagraphStyle*)style; 28 | 29 | @end 30 | 31 | NS_ASSUME_NONNULL_END 32 | -------------------------------------------------------------------------------- /Classes/RichTextView/RichTextView+TextAttachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextView+TextAttachment.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/27. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "RichTextView+TextAttachment.h" 10 | #import "UITextView+TextAttachmentUtils.h" 11 | 12 | @implementation RichTextView (TextAttachment) 13 | 14 | - (VideoTextAttachment *)insertVideoAttachment:(UIImage *)posterImg paragraphStyle:(NSMutableParagraphStyle*)style{ 15 | VideoTextAttachment *videoAttachment = [[VideoTextAttachment alloc] init]; 16 | 17 | videoAttachment.bounds = CGRectMake(0, 0, posterImg.size.width, posterImg.size.height); 18 | videoAttachment.image = posterImg; 19 | 20 | [self insertAttachment:videoAttachment paragraphStyle:style]; 21 | return videoAttachment; 22 | } 23 | 24 | - (ImageTextAttachment *)insertImgAttachment:(UIImage *)resizeImg paragraphStyle:(NSMutableParagraphStyle*)style{ 25 | ImageTextAttachment* imgAttachment = [[ImageTextAttachment alloc] init]; 26 | 27 | imgAttachment.bounds = CGRectMake(0, 0, resizeImg.size.width, resizeImg.size.height);// 28 | imgAttachment.image = resizeImg; 29 | 30 | [self insertAttachment:imgAttachment paragraphStyle:style]; 31 | return imgAttachment; 32 | } 33 | 34 | 35 | - (NSInteger)imageAttachmentCount{ 36 | __block NSInteger count = 0; 37 | [self.textStorage enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, self.textStorage.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 38 | 39 | if (value != nil && [value isKindOfClass:[ImageTextAttachment class]]) { 40 | count++; 41 | } 42 | }]; 43 | return count; 44 | } 45 | 46 | - (NSInteger)videoAttachmentCount{ 47 | __block NSInteger count = 0; 48 | [self.textStorage enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, self.textStorage.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 49 | 50 | if (value != nil && [value isKindOfClass:[VideoTextAttachment class]]) { 51 | count++; 52 | } 53 | }]; 54 | return count; 55 | } 56 | 57 | - (NSArray *)allPendingUploadAttachments{ 58 | NSMutableArray * pendingUploadAtts = [NSMutableArray arrayWithCapacity:0]; 59 | NSArray *allAttachments = [self allAttachments]; 60 | for (NSTextAttachment *att in allAttachments) { 61 | if ([att isKindOfClass:[MediaTextAttachment class]]) { 62 | if (![(MediaTextAttachment *)att isUploaded]) { 63 | [pendingUploadAtts addObject:(MediaTextAttachment *)att]; 64 | } 65 | } 66 | } 67 | return pendingUploadAtts; 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /Classes/RichTextView/RichTextView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextView.h 3 | // 4 | // 5 | // Created by tbin on 2018/7/19. 6 | // Copyright © 2018年 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "NSTextStorage+ProcessEditing.h" 11 | #import "UITextView+Binding.h" 12 | #import "UITextView+TextAttachmentUtils.h" 13 | #import "TextViewAttachmentDelegate.h" 14 | 15 | @interface RichTextView : UITextView 16 | 17 | +(instancetype _Nullable )defaultRichTextView; 18 | 19 | @property (nonatomic,assign) CGFloat estimatedReusableViewHeight; 20 | 21 | @property (nonatomic,weak)id _Nullable attachmentDelegate; 22 | 23 | /// 注册重用View 24 | - (void)registerClass:(nullable Class)reusableViewClass forReusableViewWithIdentifier:(NSString *_Nullable)identifier; 25 | 26 | /// 获取重用view 27 | - (TextAttachmentReusableView *_Nullable)dequeueReusableAttachmentViewWithIdentifier:(NSString *_Nullable)identifier; 28 | @end 29 | -------------------------------------------------------------------------------- /Classes/RichTextView/RichTextView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextView.m 3 | // 4 | // 5 | // Created by tbin on 2018/7/19. 6 | // Copyright © 2018年 bin. All rights reserved. 7 | // 8 | 9 | #import "RichTextView.h" 10 | #import "AttachmentGestureRecognizerDelegate.h" 11 | #import "AttachmentLayoutManager.h" 12 | #import "RichTextTools.h" 13 | #import "MediaTextAttachment.h" 14 | #import "NSLayoutManager+Attachment.h" 15 | #import "NSTextStorage+Utils.h" 16 | #import "NSObject+MJKeyValue.h" 17 | #import "NSObject+MJKeyValue.h" 18 | 19 | @interface RichTextView () 20 | 21 | @property (nonatomic,strong) UITapGestureRecognizer* attachmentGestureRecognizer; 22 | 23 | @property (nonatomic,strong) AttachmentGestureRecognizerDelegate * recognizerDelegate; 24 | 25 | @end 26 | 27 | @implementation RichTextView 28 | 29 | #pragma mark - init 30 | 31 | +(instancetype)defaultRichTextView{ 32 | NSTextContainer *container = [[NSTextContainer alloc] init]; 33 | container.heightTracksTextView = YES; 34 | container.widthTracksTextView = YES; 35 | 36 | NSTextStorage *storage = [[NSTextStorage alloc] init]; 37 | AttachmentLayoutManager *layoutManager = [[AttachmentLayoutManager alloc] init]; 38 | 39 | [storage addLayoutManager:layoutManager]; 40 | [layoutManager addTextContainer:container]; 41 | 42 | RichTextView *textView = [[RichTextView alloc] initWithFrame:CGRectZero textContainer:container]; 43 | textView.layoutManager.allowsNonContiguousLayout = NO; 44 | return textView; 45 | } 46 | 47 | - (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer{ 48 | self = [super initWithFrame:frame textContainer:textContainer]; 49 | if (self) { 50 | /// 添加手势 51 | [self setupAttachmentTouchDetection]; 52 | [self setupLayoutManager]; 53 | } 54 | return self; 55 | } 56 | 57 | - (void)dealloc 58 | { 59 | if ([self.textContainer.layoutManager isKindOfClass:[AttachmentLayoutManager class]]) { 60 | AttachmentLayoutManager *attLayoutManager = (AttachmentLayoutManager *)self.textContainer.layoutManager; 61 | [self removeObserver:attLayoutManager.attachmentsManager forKeyPath:@"contentOffset"]; 62 | NSLog(@"RichTextView~~removeObserver:forKeyPath:~~~contentOffset"); 63 | } 64 | NSLog(@"~~RichTextView~~~~dealloc"); 65 | } 66 | 67 | #pragma mark - estimatedReusableViewHeight 68 | 69 | - (void)setEstimatedReusableViewHeight:(CGFloat)estimatedReusableViewHeight{ 70 | _estimatedReusableViewHeight = estimatedReusableViewHeight; 71 | if (![self.textContainer.layoutManager isKindOfClass:[AttachmentLayoutManager class]]) { 72 | return; 73 | } 74 | AttachmentLayoutManager *attLayoutManager = (AttachmentLayoutManager *)self.textContainer.layoutManager; 75 | attLayoutManager.attachmentsManager.estimatedReusableViewHeight = estimatedReusableViewHeight; 76 | } 77 | 78 | #pragma mark - register & dequeue 79 | 80 | - (void)registerClass:(nullable Class)reusableViewClass forReusableViewWithIdentifier:(NSString *)identifier{ 81 | if (![self.textContainer.layoutManager isKindOfClass:[AttachmentLayoutManager class]]) { 82 | return; 83 | } 84 | AttachmentLayoutManager *attLayoutManager = (AttachmentLayoutManager *)self.textContainer.layoutManager; 85 | [attLayoutManager.attachmentsManager registerReusableView:reusableViewClass reuseIdentifier:identifier]; 86 | } 87 | 88 | - (TextAttachmentReusableView *)dequeueReusableAttachmentViewWithIdentifier:(NSString *)identifier{ 89 | if (![self.textContainer.layoutManager isKindOfClass:[AttachmentLayoutManager class]]) { 90 | NSAssert([self.textContainer.layoutManager isKindOfClass:AttachmentLayoutManager.class], @"layoutManager must is AttachmentLayoutManager for dequeueReusableAttachmentViewWithIdentifier:"); 91 | return nil; 92 | } 93 | AttachmentLayoutManager *attLayoutManager = (AttachmentLayoutManager *)self.textContainer.layoutManager; 94 | return [attLayoutManager.attachmentsManager dequeueReusableAttachmentView:identifier]; 95 | } 96 | 97 | #pragma mark - setup 98 | 99 | - (void)setupAttachmentTouchDetection{ 100 | for (UIGestureRecognizer *gesture in self.gestureRecognizers) { 101 | [gesture requireGestureRecognizerToFail:self.attachmentGestureRecognizer]; 102 | } 103 | [self addGestureRecognizer:self.attachmentGestureRecognizer]; 104 | } 105 | 106 | - (void)setupLayoutManager{ 107 | if (![self.textContainer.layoutManager isKindOfClass:[AttachmentLayoutManager class]]) { 108 | return; 109 | } 110 | /// init TextAttachmentsManager 111 | TextAttachmentsManager *attachmentsManager = [TextAttachmentsManager attachmentsManagerFor:self]; 112 | /// config AttachmentLayoutManager 113 | AttachmentLayoutManager *anyViewLayoutManager = (AttachmentLayoutManager *)self.textContainer.layoutManager; 114 | anyViewLayoutManager.attachmentsManager = attachmentsManager; 115 | } 116 | 117 | #pragma mark - override 118 | 119 | - (void)paste:(id)sender{ 120 | if (UIPasteboard.generalPasteboard.string.length == 0) { 121 | [super paste:sender]; 122 | return; 123 | } 124 | NSString *contentStr = UIPasteboard.generalPasteboard.string; 125 | NSMutableAttributedString *pasteAttrString = [[NSMutableAttributedString alloc] initWithString:contentStr]; 126 | pasteAttrString = [self attributedStringForAttachmentJSON:pasteAttrString]; 127 | [self.typingAttributes enumerateKeysAndObjectsUsingBlock:^(NSAttributedStringKey _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 128 | [pasteAttrString addAttribute:key value:obj range:NSMakeRange(0, pasteAttrString.length)]; 129 | }]; 130 | 131 | [self.textStorage replaceCharactersInRange:self.selectedRange withAttributedString:pasteAttrString]; 132 | UITextPosition *end = [self positionFromPosition:self.selectedTextRange.start offset:pasteAttrString.length]; 133 | self.selectedTextRange = [self textRangeFromPosition:end toPosition:end]; 134 | [self.layoutManager invalidateDisplayForCharacterRange:self.selectedRange]; 135 | } 136 | 137 | - (void)copy:(id)sender{ 138 | NSString *result = [self convertSelectedRangeToString]; 139 | if (result.length > 0) { 140 | [super copy:sender]; 141 | UIPasteboard *defaultPasteboard = [UIPasteboard generalPasteboard]; 142 | [defaultPasteboard setString:result]; 143 | return; 144 | } 145 | [super copy:sender]; 146 | } 147 | 148 | - (void)cut:(id)sender{ 149 | NSString *result = [self convertSelectedRangeToString]; 150 | if (result.length > 0) { 151 | [super cut:sender]; 152 | UIPasteboard *defaultPasteboard = [UIPasteboard generalPasteboard]; 153 | [defaultPasteboard setString:result]; 154 | return; 155 | } 156 | [super cut:sender]; 157 | } 158 | 159 | - (void)deleteBackward{ 160 | if (!(self.selectedRange.location > 0 || self.selectedRange.length > 0)) { 161 | return; 162 | } 163 | 164 | if (![self.textContainer.layoutManager isKindOfClass:[AttachmentLayoutManager class]]) { 165 | [super deleteBackward]; 166 | return; 167 | } 168 | 169 | AttachmentLayoutManager *anyViewLayoutManager = (AttachmentLayoutManager *)self.textContainer.layoutManager; 170 | NSRange range = self.selectedRange; 171 | if (range.length == 0) { 172 | NSInteger loc = (range.location - 1 < 0) ? 0 : (range.location - 1); 173 | range = NSMakeRange(loc, 1); 174 | } 175 | NSArray *attachments = [self viewAttachments:range]; 176 | for (NSTextAttachment *att in attachments) { 177 | if ([att isKindOfClass:[AnyViewTextAttachment class]]) { 178 | TextAttachmentReusableView * reusableView = [anyViewLayoutManager.attachmentsManager reusableViewInTableFor:(AnyViewTextAttachment *)att]; 179 | [reusableView removeFromSuperview]; 180 | [anyViewLayoutManager.attachmentsManager removeAttachmentFromMapTable:(AnyViewTextAttachment *)att]; 181 | } 182 | } 183 | [self.textStorage setAttributes:nil range:range]; 184 | [self.layoutManager invalidateDisplayForCharacterRange:range]; 185 | [super deleteBackward]; 186 | } 187 | 188 | /// 调整光标高度 189 | //- (CGRect)caretRectForPosition:(UITextPosition *)position{ 190 | // CGRect rect = [super caretRectForPosition:position]; 191 | // if (self.font == nil) { 192 | // return rect; 193 | // } 194 | // CGFloat newHeight = self.font.pointSize - self.font.descender; 195 | // CGFloat newY = rect.origin.y + rect.size.height - newHeight; 196 | // rect.size.height = newHeight; 197 | // rect.origin.y = newY; 198 | // return rect; 199 | //} 200 | 201 | #pragma mark - Private 202 | 203 | - (NSString *)convertSelectedRangeToString{ 204 | NSMutableString *result = [[NSMutableString alloc]initWithCapacity:0]; 205 | NSRange selectedRange = self.selectedRange; 206 | NSRange effectiveRange = NSMakeRange(selectedRange.location,0); 207 | NSUInteger length = NSMaxRange(selectedRange); 208 | while (NSMaxRange(effectiveRange) < length) { 209 | NSTextAttachment *attachment = [self.attributedText attribute:NSAttachmentAttributeName atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange]; 210 | if(attachment){ 211 | NSString *attachmentJSON = [self copyAttachmentToJSON:attachment]; 212 | if (attachmentJSON) { 213 | [result appendString:attachmentJSON]; 214 | } 215 | }else{ 216 | NSString *subStr = [self.text substringWithRange:effectiveRange]; 217 | [result appendString:subStr]; 218 | } 219 | } 220 | return [result copy]; 221 | } 222 | 223 | /// JSON 标记 224 | - (NSString *)copyAttachmentToJSON:(NSTextAttachment *)attachment{ 225 | if (![self.textContainer.layoutManager isKindOfClass:[AttachmentLayoutManager class]]) { 226 | return nil; 227 | } 228 | AttachmentLayoutManager *anyViewLayoutManager = (AttachmentLayoutManager *)self.textContainer.layoutManager; 229 | NSRange selectedRange = self.selectedRange; 230 | if(![attachment isKindOfClass:[MediaTextAttachment class]]){ 231 | return nil; 232 | } 233 | TextAttachmentReusableView * reusableView = [anyViewLayoutManager.attachmentsManager reusableViewInTableFor:(AnyViewTextAttachment *)attachment]; 234 | [reusableView removeFromSuperview]; 235 | [anyViewLayoutManager.attachmentsManager removeAttachmentFromMapTable:(AnyViewTextAttachment *)attachment]; 236 | [self.layoutManager invalidateDisplayForCharacterRange:selectedRange]; 237 | 238 | DraftAttachmentDataModel *draftAttachmentModel = [(MediaTextAttachment *)attachment attachmentDraftMetaData]; 239 | if (draftAttachmentModel) { 240 | NSDictionary *dic = [draftAttachmentModel mj_keyValuesWithIgnoredKeys:nil]; 241 | NSError *error; 242 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error]; 243 | NSString * jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 244 | if (jsonString) { 245 | jsonString = [NSString stringWithFormat:@"%@%@%@",@"",jsonString,@""]; 246 | return jsonString; 247 | } 248 | } 249 | 250 | return nil; 251 | } 252 | 253 | /// 解析JSON标记 254 | - (NSMutableAttributedString *)attributedStringForAttachmentJSON:(NSMutableAttributedString *)pasteAttrString{ 255 | if (!pasteAttrString) { 256 | return nil; 257 | } 258 | NSRegularExpression *kTopicExp = [NSRegularExpression regularExpressionWithPattern:kHtmlExpression(@"Attachment") options:NSRegularExpressionCaseInsensitive error:nil]; 259 | [kTopicExp enumerateMatchesInString:pasteAttrString.string options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, pasteAttrString.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { 260 | if (!result) return; 261 | 262 | NSString *matchStr = [pasteAttrString.string substringWithRange:result.range]; 263 | matchStr = [matchStr stringByReplacingOccurrencesOfString:@"" withString:@""]; 264 | matchStr = [matchStr stringByReplacingOccurrencesOfString:@"" withString:@""]; 265 | DraftAttachmentDataModel *draftAttachmentModel = [DraftAttachmentDataModel mj_objectWithKeyValues:matchStr]; 266 | if (draftAttachmentModel) { 267 | NSTextAttachment *attachment = [MediaTextAttachment attachmentForMetaData:draftAttachmentModel]; 268 | NSMutableAttributedString *insertion = [NSTextStorage attributedStringForAttachment:attachment typingAttributes:self.typingAttributes]; 269 | [pasteAttrString replaceCharactersInRange:result.range withAttributedString:insertion]; 270 | } 271 | }]; 272 | 273 | return pasteAttrString; 274 | } 275 | 276 | #pragma mark - Geter & Setter 277 | 278 | - (UITapGestureRecognizer *)attachmentGestureRecognizer{ 279 | if (!_attachmentGestureRecognizer) { 280 | _attachmentGestureRecognizer = [[UITapGestureRecognizer alloc] init]; 281 | _attachmentGestureRecognizer.cancelsTouchesInView = YES; 282 | _attachmentGestureRecognizer.delaysTouchesBegan = YES; 283 | _attachmentGestureRecognizer.delaysTouchesEnded = YES; 284 | _attachmentGestureRecognizer.delegate = self.recognizerDelegate; 285 | [_attachmentGestureRecognizer addTarget:self.recognizerDelegate action:@selector(richTextViewWasPressed:)]; 286 | } 287 | return _attachmentGestureRecognizer; 288 | } 289 | 290 | - (AttachmentGestureRecognizerDelegate *)recognizerDelegate{ 291 | if (!_recognizerDelegate) { 292 | _recognizerDelegate = [[AttachmentGestureRecognizerDelegate alloc] init]; 293 | _recognizerDelegate.textView = self; 294 | } 295 | return _recognizerDelegate; 296 | } 297 | 298 | @end 299 | -------------------------------------------------------------------------------- /Classes/TextAttachment/AnyViewTextAttachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // AnyViewTextAttachment.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/19. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "MediaTextAttachment.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface AnyViewTextAttachment : MediaTextAttachment 15 | 16 | /// view的边距 17 | @property (nonatomic,assign) UIEdgeInsets contentInset; 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /Classes/TextAttachment/AnyViewTextAttachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // AnyViewTextAttachment.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/19. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "AnyViewTextAttachment.h" 10 | 11 | @implementation AnyViewTextAttachment 12 | 13 | - (void)dealloc{ 14 | NSLog(@"~~~AnyViewTextAttachment~~~~dealloc~"); 15 | } 16 | 17 | - (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex{ 18 | return self.bounds; 19 | } 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Classes/TextAttachment/AudioTextAttachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // AudioTextAttachment.h 3 | // HOHO 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import "AnyViewTextAttachment.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface AudioTextAttachment : AnyViewTextAttachment 14 | 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /Classes/TextAttachment/AudioTextAttachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // AudioTextAttachment.m 3 | // HOHO 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import "AudioTextAttachment.h" 10 | 11 | @implementation AudioTextAttachment 12 | 13 | #pragma mark - CopyTextAttachmentProtocol 14 | 15 | - (DraftAttachmentDataModel *)attachmentDraftMetaData{ 16 | DraftAttachmentDataModel *dataModel = [[DraftAttachmentDataModel alloc] init]; 17 | dataModel.attachmentType = AudioAttachmentType; 18 | dataModel.attachmentWidth = self.bounds.size.width; 19 | dataModel.attachmentHeight = self.bounds.size.height; 20 | // NSAssert(NO, @"TODO ...Function"); 21 | return dataModel; 22 | } 23 | 24 | - (instancetype)setupAttachmentForMetaData:(DraftAttachmentDataModel *)metaData{ 25 | // NSAssert(NO, @"TODO ...Function"); 26 | return self; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Classes/TextAttachment/CopyTextAttachmentProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // CopyTextAttachmentProtocol.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/27. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DraftMetaDataModel.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @protocol CopyTextAttachmentProtocol 15 | 16 | /// 支持 NSTextAttachment 复制、粘贴。 17 | - (DraftAttachmentDataModel *)attachmentDraftMetaData; 18 | 19 | /// 支持 NSTextAttachment 复制、粘贴。 20 | - (instancetype)setupAttachmentForMetaData:(DraftAttachmentDataModel *)metaData; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /Classes/TextAttachment/GifTextAttachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // GifTextAttachment.h 3 | // HOHO 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import "AnyViewTextAttachment.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface GifTextAttachment : AnyViewTextAttachment 14 | 15 | @property (nonatomic,copy) NSString *gif; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /Classes/TextAttachment/GifTextAttachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // GifTextAttachment.m 3 | // HOHO 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import "GifTextAttachment.h" 10 | 11 | @implementation GifTextAttachment 12 | 13 | #pragma mark - CopyTextAttachmentProtocol 14 | 15 | - (DraftAttachmentDataModel *)attachmentDraftMetaData{ 16 | DraftAttachmentDataModel *dataModel = [[DraftAttachmentDataModel alloc] init]; 17 | dataModel.attachmentType = GifAttachmentType; 18 | dataModel.attachmentWidth = self.bounds.size.width; 19 | dataModel.attachmentHeight = self.bounds.size.height; 20 | dataModel.userInfo = @{@"gif":@"niconiconi"}; 21 | 22 | return dataModel; 23 | } 24 | 25 | - (instancetype)setupAttachmentForMetaData:(DraftAttachmentDataModel *)metaData{ 26 | self.gif = metaData.userInfo[@"gif"]; 27 | return self; 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Classes/TextAttachment/ImageTextAttachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // ImageTextAttachment.h 3 | // RichText 4 | // 5 | // Created by tbin on 2018/7/16. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "MediaTextAttachment.h" 12 | 13 | @interface ImageTextAttachment : MediaTextAttachment 14 | 15 | @property (nonatomic,copy) NSURL *imageURL; 16 | 17 | @property (nonatomic,strong) PHAsset *imgAsset; 18 | 19 | @property (nonatomic,assign) CGSize orgImgSize; 20 | 21 | @property (nonatomic,copy) NSURL * thumbnailDiskPath; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Classes/TextAttachment/ImageTextAttachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // ImageTextAttachment.m 3 | // RichText 4 | // 5 | // Created by tbin on 2018/7/16. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import "ImageTextAttachment.h" 10 | #import "UIImage+Utils.h" 11 | 12 | @implementation ImageTextAttachment 13 | 14 | #pragma mark - CopyTextAttachmentProtocol 15 | 16 | - (DraftAttachmentDataModel *)attachmentDraftMetaData{ 17 | DraftAttachmentDataModel *dataModel = [[DraftAttachmentDataModel alloc] init]; 18 | dataModel.attachmentType = ImgAttachmentType; 19 | dataModel.attachmentWidth = self.bounds.size.width; 20 | dataModel.attachmentHeight = self.bounds.size.height; 21 | 22 | dataModel.diskPath = self.thumbnailDiskPath.absoluteString; 23 | return dataModel; 24 | } 25 | 26 | - (instancetype)setupAttachmentForMetaData:(DraftAttachmentDataModel *)metaData{ 27 | UIImage *img = [UIImage imageWithContentsOfFile:metaData.diskPath]; 28 | self.image = img; 29 | return self; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /Classes/TextAttachment/MediaTextAttachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaTextAttachment.h 3 | // 4 | // 5 | // Created by tbin on 2018/7/17. 6 | // Copyright © 2018年 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CopyTextAttachmentProtocol.h" 11 | 12 | @interface MediaTextAttachment : NSTextAttachment 13 | 14 | @property (nonatomic,assign) BOOL isUploaded; 15 | 16 | + (MediaTextAttachment *)attachmentForMetaData:(DraftAttachmentDataModel *)metaData NS_REQUIRES_SUPER; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Classes/TextAttachment/MediaTextAttachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // MediaTextAttachment.m 3 | // 4 | // 5 | // Created by tbin on 2018/7/17. 6 | // Copyright © 2018年 bin. All rights reserved. 7 | // 8 | 9 | #import "MediaTextAttachment.h" 10 | 11 | #import "ImageTextAttachment.h" 12 | #import "GifTextAttachment.h" 13 | #import "AudioTextAttachment.h" 14 | #import "VideoTextAttachment.h" 15 | 16 | @implementation MediaTextAttachment 17 | 18 | + (MediaTextAttachment *)attachmentForMetaData:(DraftAttachmentDataModel *)metaData{ 19 | MediaTextAttachment * textAttachment = nil; 20 | if (metaData.attachmentType == ImgAttachmentType) { 21 | textAttachment = [[ImageTextAttachment alloc] init]; 22 | }else if (metaData.attachmentType == GifAttachmentType){ 23 | textAttachment = [[GifTextAttachment alloc] init]; 24 | }else if (metaData.attachmentType == AudioAttachmentType){ 25 | textAttachment = [[AudioTextAttachment alloc] init]; 26 | }else if (metaData.attachmentType == VideoAttachmentType){ 27 | textAttachment = [[VideoTextAttachment alloc] init]; 28 | }else{ 29 | } 30 | 31 | textAttachment.bounds = CGRectMake(0, 0, metaData.attachmentWidth, metaData.attachmentHeight); 32 | return [textAttachment setupAttachmentForMetaData:metaData]; 33 | } 34 | 35 | #pragma mark - CopyTextAttachmentProtocol 36 | 37 | - (DraftAttachmentDataModel *)attachmentDraftMetaData{ 38 | return nil; 39 | } 40 | 41 | - (instancetype)setupAttachmentForMetaData:(DraftAttachmentDataModel *)metaData{ 42 | return self; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /Classes/TextAttachment/VideoTextAttachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // VideoTextAttachment.h 3 | // RichText 4 | // 5 | // Created by tbin on 2018/7/16. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "MediaTextAttachment.h" 12 | 13 | @interface VideoTextAttachment : MediaTextAttachment 14 | 15 | /// 已上传的视频封面远程地址 16 | @property (nonatomic,copy) NSURL *posterURL; 17 | /// 插入视频时已经写入沙盒路径 18 | @property (nonatomic,copy) NSURL *posterDiskPath; 19 | /// 封图尺寸 20 | @property (nonatomic,assign) CGSize coverSize; 21 | 22 | 23 | /// 已上传的视频远程地址 24 | @property (nonatomic,copy) NSURL *videoURL; 25 | /// 对应相册资源 26 | @property (nonatomic,strong) PHAsset *videoAsset; 27 | /// 上传时需判断是否为nil , 否则需要重新压缩写入沙盒 by videoAsset 28 | @property (nonatomic,copy) NSURL *videoDiskPath; 29 | 30 | 31 | /// 视频信息 32 | @property (nonatomic,assign) float duration; 33 | @property (nonatomic,assign) long long dataLength; 34 | @property (nonatomic,copy) NSString *format; 35 | 36 | @end 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Classes/TextAttachment/VideoTextAttachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // VideoTextAttachment.m 3 | // RichText 4 | // 5 | // Created by tbin on 2018/7/16. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import "VideoTextAttachment.h" 10 | 11 | @implementation VideoTextAttachment 12 | 13 | #pragma mark - CopyTextAttachmentProtocol 14 | 15 | - (DraftAttachmentDataModel *)attachmentDraftMetaData{ 16 | DraftAttachmentDataModel *dataModel = [[DraftAttachmentDataModel alloc] init]; 17 | dataModel.attachmentType = ImgAttachmentType; 18 | dataModel.attachmentWidth = self.bounds.size.width; 19 | dataModel.attachmentHeight = self.bounds.size.height; 20 | NSAssert(NO, @"TODO ...Function"); 21 | return dataModel; 22 | } 23 | 24 | - (instancetype)setupAttachmentForMetaData:(DraftAttachmentDataModel *)metaData{ 25 | NSAssert(NO, @"TODO ...Function"); 26 | return self; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/AttachmentGestureRecognizerDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentGestureRecognizerDelegate.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RichTextView.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface AttachmentGestureRecognizerDelegate : NSObject 15 | 16 | @property (nonatomic,weak) RichTextView *textView; 17 | 18 | - (void)richTextViewWasPressed:(UIGestureRecognizer *)recognizer; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/AttachmentGestureRecognizerDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentGestureRecognizerDelegate.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "AttachmentGestureRecognizerDelegate.h" 10 | 11 | @interface AttachmentGestureRecognizerDelegate () 12 | 13 | @property (nonatomic,weak) NSTextAttachment *currentSelectedAttachment; 14 | 15 | @end 16 | 17 | @implementation AttachmentGestureRecognizerDelegate 18 | 19 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(nonnull UIGestureRecognizer *)otherGestureRecognizer{ 20 | return YES; 21 | } 22 | 23 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{ 24 | if (!self.textView) { 25 | return NO; 26 | } 27 | 28 | CGPoint locationInTextView = [touch locationInView:self.textView]; 29 | BOOL isAttachmentInLocation = [self.textView attachmentAtPoint:locationInTextView] != nil; 30 | 31 | if (!isAttachmentInLocation) { 32 | NSTextAttachment *selectedAttachment = self.currentSelectedAttachment; 33 | if (selectedAttachment && [self.textView.attachmentDelegate respondsToSelector:@selector(textView:deselected:atPoint:)]) { 34 | [self.textView.attachmentDelegate textView:self.textView deselected:selectedAttachment atPoint:locationInTextView]; 35 | } 36 | self.currentSelectedAttachment = nil; 37 | } 38 | return isAttachmentInLocation; 39 | } 40 | 41 | 42 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ 43 | if (!self.textView) { 44 | return NO; 45 | } 46 | CGPoint locationInTextView = [gestureRecognizer locationInView:self.textView]; 47 | 48 | NSTextAttachment *pointAttachment = [self.textView attachmentAtPoint:locationInTextView]; 49 | 50 | if (!pointAttachment) { 51 | NSTextAttachment *selectedAttachment = self.currentSelectedAttachment; 52 | if (selectedAttachment && [self.textView.attachmentDelegate respondsToSelector:@selector(textView:deselected:atPoint:)]) { 53 | [self.textView.attachmentDelegate textView:self.textView deselected:selectedAttachment atPoint:locationInTextView]; 54 | } 55 | self.currentSelectedAttachment = nil; 56 | return NO; 57 | } 58 | return YES; 59 | } 60 | 61 | 62 | - (void)richTextViewWasPressed:(UIGestureRecognizer *)recognizer{ 63 | if (self.textView == nil || recognizer.state != UIGestureRecognizerStateRecognized) { 64 | return; 65 | } 66 | CGPoint locationInTextView = [recognizer locationInView:self.textView]; 67 | NSTextAttachment *attachment = [self.textView attachmentAtPoint:locationInTextView]; 68 | if (!attachment) { 69 | return; 70 | } 71 | 72 | if ([self.textView.attachmentDelegate respondsToSelector:@selector(textView:tapedAttachment:)]) { 73 | [self.textView.attachmentDelegate textView:self.textView tapedAttachment:attachment]; 74 | } 75 | self.currentSelectedAttachment = attachment; 76 | } 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/AttachmentLayoutManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentLayoutManager.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TextAttachmentsManager.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface AttachmentLayoutManager : NSLayoutManager 15 | 16 | @property (nonatomic,strong) TextAttachmentsManager *attachmentsManager; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/AttachmentLayoutManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentLayoutManager.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "AttachmentLayoutManager.h" 10 | 11 | @interface AttachmentLayoutManager () 12 | 13 | @end 14 | 15 | @implementation AttachmentLayoutManager 16 | 17 | - (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin{ 18 | [super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin]; 19 | if (!self.attachmentsManager) { 20 | return; 21 | } 22 | [self.textStorage enumerateAttribute:NSAttachmentAttributeName inRange:glyphsToShow options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSTextAttachment* attachment, NSRange range, BOOL * _Nonnull stop) { 23 | if (!attachment) { 24 | return ; 25 | } 26 | NSUInteger glyphsIndex = [self glyphIndexForCharacterAtIndex:range.location]; 27 | NSTextContainer *textContainer = [self textContainerForGlyphAtIndex:glyphsIndex effectiveRange:nil]; 28 | CGRect glyphsRect = [self boundingRectForGlyphRange:NSMakeRange(glyphsIndex, 1) inTextContainer:textContainer]; 29 | 30 | CGFloat x = origin.x + CGRectGetMinX(glyphsRect); 31 | CGFloat y = origin.y + CGRectGetMinY(glyphsRect); 32 | CGFloat width = CGRectGetWidth(glyphsRect); 33 | CGFloat height = CGRectGetHeight(glyphsRect); 34 | 35 | if ([attachment isKindOfClass:[AnyViewTextAttachment class]]) { 36 | AnyViewTextAttachment *anyViewAttachment = (AnyViewTextAttachment *)attachment; 37 | TextAttachmentReusableView *reusableView = [self.attachmentsManager reusableViewForAttachment:anyViewAttachment]; 38 | 39 | UIEdgeInsets contentInset = anyViewAttachment.contentInset; 40 | if (UIEdgeInsetsEqualToEdgeInsets(contentInset, UIEdgeInsetsZero)) { 41 | reusableView.frame = CGRectMake(x, y, width, height); 42 | }else{ 43 | reusableView.frame = CGRectMake(x + contentInset.left, y + contentInset.top, width - contentInset.left - contentInset.right, height - contentInset.bottom - contentInset.top); 44 | } 45 | }else{ 46 | /// fix 原生(其它类型)附件 和 AnyViewTextAttachment 混用, 重叠显示问题 47 | if (!(CGRectGetWidth(glyphsRect) > 0 && CGRectGetHeight(glyphsRect) > 0)) { 48 | return ; 49 | } 50 | CGRect frame = CGRectMake(x, y, width, height); 51 | [[self.attachmentsManager anyViewAttachmentsInTable] enumerateObjectsUsingBlock:^(AnyViewTextAttachment * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 52 | 53 | TextAttachmentReusableView *reusableView = [self.attachmentsManager reusableViewInTableFor:obj]; 54 | if (reusableView && CGRectIntersectsRect(reusableView.frame, frame)) { 55 | reusableView.frame = CGRectZero; 56 | } 57 | }]; 58 | } 59 | 60 | }]; 61 | } 62 | 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/TextAttachmentReusableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TextAttachmentReusableView.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface TextAttachmentReusableView : UIView 14 | 15 | @property (nonatomic,copy,readonly) NSString *identifier; 16 | 17 | - (instancetype)initAttachmentReusableView:(NSString *)identifier; 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/TextAttachmentReusableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // TextAttachmentReusableView.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "TextAttachmentReusableView.h" 10 | 11 | @interface TextAttachmentReusableView () 12 | 13 | @property (nonatomic,copy,readwrite) NSString *identifier; 14 | 15 | @end 16 | 17 | @implementation TextAttachmentReusableView 18 | 19 | - (instancetype)initAttachmentReusableView:(NSString *)identifier{ 20 | self = [super init]; 21 | if (self) { 22 | self.userInteractionEnabled = NO; 23 | _identifier = identifier; 24 | 25 | self.layer.borderColor = UIColor.redColor.CGColor; 26 | self.layer.borderWidth = 0.5f; 27 | } 28 | return self; 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/TextAttachmentsManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // AnyViewTextAttachmentsManager.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/19. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RichTextView.h" 11 | #import "AnyViewTextAttachment.h" 12 | #import "TextAttachmentReusableView.h" 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | @interface TextAttachmentsManager : NSObject 17 | 18 | @property (nonatomic,assign) CGFloat estimatedReusableViewHeight; 19 | 20 | @property (nonatomic,weak,readonly) RichTextView *textView; 21 | 22 | + (instancetype)attachmentsManagerFor:(RichTextView *)textView; 23 | 24 | /// ReusableView 25 | - (void)registerReusableView:(Class)viewCls reuseIdentifier:(NSString *)identifier; 26 | - (TextAttachmentReusableView *)dequeueReusableAttachmentView:(NSString *)identifier; 27 | - (TextAttachmentReusableView *)reusableViewForAttachment:(AnyViewTextAttachment *)attachment; 28 | 29 | /// InMapTable 30 | - (NSArray *)anyViewAttachmentsInTable; 31 | - (TextAttachmentReusableView *)reusableViewInTableFor:(AnyViewTextAttachment *)anyViewAttachment; 32 | - (void)removeAttachmentFromMapTable:(AnyViewTextAttachment *)anyViewAttment; 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/TextAttachmentsManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // AnyViewTextAttachmentsManager.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/19. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "TextAttachmentsManager.h" 10 | #import "NSLayoutManager+Attachment.h" 11 | 12 | @interface TextAttachmentsManager() 13 | 14 | @property (nonatomic,strong) NSMapTable *attachmentMapTable; 15 | 16 | @property (nonatomic,strong) NSMutableDictionary *registerViewHash; 17 | 18 | @property (nonatomic,strong) NSMutableArray *reusableAttachmentViews; 19 | 20 | @property (nonatomic,weak,readwrite) RichTextView *textView; 21 | 22 | @property (nonatomic,assign) CGFloat containerOffset; 23 | @end 24 | 25 | @implementation TextAttachmentsManager 26 | 27 | + (instancetype)attachmentsManagerFor:(RichTextView *)textView{ 28 | return [[TextAttachmentsManager alloc] initWithTextView:textView]; 29 | } 30 | 31 | - (instancetype)initWithTextView:(RichTextView *)textView 32 | { 33 | self = [super init]; 34 | if (self) { 35 | _attachmentMapTable = [NSMapTable mapTableWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableStrongMemory]; 36 | _registerViewHash = [NSMutableDictionary dictionaryWithCapacity:0]; 37 | _reusableAttachmentViews = [NSMutableArray arrayWithCapacity:0]; 38 | _textView = textView; 39 | if (_textView) { 40 | [_textView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil]; 41 | } 42 | 43 | _containerOffset = CGRectGetHeight(self.textView.frame)/2; 44 | if (_containerOffset == 0) { 45 | _containerOffset = [UIScreen mainScreen].bounds.size.height/2; 46 | } 47 | } 48 | return self; 49 | } 50 | 51 | - (void)dealloc{ 52 | NSLog(@"~~~~TextAttachmentsManager~~dealloc~~"); 53 | } 54 | 55 | - (void)setEstimatedReusableViewHeight:(CGFloat)estimatedReusableViewHeight{ 56 | _estimatedReusableViewHeight = estimatedReusableViewHeight; 57 | if (estimatedReusableViewHeight * 2.5 > _containerOffset) { 58 | _containerOffset = estimatedReusableViewHeight * 2.5; 59 | }else if (estimatedReusableViewHeight * 2.5 > 1.5*[UIScreen mainScreen].bounds.size.height){ 60 | _containerOffset = 1.5*[UIScreen mainScreen].bounds.size.height; 61 | } 62 | } 63 | 64 | #pragma mark - MapTable 65 | 66 | - (NSArray *)anyViewAttachmentsInTable{ 67 | return self.attachmentMapTable.keyEnumerator.allObjects; 68 | } 69 | 70 | - (TextAttachmentReusableView *)reusableViewInTableFor:(AnyViewTextAttachment *)anyViewAttachment{ 71 | return [self.attachmentMapTable objectForKey:anyViewAttachment]; 72 | } 73 | 74 | - (void)removeAttachmentFromMapTable:(AnyViewTextAttachment *)anyViewAttment{ 75 | [self.attachmentMapTable removeObjectForKey:anyViewAttment]; 76 | } 77 | 78 | #pragma mark - Observer 79 | 80 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ 81 | if ([keyPath isEqualToString:@"contentOffset"]) { 82 | NSValue *newvalue = change[NSKeyValueChangeNewKey]; 83 | CGPoint offset = [newvalue CGPointValue]; 84 | [self textViewContentOffsetChanged:offset]; 85 | return; 86 | } 87 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 88 | } 89 | 90 | - (void)textViewContentOffsetChanged:(CGPoint)offset{ 91 | if (!self.textView || [self anyViewAttachmentsInTable].count == 0) { 92 | return; 93 | } 94 | 95 | [[self anyViewAttachmentsInTable] enumerateObjectsUsingBlock:^(AnyViewTextAttachment * _Nonnull attachment, NSUInteger idx, BOOL * _Nonnull stop) { 96 | 97 | UIView *reusableView = [self reusableViewInTableFor:attachment]; 98 | 99 | if (reusableView) { 100 | /// 是否可见 101 | CGRect actualContainer = CGRectMake(offset.x, offset.y, CGRectGetWidth(self.textView.frame) , CGRectGetHeight(self.textView.frame)); 102 | /// 预加载范围 103 | CGFloat containerOffset = self.containerOffset; 104 | CGFloat containerMinY = CGRectGetMinY(actualContainer) - containerOffset > 0 ? CGRectGetMinY(actualContainer) - containerOffset : 0; 105 | CGRect preloadContainer = CGRectMake(CGRectGetMinX(actualContainer), containerMinY, CGRectGetWidth(actualContainer), CGRectGetHeight(actualContainer) + containerOffset); 106 | 107 | bool isIntersect = CGRectIntersectsRect(reusableView.frame, preloadContainer); 108 | if (!isIntersect) { 109 | [reusableView removeFromSuperview]; 110 | [self removeAttachmentFromMapTable:attachment]; 111 | [self.reusableAttachmentViews addObject:(TextAttachmentReusableView *)reusableView]; 112 | [self.textView.layoutManager setNeedsDisplay:attachment]; 113 | } 114 | } 115 | }]; 116 | } 117 | 118 | #pragma mark - reusable 119 | 120 | - (void)registerReusableView:(Class)viewCls reuseIdentifier:(NSString *)identifier{ 121 | NSAssert(([viewCls isSubclassOfClass:[TextAttachmentReusableView class]]), @"viewCls must is TextAttachmentReusableView"); 122 | [self.registerViewHash setObject:viewCls forKey:identifier]; 123 | } 124 | 125 | - (TextAttachmentReusableView *)dequeueReusableAttachmentView:(NSString *)identifier { 126 | TextAttachmentReusableView *reusableView = [self getReusableViewFor:identifier]; 127 | if (!reusableView && [self.registerViewHash.allKeys containsObject:identifier]) { 128 | reusableView = [[self.registerViewHash[identifier] alloc] initAttachmentReusableView:identifier]; 129 | } 130 | 131 | return reusableView; 132 | } 133 | 134 | - (TextAttachmentReusableView *)reusableViewForAttachment:(AnyViewTextAttachment *)attachment{ 135 | if (!self.textView) { 136 | return nil; 137 | } 138 | TextAttachmentReusableView *reusableView = [self reusableViewInTableFor:attachment]; 139 | if (reusableView) { 140 | return reusableView; 141 | } 142 | 143 | if ([self.textView.attachmentDelegate respondsToSelector:@selector(textView:viewForAttachment:)]) { 144 | reusableView = [self.textView.attachmentDelegate textView:self.textView viewForAttachment:attachment]; 145 | [self.textView addSubview:reusableView]; 146 | [self.attachmentMapTable setObject:reusableView forKey:attachment]; 147 | } 148 | 149 | return reusableView; 150 | } 151 | 152 | - (TextAttachmentReusableView *)getReusableViewFor:(NSString *)identifier{ 153 | TextAttachmentReusableView *reusableView = nil; 154 | for (TextAttachmentReusableView *view in self.reusableAttachmentViews) { 155 | if ([view.identifier isEqualToString:identifier]) { 156 | reusableView = view; 157 | break; 158 | } 159 | } 160 | if (reusableView) { 161 | [self.reusableAttachmentViews removeObject:reusableView]; 162 | } 163 | return reusableView; 164 | } 165 | 166 | @end 167 | -------------------------------------------------------------------------------- /Classes/TextAttachmentManager/TextViewAttachmentDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // TextViewAttachmentDelegate.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AnyViewTextAttachment.h" 11 | #import "TextAttachmentReusableView.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @protocol TextViewAttachmentDelegate 16 | 17 | - (TextAttachmentReusableView *)textView:(UITextView *)textView viewForAttachment:(AnyViewTextAttachment *)attachment; 18 | 19 | - (void)textView:(UITextView *)textView tapedAttachment:(NSTextAttachment *)attachment; 20 | 21 | - (void)textView:(UITextView *)textView deselected:(NSTextAttachment *)deselectedAttachment atPoint: (CGPoint)point; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /Classes/TextKit+/NSLayoutManager+Attachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutManager+Attachment.h 3 | // RichText 4 | // 5 | // Created by tbin on 2018/7/16. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSLayoutManager (Attachment) 12 | 13 | -(void)setNeedsLayout:(NSTextAttachment *)attachment; 14 | 15 | -(void)setNeedsDisplay:(NSTextAttachment *)attachment; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Classes/TextKit+/NSLayoutManager+Attachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutManager+Attachment.m 3 | // RichText 4 | // 5 | // Created by tbin on 2018/7/16. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import "NSLayoutManager+Attachment.h" 10 | 11 | @implementation NSLayoutManager (Attachment) 12 | 13 | - (NSArray *)rangesForAttachment:(NSTextAttachment *)attachment{ 14 | 15 | if (self.textStorage == nil) { 16 | return nil; 17 | } 18 | 19 | NSRange inRange = NSMakeRange(0, self.textStorage.length); 20 | 21 | NSMutableArray *rangs = (id)[NSMutableArray arrayWithCapacity:0]; 22 | 23 | [self.textStorage enumerateAttribute:NSAttachmentAttributeName inRange:inRange options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 24 | 25 | if (value != nil && [value isKindOfClass:[NSTextAttachment class]] && attachment == value) { 26 | NSValue *rangeValue = [NSValue valueWithRange:range]; 27 | [rangs addObject:rangeValue]; 28 | } 29 | 30 | }]; 31 | 32 | if (rangs.count == 0) { 33 | return nil; 34 | } 35 | 36 | return rangs; 37 | } 38 | 39 | 40 | /// Trigger a relayout for an attachment 41 | -(void)setNeedsLayout:(NSTextAttachment *)attachment{ 42 | 43 | NSArray *rangs = [self rangesForAttachment:attachment]; 44 | 45 | if (rangs == nil) { 46 | return; 47 | } 48 | 49 | for (NSValue *rangeValue in rangs) { 50 | NSRange range = [rangeValue rangeValue]; 51 | [self invalidateLayoutForCharacterRange:range actualCharacterRange:nil]; 52 | [self invalidateDisplayForCharacterRange:range]; 53 | } 54 | } 55 | 56 | /// Trigger a re-display for an attachment 57 | -(void)setNeedsDisplay:(NSTextAttachment *)attachment{ 58 | NSArray *rangs = [self rangesForAttachment:attachment]; 59 | 60 | if (rangs == nil) { 61 | return; 62 | } 63 | // invalidate the display for the corresponding ranges 64 | for (NSValue *rangeValue in rangs) { 65 | NSRange range = [rangeValue rangeValue]; 66 | [self invalidateDisplayForCharacterRange:range]; 67 | } 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /Classes/TextKit+/NSTextStorage+Html.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextStorage+Html.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/22. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSUInteger, TextStorageHtmlTagType) { 12 | Undefined_HtmlTag = 100, 13 | /// 段落 14 | P_HtmlTag, 15 | /// 加粗 16 | B_HtmlTag, 17 | /// 下划线 18 | U_HtmlTag, 19 | /// 斜体 20 | I_HtmlTag 21 | }; 22 | 23 | NS_ASSUME_NONNULL_BEGIN 24 | 25 | @interface NSTextStorage (Html) 26 | 27 | /** 28 | 解析html中支持的标签 29 | 30 | @param simpleHtml 仅解析支持的html标签 p、b、u、i 31 | @param typingAttributes typingAttributes description 32 | @return return value description 33 | */ 34 | + (NSMutableAttributedString *)attributedStringForSimpleHtml:(NSString *)simpleHtml typingAttributes:(NSDictionary *)typingAttributes; 35 | 36 | /** 37 | 转换为简单html标签,附件使用占位符代替 38 | 39 | @param handleAttachment handleAttachment description 40 | @return return value description 41 | */ 42 | - (NSString *)draftHtmlForAttributedString:(void(^)(NSTextAttachment *attachment))handleAttachment; 43 | 44 | 45 | - (NSString *)convertToHtml; 46 | 47 | /// html标签映射富文本字符串 48 | + (NSDictionary *)mapingHtmlTagAttributedString:(NSString *)matchText; 49 | 50 | /// html标签 枚举字符串 51 | + (NSString *)enumHtmlTagString:(TextStorageHtmlTagType)tag; 52 | 53 | + (void)searchHtmlTag:(NSString *)tag inString:(NSString *)string complate:(void(^)(NSTextCheckingResult *result))complate; 54 | @end 55 | 56 | NS_ASSUME_NONNULL_END 57 | -------------------------------------------------------------------------------- /Classes/TextKit+/NSTextStorage+Html.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextStorage+Html.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/22. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "NSTextStorage+Html.h" 10 | #import "MediaTextAttachment.h" 11 | #import "ImageTextAttachment.h" 12 | #import "VideoTextAttachment.h" 13 | #import "RichTextTools.h" 14 | #import "NSTextStorage+Utils.h" 15 | 16 | @implementation NSTextStorage (Html) 17 | 18 | #pragma mark - Simple Html To NSAttributedString 19 | 20 | + (NSMutableAttributedString *)attributedStringForSimpleHtml:(NSString *)simpleHtml typingAttributes:(NSDictionary *)typingAttributes{ 21 | 22 | NSMutableAttributedString *htmlAttrString = [[NSMutableAttributedString alloc] initWithString:simpleHtml]; 23 | [typingAttributes enumerateKeysAndObjectsUsingBlock:^(NSAttributedStringKey _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 24 | [htmlAttrString addAttribute:key value:obj range:NSMakeRange(0, htmlAttrString.length)]; 25 | }]; 26 | 27 | NSArray *supportHtmlTags = @[@(P_HtmlTag),@(B_HtmlTag),@(U_HtmlTag),@(I_HtmlTag)]; 28 | 29 | for (NSNumber *tag in supportHtmlTags) { 30 | TextStorageHtmlTagType htmlTag = [tag integerValue]; 31 | 32 | [self searchHtmlTag:[self enumHtmlTagString:htmlTag] inString:htmlAttrString.string complate:^(NSTextCheckingResult *result) { 33 | NSString *htmlTagStr = [self enumHtmlTagString:htmlTag]; 34 | NSString *openTag = [NSString stringWithFormat:@"<%@>",htmlTagStr]; 35 | NSString *closeTag = [NSString stringWithFormat:@"",htmlTagStr]; 36 | 37 | NSString *matchText = [htmlAttrString.string substringWithRange:result.range]; 38 | matchText = [matchText stringByReplacingOccurrencesOfString:openTag withString:@""]; 39 | 40 | if (htmlTag == P_HtmlTag) { 41 | matchText = [matchText stringByReplacingOccurrencesOfString:closeTag withString:@""]; 42 | [htmlAttrString replaceCharactersInRange:result.range withString:matchText]; 43 | 44 | }else{ 45 | matchText = [matchText stringByReplacingOccurrencesOfString:closeTag withString:@""]; 46 | NSMutableAttributedString *attr = [self mapingHtmlTagAttributedString:matchText][htmlTagStr]; 47 | 48 | [typingAttributes enumerateKeysAndObjectsUsingBlock:^(NSAttributedStringKey _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 49 | [attr addAttribute:key value:obj range:NSMakeRange(0, matchText.length)]; 50 | 51 | /// 加粗 更换字体 52 | if (key == NSFontAttributeName && htmlTag == B_HtmlTag) { 53 | UIFont *font = typingAttributes[NSFontAttributeName]; 54 | [attr addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:font.pointSize] range:NSMakeRange(0, matchText.length)]; 55 | } 56 | }]; 57 | [htmlAttrString replaceCharactersInRange:result.range withAttributedString:attr]; 58 | } 59 | }]; 60 | } 61 | 62 | return htmlAttrString; 63 | } 64 | 65 | #pragma mark - NSAttributedString To Simple Html 66 | 67 | - (NSString *)draftHtmlForAttributedString:(void(^)(NSTextAttachment *attachment))handleAttachment{ 68 | NSMutableAttributedString *attributedString = [self mutableCopy]; 69 | [attributedString enumerateAttributesInRange:NSMakeRange(0, attributedString.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { 70 | 71 | if ([attrs.allKeys containsObject:NSAttachmentAttributeName]) { 72 | NSTextAttachment *attachment = attrs[NSAttachmentAttributeName]; 73 | if ([attachment isKindOfClass:[MediaTextAttachment class]]) { 74 | if (handleAttachment) { 75 | handleAttachment(attachment); 76 | } 77 | [attributedString replaceCharactersInRange:range withString: [NSString stringWithFormat:@"

%@

",kAttachmentDraftPlaceholder]]; 78 | } 79 | } 80 | /// 转换 b、u、i 标签 81 | NSString *htmlTagContent = [NSTextStorage htmlTagContentInAttributedString:attributedString attrs:attrs range:range]; 82 | if (htmlTagContent) { 83 | [attributedString replaceCharactersInRange:range withString:htmlTagContent]; 84 | } 85 | }]; 86 | 87 | NSString *effectiveText = [NSString stringWithFormat:@"

%@

",attributedString.string]; 88 | /// 过滤换行 89 | effectiveText = [effectiveText stringByReplacingOccurrencesOfString:@"\n" withString:@""]; 90 | effectiveText = [effectiveText stringByReplacingOccurrencesOfString:@"

" withString:@""]; 91 | return effectiveText; 92 | } 93 | 94 | + (NSString *)htmlTagContentInAttributedString:(NSAttributedString *)attributedString attrs:(NSDictionary * _Nonnull)attrs range:(NSRange)range{ 95 | if (attributedString.length == 0 || attrs.count == 0 || range.location == NSNotFound) { 96 | return nil; 97 | } 98 | 99 | /// 下划线 100 | if([attrs.allKeys containsObject:NSUnderlineStyleAttributeName]){ 101 | NSString *uTag = [NSTextStorage enumHtmlTagString:U_HtmlTag]; 102 | NSString *content = [attributedString.string substringWithRange:range]; 103 | NSString *uHtml = [NSString stringWithFormat:@"<%@>%@",uTag,content,uTag]; 104 | return uHtml; 105 | } 106 | /// 加粗 107 | if([attrs.allKeys containsObject:NSFontAttributeName]){ 108 | UIFont *font = attrs[NSFontAttributeName]; 109 | if ([font.fontName rangeOfString:@"bold"].location != NSNotFound) { 110 | NSString *bTag = [NSTextStorage enumHtmlTagString:B_HtmlTag]; 111 | NSString *content = [attributedString.string substringWithRange:range]; 112 | NSString *bHtml = [NSString stringWithFormat:@"<%@>%@",bTag,content,bTag]; 113 | return bHtml; 114 | } 115 | } 116 | /// 斜体 117 | if([attrs.allKeys containsObject:NSObliquenessAttributeName]){ 118 | NSString *iTag = [NSTextStorage enumHtmlTagString:I_HtmlTag]; 119 | NSString *content = [attributedString.string substringWithRange:range]; 120 | NSString *iHtml = [NSString stringWithFormat:@"<%@>%@",iTag,content,iTag]; 121 | return iHtml; 122 | } 123 | return nil; 124 | } 125 | 126 | - (NSString *)convertToHtml{ 127 | NSMutableAttributedString *attributedString = [self mutableCopy]; 128 | [attributedString enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 129 | if (value == nil) { 130 | return ; 131 | } 132 | NSLog(@"range:---%@",NSStringFromRange(range)); 133 | 134 | if ([value isKindOfClass:[ImageTextAttachment class]]) { 135 | ImageTextAttachment *imgAttachment = (ImageTextAttachment *)value; 136 | 137 | NSString *imgTagStr = [NSString stringWithFormat:@"

",imgAttachment.imageURL.absoluteString,imgAttachment.orgImgSize.width,imgAttachment.orgImgSize.height]; 138 | 139 | [attributedString replaceCharactersInRange:range withString:imgTagStr]; 140 | 141 | }else if ([value isKindOfClass:[VideoTextAttachment class]]){ 142 | VideoTextAttachment *videoAttachment = (VideoTextAttachment *)value; 143 | 144 | NSString *videoTagStr = [NSString stringWithFormat:@"

",videoAttachment.videoURL.absoluteString,videoAttachment.posterURL.absoluteString,videoAttachment.coverSize.width,videoAttachment.coverSize.height]; 145 | 146 | [attributedString replaceCharactersInRange:range withString:videoTagStr]; 147 | } 148 | }]; 149 | 150 | NSString *effectiveText = [NSString stringWithFormat:@"

%@

",attributedString.string]; 151 | /// 过滤换行 152 | effectiveText = [effectiveText stringByReplacingOccurrencesOfString:@"\n" withString:@""]; 153 | effectiveText = [effectiveText stringByReplacingOccurrencesOfString:@"

" withString:@""]; 154 | 155 | return effectiveText; 156 | } 157 | 158 | #pragma mark - Simple Html Tag & NSMutableAttributedString 159 | 160 | + (void)searchHtmlTag:(NSString *)tag inString:(NSString *)string complate:(void(^)(NSTextCheckingResult *result))complate{ 161 | if (!(tag.length > 0)) { 162 | return; 163 | } 164 | NSRegularExpression *kTopicExp = [NSRegularExpression regularExpressionWithPattern:kHtmlExpression(tag) options:NSRegularExpressionCaseInsensitive error:nil]; 165 | [kTopicExp enumerateMatchesInString:string options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, string.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { 166 | if (!result) return; 167 | if (complate) { 168 | complate(result); 169 | } 170 | }]; 171 | } 172 | 173 | + (NSDictionary *)mapingHtmlTagAttributedString:(NSString *)matchText { 174 | NSMutableAttributedString *bAttr = [[NSMutableAttributedString alloc] initWithString:matchText]; 175 | [bAttr addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:[UIFont systemFontSize]] range:NSMakeRange(0, matchText.length)]; 176 | 177 | NSMutableAttributedString* uAttr = [[NSMutableAttributedString alloc] initWithString:matchText]; 178 | [uAttr addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:NSMakeRange(0, matchText.length)]; 179 | [uAttr addAttribute:NSUnderlineColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, matchText.length)]; 180 | 181 | NSMutableAttributedString *iAttr = [[NSMutableAttributedString alloc] initWithString:matchText]; 182 | [iAttr addAttribute:NSObliquenessAttributeName value:@(0.5) range:NSMakeRange(0, matchText.length)]; 183 | 184 | return @{ 185 | [self enumHtmlTagString:B_HtmlTag]:bAttr, 186 | [self enumHtmlTagString:U_HtmlTag]:uAttr, 187 | [self enumHtmlTagString:I_HtmlTag]:iAttr 188 | }; 189 | } 190 | 191 | + (NSString *)enumHtmlTagString:(TextStorageHtmlTagType)tag{ 192 | switch (tag) { 193 | case P_HtmlTag: 194 | return @"p"; 195 | break; 196 | case B_HtmlTag: 197 | return @"b"; 198 | break; 199 | case U_HtmlTag: 200 | return @"u"; 201 | break; 202 | case I_HtmlTag: 203 | return @"i"; 204 | break; 205 | 206 | default: 207 | return @""; 208 | break; 209 | } 210 | } 211 | @end 212 | -------------------------------------------------------------------------------- /Classes/TextKit+/NSTextStorage+ProcessEditing.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextStorage+ProcessEditing.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/22. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @protocol NSTextStorageHookDelegate 14 | 15 | @optional 16 | - (void)textStorageHook_processEditing:(NSTextStorage *_Nullable)textStorage; 17 | 18 | @end 19 | 20 | @interface NSTextStorage (ProcessEditing) 21 | 22 | @property (nullable, weak, NS_NONATOMIC_IOSONLY) id hookDelegate; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /Classes/TextKit+/NSTextStorage+ProcessEditing.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextStorage+ProcessEditing.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/22. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "NSTextStorage+ProcessEditing.h" 10 | #import 11 | 12 | static char const * const kHookDelegate = "kHookDelegate"; 13 | 14 | @implementation NSTextStorage (ProcessEditing) 15 | 16 | - (void)hook_processEditing{ 17 | [self hook_processEditing]; 18 | 19 | if (self.hookDelegate) { 20 | [self.hookDelegate textStorageHook_processEditing:self]; 21 | } 22 | } 23 | 24 | #pragma mark - Hook 25 | 26 | + (void)textStorage_swizzleInstanceSelector:(SEL)originalSelector 27 | withNewSelector:(SEL)newSelector 28 | { 29 | Method originalMethod = class_getInstanceMethod(self, originalSelector); 30 | Method newMethod = class_getInstanceMethod(self, newSelector); 31 | 32 | BOOL methodAdded = class_addMethod([self class], 33 | originalSelector, 34 | method_getImplementation(newMethod), 35 | method_getTypeEncoding(newMethod)); 36 | 37 | if (methodAdded) { 38 | class_replaceMethod([self class], 39 | newSelector, 40 | method_getImplementation(originalMethod), 41 | method_getTypeEncoding(originalMethod)); 42 | } else { 43 | method_exchangeImplementations(originalMethod, newMethod); 44 | } 45 | } 46 | 47 | 48 | + (void)load{ 49 | [self textStorage_swizzleInstanceSelector:@selector(processEditing) 50 | withNewSelector:@selector(hook_processEditing)]; 51 | } 52 | 53 | #pragma mark - Getter/Setter 54 | 55 | - (void)setHookDelegate:(id)hookDelegate 56 | { 57 | id __weak weakObject = hookDelegate; 58 | id (^block)() = ^{ return weakObject; }; 59 | objc_setAssociatedObject(self, &kHookDelegate, 60 | block, OBJC_ASSOCIATION_COPY); 61 | } 62 | 63 | - (id)hookDelegate{ 64 | id (^block)() = objc_getAssociatedObject(self, &kHookDelegate); 65 | return (block ? block() : nil); 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /Classes/TextKit+/NSTextStorage+Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextStorage+Utils.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/27. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface NSTextStorage (Utils) 14 | 15 | + (NSMutableAttributedString *)attributedStringForAttachment:(NSTextAttachment *)attachment typingAttributes:(NSDictionary *)typingAttributes; 16 | 17 | 18 | - (void)removeAttributeFor:(NSArray *)keys range:(NSRange)range typingAttributes:(NSDictionary *)typingAttributes; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /Classes/TextKit+/NSTextStorage+Utils.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextStorage+Utils.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/27. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "NSTextStorage+Utils.h" 10 | 11 | @implementation NSTextStorage (Utils) 12 | 13 | + (NSMutableAttributedString *)attributedStringForAttachment:(NSTextAttachment *)attachment typingAttributes:(NSDictionary *)typingAttributes{ 14 | NSAttributedString * attachmentString = [NSAttributedString attributedStringWithAttachment:attachment]; 15 | NSMutableAttributedString* separatorString = [[NSMutableAttributedString alloc] initWithString:@"\n"]; 16 | NSMutableAttributedString* insertion = [[NSMutableAttributedString alloc] init]; 17 | 18 | [typingAttributes enumerateKeysAndObjectsUsingBlock:^(NSAttributedStringKey _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 19 | [separatorString addAttribute:key value:obj range:NSMakeRange(0, separatorString.length)]; 20 | [insertion addAttribute:key value:obj range:NSMakeRange(0, insertion.length)]; 21 | }]; 22 | 23 | [insertion appendAttributedString:separatorString]; 24 | [insertion appendAttributedString:attachmentString]; 25 | [insertion appendAttributedString:separatorString]; 26 | return insertion; 27 | } 28 | 29 | 30 | #pragma mark - toggle Style 31 | 32 | - (void)removeAttributeFor:(NSArray *)keys range:(NSRange)range typingAttributes:(NSDictionary *)typingAttributes{ 33 | if (range.location == NSNotFound || range.location + range.length > self.length) { 34 | return; 35 | } 36 | [self beginEditing]; 37 | 38 | for (NSAttributedStringKey key in keys) { 39 | [self removeAttribute:key range:range]; 40 | if ([key isEqualToString:NSFontAttributeName]) { 41 | if ([typingAttributes.allKeys containsObject:NSFontAttributeName] ) { 42 | [self addAttribute:NSFontAttributeName value:typingAttributes[NSFontAttributeName] range:range]; 43 | }else{ 44 | [self addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:[UIFont systemFontSize]] range:range]; 45 | } 46 | } 47 | } 48 | 49 | [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; 50 | [self endEditing]; 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /Classes/UITextView+/UITextView+Binding.h: -------------------------------------------------------------------------------- 1 | // 2 | // UITextView+Binding.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | static NSString * _Nullable const NSAttributedStringBindingKey = @"Tag_Custom"; 12 | static NSString * _Nullable const NSAttributedStringHighlightingKey = @"Highlighting_Custom"; 13 | /// 需要追加一个空格 14 | #define AttributedStringBindingText(text) ([NSString stringWithFormat:@"%@ ", text]) 15 | 16 | NS_ASSUME_NONNULL_BEGIN 17 | 18 | @interface UITextView (Binding) 19 | 20 | /** 21 | 对指定range内容绑定,和高亮 22 | 23 | NSString *bindText = AttributedStringBindingText(newTopicName); 24 | [textView insertText:bindText]; 25 | ///根据当前光标位置判断位置 带空额 绑定文本后面带有空格 26 | NSRange tnRange = NSMakeRange(textView.selectedRange.location - bindText.length, bindText.length - 1); 27 | [textView addBindingAttributeInRange:tnRange highlightingColor:color]; 28 | 29 | @param range range description 30 | @param color color description 31 | */ 32 | - (void)addBindingAttributeInRange:(NSRange)range highlightingColor:(UIColor *)color; 33 | 34 | /** 35 | 指定NSAttributedStringKey 中不允许光标插入。只能选中整体。 36 | - (void)textViewDidChangeSelection:(UITextView *)textView { 37 | [textView bindSelectRange:NSAttributedStringBindingKey textViewDelegate:self]; 38 | } 39 | 40 | @param attrKey 指定key 如 NSAttributedStringBindingKey 41 | @param textViewDelegate textViewDelegate description 42 | */ 43 | - (void)bindSelectRange:(NSAttributedStringKey )attrKey textViewDelegate:(id)textViewDelegate; 44 | 45 | /** 46 | 是否绑定删除 指定的NSAttributedStringKey 47 | - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{ 48 | if ( [textView bindDelete:NSAttributedStringBindingKey inRange:range] == NO ) { 49 | if ([textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { 50 | [textView.delegate textViewDidChange:textView]; 51 | } 52 | return NO; 53 | } 54 | return YES; 55 | } 56 | 中调用 57 | 58 | @param attrKey 如:NSAttributedStringBindingKey 59 | @param inRange inRange description 60 | @return return 是否命中绑定Key 61 | */ 62 | - (BOOL)bindDelete:(NSAttributedStringKey )attrKey inRange:(NSRange)inRange; 63 | 64 | @end 65 | 66 | NS_ASSUME_NONNULL_END 67 | -------------------------------------------------------------------------------- /Classes/UITextView+/UITextView+Binding.m: -------------------------------------------------------------------------------- 1 | // 2 | // UITextView+Binding.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "UITextView+Binding.h" 10 | 11 | @implementation UITextView (Binding) 12 | 13 | #pragma mark - Binding 14 | 15 | - (void)addBindingAttributeInRange:(NSRange)range highlightingColor:(UIColor *)color{ 16 | [self.textStorage addAttribute:NSAttributedStringHighlightingKey value:@"You_Want_Somthing" range:range]; 17 | [self.textStorage addAttribute:NSAttributedStringBindingKey value:@"You_Want_Somthing" range:range]; 18 | [self.textStorage addAttribute:NSForegroundColorAttributeName value:color range:range]; 19 | } 20 | 21 | - (void)bindSelectRange:(NSAttributedStringKey )attrKey textViewDelegate:(id)textViewDelegate{ 22 | UITextRange * selectedTextRange = self.selectedTextRange; 23 | if (selectedTextRange == nil){ 24 | return; 25 | } 26 | 27 | [self.textStorage enumerateAttribute:attrKey inRange:NSMakeRange(0, self.textStorage.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 28 | if (value == nil) { 29 | return ; 30 | } 31 | 32 | UITextPosition *start = [self positionFromPosition:self.beginningOfDocument offset:range.location]; 33 | if (start == nil){ 34 | return; 35 | } 36 | // UITextPosition *end = [self positionFromPosition:start offset:range.length]; 37 | /// 绑定内容后追加的空格长度 38 | UITextPosition *end = [self positionFromPosition:start offset:range.length + 1]; 39 | if (end == nil){ 40 | return; 41 | } 42 | 43 | UITextPosition *harf = [self positionFromPosition:start offset:range.length/2]; 44 | if (harf == nil){ 45 | return; 46 | } 47 | 48 | /// 处理选中 49 | if ( [self offsetFromPosition:selectedTextRange.start toPosition:selectedTextRange.end] > 0){ 50 | UITextPosition * shouldEnd = selectedTextRange.end; 51 | UITextPosition * shouldStart = selectedTextRange.start; 52 | 53 | /// 结束点在当前范围内 >= start && < end 54 | if( ([self comparePosition:selectedTextRange.end toPosition:start] == NSOrderedDescending || [self comparePosition:selectedTextRange.end toPosition:start] == NSOrderedSame) 55 | && [self comparePosition:selectedTextRange.end toPosition:end] == NSOrderedAscending) { 56 | shouldEnd = end; 57 | } 58 | 59 | /// 开始点在当前范围内 > start && <= end 60 | if ( [self comparePosition:selectedTextRange.start toPosition:start] == NSOrderedDescending 61 | && ( [self comparePosition:selectedTextRange.start toPosition:end] == NSOrderedAscending || [self comparePosition:selectedTextRange.start toPosition:end] == NSOrderedSame)){ 62 | shouldStart = start; 63 | } 64 | 65 | if ( [self comparePosition:selectedTextRange.start toPosition:shouldStart] != NSOrderedSame || 66 | [self comparePosition:selectedTextRange.end toPosition:shouldEnd] != NSOrderedSame ){ 67 | self.delegate = nil; /// 打破循环调用 mmp 68 | self.selectedTextRange = [self textRangeFromPosition:shouldStart toPosition:shouldEnd]; 69 | self.delegate = textViewDelegate; 70 | } 71 | return; 72 | } 73 | 74 | /// 是否在当前区域 75 | BOOL isArea = ([self comparePosition:selectedTextRange.start toPosition:start] == NSOrderedDescending 76 | || [self comparePosition:selectedTextRange.start toPosition:start] == NSOrderedSame) 77 | && ( [self comparePosition:selectedTextRange.start toPosition:end] == NSOrderedAscending 78 | || [self comparePosition:selectedTextRange.start toPosition:end] == NSOrderedSame ); 79 | 80 | if (isArea){ 81 | /// 重置光标 82 | if ([self comparePosition:selectedTextRange.start toPosition:harf] == NSOrderedAscending || 83 | [self comparePosition:selectedTextRange.start toPosition:harf] == NSOrderedSame ) { 84 | /// 位于左边 85 | self.delegate = nil; /// 打破循环调用 mmp 86 | self.selectedTextRange = [self textRangeFromPosition:start toPosition:start]; 87 | self.delegate = textViewDelegate; 88 | }else { 89 | /// 位于右边 90 | self.delegate = nil; 91 | self.selectedTextRange = [self textRangeFromPosition:end toPosition:end]; 92 | self.delegate = textViewDelegate; 93 | } 94 | } 95 | 96 | }]; 97 | } 98 | 99 | /// 绑定删除 对指定的key 100 | - (BOOL)bindDelete:(NSAttributedStringKey )attrKey inRange:(NSRange)inRange{ 101 | 102 | if (inRange.location > self.textStorage.length ){ 103 | return YES; 104 | } 105 | __block BOOL shouldReplace = false; 106 | __block NSRange currentReplacementRange = inRange; 107 | 108 | [self.textStorage enumerateAttribute:attrKey inRange:NSMakeRange(0, self.textStorage.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 109 | if (value == nil){ 110 | return ; 111 | } 112 | /// 处理边界情况 113 | /// inRange.location == (attrRange.location + attrRange.length) 114 | /// #a# #b# ---> #a##b# 115 | /// 删除中间空格, 会被识别成一个整体(相同的富文本key),判断光标位置等于 #a#结束位置就要绑定删除了。 116 | /// 117 | BOOL isReturn = inRange.location > range.location && ( NSLocationInRange(inRange.location, range) == YES || inRange.location == (range.location + range.length) ); 118 | // BOOL isReturn = inRange.location > range.location && ( NSLocationInRange(inRange.location, range) == YES); 119 | if (!isReturn){ 120 | return; 121 | } 122 | 123 | currentReplacementRange = range; 124 | shouldReplace = YES; 125 | }]; 126 | 127 | if (shouldReplace) { 128 | 129 | [self.textStorage removeAttribute:attrKey range:currentReplacementRange]; 130 | // [self.textStorage replaceCharactersInRange:currentReplacementRange withString:@""]; 131 | ///删除内容后的空格 132 | [self.textStorage replaceCharactersInRange:NSMakeRange(currentReplacementRange.location, currentReplacementRange.length + 1) withString:@""]; 133 | 134 | // set the cursor position to the end of the edited location 135 | UITextPosition *cursorPosition = [self positionFromPosition:self.beginningOfDocument offset:currentReplacementRange.location]; 136 | if (cursorPosition != nil){ 137 | self.selectedTextRange = [self textRangeFromPosition:cursorPosition toPosition:cursorPosition]; 138 | } 139 | 140 | return NO; 141 | } 142 | 143 | return YES; 144 | } 145 | 146 | @end 147 | -------------------------------------------------------------------------------- /Classes/UITextView+/UITextView+RichText.h: -------------------------------------------------------------------------------- 1 | // 2 | // UITextView+RichText.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/23. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface UITextView (RichText) 14 | 15 | /// Style 16 | - (void)removeBold; 17 | 18 | - (void)removeUnderline; 19 | 20 | - (void)removeObliqueness; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /Classes/UITextView+/UITextView+RichText.m: -------------------------------------------------------------------------------- 1 | // 2 | // UITextView+RichText.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/23. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "UITextView+RichText.h" 10 | #import "NSTextStorage+Utils.h" 11 | 12 | @implementation UITextView (RichText) 13 | 14 | #pragma mark - Style 15 | 16 | - (void)removeBold{ 17 | [self.textStorage removeAttributeFor:@[NSFontAttributeName] range:self.selectedRange typingAttributes:self.typingAttributes]; 18 | } 19 | 20 | - (void)removeUnderline{ 21 | [self.textStorage removeAttributeFor:@[NSUnderlineStyleAttributeName,NSUnderlineColorAttributeName] range:self.selectedRange typingAttributes:self.typingAttributes]; 22 | } 23 | 24 | - (void)removeObliqueness{ 25 | [self.textStorage removeAttributeFor:@[NSObliquenessAttributeName] range:self.selectedRange typingAttributes:self.typingAttributes]; 26 | } 27 | 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Classes/UITextView+/UITextView+TextAttachmentUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // UITextView+TextAttachmentUtils.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface UITextView (TextAttachmentUtils) 14 | 15 | - (NSArray *)allAttachments; 16 | 17 | - (NSArray *)viewAttachments:(NSRange)inRange; 18 | 19 | - (NSTextAttachment *)attachmentAtPoint:(CGPoint)point; 20 | 21 | /// 插入附件,前后各有换行 22 | - (void)insertAttachment:(NSTextAttachment *)attachment paragraphStyle:(NSMutableParagraphStyle*)style; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /Classes/UITextView+/UITextView+TextAttachmentUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // UITextView+TextAttachmentUtils.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/20. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "UITextView+TextAttachmentUtils.h" 10 | #import "NSTextStorage+Utils.h" 11 | 12 | @implementation UITextView (TextAttachmentUtils) 13 | 14 | #pragma mark - viewAttachments 15 | 16 | - (NSArray *)allAttachments{ 17 | NSMutableArray *attachments = (id)[NSMutableArray arrayWithCapacity:0]; 18 | [self.textStorage enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, self.textStorage.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 19 | if (value != nil) { 20 | [attachments addObject:value]; 21 | } 22 | }]; 23 | return attachments; 24 | } 25 | 26 | - (NSArray *)viewAttachments:(NSRange)inRange { 27 | 28 | NSMutableArray *atts = [NSMutableArray arrayWithCapacity:0]; 29 | 30 | [self.textStorage enumerateAttribute:NSAttachmentAttributeName inRange:inRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 31 | 32 | if (value != nil && [value isKindOfClass:[NSTextAttachment class]]) { 33 | [atts addObject:value]; 34 | } 35 | 36 | }]; 37 | 38 | return atts; 39 | } 40 | 41 | - (NSTextAttachment *)attachmentAtPoint:(CGPoint)point{ 42 | NSUInteger index = [self.layoutManager characterIndexForPoint:point inTextContainer:self.textContainer fractionOfDistanceBetweenInsertionPoints:nil]; 43 | 44 | if (!(index < self.textStorage.length)) { 45 | return nil; 46 | } 47 | 48 | NSRange effectiveRange; 49 | NSTextAttachment *attachment = [self.textStorage attribute:NSAttachmentAttributeName atIndex:index effectiveRange:&effectiveRange]; 50 | if (!attachment) { 51 | return nil; 52 | } 53 | 54 | CGRect bounds = [self.layoutManager boundingRectForGlyphRange:effectiveRange inTextContainer:self.textContainer]; 55 | bounds.origin.x += self.textContainerInset.left; 56 | bounds.origin.y += self.textContainerInset.top; 57 | 58 | return CGRectContainsPoint(bounds, point) ? attachment : nil; 59 | } 60 | 61 | 62 | - (void)insertAttachment:(NSTextAttachment *)attachment paragraphStyle:(NSMutableParagraphStyle*)style{ 63 | NSAttributedString* insertion = [NSTextStorage attributedStringForAttachment:attachment typingAttributes:self.typingAttributes]; 64 | 65 | [self.textStorage insertAttributedString:insertion atIndex:self.selectedRange.location]; 66 | 67 | /// 重置光标 68 | UITextPosition *end = [self positionFromPosition:self.selectedTextRange.start offset:3]; 69 | self.selectedTextRange = [self textRangeFromPosition:end toPosition:end]; 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /Classes/Utils/MediaCompressTool.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaCompressTool.h 3 | // RichText 4 | // 5 | // Created by tbin on 2018/7/17. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "UIImage+Utils.h" 12 | 13 | typedef NS_ENUM(NSUInteger,XFCheckVideoResult){ 14 | XFCheckVideoUndefined = 0, 15 | /// 格式错误 16 | XFVideoUnavailableFormat , 17 | /// 视频太大 18 | XFVideoSizeTooBig, 19 | /// 视频时长太短 20 | XFVideoDurationTooShort, 21 | }; 22 | 23 | // 500M 24 | static NSUInteger videoMaxSize = 500; 25 | // 5s 26 | static NSUInteger videoMinDuration = 5; 27 | 28 | @interface MediaCompressTool : NSObject 29 | 30 | /// 压缩图片到 1M 31 | + (NSData *_Nullable)compressImage:(NSData *_Nonnull)imgData; 32 | 33 | /// 压缩视频进度 34 | + (void)compressVideo:(PHAsset *_Nonnull)asset 35 | completion:(void(^_Nullable)(NSURL * _Nullable videoDiskPath))completion 36 | progress:(void(^_Nullable)(AVAssetExportSession * _Nullable exportSession))progressCallback; 37 | 38 | /// 检查视频格式、大小、时长 39 | + (void)checkVideo:(PHAsset *_Nullable)asset 40 | unavailable:(void(^_Nullable)(NSString * _Nullable errormsg,NSString * _Nullable info,XFCheckVideoResult type))unavailable 41 | available:(void(^_Nullable)(float duration,long long dataLength,NSString * _Nullable format))available; 42 | 43 | /// 合成 视频附件 封图(带播放icon) 44 | + (void)handleVideoAttachment:(UIImage *_Nullable)orgImg 45 | resize:(CGSize)resize 46 | complate:(void(^_Nullable)(UIImage * _Nullable resizeImg,NSURL * _Nullable imgPath))complate; 47 | 48 | /// 图片附件 显示的图片 49 | + (void)handleImgAttachment:(UIImage *_Nullable)orgImg 50 | resize:(CGSize)resize 51 | complate:(void(^_Nullable)(UIImage * _Nullable resizeImg,NSURL* _Nullable resizeImgDiskPath))complate; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /Classes/Utils/MediaCompressTool.m: -------------------------------------------------------------------------------- 1 | // 2 | // XFMediaCompressTool.m 3 | // RichText 4 | // 5 | // Created by tbin on 2018/7/17. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import "MediaCompressTool.h" 10 | #import "UIImage+Utils.h" 11 | 12 | /// 图片压缩目标 1M 13 | #define IMG_TARGETSIZE (1 * 1024 * 1024) 14 | 15 | @implementation MediaCompressTool 16 | 17 | + (NSData *)compressImage:(NSData *)imgData{ 18 | /// 超过1M 压缩 19 | if (imgData.length <= IMG_TARGETSIZE || imgData == nil) { 20 | return imgData; 21 | } 22 | 23 | UIImage *img = [UIImage imageWithData:imgData]; 24 | return [UIImage compressImage:img toSize:IMG_TARGETSIZE]; 25 | } 26 | 27 | + (void)compressVideo:(PHAsset *)asset completion:(void(^)(NSURL *videoDiskPath))completion progress:(void(^)(AVAssetExportSession * exportSession))progressCallback { 28 | NSURL *documentUrl = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]; 29 | NSTimeInterval timeInterval = [NSDate date].timeIntervalSince1970 * 1000; 30 | NSURL *tempFileURL = [NSURL URLWithString:[NSString stringWithFormat:@"video_%.f.mp4",timeInterval] relativeToURL:documentUrl]; 31 | 32 | PHVideoRequestOptions * options = [[PHVideoRequestOptions alloc] init]; 33 | [options setNetworkAccessAllowed:YES]; 34 | [PHImageManager.defaultManager requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset * _Nullable avAsset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) { 35 | 36 | NSArray *compatiblePresets = [AVAssetExportSession exportPresetsCompatibleWithAsset:avAsset]; 37 | 38 | if (![compatiblePresets containsObject:AVAssetExportPresetMediumQuality]) { 39 | completion(nil); 40 | return; 41 | } 42 | 43 | AVAssetExportSession * exportSession = [AVAssetExportSession exportSessionWithAsset:avAsset presetName:AVAssetExportPresetMediumQuality]; 44 | if (exportSession == nil) { 45 | completion(nil); 46 | return; 47 | } 48 | 49 | // 视频转码压缩后的本地保存地址 50 | exportSession.outputURL = tempFileURL; 51 | // 优化网络 52 | exportSession.shouldOptimizeForNetworkUse = YES; 53 | exportSession.outputFileType = AVFileTypeMPEG4; 54 | 55 | if (progressCallback) { 56 | progressCallback(exportSession); 57 | } 58 | /// 导出 59 | [exportSession exportAsynchronouslyWithCompletionHandler:^{ 60 | if (exportSession.status == AVAssetExportSessionStatusFailed) { 61 | completion(nil); 62 | }else if (exportSession.status == AVAssetExportSessionStatusCompleted){ 63 | completion(tempFileURL); 64 | } 65 | }]; 66 | }]; 67 | } 68 | 69 | + (void)checkVideo:(PHAsset *)asset unavailable:(void(^)(NSString *errormsg,NSString *info,XFCheckVideoResult type))unavailable available:(void(^)(float duration,long long dataLength,NSString *format))available{ 70 | PHVideoRequestOptions* options = [[PHVideoRequestOptions alloc] init]; 71 | [options setNetworkAccessAllowed:YES]; 72 | [PHImageManager.defaultManager requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset * _Nullable avAsset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) { 73 | CMTime assetTime = [avAsset duration]; 74 | float duration = CMTimeGetSeconds(assetTime); 75 | NSLog(@"视频时长是:%f",duration ); 76 | AVURLAsset *avurlAsset = (AVURLAsset *)avAsset; 77 | if (avAsset == nil) { 78 | unavailable(nil,nil,XFCheckVideoUndefined); 79 | return; 80 | } 81 | NSURL* videoUrl = avurlAsset.URL; 82 | NSData * data = [NSData dataWithContentsOfURL:videoUrl]; 83 | NSLog(@"===%f=",(unsigned long)data.length / 1024.0 / 1024.0); 84 | 85 | NSString * errorString = nil; 86 | NSString *format = [videoUrl.absoluteString.lastPathComponent.lowercaseString componentsSeparatedByString:@"."].lastObject; 87 | 88 | if(![videoUrl.absoluteString.lastPathComponent.lowercaseString containsString:@"mp4"]&& 89 | ![videoUrl.absoluteString.lastPathComponent.lowercaseString containsString:@"mov"]){//格式不符合 90 | errorString = @"目前仅支持上传MP4、MOV格式的视频哦"; 91 | unavailable(errorString,format,XFVideoUnavailableFormat); 92 | return; 93 | }else if(data.length > videoMaxSize*1024*1024){ 94 | errorString = [NSString stringWithFormat:@"为了保证您的视频上传体验,被添加的视频文件不要超过%ldMB哦",videoMaxSize]; 95 | NSString* videoSize = [NSString stringWithFormat:@"%.1f",(double)data.length/1024.0/1024.0]; 96 | unavailable(errorString,videoSize,XFVideoSizeTooBig); 97 | return; 98 | } 99 | else if( duration < videoMinDuration){//视频时间 不少于5秒 100 | errorString = [NSString stringWithFormat:@"为保证视频播放体验\n目前仅支持上传时长不少于%ld秒的视频哦", videoMinDuration]; 101 | unavailable(errorString,@(duration).stringValue,XFVideoDurationTooShort); 102 | return; 103 | } 104 | /// 子线程 105 | available(duration,data.length,format); 106 | }]; 107 | 108 | } 109 | 110 | #pragma mark - 处理 附件图片 111 | 112 | + (void)handleVideoAttachment:(UIImage *)orgImg resize:(CGSize)resize complate:(void(^)(UIImage *resizeImg,NSURL *imgPath))complate{ 113 | 114 | /// 保存视频封面 115 | CGFloat scale = resize.width / orgImg.size.width; 116 | 117 | CGFloat hight = orgImg.size.height * scale; 118 | 119 | CGSize newResiz = CGSizeMake(resize.width, hight); 120 | 121 | UIImage* aspectScaledToFitImage = [UIImage scaleImage:orgImg toSize:newResiz]; 122 | 123 | UIImage* coverImg = [UIImage composeVideoCoverImg:aspectScaledToFitImage]; 124 | 125 | /// 原图写入 126 | NSURL *fileURL = [orgImg saveToDisk]; 127 | 128 | complate(coverImg,fileURL); 129 | } 130 | 131 | 132 | + (void)handleImgAttachment:(UIImage *)orgImg resize:(CGSize)resize complate:(void(^)(UIImage *resizeImg,NSURL*resizeImgDiskPath))complate{ 133 | 134 | CGFloat scale = resize.width / orgImg.size.width; 135 | 136 | CGFloat hight = orgImg.size.height * scale; 137 | 138 | CGSize newSize = CGSizeMake(resize.width, hight); 139 | 140 | UIImage* aspectScaledToFitImage = [UIImage scaleImage:orgImg toSize:newSize]; 141 | 142 | /// 写入磁盘 143 | NSURL *diskPath = [aspectScaledToFitImage saveToDisk]; 144 | 145 | complate(aspectScaledToFitImage,diskPath); 146 | } 147 | 148 | @end 149 | -------------------------------------------------------------------------------- /Classes/Utils/UIImage+Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Utils.h 3 | // 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface UIImage (Utils) 14 | 15 | /// 压缩图片到目标尺寸 16 | + (NSData *)compressImage:(UIImage *)img toSize:(long long)targetSize; 17 | /// 合成视频封图 + 播放icon 18 | + (UIImage *)composeVideoCoverImg:(UIImage *)videoImg; 19 | 20 | @end 21 | 22 | @interface UIImage (Resize) 23 | 24 | /// 修改图片CGSize 25 | + (UIImage *)scaleImage:(UIImage *)originalImage toSize:(CGSize)size; 26 | + (CGSize)estimateNewSize:(CGSize)newSize forImage:(UIImage *)image; 27 | + (UIImage *)scaleImage:(UIImage *)image proportionallyToSize:(CGSize)newSize; 28 | 29 | @end 30 | 31 | @interface UIImage (Placeholder) 32 | 33 | + (UIImage *)placeholderColor:(UIColor *)color size:(CGSize)size; 34 | 35 | @end 36 | 37 | 38 | @interface UIImage (Save) 39 | 40 | - (NSURL *)saveToDisk; 41 | 42 | + (UIImage *)loadImgWith:(NSString *)fileName; 43 | 44 | @end 45 | 46 | NS_ASSUME_NONNULL_END 47 | -------------------------------------------------------------------------------- /Classes/Utils/UIImage+Utils.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Utils.m 3 | // 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 bin. All rights reserved. 7 | // 8 | 9 | #import "UIImage+Utils.h" 10 | #import "RichTextTools.h" 11 | 12 | @implementation UIImage (Utils) 13 | 14 | + (NSData *)compressImage:(UIImage *)img toSize:(long long)targetSize { 15 | UIGraphicsBeginImageContext(img.size); 16 | [img drawInRect:CGRectMake(0, 0, img.size.width, img.size.height)]; 17 | 18 | UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); 19 | if (newImage == nil) { 20 | UIGraphicsEndImageContext(); 21 | return nil; 22 | } 23 | UIGraphicsEndImageContext(); 24 | 25 | // targetSize以字节(K)为单位 26 | long long maxFileSize = targetSize * 1024; 27 | CGFloat compression = 0.9; 28 | CGFloat maxCompression = 0.1; 29 | NSData *compressedData = UIImageJPEGRepresentation(newImage, compression); 30 | if (compressedData == nil) { 31 | return nil; 32 | } 33 | while (compressedData.length > maxFileSize && compression > maxCompression ) { 34 | compression = compression - 0.1; 35 | compressedData = UIImageJPEGRepresentation(newImage, compression); 36 | } 37 | return compressedData; 38 | } 39 | 40 | 41 | + (UIImage *)composeVideoCoverImg:(UIImage *)originImg{ 42 | if (originImg == nil) { 43 | return nil; 44 | } 45 | UIImage *tempImage = [UIImage imageWithCGImage:[originImg CGImage]]; 46 | 47 | UIImage* icon = XFRichTextImage(@"movie_play"); 48 | //[UIImage imageNamed:@"movie_play"]; 49 | if (icon == nil) { 50 | return nil; 51 | } 52 | CGImageRef iconRef = icon.CGImage; 53 | CGFloat iconW = CGImageGetWidth(iconRef); 54 | CGFloat iconH = CGImageGetHeight(iconRef); 55 | 56 | CGImageRef videoImgRef = tempImage.CGImage; 57 | CGFloat videoImgW = CGImageGetWidth(videoImgRef); 58 | CGFloat videoImgH = CGImageGetHeight(videoImgRef); 59 | 60 | UIGraphicsBeginImageContext(CGSizeMake(videoImgW, videoImgH)); 61 | [tempImage drawInRect:CGRectMake(0, 0, videoImgW, videoImgH)]; 62 | [icon drawInRect:CGRectMake(videoImgW/2 - iconW/2, videoImgH/2 - iconH/2, iconW, iconH)]; 63 | UIImage *coverImg = UIGraphicsGetImageFromCurrentImageContext(); 64 | UIGraphicsEndImageContext(); 65 | 66 | // CGImageRelease(iconRef); 67 | // CGImageRelease(videoImgRef); 68 | return coverImg; 69 | } 70 | 71 | @end 72 | 73 | 74 | @implementation UIImage (Resize) 75 | 76 | + (UIImage *)scaleImage:(UIImage *)originalImage toSize:(CGSize)size 77 | { 78 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 79 | CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast); 80 | CGContextClearRect(context, CGRectMake(0, 0, size.width, size.height)); 81 | 82 | if (originalImage.imageOrientation == UIImageOrientationRight) { 83 | CGContextRotateCTM(context, -M_PI_2); 84 | CGContextTranslateCTM(context, -size.height, 0.0f); 85 | CGContextDrawImage(context, CGRectMake(0, 0, size.height, size.width), originalImage.CGImage); 86 | } else { 87 | CGContextDrawImage(context, CGRectMake(0, 0, size.width, size.height), originalImage.CGImage); 88 | } 89 | 90 | CGImageRef scaledImage = CGBitmapContextCreateImage(context); 91 | CGColorSpaceRelease(colorSpace); 92 | CGContextRelease(context); 93 | 94 | UIImage *image = [UIImage imageWithCGImage:scaledImage]; 95 | CGImageRelease(scaledImage); 96 | 97 | return image; 98 | } 99 | 100 | + (CGSize)estimateNewSize:(CGSize)newSize forImage:(UIImage *)image 101 | { 102 | if (image.size.width > image.size.height) { 103 | newSize = CGSizeMake((image.size.width/image.size.height) * newSize.height, newSize.height); 104 | } else { 105 | newSize = CGSizeMake(newSize.width, (image.size.height/image.size.width) * newSize.width); 106 | } 107 | 108 | return newSize; 109 | } 110 | 111 | + (UIImage *)scaleImage:(UIImage *)image proportionallyToSize:(CGSize)newSize 112 | { 113 | return [self scaleImage:image toSize:[self estimateNewSize:newSize forImage:image]]; 114 | } 115 | 116 | @end 117 | 118 | 119 | @implementation UIImage (Placeholder) 120 | 121 | + (UIImage *)placeholderColor:(UIColor *)color size:(CGSize)size{ 122 | UIGraphicsBeginImageContextWithOptions(size, NO, 0); 123 | [color setFill]; 124 | [[UIBezierPath bezierPathWithRect:CGRectMake(0, 0, size.width, size.height)] fill]; 125 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 126 | UIGraphicsEndImageContext(); 127 | return image; 128 | } 129 | 130 | @end 131 | 132 | @implementation UIImage (Save) 133 | 134 | - (NSURL *)saveToDisk{ 135 | NSString *fileName = [NSString stringWithFormat:@"%@.%@",[NSUUID UUID].UUIDString,@"jpg"]; 136 | NSData* imageData = UIImageJPEGRepresentation(self, 1); 137 | if (!imageData) { 138 | imageData = UIImagePNGRepresentation(self); 139 | fileName = [fileName stringByReplacingOccurrencesOfString:@".jpg" withString:@".png"]; 140 | } 141 | 142 | NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 143 | 144 | NSString* documentsDirectory = [paths objectAtIndex:0]; 145 | 146 | // Now we get the full path to the file 147 | 148 | NSString* fullPathToFile = [documentsDirectory stringByAppendingPathComponent:fileName]; 149 | 150 | // and then we write it out 151 | 152 | BOOL isWrite = [imageData writeToFile:fullPathToFile atomically:NO]; 153 | 154 | if (!isWrite) { 155 | return nil; 156 | } 157 | return [NSURL URLWithString:fullPathToFile]; 158 | } 159 | 160 | + (UIImage *)loadImgWith:(NSString *)fileName{ 161 | if (fileName.length == 0) { 162 | return nil; 163 | } 164 | NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 165 | NSString* documentsDirectory = [paths objectAtIndex:0]; 166 | NSString* fullPathToFile = [documentsDirectory stringByAppendingPathComponent:fileName]; 167 | if (![[NSFileManager defaultManager] isExecutableFileAtPath:fullPathToFile]) { 168 | return nil; 169 | } 170 | return [UIImage imageWithContentsOfFile:fullPathToFile]; 171 | } 172 | 173 | @end 174 | 175 | -------------------------------------------------------------------------------- /Classes/XFRichTextTool.bundle/movie_play@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/Classes/XFRichTextTool.bundle/movie_play@2x.png -------------------------------------------------------------------------------- /Classes/XFRichTextTool.bundle/movie_play@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/Classes/XFRichTextTool.bundle/movie_play@3x.png -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | 4 | platform :ios, '8.0' 5 | inhibit_all_warnings! 6 | # 设置不在发送使用统计加快速度 7 | ENV["COCOAPODS_DISABLE_STATS"] = "true" 8 | 9 | target 'TTRichTextView' do 10 | 11 | pod 'LKDBHelper' 12 | pod 'Masonry' 13 | pod 'YYKit' 14 | pod 'MJExtension' 15 | pod 'TZImagePickerController' #图片视频选择 16 | 17 | end 18 | 19 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FMDB (2.7.5): 3 | - FMDB/standard (= 2.7.5) 4 | - FMDB/standard (2.7.5) 5 | - LKDBHelper (2.5.1): 6 | - LKDBHelper/standard (= 2.5.1) 7 | - LKDBHelper/standard (2.5.1): 8 | - FMDB 9 | - Masonry (1.1.0) 10 | - MJExtension (3.0.17) 11 | - TZImagePickerController (3.2.1) 12 | - YYKit (1.0.9): 13 | - YYKit/no-arc (= 1.0.9) 14 | - YYKit/no-arc (1.0.9) 15 | 16 | DEPENDENCIES: 17 | - LKDBHelper 18 | - Masonry 19 | - MJExtension 20 | - TZImagePickerController 21 | - YYKit 22 | 23 | SPEC REPOS: 24 | https://github.com/cocoapods/specs.git: 25 | - FMDB 26 | - LKDBHelper 27 | - Masonry 28 | - MJExtension 29 | - TZImagePickerController 30 | - YYKit 31 | 32 | SPEC CHECKSUMS: 33 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 34 | LKDBHelper: 9a276ba51f2e16263d46a9517e16cc9b03472664 35 | Masonry: 678fab65091a9290e40e2832a55e7ab731aad201 36 | MJExtension: 74ec83124a68891619fb7ba9c5c811bbf1691076 37 | TZImagePickerController: bf4c57b98d8707fce41ea6be872414a71c7a8c9d 38 | YYKit: 7cda43304a8dc3696c449041e2cb3107b4e236e7 39 | 40 | PODFILE CHECKSUM: 0736eb3ea3506927a66b20c7d268885bcfc368e8 41 | 42 | COCOAPODS: 1.6.1 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TTRichTextView 2 | TextKit富文本编辑器,支持任意UIView 作为文本附件, 重用机制优化性能 3 | 4 | ![](richtext.gif) 5 | ![](richtext_empty.gif) 6 | 7 | 支持以下功能 8 | 9 | * 插入任意UIView做为附件,支持手势交互,支持复制粘贴 10 | * 支持 NSTextAttachment 混用 11 | * 富文本局部刷新 12 | * 插入 视频 和 图片 及处理 13 | * 输入高亮 14 | * 转换简单html标签 方便传输 15 | * 草稿功能 16 | -------------------------------------------------------------------------------- /TTRichTextView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TTRichTextView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TTRichTextView.xcodeproj/xcuserdata/qyer.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TTRichTextView.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 7 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TTRichTextView.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TTRichTextView.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TTRichTextView.xcworkspace/xcuserdata/qyer.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /TTRichTextView.xcworkspace/xcuserdata/qyer.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /TTRichTextView/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /TTRichTextView/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // 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. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // 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. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // 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. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_login_backBtn2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "cf_login_backBtn2@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "cf_login_backBtn2@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_login_backBtn2.imageset/cf_login_backBtn2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/Assets.xcassets/cf_login_backBtn2.imageset/cf_login_backBtn2@2x.png -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_login_backBtn2.imageset/cf_login_backBtn2@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/Assets.xcassets/cf_login_backBtn2.imageset/cf_login_backBtn2@3x.png -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_richText_tool_image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "tupian_normal@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "tupian_normal@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_richText_tool_image.imageset/tupian_normal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/Assets.xcassets/cf_richText_tool_image.imageset/tupian_normal@2x.png -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_richText_tool_image.imageset/tupian_normal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/Assets.xcassets/cf_richText_tool_image.imageset/tupian_normal@3x.png -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_richText_tool_video_normal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "视频-icon@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "视频-icon@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_richText_tool_video_normal.imageset/视频-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/Assets.xcassets/cf_richText_tool_video_normal.imageset/视频-icon@2x.png -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/cf_richText_tool_video_normal.imageset/视频-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/Assets.xcassets/cf_richText_tool_video_normal.imageset/视频-icon@3x.png -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/rich_topic_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "rich_topic_icon@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "rich_topic_icon@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/rich_topic_icon.imageset/rich_topic_icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/Assets.xcassets/rich_topic_icon.imageset/rich_topic_icon@2x.png -------------------------------------------------------------------------------- /TTRichTextView/Assets.xcassets/rich_topic_icon.imageset/rich_topic_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/Assets.xcassets/rich_topic_icon.imageset/rich_topic_icon@3x.png -------------------------------------------------------------------------------- /TTRichTextView/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TTRichTextView/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /TTRichTextView/Controller/DraftListViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DraftListViewController.h 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface DraftListViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /TTRichTextView/Controller/DraftListViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DraftListViewController.m 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import "DraftListViewController.h" 10 | 11 | @interface DraftListViewController () 12 | 13 | 14 | 15 | @property (nonatomic,strong) UITableView *tableView; 16 | 17 | @end 18 | 19 | @implementation DraftListViewController 20 | 21 | - (void)viewDidLoad { 22 | [super viewDidLoad]; 23 | // Do any additional setup after loading the view. 24 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds]; 25 | _tableView.dataSource = self; 26 | _tableView.delegate = self; 27 | [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"demoCell"]; 28 | [self.view addSubview:self.tableView]; 29 | 30 | } 31 | 32 | 33 | 34 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ 35 | return 80; 36 | } 37 | 38 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ 39 | return 2; 40 | } 41 | 42 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 43 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"demoCell"]; 44 | cell.textLabel.text = @"草稿"; 45 | return cell; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /TTRichTextView/Controller/PostRichEditViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PostRichEditViewController.h 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import "RichTextViewController.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface PostRichEditViewController : RichTextViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /TTRichTextView/Controller/PostRichEditViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // PostRichEditViewController.m 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import "PostRichEditViewController.h" 10 | #import "Masonry.h" 11 | 12 | #import "NSTextStorage+Html.h" 13 | #import "MediaCompressTool.h" 14 | 15 | #import "AudioTextAttachment.h" 16 | #import "GifTextAttachment.h" 17 | 18 | #import "GifAttachmentReusableView.h" 19 | #import "AudioAttachmentReusableView.h" 20 | 21 | #import "NSTextStorage+Draft.h" 22 | 23 | @interface PostRichEditViewController () 24 | 25 | @property (nonatomic,strong) UITextField *titleField; 26 | 27 | @property (nonatomic,assign) BOOL richTextIsFirstResponder; 28 | 29 | @end 30 | 31 | @implementation PostRichEditViewController 32 | 33 | 34 | 35 | #pragma mark - left cycle 36 | 37 | - (void)viewDidLoad { 38 | [super viewDidLoad]; 39 | // Do any additional setup after loading the view. 40 | 41 | [self setupLayout]; 42 | 43 | // config AttachmentReusableView 44 | [self.textView registerClass:AudioAttachmentReusableView.class forReusableViewWithIdentifier:NSStringFromClass(AudioAttachmentReusableView.class)]; 45 | [self.textView registerClass:GifAttachmentReusableView.class forReusableViewWithIdentifier:NSStringFromClass(GifAttachmentReusableView.class)]; 46 | } 47 | 48 | 49 | #pragma mark - Send Post Paramters 50 | 51 | - (void)sendPostContentRequest{ 52 | NSString *content = [self.textView.textStorage convertToHtml]; 53 | NSLog(@"---content Text-%@-",content); 54 | } 55 | 56 | -(void)sendMenuPressed{ 57 | 58 | [self.textView.textStorage buildDraft:RichTextHtml complated:^(DraftMetaDataModel * _Nonnull draftModel) { 59 | NSLog(@"~~RichTextHtml~~~%@",draftModel.richText); 60 | }]; 61 | 62 | } 63 | 64 | -(void)backMenuPressed{ 65 | [self.textView resignFirstResponder]; 66 | [self.titleField resignFirstResponder]; 67 | [self resignFirstResponder]; 68 | 69 | if (self.textView.text.length == 0) { 70 | [super backMenuPressed]; 71 | return; 72 | } 73 | 74 | [super backMenuPressed]; 75 | } 76 | 77 | 78 | #pragma mark - UITextFieldDelegate 79 | 80 | - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{ 81 | self.textView.inputAccessoryView = nil; 82 | [self.textView reloadInputViews]; 83 | [self reloadInputViews]; 84 | return YES; 85 | } 86 | 87 | 88 | #pragma mark - UITextViewDelegate 89 | 90 | - (BOOL)textViewShouldBeginEditing:(UITextView *)textView{ 91 | self.textView.inputAccessoryView = self.textInputView; 92 | return YES; 93 | } 94 | 95 | -(void)textViewDidChange:(UITextView *)textView{ 96 | if (textView != self.textView) { 97 | return; 98 | } 99 | 100 | /// 重置 101 | NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; 102 | style.lineSpacing = 10; 103 | self.textView.typingAttributes = @{NSFontAttributeName:[UIFont systemFontOfSize:AutoSize(15)], 104 | NSForegroundColorAttributeName:[UIColor blackColor], 105 | NSParagraphStyleAttributeName:style 106 | }; 107 | 108 | self.textInputView.currentTextLength = self.textView.text.length; 109 | 110 | self.placeholderLabel.hidden = (self.textView.text.length > 0); 111 | /// 限制图片和视频个数 112 | self.textInputView.vedioBtn.enabled = ([self.textView videoAttachmentCount] < maxVedioCount); 113 | self.textInputView.imgBtn.enabled = ([self.textView imageAttachmentCount] < maxPhotoCount); 114 | } 115 | 116 | - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{ 117 | 118 | if([text isEqualToString:@"☻"]){//防止输入^_^崩溃。☻是我输入^_^后系统打印出来的,也就是说输入^_^,系统却理解成了☻ 119 | [textView insertText:@"^_^"];//如果你的服务器识别不了,所以还是把这句删除吧,删除后就变成禁止输入^_^ 120 | if ([self.textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { 121 | [self.textView.delegate textViewDidChange:self.textView]; 122 | } 123 | return NO; 124 | } 125 | 126 | if ( [textView bindDelete:NSAttributedStringBindingKey inRange:range] == NO ) { 127 | if ([self.textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { 128 | [self.textView.delegate textViewDidChange:self.textView]; 129 | } 130 | return NO; 131 | } 132 | // if ([text isEqualToString:@"@"]){ 133 | // 134 | // return NO; 135 | // }else 136 | 137 | /* 输入#字符 拉起页面 138 | if ([text isEqualToString:@"#"]){ 139 | [self insertTagAction]; 140 | return NO; 141 | } */ 142 | return YES; 143 | } 144 | 145 | - (void)textViewDidChangeSelection:(UITextView *)textView { 146 | [textView bindSelectRange:NSAttributedStringBindingKey textViewDelegate:self]; 147 | } 148 | 149 | #pragma mark - TextViewAttachmentDelegate 150 | 151 | - (TextAttachmentReusableView *)textView:(UITextView *)textView viewForAttachment:(AnyViewTextAttachment *)attachment{ 152 | if (![textView isKindOfClass:RichTextView.class]) { 153 | return nil; 154 | } 155 | 156 | TextAttachmentReusableView *reusableView = nil; 157 | 158 | if ([attachment isKindOfClass:[AudioTextAttachment class]]) { 159 | 160 | NSString *identifier = NSStringFromClass(AudioAttachmentReusableView.class); 161 | 162 | reusableView = [(RichTextView *)textView dequeueReusableAttachmentViewWithIdentifier:identifier]; 163 | 164 | }else if ([attachment isKindOfClass:[GifTextAttachment class]]){ 165 | 166 | NSString *identifier = NSStringFromClass(GifAttachmentReusableView.class); 167 | 168 | reusableView = [(RichTextView *)textView dequeueReusableAttachmentViewWithIdentifier:identifier]; 169 | } 170 | 171 | return reusableView; 172 | } 173 | 174 | 175 | - (void)textView:(UITextView *)textView tapedAttachment:(NSTextAttachment *)attachment{ 176 | 177 | NSLog(@"!!!!tapedAttachment!!!!!!!!!%@",attachment); 178 | } 179 | 180 | - (void)textView:(UITextView *)textView deselected:(NSTextAttachment *)deselectedAttachment atPoint:(CGPoint)point{ 181 | 182 | NSLog(@"!!!deselected!!!!!!!!!!%@",deselectedAttachment); 183 | } 184 | 185 | 186 | #pragma mark - ConfigUI 187 | 188 | - (void)setupLayout{ 189 | UIView *line = [UIView new]; 190 | line.backgroundColor = [UIColor colorWithRed:214/255.0 green:214/255.0 blue:214/255.0 alpha:1]; 191 | 192 | [self.view addSubview:self.titleField]; 193 | [self.view addSubview:line]; 194 | [self.view addSubview:self.textView]; 195 | [self.view addSubview:self.placeholderLabel]; 196 | 197 | [self.titleField mas_makeConstraints:^(MASConstraintMaker *make) { 198 | make.top.equalTo(self.view).offset(TopBarHeight); 199 | make.left.equalTo(self.view.mas_left).offset(AutoSize(10)); 200 | make.height.mas_equalTo(48); 201 | make.width.mas_equalTo(ScreenWidth - 2*AutoSize(10)); 202 | }]; 203 | 204 | [line mas_makeConstraints:^(MASConstraintMaker *make) { 205 | make.left.right.equalTo(self.view); 206 | make.top.equalTo(self.titleField.mas_bottom); 207 | make.height.mas_equalTo(0.5); 208 | make.width.mas_equalTo(ScreenWidth); 209 | }]; 210 | 211 | [self.textView mas_makeConstraints:^(MASConstraintMaker *make) { 212 | make.left.equalTo(self.view.mas_left); 213 | make.right.equalTo(self.view.mas_right); 214 | make.top.equalTo(line.mas_bottom).offset(10); 215 | if (@available(iOS 11.0, *)) { 216 | make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom); 217 | } else { 218 | make.bottom.equalTo(self.view); 219 | } 220 | }]; 221 | 222 | [self.placeholderLabel mas_makeConstraints:^(MASConstraintMaker *make) { 223 | make.left.equalTo(self.textView.mas_left).offset(10); 224 | make.top.equalTo(self.textView.mas_top); 225 | }]; 226 | } 227 | 228 | 229 | #pragma mark - Getter 230 | 231 | - (UITextField *)titleField { 232 | if (_titleField == nil) { 233 | _titleField = [[UITextField alloc] init]; 234 | _titleField.placeholder = @"标题 (必填)"; 235 | _titleField.font = [UIFont systemFontOfSize:15]; 236 | _titleField.delegate = self; 237 | } 238 | return _titleField; 239 | } 240 | @end 241 | -------------------------------------------------------------------------------- /TTRichTextView/Controller/RichTextViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextViewController.h 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RichTextInputAccessoryView.h" 11 | #import "RichTextView.h" 12 | #import "TextViewAttachmentDelegate.h" 13 | #import "RichTextView+TextAttachment.h" 14 | #import "UITextView+RichText.h" 15 | #import "Define.h" 16 | #import "TZImagePickerController.h" 17 | 18 | NS_ASSUME_NONNULL_BEGIN 19 | 20 | static NSString *const kTopicExpStr = @"#[^@#;<>\\s]+#"; 21 | static NSInteger const inputAccessoryViewHeight = 45; 22 | 23 | static NSInteger const maxVedioCount = 1000000; 24 | static NSInteger const maxPhotoCount = 900000; 25 | 26 | @interface RichTextViewController : UIViewController 27 | 28 | @property (nonatomic,assign) BOOL isEmptyContent; 29 | 30 | @property (nonatomic,strong,readonly) RichTextView *textView; 31 | 32 | @property (nonatomic,strong,readonly) UILabel *placeholderLabel; 33 | 34 | @property (nonatomic,strong) RichTextInputAccessoryView * textInputView; 35 | 36 | @property (assign, nonatomic,readonly) CGFloat kbHeight; 37 | 38 | /// should overrid 39 | - (void)backMenuPressed; 40 | 41 | - (void)sendMenuPressed; 42 | 43 | 44 | - (void)insertTagAction; 45 | - (void)insertAudioAction; 46 | - (void)insertGifAction; 47 | - (void)insertBAction; 48 | - (void)insertImgAction; 49 | - (void)insertVedioAction; 50 | @end 51 | 52 | NS_ASSUME_NONNULL_END 53 | -------------------------------------------------------------------------------- /TTRichTextView/Controller/RichTextViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextViewController.m 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import "RichTextViewController.h" 10 | #import "MediaCompressTool.h" 11 | #import "NSTextStorage+Draft.h" 12 | #import "RichTextView+Draft.h" 13 | #import "AttachmentLayoutManager.h" 14 | 15 | #import "AudioTextAttachment.h" 16 | #import "GifTextAttachment.h" 17 | 18 | #import "GifAttachmentReusableView.h" 19 | #import "AudioAttachmentReusableView.h" 20 | 21 | @interface RichTextViewController () 22 | 23 | @property (nonatomic,strong) RichTextView *textView; 24 | 25 | @property (nonatomic,strong) UILabel *placeholderLabel; 26 | 27 | @property (assign, nonatomic) CGFloat kbHeight; 28 | 29 | @end 30 | 31 | @implementation RichTextViewController 32 | 33 | - (void)viewDidLoad { 34 | [super viewDidLoad]; 35 | // Do any additional setup after loading the view. 36 | self.view.backgroundColor = [UIColor whiteColor]; 37 | 38 | /// 导航栏 39 | [self setupNavView]; 40 | /// 监听键盘 41 | [self observeKeyboard]; 42 | 43 | 44 | // actions 45 | [self.textInputView.imgBtn addTarget:self action:@selector(insertImgAction) forControlEvents:UIControlEventTouchUpInside]; 46 | 47 | [self.textInputView.vedioBtn addTarget:self action:@selector(insertVedioAction) forControlEvents:UIControlEventTouchUpInside]; 48 | 49 | [self.textInputView.tagBtn addTarget:self action:@selector(insertTagAction) forControlEvents:UIControlEventTouchUpInside]; 50 | 51 | 52 | [self.textInputView.audioBtn addTarget:self action:@selector(insertAudioAction) forControlEvents:UIControlEventTouchUpInside]; 53 | 54 | [self.textInputView.anyBtn addTarget:self action:@selector(insertGifAction) forControlEvents:UIControlEventTouchUpInside]; 55 | 56 | [self.textInputView.bBtn addTarget:self action:@selector(insertBAction) forControlEvents:UIControlEventTouchUpInside]; 57 | } 58 | 59 | - (void)didReceiveMemoryWarning { 60 | [super didReceiveMemoryWarning]; 61 | // Dispose of any resources that can be recreated. 62 | NSLog(@"~~~~didReceiveMemoryWarning~~~"); 63 | } 64 | 65 | #pragma mark - inputAccessoryView 66 | 67 | - (BOOL)canBecomeFirstResponder{ 68 | return YES; 69 | } 70 | 71 | - (UIView *)inputAccessoryView{ 72 | return self.textInputView; 73 | } 74 | 75 | 76 | #pragma mark - insert Action 77 | 78 | - (void)insertTagAction { 79 | NSString* newTopicName = [NSString stringWithFormat:@"#%@#", @"测试话题"]; 80 | /// 内容后面保留一个空格 81 | NSString *bindText = AttributedStringBindingText(newTopicName); 82 | [self.textView insertText:bindText]; 83 | [self.textView becomeFirstResponder]; 84 | 85 | ///根据当前光标位置判断位置 带空额 绑定文本后面带有空格 86 | NSRange tnRange = NSMakeRange(self.textView.selectedRange.location - bindText.length, bindText.length - 1); 87 | /// 仅插入的话题才高亮: 88 | NSRegularExpression *kTopicExp = [NSRegularExpression regularExpressionWithPattern:kTopicExpStr options:kNilOptions error:nil]; 89 | NSRange allRange = NSMakeRange(0, self.textView.textStorage.length); 90 | [kTopicExp enumerateMatchesInString:self.textView.textStorage.string options:NSMatchingWithoutAnchoringBounds range:allRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { 91 | if (!result) return; 92 | 93 | /// 当前的话题才加入高亮 94 | if (!NSEqualRanges(tnRange, result.range)) { 95 | return; 96 | } 97 | [self.textView addBindingAttributeInRange:result.range highlightingColor:[UIColor colorWithRed:253/255.0 green:136/255.0 blue:43/255.0 alpha:1] ]; 98 | }]; 99 | 100 | } 101 | 102 | - (void)insertAudioAction{ 103 | CGSize size = CGSizeMake(self.textView.textContainer.size.width, 50); 104 | 105 | AudioTextAttachment *audioAttchement = [[AudioTextAttachment alloc] init]; 106 | audioAttchement.bounds = CGRectMake(0, 0, size.width, size.height); 107 | [self.textView insertAttachment:audioAttchement paragraphStyle:nil]; 108 | 109 | /// 限制图片个数 和 提示语隐藏 110 | if ([self.textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { 111 | [self.textView.delegate textViewDidChange:self.textView]; 112 | } 113 | [self.textView becomeFirstResponder]; 114 | } 115 | 116 | - (void)insertGifAction{ 117 | CGSize size = CGSizeMake(self.textView.textContainer.size.width, 200); 118 | GifTextAttachment *gifAttchement = [[GifTextAttachment alloc] init]; 119 | gifAttchement.bounds = CGRectMake(0, 0, size.width, size.height); 120 | [self.textView insertAttachment:gifAttchement paragraphStyle:nil]; 121 | 122 | /// 限制图片个数 和 提示语隐藏 123 | if ([self.textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { 124 | [self.textView.delegate textViewDidChange:self.textView]; 125 | } 126 | [self.textView becomeFirstResponder]; 127 | } 128 | 129 | - (void)insertBAction{ 130 | [self.textView toggleUnderline:self.textView]; 131 | // [self.textView removeUnderlineForRange:self.textView.selectedRange]; 132 | // [self.textView removeBold]; 133 | } 134 | 135 | - (void)insertImgAction{ 136 | CGSize newSize = CGSizeMake(self.textView.textContainer.size.width, 0); 137 | TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] init]; 138 | imagePickerVc.allowTakePicture = YES; 139 | imagePickerVc.allowPickingVideo = NO; 140 | imagePickerVc.maxImagesCount = maxPhotoCount - [self.textView imageAttachmentCount]; 141 | WeakSelf 142 | imagePickerVc.didFinishPickingPhotosHandle = ^(NSArray *photos, NSArray *assets, BOOL isSelectOriginalPhoto) { 143 | for (UIImage *photo in photos) { 144 | NSInteger index = [photos indexOfObject:photo]; 145 | PHAsset *photoAsset = assets[index]; 146 | /// 插入图片附件 147 | [weakself insertImageAttchment:photo resize:newSize photoAsset:photoAsset]; 148 | } 149 | 150 | /// 限制图片个数 和 提示语隐藏 151 | if ([weakself.textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { 152 | [weakself.textView.delegate textViewDidChange:weakself.textView]; 153 | } 154 | [weakself.textView becomeFirstResponder]; 155 | }; 156 | 157 | [self presentViewController:imagePickerVc animated:true completion:nil]; 158 | } 159 | 160 | - (void)insertVedioAction{ 161 | CGSize newSize = CGSizeMake(self.textView.textContainer.size.width, 0); 162 | TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] init]; 163 | imagePickerVc.allowTakePicture = NO; 164 | imagePickerVc.allowPickingVideo = YES; 165 | imagePickerVc.allowPickingImage = NO; 166 | WeakSelf 167 | imagePickerVc.didFinishPickingVideoHandle = ^(UIImage *coverImage, id asset) { 168 | if (![asset isKindOfClass:[PHAsset class]] || coverImage == nil) { 169 | return; 170 | } 171 | /// 检查视频是否合规 172 | [MediaCompressTool checkVideo:asset unavailable:^(NSString *errormsg,NSString *info,XFCheckVideoResult type){ 173 | 174 | /// 弹框 175 | [weakself alertViewWhenUnavailableVideo:errormsg]; 176 | 177 | } available:^(float duration,long long dataLength,NSString *format){ 178 | /// 插入视频附件 179 | [weakself insertVideoAttchment:coverImage resize:newSize videoAsset:asset complate:^(VideoTextAttachment *videoAttachment) { 180 | /// 更新视频信息 181 | videoAttachment.duration = duration; 182 | videoAttachment.dataLength = dataLength; 183 | videoAttachment.format = format; 184 | 185 | /// 限制视频个数 和 提示语隐藏 186 | if ([weakself.textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { 187 | [weakself.textView.delegate textViewDidChange:weakself.textView]; 188 | } 189 | [weakself.textView becomeFirstResponder]; 190 | }]; 191 | }]; 192 | }; 193 | 194 | [self presentViewController:imagePickerVc animated:true completion:nil]; 195 | } 196 | 197 | - (void)insertImageAttchment:(UIImage *)photo resize:(CGSize)newSize photoAsset:(PHAsset *)asset{ 198 | /// 处理照片 199 | WeakSelf 200 | [MediaCompressTool handleImgAttachment:photo resize:newSize complate:^(UIImage *resizeImg ,NSURL* resizeImgDiskPath) { 201 | 202 | ImageTextAttachment *imgAttachment = [weakself.textView insertImgAttachment:resizeImg paragraphStyle:nil]; 203 | 204 | imgAttachment.imgAsset = asset; 205 | 206 | imgAttachment.thumbnailDiskPath = resizeImgDiskPath; 207 | }]; 208 | } 209 | 210 | -(void)insertVideoAttchment:(UIImage *)coverImage resize:(CGSize)newSize videoAsset:(PHAsset *)asset complate:(void(^)(VideoTextAttachment * videoAttachment))complate{ 211 | 212 | /// 处理视频 和 封面逻辑 213 | WeakSelf 214 | [MediaCompressTool handleVideoAttachment:coverImage resize:newSize complate:^(UIImage *resizeImg, NSURL *imgPath) { 215 | dispatch_async(dispatch_get_main_queue(), ^{ 216 | VideoTextAttachment * videoAttachment = [weakself.textView insertVideoAttachment:resizeImg paragraphStyle:nil]; 217 | videoAttachment.posterDiskPath = imgPath; 218 | videoAttachment.videoAsset = asset; 219 | videoAttachment.coverSize = coverImage.size; 220 | 221 | /// 插入视频附件完成 222 | if (complate) { 223 | complate(videoAttachment); 224 | } 225 | }); 226 | }]; 227 | } 228 | 229 | - (void)alertViewWhenUnavailableVideo:(NSString *)msg{ 230 | UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"视频添加失败" message:msg preferredStyle:UIAlertControllerStyleAlert]; 231 | UIAlertAction * cancelAction = [UIAlertAction actionWithTitle:@"我知道了" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 232 | }]; 233 | UIAlertAction * action = [UIAlertAction actionWithTitle:@"重新选择" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 234 | [self insertVedioAction]; 235 | }]; 236 | [alert addAction:cancelAction]; 237 | [alert addAction:action]; 238 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 239 | [self presentViewController:alert animated:YES completion:nil]; 240 | }); 241 | } 242 | 243 | 244 | #pragma mark - overried 245 | 246 | - (void)backMenuPressed{ 247 | [self.navigationController popViewControllerAnimated:YES]; 248 | } 249 | 250 | - (void)sendMenuPressed{ 251 | 252 | } 253 | 254 | #pragma mark - keyboardWillShow 255 | 256 | - (void)observeKeyboard{ 257 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(adjustForKeyboard:) name:UIKeyboardWillHideNotification object:nil]; 258 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(adjustForKeyboard:) name:UIKeyboardWillShowNotification object:nil]; 259 | } 260 | 261 | - (void)adjustForKeyboard:(NSNotification *)notification{ 262 | CGRect keyboardScreenEndFrame = [(NSValue * )notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; 263 | CGRect keyboardViewEndFrame = [self.view convertRect:keyboardScreenEndFrame fromView:self.view.window]; 264 | 265 | /// 键盘高度会包含 inputAccessoryView 的高度 266 | self.kbHeight = keyboardViewEndFrame.size.height; 267 | if ([notification.name isEqualToString:UIKeyboardWillHideNotification]) { 268 | self.textView.contentInset = UIEdgeInsetsZero; 269 | }else{ 270 | self.textView.contentInset = UIEdgeInsetsMake(0, 0, keyboardViewEndFrame.size.height, 0); 271 | } 272 | 273 | self.textView.scrollIndicatorInsets = self.textView.contentInset; 274 | [self performSelector:@selector(scrollToCaret) withObject:nil afterDelay:0.01]; 275 | } 276 | 277 | - (void)scrollToCaret{ 278 | if (self.textView.isFirstResponder == NO) { 279 | return; 280 | } 281 | CGRect rect = [self.textView caretRectForPosition:self.textView.selectedTextRange.start]; 282 | [self.textView scrollRectToVisible:rect animated:YES]; 283 | } 284 | 285 | #pragma mark - NSTextStorageHookDelegate 286 | 287 | - (void)textStorageHook_processEditing:(NSTextStorage *)textStorage{ 288 | /// 输入高亮 289 | NSRange paragaphRange = [self.textView.textStorage.string paragraphRangeForRange: self.textView.textStorage.editedRange]; 290 | [self.textView.textStorage removeAttribute:NSForegroundColorAttributeName range:paragaphRange]; 291 | NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; 292 | style.lineSpacing = 10; 293 | [self.textView.textStorage addAttribute:NSParagraphStyleAttributeName value:style range:paragaphRange]; 294 | 295 | NSRegularExpression *kTopicExp = [NSRegularExpression regularExpressionWithPattern:@"#[^@#;<>\\s]+#" options:NSRegularExpressionCaseInsensitive error:nil]; 296 | [kTopicExp enumerateMatchesInString:self.textView.textStorage.string options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, self.textView.textStorage.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { 297 | if (!result) return; 298 | 299 | [self.textView.textStorage addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithRed:253/255.0 green:136/255.0 blue:43/255.0 alpha:1] range:result.range]; 300 | }]; 301 | 302 | 303 | 304 | // /// 仅高亮 插入内容,输入不高亮 305 | // NSRange paragaphRange = [self.textView.textStorage.string paragraphRangeForRange: self.textView.textStorage.editedRange]; 306 | // [self.textView.textStorage removeAttribute:NSForegroundColorAttributeName range:paragaphRange]; 307 | // 308 | // NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; 309 | // style.lineSpacing = 10; 310 | // [self.textView.textStorage addAttribute:NSParagraphStyleAttributeName value:style range:paragaphRange]; 311 | // 312 | // [self.textView.textStorage enumerateAttribute:NSAttributedStringHighlightingKey inRange:NSMakeRange(0, self.textView.textStorage.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { 313 | // if (value == nil) { 314 | // return ; 315 | // } 316 | // [self.textView.textStorage addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithRed:253/255.0 green:136/255.0 blue:43/255.0 alpha:1] range:range]; 317 | // }]; 318 | } 319 | 320 | #pragma mark - UIScrollViewDelegate 321 | 322 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ 323 | [self.view endEditing:YES]; 324 | self.textInputView.hidden = NO; 325 | } 326 | 327 | #pragma mark - setupNavView 328 | 329 | - (void)setupNavView { 330 | self.view.backgroundColor = UIColor.whiteColor; 331 | 332 | UIBarButtonItem * barButtonItem = nil; 333 | barButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"发送" 334 | style:UIBarButtonItemStylePlain 335 | target:self 336 | action:@selector(sendMenuPressed)]; 337 | barButtonItem.tintColor = UIColor.orangeColor; 338 | self.navigationItem.rightBarButtonItem = barButtonItem; 339 | 340 | 341 | barButtonItem = [[UIBarButtonItem alloc] initWithImage:[[UIImage imageNamed:@"cf_login_backBtn2"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] 342 | style:UIBarButtonItemStylePlain 343 | target:self 344 | action:@selector(backMenuPressed)]; 345 | self.navigationItem.leftBarButtonItem = barButtonItem; 346 | } 347 | 348 | 349 | #pragma mark - Getter 350 | 351 | - (UILabel *)placeholderLabel { 352 | if(_placeholderLabel == nil){ 353 | _placeholderLabel = [[UILabel alloc] init]; 354 | _placeholderLabel.font = [UIFont systemFontOfSize:15]; 355 | _placeholderLabel.textColor = [UIColor grayColor]; 356 | _placeholderLabel.text = @"分享游戏新鲜事"; 357 | } 358 | return _placeholderLabel; 359 | } 360 | 361 | - (RichTextView *)textView { 362 | if (_textView == nil){ 363 | NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; 364 | style.lineSpacing = 10; 365 | NSDictionary *defaultTypingAttributes = @{NSFontAttributeName:[UIFont systemFontOfSize:15], 366 | NSForegroundColorAttributeName:[UIColor blackColor], 367 | NSParagraphStyleAttributeName:style 368 | }; 369 | 370 | #warning 模拟从草稿加载 371 | DraftMetaDataModel *draftModel = nil; 372 | if (!self.isEmptyContent) { 373 | draftModel = [DraftMetaDataModel defaultDraftModel]; 374 | } 375 | 376 | _textView = [RichTextView richTextViewWithDraft:draftModel typingAttributes:defaultTypingAttributes]; 377 | _textView.textContainerInset = UIEdgeInsetsMake(0, 10, 20, 10); 378 | _textView.textContainer.lineFragmentPadding = 0; 379 | _textView.backgroundColor = UIColor.whiteColor; 380 | 381 | _textView.layer.borderColor = UIColor.redColor.CGColor; 382 | _textView.layer.borderWidth = 0.5f; 383 | 384 | _textView.delegate = self; 385 | _textView.attachmentDelegate = self; 386 | _textView.textStorage.hookDelegate = self; 387 | 388 | if (_textView.textStorage.length > 0 && [_textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { 389 | [_textView.delegate textViewDidChange:_textView]; 390 | } 391 | } 392 | return _textView; 393 | } 394 | 395 | - (RichTextInputAccessoryView *)textInputView { 396 | if (_textInputView == nil){ 397 | _textInputView = [[RichTextInputAccessoryView alloc] init]; 398 | } 399 | return _textInputView; 400 | } 401 | 402 | @end 403 | -------------------------------------------------------------------------------- /TTRichTextView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | NSMicrophoneUsageDescription 22 | 温馨提示:请允许使用麦克风!^_^ 23 | NSPhotoLibraryUsageDescription 24 | 温馨提示:请允许访问相册,才可以保存图片哦!^_^ 25 | NSCameraUsageDescription 26 | 温馨提示:请允许访问相机!^_^ 27 | NSPhotoLibraryAddUsageDescription 28 | 温馨提示:需要您的同意,才能保存图片到您的相册^_^ 29 | LSRequiresIPhoneOS 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UIRequiredDeviceCapabilities 36 | 37 | armv7 38 | 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /TTRichTextView/Utility/Define.h: -------------------------------------------------------------------------------- 1 | // 2 | // Define.h 3 | // 4 | // 5 | // Created by tbin on 2018/9/25. 6 | // Copyright © 2018年 bin. All rights reserved. 7 | // 8 | 9 | #ifndef Define_h 10 | #define Define_h 11 | 12 | #pragma mark - Frame 13 | 14 | /// 屏幕的宽度 15 | #define ScreenWidth [UIScreen mainScreen].bounds.size.width 16 | /// 屏幕的高度 17 | #define ScreenHeight [UIScreen mainScreen].bounds.size.height 18 | 19 | /// 状态栏高度,非刘海屏20,刘海屏44, 注:状态栏隐藏返回为0 20 | #define StatusBarHeight [[UIApplication sharedApplication] statusBarFrame].size.height 21 | 22 | /// 导航栏高度,非刘海屏64,刘海屏88 23 | #define TopBarHeight (StatusBarHeight + 44) 24 | 25 | /// 底部按钮距离高度 26 | #define iPhoneXSafeAreaBottom ( [UIApplication sharedApplication].statusBarFrame.size.height > 20 ? 34 : 0 ) 27 | 28 | /// 以iPhone6为标准,宽、高度自适应 29 | #define AutoSize(x) (x) *(ScreenWidth/375.0) 30 | #define AutoSizeRectMake(x,y,width,height) CGRectMake(AutoSize(x),AutoSize(y),AutoSize(width),AutoSize(height) 31 | #define AutoSizeSizeMake(width,height) CGSizeMake(AutoSize(width),AutoSize(height)) 32 | #define AutoSizePointMake(x,y) CGPointMake(AutoSize(x),AutoSize(y)) 33 | 34 | /// 宽度的倍率(苹果6) 35 | #define WidthScale ScreenWidth/375.0 36 | /// 高度的倍率(苹果6) 37 | #define HeightScale ScreenHeight/667.0 38 | 39 | #define FontSize(size) ([UIFont systemFontOfSize:size]) 40 | 41 | #pragma mark - Color 42 | #define RGBColor(r,g,b) RGBColorAlpha(r,g,b,1) 43 | #define RGBColorSame(x) RGBColorAlpha(x,x,x,1) 44 | #define RGBColorAlpha(r,g,b,a) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:(a)] 45 | 46 | #define HexColor(x) HexColorWithAlpha(x, 1) 47 | #define HexColorWithAlpha(x, a) [UIColor colorWithRed:((float)((x & 0xFF0000) >> 16))/255.0 green:((float)((x & 0xFF00) >> 8))/255.0 blue:((float)(x & 0xFF))/255.0 alpha:(a)] 48 | 49 | #define Text_blackColor HexColor(0x333333) 50 | #define Text_title_blackColor HexColor(0x161418) 51 | #define Text_grayColor [[UIColor blackColor] colorWithAlphaComponent:0.4] 52 | #define Text_orangeColor HexColor(0xfd882b) 53 | #define Text_gameGrayColor HexColor(0x999999) 54 | #define Text_textGrayColor HexColor(0x7f7f7f) 55 | 56 | 57 | 58 | #pragma mark - Font 59 | #define FontSize(f) [UIFont systemFontOfSize:AutoSize(f)] 60 | #define BoldFont(f) [UIFont boldSystemFontOfSize:AutoSize(f)] 61 | 62 | 63 | 64 | #pragma mark - Version 65 | //iOS系统版本 66 | #define SystemVersion ([[[UIDevice currentDevice] systemVersion] floatValue]) 67 | //app版本(用户可见) 68 | #define AppVersion [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] 69 | //app build版本 70 | #define AppBuildVersion [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] 71 | //app名称 72 | #define AppName [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"] 73 | //语言 74 | #define AppLanguage ([[NSLocale preferredLanguages] objectAtIndex:0]) 75 | 76 | 77 | 78 | #pragma mark - Path 79 | #define DocumentPath [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] 80 | #define LibraryPath [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0] 81 | #define CachesPath [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 82 | 83 | 84 | 85 | #pragma mark - Device 86 | /// 是否刘海屏。查了一下iPhone的官方报道说法,看到这么一个词:iPhoneX notch槽口,刻痕的意思。 87 | #define IsiPhoneNotch (StatusBarHeight == 44 ? YES : NO) 88 | 89 | #define iPhone4 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 960), [[UIScreen mainScreen] currentMode].size) : NO) 90 | 91 | #define iPhone5 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 1136), [[UIScreen mainScreen] currentMode].size) : NO) 92 | 93 | //判断iphone6 94 | #define iPhone6 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(750, 1334), [[UIScreen mainScreen] currentMode].size) : NO) 95 | 96 | // 判断iphone6+ 97 | #define iPhone6Plus ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1242, 2208), [[UIScreen mainScreen] currentMode].size) : NO) 98 | 99 | // 判断iPhoneX 100 | #define iPhoneX ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO) 101 | 102 | 103 | 104 | #pragma mark - 自定义 105 | /// WeakSelf 106 | #define WeakSelf WeakObj(self) 107 | #define WeakObj(type) __weak typeof(type) weak##type = type; 108 | 109 | /// strong WeakSelf 110 | #define StrongSelf StrongObj(self) 111 | #define StrongObj(type) __strong typeof(type) type = weak##type; 112 | 113 | 114 | #endif /* Define_h */ 115 | -------------------------------------------------------------------------------- /TTRichTextView/Utility/NSObject+Swizzle.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+Swizzle.h 3 | // 4 | // 5 | // Created by Bin on 16/2/2. 6 | // Copyright © 2016年 BKER-inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSObject (Swizzle) 12 | 13 | + (void) swizzleInstanceSelector:(SEL)originalSelector 14 | withNewSelector:(SEL)newSelector; 15 | 16 | + (void) swizzleClassSelector:(SEL)originalSelector 17 | withNewSelector:(SEL)newSelector; 18 | 19 | + (void) swizzleSelector:(SEL)originalSelector 20 | withNewSelector:(SEL)newSelector 21 | andNewIMP:(IMP)imp; 22 | @end 23 | -------------------------------------------------------------------------------- /TTRichTextView/Utility/NSObject+Swizzle.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+Swizzle.m 3 | // 4 | // 5 | // Created by Bin on 16/2/2. 6 | // Copyright © 2016年 BKER-inc. All rights reserved. 7 | // 8 | 9 | #import "NSObject+Swizzle.h" 10 | 11 | #import 12 | 13 | @implementation NSObject (Swizzle) 14 | 15 | + (void) swizzleInstanceSelector:(SEL)originalSelector 16 | withNewSelector:(SEL)newSelector 17 | { 18 | Method originalMethod = class_getInstanceMethod(self, originalSelector); 19 | Method newMethod = class_getInstanceMethod(self, newSelector); 20 | 21 | BOOL methodAdded = class_addMethod([self class], 22 | originalSelector, 23 | method_getImplementation(newMethod), 24 | method_getTypeEncoding(newMethod)); 25 | 26 | if (methodAdded) { 27 | class_replaceMethod([self class], 28 | newSelector, 29 | method_getImplementation(originalMethod), 30 | method_getTypeEncoding(originalMethod)); 31 | } else { 32 | method_exchangeImplementations(originalMethod, newMethod); 33 | } 34 | } 35 | 36 | + (void) swizzleClassSelector:(SEL)originalSelector 37 | withNewSelector:(SEL)newSelector 38 | { 39 | Method originalMethod = class_getClassMethod(self, originalSelector); 40 | Method newMethod = class_getClassMethod(self, newSelector); 41 | 42 | BOOL methodAdded = class_addMethod(object_getClass(self), 43 | originalSelector, 44 | method_getImplementation(newMethod), 45 | method_getTypeEncoding(newMethod)); 46 | 47 | if (methodAdded) { 48 | class_replaceMethod(object_getClass(self), 49 | newSelector, 50 | method_getImplementation(originalMethod), 51 | method_getTypeEncoding(originalMethod)); 52 | } else { 53 | method_exchangeImplementations(originalMethod, newMethod); 54 | } 55 | } 56 | 57 | + (void) swizzleSelector:(SEL)originalSelector 58 | withNewSelector:(SEL)newSelector 59 | andNewIMP:(IMP)imp{ 60 | 61 | Method originMethod = class_getInstanceMethod(self, originalSelector); 62 | const char * methodEncodeType = method_getTypeEncoding(originMethod); 63 | BOOL methodAdded = class_addMethod(self, newSelector, imp, methodEncodeType); 64 | 65 | if (methodAdded) { 66 | Method newMethod = class_getInstanceMethod(self,newSelector); 67 | method_exchangeImplementations(newMethod, originMethod); 68 | }else{ 69 | #ifdef DEBUG 70 | NSLog(@"===swizzleSelector==faile="); 71 | NSAssert(NO,@"===========swizzleSelector==faile========="); 72 | #endif 73 | } 74 | } 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /TTRichTextView/Utility/UIViewController+Base.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Base.h 3 | // 4 | // 5 | // Created by tbin on 2018/8/13. 6 | // Copyright © 2018年 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "NSObject+Swizzle.h" 11 | 12 | @protocol UIViewControllerDelegate 13 | 14 | @optional 15 | 16 | /** 17 | * 是否需要顶部导航栏 18 | * 19 | */ 20 | - (BOOL)isNeedTopNavView; 21 | 22 | 23 | /** 24 | * 顶部导航栏的高度 25 | * 26 | */ 27 | - (CGFloat)topNavViewHeight; 28 | 29 | /** 30 | * 左侧按钮的图片名称 31 | * 32 | * @return 图片名称 33 | */ 34 | - (NSString *)leftButtonImageName; 35 | 36 | /** 37 | * 右侧按钮的图片名称 38 | * 39 | * @return 图片名称 40 | */ 41 | - (NSString *)rightButtonImageName; 42 | 43 | /** 44 | * 右侧按钮的图片名称 45 | * 46 | * @return 图片名称 47 | */ 48 | - (NSString *)rightButtonTitle; 49 | 50 | /** 51 | * 导航栏左侧按钮点击触发的方法 52 | */ 53 | - (void)leftButtonClickedHandler; 54 | 55 | /** 56 | * 导航栏右侧按钮点击触发的方法 57 | */ 58 | - (void)rightButtonClickedHandler; 59 | 60 | @end 61 | 62 | @interface UIViewController (Base) 63 | 64 | /** 65 | * 导航栏视图 66 | */ 67 | @property (strong, nonatomic) UIView *topNavView; 68 | 69 | /** 70 | * 主视图 71 | */ 72 | @property (strong, nonatomic) UIView *contentView; 73 | 74 | /** 75 | * 导航栏左侧按钮 76 | */ 77 | @property (strong, nonatomic) UIButton *leftButton; 78 | 79 | /** 80 | * 导航栏预测按钮 81 | */ 82 | @property (strong, nonatomic) UIButton *rightButton; 83 | 84 | /** 85 | * 标题Label 86 | */ 87 | @property (strong, nonatomic) UILabel *titleLabel; 88 | 89 | /** 90 | * 导航栏和主视图分割线 91 | */ 92 | @property (strong, nonatomic) UIView *lineView; 93 | 94 | /** 95 | * 当前视图是否是POPUP形式 96 | */ 97 | @property (assign, nonatomic) BOOL isPopup; 98 | 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /TTRichTextView/Utility/UIViewController+Base.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Base.m 3 | // 4 | // 5 | // Created by tbin on 2018/8/13. 6 | // Copyright © 2018年 bin. All rights reserved. 7 | // 8 | 9 | #import "UIViewController+Base.h" 10 | #import 11 | 12 | static char const * const kTopNavView = "topNavView"; 13 | static char const * const kContentView = "contentView"; 14 | static char const * const kLeftButton = "LeftButton"; 15 | static char const * const kRightbutton = "rightButton"; 16 | static char const * const kTitleLabel = "titleLabel"; 17 | static char const * const kLineView = "lineView"; 18 | static char const * const kIsPopup = "isPopup"; 19 | static char const * const kChildDelegate = "ChildDelegate"; 20 | 21 | @interface UIViewController () 22 | 23 | @property (weak, nonatomic) id childDelegate; 24 | 25 | @end 26 | 27 | @implementation UIViewController (Base) 28 | 29 | - (instancetype)init_base 30 | { 31 | self = [self init_base]; 32 | if (self) { 33 | 34 | self.childDelegate = self; 35 | 36 | } 37 | return self; 38 | } 39 | 40 | #pragma mark - Hook 41 | 42 | - (void)base_viewDidLoad 43 | { 44 | BOOL isNeedTopNavView = NO; 45 | if ([self.childDelegate respondsToSelector:@selector(isNeedTopNavView)]) { 46 | isNeedTopNavView = [self.childDelegate isNeedTopNavView]; 47 | } 48 | 49 | if (isNeedTopNavView) { 50 | 51 | [self setupTopNavView]; 52 | 53 | [self setupContentView]; 54 | 55 | [self setupLeftButton]; 56 | 57 | [self setupTitleLabel]; 58 | 59 | [self setupRightButton]; 60 | 61 | [self setupLineView]; 62 | 63 | } 64 | 65 | [self base_viewDidLoad]; 66 | } 67 | 68 | #pragma mark - Button Click Event 69 | 70 | - (void)leftButtonClicked:(UIButton *)button 71 | { 72 | if ([self.childDelegate respondsToSelector:@selector(leftButtonClickedHandler)]) { 73 | [self.childDelegate leftButtonClickedHandler]; 74 | } 75 | } 76 | 77 | - (void)rightButtonClicked:(UIButton *)button 78 | { 79 | if ([self.childDelegate respondsToSelector:@selector(rightButtonClickedHandler)]) { 80 | [self.childDelegate rightButtonClickedHandler]; 81 | } 82 | } 83 | 84 | #pragma mark - SetupSubviews Method 85 | 86 | - (void)setupTopNavView 87 | { 88 | CGFloat topNavViewHeight = 0; 89 | 90 | if ([self.childDelegate respondsToSelector:@selector(topNavViewHeight)]) { 91 | topNavViewHeight = [self.childDelegate topNavViewHeight]; 92 | } 93 | 94 | CGFloat width = [UIScreen mainScreen].bounds.size.width; 95 | CGFloat statusBarH = [UIApplication sharedApplication].statusBarFrame.size.height; 96 | CGFloat height = statusBarH + topNavViewHeight; 97 | 98 | UIView *topNavView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, height)]; 99 | topNavView.backgroundColor = [UIColor clearColor]; 100 | [self.view addSubview:topNavView]; 101 | 102 | self.topNavView = topNavView; 103 | } 104 | 105 | - (void)setupContentView 106 | { 107 | CGFloat width = [UIScreen mainScreen].bounds.size.width; 108 | CGFloat topNavViewHeight = self.topNavView.bounds.size.height; 109 | // CGFloat safeAreaBottom = [UIApplication sharedApplication].statusBarFrame.size.height > 20 ? 34 : 0 ; 110 | CGFloat height = [UIScreen mainScreen].bounds.size.height - topNavViewHeight; 111 | 112 | UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, topNavViewHeight, width, height)]; 113 | contentView.backgroundColor = [UIColor clearColor]; 114 | [self.view addSubview:contentView]; 115 | 116 | self.contentView = contentView; 117 | } 118 | 119 | - (void)setupLeftButton 120 | { 121 | NSString *leftButtonImageName = @""; 122 | 123 | if ([self.childDelegate respondsToSelector:@selector(leftButtonImageName)]) { 124 | leftButtonImageName = [self.childDelegate leftButtonImageName]; 125 | } 126 | 127 | UIImage *leftButtonImage = [UIImage imageNamed:leftButtonImageName]; 128 | 129 | if (!leftButtonImage) { 130 | return; 131 | } 132 | 133 | CGFloat width = leftButtonImage.size.width; 134 | CGFloat height = leftButtonImage.size.height; 135 | 136 | CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height; 137 | CGFloat y = (self.topNavView.bounds.size.height - statusBarHeight - height) / 2 + statusBarHeight; 138 | 139 | UIButton *leftButton = [[UIButton alloc] initWithFrame:CGRectMake(0, y, width, height)]; 140 | leftButton.backgroundColor = [UIColor clearColor]; 141 | [leftButton setImage:leftButtonImage forState:UIControlStateNormal]; 142 | [leftButton addTarget:self action:@selector(leftButtonClicked:) forControlEvents:UIControlEventTouchUpInside]; 143 | [self.topNavView addSubview:leftButton]; 144 | 145 | self.leftButton = leftButton; 146 | } 147 | 148 | - (void)setupRightButton 149 | { 150 | NSString *rightButtonImageName = @""; 151 | 152 | if ([self.childDelegate respondsToSelector:@selector(rightButtonImageName)]) { 153 | rightButtonImageName = [self.childDelegate rightButtonImageName]; 154 | } 155 | 156 | UIImage *rightButtonImage = [UIImage imageNamed:rightButtonImageName]; 157 | 158 | if (!rightButtonImage) { 159 | return; 160 | } 161 | 162 | CGFloat width = 30; 163 | CGFloat height = 30; 164 | 165 | CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height; 166 | CGFloat y = (self.topNavView.bounds.size.height - statusBarHeight - height) / 2 + statusBarHeight; 167 | 168 | CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; 169 | CGFloat x = screenWidth - width; 170 | 171 | UIButton *rightButton = [[UIButton alloc] initWithFrame:CGRectMake(x, y, width, height)]; 172 | rightButton.backgroundColor = [UIColor clearColor]; 173 | [rightButton setImage:rightButtonImage forState:UIControlStateNormal]; 174 | [rightButton addTarget:self action:@selector(rightButtonClicked:) forControlEvents:UIControlEventTouchUpInside]; 175 | [self.topNavView addSubview:rightButton]; 176 | 177 | self.rightButton = rightButton; 178 | } 179 | 180 | - (void)setupTitleLabel 181 | { 182 | CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height; 183 | CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; 184 | 185 | CGFloat x = self.leftButton.bounds.size.width + 10; 186 | CGFloat y = statusBarHeight; 187 | CGFloat width = screenWidth - x * 2; 188 | CGFloat height = self.topNavView.bounds.size.height - statusBarHeight; 189 | 190 | UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(x, y, width, height)]; 191 | titleLabel.textAlignment = NSTextAlignmentCenter; 192 | titleLabel.textColor = [UIColor blackColor]; 193 | titleLabel.backgroundColor = [UIColor clearColor]; 194 | [self.topNavView addSubview:titleLabel]; 195 | 196 | self.titleLabel = titleLabel; 197 | } 198 | 199 | - (void)setupLineView 200 | { 201 | CGFloat width = [UIScreen mainScreen].bounds.size.width; 202 | CGFloat height = 0.5; 203 | CGFloat x = 0; 204 | CGFloat y = self.topNavView.bounds.size.height - height; 205 | UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(x, y, width, height)]; 206 | lineView.backgroundColor = [UIColor clearColor]; 207 | [self.topNavView addSubview:lineView]; 208 | 209 | self.lineView = lineView; 210 | } 211 | 212 | 213 | #pragma mark - Swizzle Method 214 | 215 | + (void)load { 216 | [self swizzleInstanceSelector:@selector(viewDidLoad) withNewSelector:@selector(base_viewDidLoad)]; 217 | [self swizzleInstanceSelector:@selector(init) withNewSelector:@selector(init_base)]; 218 | } 219 | 220 | #pragma mark - Setter Method 221 | 222 | - (void)setTopNavView:(UIView *)topNavView 223 | { 224 | if (topNavView != self.topNavView) { 225 | objc_setAssociatedObject(self, &kTopNavView, topNavView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 226 | } 227 | } 228 | 229 | - (void)setContentView:(UIView *)contentView 230 | { 231 | if (contentView != self.contentView) { 232 | objc_setAssociatedObject(self, &kContentView, contentView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 233 | } 234 | } 235 | 236 | - (void)setLeftButton:(UIButton *)leftButton 237 | { 238 | if (leftButton != self.leftButton) { 239 | objc_setAssociatedObject(self, &kLeftButton, leftButton, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 240 | } 241 | } 242 | 243 | - (void)setRightButton:(UIButton *)rightButton 244 | { 245 | if (rightButton != self.rightButton) { 246 | objc_setAssociatedObject(self, &kRightbutton, rightButton, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 247 | } 248 | } 249 | 250 | - (void)setTitleLabel:(UILabel *)titleLabel 251 | { 252 | if (titleLabel != self.titleLabel) { 253 | objc_setAssociatedObject(self, &kTitleLabel, titleLabel, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 254 | } 255 | } 256 | 257 | - (void)setLineView:(UIView *)lineView 258 | { 259 | if (lineView != self.lineView) { 260 | objc_setAssociatedObject(self, &kLineView, lineView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 261 | } 262 | } 263 | 264 | - (void)setIsPopup:(BOOL)isPopup 265 | { 266 | objc_setAssociatedObject(self, &kIsPopup, @(isPopup), OBJC_ASSOCIATION_ASSIGN); 267 | } 268 | 269 | - (void)setChildDelegate:(id)childDelegate 270 | { 271 | if (![childDelegate isEqual:self.childDelegate]) { 272 | objc_setAssociatedObject(self, &kChildDelegate, childDelegate, OBJC_ASSOCIATION_ASSIGN); 273 | } 274 | } 275 | 276 | 277 | #pragma mark - Getter Method 278 | 279 | - (UIView *)topNavView 280 | { 281 | return objc_getAssociatedObject(self, &kTopNavView); 282 | } 283 | 284 | - (UIView *)contentView 285 | { 286 | return objc_getAssociatedObject(self, &kContentView); 287 | } 288 | 289 | - (UIButton *)leftButton 290 | { 291 | return objc_getAssociatedObject(self, &kLeftButton); 292 | } 293 | 294 | - (UIButton *)rightButton 295 | { 296 | return objc_getAssociatedObject(self, &kRightbutton); 297 | } 298 | 299 | - (UILabel *)titleLabel 300 | { 301 | return objc_getAssociatedObject(self, &kTitleLabel); 302 | } 303 | 304 | - (UIView *)lineView 305 | { 306 | return objc_getAssociatedObject(self, &kLineView); 307 | } 308 | 309 | - (BOOL)isPopup 310 | { 311 | return [objc_getAssociatedObject(self, &kIsPopup) boolValue]; 312 | } 313 | 314 | - (id)childDelegate 315 | { 316 | return objc_getAssociatedObject(self, &kChildDelegate); 317 | } 318 | 319 | @end 320 | -------------------------------------------------------------------------------- /TTRichTextView/Utility/UIViewController+XF.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+XF.h 3 | // 4 | // 5 | // Created by Bin on 2018/4/19. 6 | // Copyright © 2018年 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import 10 | # import "UIViewController+Base.h" 11 | 12 | @interface UIViewController (XF) 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /TTRichTextView/Utility/UIViewController+XF.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+XF.m 3 | // 4 | // 5 | // Created by Bin on 2018/4/19. 6 | // Copyright © 2018年 bin. All rights reserved. 7 | // 8 | 9 | #import "UIViewController+XF.h" 10 | 11 | @implementation UIViewController (XF) 12 | 13 | + (void)load 14 | { 15 | [self swizzleInstanceSelector:@selector(viewDidLoad) withNewSelector:@selector(guide_viewDidLoad)]; 16 | 17 | [self swizzleInstanceSelector:@selector(viewWillAppear:) withNewSelector:@selector(guide_viewWillApper:)]; 18 | } 19 | 20 | 21 | #pragma mark - Hood Method 22 | 23 | - (void)guide_viewDidLoad 24 | { 25 | [self guide_viewDidLoad]; 26 | 27 | BOOL isNeedTopNavView = NO; 28 | if ([self respondsToSelector:@selector(isNeedTopNavView)]) { 29 | isNeedTopNavView = [self isNeedTopNavView]; 30 | } 31 | 32 | if (isNeedTopNavView) { 33 | [self setupTopNavViewCustom]; 34 | [self setupTopNavViewConstraints]; 35 | } 36 | } 37 | 38 | 39 | - (void)guide_viewWillApper:(BOOL)animated 40 | { 41 | if ([self respondsToSelector:@selector(isNeedTopNavView)]) { 42 | NSLog(@"当前显示的VC的名称为:%@",NSStringFromClass(self.class)); 43 | [[UIApplication sharedApplication].delegate performSelector:@selector(setViewController:) withObject:self]; 44 | } 45 | 46 | [self guide_viewWillApper:animated]; 47 | 48 | if ([self respondsToSelector:@selector(isNeedTopNavView)]) { 49 | [self.navigationController setNavigationBarHidden:[self isNeedTopNavView] animated:animated]; 50 | } 51 | 52 | 53 | } 54 | 55 | #pragma mark - Private Method 56 | 57 | - (void)setupTopNavViewCustom 58 | { 59 | self.titleLabel.font = [UIFont systemFontOfSize:14]; 60 | self.titleLabel.textColor = [UIColor blackColor]; 61 | 62 | self.topNavView.backgroundColor = [UIColor whiteColor]; 63 | self.lineView.backgroundColor = [UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]; 64 | } 65 | 66 | - (void)setupTopNavViewConstraints 67 | { 68 | CGFloat statusBarH = [UIApplication sharedApplication].statusBarFrame.size.height; 69 | 70 | self.lineView.frame = CGRectMake(0, CGRectGetHeight(self.topNavView.frame) - 0.5,CGRectGetWidth(self.topNavView.frame) , 0.5); 71 | 72 | self.leftButton.frame = CGRectMake(0, CGRectGetMinY(self.topNavView.frame) + statusBarH , 45, CGRectGetHeight(self.topNavView.frame) - statusBarH - CGRectGetHeight(self.lineView.frame)); 73 | 74 | self.titleLabel.frame = CGRectMake( CGRectGetMaxX(self.leftButton.frame) + 5,CGRectGetMinY(self.topNavView.frame) + statusBarH, self.view.frame.size.width - 100, CGRectGetHeight(self.topNavView.frame) - statusBarH - CGRectGetHeight(self.lineView.frame)); 75 | 76 | self.rightButton.frame = CGRectMake( CGRectGetWidth(self.topNavView.frame) - 45, CGRectGetMinY(self.leftButton.frame), 45, CGRectGetHeight( self.leftButton.frame)); 77 | 78 | 79 | self.rightButton.layer.borderColor = [UIColor redColor].CGColor; 80 | self.rightButton.layer.borderWidth = 0.5f; 81 | 82 | self.leftButton.layer.borderColor = [UIColor redColor].CGColor; 83 | self.leftButton.layer.borderWidth = 0.5f; 84 | 85 | self.titleLabel.layer.borderColor = [UIColor redColor].CGColor; 86 | self.titleLabel.layer.borderWidth = 0.5f; 87 | } 88 | 89 | #pragma mark - UIViewControllerDelegate 90 | 91 | - (CGFloat)topNavViewHeight 92 | { 93 | return 44; 94 | } 95 | 96 | - (NSString *)leftButtonImageName 97 | { 98 | return self.isPopup ? @"" : @"cf_login_backBtn2"; 99 | } 100 | 101 | - (NSString *)rightButtonImageName 102 | { 103 | return @""; 104 | } 105 | 106 | - (void)leftButtonClickedHandler 107 | { 108 | if(self.navigationController.viewControllers.count>1){//如果当前栈缓存大于1个VC,则默认pop 109 | [self.navigationController popViewControllerAnimated:YES]; 110 | }else{//否则dismiss回上一个级 111 | [self.navigationController dismissViewControllerAnimated:YES completion:NULL]; 112 | } 113 | } 114 | 115 | @end 116 | -------------------------------------------------------------------------------- /TTRichTextView/View/AudioAttachmentReusableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // AudioAttachmentReusableView.h 3 | // HOHO 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import "TextAttachmentReusableView.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface AudioAttachmentReusableView : TextAttachmentReusableView 14 | 15 | @property (nonatomic,strong,readonly) UIImageView *imgView; 16 | 17 | @property (nonatomic,strong,readonly) UILabel *leftLb; 18 | 19 | @property (nonatomic,strong,readonly) UILabel *rightLb; 20 | 21 | @property (nonatomic,strong,readonly) UIProgressView *progressView; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /TTRichTextView/View/AudioAttachmentReusableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // AudioAttachmentReusableView.m 3 | // HOHO 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import "AudioAttachmentReusableView.h" 10 | #import "RichTextTools.h" 11 | #import 12 | 13 | @interface AudioAttachmentReusableView() 14 | 15 | @property (nonatomic,strong) UIImageView *imgView; 16 | 17 | @property (nonatomic,strong) UIButton *playerBtn; 18 | 19 | @property (nonatomic,strong) UILabel *leftLb; 20 | 21 | @property (nonatomic,strong) UILabel *rightLb; 22 | 23 | @property (nonatomic,strong) UIProgressView *progressView; 24 | 25 | @end 26 | 27 | @implementation AudioAttachmentReusableView 28 | 29 | - (UIButton *)playerBtn{ 30 | if (!_playerBtn) { 31 | _playerBtn = [[UIButton alloc] init]; 32 | [_playerBtn setImage:XFRichTextImage(@"movie_play") forState:UIControlStateNormal]; 33 | } 34 | return _playerBtn; 35 | } 36 | 37 | - (UILabel *)leftLb{ 38 | if (!_leftLb) { 39 | _leftLb = [UILabel new]; 40 | _leftLb.font = [UIFont systemFontOfSize:10]; 41 | _leftLb.textColor = [UIColor blackColor]; 42 | _leftLb.text = @"00:00"; 43 | } 44 | return _leftLb; 45 | } 46 | 47 | - (UILabel *)rightLb{ 48 | if (!_rightLb) { 49 | _rightLb = [UILabel new]; 50 | _rightLb.font = [UIFont systemFontOfSize:10]; 51 | _rightLb.textColor = [UIColor blackColor]; 52 | _rightLb.text = @"00:05"; 53 | } 54 | return _rightLb; 55 | } 56 | 57 | - (UIProgressView *)progressView{ 58 | if (!_progressView) { 59 | _progressView = [[UIProgressView alloc] init]; 60 | //设置进度条的颜色 61 | _progressView.progressTintColor = [UIColor orangeColor]; 62 | //设置进度条的当前值,范围:0~1; 63 | _progressView.progress = 0.0; 64 | _progressView.progressViewStyle = UIProgressViewStyleDefault; 65 | } 66 | return _progressView; 67 | } 68 | 69 | - (instancetype)initAttachmentReusableView:(NSString *)identifier{ 70 | self = [super initAttachmentReusableView:identifier]; 71 | if (self) { 72 | self.backgroundColor = [UIColor cyanColor]; 73 | 74 | [self addSubview:self.playerBtn]; 75 | [self addSubview:self.leftLb]; 76 | [self addSubview:self.rightLb]; 77 | [self addSubview:self.progressView]; 78 | 79 | [self.playerBtn mas_makeConstraints:^(MASConstraintMaker *make) { 80 | make.centerY.equalTo(self.mas_centerY); 81 | make.left.equalTo(self.mas_left).offset(5); 82 | }]; 83 | 84 | [self.leftLb mas_makeConstraints:^(MASConstraintMaker *make) { 85 | make.left.equalTo(self.playerBtn.mas_right).offset(5); 86 | make.centerY.equalTo(self.mas_centerY); 87 | }]; 88 | 89 | [self.progressView mas_makeConstraints:^(MASConstraintMaker *make) { 90 | make.left.equalTo(self.leftLb.mas_right).offset(2); 91 | make.centerY.equalTo(self.mas_centerY); 92 | make.right.equalTo(self.rightLb.mas_left).offset(-2); 93 | }]; 94 | 95 | [self.rightLb mas_makeConstraints:^(MASConstraintMaker *make) { 96 | make.right.equalTo(self.mas_right).offset(-5); 97 | make.centerY.equalTo(self.mas_centerY); 98 | }]; 99 | } 100 | return self; 101 | } 102 | 103 | 104 | - (UIImageView *)imgView{ 105 | if (!_imgView) { 106 | _imgView = [[UIImageView alloc] init]; 107 | _imgView.backgroundColor = [UIColor yellowColor]; 108 | } 109 | return _imgView; 110 | } 111 | @end 112 | -------------------------------------------------------------------------------- /TTRichTextView/View/GifAttachmentReusableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // GifAttachmentReusableView.h 3 | // HOHO 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import "TextAttachmentReusableView.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface GifAttachmentReusableView : TextAttachmentReusableView 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /TTRichTextView/View/GifAttachmentReusableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // GifAttachmentReusableView.m 3 | // HOHO 4 | // 5 | // Created by tbin on 2018/11/21. 6 | // Copyright © 2018 ThinkFly. All rights reserved. 7 | // 8 | 9 | #import "GifAttachmentReusableView.h" 10 | #import "YYAnimatedImageView.h" 11 | #import "RichTextTools.h" 12 | #import 13 | #import 14 | #import 15 | 16 | @implementation GifAttachmentReusableView 17 | 18 | - (instancetype)initAttachmentReusableView:(NSString *)identifier{ 19 | self = [super initAttachmentReusableView:identifier]; 20 | if (self) { 21 | 22 | YYImage *image = [YYImage imageNamed:@"niconiconi"]; 23 | YYAnimatedImageView *imgView = [[YYAnimatedImageView alloc] initWithImage:image]; 24 | [self addSubview:imgView]; 25 | 26 | [imgView mas_makeConstraints:^(MASConstraintMaker *make) { 27 | make.centerX.centerY.equalTo(self); 28 | make.width.equalTo(self.mas_width); 29 | make.height.equalTo(self.mas_height); 30 | }]; 31 | } 32 | return self; 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /TTRichTextView/View/RichTextInputAccessoryView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextInputView.h 3 | // RichText 4 | // 5 | // Created by bin on 2018/7/15. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface RichTextInputAccessoryView : UIView 14 | 15 | @property (nonatomic,strong,readonly) UIButton *imgBtn; 16 | @property (nonatomic,strong,readonly) UIButton *vedioBtn; 17 | @property (nonatomic,strong,readonly) UIButton *tagBtn; 18 | 19 | @property (nonatomic,strong,readonly) UIButton *audioBtn; 20 | @property (nonatomic,strong,readonly) UIButton *anyBtn; 21 | @property (nonatomic,strong,readonly) UIButton *bBtn; 22 | 23 | @property (assign, nonatomic) NSInteger inputAccessoryViewHeight; 24 | 25 | @property (assign, nonatomic) NSInteger maxTextLength; 26 | 27 | @property (assign, nonatomic) NSInteger currentTextLength; 28 | 29 | 30 | @end 31 | 32 | NS_ASSUME_NONNULL_END 33 | -------------------------------------------------------------------------------- /TTRichTextView/View/RichTextInputAccessoryView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextInputView.m 3 | // RichText 4 | // 5 | // Created by bin on 2018/7/15. 6 | // Copyright © 2018年 me.test. All rights reserved. 7 | // 8 | 9 | #import "RichTextInputAccessoryView.h" 10 | #import "Masonry.h" 11 | #import "TZImagePickerController.h" 12 | #import 13 | @interface RichTextInputAccessoryView() 14 | 15 | @property (nonatomic,strong) UIButton *imgBtn; 16 | @property (nonatomic,strong) UIButton *vedioBtn; 17 | @property (nonatomic,strong) UIButton *tagBtn; 18 | 19 | @property (nonatomic,strong) UIButton *audioBtn; 20 | @property (nonatomic,strong) UIButton *anyBtn; 21 | @property (nonatomic,strong) UIButton *bBtn; 22 | 23 | @property (strong, nonatomic) UILabel *textLengthLabel; 24 | 25 | @end 26 | 27 | @implementation RichTextInputAccessoryView 28 | 29 | - (instancetype)init 30 | { 31 | return [self initWithFrame:CGRectZero]; 32 | } 33 | 34 | - (instancetype)initWithFrame:(CGRect)frame 35 | { 36 | self = [super initWithFrame:frame]; 37 | if (self) { 38 | self.frame = frame; 39 | _inputAccessoryViewHeight = 45; 40 | _maxTextLength = 500; 41 | 42 | self.autoresizingMask = UIViewAutoresizingFlexibleHeight; 43 | self.backgroundColor = [UIColor colorWithRed:242/255.0 green:242/255.0 blue:242/255.0 alpha:1]; 44 | 45 | [self addSubview: self.imgBtn]; 46 | [self addSubview: self.vedioBtn]; 47 | [self addSubview: self.tagBtn]; 48 | [self addSubview:self.audioBtn]; 49 | [self addSubview: self.anyBtn]; 50 | [self addSubview: self.bBtn]; 51 | 52 | [self.imgBtn mas_makeConstraints:^(MASConstraintMaker *make) { 53 | make.height.top.equalTo(self); 54 | make.left.equalTo(self).offset(10); 55 | }]; 56 | 57 | [self.vedioBtn mas_makeConstraints:^(MASConstraintMaker *make) { 58 | make.height.top.equalTo(self); 59 | make.left.equalTo(self.imgBtn.mas_right).offset(30); 60 | }]; 61 | 62 | [self.tagBtn mas_makeConstraints:^(MASConstraintMaker *make) { 63 | make.height.top.equalTo(self); 64 | make.left.equalTo(self.vedioBtn.mas_right).offset(30); 65 | }]; 66 | 67 | [self.audioBtn mas_makeConstraints:^(MASConstraintMaker *make) { 68 | make.height.top.equalTo(self); 69 | make.left.equalTo(self.tagBtn.mas_right).offset(30); 70 | }]; 71 | 72 | [self.anyBtn mas_makeConstraints:^(MASConstraintMaker *make) { 73 | make.height.top.equalTo(self); 74 | make.left.equalTo(self.audioBtn.mas_right).offset(30); 75 | }]; 76 | 77 | [self.bBtn mas_makeConstraints:^(MASConstraintMaker *make) { 78 | make.height.top.equalTo(self); 79 | make.left.equalTo(self.anyBtn.mas_right).offset(30); 80 | }]; 81 | 82 | } 83 | return self; 84 | } 85 | 86 | 87 | - (CGSize)intrinsicContentSize { 88 | return CGSizeMake([UIScreen mainScreen].bounds.size.width, self.inputAccessoryViewHeight); 89 | } 90 | 91 | 92 | - (void)setCurrentTextLength:(NSInteger)currentTextLength 93 | { 94 | _currentTextLength = currentTextLength; 95 | self.textLengthLabel.text = [NSString stringWithFormat:@"%@/%@",@(currentTextLength),@(_maxTextLength)]; 96 | CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; 97 | CGSize size = [self.textLengthLabel sizeThatFits:CGSizeMake(screenWidth/3, 100)]; 98 | self.textLengthLabel.size = size; 99 | self.textLengthLabel.center = CGPointMake(screenWidth-self.textLengthLabel.width/2-10, self.height/2 + 5); 100 | } 101 | 102 | 103 | 104 | 105 | /// fix iPhone X 106 | /// https://stackoverflow.com/questions/46282987/iphone-x-how-to-handle-view-controller-inputaccessoryview 107 | - (void)didMoveToWindow{ 108 | [super didMoveToWindow]; 109 | if (@available(iOS 11.0, *)) { 110 | if (self.window.safeAreaLayoutGuide != nil) { 111 | [[self.bottomAnchor constraintLessThanOrEqualToSystemSpacingBelowAnchor:[self.window.safeAreaLayoutGuide bottomAnchor] multiplier:1.0] setActive:YES]; 112 | } 113 | } 114 | } 115 | 116 | 117 | - (UIButton *)imgBtn{ 118 | if (_imgBtn == nil){ 119 | _imgBtn = [[UIButton alloc] init]; 120 | [_imgBtn setImage:[UIImage imageNamed:@"cf_richText_tool_image"] forState:UIControlStateNormal]; 121 | [_imgBtn setImage:[UIImage imageNamed:@"cf_richText_tool_image_unenable"] forState:UIControlStateDisabled]; 122 | 123 | } 124 | return _imgBtn; 125 | } 126 | 127 | - (UIButton *)vedioBtn{ 128 | if (_vedioBtn == nil){ 129 | _vedioBtn = [[UIButton alloc] init]; 130 | [_vedioBtn setImage:[UIImage imageNamed:@"cf_richText_tool_video_normal"] forState:UIControlStateNormal]; 131 | [_vedioBtn setImage:[UIImage imageNamed:@"cf_richText_tool_video_un"] forState:UIControlStateDisabled]; 132 | 133 | } 134 | return _vedioBtn; 135 | } 136 | 137 | - (UIButton *)tagBtn{ 138 | if (_tagBtn == nil){ 139 | _tagBtn = [[UIButton alloc] init]; 140 | [_tagBtn setImage:[UIImage imageNamed:@"rich_topic_icon"] forState:UIControlStateNormal]; 141 | } 142 | return _tagBtn; 143 | } 144 | 145 | - (UIButton *)anyBtn{ 146 | if (!_anyBtn) { 147 | _anyBtn = [[UIButton alloc] init]; 148 | [_anyBtn setTitle:@"gif" forState:UIControlStateNormal]; 149 | [_anyBtn setTitleColor:UIColor.orangeColor forState:UIControlStateNormal]; 150 | } 151 | return _anyBtn; 152 | } 153 | 154 | - (UIButton *)audioBtn{ 155 | if (!_audioBtn) { 156 | _audioBtn = [[UIButton alloc] init]; 157 | [_audioBtn setTitle:@"audio" forState:UIControlStateNormal]; 158 | [_audioBtn setTitleColor:UIColor.orangeColor forState:UIControlStateNormal]; 159 | } 160 | return _audioBtn; 161 | } 162 | 163 | 164 | - (UIButton *)bBtn{ 165 | if (!_bBtn) { 166 | _bBtn = [[UIButton alloc] init]; 167 | [_bBtn setTitle:@"B" forState:UIControlStateNormal]; 168 | [_bBtn setTitleColor:UIColor.orangeColor forState:UIControlStateNormal]; 169 | } 170 | return _bBtn; 171 | } 172 | 173 | - (UILabel *)textLengthLabel 174 | { 175 | if(!_textLengthLabel){ 176 | _textLengthLabel = [UILabel new]; 177 | _textLengthLabel.textColor = [UIColor redColor]; 178 | _textLengthLabel.font = [UIFont systemFontOfSize:10]; 179 | [self addSubview:_textLengthLabel]; 180 | } 181 | return _textLengthLabel; 182 | } 183 | 184 | @end 185 | 186 | -------------------------------------------------------------------------------- /TTRichTextView/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /TTRichTextView/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "RichTextViewController.h" 11 | #import "DraftListViewController.h" 12 | #import "PostRichEditViewController.h" 13 | 14 | @interface ViewController () 15 | 16 | @property (nonatomic,strong) UITableView *tableView; 17 | 18 | @end 19 | 20 | @implementation ViewController 21 | 22 | - (void)viewDidLoad { 23 | [super viewDidLoad]; 24 | // Do any additional setup after loading the view. 25 | 26 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds]; 27 | _tableView.dataSource = self; 28 | _tableView.delegate = self; 29 | [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"demoCell"]; 30 | [self.view addSubview:self.tableView]; 31 | } 32 | 33 | 34 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ 35 | return 80; 36 | } 37 | 38 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ 39 | return 3; 40 | } 41 | 42 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 43 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"demoCell"]; 44 | if (indexPath.row == 0) { 45 | cell.textLabel.text = @"编辑器-html"; 46 | 47 | }else if(indexPath.row == 1){ 48 | 49 | cell.textLabel.text = @"编辑器-empty"; 50 | 51 | }else{ 52 | cell.textLabel.text = @"草稿"; 53 | } 54 | return cell; 55 | } 56 | 57 | 58 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 59 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 60 | 61 | 62 | if (indexPath.row == 0) { 63 | PostRichEditViewController *vc = [PostRichEditViewController new]; 64 | [self.navigationController pushViewController:vc animated:YES]; 65 | }else if(indexPath.row == 1){ 66 | 67 | PostRichEditViewController *vc = [PostRichEditViewController new]; 68 | vc.isEmptyContent = YES; 69 | [self.navigationController pushViewController:vc animated:YES]; 70 | 71 | }else{ 72 | DraftListViewController *vc = [DraftListViewController new]; 73 | [self.navigationController pushViewController:vc animated:YES]; 74 | } 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /TTRichTextView/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // TTRichTextView 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TTRichTextView/niconiconi@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/TTRichTextView/niconiconi@2x.gif -------------------------------------------------------------------------------- /TTRichTextViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TTRichTextViewTests/TTRichTextViewTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TTRichTextViewTests.m 3 | // TTRichTextViewTests 4 | // 5 | // Created by bin on 2019/7/30. 6 | // Copyright © 2019 xp.bin.pro. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface TTRichTextViewTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation TTRichTextViewTests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | - (void)tearDown { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | - (void)testExample { 26 | // This is an example of a functional test case. 27 | // Use XCTAssert and related functions to verify your tests produce the correct results. 28 | } 29 | 30 | - (void)testPerformanceExample { 31 | // This is an example of a performance test case. 32 | [self measureBlock:^{ 33 | // Put the code you want to measure the time of here. 34 | }]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /richtext.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/richtext.gif -------------------------------------------------------------------------------- /richtext_empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githhhh/TTRichTextView/e3b82ea4eab4771f9c97a9cbf9fedc8438b4b3a4/richtext_empty.gif --------------------------------------------------------------------------------