Get Folder's Number of Elements

How can I get element number of a folder as a value for label?

FileManager is what you want.

https://developer.apple.com/documentation/foundation/filemanager

import Foundation
let path = "/"
let numberOfItems = try FileManager.default.contentsOfDirectory(atPath: path).count

2 Likes

contentsOfDirectory(atPath:) will work but my general advice is that you use the URL based API for working with the file system. Specifically, to enumerate a directory use contentsOfDirectory(at:includingPropertiesForKeys:options:). That has the advantage that the returned URL values have cached the properties you request. For example, if only want to count visible items in the directory, you’d need to fetch the .isHiddenKey property for each item, and doing that as part of the enumeration is much faster than doing it one item at a time.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

10 Likes

@eskimo @Diggory thank you so much for information! I achieved what I want!

I wish this information was part of the docs, instead of part of the lore.

Neither the docs on the web, nor the header comments mention this (which by the way are two different sets of text).

2 Likes

The "deprecation" of paths in favor of URLs? Yes, that's a topic that imho would benefit from some cleanup - ideally, there would also be a separate type for local URLs (Swift itself doesn't offer much to work with files, does it? It's all inherited from Foundation).

in my case it gives me a number of elements in my path to the folder in place of number of elements at the endpoint.

var numberOfComponents = 0
            do{
                numberOfComponents = try FileManager.default.contentsOfDirectory(atPath: url.path).count
            }catch let error{
                print(error.localizedDescription)
            }

where url.path in the path to the wanted directory.

i remember old mac API was returning the number of files/folders in a given folder directly without any enumeration, that's no longer possible?

(what would be really cool to have readily available without enumeration is folder sizes!)

i remember old mac API was returning the number of files/folders in a
given folder directly without any enumeration

In HFS [Plus] terms that’s known as the valence (hence old APIs like kFSCatInfoValence).

that's no longer possible?

On Apple platforms you can get it via the ATTR_DIR_ENTRYCOUNT attribute; see the getattrlist man page for the details.

I want to highlight this comment from that man page:

Furthermore, you should only request the attributes that you need.
Some attributes are expensive to calculate on some volume formats.
For example, ATTR_DIR_ENTRYCOUNT is usually expensive to calculate
on non-HFS [Plus] volumes. If you don't need a particular attribute,
you should not ask for it.

[Hmmm, the writing style in this quote seems very familiar somehow (-: ]

I presume that APFS also has fast support for this but I’ve never actually confirmed that.

In most cases this feature isn’t useful because you rarely want the total number of items, but rather the total number of items matching a predicate, like the number of files with a specific extension, or the number of directories.

what would be really cool to have readily available without
enumeration is folder sizes!

Indeed. This is, alas, a staggeringly difficult thing to support. Even if you ignore the technical challenges of implementing this within the file system, there are conceptual challenges. Allocation blocks, hard links, sparse files, storing file data in the catalogue B-tree, on-the-fly compression, copy on write, and so on, all mean that there’s no such thing as file size. Rather, it’s what would this file’s size be if I did operation X, where X would be copy it to the same volume, copy it to a different volume, serialise it in format X, and so on.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

8 Likes

I also wish there was clearer documentation about why Foundation/Apple went this route. I for one profoundly disagree with it.

URLs use an abstract/idealised model of a hierarchical path; they don't know the details of how your filesystem actually works. For example, on Windows, there are several kinds of path roots (drive letters, UNC shares, etc) which you should not be able to pop using ".." components, and each platform has its own rules for path normalisation, case-sensitivity, etc, which URL normalisation should not inherit (else URL-level operations suddenly become platform- and even filesystem-specific, the very opposite of "universal").

If you want to deal with file paths, my advice is to rely on a domain expert such as swift-system's FilePath. I hope Foundation reverses course and introduces a new set of FilePath-based APIs.

file URLs have never really been well-defined, with no standard way to convert between a file URL and its local path representation. The closest we ever got was RFC-8089, consisting of some non-normative "observed behaviors" and "interactions that might be encountered".

1 Like

IMPORTANT I want to stress that I wasn’t involved in making these decisions, I have just watched them go by over the years.

I also wish there was clearer documentation about why Foundation/Apple
went this route.

I don’t think we ever documented this officially, but to understand this choice you have to look at the history of macOS. Traditional Mac OS did not use paths a lot. Rather, files were identified by an FSSpec [1] [2], which contains a volume identifier, a directory ID, and a name. The directory ID was an HFS [Plus] catalogue node ID (CNID), which is kinda like an inode number [3].

Additionally, starting with System 7 it was possible to track a file with a volume identifier and the file ID, that is, the CNID of the file itself.

This was quite tricky to support on a Unix-y platform like Mac OS X. At the lowest levels of the system you needed the ability to manipulate files based on CNIDs rather than paths. For an explanation of how this was done, see QA1113 The "/.vol" directory and "volfs" (note, however, that volfs is no longer a thing and the same functionality is now implemented in a very different way).

Moreover, this ability to work with files based on their CNID was important for Foundation as well. We solved this problem with file reference URLs. These used [4] a different URL scheme, one that encoded the traditional FSSpec values in the URL path. This is why we have routines like -[NSURL isFileURL], which returned true for URLs with both the standard file URL scheme and the custom one used by file reference URLs [5], and -fileReferenceURL, which creates a file reference URL from a standard URL.

So far, so much obscure backward compatibility. However, since we made the decision to use file URLs we’ve exploited that to significant advantage:

  • File URLs can cache property values. This is really useful for things like -[NSFileManager contentsOfDirectoryAtURL:includingPropertiesForKeys:options:error:], which returns an easy-to-use array of URLs that’s also performant because those URLs cache the attributes that you request via the includingPropertiesForKeys parameter. It’s a bit shocking that, in most practical scenarios, this NSURL API is faster than the lower-level readdir API. To beat it you have to call getdirentriesattr directly (see its man page).

  • File URLs can hold extra metadata. A critical use case here is the security scope used by the App Sandbox on macOS and the general sandbox on iOS and its descendents.

Moreover, my experience is that file paths have all sorts of gotchas that folks tend to ignore because they’re either obscure (like normalisation) or well-known (like getting the separators right). If you give someone a file path they’ll inevitably start manipulating it as a string, and at some point that’ll end badly.

Honestly, I’m not a big fan of file names — in the sense that I consider the name of a file to be just one item of metadata that’s not fundamentally different from, say, its creation date — but that’s an argument for another day (-:

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] Technically an FSSpec is a System 7 thing, but systems prior to that used the same info just not packed into a handy structure.

[2] Late in traditional Mac OS’s life (Mac OS 9?) we introduced a new File Manager API based on FSRef values. You can think of these as an opaque form of FSSpec, so I’m going to focus on FSSpec here. However, the way that these FSRef values worked made it even more important that Mac OS X was able to represent them with high fidelity.

[3] There are important semantic differences here but if you come from a Unix background this is the closest approximation.

[4] Again, this has changed since it was originally introduced.

[5] To be clear, this custom scheme is no longer in use and file reference URLs now use the standard file scheme with a path that encode the necessary info.

26 Likes

Fascinating, thanks Professor Quinn!

I noticed file-reference URLs, but I figured they were some kind of performance optimisation rather than a classic MacOS compatibility thing.

It's worth pointing out that neither of these are a consequence of URLs - no URL standard has ever accommodated cached property values or extra metadata which is not part of the URL string; these all come from Foundation's URL object, and they could be achieved in the very same way with any other object, including one based on file paths.

This is actually an important point, because it highlights some of the sharp edges of those APIs and why they exist. Since these extra values are part of a specific URL instance and not the URL string, they are fragile - turning the URL in to a string and back, perhaps because it is being encoded to JSON or some other intermediary format, loses all of this information. You also need to decide whether this makes the URLs not-equal (i.e. not interchangeable), and there's no perfect answer - if a URL with cached values is equal to one without, storing/retrieving the URL in a Set might lose the information, making it unreliable; if they are not equal, dictionary lookups won't work. Even a minor refactoring might send your URL down a path where the information gets lost or replaced with stale information from another instance.

Foundation has a very similar problem with the relativeString-baseURL split, which is another example of object-level information rather than URL-level information:

import Foundation

let urlA = URL(string: "http://example.com/a/b/c")!
let urlB = URL(string: "/a/b/c", relativeTo: URL(string: "http://example.com")!)!
let urlC = URL(string: "b/c", relativeTo: URL(string: "http://example.com/a/")!)!

// All of these URLs have the same .absoluteString.
urlA.absoluteString == urlB.absoluteString // true
urlB.absoluteString == urlC.absoluteString // true

// But they are not interchangeable.
urlA == urlB // false (!)
urlB == urlC // false (!)
URL(string: urlB.absoluteString) == urlB // false (!)

// Let's imagine an application using URLs as keys in a dictionary:
var operations: [URL: TaskHandle] = [:]
operations[urlA] = TaskHandle { ... }
operations[urlA] // TaskHandle
operations[urlB] // nil (!)

It also adds to the overheads and complexity of the URL object, making it difficult to reason about lifetimes and memory usage, and making deinit more complex. And unless file URLs have their own type, all URLs pay that cost. So while there are advantages from the APIs you mentioned, they do come with significant costs to both performance and semantics.

Personally, I see these as legacy design artefacts, from an object-oriented rather than value-oriented world. I think if it was being made today, with all the capabilities that Swift provides, those cached resource values would likely just be part of a simple wrapper struct - giving you a clearer indication that they exist and how to preserve them.

Security-scoped URLs are interesting. I need to investigate them a bit more, but fundamentally I can't see any reason why they couldn't be part of a special path format or FS location, like the /.vol folder you mentioned. That would allow them to be used in file paths as well as URLs.

6 Likes