简体   繁体   中英

Xamarin.iOS UIDocumentBrowser fails to create file in third-party containers

I've been tryin to implement a "Save File" Dialog of sorts on iOS in Xamarin using UIDocumentBrowserViewController. Everything works for creating and selecting a file on the phone and in iCloud. But when creating a document, it fails silently in OneDrive (full on pretends to work until you try and access it elsewhere) and with an error message in Google Drive ("The operation couldn't be completed. (com.apple.DocumentManager error 1.)").

As far as I can tell from my reading, if it works in iCloud, it should work in One Drive and Google Drive, but I can't seem to figure out what I might be missing. I've posted the code below; it is based loosely on 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, false, 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", UIAlertActionStyle.Cancel, null));

            controller.PresentViewController(dialog, false, null);
        }

        //Delegate for when user picks files (not used at this time)
        public override void DidPickDocumentsAtUrls(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", UIAlertActionStyle.Cancel, null));

            controller.PresentViewController(dialog, false, 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", UIAlertActionStyle.Cancel, null));

            controller.PresentViewController(dialog, false, 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, false, 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 can work with file stored locally and in the iCloud directly.

However, it seems can not be used for third-party storage services directly. Have a check with apple document .

Third-party storage services can also provide access to the documents they manage by implementing a File Provider extension (iOS 11 or later). For more information, see File Provider .

Therefore, you will need a File provider extension. You could have a look at this xamarin official sample to know how to achieve that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM