I wanted to be able to share a token between my macOS, iOS app and watchOS app so that I could login on the phone and be automatically logged in on the watch too. I wanted to save the token to the iCloud KeyChain on the phone or mac and have it automatically sync to all the other devices.

Set up Keychain Sharing

To do this I had to enable Keychain Sharing in both the iOS, macOS and watchOS app in Xcode, and make them both use the same Keychain Group. My app is called TapStarter and my company is Atadore. You’ll need to use something different.

The iOS settings: iOS settings Screenshot

The watchOS settings: watchOS settings Screenshot

Note that the watchOS KeyChain group is different from the one that is created by default.

I did the same thing for the macOS app too.

Set the password to be synchronizable, and use the keychain group

This is the code I use to set the token

import Foundation
import os

class KeyStore {
    
    let account = "Token"
    let group = "XXXXXXXXXX.com.atadore.TapStarter"
    
    func store(token : String) {
        let data = token.data(using: .utf8)!
        let addquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword as String,
                                       kSecAttrAccount as String: account,
                                       kSecValueData as String: data,
                                       kSecAttrSynchronizable as String : kCFBooleanTrue!,
                                       kSecAttrAccessGroup as String : group
        ]
        SecItemDelete(addquery as CFDictionary)
        let status : OSStatus = SecItemAdd(addquery as CFDictionary, nil)
        guard status == errSecSuccess else {
            os_log("store: whoops")
            return
        }
    }
    
    func clear() {
        let addquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword as String,
                                       kSecAttrAccount as String: account,
                                       kSecAttrSynchronizable as String : kCFBooleanTrue!,
                                       kSecAttrAccessGroup as String : group
        ]
        SecItemDelete(addquery as CFDictionary)
    }
    
    func retrieve() -> String? {
        let getquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                       kSecAttrAccount as String: account,
                                       kSecReturnData as String: kCFBooleanTrue!,
                                       kSecMatchLimit as String : kSecMatchLimitOne,
                                       kSecAttrSynchronizable as String : kCFBooleanTrue!,
                                       kSecAttrAccessGroup as String : group
        ]
        
        var item: CFTypeRef?
        let status = SecItemCopyMatching(getquery as CFDictionary, &item)
        guard status == errSecSuccess else {
            os_log("keyStore.retrieve SecItemCopyMatching error \(status)")
            return nil
        }
        
        guard let data = item as? Data? else {
            os_log("keyStore.retrieve not data")
            return nil
        }
        
        return String(data: data!, encoding: String.Encoding.utf8)
    }
    
}

You should modify XXXXXXXXXX to match your team ID which you can see if you login to Apple’s developer portal, on the top right under your user name. You can also change Token to be something more meaningful.

The key is to enable the kSecAttrSynchronizable flag, and to set the ksecattraccessgroup to the group.

It takes a moment to sync, but I’m sharing my token between iOS, watchOS and macOS. I’d expect tvOS to work too. This is a lifesaver if you are not using Sign in with Apple, and you need a token in your Watch app to make network calls.