Update: the two smaller API gaps originally filed here have been split out as requested —
#496 (additional parameters on token requests) and #497 (Migration.setTokens not seeding the
snapshot nonce, filed as a bug). This issue now covers only the Swift package.
Summary
The docs say a Swift package for native iOS apps is "still pending". I'd like to help make that
happen, and I have a working proposal in #495 — but I'd rather agree the shape with you than have you
spend review time on something you'd want built differently.
Context: we're currently migrating a production Android app from AppAuth to Lokksmith — the work is
in progress and not released yet — and we'd like to do the same for our native Swift iOS app, which
is what led me here. So this comes from wanting to use Lokksmith in production rather than from
wanting to write a library.
The API isn't reachable from Swift
Even with a framework in hand, lokksmith-core's public surface doesn't survive Objective-C export:
create / getOrCreate take a CreateContext builder configured through write-only extension
properties (CreateContext.id, .discoveryUrl, .metadata). Extension properties aren't
exported as settable members, so a client cannot be created from Swift at all.
Lokksmith.Options needs CoroutineScope, HttpClientEngine and SuspendFunction0<Instant>,
and Objective-C export drops default arguments — so Swift would have to supply all five.
Client.key / Client.id are value classes.
Client.tokens is a StateFlow; runWithTokens takes a suspend function type, which cannot be
exported as a parameter at all.
Scope is a sealed class; errors are Kotlin exception types, which surface as opaque NSErrors
without @Throws.
So a hand-written facade is needed regardless — which I think is fine, and arguably better than
exporting core wholesale: the facade can be small and shaped for Swift rather than mirroring Kotlin.
There's no artifact to link
No Package.swift, no XCFramework in the release.
Why Objective-C export rather than Kotlin's native Swift export
Worth being explicit about this, since Swift export is the more modern answer and it would be
reasonable to ask why the proposal doesn't use it.
Kotlin's native Swift export is genuinely nicer where it applies: suspend maps to Swift async,
Flow to AsyncSequence, and there's no Objective-C layer flattening namespaces or renaming enum
entries. We already use it for one of our own internal KMP packages and it's working well so far, so
this isn't scepticism about the feature.
Two reasons we didn't propose it here:
- It's still Alpha, with breaking changes expected. Choosing a not-yet-stable export mechanism
is a very different decision for a published library than for our own internal package, where we
control both sides and can absorb churn on our own schedule. That call belongs to you, not to a
contributor — so proposing it felt out of place.
- It doesn't produce a distributable artifact yet. As far as I can see, it emits Swift sources
plus a static library into the Xcode build directory, for a workflow where Xcode drives the Kotlin
build. That's fine when the consumer builds the Kotlin themselves — it's how our internal package
works — but a published Swift package has to ship a prebuilt binary that SPM can fetch from a
release, and I don't think Swift export can do that today. Happy to be corrected.
So Objective-C export plus the XCFramework() DSL and an SPM .binaryTarget looks like the only
route that gets a native iOS app a real dependency today. If Swift export becomes the right answer
later, the facade module is the natural place to switch: its whole job is to be the boundary, so the
public Swift shape could stay similar while the mechanism underneath changes.
What's in #495
A lokksmith-swift module, verified end to end on Kotlin 2.4.10 / Xcode 26.6:
import Lokksmith
let lokksmith = LokksmithManager()
let client = try await lokksmith.getOrCreateClient(
key: "main",
configuration: .companion.discovery(
clientId: "my-client-id",
discoveryUrl: "https://example.com/.well-known/openid-configuration"
)
)
let request = LokksmithAuthorizationRequest(redirectUri: "my-app://openid-response")
request.scopes = ["profile", "email"]
// Presents ASWebAuthenticationSession, exchanges the code, validates and persists.
// nil when the user dismissed the browser.
if let tokens = try await client.authorize(request: request) { … }
// Synchronous, no network — for "is the user signed in?"
if client.isAuthenticated { … }
// Refreshes only when needed.
let accessToken = try await client.freshTokens().accessToken.token
Notable points:
lokksmith-core stays an implementation dependency and is deliberately not exported, so the
generated header contains only Lokksmith* declarations. Verified against the built header.
LokksmithFailure.kind separates a provider rejection (oAuthRejection) from a transient
transport failure — the distinction a client app has to act on, and one that is currently
awkward to recover from a bare NSError.
- A
swiftApiSmokeTest Gradle task type-checks a real Swift sample against the assembled
XCFramework and is wired into check. Since CI already runs on macos-latest, this guards the
exported surface and the documented snippets. I confirmed it fails on a deliberate break — worth
having, because interop regressions (a default argument added, an enum renamed) are invisible from
Kotlin.
- Release flow: a
packageSwiftArtifact task zips the XCFramework and prints the SHA-256, which I
verified matches swift package compute-checksum. Wiring that into your release automation is
your call — I left Package.swift with a placeholder checksum rather than guess at how you'd want
release-please to update it.
Open questions, all yours to decide
- Naming. I used
LokksmithManager, LokksmithClient, LokksmithTokens. The Lokksmith
prefix is needed because Objective-C export has no namespaces, but LokksmithManager for the
thing that is Lokksmith in Kotlin is a compromise. Open to anything.
- Scope of the facade. I covered create/get/delete client, authorization, refresh,
freshTokens,
reset, end-session and migration. I left out Snapshot, Jwt/Claims and multiple-client
niceties.
- Targets.
iosArm64 + iosSimulatorArm64, matching core. No iosX64, so Intel Macs can't run
it in the simulator. Adding it would mean adding the target to core too.
- iOS 15 minimum, which is just Kotlin/Native's default here.
- Whether you want this as a module in this repo at all, versus a separate one.
AI disclosure: this issue text, the description of #495 and the module code in it were written
with the help of Claude (Claude Code, Opus 5), with me directing and reviewing. The commit on #495
carries a Co-Authored-By: Claude Opus 5 trailer. The verification claims above were actually
executed rather than asserted — see #495 for what was and wasn't checked.
Summary
The docs say a Swift package for native iOS apps is "still pending". I'd like to help make that
happen, and I have a working proposal in #495 — but I'd rather agree the shape with you than have you
spend review time on something you'd want built differently.
Context: we're currently migrating a production Android app from AppAuth to Lokksmith — the work is
in progress and not released yet — and we'd like to do the same for our native Swift iOS app, which
is what led me here. So this comes from wanting to use Lokksmith in production rather than from
wanting to write a library.
The API isn't reachable from Swift
Even with a framework in hand,
lokksmith-core's public surface doesn't survive Objective-C export:create/getOrCreatetake aCreateContextbuilder configured through write-only extensionproperties (
CreateContext.id,.discoveryUrl,.metadata). Extension properties aren'texported as settable members, so a client cannot be created from Swift at all.
Lokksmith.OptionsneedsCoroutineScope,HttpClientEngineandSuspendFunction0<Instant>,and Objective-C export drops default arguments — so Swift would have to supply all five.
Client.key/Client.idarevalue classes.Client.tokensis aStateFlow;runWithTokenstakes a suspend function type, which cannot beexported as a parameter at all.
Scopeis a sealed class; errors are Kotlin exception types, which surface as opaqueNSErrorswithout
@Throws.So a hand-written facade is needed regardless — which I think is fine, and arguably better than
exporting core wholesale: the facade can be small and shaped for Swift rather than mirroring Kotlin.
There's no artifact to link
No
Package.swift, no XCFramework in the release.Why Objective-C export rather than Kotlin's native Swift export
Worth being explicit about this, since Swift export is the more modern answer and it would be
reasonable to ask why the proposal doesn't use it.
Kotlin's native Swift export is genuinely nicer where it applies:
suspendmaps to Swiftasync,FlowtoAsyncSequence, and there's no Objective-C layer flattening namespaces or renaming enumentries. We already use it for one of our own internal KMP packages and it's working well so far, so
this isn't scepticism about the feature.
Two reasons we didn't propose it here:
is a very different decision for a published library than for our own internal package, where we
control both sides and can absorb churn on our own schedule. That call belongs to you, not to a
contributor — so proposing it felt out of place.
plus a static library into the Xcode build directory, for a workflow where Xcode drives the Kotlin
build. That's fine when the consumer builds the Kotlin themselves — it's how our internal package
works — but a published Swift package has to ship a prebuilt binary that SPM can fetch from a
release, and I don't think Swift export can do that today. Happy to be corrected.
So Objective-C export plus the
XCFramework()DSL and an SPM.binaryTargetlooks like the onlyroute that gets a native iOS app a real dependency today. If Swift export becomes the right answer
later, the facade module is the natural place to switch: its whole job is to be the boundary, so the
public Swift shape could stay similar while the mechanism underneath changes.
What's in #495
A
lokksmith-swiftmodule, verified end to end on Kotlin 2.4.10 / Xcode 26.6:Notable points:
lokksmith-corestays animplementationdependency and is deliberately not exported, so thegenerated header contains only
Lokksmith*declarations. Verified against the built header.LokksmithFailure.kindseparates a provider rejection (oAuthRejection) from a transienttransportfailure — the distinction a client app has to act on, and one that is currentlyawkward to recover from a bare
NSError.swiftApiSmokeTestGradle task type-checks a real Swift sample against the assembledXCFramework and is wired into
check. Since CI already runs onmacos-latest, this guards theexported surface and the documented snippets. I confirmed it fails on a deliberate break — worth
having, because interop regressions (a default argument added, an enum renamed) are invisible from
Kotlin.
packageSwiftArtifacttask zips the XCFramework and prints the SHA-256, which Iverified matches
swift package compute-checksum. Wiring that into your release automation isyour call — I left
Package.swiftwith a placeholder checksum rather than guess at how you'd wantrelease-pleaseto update it.Open questions, all yours to decide
LokksmithManager,LokksmithClient,LokksmithTokens. TheLokksmithprefix is needed because Objective-C export has no namespaces, but
LokksmithManagerfor thething that is
Lokksmithin Kotlin is a compromise. Open to anything.freshTokens,reset, end-session and migration. I left out
Snapshot,Jwt/Claimsand multiple-clientniceties.
iosArm64+iosSimulatorArm64, matching core. NoiosX64, so Intel Macs can't runit in the simulator. Adding it would mean adding the target to core too.
AI disclosure: this issue text, the description of #495 and the module code in it were written
with the help of Claude (Claude Code, Opus 5), with me directing and reviewing. The commit on #495
carries a
Co-Authored-By: Claude Opus 5trailer. The verification claims above were actuallyexecuted rather than asserted — see #495 for what was and wasn't checked.