macOS - How to have NSSavePanel to add a file extension in the file name? - swift

I'm using this code to give the user the choice to specify a name and a location where to save a plain text file on disk. All seems to work but the saved file hasn't any extension. Actually I have not specify an extension in any part of my code, I read NSSavePanel documentation without notice the part where explained this option.
Here is the code I'm using:
let textToExport = mainTextField.textStorage?.string
if textToExport != "" {
let mySave = NSSavePanel()
mySave.begin { (result) -> Void in
if result == NSFileHandlingPanelOKButton {
let filename = mySave.url
do {
try textToExport?.write(to: filename!, atomically: true, encoding: String.Encoding.utf8)
} catch {
// failed to write file (bad permissions, bad filename etc.)
}
} else {
NSBeep()
}
}
}

Add the line
mySave.allowedFileTypes = ["txt"]
before presenting the panel.
From the documentation:
The value of this property specifies the file types the user can save
the file as. A file type can be a common file extension, or a UTI. The
default value of this property is nil, which indicates that any file
type can be used. (Note that if the array is not nil and the array
contains no items, an exception is raised.)
If no extension is given by the user, the first item in the
allowedFileTypes array will be used as the extension for the save
panel. If the user specifies a type not in the array, and
allowsOtherFileTypes is true, they will be presented with another
dialog when prompted to save.

Related

MacOS: how to get "Last opened" attribute of file?

In some files in OS exist "Last opened" attribute:
modified and opened attribute is possible to get by the following way:
//modified date
try? FileManager.default.attributesOfItem(atPath: url.path)[FileAttributeKey.modificationDate] as? Date
//creation date
try? FileManager.default.attributesOfItem(atPath: url.path)[FileAttributeKey.creationDate] as? Date
But how to get "Last opened" date?
AFAIK, there is no way to get the last time the file was opened. Instead, you have to get the last time it was last read, written or its directory entry was modified.
Leo's suggestion in comments for another answer to use URLResourceValues.contentAccessDate is probably the cleanest way, especially since you already have a URL, which is typically the case these days.
func lastAccessDate(forURL url: URL) -> Date?
{
return try? url.resourceValues(
forKeys: [.contentAccessDateKey]).contentAccessDate
}
You can also reach down into the BSD layer using the path:
import Darwin // or Foundation
func lastAccessDate(forFileAtPath path: String) -> Date?
{
return path.withCString
{
var statStruct = Darwin.stat()
guard stat($0, &statStruct) == 0 else { return nil }
return Date(
timeIntervalSince1970: TimeInterval(statStruct.st_atimespec.tv_sec)
)
}
}
I'm not 100% of the of the behavior of resourceValues if the URL specified is a symbolic link, but stat() will return information about the file system inode pointed to by the link. If you want information directly about the link itself, use lstat() instead. stat() and lstat() are the same otherwise.
I'm pretty sure that URLResourceValues.contentAccessDate uses either stat() or lstat() under the hood.
One thing to keep in mind is that the last access time is not the last time the file was opened, but rather the last time it was read. The the man page for stat says:
The time-related fields of struct stat are as follows:
st_atime Time when file data last accessed. Changed by the mknod(2), utimes(2) and read(2) system calls.
st_mtime Time when file data last modified. Changed by the mknod(2), utimes(2) and write(2) system calls.
st_ctime Time when file status was last changed (inode data modification). Changed by the chmod(2), chown(2), link(2), mknod(2), rename(2), unlink(2),
utimes(2) and write(2) system calls.
st_birthtime Time of file creation. Only set once when the file is created. This field is only available in the 64 bit inode variants. On filesystems where
birthtime is not available, this field is set to 0 (i.e. epoch).
There the man page is referring to the 32-bit member field names, but the same would apply to the 64-bit names, st_atimespec, st_mtimespec, st_ctimespec, and st_birthtimespec.
To appromixate the getting the last time a file was opened, you'd want to get the the latest of st_atimespec, st_mtimespec and maybe st_ctimespec if you also want to include include changes to the directory entry that don't modify the contents, such as renaming the file or setting its permissions. So you'd need something like this:
func lastReadOrWrite(forFileAtPath path: String) -> Date?
{
return path.withCString
{
var statStruct = Darwin.stat()
guard stat($0, &statStruct) == 0 else { return nil }
let lastRead = Date(
timeIntervalSince1970: TimeInterval(statStruct.st_atimespec.tv_sec)
)
let lastWrite = Date(
timeIntervalSince1970: TimeInterval(statStruct.st_mtimespec.tv_sec)
)
// If you want to include dir entry updates
let lastDirEntryChange = Date(
timeIntervalSince1970: TimeInterval(statStruct.st_ctimespec.tv_sec)
)
return max( lastRead, max(lastWrite, lastDirEntryChange) )
}
}
or using URLResourceValues
func lastReadOrWriteDate(forURL url: URL) -> Date?
{
let valKeys: Set<URLResourceKey> =
[.contentAccessDateKey, .contentModificationDateKey, .attributeModificationDateKey]
guard let urlVals = try? url.resourceValues(forKeys:valKeys)
else { return nil }
let lastRead = urlVals.contentAccessDate ?? .distantPast
let lastWrite = urlVals.contentModificationDate ?? .distantPast
// If you want to include dir entry updates
let lastAttribMod = urlVals.attributeModificationDate ?? .distantPast
return max(lastRead, max(lastWrite, lastAttribMod))
}
Of course, if some process simply opens a file and then closes it without reading or writing, that will go unnoticed, but then if it didn't read or write, does it matter that it opened the file?

Link app object to file on disk with metadata

Following this topic : iOS PDFkit cannot add custom attribute to pdf document
My app is using PDFKit to save files
I'm trying to set custom key metadata to PDFDocument I save on the device.
The object in my app ('Test') has two important properties :
id: a UUID to be able to retrieve the file on disk (the linked file on disk URL is this_UUID.jpg).
name: a human-readable string set by the user.
This cause some problems :
the file name is a UUID not human readable, so it's bad user experience.
If the user renames the file, the app won't be able to get the file.
So the id is to have a human-readable label for the file. So when the user opens the File app he can find it easily. And add metadata with the id so my app can retrieve it even if renamed. Looks like a nice solution right?
// First I create my own attribute
fileprivate extension PDFDocumentAttribute {
static let testId = PDFDocumentAttribute(rawValue: "com.sc.testID")
}
// Then I set the news attributes before saving the file, in the 'test' class
func setDocument(pdf: PDFDocument) {
let fileURL = self.getPDFDocumentURL()
print("the metadata is \(pdf.documentAttributes)") // print an empty dictionary
pdf.documentAttributes?[PDFDocumentAttribute.testId] = self.id
pdf.documentAttributes?[PDFDocumentAttribute.titleAttribute] = self.name // I suppose the ddisplay name of the document ? It's not, so what is that ?
print("the metadata is now \(pdf.documentAttributes)") // both are printed, it looks ok
//pdf.write(to: fileURL) // I tested this one too, same issues
let data = pdf.dataRepresentation()
do {
try data?.write(to: fileURL, options: .completeFileProtection)
} catch {
print(error.localizedDescription)
}
}
From here it looks ok, when I want to retrieve the pdf document I will check in the folder the id of each doc and return the doc when id match. But the problem is when I get the documentAttributes the attribute 'testId' isn't in. Note the native title, is set correctly.
So I could get the id from there but that looks pretty inappropriate
//still in 'Test' class
func getPDFDocument() -> PDFDocument? {
// get all docs in the folder ad check metadata for each
let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
do {
let fileURLs = try fileManager.contentsOfDirectory(at: SchoolTest.getSubjectFolderURL(subject: self.subject!), includingPropertiesForKeys: nil)
for url in fileURLs {
print("the doc attributes are : \(PDFDocument(url: url)?.documentAttributes)") // contain title and others preset by Apple but not my custom 'testId'
if let doc = PDFDocument(url: url), doc.documentAttributes?[PDFDocumentAttribute.titleAttribute/*testId*/] as? String == self.documentName {
return doc // this work temporary
}
}
} catch {
print("Error while enumerating files \(documentsURL.path): \(error.localizedDescription)")
}
return nil
}
Display name:
Currently, the display name/label displayed in the File app is the file name (from URL).
This can cause problems too because if two 'Test' have the same name, their linked file gonna have the same URL. So when the most recent one will be saved on disk it will overwrite the other.
That's a problem I don't have when using the 'Test' id property for the file URL.
If I could set a display name for the file and keep the URL with the UUID that should resolve the problem.
Directories have the same localizing issue, I have named them with English but Apple native directories are localized. A localized name could be nice for user experience.
After hours of research, I can't find a way to localize or apply a display name to my files/directories.
// I tried that without positive result
var url = fileURL as NSURL
try url.setResourceValue("localized label", forKey: .localizedLabelKey)
print("localized name is \(try url.resourceValues(forKeys: [.localizedLabelKey]))")
let newURL = url as URL
try data?.write(to: newURL, options: .completeFileProtection)
Am I doing something badly? How should we do when adding custom metada to a file?

Validity time of the URL from an NSItemProvider

I’m writing a sharing extension that will accept images and perform some action with them. Within a method of my UIViewController subclass, I can access URLs to a particular representation of the files by writing this:
guard let context = self.extensionContext else {
return
}
guard let items = context.inputItems as? [NSExtensionItem] else {
return
}
for item in items {
guard let attachments = item.attachments else {
continue
}
for attachment in attachments {
guard attachment.hasItemConformingToTypeIdentifier("public.jpeg") else {
continue
}
attachment.loadFileRepresentation(forTypeIdentifier: "public.jpeg") { (url, error) in
if let url = url {
// How long is this "url" valid?
}
}
}
}
In the block I pass to loadFileRepresentation(forTypeIdentifier:completionHandler:), I’m given the URL to a file—in this case, a JPEG. Can I assume anything about how long this URL is valid? Specifically, is it safe to write the URL itself to some shared storage area so that my app can access the pointed-to file later? Or should I assume that the URL is ephemeral and that, if I want access to the file it points at, I should make my own copy of that file within this block?
The documentation for loadFileRepresentation states:
This method writes a copy of the file’s data to a temporary file, which the system deletes when the completion handler returns.
So url is valid to the closing curly brace of the completion handler.
You need to copy the file to a known location with the sandbox before you are done in the completion handler if you need access to the file beyond the completion handler.

Save file but hide file extension - Cocoa with Key Value Coding

I'm saving some objects to a file using Key Value Coding. I'd like the extension of the saved file to be hidden (or at least be hidden unless the value in Finder → Preferences → Advanced "Show All File Extensions" is true), but I can't seem to get it working.
I'm saving the file like so:
let saveDialog = NSSavePanel()
saveDialog.allowedFileTypes = ["purr"]
saveDialog.beginWithCompletionHandler() { (result: Int) -> Void in
if result == NSFileHandlingPanelOKButton {
NSFileManager.defaultManager()
.createFileAtPath(saveDialog.URL!.path!, contents: NSData(), attributes: [NSFileExtensionHidden: NSNumber(bool: true)])
let _ = NSKeyedArchiver.archiveRootObject(safePhrases, toFile: saveDialog.URL!.path!)
}
}
return saveDialog.URL
But when viewing the saved files in Finder, the extension is always visible. How can I resolve this?
After Willeke suggestion I set the attributes after writing the file, using NSFileManager's setAttributes:ofItemAtPath:error.
do { try NSFileManager.defaultManager().setAttributes
([NSFileExtensionHidden: NSNumber(bool: true)], ofItemAtPath: saveDialog.URL!.path!) }
catch _{ Swift.print("Unable to hide extension") }

Show folder's contents in finder using Swift

I want to be able to select a folder and show its contents in the Finder. I have managed to select the folder itself and select a file within the folder. But I don't know how to show the contents of an empty folder.
e.g.
Folder A/Folder B
I want to display the contents of folder Folder B (which could be empty).
I have written the following code:
func showFolder(fileName : String)
{
var dataPath = homeDirectory.stringByAppendingPathComponent(fileName)
var urlPath = NSURL(fileURLWithPath: dataPath)
var selectedURLs = [urlPath!]
NSWorkspace.sharedWorkspace().activateFileViewerSelectingURLs(selectedURLs)
}
This only opens Folder A with Folder B highlighted. This is very close, but not quite right.
I need to be able to open Folder B with nothing highlighted. I'm obviously using the wrong command.
Use the selectFile method and pass nil as first argument and the path to the folder to be shown as second argument.
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: "/Users/")
2021 | SWIFT 5.1:
func showInFinder(url: URL?) {
guard let url = url else { return }
if url.isDirectory {
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.path)
} else {
NSWorkspace.shared.activateFileViewerSelecting([url])
}
}
extension URL {
var isDirectory: Bool {
return (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
}
}
showInFinder:
Folder's url = will show content of the folder.
File's url = will open in Finder file's parent and select file there.
Url is nil = Will do nothing
File/path does not exist = Will do nothing
Swift 2.1 code to Launch OS X Finder
Use the selectFile or activateFileViewerSelectingURLs to select files.
Select 1 item in finder with path YOUR_PATH_STRING
NSWorkspace.sharedWorkspace().selectFile(YOUR_PATH_STRING, inFileViewerRootedAtPath: "")
The second param use empty string, if you specify an empty string "" for this parameter, the file is selected in the main viewer.
If you want to select 1 or more files use activateFileViewerSelectingURLs(_ fileURLs: [NSURL])
To select one file
NSWorkspace.sharedWorkspace().activateFileViewerSelectingURLs([NSURL].init(arrayLiteral: NSURL.init(fileURLWithPath: YOUR_PATH_STRING)))
To select multiple files
let urls : [NSURL] = [NSURL.init(fileURLWithPath: "/Users/USER_NAME/Pictures"),
NSURL.init(fileURLWithPath: "/Users/USER_NAME/Music")]
If you provide item that are not in the same folder more windows selecting the specified files are open.
let urls : [NSURL] = [NSURL.init(fileURLWithPath: "/Users/USER_NAME/Pictures"),
NSURL.init(fileURLWithPath: "/Users/USER_NAME/Music"),
NSURL.init(fileURLWithPath: "/Users")]