-
Notifications
You must be signed in to change notification settings - Fork 470
Fix macios filepicker #2981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix macios filepicker #2981
Conversation
bijington
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Matt. LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes a bug in the FileSaver implementation for macOS and iOS by correctly configuring the UIDocumentPickerViewController to treat the operation as saving a new file rather than copying an existing one.
- Updates the
UIDocumentPickerViewControllerconstructor call to include theasCopy: trueparameter - Ensures the OS properly handles moving temporary files to user-selected locations during save operations
| var tcs = taskCompetedSource = new(cancellationToken); | ||
|
|
||
| documentPickerViewController = new([fileUrl]) | ||
| documentPickerViewController = new([fileUrl], true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we delete temp file in this case if we copy it instead of move?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so. I started refactoring this, I wanted to see if I could make it a more logical flow, and added a Cleanup method that handled that and the dispose. But I didn't want to introduce too many changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally if we don't use the temp file at all and write directly to the target location.
To make it simple for now you can wrap the code in try/finally block and delete temp file in finally section if it exists. It should not add complexity and many changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally if we don't use the temp file at all and write directly to the target location.
I've spent a bit of time looking into this and I don't think it's practical. The only way to write directly outside the app's sandbox is using a security scoped URL and something like NSFileCoordinator. The problem is, this requires the user to grant access to it first, so it's not something we can do with a new file.
Additionally, we would have to track that the user has granted access and have safety checks that fall back to the temp file approach if not. And the problem with the first part is that it requires app-wide tracking and coordination. It's not impossible (far from it) but it's outside the scope of the vertical slice of this API.
The current approach (creating a temp file, then moving) is the canonical approach with Apple's API.
To make it simple for now you can wrap the code in try/finally block and delete temp file in finally section if it exists. It should not add complexity and many changes.
Agreed 🙂 Done in 47ddfe5. Please let me know if this is ok, or if you want me to make any other changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made a tiny change to ensure the temp file is always deleted. Please let me know if it looks good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. I made a couple more changes, I updated the exception message wording a little, and moved the controller and event handler registration to address the comment below. I also updated the finally to delete the temp directory instead of the file, to avoid accumulation of orphaned guid-named directories.
Ensures temporary directory used by the file picker is always removed after the file selection is made, regardless of success or failure. Also improves error handling when a view controller cannot be retrieved. Fixes CommunityToolkit#2460
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs
Outdated
Show resolved
Hide resolved
src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs
Show resolved
Hide resolved
Ensures temporary directory used by the file picker is correctly removed on iOS. Moves the directory removal to the completion handler of the view controller presentation, ensuring it's executed after the picker is dismissed. Handles exceptions during file removal by re-throwing them after attempting to remove the temporary directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs
Outdated
Show resolved
Hide resolved
src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs
Show resolved
Hide resolved
…mplementation.macios.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
| catch | ||
| { | ||
| throw new FileSaveException("Unable to get a window where to present the file saver UI."); | ||
| fileManager.Remove(tempDirectoryPath, out _); |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The catch block doesn't handle cleanup of event handlers or dispose the document picker. If an exception occurs after the event handlers are attached (lines 44-45), these handlers will remain attached and could cause memory leaks or unexpected behavior in subsequent operations.
Consider calling InternalDispose() in the catch block to ensure proper cleanup of all resources.
| fileManager.Remove(tempDirectoryPath, out _); | |
| fileManager.Remove(tempDirectoryPath, out _); | |
| InternalDispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Move temp file removal to finally block to ensure cleanup always occurs. Remove redundant cleanup from view controller presentation callback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
| documentPickerViewController.DidPickDocumentAtUrls += DocumentPickerViewControllerOnDidPickDocumentAtUrls; | ||
| documentPickerViewController.WasCancelled += DocumentPickerViewControllerOnWasCancelled; | ||
|
|
||
| var currentViewController = Platform.GetCurrentUIViewController(); | ||
| if (currentViewController is not null) | ||
| { | ||
| currentViewController.PresentViewController(documentPickerViewController, true, null); | ||
| var currentViewController = Platform.GetCurrentUIViewController(); | ||
| if (currentViewController is not null) | ||
| { | ||
| currentViewController.PresentViewController(documentPickerViewController, true, null); | ||
| } | ||
| else | ||
| { | ||
| throw new FileSaveException("Unable to get a window where to present the file saver UI."); | ||
| } |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an exception is thrown at line 54 (when currentViewController is null), the event handlers registered on lines 44-45 will not be cleaned up, potentially causing memory leaks. The documentPickerViewController won't be disposed either. Consider moving the event handler registration and view controller presentation inside a try-catch block, or ensure cleanup happens in the finally block by calling InternalDispose() when needed.
…essage, ensure temp directory is deleted (not just temp file)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
| taskCompetedSource?.TrySetCanceled(CancellationToken.None); | ||
| var tcs = taskCompetedSource = new(cancellationToken); | ||
|
|
||
| documentPickerViewController = new([fileUrl], true) |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using a named argument for the boolean parameter to improve code readability. The current code new([fileUrl], true) doesn't make it clear what true means. Using new([fileUrl], asCopy: true) would make the intent explicit that this parameter controls whether the operation should be treated as a copy/save operation rather than just opening/referencing the file.
| documentPickerViewController = new([fileUrl], true) | |
| documentPickerViewController = new([fileUrl], asCopy: true) |
| public sealed partial class FileSaverImplementation : IFileSaver | ||
| { | ||
| UIDocumentPickerViewController? documentPickerViewController; | ||
| TaskCompletionSource<string>? taskCompetedSource; |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field name "taskCompetedSource" contains a typo - it should be "taskCompletedSource" (missing 'l'). This typo is carried over from existing code but should be corrected for clarity and professionalism.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.
| public sealed partial class FileSaverImplementation : IFileSaver | ||
| { | ||
| UIDocumentPickerViewController? documentPickerViewController; | ||
| TaskCompletionSource<string>? taskCompetedSource; |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field name has a typo: "taskCompetedSource" should be "taskCompletedSource" (missing 'l' in 'Completed').
src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs
Show resolved
Hide resolved
src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| AllowsMultipleSelection = false | ||
| }; | ||
|
|
||
| TaskCompletionSource<Folder>? taskCompetedSource; |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field name 'taskCompetedSource' contains a spelling error. It should be 'taskCompletedSource' to match the corrected spelling in FileSaverImplementation.macios.cs. This typo exists in all usages throughout the file (lines 13, 20, 21, 54, 60).
| documentPickerViewController.DidPickDocumentAtUrls += DocumentPickerViewControllerOnDidPickDocumentAtUrls; | ||
| documentPickerViewController.WasCancelled += DocumentPickerViewControllerOnWasCancelled; |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition: the event handlers (DocumentPickerViewControllerOnDidPickDocumentAtUrls and DocumentPickerViewControllerOnWasCancelled) reference the field 'taskCompetedSource', but this field could be modified by a subsequent call to InternalPickAsync before the event handlers fire. If InternalPickAsync is called again while a previous operation is still pending, the new TaskCompletionSource will overwrite the field at line 21, causing the first operation's event handlers to complete the wrong task.
Consider capturing the TaskCompletionSource in a local variable and using that local variable in the event handlers to avoid this race condition. This would require using lambda expressions instead of method references for the event handlers.
| void DocumentPickerViewControllerOnDidPickDocumentAtUrls(object? sender, UIDocumentPickedAtUrlsEventArgs e) | ||
| { | ||
| try | ||
| if (e.Urls.Length == 0) |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback to 'e.Urls[0].ToString()' may not provide the correct file path format expected by callers. The original code threw a FileSaveException when Path was null, which is more explicit about the failure. Using ToString() as a fallback could return a URL string (e.g., "file:///path/to/file") instead of a file system path, which may cause issues for consumers expecting a standard file path.
Consider whether this fallback is intentional and correct, or if throwing an exception (as in the original code) would be more appropriate to maintain consistency with the error handling pattern used elsewhere (e.g., line 59 in FolderPickerImplementation which still throws when Path is null).
|
|
||
| documentPickerViewController = new([fileUrl]) | ||
| await WriteStream(stream, fileUrl.Path ?? throw new FileSaveException("Path cannot be null."), progress, cancellationToken); | ||
| UIDocumentPickerViewController? documentPickerViewController = new([fileUrl], true) |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nullable annotation '?' on documentPickerViewController is unnecessary since the variable is immediately initialized and never assigned null. The variable should be declared as non-nullable: 'UIDocumentPickerViewController documentPickerViewController'.
| UIDocumentPickerViewController? documentPickerViewController = new([fileUrl], true) | |
| UIDocumentPickerViewController documentPickerViewController = new([fileUrl], true) |
| { | ||
| throw new FileSaveException("Cannot present file picker: No active view controller found. Ensure the app is active with a visible window."); | ||
| } | ||
|
|
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's trailing whitespace at the end of this line. Consider removing it for consistency with the codebase formatting standards.
|
|
||
| taskCompletedSource?.TrySetCanceled(CancellationToken.None); | ||
| var tcs = taskCompletedSource = new(cancellationToken); | ||
|
|
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's trailing whitespace at the end of this line. Consider removing it for consistency with the codebase formatting standards.
Description of Change
Updates the
FileSavermaciOS implementation to use the newasCopyargument in theUIDocumentPickerViewControllerto tell the OS that you're saving a new file (not copying) when moving the temp file out of the sandobx to the users' desired location.Linked Issues
PR Checklist
approved(bug) orChampioned(feature/proposal)mainat time of PRAdditional information
Only affects macOS and iOS. Screenshots show the resultant change on a physical iOS device: