Save an image to MacOS file system with SwiftUI
SwiftUI does not have a control to present an open/save dialog to the user to allow saving files to the macOS filesystem. One could build such a view from scratch, but it is easier to use NSSavePanel from AppKit. This article demonstrates how to use NSSavePanel to get the URL for the file to be saved and use NSBitmapImageRep to save a png image. Note it is necessary to set the User Selected File Read/Write entitlements for the app to allow the app to save files.
Starter App
Create a new macOS app in Xcode. Add some images to the App Assets by dragging the images into the Assets. These can be given descriptive names to be used when referencing the images. This code lays out the main view with a title, an Image and buttons to change the image and save the image.
1struct ContentView: View {
2 var body: some View {
3 ZStack {
4 Color(red: 214/255, green: 232/255, blue: 248/255)
5 .edgesIgnoringSafeArea(.all)
6
7 VStack(spacing:20) {
8 Text("Saving Image to File System")
9 .foregroundColor(.blue)
10 .font(.title)
11 .fontWeight(.bold)
12
13 Image("cake")
14 .resizable()
15 .scaledToFit()
16 .frame(width: 400, height: 400, alignment: .center)
17
18 HStack {
19 Button("Change Image") {
20
21 }
22 .buttonStyle(BlueButtonStyle())
23
24 Button("Save Image") {
25
26 }
27 .buttonStyle(BlueButtonStyle())
28 }
29
30 Spacer()
31 }
32 .padding(50)
33 }
34 }
35}
Create new macOS App in Xcode
Add images to App Assets
Initial SwiftUI macOS app to display an image
NSSavePanel
The best way to save any file in macOS is to present the user with an open/save
dialog that will allow the user to navigate the file system and choose where to save
the file. Unfortunately, there is no native SwiftUI view or control available to
browse and save files from SwiftUI, but there is in AppKit. AppKit provides
NSSavePanel that presents a panel in a separate process, which allows the user to
set the name of the file and specify the location to save the file. Add a function
showSavePanel
to the view to instantiate the NSSavePanel and capture the response.
1 func showSavePanel() -> URL? {
2 let savePanel = NSSavePanel()
3 savePanel.allowedContentTypes = [.png]
4 savePanel.canCreateDirectories = true
5 savePanel.isExtensionHidden = false
6 savePanel.title = "Save your image"
7 savePanel.message = "Choose a folder and a name to store the image."
8 savePanel.nameFieldLabel = "Image file name:"
9
10 let response = savePanel.runModal()
11 return response == .OK ? savePanel.url : nil
12 }
13
14. . .
15
16 .buttonStyle(BlueButtonStyle())
17
18 Button("Save Image") {
19 showSavePanel()
20 }
21 .buttonStyle(BlueButtonStyle())
22. . .
23
Running this App now and selecting the Save Image
button results in a runtime
error. The default entitlements only allow reading from the file system.
ERROR: Unable to display save panel: your app has the User Selected File Read entitlement but it needs User Selected File Read/Write to display save panels. Please ensure that your app's target capabilities include the proper entitlements.
Error using NSSavePanel without Read/Write entitlement
Read/Write Entitlement
To set the file access entitlements, select the App in the Project Navigator panel.
Then select the app under TARGETS and select Signing & Capabilities. Look
for File Access in the App Sandbox settings. Change the Permission & Access
value to Read/Write
for User Selected File.
Change permissions to Read/Write on 'User Selected File' for App
Run the application again and select Save Image
. This time there is no error and
the save panel is displayed. Of course the file is not saved as that has not been
implemented yet.
Save panel is displayed without an error when permissions set to Read/Write
Save the Image
The showSavePanel
function uses NSSavePanel to allow the user to specify the
folder and file and returns an optional URL. The NSSavePanel provides a lot of
functionality out of the box such as the options to create new folders or to cancel.
It also handles the case where the filename is blanked out by disabling the Save
button. The returned optional url can easily be tested and only proceed to save the
image when the url is not nil.
The savePNG
function creates an NSImage using the named resource and uses
NSBitmapImageRep to format the bitmap representation’s image data using png
storage type and an empty property set. This data is then written to the path set
from the NSSavePanel.
1struct ContentView: View {
2
3 func showSavePanel() -> URL? {
4 let savePanel = NSSavePanel()
5 savePanel.allowedContentTypes = [.png]
6 savePanel.canCreateDirectories = true
7 savePanel.isExtensionHidden = false
8 savePanel.title = "Save your image"
9 savePanel.message = "Choose a folder and a name to store the image."
10 savePanel.nameFieldLabel = "Image file name:"
11
12 let response = savePanel.runModal()
13 return response == .OK ? savePanel.url : nil
14 }
15
16 func savePNG(imageName: String, path: URL) {
17 let image = NSImage(named: imageName)!
18 let imageRepresentation = NSBitmapImageRep(data: image.tiffRepresentation!)
19 let pngData = imageRepresentation?.representation(using: .png, properties: [:])
20 do {
21 try pngData!.write(to: path)
22 } catch {
23 print(error)
24 }
25 }
26
27 var body: some View {
28 ZStack {
29 Color(red: 214/255, green: 232/255, blue: 248/255)
30 .edgesIgnoringSafeArea(.all)
31
32 VStack(spacing:20) {
33 Text("Saving Image to File System")
34 .foregroundColor(.blue)
35 .font(.title)
36 .fontWeight(.bold)
37
38 Image("cake")
39 .resizable()
40 .scaledToFit()
41 .frame(width: 400, height: 400, alignment: .center)
42
43 HStack {
44 Button("Change Image") {
45
46 }
47 .buttonStyle(BlueButtonStyle())
48
49 Button("Save Image") {
50 if let url = showSavePanel() {
51 savePNG(imageName: "cake", path: url)
52 }
53 }
54 .buttonStyle(BlueButtonStyle())
55 }
56
57 Spacer()
58 }
59 .padding(50)
60 }
61 }
62}
Save the image to a folder in Docuements folder
Switch Image and Save
The code above is hard-coded to save the cake image. Let's implement the
Change Image
and save the displayed image. An Array of images is created and a
state property is used to store the index of the currently selected image. The
Change Image
button iterates over the list of images and the Save Image
is set to
save the currently selected image.
1struct ContentView: View {
2 @State private var selectedImage = 1
3
4 private var images: [String] = ["cake", "mountain", "coffee", "trees"]
5
6 func showSavePanel() -> URL? {
7 let savePanel = NSSavePanel()
8 savePanel.allowedContentTypes = [.png]
9 savePanel.canCreateDirectories = true
10 savePanel.isExtensionHidden = false
11 savePanel.title = "Save your image"
12 savePanel.message = "Choose a folder and a name to store the image."
13 savePanel.nameFieldLabel = "Image file name:"
14
15 let response = savePanel.runModal()
16 return response == .OK ? savePanel.url : nil
17 }
18
19 func savePNG(imageName: String, path: URL) {
20 let image = NSImage(named: imageName)!
21 let imageRepresentation = NSBitmapImageRep(data: image.tiffRepresentation!)
22 let pngData = imageRepresentation?.representation(using: .png, properties: [:])
23 do {
24 try pngData!.write(to: path)
25 } catch {
26 print(error)
27 }
28 }
29
30 var body: some View {
31 ZStack {
32 Color(red: 214/255, green: 232/255, blue: 248/255)
33 .edgesIgnoringSafeArea(.all)
34
35 VStack(spacing:20) {
36 Text("Saving Image to File System")
37 .foregroundColor(.blue)
38 .font(.title)
39 .fontWeight(.bold)
40
41 Image(images[selectedImage])
42 .resizable()
43 .scaledToFit()
44 .frame(width: 400, height: 400, alignment: .center)
45
46 HStack {
47 Button("Change Image") {
48 selectedImage = (selectedImage + 1) % images.count
49 }
50 .buttonStyle(BlueButtonStyle())
51
52 Button("Save Image") {
53 if let url = showSavePanel() {
54 savePNG(imageName: images[selectedImage], path: url)
55 }
56 }
57 .buttonStyle(BlueButtonStyle())
58 }
59
60 Spacer()
61 }
62 .padding(50)
63 }
64 }
65}
Switch image from Assets and save the image
Images from Assets are saved to folder in Documents folder
Conclusion
I feel that SwiftUI should have a native mechanism of presenting the user with a panel to open or save to the file system. No doubt there are challenges of making a consistent UI that would work in SwiftUI on the different Operating Systems supported. For now, we will have to resort to using AppKit in macOS SwiftUI applications. This article demonstrated how to use NSSavePanel to present the panel to the user and return the URL for the file and how to use NSBitmapImageRep to save a png image. The User Selected File entitlement has to be set to Read/Write to allow the app to save files.