iOS WebView中下载文件并保存文件到手机

1,500 阅读4分钟

前言

估计有很多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…

github.com/kuttz/WKDow…