swiftmacosswiftuifile-management

Is there a faster way to loop through the installed applications on macOS?


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 Screenshot of bad app performance (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?


Solution

  • 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"