Skip to content

Instantly share code, notes, and snippets.

@scriptingosx
Last active November 1, 2022 14:19
Embed
What would you like to do?
The files you will need to follow along the creation of the "Chooser" example app from my MacSysAdmin Online 2021 presentation.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadContent</key>
<dict>
<key>com.scriptingosx.ChooserUI</key>
<dict>
<key>Forced</key>
<array>
<dict>
<key>mcx_preference_settings</key>
<dict>
<key>urlScheme</key>
<string>mailto</string>
</dict>
</dict>
</array>
</dict>
</dict>
<key>PayloadEnabled</key>
<true/>
<key>PayloadIdentifier</key>
<string>MCXToProfile.1c6b1ed7-d170-4c11-9950-2da5d7cca247.alacarte.customsettings.8df89e15-83ba-4b94-bc91-c9422f41439c</string>
<key>PayloadType</key>
<string>com.apple.ManagedClient.preferences</string>
<key>PayloadUUID</key>
<string>8df89e15-83ba-4b94-bc91-c9422f41439c</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDescription</key>
<string>Included custom settings:
ChooserUI
Git revision: eacd7a583e</string>
<key>PayloadDisplayName</key>
<string>MCXToProfile: ChooserUI</string>
<key>PayloadIdentifier</key>
<string>com.scriptingosx.ChooserUI</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadRemovalDisallowed</key>
<true/>
<key>PayloadScope</key>
<string>System</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>1c6b1ed7-d170-4c11-9950-2da5d7cca247</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
//
// LSApp.swift
// LSApplication
//
// Created by Armin Briegel on 2021-08-30.
//
import AppKit
/**
An LSApp object represents an application on disk for use this with the LSKit functions.
*/
struct LSApp : Identifiable, Equatable {
/** local file URL of the app*/
let url: URL
/** the app's bundle object*/
let bundle: Bundle
/** app bundle identifier */
let identifier: String
/** app name (from bundle)*/
let name: String
/** path to the app */
var path: String {
return url.path
}
/** returns the identifier */
var id: String {
return identifier
}
/** app icon (256x256) */
var icon: NSImage {
let icon = NSWorkspace.shared.icon(forFile: url.path)
icon.size = NSSize(width: 256.0, height: 256.0)
return icon
}
/** returns whether the app is the default handler for the url scheme */
func isDefault(for scheme: String) -> Bool {
return LSKit.defaultURL(for: scheme) == self.url
}
/**
default initializer
- Parameter url: local file URL to the application
*/
init?(url: URL) {
self.url = url
guard let newBundle = Bundle(url: url) else { return nil }
self.bundle = newBundle
self.identifier = self.bundle.bundleIdentifier ?? ""
self.name = self.bundle.infoDictionary?["CFBundleName"] as? String ?? ""
}
/** LSApps are equal, when the identifiers are equal */
static func == (lhs: LSApp, rhs: LSApp) -> Bool {
return lhs.id == rhs.id
}
}
/**
adds functions to LSKit that return LSApp objects instead of URLs
*/
extension LSKit {
/**
gets a list of applications for the given url Scheme
- Parameter for: the url scheme (e.g. "http")
- Returns: Array of LSApp objects
*/
static func applications(for scheme: String) -> [LSApp] {
var applicationList = [LSApp]()
let urlList = urls(for: scheme)
for appURL in urlList {
if let newApp = LSApp(url: appURL) {
applicationList.append(newApp)
}
}
return applicationList
}
/**
gets the default application for the give URL scheme
- Parameter for: the url scheme (e.g. "http")
- Returns: the default application as an LSApp
*/
static func defaultApplication(for scheme: String) -> LSApp? {
if let defaultAppURL = defaultURL(for: scheme) {
return LSApp(url: defaultAppURL)
} else {
return nil
}
}
}
//
// LSKit.swift
// LSKit
//
// Created by Armin Briegel on 2021-08-30.
//
import Foundation
import AppKit
class LSKit {
/**
returns a list of URLs to applications that can open URLs starting with the scheme
- Parameter scheme: url scheme (excluding the `:` or `/`, e.g. `http`)
*/
static func urls(for scheme: String) -> [URL] {
guard let url = URL(string: "\(scheme):") else { return [URL]() }
if #available(macOS 12, *) {
//print("running on macOS 12, using NSWorkspace")
let ws = NSWorkspace.shared
return ws.urlsForApplications(toOpen: url)
} else {
var urlList = [URL]()
if let result = LSCopyApplicationURLsForURL(url as CFURL, .all) {
let cfURLList = result.takeRetainedValue() as Array
for item in cfURLList {
if let appURL = item as? URL {
urlList.append(appURL)
}
}
}
return urlList
}
}
/**
returns URL to the default application for URLs starting with scheme
- Parameter scheme: url scheme (excluding the `:` or `/`, e.g. `http`)
- Returns: urls to default application
*/
static func defaultURL(for scheme: String) -> URL? {
guard let url = URL(string: "\(scheme):") else { return nil }
if #available(macOS 12, *) {
//print("running on macOS 12, using NSWorkspace")
let ws = NSWorkspace.shared
return ws.urlForApplication(toOpen: url)
} else {
if let result = LSCopyDefaultApplicationURLForURL(url as CFURL, .all, nil) {
let appURL = result.takeRetainedValue() as URL
return appURL
}
return nil
}
}
/**
set the default app for scheme to the app with the identifier
- Parameters:
- identifier: bundle id of the new default application
- scheme: url scheme (excluding the `:` or `/`, e.g. `http`)
- Returns: OSStatus (discardable)
*/
@discardableResult static func setDefault(identifier: String, for scheme: String) -> OSStatus {
if #available(macOS 12, *) {
// print("running on macOS 12, using NSWorkspace")
let ws = NSWorkspace.shared
// since the new NSWorkspace function is asynchronous we have to use semaphores here
let semaphore = DispatchSemaphore(value: 0)
var errCode: OSStatus = 0
guard let appURL = ws.urlForApplication(withBundleIdentifier: identifier) else { return 1 }
ws.setDefaultApplication(at: appURL, toOpenURLsWithScheme: scheme) { err in
// err is an NSError wrapped in a CocoaError
if let err = err as? CocoaError {
if let underlyingError = err.errorUserInfo["NSUnderlyingError"] as? NSError {
errCode = OSStatus(clamping: underlyingError.code)
}
}
semaphore.signal()
}
semaphore.wait()
return errCode
} else {
return LSSetDefaultHandlerForURLScheme(scheme as CFString, identifier as CFString)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment