Add support for providing system file selector on iOS

This commit is contained in:
Salman Alshamrani
2024-12-07 04:20:15 -05:00
parent 83ea26dfca
commit cfc3b220ef
5 changed files with 128 additions and 8 deletions

View File

@@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Runtime.Versioning;
using Foundation;
using osu.Framework.Platform;
using UIKit;
using UniformTypeIdentifiers;
namespace osu.Framework.iOS
{
[SupportedOSPlatform("ios14.0")]
public class IOSFileSelector : UIDocumentPickerDelegate, ISystemFileSelector
{
public event Action<FileInfo>? Selected;
private readonly UIWindow window;
private readonly UIDocumentPickerViewController viewController;
public IOSFileSelector(UIWindow window, string[] allowedExtensions)
{
this.window = window;
UTType[] utTypes;
if (allowedExtensions.Length == 0)
utTypes = new[] { UTTypes.Data };
else
{
utTypes = new UTType[allowedExtensions.Length];
for (int i = 0; i < allowedExtensions.Length; i++)
{
string extension = allowedExtensions[i];
var type = UTType.CreateFromExtension(extension.Replace(".", string.Empty));
if (type == null)
throw new InvalidOperationException($"System failed to recognise extension \"{extension}\" when preparing the file selector.\n");
utTypes[i] = type;
}
}
// files must be provided as copies, as they may be originally located in places that cannot be accessed freely (aka. iCloud Drive).
// we can acquire access to those files via startAccessingSecurityScopedResource but we must know when the game has finished using them.
// todo: refactor FileSelector/DirectorySelector to be aware when the game finished using a file/directory.
viewController = new UIDocumentPickerViewController(utTypes, true);
viewController.Delegate = this;
}
public void Present()
{
UIApplication.SharedApplication.InvokeOnMainThread(() =>
{
window.RootViewController!.PresentViewController(viewController, true, null);
});
}
public override void DidPickDocument(UIDocumentPickerViewController controller, NSUrl url)
=> Selected?.Invoke(new FileInfo(url.Path!));
protected override void Dispose(bool disposing)
{
viewController.Dispose();
base.Dispose(disposing);
}
}
}

View File

@@ -23,6 +23,8 @@ namespace osu.Framework.iOS
{
public class IOSGameHost : SDLGameHost
{
private IOSWindow iosWindow => (IOSWindow)Window;
public IOSGameHost()
: base(string.Empty)
{
@@ -76,6 +78,23 @@ namespace osu.Framework.iOS
public override VideoDecoder CreateVideoDecoder(Stream stream)
=> new IOSVideoDecoder(Renderer, stream);
public override ISystemFileSelector? CreateSystemFileSelector(string[] allowedExtensions)
{
IOSFileSelector? selector = null;
UIApplication.SharedApplication.InvokeOnMainThread(() =>
{
// creating UIDocumentPickerViewController programmatically is only supported on iOS 14.0+.
// on lower versions, return null and fall back to our normal file selector display.
if (!OperatingSystem.IsIOSVersionAtLeast(14))
return;
selector = new IOSFileSelector(iosWindow.UIWindow, allowedExtensions);
});
return selector;
}
public override IEnumerable<KeyBinding> PlatformKeyBindings => MacOSGameHost.KeyBindings;
}
}

View File

@@ -18,7 +18,9 @@ namespace osu.Framework.iOS
{
internal class IOSWindow : SDL3MobileWindow
{
private UIWindow? window;
private UIWindow? uiWindow;
public UIWindow UIWindow => uiWindow!;
public override Size Size
{
@@ -27,7 +29,7 @@ namespace osu.Framework.iOS
{
base.Size = value;
if (window != null)
if (uiWindow != null)
updateSafeArea();
}
}
@@ -43,7 +45,7 @@ namespace osu.Framework.iOS
base.Create();
window = Runtime.GetNSObject<UIWindow>(WindowHandle);
uiWindow = Runtime.GetNSObject<UIWindow>(WindowHandle)!;
updateSafeArea();
}
@@ -73,14 +75,14 @@ namespace osu.Framework.iOS
private void updateSafeArea()
{
Debug.Assert(window != null);
Debug.Assert(uiWindow != null);
SafeAreaPadding.Value = new MarginPadding
{
Top = (float)window.SafeAreaInsets.Top * Scale,
Left = (float)window.SafeAreaInsets.Left * Scale,
Bottom = (float)window.SafeAreaInsets.Bottom * Scale,
Right = (float)window.SafeAreaInsets.Right * Scale,
Top = (float)uiWindow.SafeAreaInsets.Top * Scale,
Left = (float)uiWindow.SafeAreaInsets.Left * Scale,
Bottom = (float)uiWindow.SafeAreaInsets.Bottom * Scale,
Right = (float)uiWindow.SafeAreaInsets.Right * Scale,
};
}
}