diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index b4c7c07eb6..a1aa23e506 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -16,7 +16,6 @@ 1 2 - 3 UIRequiredDeviceCapabilities @@ -38,5 +37,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + com.apple.security.files.user-selected.read-write + diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 4c1bf97c6f..79d88c13b0 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -26,7 +26,7 @@ internal class IOSStorageProvider : IStorageProvider public bool CanOpen => true; - public bool CanSave => false; + public bool CanSave => true; public bool CanPickFolder => true; @@ -161,10 +161,72 @@ internal class IOSStorageProvider : IStorageProvider return Task.FromResult(new IOSStorageFolder(uri, wellKnownFolder)); } - public Task SaveFilePickerAsync(FilePickerSaveOptions options) + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { - return Task.FromException( - new PlatformNotSupportedException("Save file picker is not supported by iOS")); + /* + This requires a bit of dialog here... + To save a file, we need to present the user with a document picker + This requires a temp file to be created and used to "export" the file to. + When the user picks the file location and name, UIDocumentPickerViewController + will give back the URI to the real file location, which we can then use + to give back as an IStorageFile. + https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller + Yes, it is weird, but without the temp file it will explode. + */ + + // Create a temporary file to use with the document picker + var tempFileName = StorageProviderHelpers.NameWithExtension( + options.SuggestedFileName ?? "document", + options.DefaultExtension, + options.FileTypeChoices?.FirstOrDefault()); + + var tempDir = NSFileManager.DefaultManager.GetTemporaryDirectory().Append(Guid.NewGuid().ToString(), true); + if (tempDir == null) + { + throw new InvalidOperationException("Failed to get temporary directory for save file picker"); + } + + var isDirectoryCreated = NSFileManager.DefaultManager.CreateDirectory(tempDir, true, null, out var error); + if (!isDirectoryCreated) + { + throw new InvalidOperationException("Failed to create temporary directory for save file picker"); + } + + var tempFileUrl = tempDir.Append(tempFileName, false); + + // Create an empty file at the temp location + NSData.FromBytes(0, 0).Save(tempFileUrl, false); + + UIDocumentPickerViewController documentPicker; + if (OperatingSystem.IsIOSVersionAtLeast(14)) + { + documentPicker = new UIDocumentPickerViewController(new[] { tempFileUrl }, asCopy: true); + } + else + { +#pragma warning disable CA1422 + documentPicker = new UIDocumentPickerViewController(tempFileUrl, UIDocumentPickerMode.ExportToService); +#pragma warning restore CA1422 + } + + using (documentPicker) + { + if (OperatingSystem.IsIOSVersionAtLeast(13)) + { + documentPicker.DirectoryUrl = GetUrlFromFolder(options.SuggestedStartLocation); + } + + documentPicker.Title = options.Title; + + var tcs = new TaskCompletionSource(); + documentPicker.Delegate = new PickerDelegate(urls => tcs.TrySetResult(urls)); + var urls = await ShowPicker(documentPicker, tcs); + + // Clean up the temporary directory + NSFileManager.DefaultManager.Remove(tempDir, out _); + + return urls.FirstOrDefault() is { } url ? new IOSStorageFile(url) : null; + } } public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options)