Skip to content

Integrating the Tapkey Mobile SDK for iOS

Latest Version

The latest version of the Tapkey Mobile SDK for iOS is


This documentation refers to version and later. This documentation may not be appropriate for older versions. Take a look at the upgrade section for upgrading to the latest version of the Tapkey Mobile SDK.

Sample App

A Tapkey Mobile SDK sample for iOS app is available on GitHub.


The Tapkey Mobile SDK is written in Swift and is distributed as a compiled static Swift 5.5 library. The target App has to be built with Xcode 13 or later.

Adding the Tapkey Mobile SDK via CocoaPods

The Tapkey Mobile SDK is published as a Pod in a private Podspec repository. To tell CocoaPods where to find the artefacts add the Tapkey Podspec repository, add the official Tapkey Podspec repository as a source to the beginning of the Podfile and add TapkeyMobileLib as a dependency to your target app. Also make sure, use_frameworks! is set.

source ''
source ''


target 'App' do
    pod 'TapkeyMobileLib', ''

Bootstrap the Tapkey Mobile SDK

  1. Import the TapkeyMobileLib module.
  2. Use the TKMServiceFactoryBuilder to create an instance of the TKMServiceFactory in the willFinishLaunchingWithOptions callback in the app delegate.
import TapkeyMobileLib
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

    private var TKMServiceFactory: TKMServiceFactory!

    func application(_ application: UIApplication,
                     willFinishLaunchingWithOptions launchOptions:
                     [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

        // Build service factory
        self.TKMServiceFactory = TKMServiceFactoryBuilder()

Polling for Data

It is recommended to use Background App Refresh to poll the Tapkey Trust Service for notifications. This is configured in the app delegate:

// Background App Refresh
func application(
        _ application: UIApplication,
        performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

    // Configure the Tapkey SDK to poll for notifications.
    // Run the code via runAsyncInBackground to prevent the app from sleeping while fetching is in progress.
    runAsyncInBackground(application, promise:
                .pollForNotificationsAsync(cancellationToken: TKMCancellationTokens.None)
                .finallyOnUi {
While the Background App Refresh is periodically looking for updated keys, it is recommended to poll for updates whenever updates are expected. NotificationManager#pollForNotificationsAsync() can be used to manually check for updates. Manually checking for updated keys is recommended in the following situations:

  • After a user has been logged in to the SDK.
  • After a new grant has been issued for a logged-in user.
  • After a grant that affects the current user has been modified or revoked.
  • After grants, affecting locks that one of the logged-in users has access to, have been revoked. The logged-in users might receive updated revocation information.

Logging In Users

The SDK takes a Tapkey access token to log in a user. Retrieving such an access token is part of the implementing application. Usually, either the Token Exchange or Authentication Code with PKCE grant type will be used. For more information about authenticating against Tapkey, take a look at the authentication section.

Once an access token has been obtained, it can be used to log in a user as outlined below:

TKMServiceFactory.userManager.logInAsync(accessToken: "eyJhbGciOiJSUz...O-YbBq8F7086rQi-kEbERp4dA3r0WonpHnmYcXEnA", cancellationToken: ct)
    .continueOnUi(userId -> {
        // User logged in, use user's ID here.

Subsequently, userManager.users will yield a list of IDs of all logged-in users. In most applications, only one user will be logged in at a time.

val userId = userManager.users[0]
// Fetch information about the user using the ID

Note that a token refresh handler must be registered with the TKMServiceFactory prior to logging in any users. This can be done using the TKMServiceFactoryBuilder's setTokenRefreshHandler(tokenRefreshHandler: TKMTokenRefreshHandler) method.

// In the `willFinishLaunchingWithOptions` callback in the app delegate:

// Build service factory
self.tapkeyServiceFactory = TKMServiceFactoryBuilder()
// Sample token refresh handler implementation

import RxSwift
import TapkeyMobileLib

class SampleTokenRefreshHandler: TKMTokenRefreshHandler {

    func refreshAuthenticationAsync(userId: String, cancellationToken: TKMCancellationToken) -> TKMPromise<String> {
        let promiseSource = TKMPromiseSource<String>()

        let accessToken = getNewTapkeyAccessTokenFromCustomApplicationLogic()

        // OK:

        // Or, in case no access token could be obtained:
        promiseSource.setError(TKMError(errorDescriptor: TKMErrorDescriptor(
                code: TKMAuthenticationHandlerErrorCodes.TokenRefreshFailed,
                message: "No new token can be obtained.",
                details: nil)))

        return promiseSource.promise

    func onRefreshFailed(userId: String) {
         * Will be called if re-authentication failed and a manual call to
         * userManager.updateAccessToken() is required.

Searching for Nearby Locks

Searching for nearby locks can be done via the TKMBleLockScanner. An instance of TKMBleLockScanner can be obtained from the TKMServiceFactory.

TKMBleLockScanner bleLockScanner = tapkeyServiceFactory.bleLockScanner

// Returns an observer registration to stop scanning once finished.
bleScanObserverRegistration = bleLockScanner.startForegroundScan()

To observe locks entering and leaving the range of this device, the TKMBleLockScanner's observable can be used.

// Listen for Tapkey locks coming into or leaving range.
nearbyLocksObserverRegistration = bleLockScanner.observable
    .addObserver(locks -> {
        // Handle nearby locks here, e.g. present them to the user.

// Alternatively, the locks property can be used to get a list of nearby locks in this moment.
nearbyLocks = bleLockScanner.locks

Additionally to scanning for all locks nearby, it is also possible to check if a specific lock is nearby. Please note, that scanning must still be ongoing (via startForegroundScan()) otherwise isLockNearby() will always return false.

// Check if a specific lock is nearby.
bleLockScanner.isLockNearby(physicalLockId: physicalLockId)

This method can be especially used in conjunction with a list of keys. You may check, for each key the user holds, the corresponding nearby locks and show them in your user interface.

Make sure to stop scanning if the app does not require information about nearby locks anymore:

// Stop scanning for nearby locks.
if self.nearbyLocksObserverRegistration != nil {
    self.nearbyLocksObserverRegistration = nil

Triggering a Lock

After a nearby lock was found with the TKMBleLockScanner, it can be opened using the trigger lock command, which is available from the TKMCommandExecutionFacade. Any command inside the command execution facade, requires a TLCP connection to a lock. The following example uses TKMBleLockCommunicator to establish a TCLP connection via BLE.

let ct = TKMCancellationTokens.fromTimeout(timeoutMs: 15000)

// Use the BLE lock communicator to send a command to the lock
return bleLockCommunicator.executeCommandAsync(
                bluetoothAddress: bluetoothAddress,
                physicalLockId: physicalLockId,
                commandFunc: { tlcpConnection -> TKMPromise<TKMCommandResult> in

                    let triggerLockCommand = TKMDefaultTriggerLockCommandBuilder()

                    // Pass the TLCP connection to the command execution facade
                    return self.commandExecutionFacade!.executeStandardCommandAsync(
                        command: triggerLockCommand,
                        cancellationToken: ct)
                cancellationToken: ct)

    // Process the command's result
    .continueOnUi({ commandResult in
        let code: TKMCommandResult.TKMCommandResultCode = commandResult?.code ??

        switch code {
        case TKMCommandResult.TKMCommandResultCode.ok:
            return true
            return false
    .catchOnUi({ (e: TKMAsyncError) -> Bool in

        let syncError = e.syncSrcError

        // A TKMRuntimeError.bleInconsistencyError indicates that the bluetooth chip of the lock
        // did not response as expected and should not happen. But the iPhone is known to cache some
        // bluetooth connection specific cofigurations which might cause issues when the bluetooth
        // configuration of the lock was changed during an firmware upgrade.
        // In such very rare and unexpected cases a restart of the iphone should resolve the issue.
        if case TKMRuntimeError.bleInconsistencyError(_) = syncError {
            NSLog("Trigger lock failed because of a bleInconsistencyError")
            // Inform the user that an unexpected happend and a restart of the iphone might help.
            return false

        NSLog("Trigger lock failed")
        return false


By default the Tapkey Mobile Lib writes its logs to the system logging system. To be able to collect logs methodical for debugging and investigations, a custom logger can be used instead.


A custom logger replaces the Tapkey default logger, therefore logs won't be written to LogCat anymore in such cases.

Create a custom implementation of the TKMLogger.

class CustomLogger: TKMLogger {
    func println(priority: Int32, tag: String, msg: String, error: Error?) {
        // Custom logging implementation

The priority is divided into following static priority levels:


Add an instance of the custom implementation to TKMLogs. This should be done preferably before creating the TKMServiceFactoryBuilder to avoid lost logs during the bootrap.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {


        self.tapkeyServiceFactory = TKMServiceFactoryBuilder()