iOS使用UITableView实现的富文本编辑器

简介:
本文讲的是 iOS使用UITableView实现的富文本编辑器, 公司最近做一个项目,其中有一个模块是富文本编辑模块,之前没做个类似的功能模块,本来以为这个功能很常见应该会有已经造好的轮子,或许我只要找到轮子,研究下轮子,然后修改打磨轮子,这件事就八九不离十了。不过,还是 too young to simple 了,有些事,还是得自己去面对的,或许这就叫做成长,感觉最近一年,对于编程这件事,更多了一点热爱,我感觉我不配过只会复制粘贴代码的人生,编程需要有挑战。所以,遇到困难,保持一份正念,路其实就在脚下,如果没有困难,那就制造困哪,迎难而上,人生没有白走的路,每一步都算数,毒鸡汤就到此为止,下面是干货了。

结果

没图没真相,下面是几张实现的效果图

实现的功能包含了:

  • 编辑器文字编辑
  • 编辑器图片编辑
  • 编辑器图文混排编辑
  • 编辑器图片上传,带有进度和失败提示,可以重新上传操作
  • 编辑器模型转换为HTML格式内容
  • 配套的Java实现的服务器

以及客户端代码开源托管地址:RichTextEditDemo

还有java实现的文件服务器代码开源托管地址:javawebserverdemo

调研分析

基本上有以下几种的实现方案:

  1. UITextView结合NSAttributeString实现图文混排编辑,这个方案可以在网上找到对应的开源代码,比如 SimpleWord 的实现就是使用这种方式,不过缺点是图片不能有交互,比如说在图片上添加进度条,添加上传失败提示,图片点击事件处理等等都不行,如果没有这种需求那么可以选择这种方案。
  2. 使用WebView通过js和原生的交互实现,比如 WordPress-Editor 、RichTextDemo ,主要的问题就是性能不够好,还有需要你懂得前端知识才能上手。
  3. 使用CoreText或者TextKit,这种也有实现方案的开源代码,比如说这个 YYText ,这个很有名气,不过他使用的图片插入编辑图片的位置是固定的,文字是围绕着图片,所以这种不符合我的要求,如果要使用这种方案,那修改的地方有很多,并且CoreText/TextKit使用是有一定的门槛的。
  4. 使用UITableView结合UITextView的假实现,主要的思路是每个Cell是一个文字输入的UITextView或者是用于显示图片使用的UITextView,图片显示之所以是选择UITextView是因为图片位置需要有输入光标,所以使用UITextView结合NSAttributeString的方式正好可以实现这个功能。图片和文字混排也就是显示图片的Cell和显示文字的Cell混排就可以实现了,主要的工作量是处理光标位置输入以及处理光标位置删除。

选型定型

前面三种方案都有了开源的实现,不过都不满足需要,只有第二种方案会比较接近一点,不过WebView结合JS的操作确实是性能不够好,内存占用也比较高, WordPress-Editor 、RichTextDemo ,这两种方法实现的编辑器会明显的感觉到不够流畅,并且离需要还有挺大的距离,所有没有选择在这基础上进行二次开发。第三种方案在网上有比较多的人推荐,不过我想他们大概也只是推荐而已,真正实现起来需要花费大把的时间,需要填的坑有很多,考虑到时间有限,以及项目的进度安排,这个坑我就没有去踩了。

我最终选择的是第四种方案,这种方案好的地方就是UITableView、UITextView都是十分熟悉的组件,使用组合的模式通过以上的分析,理论上是没有问题的,并且,UITableView有复用Cell的优势,所以时间性能和空间性能应该是不差的。

实现细节分析

使用UITableView集合UITextView的这种方案有很多细节需要注意

  1. Cell中添加UITextView,文字输入换行或者超过一行Cell高度自动伸缩处理
  2. Cell中添加UITextView显示图片的处理
  3. 光标处删除和添加图片的处理,换行的处理

需要解决问题,好的是有些是已经有人遇到并且解决的,其他的即使其他人没有遇到过,作为第一个吃螃蟹的人,我们详细的去分析下其实也不难

1.这个问题刚好有人遇到过,这里就直接发链接了iOS UITextView 输入内容实时更新cell的高度

实现上面效果的基本原理是:

1.在 cell 中设置好 text view 的 autolayout,让 cell 可以根据内容自适应大小

2.text view 中输入内容,根据内容更新 textView 的高度

3.调用 tableView 的 beginUpdates 和 endUpdates,重新计算 cell 的高度

4.将 text view 更新后的数据保存,以免 table view 滚动超过一屏再滚回来 text view 中的数据又不刷新成原来的数据了。

2.这个问题很简单,使用属性文字就行了,下面直接贴代码了

NSAttributedString结合NSTextAttachment就行了


 
 
  1. /** 
  2.  显示图片的属性文字 
  3.  */ 
  4. - (NSAttributedString*)attrStringWithContainerWidth:(NSInteger)containerWidth { 
  5.     if (!_attrString) { 
  6.         CGFloat showImageWidth = containerWidth - MMEditConfig.editAreaLeftPadding - MMEditConfig.editAreaRightPadding - MMEditConfig.imageDeltaWidth; 
  7.         NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init]; 
  8.         CGRect rect = CGRectZero; 
  9.         rect.size.width = showImageWidth; 
  10.         rect.size.height = showImageWidth * self.image.size.height / self.image.size.width; 
  11.         textAttachment.bounds = rect; 
  12.         textAttachment.image = self.image; 
  13.  
  14.         NSAttributedString *attachmentString = [NSAttributedString attributedStringWithAttachment:textAttachment]; 
  15.         NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@""]; 
  16.         [attributedString insertAttributedString:attachmentString atIndex:0]; 
  17.         _attrString = attributedString; 
  18.  
  19.         // 设置Size 
  20.         CGRect tmpImageFrame = rect; 
  21.         tmpImageFrame.size.height += MMEditConfig.editAreaTopPadding + MMEditConfig.editAreaBottomPadding; 
  22.         _imageFrame = tmpImageFrame; 
  23.     } 
  24.     return _attrString; 
  25.  

3.这个问题比较棘手,我自己也是先把可能的情况列出来,然后一个一个分支去处理这些情况,不难就是麻烦,下面的文本是我写在 备忘录 上的情况分析,- [x] 这种标识这种情况已经实现,- [ ] 这种标识暂时未实现,后面这部分会进行优化,主要的工作已经完成了,优化的工作量不会很大了。

UITableView实现的编辑器

return换行情况分析:
- [x] text节点:不处理  
- [x] Image节点-前面:上面是text,光标移动到上面一行,并且在最后添加一个换行,定位光标在最后将
- [x] Image节点-前面:上面是图片或者空,在上面添加一个Text节点,光标移动到上面一行,
- [x] Image节点-后面:下面是图片或者空,在下面添加一个Text节点,光标移动到下面一行,
- [x] Image节点-后面:下面是text,光标移动到下面一行,并且在最前面添加一个换行,定位光标在最前面

Delete情况分析:  
- [x] Text节点-当前的Text不为空-前面-:上面是图片,定位光标到上面图片的最后
- [x] Text节点-当前的Text不为空-前面-:上面是Text,合并当前Text和上面Text  这种情况不存在,在图片删除的时候进行合并
- [x] Text节点-当前的Text不为空-前面-:上面是空,不处理
- [x] Text节点-当前的Text为空-前面-没有其他元素(第一个)-:不处理
- [x] Text节点-当前的Text为空-前面-有其他元素-:删除这一行,定位光标到下面图片的最后
- [x] Text节点-当前的Text不为空-后面-:正常删除
- [x] Text节点-当前的Text为空-后面-:正常删除,和第三种情况:为空的情况处理一样

- [x] Image节点-前面-上面为Text(不为空)/Image定位到上面元素的后面
- [x] Image节点-前面-上面为Text(为空):删除上面Text节点
- [x] Image节点-前面-上面为空:不处理
- [ ] Image节点-后面-上面为空(第一个位置)-列表只有一个元素:添加一个Text节点,删除当前Image节点,光标放在添加的Text节点上 ****TODO:上面元素不处于显示区域不可定位****
- [x] Image节点-后面-上面为空(第一个位置)-列表多于一个元素:删除当前节点,光标放在后面元素之前
- [x] Image节点-后面-上面为图片:删除Image节点,定位到上面元素的后面
- [x] Image节点-后面-上面为Text-下面为图片或者空:删除Image节点,定位到上面元素的后面
- [x] Image节点-后面-上面为Text-下面为Text:删除Image节点,合并下面的Text到上面,删除下面Text节点,定位到上面元素的后面

图片节点添加文字的情况分析:
- [ ] 前面输入文字
- [ ] 后面输入文字

插入图片的情况分析:
- [x] activeIndex是Image节点-后面:下面添加一个图片节点
- [x] activeIndex是Image节点-前面:上面添加一个图片节点
- [x] activeIndex是Text节点:拆分光标前后内容插入一个图片节点和Text节点
- [x] 图片插入之后更新 activeIndexPath 

基本上分析就到此为止了,talk is cheap, show me code,下面就是代码实现了。

代码实现

编辑模块

文字输入框的Cell实现

下面是文字输入框的Cell的主要代码,包含了

  1. 初始设置文字编辑Cell的高度、文字内容、是否显示Placeholder
  2. 在 UITextViewDelegate 回调方法 textViewDidChange 中处理Cell的高度自动拉伸
  3. 删除的回调方法中处理前面删除和后面删除,删除回调的代理方法是继承 UITextView 重写 deleteBackward 方法进行的回调,具体的可以额查看 MMTextView 这个类的实现,很简单的一个实现。

 
 
  1. @implementation MMRichTextCell 
  2. // ... 
  3. - (void)updateWithData:(id)data indexPath:(NSIndexPath*)indexPath { 
  4.     if ([data isKindOfClass:[MMRichTextModel class]]) { 
  5.         MMRichTextModel* textModel = (MMRichTextModel*)data; 
  6.         _textModel = textModel; 
  7.  
  8.         // 重新设置TextView的约束 
  9.         [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) { 
  10.             make.left.top.right.equalTo(self); 
  11.             make.bottom.equalTo(self).priority(900); 
  12.             make.height.equalTo(@(textModel.textFrame.size.height)); 
  13.         }]; 
  14.         // Content 
  15.         _textView.text = textModel.textContent; 
  16.         // Placeholder 
  17.         if (indexPath.row == 0) { 
  18.             self.textView.showPlaceHolder = YES; 
  19.         } else { 
  20.             self.textView.showPlaceHolder = NO
  21.         } 
  22.     } 
  23.  
  24. - (void)beginEditing { 
  25.     [_textView becomeFirstResponder]; 
  26.  
  27.     if (![_textView.text isEqualToString:_textModel.textContent]) { 
  28.         _textView.text = _textModel.textContent; 
  29.  
  30.         // 手动调用回调方法修改 
  31.         [self textViewDidChange:_textView]; 
  32.     } 
  33.  
  34.     if ([self curIndexPath].row == 0) { 
  35.         self.textView.showPlaceHolder = YES; 
  36.     } else { 
  37.         self.textView.showPlaceHolder = NO
  38.     } 
  39.  
  40. #pragma mark - ......::::::: UITextViewDelegate :::::::...... 
  41.  
  42. - (void)textViewDidChange:(UITextView *)textView { 
  43.     CGRect frame = textView.frame; 
  44.     CGSize constraintSize = CGSizeMake(frame.size.width, MAXFLOAT); 
  45.     CGSize size = [textView sizeThatFits:constraintSize]; 
  46.  
  47.     // 更新模型数据 
  48.     _textModel.textFrame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, size.height); 
  49.     _textModel.textContent = textView.text; 
  50.     _textModel.selectedRange = textView.selectedRange; 
  51.     _textModel.isEditing = YES; 
  52.  
  53.     if (ABS(_textView.frame.size.height - size.height) > 5) { 
  54.  
  55.         // 重新设置TextView的约束 
  56.         [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) { 
  57.             make.left.top.right.equalTo(self); 
  58.             make.bottom.equalTo(self).priority(900); 
  59.             make.height.equalTo(@(_textModel.textFrame.size.height)); 
  60.         }]; 
  61.  
  62.         UITableView* tableView = [self containerTableView]; 
  63.         [tableView beginUpdates]; 
  64.         [tableView endUpdates]; 
  65.     } 
  66.  
  67. - (BOOL)textViewShouldBeginEditing:(UITextView *)textView { 
  68.     textView.inputAccessoryView = [self.delegate mm_inputAccessoryView]; 
  69.     if ([self.delegate respondsToSelector:@selector(mm_updateActiveIndexPath:)]) { 
  70.         [self.delegate mm_updateActiveIndexPath:[self curIndexPath]]; 
  71.     } 
  72.     return YES; 
  73.  
  74. - (BOOL)textViewShouldEndEditing:(UITextView *)textView { 
  75.     textView.inputAccessoryView = nil; 
  76.     return YES; 
  77.  
  78. - (void)textViewDeleteBackward:(MMTextView *)textView { 
  79.     // 处理删除 
  80.     NSRange selRange = textView.selectedRange; 
  81.     if (selRange.location == 0) { 
  82.         if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) { 
  83.             [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]]; 
  84.         } 
  85.     } else { 
  86.         if ([self.delegate respondsToSelector:@selector(mm_PostDeleteItemAtIndexPath:)]) { 
  87.             [self.delegate mm_PostDeleteItemAtIndexPath:[self curIndexPath]]; 
  88.         } 
  89.     } 
  90.  
  91. @end  

显示图片Cell的实现

下面显示图片Cell的实现,主要包含了

  1. 初始设置文字编辑Cell的高度、图片显示内容
  2. 在 UITextViewDelegate 回调方法 shouldChangeTextInRange 中处理换行和删除,这个地方的删除和Text编辑的Cell不一样,所以在这边做了特殊的处理,具体看一看 shouldChangeTextInRange 这个方法的处理方式。
  3. 处理图片上传的进度回调、失败回调、成功回调

 
 
  1. @implementation MMRichImageCell 
  2. // 省略部否代码... 
  3. - (void)updateWithData:(id)data { 
  4.     if ([data isKindOfClass:[MMRichImageModel class]]) { 
  5.         MMRichImageModel* imageModel = (MMRichImageModel*)data; 
  6.         // 设置旧的数据delegate为nil 
  7.         _imageModel.uploadDelegate = nil; 
  8.         _imageModel = imageModel; 
  9.         // 设置新的数据delegate 
  10.         _imageModel.uploadDelegate = self; 
  11.  
  12.         CGFloat width = [MMRichTextConfig sharedInstance].editAreaWidth; 
  13.         NSAttributedString* imgAttrStr = [_imageModel attrStringWithContainerWidth:width]; 
  14.         _textView.attributedText = imgAttrStr; 
  15.         // 重新设置TextView的约束 
  16.         [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) { 
  17.             make.left.top.right.equalTo(self); 
  18.             make.bottom.equalTo(self).priority(900); 
  19.             make.height.equalTo(@(imageModel.imageFrame.size.height)); 
  20.         }]; 
  21.  
  22.         self.reloadButton.hidden = YES; 
  23.  
  24.         // 根据上传的状态设置图片信息 
  25.         if (_imageModel.isDone) { 
  26.             self.progressView.hidden = NO
  27.             self.progressView.progress = _imageModel.uploadProgress; 
  28.             self.reloadButton.hidden = YES; 
  29.         } 
  30.         if (_imageModel.isFailed) { 
  31.             self.progressView.hidden = NO
  32.             self.progressView.progress = _imageModel.uploadProgress; 
  33.             self.reloadButton.hidden = NO
  34.         } 
  35.         if (_imageModel.uploadProgress > 0) { 
  36.             self.progressView.hidden = NO
  37.             self.progressView.progress = _imageModel.uploadProgress; 
  38.             self.reloadButton.hidden = YES; 
  39.         } 
  40.     } 
  41.  
  42. #pragma mark - ......::::::: UITextViewDelegate :::::::...... 
  43.  
  44. - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { 
  45.     // 处理换行 
  46.     if ([text isEqualToString:@"\n"]) { 
  47.         if (range.location == 0 && range.length == 0) { 
  48.             // 在前面添加换行 
  49.             if ([self.delegate respondsToSelector:@selector(mm_preInsertTextLineAtIndexPath:textContent:)]) { 
  50.                 [self.delegate mm_preInsertTextLineAtIndexPath:[self curIndexPath]textContent:nil]; 
  51.             } 
  52.         } else if (range.location == 1 && range.length == 0) { 
  53.             // 在后面添加换行 
  54.             if ([self.delegate respondsToSelector:@selector(mm_postInsertTextLineAtIndexPath:textContent:)]) { 
  55.                 [self.delegate mm_postInsertTextLineAtIndexPath:[self curIndexPath] textContent:nil]; 
  56.             } 
  57.         } else if (range.location == 0 && range.length == 2) { 
  58.             // 选中和换行 
  59.         } 
  60.     } 
  61.  
  62.     // 处理删除 
  63.     if ([text isEqualToString:@""]) { 
  64.         NSRange selRange = textView.selectedRange; 
  65.         if (selRange.location == 0 && selRange.length == 0) { 
  66.             // 处理删除 
  67.             if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) { 
  68.                 [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]]; 
  69.             } 
  70.         } else if (selRange.location == 1 && selRange.length == 0) { 
  71.             // 处理删除 
  72.             if ([self.delegate respondsToSelector:@selector(mm_PostDeleteItemAtIndexPath:)]) { 
  73.                 [self.delegate mm_PostDeleteItemAtIndexPath:[self curIndexPath]]; 
  74.             } 
  75.         } else if (selRange.location == 0 && selRange.length == 2) { 
  76.             // 处理删除 
  77.             if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) { 
  78.                 [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]]; 
  79.             } 
  80.         } 
  81.     } 
  82.     return NO
  83.  
  84. - (BOOL)textViewShouldBeginEditing:(UITextView *)textView { 
  85.     textView.inputAccessoryView = [self.delegate mm_inputAccessoryView]; 
  86.     if ([self.delegate respondsToSelector:@selector(mm_updateActiveIndexPath:)]) { 
  87.         [self.delegate mm_updateActiveIndexPath:[self curIndexPath]]; 
  88.     } 
  89.     return YES; 
  90.  
  91. - (BOOL)textViewShouldEndEditing:(UITextView *)textView { 
  92.     textView.inputAccessoryView = nil; 
  93.     return YES; 
  94.  
  95.  
  96. #pragma mark - ......::::::: MMRichImageUploadDelegate :::::::...... 
  97.  
  98. // 上传进度回调 
  99. - (void)uploadProgress:(float)progress { 
  100.     dispatch_async(dispatch_get_main_queue(), ^{ 
  101.         [self.progressView setProgress:progress]; 
  102.     }); 
  103.  
  104. // 上传失败回调 
  105. - (void)uploadFail { 
  106.     [self.progressView setProgress:0.01f]; 
  107.     self.reloadButton.hidden = NO
  108.  
  109. // 上传完成回调 
  110. - (void)uploadDone { 
  111.     [self.progressView setProgress:1.0f]; 
  112.  
  113.  
  114. @end  

图片上传模块

图片上传模块中,上传的元素和上传回调抽象了对应的协议,图片上传模块是一个单利的管理类,管理进行中的上传元素和排队中的上传元素,

图片上传的元素和上传回调的抽象协议


 
 
  1. @protocol UploadItemCallBackProtocal <NSObject> 
  2.  
  3. - (void)mm_uploadProgress:(float)progress; 
  4. - (void)mm_uploadFailed; 
  5. - (void)mm_uploadDone:(NSString*)remoteImageUrlString; 
  6.  
  7. @end 
  8.  
  9. @protocol UploadItemProtocal <NSObject> 
  10.  
  11. - (NSData*)mm_uploadData; 
  12. - (NSURL*)mm_uploadFileURL; 
  13.  
  14. @end  

图片上传的管理类

图片上传使用的是 NSURLSessionUploadTask 类处理

  1. 在 completionHandler 回调中处理结果
  2. 在NSURLSessionDelegate 的方法 URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend: 中处理上传进度
  3. 在NSURLSessionDelegate 的方法 URLSession:task:didCompleteWithError: 中处理失败

上传管理类的关键代码如下:


 
 
  1. @interface MMFileUploadUtil () <NSURLSessionDataDelegate, NSURLSessionDelegate, NSURLSessionTaskDelegate> 
  2. @property (strong,nonatomic) NSURLSession * session; 
  3. @property (nonatomic, strong) NSMutableArray* uploadingItems; 
  4. @property (nonatomic, strong) NSMutableDictionary* uploadingTaskIDToUploadItemMap; 
  5. @property (nonatomic, strong) NSMutableArray* todoItems; 
  6.  
  7. @property (nonatomic, assign) NSInteger maxUploadTask; 
  8. @end 
  9.  
  10. @implementation MMFileUploadUtil 
  11.  
  12. - (void)addUploadItem:(id<UploadItemProtocal, UploadItemCallBackProtocal>)uploadItem { 
  13.     [self.todoItems addObject:uploadItem]; 
  14.     [self startNextUploadTask]; 
  15.  
  16. - (void)startNextUploadTask { 
  17.     if (self.uploadingItems.count < _maxUploadTask) { 
  18.         // 添加下一个任务 
  19.         if (self.todoItems.count > 0) { 
  20.             id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = self.todoItems.firstObject; 
  21.             [self.uploadingItems addObject:uploadItem]; 
  22.             [self.todoItems removeObject:uploadItem]; 
  23.  
  24.             [self uploadItem:uploadItem]; 
  25.         } 
  26.     } 
  27.  
  28. - (void)uploadItem:(id<UploadItemProtocal, UploadItemCallBackProtocal>)uploadItem { 
  29.     NSMutableURLRequest * request = [self TSuploadTaskRequest]; 
  30.  
  31.     NSData* uploadData = [uploadItem mm_uploadData]; 
  32.     NSData* totalData = [self TSuploadTaskRequestBody:uploadData]; 
  33.  
  34.     __block NSURLSessionUploadTask * uploadtask = nil; 
  35.     uploadtask = [self.session uploadTaskWithRequest:request fromData:totalData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 
  36.         NSString* result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 
  37.         NSLog(@"completionHandler  %@", result); 
  38.  
  39.         NSString* imgUrlString = @""
  40.         NSError *JSONSerializationError; 
  41.         id obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&JSONSerializationError]; 
  42.         if ([obj isKindOfClass:[NSDictionary class]]) { 
  43.             imgUrlString = [obj objectForKey:@"url"]; 
  44.         } 
  45.         // 成功回调 
  46.         // FIXME: ZYT uploadtask ??? 
  47.         id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(uploadtask.taskIdentifier)]; 
  48.         if (uploadItem) { 
  49.             if ([uploadItem respondsToSelector:@selector(mm_uploadDone:)]) { 
  50.                 [uploadItem mm_uploadDone:imgUrlString]; 
  51.             } 
  52.             [self.uploadingTaskIDToUploadItemMap removeObjectForKey:@(uploadtask.taskIdentifier)]; 
  53.             [self.uploadingItems removeObject:uploadItem]; 
  54.         } 
  55.  
  56.         [self startNextUploadTask]; 
  57.     }]; 
  58.     [uploadtask resume]; 
  59.  
  60.     // 添加到映射中 
  61.     [self.uploadingTaskIDToUploadItemMap setObject:uploadItem forKey:@(uploadtask.taskIdentifier)]; 
  62.  
  63. #pragma mark - ......::::::: NSURLSessionDelegate :::::::...... 
  64.  
  65. -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ 
  66.     NSLog(@"didCompleteWithError = %@",error.description); 
  67.  
  68.     // 失败回调 
  69.     if (error) { 
  70.         id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(task.taskIdentifier)]; 
  71.         if (uploadItem) { 
  72.             if ([uploadItem respondsToSelector:@selector(mm_uploadFailed)]) { 
  73.                 [uploadItem mm_uploadFailed]; 
  74.             } 
  75.             [self.uploadingTaskIDToUploadItemMap removeObjectForKey:@(task.taskIdentifier)]; 
  76.             [self.uploadingItems removeObject:uploadItem]; 
  77.         } 
  78.     } 
  79.  
  80.     [self startNextUploadTask]; 
  81.  
  82. -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{ 
  83.     NSLog(@"bytesSent:%@-totalBytesSent:%@-totalBytesExpectedToSend:%@", @(bytesSent), @(totalBytesSent), @(totalBytesExpectedToSend)); 
  84.  
  85.     // 进度回调 
  86.     id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(task.taskIdentifier)]; 
  87.     if ([uploadItem respondsToSelector:@selector(mm_uploadProgress:)]) { 
  88.         [uploadItem mm_uploadProgress:(totalBytesSent * 1.0f/totalBytesExpectedToSend)]; 
  89.     } 
  90.  
  91. @end  

图片上传的回调会通过 UploadItemCallBackProtocal 协议的实现方法回调到图片编辑的模型中,更新对应的数据。图片编辑的数据模型是 MMRichImageModel ,该模型实现了 UploadItemProtocal 和


 
 
  1. @implementation MMRichImageModel 
  2.  
  3. - (void)setUploadProgress:(float)uploadProgress { 
  4.     _uploadProgress = uploadProgress; 
  5.     if ([_uploadDelegate respondsToSelector:@selector(uploadProgress:)]) { 
  6.         [_uploadDelegate uploadProgress:uploadProgress]; 
  7.     } 
  8.  
  9. - (void)setIsDone:(BOOL)isDone { 
  10.     _isDone = isDone; 
  11.     if ([_uploadDelegate respondsToSelector:@selector(uploadDone)]) { 
  12.         [_uploadDelegate uploadDone]; 
  13.     } 
  14.  
  15. - (void)setIsFailed:(BOOL)isFailed { 
  16.     _isFailed = isFailed; 
  17.     if ([_uploadDelegate respondsToSelector:@selector(uploadFail)]) { 
  18.         [_uploadDelegate uploadFail]; 
  19.     } 
  20.  
  21.  
  22. #pragma mark - ......::::::: UploadItemCallBackProtocal :::::::...... 
  23. - (void)mm_uploadProgress:(float)progress { 
  24.     self.uploadProgress = progress; 
  25.  
  26. - (void)mm_uploadFailed { 
  27.     self.isFailed = YES; 
  28.  
  29. - (void)mm_uploadDone:(NSString *)remoteImageUrlString { 
  30.     self.remoteImageUrlString = remoteImageUrlString; 
  31.     self.isDone = YES; 
  32.  
  33.  
  34. #pragma mark - ......::::::: UploadItemProtocal :::::::...... 
  35. - (NSData*)mm_uploadData { 
  36.     return UIImageJPEGRepresentation(_image, 0.6); 
  37.  
  38. - (NSURL*)mm_uploadFileURL { 
  39.     return nil; 
  40.  
  41. @end  

内容处理模块

最终是要把内容序列化然后上传到服务端的,我们的序列化方案是转换为HTML,内容处理模块主要包含了以下几点:

  • 生成HTML格式的内容
  • 验证内容是否有效,判断图片时候全部上传成功
  • 压缩图片
  • 保存图片到本地

这部分收尾的工作比较的简单,下面是实现代码:


 
 
  1. #define kRichContentEditCache      @"RichContentEditCache" 
  2.  
  3.  
  4. @implementation MMRichContentUtil 
  5.  
  6. + (NSString*)htmlContentFromRichContents:(NSArray*)richContents { 
  7.     NSMutableString *htmlContent = [NSMutableString string]; 
  8.  
  9.     for (int i = 0; i< richContents.count; i++) { 
  10.         NSObject* content = richContents[i]; 
  11.         if ([content isKindOfClass:[MMRichImageModel class]]) { 
  12.             MMRichImageModel* imgContent = (MMRichImageModel*)content; 
  13.             [htmlContent appendString:[NSString stringWithFormat:@"<img src=\"%@\" width=\"%@\" height=\"%@\" />", imgContent.remoteImageUrlString, @(imgContent.image.size.width), @(imgContent.image.size.height)]]; 
  14.         } else if ([content isKindOfClass:[MMRichTextModel class]]) { 
  15.             MMRichTextModel* textContent = (MMRichTextModel*)content; 
  16.             [htmlContent appendString:textContent.textContent]; 
  17.         } 
  18.  
  19.         // 添加换行 
  20.         if (i != richContents.count - 1) { 
  21.             [htmlContent appendString:@"<br />"]; 
  22.         } 
  23.     } 
  24.  
  25.     return htmlContent; 
  26.  
  27. + (BOOL)validateRichContents:(NSArray*)richContents { 
  28.     for (int i = 0; i< richContents.count; i++) { 
  29.         NSObject* content = richContents[i]; 
  30.         if ([content isKindOfClass:[MMRichImageModel class]]) { 
  31.             MMRichImageModel* imgContent = (MMRichImageModel*)content; 
  32.             if (imgContent.isDone == NO) { 
  33.                 return NO
  34.             } 
  35.         } 
  36.     } 
  37.     return YES; 
  38.  
  39. + (UIImage*)scaleImage:(UIImage*)originalImage { 
  40.     float scaledWidth = 1242; 
  41.     return [originalImage scaletoSize:scaledWidth]; 
  42.  
  43. + (NSString*)saveImageToLocal:(UIImage*)image { 
  44.     NSString *path=[self createDirectory:kRichContentEditCache]; 
  45.     NSData* data = UIImageJPEGRepresentation(image, 1.0); 
  46.     NSString *filePath = [path stringByAppendingPathComponent:[self.class genRandomFileName]]; 
  47.     [data writeToFile:filePath atomically:YES]; 
  48.     return filePath; 
  49.  
  50. // 创建文件夹 
  51. + (NSString *)createDirectory:(NSString *)path { 
  52.     BOOL isDir = NO
  53.     NSString *finalPath = [CACHE_PATH stringByAppendingPathComponent:path]; 
  54.  
  55.     if (!([[NSFileManager defaultManager] fileExistsAtPath:finalPath 
  56.                                                isDirectory:&isDir] 
  57.           && isDir)) 
  58.     { 
  59.         [[NSFileManager defaultManager] createDirectoryAtPath:finalPath 
  60.                                  withIntermediateDirectories :YES 
  61.                                                   attributes :nil 
  62.                                                        error :nil]; 
  63.     } 
  64.  
  65.     return finalPath; 
  66.  
  67. + (NSString*)genRandomFileName { 
  68.     NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970]; 
  69.     uint32_t random = arc4random_uniform(10000); 
  70.     return [NSString stringWithFormat:@"%@-%@.png", @(timeStamp), @(random)]; 
  71.  
  72. @end  

总结

这个功能从选型定型到实现大概花费了3天的时间,因为时间原因,有很多地方优化的不到位,如果看官有建议意见希望给我留言,我会继续完善,或者你有时间欢迎加入这个项目,可以一起做得更好,代码开源看下面的链接。


本文作者: aron1992

来源:51CTO

原文标题:iOS使用UITableView实现的富文本编辑器
相关文章
|
6月前
|
监控 iOS开发
iOS15适配问题:viewForSupplementaryElementOfKind表头和表尾复用闪退,UITableView section header多22像素等问题
iOS15适配问题:viewForSupplementaryElementOfKind表头和表尾复用闪退,UITableView section header多22像素等问题
95 0
|
安全 数据安全/隐私保护 iOS开发
iOS小技能:【发红包】使用tweak和lua脚本结合进行实现
我们开发的大部分越狱程序,都是编译成动态链接库(`例如:介绍的越狱程序(Tweak)开发,就是动态链接库。`),然后通过越狱平台的MobileSubstrate(iOS7上叫CydiaSubstrate)来加载进入目标程序(Target),通过对目标程序的挂钩(Hook),来实现相应的功能。
332 0
|
iOS开发
IOS的UITableView控件简单使用
IOS的UITableView控件简单使用
158 0
|
缓存 算法 测试技术
iOS UITableView性能优化
iOS UITableView性能优化
iOS UITableView性能优化
|
Android开发 iOS开发
iOS开发 - 商品详情页两种分页模式,只提供思路和实现方式。
iOS开发 - 商品详情页两种分页模式,只提供思路和实现方式。
412 0
iOS开发 - 商品详情页两种分页模式,只提供思路和实现方式。
|
存储 安全 iOS开发
iOS开发 - 继udid,Mac地址等一系列唯一标识无效后,如何用KeyChain来实现设备唯一性
iOS开发 - 继udid,Mac地址等一系列唯一标识无效后,如何用KeyChain来实现设备唯一性
473 0
iOS开发 - 继udid,Mac地址等一系列唯一标识无效后,如何用KeyChain来实现设备唯一性
|
Swift 数据安全/隐私保护 iOS开发
iOS开发 - swift通过Alamofire实现https通信
iOS开发 - swift通过Alamofire实现https通信
429 0
iOS开发 - swift通过Alamofire实现https通信
|
开发者 iOS开发
iOS开发 - 用AFNetworking实现https单向验证,双向验证
iOS开发 - 用AFNetworking实现https单向验证,双向验证
422 0
iOS开发 - 用AFNetworking实现https单向验证,双向验证
|
iOS开发
iOS小技能:自动布局实现兄弟控件N等分且宽高比例是1:N(xib 上实现)
本文为 iOS视图约束专题的第三篇:xib上使用自动布局教程
180 0
|
JavaScript 前端开发 搜索推荐
iOS小技能:富文本编辑器(上篇)
设计思路: 编辑器基于WKWebview实现,Editor使用WKWebview加载一个本地editor.html文件,Editor使用evaluateJavaScript执行JS往本地html添加标签代码,编辑器最终输出富文本字符串(html代码)传输给服务器。
282 0
iOS小技能:富文本编辑器(上篇)