公司最近作一个项目,其中有一个模块是富文本编辑模块,以前没作个相似的功能模块,原本觉得这个功能很常见应该会有已经造好的轮子,或许我只要找到轮子,研究下轮子,而后修改打磨轮子,这件事就八九不离十了。不过,仍是 too young to simple 了,有些事,仍是得本身去面对的,或许这就叫作成长,感受最近一年,对于编程这件事,更多了一点热爱,我感受我不配过只会复制粘贴代码的人生,编程须要有挑战。因此,遇到困难,保持一份正念,路其实就在脚下,若是没有困难,那就制造困哪,迎难而上,人生没有白走的路,每一步都算数,毒鸡汤就到此为止,下面是干货了。html
实现的功能包含了:前端
后期有进行了性能的优化,能够看个人这篇文章: iOS使用Instrument-Time Profiler工具分析和优化性能问题java
以及客户端代码开源托管地址:MMRichTextEdit
还有java实现的文件服务器代码开源托管地址:javawebserverdemoios
没图没真相,下面是几张实现的效果图
git
基本上有如下几种的实现方案:github
前面三种方案都有了开源的实现,不过都不知足须要,只有第二种方案会比较接近一点,不过WebView结合JS的操做确实是性能不够好,内存占用也比较高, WordPress-Editor 、RichTextDemo ,这两种方法实现的编辑器会明显的感受到不够流畅,而且离须要还有挺大的距离,全部没有选择在这基础上进行二次开发。第三种方案在网上有比较多的人推荐,不过我想他们大概也只是推荐而已,真正实现起来须要花费大把的时间,须要填的坑有不少,考虑到时间有限,以及项目的进度安排,这个坑我就没有去踩了。
我最终选择的是第四种方案,这种方案好的地方就是UITableView、UITextView都是十分熟悉的组件,使用组合的模式经过以上的分析,理论上是没有问题的,而且,UITableView有复用Cell的优点,因此时间性能和空间性能应该是不差的。web
使用UITableView集合UITextView的这种方案有不少细节须要注意编程
须要解决问题,好的是有些是已经有人遇到而且解决的,其余的即便其余人没有遇到过,做为第一个吃螃蟹的人,咱们详细的去分析下其实也不难bash
实现上面效果的基本原理是:
1.在 cell 中设置好 text view 的 autolayout,让 cell 能够根据内容自适应大小
2.text view 中输入内容,根据内容更新 textView 的高度
3.调用 tableView 的 beginUpdates 和 endUpdates,从新计算 cell 的高度
4.将 text view 更新后的数据保存,以避免 table view 滚动超过一屏再滚回来 text view 中的数据又不刷新成原来的数据了。服务器
注意:上面文章中提到的思路是对的,不过在开发过程当中遇到一个问题:使用自动布局计算高度的方式调用 tableView 的 beginUpdates 和 endUpdates,从新计算 cell 的高度会出现一个严重的BUG,textView中的文字会偏移致使不在正确的位置,因此实际的项目中禁用了tableView自动计算Cell高度的特性,采用手动计算Cell高度的方式,具体的能够看个人项目代码。
2.这个问题很简单,使用属性文字就好了,下面直接贴代码了
NSAttributedString结合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] 这种标识这种状况已经实现,- [ ] 这种标识暂时未实现,后面这部分会进行优化,主要的工做已经完成了,优化的工做量不会很大了。
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的主要代码,包含了
UITextViewDelegate
回调方法 textViewDidChange
中处理Cell的高度自动拉伸UITextView
重写 deleteBackward
方法进行的回调,具体的能够额查看 MMTextView
这个类的实现,很简单的一个实现。@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;
// 手动调用回调方法修改
[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];
// 更新模型数据
_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的实现,主要包含了
UITextViewDelegate
回调方法 shouldChangeTextInRange
中处理换行和删除,这个地方的删除和Text编辑的Cell不同,因此在这边作了特殊的处理,具体看一看 shouldChangeTextInRange
这个方法的处理方式。@implementation MMRichImageCell
// 省略部否代码...
- (void)updateWithData:(id)data {
if ([data isKindOfClass:[MMRichImageModel class]]) {
MMRichImageModel* imageModel = (MMRichImageModel*)data;
// 设置旧的数据delegate为nil
_imageModel.uploadDelegate = nil;
_imageModel = imageModel;
// 设置新的数据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;
// 根据上传的状态设置图片信息
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 :::::::......
// 上传进度回调
- (void)uploadProgress:(float)progress {
dispatch_async(dispatch_get_main_queue(), ^{
[self.progressView setProgress:progress];
});
}
// 上传失败回调
- (void)uploadFail {
[self.progressView setProgress:0.01f];
self.reloadButton.hidden = NO;
}
// 上传完成回调
- (void)uploadDone {
[self.progressView setProgress:1.0f];
}
@end
复制代码
图片上传模块中,上传的元素和上传回调抽象了对应的协议,图片上传模块是一个单利的管理类,管理进行中的上传元素和排队中的上传元素,
@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
回调中处理结果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"];
}
// 成功回调
// 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);
// 失败回调
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));
// 进度回调
id<UploadItemProtocal, UploadItemCallBackProtocal> uploadItem = [self.uploadingTaskIDToUploadItemMap objectForKey:@(task.taskIdentifier)];
if ([uploadItem respondsToSelector:@selector(mm_uploadProgress:)]) {
[uploadItem mm_uploadProgress:(totalBytesSent * 1.0f/totalBytesExpectedToSend)];
}
}
@end
复制代码
图片上传的回调会经过 UploadItemCallBackProtocal
协议的实现方法回调到图片编辑的模型中,更新对应的数据。图片编辑的数据模型是 MMRichImageModel
,该模型实现了 UploadItemProtocal
和 UploadItemCallBackProtocal
协议,实现 UploadItemCallBackProtocal
的方法更新数据模型的同时,会经过delegate通知到Cell更新进度和失败成功的状态。 关键的实现以下
@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
复制代码
最终是要把内容序列化而后上传到服务端的,咱们的序列化方案是转换为HTML,内容处理模块主要包含了如下几点:
这部分收尾的工做比较的简单,下面是实现代码:
#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;
}
// 建立文件夹
+ (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
复制代码
这个功能从选型定型到实现大概花费了3天的时间,由于时间缘由,有不少地方优化的不到位,若是看官有建议意见但愿给我留言,我会继续完善,或者你有时间欢迎加入这个项目,能够一块儿作得更好,代码开源看下面的连接。
客户端代码开源托管地址:MMRichTextEdit
java实现的文件服务器代码开源托管地址:javawebserverdemo
iOS UITextView 输入内容实时更新cell的高度
如何实现移动端的图文混排编辑功能?
JavaWeb实现文件上传下载功能实例解析
使用NSURLSessionUploadTask完成上传文件