Loading resources from disk

You’re fine with your own files, but if you want to, for instance, import an image, this is no longer as straightforward as it used to be.

I had code going back a couple of years, and I know it used to work, I wanted to import it into my current project, and not only did it fail, I found a lot of people with similar (but not identical) problems, and no solutions that weren’t ‘disable the sandbox’.

Step 1: Check Sandbox File permissions

In the ‘Target’ ‘Signing and Capabilities’ tab, check that your app has permission to access files. (This is ‘Read’ by default; ‘None’ will crash your app the moment you attempt to use a .fileImporter)

Step 2: Use a .fileImporter to get the URL of an image, create an NSImage from it, display in your app

import SwiftUI

struct OpenImageView: View {

@State var filename = "Filename"
@State var fileImportIsActive: Bool = false
@State var imageChanged: Bool = false
@State var allImages: [NSImage] = []

var body: some View {
HStack {
Text(filename)
.padding()
if imageChanged == true {
if let image = allImages.last{
Image(nsImage: image)
.resizable()
.scaledToFit()
} else {
Text("No can haz image")
}
}

Button(action: { fileImportIsActive = true}, label: {
Text("Import Image")
})
.padding()
.fileImporter(
isPresented: $fileImportIsActive,
allowedContentTypes: [.image],
allowsMultipleSelection: false
) { result in
do {
guard let selectedFileURL: URL = try result.get().first else { return }
guard selectedFileURL.startAccessingSecurityScopedResource() == true else {return}
guard let image = NSImage(contentsOf: selectedFileURL) else {
return }
selectedFileURL.stopAccessingSecurityScopedResource()

self.filename = selectedFileURL.lastPathComponent
allImages.append(image)
imageChanged = true

} catch {
print("Unable to read file contents")
print(error.localizedDescription)
}
}
}
.frame(width: 400, height: 200, alignment: .center)
}
}

I am aware that an explicit ‘imageHasChanged’ Bool can be avoided by observing allImages, but that was not the point here, and sometimes – for reasons of debugging – more granularity is needed.

Step 3: Remember the Scoop

Make it a habit to check that not only you gave your app permission to access a resource (if you don’t, it will fail silently, the image will simply be nil), but that you revoke permission at the earliest opportunity.

Apple has a dire warning about that:

If you fail to relinquish your access to file-system resources when you no longer need them, your app leaks kernel resources. If sufficient kernel resources leak, your app loses its ability to add file-system locations to its sandbox, such as with Powerbox or security-scoped bookmarks, until relaunched.

This sounds like a bad problem to have, because it will be intermittend and occur away from the site of the problem, so make checking for this a part of your process.

As an aside: looking for this I found an awful lot of articles stating variations on ‘SwiftUI has no OpenDialog, so here’s how you use AppKit’ and no, it’s not an OpenDialog, it’s a .fileImporter – and similarly, there is no SaveDialog, but a .fileExporter
I hve found that a number of times with SwiftUI: It’s not that Swift UI doesn’t have the functionality, but the mechanism is different (or there’s a silent bridge to UIKit/Appkit) and it can be very difficult to find out ‘how to do x in SwiftUI’.