Xamarin.iOS UIDocumentBrowser 无法在第三方容器中创建文件

问题描述

我一直在尝试使用 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 以了解如何实现。