Autostart in macOS
Registering your app as a LoginItem
If you search on how to register your app programmatically as a LoginItem you find a plethora of outdated information. So I thought it might be helpful to share this.
Basically you can register your app through SMAppService.mainApp.register()
and unregister it with SMAppService.mainApp.unregister()
. Checking what is currently configured is possible through SMAppService.mainApp.status
. The thing that bugged me is: I found no way to get a notification of some kind if someone deletes your app from the LoginItems via the Settings app. I tried KVO without success. Thanks Dave for confirming.
Say you want to show a toggle for the autostart feature in your app that might easily get out of sync with what is really configured in your system (if the LoginItem gets deleted via the Settings app). So we need to work around that.
The best idea so far is to re-check the current state when the mouse re-enters our window. Thanks Kilian for the idea!
Autostart Model
import Combine
import ServiceManagement
extension String: LocalizedError {
public var errorDescription: String? {
return self
}
}
final class Autostart: ObservableObject {
enum State: Equatable {
case unknown
case pending
case autostart(Bool)
static func == (lhs: State, rhs: State) -> Bool {
switch (lhs, rhs) {
case (.unknown, .unknown), (.pending, .pending):
return true
case let (.autostart(lhsValue), .autostart(rhsValue)):
return lhsValue == rhsValue
default:
return false
}
}
}
@Published var hasAutostart: State = .unknown
private var cancellables = Set<AnyCancellable>()
init(useTimer: Bool = false) {
check()
if useTimer {
Timer.publish(every: 1.0, on: .current, in: .common).autoconnect().sink { [weak self] _ in
self?.check()
}.store(in: &cancellables)
}
}
func check() {
hasAutostart = .autostart(SMAppService.mainApp.status == .enabled)
}
func request(autostart: Bool) throws {
hasAutostart = .pending
if autostart {
#if DEBUG
if Bool.random() {
throw "Your random error that no one saw comming..."
}
#endif
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
hasAutostart = .autostart(autostart)
}
}
A simple view
import SwiftUI
struct ContentView: View {
@StateObject var autostart = Autostart(useTimer: true)
@State private var errorMsg: String?
var body: some View {
let projection = Binding<Bool> {
if case let .autostart(value) = autostart.hasAutostart {
return value
}
return false
} set: { wantsAutostart in
do {
try autostart.request(autostart: wantsAutostart)
errorMsg = nil
} catch {
errorMsg = error.localizedDescription
}
}
return ZStack {
Color.clear.onHover { _ in
autostart.check()
}
VStack {
if let errorMsg {
Text(verbatim: errorMsg).foregroundStyle(.red)
}
Toggle(isOn: projection) {
Text(verbatim: "wants Autostart")
}
.toggleStyle(SwitchToggleStyle())
}
}
}
}