├── .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 | 
5 | 
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
--------------------------------------------------------------------------------