iOS使用UITableView實現(xiàn)的富文本編輯器
前言
公司最近做一個項目,其中有一個模塊是富文本編輯模塊,之前沒做個類似的功能模塊,本來以為這個功能很常見應該會有已經(jīng)造好的輪子,或許我只要找到輪子,研究下輪子,然后修改打磨輪子,這件事就八九不離十了。不過,還是 too young to simple 了,有些事,還是得自己去面對的,或許這就叫做成長,感覺最近一年,對于編程這件事,更多了一點熱愛,我感覺我不配過只會復制粘貼代碼的人生,編程需要有挑戰(zhàn)。所以,遇到困難,保持一份正念,路其實就在腳下,如果沒有困難,那就制造困哪,迎難而上,人生沒有白走的路,每一步都算數(shù),毒雞湯就到此為止,下面是干貨了。
結(jié)果
沒圖沒真相,下面是幾張實現(xiàn)的效果圖
實現(xiàn)的功能包含了:
- 編輯器文字編輯
- 編輯器圖片編輯
- 編輯器圖文混排編輯
- 編輯器圖片上傳,帶有進度和失敗提示,可以重新上傳操作
- 編輯器模型轉(zhuǎn)換為HTML格式內(nèi)容
- 配套的Java實現(xiàn)的服務器
以及客戶端代碼開源托管地址:RichTextEditDemo
還有java實現(xiàn)的文件服務器代碼開源托管地址:javawebserverdemo
調(diào)研分析
基本上有以下幾種的實現(xiàn)方案:
- UITextView結(jié)合NSAttributeString實現(xiàn)圖文混排編輯,這個方案可以在網(wǎng)上找到對應的開源代碼,比如 SimpleWord 的實現(xiàn)就是使用這種方式,不過缺點是圖片不能有交互,比如說在圖片上添加進度條,添加上傳失敗提示,圖片點擊事件處理等等都不行,如果沒有這種需求那么可以選擇這種方案。
- 使用WebView通過js和原生的交互實現(xiàn),比如 WordPress-Editor 、RichTextDemo ,主要的問題就是性能不夠好,還有需要你懂得前端知識才能上手。
- 使用CoreText或者TextKit,這種也有實現(xiàn)方案的開源代碼,比如說這個 YYText ,這個很有名氣,不過他使用的圖片插入編輯圖片的位置是固定的,文字是圍繞著圖片,所以這種不符合我的要求,如果要使用這種方案,那修改的地方有很多,并且CoreText/TextKit使用是有一定的門檻的。
- 使用UITableView結(jié)合UITextView的假實現(xiàn),主要的思路是每個Cell是一個文字輸入的UITextView或者是用于顯示圖片使用的UITextView,圖片顯示之所以是選擇UITextView是因為圖片位置需要有輸入光標,所以使用UITextView結(jié)合NSAttributeString的方式正好可以實現(xiàn)這個功能。圖片和文字混排也就是顯示圖片的Cell和顯示文字的Cell混排就可以實現(xiàn)了,主要的工作量是處理光標位置輸入以及處理光標位置刪除。
選型定型
前面三種方案都有了開源的實現(xiàn),不過都不滿足需要,只有第二種方案會比較接近一點,不過WebView結(jié)合JS的操作確實是性能不夠好,內(nèi)存占用也比較高, WordPress-Editor 、RichTextDemo ,這兩種方法實現(xiàn)的編輯器會明顯的感覺到不夠流暢,并且離需要還有挺大的距離,所有沒有選擇在這基礎上進行二次開發(fā)。第三種方案在網(wǎng)上有比較多的人推薦,不過我想他們大概也只是推薦而已,真正實現(xiàn)起來需要花費大把的時間,需要填的坑有很多,考慮到時間有限,以及項目的進度安排,這個坑我就沒有去踩了。
我最終選擇的是第四種方案,這種方案好的地方就是UITableView、UITextView都是十分熟悉的組件,使用組合的模式通過以上的分析,理論上是沒有問題的,并且,UITableView有復用Cell的優(yōu)勢,所以時間性能和空間性能應該是不差的。
實現(xiàn)細節(jié)分析
使用UITableView集合UITextView的這種方案有很多細節(jié)需要注意
- Cell中添加UITextView,文字輸入換行或者超過一行Cell高度自動伸縮處理
- Cell中添加UITextView顯示圖片的處理
- 光標處刪除和添加圖片的處理,換行的處理
需要解決問題,好的是有些是已經(jīng)有人遇到并且解決的,其他的即使其他人沒有遇到過,作為***個吃螃蟹的人,我們詳細的去分析下其實也不難
1.這個問題剛好有人遇到過,這里就直接發(fā)鏈接了iOS UITextView 輸入內(nèi)容實時更新cell的高度
實現(xiàn)上面效果的基本原理是:
1.在 cell 中設置好 text view 的 autolayout,讓 cell 可以根據(jù)內(nèi)容自適應大小
2.text view 中輸入內(nèi)容,根據(jù)內(nèi)容更新 textView 的高度
3.調(diào)用 tableView 的 beginUpdates 和 endUpdates,重新計算 cell 的高度
4.將 text view 更新后的數(shù)據(jù)保存,以免 table view 滾動超過一屏再滾回來 text view 中的數(shù)據(jù)又不刷新成原來的數(shù)據(jù)了。
2.這個問題很簡單,使用屬性文字就行了,下面直接貼代碼了
NSAttributedString結(jié)合NSTextAttachment就行了
- /**
- 顯示圖片的屬性文字
- */
- - (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;
- // 設置Size
- CGRect tmpImageFrame = rect;
- tmpImageFrame.size.height += MMEditConfig.editAreaTopPadding + MMEditConfig.editAreaBottomPadding;
- _imageFrame = tmpImageFrame;
- }
- return _attrString;
- }
3.這個問題比較棘手,我自己也是先把可能的情況列出來,然后一個一個分支去處理這些情況,不難就是麻煩,下面的文本是我寫在 備忘錄 上的情況分析,- [x] 這種標識這種情況已經(jīng)實現(xiàn),- [ ] 這種標識暫時未實現(xiàn),后面這部分會進行優(yōu)化,主要的工作已經(jīng)完成了,優(yōu)化的工作量不會很大了。
UITableView實現(xiàn)的編輯器 return換行情況分析: - [x] text節(jié)點:不處理 - [x] Image節(jié)點-前面:上面是text,光標移動到上面一行,并且在***添加一個換行,定位光標在***將 - [x] Image節(jié)點-前面:上面是圖片或者空,在上面添加一個Text節(jié)點,光標移動到上面一行, - [x] Image節(jié)點-后面:下面是圖片或者空,在下面添加一個Text節(jié)點,光標移動到下面一行, - [x] Image節(jié)點-后面:下面是text,光標移動到下面一行,并且在最前面添加一個換行,定位光標在最前面 Delete情況分析: - [x] Text節(jié)點-當前的Text不為***面-:上面是圖片,定位光標到上面圖片的*** - [x] Text節(jié)點-當前的Text不為***面-:上面是Text,合并當前Text和上面Text 這種情況不存在,在圖片刪除的時候進行合并 - [x] Text節(jié)點-當前的Text不為***面-:上面是空,不處理 - [x] Text節(jié)點-當前的Text為***面-沒有其他元素(***個)-:不處理 - [x] Text節(jié)點-當前的Text為***面-有其他元素-:刪除這一行,定位光標到下面圖片的*** - [x] Text節(jié)點-當前的Text不為空-后面-:正常刪除 - [x] Text節(jié)點-當前的Text為空-后面-:正常刪除,和第三種情況:為空的情況處理一樣 - [x] Image節(jié)點-前面-上面為Text(不為空)/Image定位到上面元素的后面 - [x] Image節(jié)點-前面-上面為Text(為空):刪除上面Text節(jié)點 - [x] Image節(jié)點-前面-上面為空:不處理 - [ ] Image節(jié)點-后面-上面為空(***個位置)-列表只有一個元素:添加一個Text節(jié)點,刪除當前Image節(jié)點,光標放在添加的Text節(jié)點上 ****TODO:上面元素不處于顯示區(qū)域不可定位**** - [x] Image節(jié)點-后面-上面為空(***個位置)-列表多于一個元素:刪除當前節(jié)點,光標放在后面元素之前 - [x] Image節(jié)點-后面-上面為圖片:刪除Image節(jié)點,定位到上面元素的后面 - [x] Image節(jié)點-后面-上面為Text-下面為圖片或者空:刪除Image節(jié)點,定位到上面元素的后面 - [x] Image節(jié)點-后面-上面為Text-下面為Text:刪除Image節(jié)點,合并下面的Text到上面,刪除下面Text節(jié)點,定位到上面元素的后面 圖片節(jié)點添加文字的情況分析: - [ ] 前面輸入文字 - [ ] 后面輸入文字 插入圖片的情況分析: - [x] activeIndex是Image節(jié)點-后面:下面添加一個圖片節(jié)點 - [x] activeIndex是Image節(jié)點-前面:上面添加一個圖片節(jié)點 - [x] activeIndex是Text節(jié)點:拆分光標前后內(nèi)容插入一個圖片節(jié)點和Text節(jié)點 - [x] 圖片插入之后更新 activeIndexPath
基本上分析就到此為止了,talk is cheap, show me code,下面就是代碼實現(xiàn)了。
代碼實現(xiàn)
編輯模塊
文字輸入框的Cell實現(xiàn)
下面是文字輸入框的Cell的主要代碼,包含了
- 初始設置文字編輯Cell的高度、文字內(nèi)容、是否顯示Placeholder
- 在 UITextViewDelegate 回調(diào)方法 textViewDidChange 中處理Cell的高度自動拉伸
- 刪除的回調(diào)方法中處理前面刪除和后面刪除,刪除回調(diào)的代理方法是繼承 UITextView 重寫 deleteBackward 方法進行的回調(diào),具體的可以額查看 MMTextView 這個類的實現(xiàn),很簡單的一個實現(xiàn)。
- @implementation MMRichTextCell
- // ...
- - (void)updateWithData:(id)data indexPath:(NSIndexPath*)indexPath {
- if ([data isKindOfClass:[MMRichTextModel class]]) {
- MMRichTextModel* textModel = (MMRichTextModel*)data;
- _textModel = textModel;
- // 重新設置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;
- // 手動調(diào)用回調(diào)方法修改
- [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];
- // 更新模型數(shù)據(jù)
- _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) {
- // 重新設置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 {
- // 處理刪除
- 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
顯示圖片Cell的實現(xiàn)
下面顯示圖片Cell的實現(xiàn),主要包含了
- 初始設置文字編輯Cell的高度、圖片顯示內(nèi)容
- 在 UITextViewDelegate 回調(diào)方法 shouldChangeTextInRange 中處理換行和刪除,這個地方的刪除和Text編輯的Cell不一樣,所以在這邊做了特殊的處理,具體看一看 shouldChangeTextInRange 這個方法的處理方式。
- 處理圖片上傳的進度回調(diào)、失敗回調(diào)、成功回調(diào)
- @implementation MMRichImageCell
- // 省略部否代碼...
- - (void)updateWithData:(id)data {
- if ([data isKindOfClass:[MMRichImageModel class]]) {
- MMRichImageModel* imageModel = (MMRichImageModel*)data;
- // 設置舊的數(shù)據(jù)delegate為nil
- _imageModel.uploadDelegate = nil;
- _imageModel = imageModel;
- // 設置新的數(shù)據(jù)delegate
- _imageModel.uploadDelegate = self;
- CGFloat width = [MMRichTextConfig sharedInstance].editAreaWidth;
- NSAttributedString* imgAttrStr = [_imageModel attrStringWithContainerWidth:width];
- _textView.attributedText = imgAttrStr;
- // 重新設置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;
- // 根據(jù)上傳的狀態(tài)設置圖片信息
- 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 {
- // 處理換行
- if ([text isEqualToString:@"\n"]) {
- if (range.location == 0 && range.length == 0) {
- // 在前面添加換行
- if ([self.delegate respondsToSelector:@selector(mm_preInsertTextLineAtIndexPath:textContent:)]) {
- [self.delegate mm_preInsertTextLineAtIndexPath:[self curIndexPath]textContent:nil];
- }
- } else if (range.location == 1 && range.length == 0) {
- // 在后面添加換行
- if ([self.delegate respondsToSelector:@selector(mm_postInsertTextLineAtIndexPath:textContent:)]) {
- [self.delegate mm_postInsertTextLineAtIndexPath:[self curIndexPath] textContent:nil];
- }
- } else if (range.location == 0 && range.length == 2) {
- // 選中和換行
- }
- }
- // 處理刪除
- if ([text isEqualToString:@""]) {
- NSRange selRange = textView.selectedRange;
- if (selRange.location == 0 && selRange.length == 0) {
- // 處理刪除
- if ([self.delegate respondsToSelector:@selector(mm_preDeleteItemAtIndexPath:)]) {
- [self.delegate mm_preDeleteItemAtIndexPath:[self curIndexPath]];
- }
- } else if (selRange.location == 1 && selRange.length == 0) {
- // 處理刪除
- if ([self.delegate respondsToSelector:@selector(mm_PostDeleteItemAtIndexPath:)]) {
- [self.delegate mm_PostDeleteItemAtIndexPath:[self curIndexPath]];
- }
- } else if (selRange.location == 0 && selRange.length == 2) {
- // 處理刪除
- 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 :::::::......
- // 上傳進度回調(diào)
- - (void)uploadProgress:(float)progress {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.progressView setProgress:progress];
- });
- }
- // 上傳失敗回調(diào)
- - (void)uploadFail {
- [self.progressView setProgress:0.01f];
- self.reloadButton.hidden = NO;
- }
- // 上傳完成回調(diào)
- - (void)uploadDone {
- [self.progressView setProgress:1.0f];
- }
- @end
圖片上傳模塊
圖片上傳模塊中,上傳的元素和上傳回調(diào)抽象了對應的協(xié)議,圖片上傳模塊是一個單利的管理類,管理進行中的上傳元素和排隊中的上傳元素,
圖片上傳的元素和上傳回調(diào)的抽象協(xié)議
- @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
圖片上傳的管理類
圖片上傳使用的是 NSURLSessionUploadTask 類處理
- 在 completionHandler 回調(diào)中處理結(jié)果
- 在NSURLSessionDelegate 的方法 URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend: 中處理上傳進度
- 在NSURLSessionDelegate 的方法 URLSession:task:didCompleteWithError: 中處理失敗
上傳管理類的關鍵代碼如下:
- @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) {
- // 添加下一個任務
- 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"];
- }
- // 成功回調(diào)
- // 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];
- // 添加到映射中
- [self.uploadingTaskIDToUploadItemMap setObject:uploadItem forKey:@(uploadtask.taskIdentifier)];
- }
- #pragma mark - ......::::::: NSURLSessionDelegate :::::::......
- -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
- NSLog(@"didCompleteWithError = %@",error.description);
- // 失敗回調(diào)
- 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));
- // 進度回調(diào)
- id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(task.taskIdentifier)];
- if ([uploadItem respondsToSelector:@selector(mm_uploadProgress:)]) {
- [uploadItem mm_uploadProgress:(totalBytesSent * 1.0f/totalBytesExpectedToSend)];
- }
- }
- @end
圖片上傳的回調(diào)會通過 UploadItemCallBackProtocal 協(xié)議的實現(xiàn)方法回調(diào)到圖片編輯的模型中,更新對應的數(shù)據(jù)。圖片編輯的數(shù)據(jù)模型是 MMRichImageModel ,該模型實現(xiàn)了 UploadItemProtocal 和
- @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
內(nèi)容處理模塊
最終是要把內(nèi)容序列化然后上傳到服務端的,我們的序列化方案是轉(zhuǎn)換為HTML,內(nèi)容處理模塊主要包含了以下幾點:
- 生成HTML格式的內(nèi)容
- 驗證內(nèi)容是否有效,判斷圖片時候全部上傳成功
- 壓縮圖片
- 保存圖片到本地
這部分收尾的工作比較的簡單,下面是實現(xiàn)代碼:
- #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];
- }
- // 添加換行
- 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;
- }
- // 創(chuàng)建文件夾
- + (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
總結(jié)
這個功能從選型定型到實現(xiàn)大概花費了3天的時間,因為時間原因,有很多地方優(yōu)化的不到位,如果看官有建議意見希望給我留言,我會繼續(xù)完善,或者你有時間歡迎加入這個項目,可以一起做得更好,代碼開源看下面的鏈接。
代碼托管位置
客戶端代碼開源托管地址:RichTextEditDemo
java實現(xiàn)的文件服務器代碼開源托管地址:[javawebserverdemo] (http://git.oschina.net/dhar/javawebdemo)
參考鏈接