Skip to Content
macOS (Swift)SwiftUI and AppKit

SwiftUI and AppKit

LicenseManager works anywhere: it’s Sendable, its getters are synchronous, and its methods are async. For SwiftUI, LicenseObserver publishes it to your views.

LicenseObserver

A LicenseObserver is an ObservableObject on the main actor. Create it once, with your LicenseManager, and give it to your views:

MyApp.swift
@main struct MyApp: App { @StateObject private var license = LicenseObserver(manager: licenseManager) var body: some Scene { WindowGroup { Group { switch license.state { case .loading: ProgressView("Checking your license…") case .active: ContentView() case .missingKey: ActivationView() default: LockedView() } } .environmentObject(license) } } }
Property or methodWhat it is
stateThe license state, published.
offlineStateThe offline state, published when it changes between online and offline.
latestAllowedVersionSee .versionNotAllowed, published.
licenseKeyHintThe last 5 characters of the stored key, like B3JDF, or nil: to show which key is in use, without showing all of it.
portalURLThe customer portal of your product.
activate(key:)Activates a key: setLicenseKey(_:) of the manager.
refresh()Validates the stored key now.
openPortal()Opens the customer portal in the default browser.
managerThe LicenseManager.

It starts with the manager’s current values, and updates them on every change, on the main actor: there’s no “window opened after a change” race.

A locked screen

LockedView.swift
struct LockedView: View { @EnvironmentObject private var license: LicenseObserver var body: some View { VStack(spacing: 12) { Text(title).font(.title2.bold()) HStack { Button("Try again") { Task { await license.refresh() } } Button("Manage my license") { license.openPortal() } } } } var title: String { switch license.state { case .expired: "Your license has expired" case .machineLimitReached: "This license is used on too many Macs" case .versionNotAllowed: if case .version(let version) = license.latestAllowedVersion { "Your license includes up to version \(version)" } else { "This version isn't included in your license" } case .offlineExpired: "Connect to the internet to check your license" case .clockTampered: "Your Mac's date and time look wrong" default: "Your license key is no longer valid" } } }

See license states for what to show in each one.

Gating features

Hiding a menu item or a button isn’t enough on its own: check the license where the paid feature runs, with the manager’s state, which you can read from any thread:

func exportPDF() throws { guard licenseManager.state == .active else { throw AppError.licenseRequired } // … }

For a feature that must not run before the startup validation, await licenseManager.ready() first.

No license check stops someone determined to patch your binary. The SDK stops the common cases: a shared key over its machine limit, a forged token, a clock set back.

AppKit

In an AppKit app, create the manager in your app delegate, and react to changes with states, an AsyncStream that starts with the current state:

AppDelegate.swift
@main final class AppDelegate: NSObject, NSApplicationDelegate { private var licenseTask: Task<Void, Never>? func applicationDidFinishLaunching(_ notification: Notification) { licenseTask = Task { @MainActor in for await state in licenseManager.states { switch state { case .loading: break case .active: showMainWindow() case .missingKey: showActivationWindow() default: showLockedWindow(for: state) } } } } @IBAction func openLicensePortal(_ sender: Any?) { NSWorkspace.shared.open(licenseManager.portalURL) } }

onStateChange(_:) is the callback alternative: it returns a Subscription, cancelled when you call cancel() or release it. Its listener runs on the SDK’s actor: hop to the main actor before touching your UI.

Concurrency

  • LicenseManager is Sendable: share one instance between threads, tasks and actors.
  • state, licenseKey, offlineState, latestAllowedVersion, portalURL and isClockTampered are synchronous and thread-safe.
  • ready(), setLicenseKey(_:), refresh() and fingerprint() are async, and never block the main thread: network, Keychain and files run on the SDK’s own actor.
  • dispose() stops the background work: renewals, retries and the clock check. Releasing the last reference to the manager does it too. Most apps keep it for their whole lifetime and never call it.