iOS 17 beta "You don’t have permission to save the file" crash on device

Issue

This issue is reproducible on iOS 17 beta 4 and iOS 17.0 (21A5303d) public beta.

When try to create a folder using the FileManager API, the app crashes with the following stack trace.

Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “Samples” in the folder “…”." UserInfo={NSURL=file://…/Samples.app/, NSUnderlyingError=0x28100cf00 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}

What is the user impact?

Users will not be able to run the app on iOS 17

Steps to Reproduce

  1. Create a new project
  2. Add the following code
import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .onAppear {
                _ = makeTemporaryDirectory()
            }
    }
    
    /// Creates a temporary directory.
    private func makeTemporaryDirectory() -> URL {
        try! FileManager.default.url(
            for: .itemReplacementDirectory,
            in: .userDomainMask,
            appropriateFor: Bundle.main.bundleURL,
            create: true
        )
    }
}
  1. Build and run the app. It crashes on the force try line.

O/S: iOS 17 (Beta 4) / iOS 17.0 (21A5303d)
Device: iPad Pro Gen 4, iPhone 11. Only happens on real device, not on the simulators

Accepted Reply

I suspect that you’re seeing the error because of this: File system changes introduced in iOS 17. You’ve told FileManager to find you the replacement directory that’s appropriate for Bundle.main.bundleURL. Now that your app is on a different volume than your container, that’s no longer possible.

Where do you ultimately plan to save your files? In your app’s Documents directory? If so, call this API twice, once to get the location of the Documents directory and then again to get a replacement directory that’s appropriate for that.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

I suspect that you’re seeing the error because of this: File system changes introduced in iOS 17. You’ve told FileManager to find you the replacement directory that’s appropriate for Bundle.main.bundleURL. Now that your app is on a different volume than your container, that’s no longer possible.

Where do you ultimately plan to save your files? In your app’s Documents directory? If so, call this API twice, once to get the location of the Documents directory and then again to get a replacement directory that’s appropriate for that.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you Eskimo!

Where do you ultimately plan to save your files?

As the API semantically indicates, we want to create a temporary directory specific for the current user, for our own app, preferably with the app's display name in the path component so it is easier to look up when debugging.

We don't really mind where the temporary directory is located; just want to be a good citizen, limit the files within our own subdirectory, and not to pollute the wider tmp folder, i.e., FileManager.default.temporaryDirectory. And we want to clean up the subfolder at a certain point.

I have to apologize that I didn't scrutinize the doc (or fully understand its intricacy) of the url(for:in:appropriateFor:create:) API, and missed the "Only the volume of this parameter is used." part in the doc. Took it for granted for too long that iOS/macOS only uses 1 volume for user data storage.

What is the recommended approach for creating a temporary directory? I think the url(for:in:appropriateFor:create:) is still the recommended API to use. Should I…

  • Pass nil to the appropriateFor URL, and manually create a subdirectory using an UUID
  • Use cachesDirectory instead of itemReplacementDirectory
  • Use the system temp directory at FileManager.default.temporaryDirectory, and manually create a subdirectory using an UUID

Edit: it seems for iOS 16+ there would be a different solution such as URL.temporaryDirectory. At the mean time, I'll apply what Quinn suggested.

try! FileManager.default.url(
    for: .itemReplacementDirectory,
    in: .userDomainMask,
    appropriateFor: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first,
    create: true
)

A few things here:

  • The basic reason "itemReplacementDirectory" exists is to allow you to find/create temporary directories on specific volumes. The main use case here is as an intermediate location when implementing "safe save" operations. Copy the original file to the replacement directory, modify the copy, then use the atomic replace to complete the copy. If you don't care what volume files are on, then you can just use "NSTemporaryDirectory()" (or "Caches" if you prefer). I have a very difficult time thinking of any reason why you'd use "itemReplacementDirectory" AND pass in NULL.

  • You said that one of your goals was to "not to pollute the wider tmp folder", but I'm not sure what that means in the context of iOS. There is no "wider" tmp directory. The only tmp directory you have access to is inside your apps container and is "yours". Creating a directory(s) for your own use inside tmp/Caches is perfectly reasonable (it keeps things "tidier" and helps prevent name collisions), but I don't think completely avoiding either is a "good" idea.

  • The "itemReplacementDirectory" is not equivalent to tmp or Caches, as it may not be cleaned out in the same way as tmp/Caches is. The problem there is the word "may". Per the API contract, we COULD place it inside either of those directories (in which case, it will be cleaned out) but... we might not. That inconsistency is a obviously problem.

Pulling all of this together into recommendations:

  1. If you need a temporary location on the same volume as a particular file (for example, for safe saving), use "itemReplacementDirectory". That's what it's actually "for".

  2. If you need a temporary directory and you want the system to delete the data as necessary, use "NSTemporaryDirectory()" or "NSCachesDirectory".

  3. If you want to manage the storage your self, then you can create your own directory in "NSLibraryDirectory" or "NSApplicationSupportDirectory". In that case, make sure you exclude directory from backups:

https://developer.apple.com/documentation/foundation/optimizing_your_app_s_data_for_icloud_backup/

https://developer.apple.com/documentation/foundation/nsurlisexcludedfrombackupkey

-Kevin Elliott
DTS Engineer, CoreOS/Hardware

Kevin, thanks for your response. That clears things up quite a bit. I'll try to summarize what I've learned here, as well as some lingering questions - feel free to correct.

  • Learned
  1. My question in the original post was caused by the File system changes introduced in iOS 17 - iOS 17 will put the app bundle and its data container on different volumes.
    When url(for:in:appropriateFor:create:) creates an itemReplacementDirectory, it uses the volume from the appropriateFor url: parameter.
    Since the app bundle is on a different volume now, the method throws a file permission error.

  2. There are 2 kinds of temporary files - a) those will eventually end up in another destination, and b) those don't need to be kept around and will be purged with the temporary directory.

    • in case a), the temporary file is intended to replace another file, say we want to modify the temporary copy and overwrite the original file, itemReplacementDirectory with url(for:in:appropriateFor:create:) is the most suitable API.
    • in case b), the temporary file may be just an transitory output that we may read from, then throw away. NSTemporaryDirectory() and its equivalents are the most suitable.
  3. The itemReplacementDirectory has no guarantee to be under the tmp directory, and we should not rely on any folder structure of its parent directories, to recursively remove items, or whatever.


  • Questions
  1. There are a couple of doc pages talking about temporary directories on Apple Developer Documentation.

    1. https://developer.apple.com/documentation/foundation/1409211-nstemporarydirectory
    2. https://developer.apple.com/documentation/foundation/filemanager/1642996-temporarydirectory
    3. https://developer.apple.com/documentation/foundation/filemanager/1407693-url
    4. https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/itemreplacementdirectory
    • On the 1st page, it says "see url(for:in:appropriateFor:create:) for the preferred means of finding the correct temporary directory".
    • On the 3rd page discussion section, it says "You can use this method to create a new temporary directory."
    • On the 4th page, it says "Pass this constant … to create a temporary directory."

    These docs were where the confusion really came from. It pushed us to use url(for:in:appropriateFor:create:) API in the first place, only later did we find out that API best suits a edit-and-move temp file (case a above), not for a use-and-throw temp file.
    Hope some distinction between these 2 usecases can be added to the doc's discussion.

  2. Two luxuries come with using url(for:in:appropriateFor:create:) with itemReplacementDirectory API are…
    a) it helps create the directory instead of us calling createDirectory manually, and
    b) it creates a subdirectory that is app-specific. This doesn't mean much on iOS, but as we are doing cross-platform apps/frameworks, this make macOS development easier. Besides, the item replacement directory always has the bundle name in part of its path components, which makes debugging easier. i.e.,

# NSTemporaryDirectory()
file:///var/folders/09/qv7t964n5nv1zh6bqvzthlvc0000gp/T/
# itemReplacementDirectory
file:///var/folders/09/qv7t964n5nv1zh6bqvzthlvc0000gp/T/TemporaryItems/NSIRD_MyProject_dNMkNh/

Indeed, we can easily create our own method to provide these capabilities, but it's always hard to say no to a native one-liner API that behaves almost exactly as we wanted. 😉