diff --git a/src/Uno.UI/Resources/Strings/cs-CZ/Resources.resw b/src/Uno.UI/Resources/Strings/cs-CZ/Resources.resw
index b67a746f16be..e2e47a09eea8 100644
--- a/src/Uno.UI/Resources/Strings/cs-CZ/Resources.resw
+++ b/src/Uno.UI/Resources/Strings/cs-CZ/Resources.resw
@@ -126,6 +126,9 @@
iOS Security Scoped
+
+ iOS PHPicker
+
Tento počítač
@@ -135,4 +138,4 @@
JS File Access API
-
\ No newline at end of file
+
diff --git a/src/Uno.UI/Resources/Strings/en/Resources.resw b/src/Uno.UI/Resources/Strings/en/Resources.resw
index 3aca64c1bf6e..f438ef279457 100644
--- a/src/Uno.UI/Resources/Strings/en/Resources.resw
+++ b/src/Uno.UI/Resources/Strings/en/Resources.resw
@@ -126,6 +126,9 @@
iOS Security Scoped
+
+ iOS PHPicker
+
This PC
diff --git a/src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs b/src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs
index a4579a75761c..33b1aaffe2b9 100644
--- a/src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs
+++ b/src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs
@@ -1,52 +1,14 @@
using Foundation;
using System;
-namespace Uno.Devices.Sensors.Helpers
+namespace Uno.Devices.Sensors.Helpers;
+
+internal static class SensorHelpers
{
- internal static class SensorHelpers
+ public static DateTimeOffset TimestampToDateTimeOffset(double timestamp)
{
- private static readonly DateTimeOffset NSDateConversionStart =
- new DateTimeOffset(2001, 1, 1, 0, 0, 0, TimeSpan.Zero);
-
- public static DateTimeOffset TimestampToDateTimeOffset(double timestamp)
- {
- var bootTime = NSDate.FromTimeIntervalSinceNow(-NSProcessInfo.ProcessInfo.SystemUptime);
- var date = (DateTime)bootTime.AddSeconds(timestamp);
- return new DateTimeOffset(date);
- }
-
- public static DateTimeOffset NSDateToDateTimeOffset(NSDate nsDate)
- {
- if (nsDate == NSDate.DistantPast)
- {
- return DateTimeOffset.MinValue;
- }
- else if (nsDate == NSDate.DistantFuture)
- {
- return DateTimeOffset.MaxValue;
- }
-
- return NSDateConversionStart.AddSeconds(
- nsDate.SecondsSinceReferenceDate);
- }
-
- public static NSDate DateTimeOffsetToNSDate(DateTimeOffset dateTimeOffset)
- {
- if (dateTimeOffset == DateTimeOffset.MinValue)
- {
- return NSDate.DistantPast;
- }
- else if (dateTimeOffset == DateTimeOffset.MaxValue)
- {
- return NSDate.DistantFuture;
- }
-
- var dateInSecondsFromStart = dateTimeOffset
- .ToUniversalTime()
- .Subtract(NSDateConversionStart.UtcDateTime);
-
- return NSDate.FromTimeIntervalSinceReferenceDate(
- dateInSecondsFromStart.TotalSeconds);
- }
+ var bootTime = NSDate.FromTimeIntervalSinceNow(-NSProcessInfo.ProcessInfo.SystemUptime);
+ var date = (DateTime)bootTime.AddSeconds(timestamp);
+ return new DateTimeOffset(date);
}
}
diff --git a/src/Uno.UWP/Extensions/NSDateExtensions.cs b/src/Uno.UWP/Extensions/NSDateExtensions.cs
new file mode 100644
index 000000000000..f3b093deb2c0
--- /dev/null
+++ b/src/Uno.UWP/Extensions/NSDateExtensions.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Foundation;
+
+namespace Windows.Extensions;
+
+internal static class NSDateExtensions
+{
+ private static readonly DateTimeOffset NSDateConversionStart =
+ new DateTimeOffset(2001, 1, 1, 0, 0, 0, TimeSpan.Zero);
+
+ public static DateTimeOffset ToDateTimeOffset(this NSDate nsDate)
+ {
+ if (nsDate == NSDate.DistantPast)
+ {
+ return DateTimeOffset.MinValue;
+ }
+ else if (nsDate == NSDate.DistantFuture)
+ {
+ return DateTimeOffset.MaxValue;
+ }
+
+ return NSDateConversionStart.AddSeconds(
+ nsDate.SecondsSinceReferenceDate);
+ }
+
+ public static NSDate ToNSDate(this DateTimeOffset dateTimeOffset)
+ {
+ if (dateTimeOffset == DateTimeOffset.MinValue)
+ {
+ return NSDate.DistantPast;
+ }
+ else if (dateTimeOffset == DateTimeOffset.MaxValue)
+ {
+ return NSDate.DistantFuture;
+ }
+
+ var dateInSecondsFromStart = dateTimeOffset
+ .ToUniversalTime()
+ .Subtract(NSDateConversionStart.UtcDateTime);
+
+ return NSDate.FromTimeIntervalSinceReferenceDate(
+ dateInSecondsFromStart.TotalSeconds);
+ }
+}
diff --git a/src/Uno.UWP/Storage/Internal/StorageProviders.cs b/src/Uno.UWP/Storage/Internal/StorageProviders.cs
index 3c51c1893398..40c30b2affdd 100644
--- a/src/Uno.UWP/Storage/Internal/StorageProviders.cs
+++ b/src/Uno.UWP/Storage/Internal/StorageProviders.cs
@@ -18,6 +18,8 @@ internal static class StorageProviders
#if __IOS__
public static StorageProvider IosSecurityScoped { get; } = new StorageProvider("iossecurityscoped", "StorageProviderIosSecurityScopedDisplayName");
+
+ public static StorageProvider IosPHPicker { get; } = new("iosphpicker", "StorageProviderIosPHPickerDisplayName");
#endif
}
}
diff --git a/src/Uno.UWP/Storage/Pickers/FileOpenPicker.cs b/src/Uno.UWP/Storage/Pickers/FileOpenPicker.cs
index 54bb0e459acf..0a1e4c0527db 100644
--- a/src/Uno.UWP/Storage/Pickers/FileOpenPicker.cs
+++ b/src/Uno.UWP/Storage/Pickers/FileOpenPicker.cs
@@ -100,21 +100,4 @@ private void ValidateConfiguration()
}
#endif
}
-
- public static class FileOpenPickerExtensions
- {
- ///
- /// Sets the file limit a user can select when picking multiple files.
- ///
- /// The maximum number of files that the user can pick.
-#if !__IOS__
- [global::Uno.NotImplemented]
-#endif
- public static void SetMultipleFilesLimit(this FileOpenPicker picker, int limit)
- {
-#if __IOS__
- picker.SetMultipleFileLimit(limit);
-#endif
- }
- }
}
diff --git a/src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs b/src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs
index a099da4a6b29..fac719e9d94e 100644
--- a/src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs
+++ b/src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs
@@ -5,22 +5,22 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Uno.Storage.Pickers.Internal;
-using UIKit;
using Foundation;
-using Windows.ApplicationModel.Core;
-using Uno.Helpers.Theming;
+using MobileCoreServices;
+using Photos;
using PhotosUI;
+using UIKit;
+using Uno.Helpers.Theming;
+using Uno.Storage.Pickers.Internal;
using Uno.UI.Dispatching;
-using MobileCoreServices;
-using Windows.Foundation.Metadata;
-using Uno.Foundation.Logging;
+using Windows.ApplicationModel.Core;
namespace Windows.Storage.Pickers
{
public partial class FileOpenPicker
{
private int _multipleFileLimit;
+ private bool _isReadOnly;
private Task PickSingleFileTaskAsync(CancellationToken token)
{
@@ -53,10 +53,9 @@ private Task> PickMultipleFilesTaskAsync(Cancellation
return tcs.Task;
}
- internal void SetMultipleFileLimit(int limit)
- {
- _multipleFileLimit = limit;
- }
+ internal void SetMultipleFileLimit(int limit) => _multipleFileLimit = limit;
+
+ internal void SetReadOnlyMode(bool readOnly) => _isReadOnly = readOnly;
private UIViewController GetViewController(bool multiple, int limit, TaskCompletionSource completionSource)
{
@@ -73,24 +72,24 @@ private UIViewController GetViewController(bool multiple, int limit, TaskComplet
};
case PickerLocationId.PicturesLibrary when multiple is true && iOS14AndAbove is true:
- var imageConfiguration = new PHPickerConfiguration
+ var imageConfiguration = new PHPickerConfiguration(PHPhotoLibrary.SharedPhotoLibrary)
{
Filter = PHPickerFilter.ImagesFilter,
SelectionLimit = limit
};
return new PHPickerViewController(imageConfiguration)
{
- Delegate = new PhotoPickerDelegate(completionSource)
+ Delegate = new PhotoPickerDelegate(completionSource, _isReadOnly)
};
case PickerLocationId.VideosLibrary when multiple is true && iOS14AndAbove is true:
- var videoConfiguration = new PHPickerConfiguration
+ var videoConfiguration = new PHPickerConfiguration(PHPhotoLibrary.SharedPhotoLibrary)
{
Filter = PHPickerFilter.VideosFilter,
SelectionLimit = limit
};
return new PHPickerViewController(videoConfiguration)
{
- Delegate = new PhotoPickerDelegate(completionSource)
+ Delegate = new PhotoPickerDelegate(completionSource, _isReadOnly)
};
default:
@@ -167,9 +166,13 @@ public override void FinishedPickingMedia(UIImagePickerController picker, NSDict
private class PhotoPickerDelegate : PHPickerViewControllerDelegate
{
private readonly TaskCompletionSource _taskCompletionSource;
+ private readonly bool _readOnly;
- public PhotoPickerDelegate(TaskCompletionSource taskCompletionSource) =>
+ public PhotoPickerDelegate(TaskCompletionSource taskCompletionSource, bool readOnly)
+ {
_taskCompletionSource = taskCompletionSource;
+ _readOnly = readOnly;
+ }
public override async void DidFinishPicking(PHPickerViewController picker, PHPickerResult[] results)
{
@@ -186,56 +189,50 @@ public override async void DidFinishPicking(PHPickerViewController picker, PHPic
private async Task> ConvertPickerResults(PHPickerResult[] results)
{
List storageFiles = new List();
- var providers = results
- .Select(res => res.ItemProvider)
- .Where(provider => provider != null && provider.RegisteredTypeIdentifiers?.Length > 0)
- .ToArray();
-
- foreach (NSItemProvider provider in providers)
+ if (_readOnly)
{
- var identifier = GetIdentifier(provider.RegisteredTypeIdentifiers ?? []) ?? "public.data";
- var data = await provider.LoadDataRepresentationAsync(identifier);
-
- if (data is null)
+ var assetIdentifiers = results
+ .Select(res => res.AssetIdentifier!)
+ .Where(id => id != null)
+ .ToArray();
+
+ var resultsByIdentifier = results.ToDictionary(res => res.AssetIdentifier!);
+ var assets = PHAsset.FetchAssetsUsingLocalIdentifiers(assetIdentifiers, null);
+ foreach (PHAsset asset in assets)
{
- continue;
+ var file = StorageFile.GetFromPHPickerResult(resultsByIdentifier[asset.LocalIdentifier], asset, null);
+ storageFiles.Add(file);
}
-
- var extension = GetExtension(identifier);
-
- var destinationUrl = NSFileManager.DefaultManager
- .GetTemporaryDirectory()
- .Append($"{NSProcessInfo.ProcessInfo.GloballyUniqueString}.{extension}", false);
- data.Save(destinationUrl, false);
-
- storageFiles.Add(StorageFile.GetFromSecurityScopedUrl(destinationUrl, null));
}
-
- return storageFiles;
- }
- private static string? GetIdentifier(string[] identifiers)
- {
- if (!(identifiers?.Length > 0))
+ else
{
- return null;
- }
+ var providers = results
+ .Select(res => res.ItemProvider)
+ .Where(provider => provider != null && provider.RegisteredTypeIdentifiers?.Length > 0)
+ .ToArray();
- if (identifiers.Any(i => i.StartsWith(UTType.LivePhoto, StringComparison.InvariantCultureIgnoreCase)) && identifiers.Contains(UTType.JPEG))
- {
- return identifiers.FirstOrDefault(i => i == UTType.JPEG);
- }
+ foreach (NSItemProvider provider in providers)
+ {
+ var identifier = StorageFile.GetUTIdentifier(provider.RegisteredTypeIdentifiers ?? []) ?? "public.data";
+ var data = await provider.LoadDataRepresentationAsync(identifier);
- if (identifiers.Contains(UTType.QuickTimeMovie))
- {
- return identifiers.FirstOrDefault(i => i == UTType.QuickTimeMovie);
- }
+ if (data is null)
+ {
+ continue;
+ }
- return identifiers.FirstOrDefault();
- }
+ var extension = StorageFile.GetUTFileExtension(identifier);
- private string? GetExtension(string identifier)
- => UTType.CopyAllTags(identifier, UTType.TagClassFilenameExtension)?.FirstOrDefault();
+ var destinationUrl = NSFileManager.DefaultManager
+ .GetTemporaryDirectory()
+ .Append($"{NSProcessInfo.ProcessInfo.GloballyUniqueString}.{extension}", false);
+ data.Save(destinationUrl, false);
+ storageFiles.Add(StorageFile.GetFromSecurityScopedUrl(destinationUrl, null));
+ }
+ }
+ return storageFiles;
+ }
}
private class FileOpenPickerDelegate : UIDocumentPickerDelegate
diff --git a/src/Uno.UWP/Storage/Pickers/FileOpenPickerExtensions.cs b/src/Uno.UWP/Storage/Pickers/FileOpenPickerExtensions.cs
new file mode 100644
index 000000000000..dc6152eb7f9c
--- /dev/null
+++ b/src/Uno.UWP/Storage/Pickers/FileOpenPickerExtensions.cs
@@ -0,0 +1,38 @@
+#nullable enable
+
+namespace Windows.Storage.Pickers;
+
+///
+/// Contains Uno Platform-specific extesions for the class.
+///
+public static class FileOpenPickerExtensions
+{
+ ///
+ /// Sets the file limit a user can select when picking multiple files.
+ ///
+ /// The maximum number of files that the user can pick.
+#if !__IOS__
+ [global::Uno.NotImplemented]
+#endif
+ public static void SetMultipleFilesLimit(this FileOpenPicker picker, int limit)
+ {
+#if __IOS__
+ picker.SetMultipleFileLimit(limit);
+#endif
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+#if !__IOS__
+ [global::Uno.NotImplemented]
+#endif
+ public static void SetReadOnlyMode(this FileOpenPicker picker, bool readOnly)
+ {
+#if __IOS__
+ picker.SetReadOnlyMode(readOnly);
+#endif
+ }
+}
diff --git a/src/Uno.UWP/Storage/StorageFile.iOS.cs b/src/Uno.UWP/Storage/StorageFile.iOS.cs
index 6e7cd676e4d1..5417fb4baaba 100644
--- a/src/Uno.UWP/Storage/StorageFile.iOS.cs
+++ b/src/Uno.UWP/Storage/StorageFile.iOS.cs
@@ -1,18 +1,20 @@
#nullable enable
using System;
+using System.IO;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Foundation;
+using MobileCoreServices;
+using Photos;
+using PhotosUI;
using UIKit;
-using Windows.Storage.FileProperties;
-using Windows.Storage.Streams;
using Uno.Storage.Internal;
using Uno.Storage.Streams.Internal;
-using System.IO;
-using System.Linq;
-using MobileCoreServices;
-using SystemPath = System.IO.Path;
+using Windows.Extensions;
+using Windows.Storage.FileProperties;
+using Windows.Storage.Streams;
namespace Windows.Storage
{
@@ -21,6 +23,9 @@ public partial class StorageFile
internal static StorageFile GetFromSecurityScopedUrl(NSUrl nsUrl, StorageFolder? parent) =>
new StorageFile(new SecurityScopedFile(nsUrl, parent));
+ internal static StorageFile GetFromPHPickerResult(PHPickerResult result, PHAsset phAsset, StorageFolder? parent) =>
+ new StorageFile(new PHAssetFile(phAsset, result, parent));
+
internal class SecurityScopedFile : ImplementationBase
{
private readonly NSUrl _nsUrl;
@@ -95,5 +100,138 @@ protected override bool IsEqual(ImplementationBase implementation) =>
implementation is SecurityScopedFile file &&
file._nsUrl.FilePathUrl?.Path == _nsUrl.FilePathUrl?.Path;
}
+
+ internal class PHAssetFile : ImplementationBase
+ {
+ private readonly PHAsset _phAsset;
+ private readonly PHPickerResult _pickerResult;
+ private StorageFolder? _parent;
+
+ private NSUrl? _fileUrl;
+
+ public PHAssetFile(PHAsset phAsset, PHPickerResult pickerResult, StorageFolder? parent) : base(string.Empty)
+ {
+ _phAsset = phAsset;
+ _pickerResult = pickerResult;
+ _parent = parent;
+
+ var resources = PHAssetResource.GetAssetResources(_phAsset);
+ Path = ((PHAssetResource)resources[0]).OriginalFilename;
+ }
+
+ public override StorageProvider Provider => StorageProviders.IosPHPicker;
+
+ public override DateTimeOffset DateCreated => _phAsset.CreationDate.ToDateTimeOffset();
+
+ public override Task DeleteAsync(CancellationToken ct, StorageDeleteOption options)
+ {
+ TaskCompletionSource resultCompletionSource = new TaskCompletionSource();
+ PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(
+ () =>
+ {
+ PHAssetChangeRequest.DeleteAssets(new PHAsset[] { _phAsset });
+ },
+ (success, error) =>
+ {
+ if (success)
+ {
+ resultCompletionSource.SetResult();
+ }
+ else
+ {
+ resultCompletionSource.SetException(new Exception(error.LocalizedDescription));
+ }
+ }
+ );
+
+ return resultCompletionSource.Task;
+ }
+
+ public override Task GetBasicPropertiesAsync(CancellationToken ct)
+ {
+ var resources = PHAssetResource.GetAssetResources(_phAsset);
+ var imageSizeBytes = (resources.FirstOrDefault()?.ValueForKey(new NSString("fileSize")) as NSNumber) ?? 0;
+ var dateModified = _phAsset.ModificationDate.ToDateTimeOffset();
+ return Task.FromResult(new BasicProperties((ulong)imageSizeBytes, dateModified));
+ }
+
+ public override Task GetParentAsync(CancellationToken ct) => Task.FromResult(_parent);
+
+ public override async Task OpenAsync(CancellationToken ct, FileAccessMode accessMode, StorageOpenOptions options)
+ {
+ await EnsureLoadedAsync();
+
+ if (_fileUrl is null)
+ {
+ throw new InvalidOperationException("The file could not be loaded.");
+ }
+
+ return new RandomAccessStreamWithContentType(FileRandomAccessStream.CreateSecurityScoped(_fileUrl, ToFileAccess(accessMode), ToFileShare(options)), ContentType);
+ }
+
+ public override async Task OpenStreamAsync(CancellationToken ct, FileAccessMode accessMode, StorageOpenOptions options)
+ {
+ await EnsureLoadedAsync();
+
+ if (_fileUrl is null)
+ {
+ throw new InvalidOperationException("The file could not be loaded.");
+ }
+
+ Func streamBuilder = () => File.Open(Path, FileMode.Open, ToFileAccess(accessMode), ToFileShare(options));
+ var streamWrapper = new SecurityScopeStreamWrapper(_fileUrl, streamBuilder);
+ return streamWrapper;
+ }
+
+ public override Task OpenTransactedWriteAsync(CancellationToken ct, StorageOpenOptions option) => throw new NotSupportedException();
+
+ protected override bool IsEqual(ImplementationBase implementation) => implementation is PHAssetFile file && file._phAsset.LocalIdentifier == _phAsset.LocalIdentifier;
+
+ private async Task EnsureLoadedAsync()
+ {
+ if (_fileUrl is not null)
+ {
+ return;
+ }
+
+ var identifier = GetUTIdentifier(_pickerResult.ItemProvider.RegisteredTypeIdentifiers ?? []) ?? "public.data";
+ var extension = GetUTFileExtension(identifier);
+ var result = await _pickerResult.ItemProvider.LoadFileRepresentationAsync(identifier);
+ if (result != null)
+ {
+ var destinationUrl = NSFileManager.DefaultManager
+ .GetTemporaryDirectory()
+ .Append($"{NSProcessInfo.ProcessInfo.GloballyUniqueString}.{extension}", false);
+
+ using Stream source = File.Open(result.Path!, FileMode.Open);
+ using Stream destination = File.Create(destinationUrl.Path!);
+ await source.CopyToAsync(destination);
+
+ _fileUrl = destinationUrl;
+ }
+ }
+ }
+
+ internal static string? GetUTIdentifier(string[] identifiers)
+ {
+ if (!(identifiers?.Length > 0))
+ {
+ return null;
+ }
+
+ if (identifiers.Any(i => i.StartsWith(UTType.LivePhoto, StringComparison.InvariantCultureIgnoreCase)) && identifiers.Contains(UTType.JPEG))
+ {
+ return identifiers.FirstOrDefault(i => i == UTType.JPEG);
+ }
+
+ if (identifiers.Contains(UTType.QuickTimeMovie))
+ {
+ return identifiers.FirstOrDefault(i => i == UTType.QuickTimeMovie);
+ }
+
+ return identifiers.FirstOrDefault();
+ }
+
+ internal static string? GetUTFileExtension(string identifier) => UTType.CopyAllTags(identifier, UTType.TagClassFilenameExtension)?.FirstOrDefault();
}
}