List installed Applications on El Capitan using Spotlight in Swift 2.2

1.4k Views Asked by At

I am currently building a mac app which in the future should be able to kill and start apps on OS X.

For that to be possible, I need to find a way to get a list of all the installed applications on the machine.

I already did quite a bit of research and have decided to use Spotlight with NSMetadataQuery to be able to get the list.

I was able to find this post about the mentioned topic and started to implement the functionality in Swift 2.2 (the weapon of choice for the project). With a bit of translation I was able to make it work and the code now successfully builds and runs. During runtime, however, I seem to be having a problem with the query itself:

<NSMetadataQuery: 0x6080000e3880> is being deallocated without first calling -stopQuery. To avoid race conditions, you should first invoke -stopQuery on the run loop on which -startQuery was called

This is the code I am currently using.

    public func doSpotlightQuery() {
    query = NSMetadataQuery()
    let predicate = NSPredicate(format: "kMDItemKind ==[c] %@", "Application")
    let defaultNotificationCenter = NSNotificationCenter()
    defaultNotificationCenter.addObserver(self, selector: #selector(queryDidFinish(_:)), name: NSMetadataQueryDidFinishGatheringNotification, object: nil)
    query.predicate = predicate
    query.startQuery()
}

public func queryDidFinish(notification: NSNotification) {
    for i in 0 ... query.resultCount {
        print(query.resultAtIndex(i).valueForAttribute(kMDItemDisplayName as String))
    }
}

Testing the

mdfind "kMDItemKind == 'Application'"

command (with variations of all kind) in the terminal of my mac didn't give me any results either which brings me to my question:

Did I set up the query in a wrong way or does this command not work in 'El Capitan'?

Can someone please help me find my mistake? I'd sure love to finally make this work!

2

There are 2 best solutions below

1
Maximillian Rose On

The dealloc message seems like the query is missing a strong reference.

var query: NSMetadataQuery? {
    willSet {
        if let query = self.query {
            query.stopQuery()
        }
    }
}

public func doSpotlightQuery() {
    query = NSMetadataQuery()
    let predicate = NSPredicate(format: "kMDItemKind ==[c] %@", "Application")
    let defaultNotificationCenter = NSNotificationCenter()
    defaultNotificationCenter.addObserver(self, selector: #selector(queryDidFinish(_:)), name: NSMetadataQueryDidFinishGatheringNotification, object: nil)
    query?.predicate = predicate
    query?.startQuery()
}

public func queryDidFinish(notification: NSNotification) {
    guard let query = notification.object as? NSMetadataQuery else {
        return
    }

    for i in 0 ... query.resultCount {
        print(query.resultAtIndex(i).valueForAttribute(kMDItemDisplayName as String))
    }
}

I'd suggest using a different predicate as kMDItemKind is a localized key according to John's comment here

so let predicate = NSPredicate(format: "kMDItemContentType == 'com.apple.application-bundle'") would work for what we're doing.

In swift 3 this could look like this:

var query: NSMetadataQuery? {
    willSet {
        if let query = self.query {
            query.stop()
        }
    }
}

public func doSpotlightQuery() {
    query = NSMetadataQuery()
    let predicate = NSPredicate(format: "kMDItemContentType == 'com.apple.application-bundle'")
    NotificationCenter.default.addObserver(self, selector: #selector(queryDidFinish(_:)), name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: nil)
    query?.predicate = predicate
    query?.start()
}

public func queryDidFinish(_ notification: NSNotification) {
    guard let query = notification.object as? NSMetadataQuery else {
        return
    }

    for result in query.results {
        guard let item = result as? NSMetadataItem else {
            print("Result was not an NSMetadataItem, \(result)")
            continue
        }
        print(item.value(forAttribute: kMDItemDisplayName as String))
    }
}
3
rohaldb On

Here is a solution that gets the contents of /applications and /applications/utilities and converts the contents to NSMetaDataItems.

public func getAllApplications() -> [NSMetadataItem] {
    let fileManager = FileManager()

    guard let applicationsFolderUrl = try? FileManager.default.url(for: .applicationDirectory, in: .localDomainMask, appropriateFor: nil, create: false) else { return [] }

    let applicationUrls = try! fileManager.contentsOfDirectory(at: applicationsFolderUrl , includingPropertiesForKeys: [], options: [FileManager.DirectoryEnumerationOptions.skipsPackageDescendants, FileManager.DirectoryEnumerationOptions.skipsSubdirectoryDescendants])

    guard let systemApplicationsFolderUrl = try? FileManager.default.url(for: .applicationDirectory, in: .systemDomainMask, appropriateFor: nil, create: false) else { return [] }

    let utilitiesFolderUrl = NSURL.init(string: "\(systemApplicationsFolderUrl.path)/Utilities") as! URL

    guard let utilitiesUrls = try? fileManager.contentsOfDirectory(at: utilitiesFolderUrl, includingPropertiesForKeys: [], options: [FileManager.DirectoryEnumerationOptions.skipsPackageDescendants, FileManager.DirectoryEnumerationOptions.skipsSubdirectoryDescendants]) else { return [] }

    let urls = applicationUrls + utilitiesUrls

    var applications = [NSMetadataItem]()

    for url in urls {
        print(url.path, fileManager.isExecutableFile(atPath: url.path))
        if fileManager.isExecutableFile(atPath: url.path) {
            guard let mdi = NSMetadataItem(url: url) else { continue }
            applications.append(mdi)
        }
    }

    for app in applications {
        print(app.value(forAttribute: kMDItemDisplayName as String))
    }
    return applications
}

(There is some clean up that can be done to it but I wrote it in a hurry)