0x00 写在前面
- 需求是实现一个下载管理器(实现了断点续传功能)
项目是写过来个下载管理的类,更新界面使用的是KVO,使用起来有些繁琐,逻辑看起来不太清晰,出问题还不太好查找原因,所以就想着换一种思路实现
0x01 实现思路
- 用一个UITableView显示下载的任务
- 自定义UITableViewCell界面,用于控制下载任务的状态
- 每个下载任务都是一个Model,下载内容变化时更新Model,通过Model更新UI界面
使用的算是MVVM模式,把下载的逻辑放到ViewModel中,以达到解耦效果
0x10 代码展示
1.控制器代码
@interface ViewController ()<UITableViewDataSource, UITableViewDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *downloadModels;
@end
@implementation ViewController
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
[self setDefault];
}
#pragma mark - Method
- (void)setDefault {
self.downloadModels = [[NSMutableArray array] mutableCopy];
NSString *url = @"http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.6.dmg";
for (int i = 0; i < 20 ; i++) {
NSString *fileName = [NSString stringWithFormat:@"qq_%d.dmg",i];
DownloadModelItem *downloadModel = [[DownloadModelItem alloc] initWithDownloadUrlStr:url andSaveFilePath:kCachePath fileName:fileName];
[self.downloadModels addObject:downloadModel];
}
}
#pragma mark - UITableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.downloadModels.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
DownloadCell *cell = [DownloadCell cellWithTableView:tableView];
cell.downloadModel = [self.downloadModels objectAtIndex:indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
@end
2.ViewModel代码
@interface ZZBaseDownloadModel ()
@property (nonatomic, strong) NSString *urlStr;
@property (nonatomic, strong) NSString *fileName;
@property (nonatomic, strong) NSString *saveFilePath;
@property (nonatomic, assign) DownloadStatus status;
@property (nonatomic, assign) long long totalExpectedToRead;
@property (nonatomic, assign) long long totalRead;
@property (nonatomic, assign) NSUInteger bytesRead;
@property (nonatomic, assign) float progress;
@property (nonatomic, assign) NSUInteger totalBytes;
@property (nonatomic, assign) long long byteSpeed;
@property (nonatomic, strong) NSDate *lastReadDate;
@property (nonatomic, strong) ZZDownloadRequest *request;
@end
@implementation ZZBaseDownloadModel
- (instancetype)initWithDownloadUrlStr:(NSString *)urlStr
andSaveFilePath:(NSString *)saveFilePath
fileName:(NSString *)fileName {
self = [super init];
if (self) {
self.urlStr = urlStr;
self.fileName = fileName;
self.saveFilePath = saveFilePath;
self.status = DownloadStatusNormal;
}
return self;
}
#pragma mark - Property Method
- (NSString *)urlStr {
return _urlStr;
}
- (NSString *)fileName {
return _fileName;
}
- (NSString *)saveFilePath {
return _saveFilePath;
}
- (DownloadStatus)status {
return _status;
}
- (long long)totalExpectedToRead {
return _totalExpectedToRead;
}
- (long long)totalRead {
return _totalRead;
}
- (NSUInteger)bytesRead {
return _bytesRead;
}
- (float)progress {
return _progress;
}
- (NSString *)speed {
NSString *speed = self.byteSpeed == 0 ? @"0 KB" : [NSByteCountFormatter stringFromByteCount:self.byteSpeed countStyle:NSByteCountFormatterCountStyleFile];
return [NSString stringWithFormat:@"%@/s", speed];
}
- (NSDate *)lastReadDate {
if (!_lastReadDate) {
_lastReadDate = [NSDate date];
}
return _lastReadDate;
}
#pragma mark - Private Method
- (BOOL)checkUrlAndSavePathEmpty {
BOOL isEmpty = NO;
isEmpty = [self.urlStr isEmpty];
isEmpty = isEmpty || [self.saveFilePath isEmpty];
return isEmpty;
}
- (void)doDownload {
__weak typeof(self) WS = self;
self.request = [ZZDownloadRequest downloadFileWithURLString:self.urlStr
downloadPath:self.saveFilePath
fileName:self.fileName
progressBlock:^(float progress, NSUInteger bytesRead, unsigned long long totalRead, unsigned long long totalExpectedToRead) {
WS.totalBytes += bytesRead;
NSDate *currentDate = [NSDate date];
//时间差
double time = [currentDate timeIntervalSinceDate:WS.lastReadDate];
if (time >= 1) {
long long speed = WS.totalBytes * 1.0 / time;
WS.byteSpeed = speed;
WS.totalBytes = 0.0;
WS.lastReadDate = currentDate;
}
WS.progress = progress;
WS.bytesRead = bytesRead;
WS.totalRead = totalRead;
WS.totalExpectedToRead = totalExpectedToRead;
if (WS.progressBlock) {
WS.progressBlock(progress, bytesRead, totalRead, totalExpectedToRead);
}
}
successBlock:self.successBlock
cancelBlock:self.cancelBlock
failureBlock:self.failureBlock];
if (self.request) {
self.status = DownloadStatusDownloading;
}
}
- (void)resetInfo {
self.byteSpeed = 0;
}
#pragma mark - Public Method
- (void)start {
if ([self checkUrlAndSavePathEmpty]) {
self.status = DownloadStatusFailed;
return;
}
if (self.status == DownloadStatusFinished || self.status == DownloadStatusDownloading) {
return;
}
[self doDownload];
}
- (void)pause {
if (self.status == DownloadStatusDownloading) {
[self.request pauseDownload];
}
self.status = DownloadStatusPause;
[self resetInfo];
}
- (void)cancel {
self.status = DownloadStatusWait;
if (self.request == nil) {
return;
}
[self.request cancelDownload];
[self resetInfo];
}
@end
3.View代码
@interface DownloadCell ()
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UILabel *infoLabel;
@property (weak, nonatomic) IBOutlet UILabel *speedLabel;
@property (weak, nonatomic) IBOutlet UIProgressView *progressBar;
@property (weak, nonatomic) IBOutlet UIButton *downloadOptBtn;
@end
@implementation DownloadCell
#pragma mark - Lifycycle
- (void)awakeFromNib {
[super awakeFromNib];
}
#pragma mark - Property Method
- (void)setDownloadModel:(DownloadModelItem *)downloadModel {
_downloadModel = downloadModel;
self.nameLabel.text = downloadModel.fileName;
self.progressBar.progress = downloadModel.progress;
[self setCellInfoWithStatus:downloadModel.status];
__weak typeof(self) WS = self;
downloadModel.progressBlock = ^(float progress, NSUInteger bytesRead, unsigned long long totalRead, unsigned long long totalExpectedToRead) {
WS.progressBar.progress = progress;
[WS setSpeedWithSpeedStr:WS.downloadModel.speed];
};
downloadModel.successBlock = ^(ZZDownloadRequest *request, id responseObject) {
[WS setCellInfoWithStatus:DownloadStatusFinished];
[WS setSpeedWithSpeedStr:ZeroSpeedString];
};
downloadModel.cancelBlock = ^(ZZDownloadRequest *request) {
[WS setCellInfoWithStatus:DownloadStatusCancel];
[WS setSpeedWithSpeedStr:ZeroSpeedString];
};
downloadModel.failureBlock = ^(ZZDownloadRequest *request, NSError *error) {
[WS setCellInfoWithStatus:DownloadStatusFailed];
[WS setSpeedWithSpeedStr:ZeroSpeedString];
};
}
#pragma mark - Private Method
- (void)setCellInfoWithStatus:(DownloadStatus)status {
NSString *introStr = @"下载";
NSString *btnTitle = @"下载";
switch (status) {
case DownloadStatusNormal:
break;
case DownloadStatusWait:
introStr = @"等待下载";
btnTitle = @"等待";
break;
case DownloadStatusDownloading:
introStr = @"正在下载...";
btnTitle = @"暂停";
break;
case DownloadStatusPause:
introStr = @"暂停下载";
btnTitle = @"下载";
break;
case DownloadStatusCancel:
introStr = @"取消下载";
btnTitle = @"下载";
break;
case DownloadStatusFinished:
introStr = @"下载完成";
btnTitle = @"已完成";
break;
case DownloadStatusFailed:
introStr = @"下载失败";
btnTitle = @"重试";
break;
}
self.infoLabel.text = introStr;
[self.downloadOptBtn setTitle:btnTitle forState:UIControlStateNormal];
self.speedLabel.text = self.downloadModel.speed;
}
- (void)setSpeedWithSpeedStr:(NSString *)speed {
self.speedLabel.text = speed;
}
#pragma mark - Public Method
+ (instancetype)cellWithTableView:(UITableView *)tableView {
static NSString *CellID = @"DownloadCellID";
DownloadCell *cell = [tableView dequeueReusableCellWithIdentifier:CellID];
if (!cell) {
cell = [[[NSBundle mainBundle] loadNibNamed:@"DownloadCell" owner:nil options:nil] lastObject];
} else {
cell.downloadModel.progressBlock = nil;
cell.downloadModel.successBlock = nil;
cell.downloadModel.cancelBlock = nil;
cell.downloadModel.failureBlock = nil;
}
return cell;
}
#pragma mark - Action
- (IBAction)OnDownloadOptBtnTap:(UIButton *)sender {
switch (self.downloadModel.status) {
case DownloadStatusNormal:
case DownloadStatusPause:
case DownloadStatusCancel:
case DownloadStatusFailed:
[self.downloadModel start];
break;
case DownloadStatusWait:
case DownloadStatusDownloading:
[self.downloadModel pause];
break;
case DownloadStatusFinished:
break;
}
[self setCellInfoWithStatus:self.downloadModel.status];
}
@end
0x11 运行效果图和Demo地址
DEMO地址: https://github.com/LeeYZ/DownloadManager