前言
估计有很多iOS开发的小伙伴都曾遇到过这问题,就是在web中通过什么方式去判断用户的下载行为?
最近要完善一个功能,于是就翻找了很多文章,但是大多文章写的判断逻辑都存在问题,于是一番搜索和研究,写下了这篇文章,希望能帮到有需要的人士。当然如果有更便捷的方式,也请各位毫不吝啬的分享一下,谢谢🙏
问题:如何在 webview 中判断用户的下载行为?
起初我也不知道如何去判断,似乎 iOS 的wkwebview中也并没有提供一个方法告诉你,用户触发了下载行为。 于是我就查看安卓端是怎样去判断的,调查的结果显示安卓可以通过回调的Response的header中取出attachment 来判断,但是iOS中并没有返回header,所以这个方法无法实现。
通过研究发现在decidePolicyForNavigationResponse中的navigationResponse返回了MIMEType,这个通常是用于表示文件类型的【关于MIMEType更加详细的解答:developer.mozilla.org/en-US/docs/… 。于是就倒推,先知道当用户是触发下载行为时,MIMEType的类型是什么?
经过调试下载时候得到的MIMEType是"application/octet-stream",这个类型一般最常见用于表示:未知的应用程序文件,执行二进制流下载操作,这里有一篇文章进行了解释:juejin.cn/post/697922…
既然如此,那么是否可以通过这个判断来感知用户的下载操作呢?于是经过测试发现,这个问题果然是可行的。
解决方法
/// 一般下载文件都是使用"application/octet-stream"类型
#define supportedMimeTypes @[@"application/octet-stream"]
///是否支持此类型文件
- (BOOL)isSupportedMimeType:(NSString *)mimeType {
return [supportedMimeTypes containsObject:mimeType];
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
BOOL isSupportedMimeType = [self isSupportedMimeType:navigationResponse.response.MIMEType];
if(isSupportedMimeType){
if (@available (iOS 14.5, *)) { //适配14.5系统
decisionHandler(WKNavigationResponsePolicyDownload);
return;
} else{//兼容 14.5 以下系统
[self downloadFileFromURL:navigationResponse.response.URL];
decisionHandler(WKNavigationResponsePolicyCancel);
return;
}
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
注意到这里仅仅是做好了判断与适配的操作,接下来还要在webview中实现WKDownloadDelegate与NSURLSessionDownloadDelegate代理方法
///适配14.5系统下载文件操作
- (void)download:(WKDownload *)download decideDestinationUsingResponse:( NSURLResponse *)response suggestedFilename:(NSString *)suggestedFilename completionHandler:(void(^)(NSURL *))completionHandler API_AVAILABLE(ios(14.5)){
///注意:不推荐直接使用suggestedFilename,因为suggestedFilename比较长的时候,就会导致下载直接失败
///获取下载的url
NSString *donwloadUrl = download.originalRequest.URL.absoluteString;
NSString *fileName = [self getFileName:donwloadUrl];
///如果存在,则删除之前的文件
BOOL isFileExist = [self isFileExist:fileName];
if(isFileExist){
[self deleteFile:fileName];
}
///创建新的沙盒路径
NSString *filePath = [self getDocumentFilePathForFileName:fileName];
NSURL *fileUrl = [NSURL fileURLWithPath:filePath];
///用一个字典记录当前的下载链接以及保存的地址
self.downloadInfo[donwloadUrl] = fileUrl;
///返回一个沙盒路径地址
completionHandler(fileUrl);
}
///下载成功
-(void)downloadDidFinish:(WKDownload *)download API_AVAILABLE(ios(14.5)){
NSString *url = download.originalRequest.URL.absoluteString;
NSURL *filePath = self.downloadInfo[url];
if(filePath){
[self.downloadInfo removeObjectForKey:url];
[self showSaveFileAlert:filePath callback:nil];
}
}
///下载失败
- (void)download:(WKDownload *)download didFailWithError:(NSError *)error resumeData:(NSData *)resumeData API_AVAILABLE(ios(14.5)){
NSString *url = download.originalRequest.URL.absoluteString;
if([[self.downloadInfo allKeys] containsObject:url]){
[self.downloadInfo removeObjectForKey:url];
}
[self showToast:@"文件下载失败,请重新尝试" duration:2];
}
///适配 14.5 以下系统
- (void)downloadFileFromURL:(NSURL *)url{
[self showHub];
NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithURL:url];
[downloadTask resume];
}
// NSURLSessionDownloadDelegate 方法
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSString *fileName = [self getFileName:downloadTask.response.URL.absoluteString];
if([self isFileExist:fileName]){
[self deleteFile:fileName];
}
NSString *filePath = [self getDocumentFilePathForFileName:fileName];
NSURL *fileUrl = [NSURL fileURLWithPath:filePath];
[self hideHub];
// 移动下载的文件到指定路径
NSError *fileError;
BOOL isSuccess = [[NSFileManager defaultManager] moveItemAtURL:location toURL:fileUrl error:&fileError];
if(isSuccess){
[self showSaveFileAlert:fileUrl callback:nil];
return;
}
[self deleteFile:fileName];
[self showToast:@"文件保存失败,请重新尝试" duration:2];
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
[self hideHub];
if(error){
[self showToast:@"文件下载失败,请重新尝试" duration:2];
}
}
///设置文件名
-(NSString *)getFileName:(NSString *)url{
NSString *lastPath = [url stringByRemovingPercentEncoding];
NSTextCheckingResult *match = [self getChineseFileName:lastPath];
NSString *fileFormat;
for(NSString *file in fileFormats) {
if([lastPath containsString:file]){
fileFormat = file;
break;
}
}
NSString *fileName;
if (match) {
NSString *name = [lastPath substringWithRange:match.range];
if(name){
fileName = [NSString stringWithFormat:@"%@%@",name,fileFormat];
}else{
fileName = [self getFileName:lastPath fileFormat:fileFormat];
}
} else {
fileName = [self getFileName:lastPath fileFormat:fileFormat];
}
if(!fileName){
NSArray *arr = [lastPath componentsSeparatedByString:fileFormat];
NSString *str = arr.firstObject;
NSString *name = [str substringFromIndex:str.length - 10];
fileName = [NSString stringWithFormat:@"%@%@",name,fileFormat];
}
return fileName;
}
-(NSString *)getFileName:(NSString *)filePath fileFormat:(NSString *)fileFormat{
if(!fileFormat || [fileFormat isEqualToString:@""] || !filePath || [filePath isEqualToString:@""]){
return nil;
}
NSArray *tempArr1 = [filePath componentsSeparatedByString:@"/"];
NSEnumerator *enumerator = [tempArr1 reverseObjectEnumerator];
NSString *fileName;
for (NSString *element in enumerator) {
if([element containsString:fileFormat]){
fileName = element;
break;
}
}
NSTextCheckingResult *match = [self getChineseFileName:fileName];
if(match) {
NSString *name = [fileName substringWithRange:match.range];
if(name){
fileName = [NSString stringWithFormat:@"%@%@",name,fileFormat];
}
}else{
NSArray *tempArr2 = [fileName componentsSeparatedByString:fileFormat];
fileName = [NSString stringWithFormat:@"%@%@",tempArr2.firstObject,fileFormat];
}
return fileName;
}
-(NSTextCheckingResult *)getChineseFileName:(NSString *)filePath{
if(filePath){
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[\u4e00-\u9fa5]+" options:0 error:nil];
NSTextCheckingResult *match = [regex firstMatchInString:filePath options:0 range:NSMakeRange(0, filePath.length)];
return match;
}
return nil;
}
-(void)showSaveFileAlert:(NSURL *)filePath callback:(ShareDocumentCb)callback{
[self downloadFileSuccessAlert:@"文件已下载成功,请点击【储存到“文件”】,即可在手机系统上【文件】中查看" callback:^{
[self shareFiles:@[filePath] callback:callback];
}];
}
-(void)downloadFileSuccessAlert:(NSString *)message callback:(void(^)(void))callback{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
if(callback){
callback();
}
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self presentViewController:alert animated:YES completion:NULL];
});
}
-(BOOL)isFileExist:(NSString *)fileName{
//获取Documents 下的文件路径
NSString *filePath = [self getDocumentFilePathForFileName:fileName];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL result = [fileManager fileExistsAtPath:filePath];
return result;
}
-(BOOL)deleteFile:(NSString *)fileName{
//获取Documents 下的文件路径
NSString *filePath = [self getDocumentFilePathForFileName:fileName];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL success = [fileManager removeItemAtPath:filePath error:nil];
return success;
}
// 获取沙盒中的文件路径
- (NSString *)getDocumentFilePathForFileName:(NSString *)fileName {
// 请根据您的实际逻辑调整这里的路径
NSString *sandboxDirectory = NSTemporaryDirectory();
return [sandboxDirectory stringByAppendingPathComponent:fileName];
}
///转发文件到其他软件
-(void)shareFiles:(NSArray <NSURL *>*)files callback:(ShareDocumentCb)callback{
UIActivityViewController *vc = [[UIActivityViewController alloc] initWithActivityItems:files applicationActivities:nil];
vc.excludedActivityTypes = @[UIActivityTypeSaveToCameraRoll,UIActivityTypeAssignToContact,UIActivityTypePostToFlickr,UIActivityTypePostToVimeo];
vc.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
if(activityError){
if(callback){
callback(-1,@"文件转发失败");
}
}else{
if(callback){
callback(0,@"文件转发成功");
}
}
};
[self presentViewController:vc animated:YES completion:nil];
}
阅读文献: medium.com/@tazwarutsh…