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: - 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.
- 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.
- 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.
- 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. - Add UITextView to Cell, and the height of the Cell will automatically expand or contract if the text input wraps or exceeds one line
- Add UITextView to display pictures in Cell
- 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 - /**
- Displays the attribute text of the image
- */
- - (NSAttributedString*)attrStringWithContainerWidth:(NSInteger)containerWidth {
- if (!_attrString) {
- CGFloat showImageWidth = containerWidth - MMEditConfig.editAreaLeftPadding - MMEditConfig.editAreaRightPadding - MMEditConfig.imageDeltaWidth;
- NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
- CGRect rect = CGRectZero;
- rect. size .width = showImageWidth;
- rect. size .height = showImageWidth * self.image. size .height / self.image. size .width;
- textAttachment.bounds = rect;
- textAttachment.image = self.image;
-
- NSAttributedString *attachmentString = [NSAttributedString attributedStringWithAttachment:textAttachment];
- NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@ "" ];
- [attributedString insertAttributedString:attachmentString atIndex:0];
- _attrString = attributedString;
-
- // Set the Size
- CGRect tmpImageFrame = rect;
- tmpImageFrame. size .height += MMEditConfig.editAreaTopPadding + MMEditConfig.editAreaBottomPadding;
- _imageFrame = tmpImageFrame;
- }
- return _attrString;
- }
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 - Initially set the height, text content, and whether to display the Placeholder of the text editing cell
- Handle automatic stretching of Cell height in UITextViewDelegate callback method textViewDidChange
- 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.
- @implementation MMRichTextCell
- // ...
- - (void)updateWithData:(id)data indexPath:(NSIndexPath*)indexPath {
- if ([data isKindOfClass:[MMRichTextModel class]]) {
- MMRichTextModel* textModel = (MMRichTextModel*)data;
- _textModel = textModel;
-
- // Reset the constraints of the TextView
- [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
- make. left . top . right .equalTo(self);
- make.bottom.equalTo(self).priority(900);
- make.height.equalTo(@(textModel.textFrame. size .height));
- }];
- // Content
- _textView.text = textModel.textContent;
- // Placeholder
- if (indexPath.row == 0) {
- self.textView.showPlaceHolder = YES;
- } else {
- self.textView.showPlaceHolder = NO ;
- }
- }
- }
-
- - (void)beginEditing {
- [_textView becomeFirstResponder];
-
- if (![_textView.text isEqualToString:_textModel.textContent]) {
- _textView.text = _textModel.textContent;
-
- // Manually call the callback method to modify
- [self textViewDidChange:_textView];
- }
-
- if ([self curIndexPath].row == 0) {
- self.textView.showPlaceHolder = YES;
- } else {
- self.textView.showPlaceHolder = NO ;
- }
- }
-
- #pragma mark - ......::::::: UITextViewDelegate :::::::...
-
- - (void)textViewDidChange:(UITextView *)textView {
- CGRect frame = textView.frame;
- CGSize constraintSize = CGSizeMake(frame. size .width, MAXFLOAT);
- CGSize size = [textView sizeThatFits:constraintSize];
-
- // Update model data
- _textModel.textFrame = CGRectMake(frame.origin.x, frame.origin.y, frame. size .width, size .height);
- _textModel.textContent = textView.text;
- _textModel.selectedRange = textView.selectedRange;
- _textModel.isEditing = YES;
-
- if ( ABS (_textView.frame. size .height - size .height) > 5) {
-
- // Reset the constraints of the TextView
- [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
- make. left . top . right .equalTo(self);
- make.bottom.equalTo(self).priority(900);
- make.height.equalTo(@(_textModel.textFrame. size .height));
- }];
-
- UITableView* tableView = [self containerTableView];
- [tableView beginUpdates];
- [tableView endUpdates];
- }
- }
-
- - (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
- textView.inputAccessoryView = [self.delegate mm_inputAccessoryView];
- if ([self.delegate respondsToSelector:@selector(mm_updateActiveIndexPath:)]) {
- [self.delegate mm_updateActiveIndexPath:[self curIndexPath]];
- }
- return YES;
- }
-
- - (BOOL)textViewShouldEndEditing:(UITextView *)textView {
- textView.inputAccessoryView = nil;
- return YES;
- }
-
- - (void)textViewDeleteBackward:(MMTextView *)textView {
- // Handle deletion
- NSRange selRange = textView.selectedRange;
- if (selRange.location == 0) {
- if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) {
- [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]];
- }
- } else {
- if ([self.delegate respondsToSelector:@selector(mm_PostDeleteItemAtIndexPath:)]) {
- [self.delegate mm_PostDeleteItemAtIndexPath:[self curIndexPath]];
- }
- }
- }
-
- @ end
Implementation of displaying picture Cell The implementation of the picture Cell is shown below, which mainly includes - Initially set the height of the text editing Cell and the image display content
- 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.
- Process the progress callback, failure callback, and success callback of image upload
- @implementation MMRichImageCell
- // Omit some negative code...
- - (void)updateWithData:(id)data {
- if ([data isKindOfClass:[MMRichImageModel class]]) {
- MMRichImageModel* imageModel = (MMRichImageModel*)data;
- // Set the old data delegate to nil
- _imageModel.uploadDelegate = nil;
- _imageModel = imageModel;
- // Set the new data delegate
- _imageModel.uploadDelegate = self;
-
- CGFloat width = [MMRichTextConfig sharedInstance].editAreaWidth;
- NSAttributedString* imgAttrStr = [_imageModel attrStringWithContainerWidth:width];
- _textView.attributedText = imgAttrStr;
- // Reset the constraints of the TextView
- [self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
- make. left . top . right .equalTo(self);
- make.bottom.equalTo(self).priority(900);
- make.height.equalTo(@(imageModel.imageFrame. size .height));
- }];
-
- self.reloadButton.hidden = YES;
-
- // Set the image information according to the uploaded status
- if (_imageModel.isDone) {
- self.progressView.hidden = NO ;
- self.progressView.progress = _imageModel.uploadProgress;
- self.reloadButton.hidden = YES;
- }
- if (_imageModel.isFailed) {
- self.progressView.hidden = NO ;
- self.progressView.progress = _imageModel.uploadProgress;
- self.reloadButton.hidden = NO ;
- }
- if (_imageModel.uploadProgress > 0) {
- self.progressView.hidden = NO ;
- self.progressView.progress = _imageModel.uploadProgress;
- self.reloadButton.hidden = YES;
- }
- }
- }
-
- #pragma mark - ......::::::: UITextViewDelegate :::::::...
-
- - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
- // Handle line breaks
- if ([text isEqualToString:@ "\n" ]) {
- if (range.location == 0 && range.length == 0) {
- // Add a line break in front
- if ([self.delegate respondsToSelector:@selector(mm_preInsertTextLineAtIndexPath:textContent:)]) {
- [self.delegate mm_preInsertTextLineAtIndexPath:[self curIndexPath]textContent:nil];
- }
- } else if (range.location == 1 && range.length == 0) {
- // Add a line break after
- if ([self.delegate respondsToSelector:@selector(mm_postInsertTextLineAtIndexPath:textContent:)]) {
- [self.delegate mm_postInsertTextLineAtIndexPath:[self curIndexPath] textContent:nil];
- }
- } else if (range.location == 0 && range.length == 2) {
- //Select and wrap
- }
- }
-
- // Handle deletion
- if ([text isEqualToString:@ "" ]) {
- NSRange selRange = textView.selectedRange;
- if (selRange.location == 0 && selRange.length == 0) {
- // Handle deletion
- if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) {
- [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]];
- }
- } else if (selRange.location == 1 && selRange.length == 0) {
- // Handle deletion
- if ([self.delegate respondsToSelector:@selector(mm_PostDeleteItemAtIndexPath:)]) {
- [self.delegate mm_PostDeleteItemAtIndexPath:[self curIndexPath]];
- }
- } else if (selRange.location == 0 && selRange.length == 2) {
- // Handle deletion
- if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) {
- [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]];
- }
- }
- }
- return NO ;
- }
-
- - (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
- textView.inputAccessoryView = [self.delegate mm_inputAccessoryView];
- if ([self.delegate respondsToSelector:@selector(mm_updateActiveIndexPath:)]) {
- [self.delegate mm_updateActiveIndexPath:[self curIndexPath]];
- }
- return YES;
- }
-
- - (BOOL)textViewShouldEndEditing:(UITextView *)textView {
- textView.inputAccessoryView = nil;
- return YES;
- }
-
-
- #pragma mark - ......::::::: MMRichImageUploadDelegate :::::::...
-
- // Upload progress callback
- - (void)uploadProgress:( float )progress {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.progressView setProgress:progress];
- });
- }
-
- // Upload failed callback
- - (void)uploadFail {
- [self.progressView setProgress:0.01f];
- self.reloadButton.hidden = NO ;
- }
-
- // Upload completion callback
- - (void)uploadDone {
- [self.progressView setProgress:1.0f];
- }
-
-
- @ 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 - @protocol UploadItemCallBackProtocal <NSObject>
-
- - (void)mm_uploadProgress:( float )progress;
- - (void)mm_uploadFailed;
- - (void)mm_uploadDone:(NSString*)remoteImageUrlString;
-
- @ end
-
- @protocol UploadItemProtocal <NSObject>
-
- - (NSData*)mm_uploadData;
- - (NSURL*)mm_uploadFileURL;
-
- @ end
Image upload management class Image upload is handled by NSURLSessionUploadTask class - Process the result in the completionHandler callback
- Handle upload progress in the NSURLSessionDelegate method URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
- Handle failure in the NSURLSessionDelegate method URLSession:task:didCompleteWithError:
The key codes of the upload management class are as follows: - @interface MMFileUploadUtil () <NSURLSessionDataDelegate, NSURLSessionDelegate, NSURLSessionTaskDelegate>
- @property (strong,nonatomic) NSURLSession * session;
- @property (nonatomic, strong) NSMutableArray* uploadingItems;
- @property (nonatomic, strong) NSMutableDictionary* uploadingTaskIDToUploadItemMap;
- @property (nonatomic, strong) NSMutableArray* todoItems;
-
- @property (nonatomic, assign) NSInteger maxUploadTask;
- @ end
-
- @implementation MMFileUploadUtil
-
- - (void)addUploadItem:(id<UploadItemProtocal, UploadItemCallBackProtocal>)uploadItem {
- [self.todoItems addObject:uploadItem];
- [self startNextUploadTask];
- }
-
- - (void)startNextUploadTask {
- if (self.uploadingItems. count < _maxUploadTask) {
- // Add the next task
- if ( self.todoItems.count > 0) {
- id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = self.todoItems.firstObject;
- [self.uploadingItems addObject:uploadItem];
- [self.todoItems removeObject:uploadItem];
-
- [self uploadItem:uploadItem];
- }
- }
- }
-
- - (void)uploadItem:(id<UploadItemProtocal, UploadItemCallBackProtocal>)uploadItem {
- NSMutableURLRequest * request = [self TSuploadTaskRequest];
-
- NSData* uploadData = [uploadItem mm_uploadData];
- NSData* totalData = [self TSuploadTaskRequestBody:uploadData];
-
- __block NSURLSessionUploadTask * uploadtask = nil;
- uploadtask = [self.session uploadTaskWithRequest:request fromData:totalData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
- NSString* result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
- NSLog(@ "completionHandler %@" , result);
-
- NSString* imgUrlString = @ "" ;
- NSError *JSONSerializationError;
- id obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&JSONSerializationError];
- if ([obj isKindOfClass:[NSDictionary class]]) {
- imgUrlString = [obj objectForKey:@ "url" ];
- }
- // Success callback
- // FIXME: ZYT uploadtask? ? ?
- id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(uploadtask.taskIdentifier)];
- if (uploadItem) {
- if ([uploadItem respondsToSelector:@selector(mm_uploadDone:)]) {
- [uploadItem mm_uploadDone:imgUrlString];
- }
- [self.uploadingTaskIDToUploadItemMap removeObjectForKey:@(uploadtask.taskIdentifier)];
- [self.uploadingItems removeObject:uploadItem];
- }
-
- [self startNextUploadTask];
- }];
- [uploadtask resume];
-
- // Add to the map
- [self.uploadingTaskIDToUploadItemMap setObject:uploadItem forKey:@(uploadtask.taskIdentifier)];
- }
-
- #pragma mark - ......::::::: NSURLSessionDelegate :::::::...
-
- -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
- NSLog(@ "didCompleteWithError = %@" ,error.description);
-
- // Failure callback
- if (error) {
- id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(task.taskIdentifier)];
- if (uploadItem) {
- if ([uploadItem respondsToSelector:@selector(mm_uploadFailed)]) {
- [uploadItem mm_uploadFailed];
- }
- [self.uploadingTaskIDToUploadItemMap removeObjectForKey:@(task.taskIdentifier)];
- [self.uploadingItems removeObject:uploadItem];
- }
- }
-
- [self startNextUploadTask];
- }
-
- -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
- NSLog(@ "bytesSent:%@-totalBytesSent:%@-totalBytesExpectedToSend:%@" , @(bytesSent), @(totalBytesSent), @(totalBytesExpectedToSend));
-
- // Progress callback
- id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(task.taskIdentifier)];
- if ([uploadItem respondsToSelector:@selector(mm_uploadProgress:)]) {
- [uploadItem mm_uploadProgress:(totalBytesSent * 1.0f/totalBytesExpectedToSend)];
- }
- }
-
- @ 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 - @implementation MMRichImageModel
-
- - (void)setUploadProgress:( float )uploadProgress {
- _uploadProgress = uploadProgress;
- if ([_uploadDelegate respondsToSelector:@selector(uploadProgress:)]) {
- [_uploadDelegate uploadProgress:uploadProgress];
- }
- }
-
- - (void)setIsDone:(BOOL)isDone {
- _isDone = isDone;
- if ([_uploadDelegate respondsToSelector:@selector(uploadDone)]) {
- [_uploadDelegate uploadDone];
- }
- }
-
- - (void)setIsFailed:(BOOL)isFailed {
- _isFailed = isFailed;
- if ([_uploadDelegate respondsToSelector:@selector(uploadFail)]) {
- [_uploadDelegate uploadFail];
- }
- }
-
-
- #pragma mark - ......::::::: UploadItemCallBackProtocal :::::::...
- - (void)mm_uploadProgress:( float )progress {
- self.uploadProgress = progress;
- }
-
- - (void)mm_uploadFailed {
- self.isFailed = YES;
- }
-
- - (void)mm_uploadDone:(NSString *)remoteImageUrlString {
- self.remoteImageUrlString = remoteImageUrlString;
- self.isDone = YES;
- }
-
-
- #pragma mark - ......::::::: UploadItemProtocal :::::::...
- - (NSData*)mm_uploadData {
- return UIImageJPEGRepresentation(_image, 0.6);
- }
-
- - (NSURL*)mm_uploadFileURL {
- return nil;
- }
-
- @ 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: - #define kRichContentEditCache @ "RichContentEditCache"
-
-
- @implementation MMRichContentUtil
-
- + (NSString*)htmlContentFromRichContents:(NSArray*)richContents {
- NSMutableString *htmlContent = [NSMutableString string];
-
- for ( int i = 0; i< richContents. count ; i++) {
- NSObject* content = richContents[i];
- if ([content isKindOfClass:[MMRichImageModel class]]) {
- MMRichImageModel* imgContent = (MMRichImageModel*)content;
- [htmlContent appendString:[NSString stringWithFormat:@ "<img src=\"%@\" width=\"%@\" height=\"%@\" />" , imgContent.remoteImageUrlString, @(imgContent.image. size .width), @(imgContent.image. size .height)]];
- } else if ([content isKindOfClass:[MMRichTextModel class]]) {
- MMRichTextModel* textContent = (MMRichTextModel*)content;
- [htmlContent appendString:textContent.textContent];
- }
-
- // Add a line break
- if (i != richContents. count - 1) {
- [htmlContent appendString:@ "<br />" ];
- }
- }
-
- return htmlContent;
- }
-
- + (BOOL)validateRichContents:(NSArray*)richContents {
- for ( int i = 0; i< richContents. count ; i++) {
- NSObject* content = richContents[i];
- if ([content isKindOfClass:[MMRichImageModel class]]) {
- MMRichImageModel* imgContent = (MMRichImageModel*)content;
- if (imgContent.isDone == NO ) {
- return NO ;
- }
- }
- }
- return YES;
- }
-
- + (UIImage*)scaleImage:(UIImage*)originalImage {
- float scaledWidth = 1242;
- return [originalImage scaletoSize:scaledWidth];
- }
-
- + (NSString*)saveImageToLocal:(UIImage*)image {
- NSString *path=[self createDirectory:kRichContentEditCache];
- NSData* data = UIImageJPEGRepresentation(image, 1.0);
- NSString *filePath = [path stringByAppendingPathComponent:[self.class genRandomFileName]];
- [data writeToFile:filePath atomically:YES];
- return filePath;
- }
-
- // Create a folder
- + (NSString *)createDirectory:(NSString *)path {
- BOOL isDir = NO ;
- NSString *finalPath = [CACHE_PATH stringByAppendingPathComponent:path];
-
- if (!([[NSFileManager defaultManager] fileExistsAtPath:finalPath
- isDirectory:&isDir]
- && isDir))
- {
- [[NSFileManager defaultManager] createDirectoryAtPath:finalPath
- withIntermediateDirectories :YES
- attributes :nil
- error :nil];
- }
-
- return finalPath;
- }
-
- + (NSString*)genRandomFileName {
- NSTimeInterval timeStamp = [[NSDate date ] timeIntervalSince1970];
- uint32_t random = arc4random_uniform(10000);
- return [NSString stringWithFormat:@ "%@-%@.png" , @( timeStamp ), @(random)];
- }
-
- @ 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
|