Skip to content

Conversation

@matt-goldman
Copy link

@matt-goldman matt-goldman commented Dec 1, 2025

Description of Change

Updates the FileSaver maciOS implementation to use the new asCopy argument in the UIDocumentPickerViewController to 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

  • Has a linked Issue, and the Issue has been approved(bug) or Championed (feature/proposal)
  • Has tests (if omitted, state reason in description)
  • Has samples (if omitted, state reason in description)
  • Rebased on top of main at time of PR
  • Changes adhere to coding standard
  • Documentation created or updated: https://github.com/MicrosoftDocs/CommunityToolkit/pulls

Additional information

Only affects macOS and iOS. Screenshots show the resultant change on a physical iOS device:

IMG_3596 IMG_3598

Copilot AI review requested due to automatic review settings December 1, 2025 19:55
Copilot finished reviewing on behalf of matt-goldman December 1, 2025 19:56
bijington
bijington previously approved these changes Dec 1, 2025
Copy link
Contributor

@bijington bijington left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Matt. LGTM!

Copy link
Contributor

Copilot AI left a 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 UIDocumentPickerViewController constructor call to include the asCopy: true parameter
  • 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)
Copy link
Collaborator

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?

Copy link
Author

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.

Copy link
Collaborator

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.

Copy link
Author

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.

Copy link
Collaborator

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

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
Copilot AI review requested due to automatic review settings December 4, 2025 16:49
Copilot finished reviewing on behalf of VladislavAntonyuk December 4, 2025 16:51
Copy link
Contributor

Copilot AI left a 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.

TheCodeTraveler and others added 2 commits December 4, 2025 11:56
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.
Copilot AI review requested due to automatic review settings December 4, 2025 17:09
Copilot finished reviewing on behalf of VladislavAntonyuk December 4, 2025 17:12
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +50 to +53
currentViewController.PresentViewController(documentPickerViewController, true, () =>
{
fileManager.Remove(tempDirectoryPath, out _);
});
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temporary directory cleanup in the completion callback may execute prematurely before the file picker operations complete. The completion callback of PresentViewController is invoked immediately after the presentation animation finishes, not after the user completes their interaction with the document picker.

This means the temp directory (and the file within it) could be deleted while the UIDocumentPickerViewController is still displaying and before the user has saved the file to their chosen location, which would cause the save operation to fail.

Consider removing the cleanup from the completion callback and instead perform the cleanup in the event handlers (DocumentPickerViewControllerOnDidPickDocumentAtUrls and DocumentPickerViewControllerOnWasCancelled) where the actual file picker interaction is complete.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VladislavAntonyuk what are your thoughts on this? I had the cleanup method, which could be called in a finally or as part of InternalDispose, that will always check for the file and remove it if it exists. If we restore that, it's always safe to call, and can be called from any catch or finally block, from dispose, or from the event handlers suggested here.

Copilot AI review requested due to automatic review settings December 6, 2025 20:57
Copy link
Contributor

Copilot AI left a 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 _);
Copy link

Copilot AI Dec 6, 2025

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.

Suggested change
fileManager.Remove(tempDirectoryPath, out _);
fileManager.Remove(tempDirectoryPath, out _);
InternalDispose();

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] FileSaver iOS displays 'Move' instead of 'Save'

4 participants