在上一篇教程中,我们介绍了如何用 MCCSframework 调用 iPhone 的相机和相册,接下来我们将继续上一篇教程的工作,介绍如何将用户选中的图片上传到后台。
在 MCCSframework 中,上传不属于网络 API,而是封装成了单独的模块。
图片的上传比较复杂,除了网络操作外,我们同样需要一些 UI 来展示用户上传成功了的图片,因此它也涉及了模型、cell 和子控制器。让我们首先从模型开始。
模型
首先,文件上传后,后台一般会以对象的形式来表示上传后的文件,我们将这个对象称作附件。我们需要创建一个模型类,用来代表附件。新建类 Attachment,类的定义如下:
@interface Attachment : JKModel
@property (nonatomic,copy) NSString *effect;
@property (nonatomic,copy) NSString *content;
@property (nonatomic,copy) NSString *mongoId;
@property (nonatomic,copy) NSString *id;
@property (nonatomic,copy) NSString *fileName;
@property (nonatomic,copy) NSString *title;
@property (nonatomic,copy) NSString *formart;
@property (nonatomic,copy) NSString *updateTime;
@property (nonatomic,copy) NSString *remark;
@property (nonatomic,copy) NSString *createTime;
@end
注意,每个后台对于附件的定义是不一样的,在真实项目中需要根据后台接口进行定义。
网络
虽然框架已经实现了通用的文件上传 API,但为了简化代码,以及不同项目的个性化需求,我们还是需要对上传逻辑进行进一步的封装。
首先新建一个类 UploadAPI,定义两个方法:
+(RACSignal*)uploadAsset:(PHAsset*)asset;
+(RACSignal*)uploadAssets:(NSArray<PHAsset*>*)assets;
这两个方法都是用于上传图片的,第一个是单个图片上传,第二个是多张图片上传。还记得上一篇教程中,我们已经将拍照后的照片也保存到相册中了,因此无论是用户从相册中选图片,还是直接拍照,我们在 PhotoPickSC 的 selectetAssets 数组中保存的都是 PHAsset 对象。因此这两个方法实际上都使用了 PHAsset 参数。
首先看第一个方法:
// MARK: ---- 上传单个 PHAsset
+(RACSignal*)uploadAsset:(PHAsset*)asset{
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSString* url = [NSString stringWithFormat:@"%@?i18n=%@",fileUploadUrl,[UWConfig userLanguage]];
[[self uploadAsset:asset key:@"files" headers:[self.class headers] toUrl:url] subscribeNext:^(id _Nullable x) {
NSError* error;
if([x[@"data"] isKindOfClass:[NSArray class]]){
NSArray *data = x[@"data"];
NSArray<Attachment*>* list = [Attachment arrayOfModelsFromDictionaries:data error:&error];
if(error==nil&&list.count>0){
[subscriber sendNext:list[0]];
[subscriber sendCompleted];
}else{
[subscriber sendError:APIError.dataParseError];
}
}else{
[subscriber sendError: APIError.dataFormatError];
}
} error:^(NSError * _Nullable error) {
[subscriber sendError:error];
}];
return nil;
}];
}
代码很简单,调用了框架提供的 NSObject+Upload 分类的 uploadAsset:key:header:toUrl
方法。值得注意的是 key 参数,是个实际上是 form 表单中 file 控件的 name,不同的后台可能会有不同定义,比如这里例子里 file 控件的 name 就叫 files。
其次是 header 参数,这里用的是 [self.class headers],这个一般是用它来传递 token 值的,比如这样:
+(NSDictionary<NSString*,NSString*>*)headers{
// 添加 token 到 headers 中
NSDictionary* headers;
NSString* token = appDelegate.auth.token;
if(token){
headers = @{@"Authorization":token};
}
return headers;
}
第二个方法是多文件上传。它更简单,只是做一个信号合并,将第一个方法的多个调用 merge 成一个信号而已:
+(RACSignal*)uploadAssets:(NSArray<PHAsset*>*)assets{
NSMutableArray* signals = [NSMutableArray new];
for(int i= 0;i < assets.count;i++){
RACSignal* sig = [UploadAPI uploadAsset:assets[i]];
if(sig)
[signals addObject:sig];
}
if(signals.count ==0)
return nil;
// 信号合并,当所有信号发送后,合并
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[[RACSignal combineLatest:signals] subscribeNext:^(RACTuple * _Nullable x) {
NSArray* arr = x.allObjects;
[subscriber sendNext:arr];
[subscriber sendCompleted];
} error:^(NSError * _Nullable error) {
[subscriber sendError:error];
}];
return nil;
}];
}
一般来说,上传功能只会是在 ViewController 中调用,因此将上面两个方法的调用封装到 ViewController 的分类中,会更方便一些。
新建 UIViewController 分类 UIViewController+Upload,在 .h 文件中添加方法:
-(void)uploadAssets:(NSArray<PHAsset*>*) assets completion:(void (^)(NSArray<Attachment*>* attachments))completion;
在 .m 文件中实现方法:
-(void)uploadAssets:(NSArray<PHAsset*>*) assets completion:(void (^)(NSArray<Attachment*>* attachments))completion{
NSMutableArray<Attachment*>* attachments = [NSMutableArray new];
if(assets && assets.count>0){
[self showHUDIndeterminate];
[[UploadAPI uploadAssets:assets] subscribeNext:^(NSArray* _Nullable x) {
[self hideHUDIndeterminate];
for(id obj in x){
if([obj isKindOfClass:Attachment.class])
[attachments addObject:obj];
}
if(completion)
completion(attachments);
} error:^(NSError * _Nullable error) {
[self hideHUDIndeterminate];
[self showHint:error.localizedDescription];
}];
}else{
if(completion)
completion(attachments);
}
}
这样,打开 MeetingAddVC.m 中,找到 configPageHeader 方法,添加一句:
@weakify(self)
self.pageHeader.rightAction = ^{
@strongify(self)
[self upload];
};
然后在 upload 方法中调用 uploadAssets 方法:
if(self.selectedAssets && self.selectedAssets.count > 0){// 如果有附件先上传附件再完成
[self uploadAssets:self.selectedAssets completion:^(NSArray<Attachment *> * _Nonnull x) {
self.meetingInfo.baseFileStorePlus = @{@"attachments":x};
[self publish];
}];
}else{// 如果无附件,直接完成
[self publish];
}
很简单,调用 uploadAssets 方法上传图片,然后在上传成功后取得后台返回的附件列表,将它塞进 meetingInfo 对象的 baseFileStorePlus (一个字典)中,连着整个表单一起提交到服务器。其中 publish 方法会调用后台的新增会议接口,这与我们今天的主题无关,跳过。
附件的展现
上传图片到服务器其实还算简单,但我们不能只是调一下 upload 接口就完了,上传后的图片同样需要展现给用户,特别是,如果我们想将新增界面和编辑界面共用一个界面时就更需要如此。比如,我们将 MeetingAddVC 同时用于新增会议和编辑会议。
这需要新的子控制器。
AttachmentsPreviewSC
新建子控制器 AttachmentsPreviewSC。在 .h 文件中,声明几个属性:
@property (strong, nonatomic) NSMutableArray<Attachment*>* attachments;
@property (strong, nonatomic) NSMutableArray<PHAsset*>* selectedAssets;
@property (strong, nonatomic) NSMutableArray<UIImage*>* selectedPhotos;
@property (strong, nonatomic) void(^photoArrayChangeBlock)(NSArray<PHAsset*>* assets);
跟 PhotoPickSC 很像,但多出了一个 attachments 数组。这很自然,因为除了用户选择的图片外,我们还要包含会议自身已有的附件图片。
在 .m 文件中同样如此,大部分代码和 PhotoPickSC 一样。因为这个类本来就是从 PhotoPickSC 复制而来的。不同的地方在于:
-
numberOfItems 方法
- (NSInteger)numberOfItems{ NSInteger i = _attachments==nil? 0: _attachments.count; i += _selectedAssets.count; i ++ ;// 最后一颗是”添加“按钮 return i; }
很简单,现在 cell 的数目不仅仅是用户拍照或选择的照片了,还应当包含已有附件的数目。
-
isAttachmentsAtIndex 方法
-(BOOL)isAttachmentsAtIndex:(NSInteger)index{ if(self.countOfAttachments==0){ return NO; }else{ return index >=0 && index < self.countOfAttachments; } }
这个方法是增加的,根据 cell 的 index 判断该 cell 是否是展现附件的 cell。
-
isSelectedAssetsAtIndex 方法
-(BOOL)isSelectedAssetsAtIndex:(NSInteger)index{ return ![self isAttachmentsAtIndex:index]; }
很简单,这个 cell 如果不是用于展现附件的,那么就是用于展现 PHAsset 的。
-
cellForItemAtIndex 方法
在这个方法中,多出了这段代码:
if([self isAttachmentsAtIndex:index]){ Attachment* attachment = _attachments[index]; // fileVO.thumbnail = nil; [AppUtil downloadWithImageId:attachment.id placeholdImage:@"logo" completion:^(UIImage * _Nonnull img) { cell.imageView.image = img; }]; }else{ cell.imageView.image = _selectedPhotos[index - self.countOfAttachments]; }
如果 cell 是用于展现附件的,那么从网络获取要展现的图片,否则直接从 selectedPhotos 中获取图片用于渲染 cell。
-
deleteHandler 方法
if([self isAttachmentsAtIndex:index]){// 删除附件 [_attachments removeObjectAtIndex:index]; }else{// 删除图片 [_selectedAssets removeObjectAtIndex:index-self.countOfAttachments]; [_selectedPhotos removeObjectAtIndex:index-self.countOfAttachments]; if(_photoArrayChangeBlock){ _photoArrayChangeBlock(_selectedAssets); } }
对于附件 cell,删除时删的是 attachments 数组。否则删的是 selectedAssets 和 selectedPhotos 数组。
-
didSelectItemAtIndex 方法
增加了附件 cell 的大图预览:
if([self isAttachmentsAtIndex:index]){ Attachment* attachment = _attachments[index]; if(attachment){ YBImageBrowser *browser = [YBImageBrowser new]; YBImageBrowseCellData *data = [YBImageBrowseCellData new]; data.thumbImage = cell.imageView.image;// 低分图 data.url= remoteImgAddr(attachment.id); data.sourceObject = cell; browser.dataSourceArray = @[data]; browser.currentIndex = 0; [browser.defaultToolBar hideOperationButton]; [browser show]; } }else{
OK,好了,剩下的代码和 PhotoPickSC 一模一样,没有必要介绍了。
现在还有一件事,就是将 这个子控制器添加到 MeetingAddVC 中。
添加子控制器
首先增加属性:
@property (strong, nonatomic) AttachmentsPreviewSC* attachmentsSC;
然后子控制器的初始化:
-(AttachmentsPreviewSC*)attachmentsSC{// 附件预览 SC
if(!_attachmentsSC){
_attachmentsSC = [AttachmentsPreviewSC new];
_attachmentsSC.inset = UIEdgeInsetsMake(15, 20, 5, 20);
_attachmentsSC.attachments = [_meetingInfo.baseFileStorePlus[@"baseFilesList"] mutableCopy];
@weakify(self)
_attachmentsSC.photoArrayChangeBlock = ^(NSArray<PHAsset*>* _Nonnull assets) {
@strongify(self)
self.selectedAssets = assets;
};
}
return _attachmentsSC;
}
添加到主控制器:
[self addSC:_meetingInfo.id ? self.attachmentsSC : self.photoSC];
这里讨了一点巧,判断 meetingInfo 的 id 是否有值,有值说明这是对一个已有会议记录的编辑,那么使用的是 AttachmentsPreviewSC 子控制器;否则说明是新建,使用的就是 PhotoPickSC 子控制器。
好了,整个图片上传的教程就介绍到这里了,感谢阅读,下次再见。