I cannot work out why hard coding a directory doesn’t work when trying to enumerate through a directory.
I have written a simple function to open a dialog and return a selected folder. The function includes a starting directory (directoryURL below):
func selectFolder(title: String, directoryURL: String = ".") -> String? {
let openPanel=NSOpenPanel();
openPanel.title = title
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canChooseFiles = false
openPanel.canCreateDirectories = true
openPanel.directoryURL = URL(fileURLWithPath: directoryURL)
if(openPanel.runModal() == NSApplication.ModalResponse.OK) {
return directoryURL; // This won’t work
return openPanel.url!.path // This is OK
}
else {
return nil
}
}
In the above function I have prematurely returned with the original directory which is a string, so the whole process is ignored. If I comment out the first return statement, then it will return the selected directory, which is also a string.
Here is a SwiftUI button to test the function:
Button(action: {
let fileManager = FileManager.default
let sourceFolder = selectFolder(title: "test", directoryURL: "/path/to/folder")
if let enumerator = fileManager.enumerator(
at: URL(fileURLWithPath: sourceFolder!),
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles,.skipsPackageDescendants]
) {
for case let fileURL as URL in enumerator {
print("fileURL: \(fileURL)")
}
}
}) {
Text("Test")
}
The purpose is to iterate through the contents of the directory, including subdirectories.
If I return the hard coded string from the function, the for case let fileURL as URL in enumerator statement has nothing, and there are no results. There are no errors either.
If I return the openPanel.url!.path, the for case … statement prints the directory contents as expected.
I can’t see what the function returns which is different from the original string.
What can I do to get a hard coded string to work?
Related
How can this function be re-written to return the variable "files"? I'm completely unfamiliar with async and completion handlers, so I wasn't able to use existing answers as a starting point. It's a simple function that returns the files from a user-selected directory.
func readFolder() {
let dialog = NSOpenPanel()
dialog.prompt = "Choose"
dialog.allowsMultipleSelection = false
dialog.canChooseDirectories = true
dialog.canCreateDirectories = true
dialog.canChooseFiles = false
dialog.showsResizeIndicator = true
dialog.showsHiddenFiles = false
dialog.begin {
(result) -> Void in if result == .OK {
let directory = dialog.url!
do {
var files = try FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: nil
)
} catch {
NSLog(error.localizedDescription)
}
}
}
}
You're on the right track with your question when you mentioned "completion handlers." With async functions, you aren't going to actually return the value directly, but rather through a function that you provide as a completion handler. This takes some getting-used-to to restructure the way you think of some of your code, but is a great concept to become familiar with.
Code first, then explanation:
func doSomethingThatRequiresFiles() {
readFolder { result in
switch result {
case .success(let files):
print(files)
case .failure(let error):
print(error)
}
}
}
func readFolder(completion: #escaping (Result<[URL],Error>) -> Void) {
let dialog = NSOpenPanel()
dialog.prompt = "Choose"
dialog.allowsMultipleSelection = false
dialog.canChooseDirectories = true
dialog.canCreateDirectories = true
dialog.canChooseFiles = false
dialog.showsResizeIndicator = true
dialog.showsHiddenFiles = false
dialog.begin { (result) -> Void in
if result == .OK {
guard let directory = dialog.url else {
assertionFailure("Not a directory")
return
}
do {
let files = try FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: nil
)
completion(.success(files))
} catch {
NSLog(error.localizedDescription)
completion(.failure(error))
}
} else {
//handle cancelled case
}
}
}
The first function (doSomethingThatRequiresFiles ) is an example of a place in your code where you want to deal with files. You'll see there's a switch statement that lets you handle either success or failure. You can see that the line print(files) is where you would put your code that needs to deal with the files somehow.
In the readFolder function, there's now a parameter (completion) gets a Result type -- it can either be an array of URLs or an Error. Read more about Result: https://www.hackingwithswift.com/articles/161/how-to-use-result-in-swift
And detail about why #escaping is used: https://www.donnywals.com/what-is-escaping-in-swift/
Inside the dialog.begin, you can see that completion gets called with either the list of files or the error.
Using async functions will be a familiar pattern when working with the filesystem (in particular if you have to deal with the iCloud APIs) and certainly with networking, where basically everything is asynchronous. It's also a good pattern to be familiar with in situations (like this one) where you're waiting for a UI interaction.
I can check if one file exists with this method:
let fileNameOne = "savedpicture1"
let fileURLOne = documentsDirectoryURL.appendingPathComponent(fileNameOne)
if !FileManager.default.fileExists(atPath: fileURLOne.path) {
removeImage(itemName: "savedpicture1", fileExtension: "jpg")
} else {
print("There was no image to remove")
}
My problem is having to repeat the same lines of code for multiple files. For instance, I would like to check if the files exist in an array of paths, but I would have to repeat the code from above for each file, and it seems too redundant. I'm wondering if there's a way to check multiple files instead of repeating the code for each single path. ".fileExists" only enables me to check one path:
let filePaths = [fileURLOne.path, fileURLTwo.path, fileURLThree.path,
fileURLFour.path]
Write a method for example
func checkFiles(with fileNames: [String] {
for fileName in fileNames {
let fileURL = documentsDirectoryURL.appendingPathComponent(fileName)
if !FileManager.default.fileExists(atPath: fileURL.path) {
removeImage(itemName: fileName, fileExtension: "jpg")
} else {
print("There was no image to remove at", fileURL)
}
}
}
and call it
let fileNames = ["savedpicture1", "savedpicture2", "savedpicture3", "savedpicture4"]
checkFiles(with: fileNames)
I'd like to open a uniquely named output file for writing either plist or data, but not having any luck in getting a handle using either URL routine of init(fileURLWithPath:) or init(string:)
func NewFileHandleForWritingFile(path: String, name: String, type: String, outFile: inout String?) -> FileHandle? {
let fm = FileManager.default
var file: String? = nil
var uniqueNum = 0
while true {
let tag = (uniqueNum > 0 ? String(format: "-%d", uniqueNum) : "")
let unique = String(format: "%#%#.%#", name, tag, type)
file = String(format: "%#/%#", path, unique)
if false == fm.fileExists(atPath: file!) { break }
// Try another tag.
uniqueNum += 1;
}
outFile = file!
do {
let fileURL = URL.init(fileURLWithPath: file!)
let fileHandle = try FileHandle.init(forWritingTo: fileURL)
print("\(file!) was opened for writing")
//set the file extension hidden attribute to YES
try fm.setAttributes([FileAttributeKey.extensionHidden: true], ofItemAtPath: file!)
return fileHandle
} catch let error {
NSApp.presentError(error)
return nil;
}
}
debugger shows
which for this URL init routine adds the scheme (file://) but otherwise the same as the other, and I'd like to prefer the newer methods which throw reutrning (-1) when just using paths. The error thrown (2) is an ENOENT (no such entity!?) as I need a handle to write to I'm confused how else to get one? The sample path is a new folder created at desktop to triage.
Unlike the previous answer, I recommend using Data's write(to:options:) API instead of FileManager's createFile(atPath:contents:attributes:), because it is a URL-based API, which is generally to be preferred over path-based ones. The Data method also throws an error instead of just returning false if it fails, so if something goes wrong, you can tell the user why.
try Data().write(to: fileURL, options: [])
I would also suggesting replacing the path-based FileManager.fileExists(atPath:) with the URL-based checkResourceIsReachable():
if false == ((try? fileURL.checkResourceIsReachable()) ?? false)
You can't create a file handle to a non-existent file. That is what is causing the ENOENT error.
Use FileManager createFile(atPath:contents:attributes:) to create the file just before creating the file handle.
do {
fm.createFile(atPath: file!, contents: nil, attributes: [FileAttributeKey.extensionHidden: true])
let fileURL = URL(fileURLWithPath: file!)
let fileHandle = try FileHandle(forWritingTo: fileURL)
print("\(file!) was opened for writing")
return fileHandle
} catch let error {
NSApp.presentError(error)
return nil;
}
I have two directories as follows:
Directory A contains file X.
Directory B contains an alias to directory A named C.
So there are two possible absolute URLs for file X: /A/X and /B/C/X. (A and B can be anywhere in my filesystem.)
What I need to do is, given the file URL for directory B (file:///B/) and either file URL for file X, determine whether or not file X is within directory B.
Here's what I came up with:
extension URL {
func isIdenticalFile(to other: URL) -> Bool {
return resolvingSymlinksInPath() == other.resolvingSymlinksInPath()
}
func contains(_ other: URL) -> Bool {
guard isFileURL, other.isFileURL, let enumerator = FileManager.default.enumerator(atPath: path) else {
return false
}
for subURL in enumerator.map({ appendingPathComponent($0 as! String) }) {
if subURL.isIdenticalFile(to: other) || subURL.contains(other) {
return true
}
}
return false
}
}
let b = URL(string: "file:///B/")!
let ax = URL(string: "file:///A/X")!
let bcx = URL(string: "file:///B/C/X")!
// Both b.contains(ax) and b.contains(bcx) are true
Is there a simpler/more efficient way to do this?
A better method to determine if two URLs refer to the same
file is to compare their fileResourceIdentifier. From the documentation:
An identifier which can be used to compare two file system objects for equality using isEqual.
Two object identifiers are equal if they have the same file system path or if the paths are linked to same inode on the same file system. This identifier is not persistent across system restarts.
Determining the resource identifier should be faster than fully
resolving the file path. In addition this detects also hard links to
the same file.
More remarks:
The recursion in your code is not necessary because the enumerator
already does a "deep" enumeration.
With enumerator(at: self, ...) you get an enumerator for URLs
instead of paths, so that you don't have to build the subURL.
The code then could look like this:
extension URL {
// Helper property get the resource identifier:
private var identifier: NSObjectProtocol? {
return (try? resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier
}
func contains(_ other: URL) -> Bool {
guard isFileURL, other.isFileURL else {
return false
}
guard let otherId = other.identifier else {
return false
}
guard let enumerator = FileManager.default.enumerator(at: self, includingPropertiesForKeys: [.fileResourceIdentifierKey]) else {
return false
}
for case let subURL as URL in enumerator {
if let fileId = subURL.identifier, fileId.isEqual(otherId) {
return true
}
}
return false
}
}
I have a file path as a string. I want to:
Test if there's a file there
Read the contents of the file as a string
the problem I'm having is that sometimes that file path involves a symbolic link (symlink). Maybe to the file itself. Maybe to one of the directories above the file.
[EDIT] closing this because the following code (that I started with), actually works just fine, there were just multiple levels of user error involved. Thanks for the input folks.
func getUserResource(relativeFilePath: String) -> String? {
let fileManager = NSFileManager.defaultManager()
let userFilePath = NSHomeDirectory() + relativeFilePath
if(fileManager.fileExistsAtPath(userFilePath))
{
do {
return try String(contentsOfFile: userFilePath, encoding: NSUTF8StringEncoding);
} catch {
return nil;
}
}
return nil;
}
If you're not sure if the symlink leads to a file or directory, you should be using fileExistsAtPath(path:, isDirectory:). fileExistsAtPath will always return true for a symlink, because technically there is a file at that path. By passing a boolean pointer to isDirectory, you can follow the symlink to a file or to a directory:
Assume symlinkToSomeFile is a symbolic link to a file and symlinkToSomeDir is a symbolic link to a directory.
let symlinkFilePath = NSHomeDirectory() + "/temp/symlinkToSomeFile"
let symlinkDirPath = NSHomeDirectory() + "/temp/symlinkToSomeDir"
var fileCheck: ObjCBool = false
var dirCheck: ObjCBool = false
print(fileManager.fileExistsAtPath(symlinkFilePath, isDirectory: &fileCheck)) // true
print(fileCheck) // false
print(fileManager.fileExistsAtPath(symlinkDirPath, isDirectory: &dirCheck)) // true
print(dirCheck) // true