I'm writing a menu bar extra that shows you a list of your installed apps and allows you to click on each button in the list to open that app. Obviously, to do this I need a list of every app the user has. The specific way I chose to do this was making a function that would loop through the files in the system's Applications folder, strip out anything in an app's contents or that didn't end in .app, and return an array containing a list of files as names, which is then iterated through to create a list of "app buttons" that the user can click on to launch the app.
The code for my function is
func enumerateAppsFolder() -> Array<String> {
var fileNames:Array<String> = []
let fileManager = FileManager.default
let enumerator:FileManager.DirectoryEnumerator = fileManager.enumerator(atPath:"/Applications/")!
while let element = enumerator.nextObject() as? String {
if element.hasSuffix("app") && !element.contains("Contents") { // checks the extension
fileNames.append(element)
}
}
return fileNames
}
And I create my list with
ForEach(enumerateAppsFolder(), id:\.self){
AppBarMenuItem(itemAppName: $0)
}
But when I do it like that, the result is what I expected, but the performance is horrible. This can be seen in the screenshot, and will just be made worse by larger applications folders on some people's systems (When the app is starting up, which takes about 5 minutes, the CPU and disk usage are also extremely high) Is there a better and faster method that will retrieve every app on the system, similarly to the macOS launchpad or "Open With.." list?
The enumerator
method of FileManager
that you are using performs a deep enumeration of the file tree. You don't want a deep enumeration, just a top-level enumeration. Use the version of the enumerator
method that has the options
parameter and pass in .skipsSubdirectoryDescendants
.
Here's an updated version of your function getting a URL
directly from FileManager
for the Applications folder and then doing a shallow enumeration to get the list of apps.
func enumerateAppsFolder() -> [String] {
var appNames = [String]()
let fileManager = FileManager.default
if let appsURL = fileManager.urls(for: .applicationDirectory, in: .localDomainMask).first {
if let enumerator = fileManager.enumerator(at: appsURL, includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants) {
while let element = enumerator.nextObject() as? URL {
if element.pathExtension == "app" { // checks the extension
appNames.append(element.deletingPathExtension().lastPathComponent)
}
}
}
}
return appNames
}
print(enumerateAppsFolder())
Sample output when run from a Swift Playground:
"Numbers", "Dropbox", "Xcode", "Apple Configurator 2", "iMovie"