引言
在移动应用中,图片上传是一个常见的功能,尤其是在个人中心或社交平台场景中,用户经常需要上传图片到服务器,用以展示个人风采或记录美好瞬间。然而,实现多图片上传的过程中,如何设计高效的上传逻辑并结合用户体验显得尤为重要。
本篇博客将通过一个具体实例——个人中心的相册功能,来介绍如何使用 Alamofire 实现多文件上传。我们将从图片选择开始,逐步实现图片数据的封装、上传请求的构建以及上传进度的展示,最终完成一套完整的多图片上传解决方案。如果你也正在寻找一种高效的实现方法,希望本文能为你提供一些思路与启发。
功能需求分析
用户的个人信息页面会展示用户的一些精选照片墙,为了补充这个信息在用户编辑页添加了的上传相册图片的功能,上传到相册的图片最多为6张,可以多选,但是已选择和已上传的总数不能超过6。
关于相册选择我们可以使用系统为我们提供的PHPickerViewController来实现,使用起来十分方便,可以直接设置选择资源类型和最大个数,关于它的使用在之前的博客中也有过介绍,有兴趣的同学可以看下面这篇博客。
iOS 系统提供的媒体资源选择器(PHPickerViewController)-CSDN博客文章浏览阅读1.6k次,点赞10次,收藏20次。在前面的博客中我们已经介绍了一个系统为我们提供的媒体选择器UIImagePickerController,它的功能很强大,但是唯一的不足就是只能选取单个媒体资源,而PHPickerViewController恰恰弥补了这一空缺。PHPickerViewController是iOS 14及更高版本中引入的一个现代化媒体选择器,旨在替代UIImagePickerController。它不仅提供了更灵活的媒体选择功能,还拥有更现代的用户界面。_phpickerviewcontrollerhttps://blog.csdn.net/weixin_39339407/article/details/140918416选择了相册的图片之后,下一步就上传图片资源到相册。Alamofire为我们提供了完整的上传方法但是由于我们需要上传多个资源,所以仍然有一些数据来需要我们处理。
接口设计
在讨论如何实现多文件上传之前,我们首先需要了解一下后端的接口设计,每个服务端对于文件上传的方式都并不是一成不变的。
在上传相册图片的需求中,整个过程被分成了两个接口:
- 文件上传接口:负责文件上传,需要设置文件的类型,以及自定义参数,来决定文件上传是用来做什么的,每次只能上传一个文件。会返回上传后的资源地址。
- 相册更新接口:使用上传后的资源地址构建列表,来更新相册。
代码实现
那我们跳过关于UI的布局刷新以及照片的选择功能,将所有精力集中到文件上传,照片选择完成之后会传递回来一个等待上传的图片数组,为了更符合业务场景,我们需要根据图片数组来构建一组新的相册模型,并添加到相册列表,然后执行上传操作。
选择照片后的处理:
/// 选择相册图片
/// - Parameter images: 图片数组
func uploadAlbum(images: [UIImage]) {
//1. 先刷新
for image in images {
let model = MWEditPhotoModel()
model.image = image
model.uploadStatus = .none
editUserPhotos.append(model)
}
self.tableView.reloadData()
//2.开始执行上传操作
self.presenter.requestUploadAlbumData(editPhotoModels: editUserPhotos) {[weak self] in
guard let self = self else { return }
self.tableView.reloadData()
}
}
- 根据选择上传的图片数组构建新的相册模型,并添加到相册列表中,刷新列表。
- 将整个相册列表当做参数传递到上传方法中。
首先会根据传递进来的相册列表数据进行过滤,获取到需要上传的进行上传:
/// 信号量最大并发控制为2
private let semaphore = DispatchSemaphore(value: 2)
/// 上传用户相册
func requestUploadAlbumData(editPhotoModels:[MWEditPhotoModel], completion: (() -> Void)?) {
uploadAlbumCompletion = completion
// 获取需要上传的模型
let needUploadModels = editPhotoModels.filter { $0.uploadStatus == .none && $0.isAdd == false }
// 上传图片
var needUploadCount = needUploadModels.count
let semaphore = self.semaphore
globalQueue.async {[weak self] in
guard let self = self else {
return
}
for model in needUploadModels {
model.uploadStatus = .uploading
semaphore.wait()
MWLogHelper.debug("开始上传图片",context: "MWEditProfilePresenter")
self.uploadAlbumImages(model) {[weak self] success in
MWLogHelper.debug("上传图片结果\(success)",context: "MWEditProfilePresenter")
defer {
// 无论结果如何,确保释放信号量
semaphore.signal()
}
guard let self = self else {
return
}
if success == false {
model.uploadStatus = .fail
}
needUploadCount -= 1
if needUploadCount == 0 {
self.requestUpdateAlbumData(editPhotoModels: editPhotoModels)
}
}
}
}
}
- 首先创建了一个信号量来控制可以同时上传的最大个数。
- 过滤出需要上传的数据执行上传操作。
- 当所有资源上传完成之后在根据列表数据来更新相册。
那我们先来看一下上传方法:
// 上传相册图片
private func uploadAlbumImages(_ editPhotoModel: MWEditPhotoModel,complection:((Bool)->Void)? = nil) {
guard let imageData = editPhotoModel.image?.compressImageQuality(maxSize: Int(640 * 640)) else {
complection?(false)
return
}
MWNetworkHelper.upLoadFile(data: imageData, kinds: "photo", fileType: MWUploadFileType.image, fileName: "photo.jpg") { result in
if let imageUrl = result?["photo"] {
var albumModel = MWProfileAlbumModel()
albumModel.littleUrl = imageUrl
albumModel.srcUrl = imageUrl
editPhotoModel.albumModel = albumModel
editPhotoModel.uploadStatus = .success
complection?(true)
} else {
editPhotoModel.uploadStatus = .fail
complection?(false)
}
} notRedirectErrorCallback: {
complection?(false)
MWToast.showToast(MWLocaleStringHelper.getString("Upload Failed"))
}
}
/// 上传文件
/// - Parameters:
/// - data: 文件数据
/// - kinds: 参数
/// cover 封面, facebookSharedImage 分享, portrait 头像, pic 图片, zip 压缩包, video 视频, , ,photo 相册, photoProcess 相册模糊, liveCover 直播间封面 photoWall, 个人主页, photoWallBig 个人主页大图
/// - fileType: 文件类型
/// - fileName: 文件名称
/// - completion: 上传结果
public class func upLoadFile(data: Data, kinds: String, fileType: MWUploadFileType, fileName: String, completion: @escaping (([String: String]?) -> Void), notRedirectErrorCallback: @escaping () -> Void) {
// 上传地址
var url = "\(MWAPIHost.host)/rest/api/usergate/uploadFile"
// 上传header
var headers = [String:String]()
// 重定向
let redirectHandler = MWRedirectHandler(data: data, kinds: kinds, fileType: fileType, fileName: fileName, completion: completion)
AF.upload(multipartFormData: { multipartFormData in
//图片
var mimeType = "image/jpg"
if fileType == .video {
mimeType = "video/mp4"
}
multipartFormData.append(data, withName: "file",fileName: fileName, mimeType: mimeType)
// 上传 kinds 参数
if let kindsData = kinds.data(using: .utf8) {
multipartFormData.append(kindsData, withName: "kinds")
}
}, to: url,method: .post, headers: MWAPIEncryEndpoint.api_postDynamic.headers).redirect(using: redirectHandler).responseJSON { response in
if response.response?.statusCode != 307 {
notRedirectErrorCallback()
}
}
}
- 文件名称和文件类型需要根据文件的具体内容来设置。
- kinds为服务端自定的参数。
- 关于重定向的暂且可以不考虑,我们的文件上传接口进行了接口的重定向,一般来讲是不需要单独处理的。
- 文件上传成功之后根据返回结果,构建了新的相册数据,并同步到相册列表。
最后我们只需要等待所有文件上传完成之后,将所有数据同步到相册接口即可:
/// 更新用户相册
/// - Parameters:
/// - editPhotoModels: 编辑相册模型
/// - completion: 完成回调
func requestUpdateAlbumData(editPhotoModels:[MWEditPhotoModel]) {
let needUpdateModels = editPhotoModels.filter { $0.uploadStatus == .success || $0.uploadStatus == .noNeed }
let albumList = needUpdateModels.map { $0.albumModel }
var photoList = [[String: Any]]()
for albumModel in albumList {
var dict = [String: Any]()
dict["littleUrl"] = albumModel?.littleUrl
dict["srcUrl"] = albumModel?.srcUrl
photoList.append(dict)
}
let params: [String: Any] = ["photoList": photoList]
MWNetworkHelper.request(endpoint: MWAPIEncryEndpoint.api_updateUserPhotos, parameters: params) {[weak self] (model, data, error) in
guard let self = self else { return }
if error != nil {
for model in needUpdateModels {
if model.uploadStatus == .success {
model.uploadStatus = .fail
}
}
}
self.uploadAlbumCompletion?()
}
}
结语
本篇博客主要介绍了使用Alamofire实现多文件的上传功能,关于文件上传的具体方案还是需要根据服务端的接口设计来实施。本文以同步用户相册为例,将上传相册图片分为两个部分,上传和同步,当所有资源上传完成之后,执行相册的同步操作。并使用信号量来控制上传的最大并发数。
希望本篇博客能够在文件上传中给大家一些启发。