0

I have a Swift program that helps organise cat shows. It generates several rtf files and an xml file, which should be saved into one folder on disk, and only have minor modifications to their file names - the first one could be named 'Cat show.rtf', then the next 'Cat show judges.rtf', and the next 'Cat show.xml'.

I would like to only display one NSSavePanel to locate the folder to save the files in, rather than keep popping up a save panel for each file.

I tried using a NSSavePanel to get a URL for the first file, and then modifying that URL for each subsequent file. The first file was saved, but each following file got a 'Operation not permitted' error and was not saved. I tried using NSSavePanel to get the folder to save the files in, but got a 'Operation not permitted' error for all of the files. I could just use NSSavePanel to get the URL for each file individually, but would rather not do that as it will confuse the users.

Does anyone have any idea how I can do this?

The code I use to change the URL is:

func newURL(from oldURL : URL, with newName: String) -> URL {
  return oldURL.deletingLastPathComponent().appendingPathComponent(newName)
}

let challengesURL  = newURL(from: url, with: "\(name).xml")
  • Are you able to provide some code you are using to get the url and update it on subsequent files. I would think error is in changing the url as I have used this method fine before. – Hongtron Mar 23 '23 at 04:18
  • I have added the code I used – John Sandercock Mar 23 '23 at 20:18
  • That url update works fine for me. Have you checked the permissions to read / write the directory you are trying to save to. If you go to the project settings, Targets, Signing and Capabilities. I have noticed in my test if I select to save to the desktop, I can save the first file as I have User Selected File set to read/write but the subsequent amended file names will not write. If I save to the user download folder, which also has read / write permissions set, the files with the updated names saved in code also save. – Hongtron Mar 24 '23 at 05:48
  • I have looked at the 'Signing & Capabilities' but cannot see any read/write permissions. Could you please explain how I can find them? Thanks. – John Sandercock Mar 25 '23 at 08:49
  • Do you have the AppSandbox enabled (https://developer.apple.com/documentation/security/app_sandbox). When the target is selected to My Mac, I select the Target and then in Signing & Capabilities there is an App Sandox. Under this there is a File Access section. In File Access I can select User Selected File which allows the user to select directory and save there in the NSSavePanel / NSOpenPanel. – Hongtron Mar 27 '23 at 06:42
  • Will the user manipulate any of these files outside of your program? If no, do they actually need to be separate files? – Caleb Mar 27 '23 at 13:34

1 Answers1

1

OK, I have confirmed what is happening here. First of all you need to have the AppSandbox enabled and the file access permissioned to allow read/write for the User Selected File. I have noted how to do this in the comments below, but as you are able to save the user selected file name I propose this is OK.

In short, the NSSavePanel only returns a Security Scoped URL for the filename entered, not the directory. As such you can save that file many times but not adjust the filename (as it is a new URL without permissions). A possible work around and how I had previously saved multiple files is to use an NSOpenPanel which allows the user to select a directory. This returns a Security Scoped directory url so you have permission to the directory within the scope. This allows you to save multiple files in that directory.

In the code below I have put the two solutions that demonstrate this and you can see with the NSSavePanel, it saves the entered filename twice with amended contents but does not write the programmatically amended filename. By using the NSOpenPanel to select a directory, and collect the filename from the user in a text field, all files are saved fine.

Hope this helps.

    struct ContentView: View {
    @State var sampleData: [String] = ["Rob" , "Jane" , "Freddy" , "Peter"]
    @State var filename: String = ""
    
    var body: some View {
        VStack {
            Text("File Tests")
            TextField("Enter Filename:", text: $filename).frame(width: 200)
            Button("Save Using Save Panel") {
                savePanel()
            }
            Button("Save NSOpenPanel Directory") {
                savePanelUsingOpenPanelv2()
            }
        }
    }
    
    func savePanelUsingOpenPanelv2() {
        let panel = NSOpenPanel()
        // Sets up so user can only select a single directory
        panel.canChooseFiles = false
        panel.canChooseDirectories = true
        panel.allowsMultipleSelection = false
        panel.showsHiddenFiles = false
        panel.title = "Select Save Directory"
        panel.prompt = "Select Save Directory"
        
        let response = panel.runModal()
        if response == .OK {
            if let panelURL = panel.url { // This is a directory url
                let saveURL = panelURL.appending(component: "\(filename).txt")
                self.saveListAsURL(saveURL) // Saving the initial filename
                print(saveURL.description)
                let newName = "NewNameDirectoryTest"
                let updatedURL = newURL(from: saveURL, with: "\(newName).txt") // Updating the url
                // Updating the array saved
                sampleData.append("Joe")
                sampleData.append("Jack")
                // Saving the updated URL / filename.
                saveListAsURL(updatedURL) // Work fine as directory has security scope.
                print(updatedURL.description)
            }
        }
    }
    
    func savePanel() {
        let savePanel = NSSavePanel()
        if savePanel.runModal() == .OK {
            if let url = savePanel.url { // This is a file url
                self.saveListAsURL(url) // Saving with the panel selected filename
                print(url.description)
                let newName = "NewNameSavePanelTest"
                let updatedURL = newURL(from: url, with: "\(newName).txt")
                
                sampleData.append("Joe")
                sampleData.append("Jack")

                saveListAsURL(updatedURL) // Saving updated url fails
                print(updatedURL.description)
                saveListAsURL(url) // saving original url again works fine.
                print(url.description)
            }
        }
    }
    
    func newURL(from oldURL : URL, with newName: String) -> URL {
      return oldURL.deletingLastPathComponent().appendingPathComponent(newName)
    }
    
    func saveListAsURL(_ url: URL) {
        let fm = FileManager()
        var saveStringArray: [String] = []
        var data = Data()
        
        for name in sampleData {
            let string = name + "\n"
            saveStringArray.append(string)
            let stringData = string.utf8
            data.append(contentsOf: stringData)
        }
        
        let saveResult = fm.createFile(atPath: url.path  , contents: data)
        print("Save results is \(saveResult)")
    }
    
}

The NSSavePanel and NSOpenPanel documentation confirm the Security Scope of the returned urls. There is some ability to save security scoped NSURL bookmarks but I have not used this previously. There are details on bookmarks in the documentation here. It seems an extra level of complexity.

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

You could look in to adding application entitlements to particular directories, but as you seem to want the the user to select anywhere they want to save the Panel approach is the way to go as wide open entitlements are not recommended.

https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/AppSandboxTemporaryExceptionEntitlements.html#//apple_ref/doc/uid/TP40011195-CH5-SW7

Hongtron
  • 217
  • 6