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