.NET MAUI中FileSaver iOS端无保存UI及路径不存在问题排查
解决.NET MAUI中FileSaver iOS端无保存UI且路径错误的问题
问题原因
iOS的沙盒机制与Android存在差异:默认的SaveAsync方法会将文件写入应用私有沙盒目录,不会触发系统保存路径选择UI,且沙盒路径的访问权限限制会导致后续EnsureSuccess()抛出"路径不存在"错误;而Android端默认调用系统文件选择器,允许用户指定公共保存位置。
解决方案
要实现iOS与Android一致的保存交互流程,需手动调用iOS系统文件选择器并配置必要权限,步骤如下:
1. 添加iOS权限配置
在iOS项目的Info.plist中添加权限描述,确保应用能访问文件系统和相册:
<key>NSDocumentsFolderUsageDescription</key> <string>需要访问文件目录以保存收据PDF</string> <key>NSPhotoLibraryAddUsageDescription</key> <string>需要访问相册以保存收据PDF</string>
2. 实现平台特定的文件选择服务
创建跨平台接口,并在iOS项目中实现系统文件选择逻辑:
共享项目接口
public interface IFileSavePicker { Task<NSUrl> PickSaveLocationAsync(string fileName); }
iOS平台实现
[assembly: Dependency(typeof(IOSFileSavePicker))] namespace YourAppNamespace.iOS { public class IOSFileSavePicker : IFileSavePicker { public Task<NSUrl> PickSaveLocationAsync(string fileName) { var tcs = new TaskCompletionSource<NSUrl>(); // 指定PDF类型的UTI var documentPicker = new UIDocumentPickerViewController( new string[] { "com.adobe.pdf" }, UIDocumentPickerMode.ExportToService); documentPicker.DidPickDocumentAtUrls += (sender, args) => { var selectedUrl = args.Urls.FirstOrDefault(); if (selectedUrl != null) { // 请求持久访问权限 bool hasAccess = selectedUrl.StartAccessingSecurityScopedResource(); tcs.SetResult(hasAccess ? selectedUrl : null); } else { tcs.SetResult(null); } }; documentPicker.WasCancelled += (sender, args) => tcs.SetResult(null); // 获取当前窗口并弹出选择器 var keyWindow = UIApplication.SharedApplication.ConnectedScenes .OfType<UIWindowScene>() .FirstOrDefault()? .Windows .FirstOrDefault(w => w.IsKeyWindow); keyWindow?.RootViewController.PresentViewController(documentPicker, true, null); return tcs.Task; } } }
3. 修改共享代码适配平台逻辑
在原代码中添加平台分支,iOS端使用自定义选择器,其他平台保留原FileSaver逻辑:
byte[] bytes = feeHistoryViewModel.networkCalls.PostAsyncWithHeaderPDF("studentfeeaudit/receipt/-1/" + ob.PaymentId + "/" + ob.SessionId).Result; if (bytes != null) { using var stream = new MemoryStream(bytes); string fileName = $"{AccountInfo.userAccount.Name} {ob.InvoiceNumber} {ob.Date}_fees.pdf"; string savedFilePath = null; #if IOS var savePicker = DependencyService.Get<IFileSavePicker>(); var selectedUrl = await savePicker.PickSaveLocationAsync(fileName); if (selectedUrl == null) { // 用户取消保存 return; } try { // 将流写入选择的文件 stream.Position = 0; using var fileStream = new FileStream(selectedUrl.Path, FileMode.Create); await stream.CopyToAsync(fileStream, cancellationTokenSource.Token); savedFilePath = selectedUrl.Path; } finally { // 释放安全访问权限 selectedUrl.StopAccessingSecurityScopedResource(); } #else var fileSaverResult = await fileSaver.SaveAsync(fileName, stream, cancellationTokenSource.Token); fileSaverResult.EnsureSuccess(); savedFilePath = fileSaverResult.FilePath; #endif if (!string.IsNullOrEmpty(savedFilePath)) { var request = new NotificationRequest { NotificationId = 1000, Title = fileName, Description = savedFilePath, ReturningData = savedFilePath }; await LocalNotificationCenter.Current.Show(request); } }
说明
- iOS端通过
UIDocumentPickerViewController触发系统保存选择UI,用户可选择保存到文件APP、相册等公共位置,避免沙盒权限问题。 - 必须调用
StartAccessingSecurityScopedResource和StopAccessingSecurityScopedResource来管理安全范围资源的访问权限,防止内存泄漏和权限错误。
内容的提问来源于stack exchange,提问作者Harshit Agarwal
相关产品推荐
相关产品推荐

