Created
May 7, 2023 00:55
-
-
Save boraseoksoon/3da6f5f3773d659299d92b3bb0954e13 to your computer and use it in GitHub Desktop.
Spotlight App Search minimal UI + functionality
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// SpotlightAppSearch | |
// | |
// Created by seoksoon jang on 2023-05-06. | |
// | |
import SwiftUI | |
import CoreServices | |
struct ContentView: View { | |
@State private var searchText: String = "" | |
@State private var installedApps: [AppItem] = [] | |
private var filteredApps: [AppItem] { | |
installedApps.filter { app in | |
searchText.isEmpty || app.name.localizedCaseInsensitiveContains(searchText) | |
} | |
} | |
var body: some View { | |
VStack { | |
TextField("Search apps", text: $searchText) | |
.padding(.horizontal) | |
AppListView(apps: filteredApps) | |
} | |
.task { | |
installedApps = await fetchInstalledApps() | |
} | |
} | |
func fetchInstalledApps() async -> [AppItem] { | |
let query = MDQueryCreate(kCFAllocatorDefault, "kMDItemContentTypeTree == 'com.apple.application-*'" as CFString, nil, nil) | |
MDQueryExecute(query, CFOptionFlags(kMDQuerySynchronous.rawValue)) | |
let count = MDQueryGetResultCount(query) | |
lazy var apps: [AppItem] = [] | |
try? await withThrowingTaskGroup(of: AppItem.self) { group in | |
for index in 0..<count { | |
group.addTask { | |
guard let rawPtr = MDQueryGetResultAtIndex(query, index), | |
let item = Unmanaged<MDItem>.fromOpaque(rawPtr).takeUnretainedValue() as MDItem?, | |
let path = MDItemCopyAttribute(item, kMDItemPath) as? String, path.hasSuffix(".app"), | |
let displayName = MDItemCopyAttribute(item, kMDItemDisplayName) as? String | |
else { | |
throw NSError(domain: "AppItemError", code: 1, userInfo: nil) | |
} | |
let url = URL(fileURLWithPath: path) | |
return AppItem(name: displayName, url: url) | |
} | |
} | |
for try await app in group { | |
apps.append(app) | |
} | |
} | |
return apps | |
} | |
} | |
struct AppRowView: View { | |
var app: AppItem | |
var body: some View { | |
HStack { | |
Image(nsImage: getAppIcon(for: app.url)) | |
.resizable() | |
.scaledToFit() | |
Text(app.name) | |
Button(action: { | |
launchApp(at: app.url) | |
}) { | |
Text("click") | |
} | |
} | |
} | |
func getAppIcon(for url: URL) -> NSImage { | |
return NSWorkspace.shared.icon(forFile: url.path) | |
} | |
func launchApp(at url: URL) { | |
NSWorkspace.shared.open(url) | |
} | |
} | |
struct AppListView: View { | |
var apps: [AppItem] | |
var body: some View { | |
List(apps) { app in | |
AppRowView(app: app) | |
} | |
} | |
} | |
struct AppItem: Identifiable, Hashable { | |
var id = UUID() | |
var name: String | |
var url: URL | |
} | |
extension AppItem { | |
init?(url: URL) { | |
guard let appName = url.deletingPathExtension().lastPathComponent as String? else { | |
return nil | |
} | |
self.name = appName | |
self.url = url | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment