问题描述
我一直在尝试使用 uidocumentbrowserviewcontroller 在 Xamarin 中的 iOS 上实现各种“保存文件”对话框。一切都适用于在手机和 iCloud 中创建和选择文件。但是在创建文档时,它在 OneDrive 中静默失败(完全假装工作,直到您尝试在其他地方访问它)并在 Google Drive 中显示错误消息(“操作无法完成。(com.apple.DocumentManager 错误) 1.)").
据我的阅读,如果它在 iCloud 中工作,它应该在 One Drive 和 Google Drive 中工作,但我似乎无法弄清楚我可能会遗漏什么。我已经在下面发布了代码;它松散地基于 Xam.Filepicker nuget。
public class SaveDocumentbrowser : uidocumentbrowserviewcontrollerDelegate
{
//Request ID for current picking call
private int requestId;
//Task returned when all is completed
private taskcompletionsource<FilePlaceholder> completionSource;
//Event which is invoked when a file was picked. Used elsewhere
internal EventHandler<FilePlaceholder> Handler { get; set; }
//Extensions used for choosing file names
private string[] _extensions;
//Called when a file has been chosen or created
private async void OnFilePicked(NSUrl destination,bool creation = false)
{
if (destination == null || !destination.IsFileUrl)
{
this.Handler?.Invoke(this,null);
return;
}
var document = new GenericDocument(destination);
var success = await document.OpenAsync();
if (!success)
{
this.Handler?.Invoke(this,null);
return;
}
async Task StreamSetter(Stream stream,FilePlaceholder placeholder)
{
document.DataStream = stream;
try
{
if (!await document.SaveAsync(destination,creation ? UIDocumentSaveOperation.ForCreating : UIDocumentSaveOperation.ForOverwriting))
{
throw new Exception("Failed to Save Document.");
}
}
finally
{
await document.CloseAsync();
}
}
var placeHolder = new FilePlaceholder(destination.AbsoluteString,destination.LastPathComponent,StreamSetter,b => document.dispose());
this.Handler?.Invoke(null,placeHolder);
}
//Delegate for when user requests document creation
public override void DidRequestDocumentCreation(uidocumentbrowserviewcontroller controller,Action<NSUrl,UIDocumentbrowserImportMode> importHandler)
{
//this is a custom view for choosing a name for the new file
var editController = new FileNameInputViewController(_extensions);
void OnEditControllerOnOnViewDiddisappear(object sender,EventArgs args)
{
editController.OnViewDiddisappear -= OnEditControllerOnOnViewDiddisappear;
if (string.IsNullOrEmpty(editController.FileName))
{
importHandler(null,UIDocumentbrowserImportMode.None);
return;
}
try
{
var documentFolder = Path.GetTempPath();
var tempFileName = editController.FileName;
var path = Path.Combine(documentFolder,tempFileName);
var tempFile = File.Create(path);
tempFile.dispose();
var url = NSUrl.CreateFileUrl(path,false,null);
importHandler(url,UIDocumentbrowserImportMode.Move);
}
catch(Exception e)
{
Debug.WriteLine("Failed to create temp doc: " + e);
var dialog = UIAlertController.Create("Error","Error creating temp file: " + e.Message,UIAlertControllerStyle.Alert);
dialog.AddAction(UIAlertAction.Create("Done",UIAlertActionStyle.Cancel,null));
controller.PresentViewController(dialog,null);
importHandler(null,UIDocumentbrowserImportMode.None);
}
}
editController.OnViewDiddisappear += OnEditControllerOnOnViewDiddisappear;
controller.PresentViewController(editController,true,null);
}
//Delegate for when user picks file
public override void DidPickDocumentUrls(uidocumentbrowserviewcontroller controller,NSUrl[] documentUrls)
{
var dialog = UIAlertController.Create("Overwriting file","Are you sure you want to overwrite this file?",UIAlertControllerStyle.Alert);
dialog.AddAction(UIAlertAction.Create("Yes",UIAlertActionStyle.Default,action => OnFilePicked(documentUrls[0])));
dialog.AddAction(UIAlertAction.Create("No",null));
controller.PresentViewController(dialog,null);
}
//Delegate for when user picks files (not used at this time)
public override void DidPickDocumentsAtUrls(uidocumentbrowserviewcontroller controller,null);
}
//Delegate for when created document successfully import
public override void DidImportDocument(uidocumentbrowserviewcontroller controller,NSUrl sourceUrl,NSUrl destinationUrl)
{
OnFilePicked(destinationUrl);
}
//Delegate for when created document fails import
public override void FailedToImportDocument(uidocumentbrowserviewcontroller controller,NSUrl documentUrl,NSError error)
{
Debug.WriteLine("Failed to import doc: " + error);
var dialog = UIAlertController.Create("Error","Error creating file: " + error,UIAlertControllerStyle.Alert);
dialog.AddAction(UIAlertAction.Create("Done",null);
}
/// <summary>
/// File picking implementation
/// </summary>
/// <param name="allowedTypes">list of allowed types; may be null</param>
/// <returns>picked file data,or null when picking was cancelled</returns>
public Task<FilePlaceholder> PickMediaAsync(string[] allowedTypes)
{
var id = this.GetRequestId();
var ntcs = new taskcompletionsource<FilePlaceholder>(id);
if (Interlocked.CompareExchange(ref this.completionSource,ntcs,null) != null)
{
throw new InvalidOperationException("Only one operation can be active at a time");
}
var allowedUtis = new string[]
{
UTType.Content,UTType.Item,"public.data"
};
if (allowedTypes != null)
{
allowedUtis = allowedTypes.Where(x => x[0] != '.').ToArray();
_extensions = allowedTypes.Where(x => x[0] == '.').ToArray();
}
else
{
_extensions = null;
}
//This is only custom so we can hook onto the dismissal event
var documentbrowser = new CustomDocumentbrowserViewController(allowedUtis)
{
AllowsDocumentCreation = true,AllowsPickingMultipleItems = false,Delegate = this,};
void OnDocumentbrowserOnOnViewDiddisappear(object sender,EventArgs args)
{
OnFilePicked(null);
}
documentbrowser.OnViewDiddisappear += OnDocumentbrowserOnOnViewDiddisappear;
UIViewController viewController = GetActiveViewController();
viewController.PresentViewController(documentbrowser,null);
this.Handler = (sender,args) =>
{
documentbrowser.OnViewDiddisappear -= OnDocumentbrowserOnOnViewDiddisappear;
documentbrowser.dismissViewController(false,null);
var tcs = Interlocked.Exchange(ref this.completionSource,null);
tcs?.SetResult(args);
};
return this.completionSource.Task;
}
//Get current view controller for presentation
private static UIViewController GetActiveViewController()
{
UIWindow window = UIApplication.SharedApplication.KeyWindow;
UIViewController viewController = window.RootViewController;
while (viewController.PresentedViewController != null)
{
viewController = viewController.PresentedViewController;
}
return viewController;
}
//increment to a new id for Task completion sources
private int GetRequestId()
{
var id = this.requestId;
if (this.requestId == int.MaxValue)
{
this.requestId = 0;
}
else
{
this.requestId++;
}
return id;
}
//custom inheritance solely so we can see if user dismisses the view
private class CustomDocumentbrowserViewController : uidocumentbrowserviewcontroller
{
public event EventHandler OnViewDiddisappear;
public override void ViewDiddisappear(bool animated)
{
base.ViewDiddisappear(animated);
OnViewDiddisappear?.Invoke(this,null);
}
public CustomDocumentbrowserViewController(string[] contentTypes) : base(contentTypes)
{
}
}
}
解决方法
UIDocumentBrowserViewController 可以处理本地和 iCloud 中直接存储的文件。
不过好像不能直接用于第三方存储服务。与apple document核对一下。
第三方存储服务还可以通过实施文件提供程序扩展(iOS 11 或更高版本)来提供对其管理的文档的访问。有关详细信息,请参阅 File Provider。
因此,您需要一个文件提供程序扩展。您可以查看此 xamarin official sample 以了解如何实现。