Rich text editor implemented using UITableView in iOS

Rich text editor implemented using UITableView in iOS

Preface

The company recently did a project, one of which is a rich text editing module. We had never done a similar function module before. I thought that this function is very common and there should be a ready-made wheel. Maybe I just need to find the wheel, study the wheel, and then modify and polish the wheel, and this thing will be almost done. However, I am still too young to simple. Some things still have to be faced by myself. Maybe this is called growth. I feel that in the past year, I have a little more love for programming. I feel that I am not worthy of a life of copying and pasting codes. Programming needs challenges. Therefore, when you encounter difficulties, keep a positive mind. The road is actually under your feet. If there are no difficulties, then create difficulties and face them head-on. There is no wasted road in life. Every step counts. The poisonous chicken soup ends here. The following is the dry goods.

result

No picture, no truth. Here are some pictures of the effect

The functions implemented include:

  • Editor text editing
  • Editor Image Editing
  • Editor mixed text and image editing
  • Editor image upload, with progress and failure prompts, you can re-upload the operation
  • Editor model converted to HTML format content
  • Supporting Java-implemented server

And the client code open source hosting address: RichTextEditDemo

There is also a file server code implemented in java, open source hosting address: javawebserverdemo

Research and Analysis

Basically, there are the following implementation schemes:

  1. UITextView combines with NSAttributeString to implement mixed text and image editing. The corresponding open source code for this solution can be found on the Internet. For example, SimpleWord is implemented in this way. However, the disadvantage is that the picture cannot be interactive. For example, adding a progress bar to the picture, adding an upload failure prompt, and image click event processing are not possible. If there is no such requirement, then you can choose this solution.
  2. Using WebView to implement interaction with native JavaScript, such as WordPress-Editor and RichTextDemo, the main problem is that the performance is not good enough, and you need to understand front-end knowledge to get started.
  3. Use CoreText or TextKit. There are also open source codes for implementing this solution, such as YYText, which is very famous. However, the position of the picture inserted and edited by it is fixed, and the text is around the picture, so this does not meet my requirements. If you want to use this solution, there are many places to modify, and there is a certain threshold for using CoreText/TextKit.
  4. The main idea is that each cell is a UITextView for text input or a UITextView for displaying pictures. The reason why UITextView is chosen for picture display is that the picture position needs an input cursor, so the use of UITextView combined with NSAttributeString can just achieve this function. Mixing pictures and texts, that is, mixing cells that display pictures and cells that display texts, can be achieved. The main workload is to process the cursor position input and the cursor position deletion.

Selection and finalization

The first three solutions have open source implementations, but none of them meet the needs. Only the second solution is closer. However, the performance of WebView combined with JS is not good enough, and the memory usage is relatively high. The editors implemented by WordPress-Editor and RichTextDemo are obviously not smooth enough, and there is still a long way to go, so no secondary development was chosen on this basis. The third solution is recommended by many people on the Internet, but I think they are probably just recommendations. It takes a lot of time to actually implement it, and there are many pits to fill. Considering the limited time and the project schedule, I didn't step on this pit.

I finally chose the fourth solution. The good thing about this solution is that UITableView and UITextView are very familiar components. Through the above analysis, there is no problem in theory using the combined mode. In addition, UITableView has the advantage of reusing Cell, so the time performance and space performance should not be bad.

Implementation details analysis

There are many details to pay attention to when using UITableView to collect UITextView.

  1. Add UITextView to Cell, and the height of the Cell will automatically expand or contract if the text input wraps or exceeds one line
  2. Add UITextView to display pictures in Cell
  3. Deleting and adding pictures at the cursor, and line breaking

There are problems that need to be solved. The good thing is that some of them have been encountered and solved by others. Even if others have not encountered them, as the first person to try it out, it is not difficult for us to analyze them in detail.

1. This problem happened to be encountered by someone, so here is a direct link to iOS UITextView input content to update the cell height in real time

The basic principle to achieve the above effect is:

1. Set the autolayout of the text view in the cell so that the cell can adapt to the size of the content

2. Enter content in the text view and update the height of the textView according to the content

3. Call tableView's beginUpdates and endUpdates to recalculate the cell's height

4. Save the updated data in the text view to prevent the data in the text view from not being refreshed to the original data when the table view is scrolled more than one screen and then scrolled back.

2. This problem is very simple. Just use attribute text. The code is posted directly below.

NSAttributedString combined with NSTextAttachment

  1. /**
  2. Displays the attribute text of the image
  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. // Set the Size  
  20. CGRect tmpImageFrame = rect;
  21. tmpImageFrame. size .height += MMEditConfig.editAreaTopPadding + MMEditConfig.editAreaBottomPadding;
  22. _imageFrame = tmpImageFrame;
  23. }
  24. return _attrString;
  25. }

3. This problem is more difficult. I also listed the possible situations first, and then handled them one by one. It was not difficult but troublesome. The following text is the situation analysis I wrote in the memo. - [x] This mark has been implemented. - [ ] This mark has not been implemented yet. This part will be optimized later. The main work has been completed and the optimization workload will not be large.

Analysis of editor return line break implemented by UITableView:
- [x] text node: do not process - [x] Image node - in front: there is text above, the cursor moves to the line above, and a line break is added at the end, the cursor is positioned at the end - [x] Image node - in front: there is an image or nothing above, a Text node is added on top, the cursor moves to the line above,
- [x] Image node - behind: there is an image or nothing below, add a Text node below, and move the cursor to the next line.
- [x] Image node - behind: Below is text, the cursor moves to the next line, and a line break is added at the front, positioning the cursor at the front Delete situation analysis:  
- [x] Text node - the current text is not the first page -: there is a picture above, position the cursor to the first page of the picture above
- [x] Text node-The current Text is not the *** side-: There is Text above, merge the current Text and the Text above. This situation does not exist, merge when the image is deleted- [x] Text node-The current Text is not the *** side-: The above is empty, do not process- [x] Text node-The current Text is the *** side-No other elements (***)-: Do not process- [x] Text node-The current Text is the *** side-There are other elements-: Delete this line and position the cursor to the *** of the image below
- [x] Text node - current Text is not empty - next -: delete normally - [x] Text node - current Text is empty - next -: delete normally, same as the third case: empty case - [x] Image node - front - above is Text (not empty)/Image is positioned behind the above element - [x] Image node - front - above is Text (empty): delete the above Text node - [x] Image node - front - above is empty: do not process - [ ] Image node - back - above is empty (*** position) - list has only one element: add a Text node, delete the current Image node, and place the cursor on the added Text node ****TODO: the above element is not in the display area and cannot be positioned****
- [x] Image node - back - top is empty (the last position) - list has more than one element: delete the current node and place the cursor before the back element - [x] Image node - back - top is picture: delete the Image node and locate behind the top element - [x] Image node - back - top is Text - bottom is picture or empty: delete the Image node and locate behind the top element - [x] Image node - back - top is Text - bottom is Text: delete the Image node, merge the bottom Text to the top, delete the bottom Text node, and locate behind the top element Analysis of adding text to the image node:
- Input text before [ ] - Input text after [ ] to insert a picture Analysis:
- [x] activeIndex is an Image node - behind: add an image node below - [x] activeIndex is an Image node - in front: add an image node above - [x] activeIndex is a Text node: split the content before and after the cursor and insert an image node and a Text node - [x] update activeIndexPath after the image is inserted

Basically, the analysis stops here. Talk is cheap, show me code. The following is the code implementation.

Code Implementation

Edit Module

Cell implementation of text input box

The following is the main code of the Cell of the text input box, which contains

  1. Initially set the height, text content, and whether to display the Placeholder of the text editing cell
  2. Handle automatic stretching of Cell height in UITextViewDelegate callback method textViewDidChange
  3. The delete callback method handles the front deletion and the back deletion. The proxy method of the delete callback inherits UITextView and rewrites the deleteBackward method to perform the callback. For details, you can view the implementation of the MMTextView class, which is a very simple implementation.
  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. // Reset the constraints of the 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.  
  25. - (void)beginEditing {
  26. [_textView becomeFirstResponder];
  27.  
  28. if (![_textView.text isEqualToString:_textModel.textContent]) {
  29. _textView.text = _textModel.textContent;
  30.  
  31. // Manually call the callback method to modify
  32. [self textViewDidChange:_textView];
  33. }
  34.  
  35. if ([self curIndexPath].row == 0) {
  36. self.textView.showPlaceHolder = YES;
  37. } else {
  38. self.textView.showPlaceHolder = NO ;
  39. }
  40. }
  41.  
  42. #pragma mark - ......::::::: UITextViewDelegate :::::::...
  43.  
  44. - (void)textViewDidChange:(UITextView *)textView {
  45. CGRect frame = textView.frame;
  46. CGSize constraintSize = CGSizeMake(frame. size .width, MAXFLOAT);
  47. CGSize size = [textView sizeThatFits:constraintSize];
  48.  
  49. // Update model data
  50. _textModel.textFrame = CGRectMake(frame.origin.x, frame.origin.y, frame. size .width, size .height);
  51. _textModel.textContent = textView.text;
  52. _textModel.selectedRange = textView.selectedRange;
  53. _textModel.isEditing = YES;
  54.  
  55. if ( ABS (_textView.frame. size .height - size .height) > 5) {
  56.  
  57. // Reset the constraints of the TextView
  58. [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
  59. make. left . top . right .equalTo(self);
  60. make.bottom.equalTo(self).priority(900);
  61. make.height.equalTo(@(_textModel.textFrame. size .height));
  62. }];
  63.  
  64. UITableView* tableView = [self containerTableView];
  65. [tableView beginUpdates];
  66. [tableView endUpdates];
  67. }
  68. }
  69.  
  70. - (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
  71. textView.inputAccessoryView = [self.delegate mm_inputAccessoryView];
  72. if ([self.delegate respondsToSelector:@selector(mm_updateActiveIndexPath:)]) {
  73. [self.delegate mm_updateActiveIndexPath:[self curIndexPath]];
  74. }
  75. return YES;
  76. }
  77.  
  78. - (BOOL)textViewShouldEndEditing:(UITextView *)textView {
  79. textView.inputAccessoryView = nil;
  80. return YES;
  81. }
  82.  
  83. - (void)textViewDeleteBackward:(MMTextView *)textView {
  84. // Handle deletion
  85. NSRange selRange = textView.selectedRange;
  86. if (selRange.location == 0) {
  87. if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) {
  88. [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]];
  89. }
  90. } else {
  91. if ([self.delegate respondsToSelector:@selector(mm_PostDeleteItemAtIndexPath:)]) {
  92. [self.delegate mm_PostDeleteItemAtIndexPath:[self curIndexPath]];
  93. }
  94. }
  95. }
  96.  
  97. @ end  

Implementation of displaying picture Cell

The implementation of the picture Cell is shown below, which mainly includes

  1. Initially set the height of the text editing Cell and the image display content
  2. In the UITextViewDelegate callback method shouldChangeTextInRange, line breaks and deletions are handled. The deletion here is different from the Cell for text editing, so special processing is done here. Let's take a look at the processing method of the shouldChangeTextInRange method.
  3. Process the progress callback, failure callback, and success callback of image upload
  1. @implementation MMRichImageCell
  2. // Omit some negative code...
  3. - (void)updateWithData:(id)data {
  4. if ([data isKindOfClass:[MMRichImageModel class]]) {
  5. MMRichImageModel* imageModel = (MMRichImageModel*)data;
  6. // Set the old data delegate to nil
  7. _imageModel.uploadDelegate = nil;
  8. _imageModel = imageModel;
  9. // Set the new data delegate
  10. _imageModel.uploadDelegate = self;
  11.  
  12. CGFloat width = [MMRichTextConfig sharedInstance].editAreaWidth;
  13. NSAttributedString* imgAttrStr = [_imageModel attrStringWithContainerWidth:width];
  14. _textView.attributedText = imgAttrStr;
  15. // Reset the constraints of the 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. // Set the image information according to the uploaded status
  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.  
  43. #pragma mark - ......::::::: UITextViewDelegate :::::::...
  44.  
  45. - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
  46. // Handle line breaks
  47. if ([text isEqualToString:@ "\n" ]) {
  48. if (range.location == 0 && range.length == 0) {
  49. // Add a line break in front
  50. if ([self.delegate respondsToSelector:@selector(mm_preInsertTextLineAtIndexPath:textContent:)]) {
  51. [self.delegate mm_preInsertTextLineAtIndexPath:[self curIndexPath]textContent:nil];
  52. }
  53. } else if (range.location == 1 && range.length == 0) {
  54. // Add a line break after
  55. if ([self.delegate respondsToSelector:@selector(mm_postInsertTextLineAtIndexPath:textContent:)]) {
  56. [self.delegate mm_postInsertTextLineAtIndexPath:[self curIndexPath] textContent:nil];
  57. }
  58. } else if (range.location == 0 && range.length == 2) {
  59. //Select and wrap
  60. }
  61. }
  62.  
  63. // Handle deletion
  64. if ([text isEqualToString:@ "" ]) {
  65. NSRange selRange = textView.selectedRange;
  66. if (selRange.location == 0 && selRange.length == 0) {
  67. // Handle deletion
  68. if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) {
  69. [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]];
  70. }
  71. } else if (selRange.location == 1 && selRange.length == 0) {
  72. // Handle deletion
  73. if ([self.delegate respondsToSelector:@selector(mm_PostDeleteItemAtIndexPath:)]) {
  74. [self.delegate mm_PostDeleteItemAtIndexPath:[self curIndexPath]];
  75. }
  76. } else if (selRange.location == 0 && selRange.length == 2) {
  77. // Handle deletion
  78. if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) {
  79. [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]];
  80. }
  81. }
  82. }
  83. return   NO ;
  84. }
  85.  
  86. - (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
  87. textView.inputAccessoryView = [self.delegate mm_inputAccessoryView];
  88. if ([self.delegate respondsToSelector:@selector(mm_updateActiveIndexPath:)]) {
  89. [self.delegate mm_updateActiveIndexPath:[self curIndexPath]];
  90. }
  91. return YES;
  92. }
  93.  
  94. - (BOOL)textViewShouldEndEditing:(UITextView *)textView {
  95. textView.inputAccessoryView = nil;
  96. return YES;
  97. }
  98.  
  99.  
  100. #pragma mark - ......::::::: MMRichImageUploadDelegate :::::::...
  101.  
  102. // Upload progress callback
  103. - (void)uploadProgress:( float )progress {
  104. dispatch_async(dispatch_get_main_queue(), ^{
  105. [self.progressView setProgress:progress];
  106. });
  107. }
  108.  
  109. // Upload failed callback
  110. - (void)uploadFail {
  111. [self.progressView setProgress:0.01f];
  112. self.reloadButton.hidden = NO ;
  113. }
  114.  
  115. // Upload completion callback
  116. - (void)uploadDone {
  117. [self.progressView setProgress:1.0f];
  118. }
  119.  
  120.  
  121. @ end  

Image upload module

In the picture upload module, the uploaded elements and upload callbacks abstract the corresponding protocols. The picture upload module is a simple management class that manages the uploaded elements in progress and the uploaded elements in the queue.

Abstract protocol for image upload elements and upload callbacks

  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  

Image upload management class

Image upload is handled by NSURLSessionUploadTask class

  1. Process the result in the completionHandler callback
  2. Handle upload progress in the NSURLSessionDelegate method URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
  3. Handle failure in the NSURLSessionDelegate method URLSession:task:didCompleteWithError:

The key codes of the upload management class are as follows:

  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.  
  17. - (void)startNextUploadTask {
  18. if (self.uploadingItems. count < _maxUploadTask) {
  19. // Add the next task
  20. if ( self.todoItems.count > 0) {
  21. id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = self.todoItems.firstObject;
  22. [self.uploadingItems addObject:uploadItem];
  23. [self.todoItems removeObject:uploadItem];
  24.  
  25. [self uploadItem:uploadItem];
  26. }
  27. }
  28. }
  29.  
  30. - (void)uploadItem:(id<UploadItemProtocal, UploadItemCallBackProtocal>)uploadItem {
  31. NSMutableURLRequest * request = [self TSuploadTaskRequest];
  32.  
  33. NSData* uploadData = [uploadItem mm_uploadData];
  34. NSData* totalData = [self TSuploadTaskRequestBody:uploadData];
  35.  
  36. __block NSURLSessionUploadTask * uploadtask = nil;
  37. uploadtask = [self.session uploadTaskWithRequest:request fromData:totalData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  38. NSString* result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  39. NSLog(@ "completionHandler %@" , result);
  40.  
  41. NSString* imgUrlString = @ "" ;
  42. NSError *JSONSerializationError;
  43. id obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&JSONSerializationError];
  44. if ([obj isKindOfClass:[NSDictionary class]]) {
  45. imgUrlString = [obj objectForKey:@ "url" ];
  46. }
  47. // Success callback
  48. // FIXME: ZYT uploadtask? ? ?
  49. id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(uploadtask.taskIdentifier)];
  50. if (uploadItem) {
  51. if ([uploadItem respondsToSelector:@selector(mm_uploadDone:)]) {
  52. [uploadItem mm_uploadDone:imgUrlString];
  53. }
  54. [self.uploadingTaskIDToUploadItemMap removeObjectForKey:@(uploadtask.taskIdentifier)];
  55. [self.uploadingItems removeObject:uploadItem];
  56. }
  57.  
  58. [self startNextUploadTask];
  59. }];
  60. [uploadtask resume];
  61.  
  62. // Add to the map
  63. [self.uploadingTaskIDToUploadItemMap setObject:uploadItem forKey:@(uploadtask.taskIdentifier)];
  64. }
  65.  
  66. #pragma mark - ......::::::: NSURLSessionDelegate :::::::...
  67.  
  68. -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
  69. NSLog(@ "didCompleteWithError = %@" ,error.description);
  70.  
  71. // Failure callback
  72. if (error) {
  73. id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(task.taskIdentifier)];
  74. if (uploadItem) {
  75. if ([uploadItem respondsToSelector:@selector(mm_uploadFailed)]) {
  76. [uploadItem mm_uploadFailed];
  77. }
  78. [self.uploadingTaskIDToUploadItemMap removeObjectForKey:@(task.taskIdentifier)];
  79. [self.uploadingItems removeObject:uploadItem];
  80. }
  81. }
  82.  
  83. [self startNextUploadTask];
  84. }
  85.  
  86. -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
  87. NSLog(@ "bytesSent:%@-totalBytesSent:%@-totalBytesExpectedToSend:%@" , @(bytesSent), @(totalBytesSent), @(totalBytesExpectedToSend));
  88.  
  89. // Progress callback
  90. id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(task.taskIdentifier)];
  91. if ([uploadItem respondsToSelector:@selector(mm_uploadProgress:)]) {
  92. [uploadItem mm_uploadProgress:(totalBytesSent * 1.0f/totalBytesExpectedToSend)];
  93. }
  94. }
  95.  
  96. @ end  

The callback of the picture upload will be called back to the picture editing model through the implementation method of the UploadItemCallBackProtocal protocol to update the corresponding data. The data model of picture editing is MMRichImageModel, which implements UploadItemProtocal and

  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.  
  10. - (void)setIsDone:(BOOL)isDone {
  11. _isDone = isDone;
  12. if ([_uploadDelegate respondsToSelector:@selector(uploadDone)]) {
  13. [_uploadDelegate uploadDone];
  14. }
  15. }
  16.  
  17. - (void)setIsFailed:(BOOL)isFailed {
  18. _isFailed = isFailed;
  19. if ([_uploadDelegate respondsToSelector:@selector(uploadFail)]) {
  20. [_uploadDelegate uploadFail];
  21. }
  22. }
  23.  
  24.  
  25. #pragma mark - ......::::::: UploadItemCallBackProtocal :::::::...
  26. - (void)mm_uploadProgress:( float )progress {
  27. self.uploadProgress = progress;
  28. }
  29.  
  30. - (void)mm_uploadFailed {
  31. self.isFailed = YES;
  32. }
  33.  
  34. - (void)mm_uploadDone:(NSString *)remoteImageUrlString {
  35. self.remoteImageUrlString = remoteImageUrlString;
  36. self.isDone = YES;
  37. }
  38.  
  39.  
  40. #pragma mark - ......::::::: UploadItemProtocal :::::::...
  41. - (NSData*)mm_uploadData {
  42. return UIImageJPEGRepresentation(_image, 0.6);
  43. }
  44.  
  45. - (NSURL*)mm_uploadFileURL {
  46. return nil;
  47. }
  48.  
  49. @ end  

Content processing module

Finally, the content needs to be serialized and uploaded to the server. Our serialization solution is to convert it into HTML. The content processing module mainly includes the following points:

  • Generate HTML content
  • Verify whether the content is valid and judge whether all pictures have been uploaded successfully
  • Compress images
  • Save the image locally

This part of the finishing work is relatively simple. Here is the implementation code:

  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. // Add a line break
  20. if (i != richContents. count - 1) {
  21. [htmlContent appendString:@ "<br />" ];
  22. }
  23. }
  24.  
  25. return htmlContent;
  26. }
  27.  
  28. + (BOOL)validateRichContents:(NSArray*)richContents {
  29. for ( int i = 0; i< richContents. count ; i++) {
  30. NSObject* content = richContents[i];
  31. if ([content isKindOfClass:[MMRichImageModel class]]) {
  32. MMRichImageModel* imgContent = (MMRichImageModel*)content;
  33. if (imgContent.isDone == NO ) {
  34. return   NO ;
  35. }
  36. }
  37. }
  38. return YES;
  39. }
  40.  
  41. + (UIImage*)scaleImage:(UIImage*)originalImage {
  42. float scaledWidth = 1242;
  43. return [originalImage scaletoSize:scaledWidth];
  44. }
  45.  
  46. + (NSString*)saveImageToLocal:(UIImage*)image {
  47. NSString *path=[self createDirectory:kRichContentEditCache];
  48. NSData* data = UIImageJPEGRepresentation(image, 1.0);
  49. NSString *filePath = [path stringByAppendingPathComponent:[self.class genRandomFileName]];
  50. [data writeToFile:filePath atomically:YES];
  51. return filePath;
  52. }
  53.  
  54. // Create a folder
  55. + (NSString *)createDirectory:(NSString *)path {
  56. BOOL isDir = NO ;
  57. NSString *finalPath = [CACHE_PATH stringByAppendingPathComponent:path];
  58.  
  59. if (!([[NSFileManager defaultManager] fileExistsAtPath:finalPath
  60. isDirectory:&isDir]
  61. && isDir))
  62. {
  63. [[NSFileManager defaultManager] createDirectoryAtPath:finalPath
  64. withIntermediateDirectories :YES
  65. attributes :nil
  66. error :nil];
  67. }
  68.  
  69. return finalPath;
  70. }
  71.  
  72. + (NSString*)genRandomFileName {
  73. NSTimeInterval timeStamp = [[NSDate date ] timeIntervalSince1970];
  74. uint32_t random = arc4random_uniform(10000);
  75. return [NSString stringWithFormat:@ "%@-%@.png" , @( timeStamp ), @(random)];
  76. }
  77.  
  78. @ end  

Summarize

It took about 3 days to complete this function from selection to implementation. Due to time constraints, many areas are not optimized. If you have any suggestions, please leave me a message and I will continue to improve it. If you have time, you are welcome to join this project and we can do better together. The code is open source and can be found in the link below.

Code hosting location

Client code open source hosting address: RichTextEditDemo

The file server code implemented in Java is open source and hosted at: [javawebserverdemo] (http://git.oschina.net/dhar/javawebdemo)

Reference Links

  • iOS UITextView input content real-time update cell height
  • How to implement the mixed text and image editing function on mobile devices?
  • JavaWeb implements file upload and download function example analysis
  • Use NSURLSessionUploadTask to upload files

<<:  Android Network Security Configuration

>>:  Summary of Commonly Used Open Source Frameworks on Android GitHub in 2017

Recommend

Android-menudrawer-master open source powerful sidebar menu

Source code introduction There are different styl...

What should I do if the Douyin showcase score is low?

There is no doubt that Douyin is a very popular s...

In-depth analysis of why Android apps are of low quality and crash

Unlike the situation a few years ago when iOS was...

How much does the Tianhe WeChat food ordering and takeaway app cost per year?

Recently, the epidemic in my hometown has become ...

Online music listening is so popular that music downloads hit an 8-year low

After records were abandoned by the entire music ...

After its head was chopped off, this chicken lived for another 2 years!

In April 1945, during the fierce battle as the Un...

Smart Autumn Harvest | "New Employees" in the Tomato Planting Greenhouse Report

Produced by: Science Popularization China Author:...