# Swift Interop — Quick Reference

Bridging Swift with C, Objective-C, Rust (via cinterop or UniFFI), and Apple system frameworks (Keychain, Secure Enclave).

## Swift ↔ Objective-C

```swift
// Expose to ObjC
@objc public class WalletBridge: NSObject {
    @objc public func generateAddress() -> String {
        // ...
    }

    @objc(signTxWithPSBT:error:)
    public func signTx(psbt: String) throws -> String {
        // ...
    }
}

// Use ObjC in Swift — bridging header
// MyApp-Bridging-Header.h
#import <SomeLib/SomeLib.h>
```

`@objc` exposes type/method to ObjC runtime. `NSObject` subclass required for class-level interop. Swift-only types (generics, structs, enums with associated values) cannot be exposed.

## Swift ↔ C

Swift directly imports C headers via Clang importer.

```c
// wallet.h
#ifndef WALLET_H
#define WALLET_H
#include <stdint.h>

typedef struct {
    char* mnemonic;
    int word_count;
} Wallet;

Wallet* wallet_new(int word_count);
void wallet_free(Wallet* w);
const char* wallet_address(Wallet* w);
#endif
```

```swift
import CWallet     // module name from module.modulemap

let wallet = wallet_new(12)
defer { wallet_free(wallet) }

if let cString = wallet_address(wallet) {
    let address = String(cString: cString)
    print(address)
}
```

```
// module.modulemap (in CWallet/)
module CWallet {
    header "wallet.h"
    export *
}
```

For Swift Package Manager:

```swift
// Package.swift
.target(
    name: "CWallet",
    publicHeadersPath: "include",
    cSettings: [.headerSearchPath(".")]
),
.target(name: "MyApp", dependencies: ["CWallet"]),
```

## Pointers in Swift

```swift
// Mutable pointer
var x: Int32 = 42
withUnsafeMutablePointer(to: &x) { ptr in
    c_function(ptr)             // takes Int32*
}

// Buffer pointer for arrays
var bytes: [UInt8] = [0x01, 0x02, 0x03]
bytes.withUnsafeMutableBufferPointer { buf in
    c_process(buf.baseAddress, Int32(buf.count))
}

// Opaque pointer (treats as void*)
let opaque = OpaquePointer(somePtr)

// String to const char*
"hello".withCString { ptr in
    c_print(ptr)                // ptr: UnsafePointer<CChar>
}

// Allocate manually
let ptr = UnsafeMutablePointer<Int32>.allocate(capacity: 100)
defer { ptr.deallocate() }
ptr.initialize(repeating: 0, count: 100)
```

**Memory rules**:
- Pointers from `withUnsafe*` are valid only within the closure
- Manually-allocated pointers must be `deallocate()`d
- Don't escape pointer past closure unless source guarantees lifetime

## Swift ↔ Rust (via UniFFI)

For BHODL-style Rust→Swift bindings, use **UniFFI** (`languages/uniffi` skill).

```rust
// Rust side (.udl file)
namespace wallet {
    string generate_mnemonic(u32 word_count);
};

interface Wallet {
    constructor(string mnemonic);
    string get_address();
};
```

UniFFI generates Swift code that wraps the FFI:

```swift
// Generated by UniFFI
import Wallet     // generated module

let mnemonic = generateMnemonic(wordCount: 12)
let wallet = Wallet(mnemonic: mnemonic)
print(wallet.getAddress())
```

For raw FFI (no UniFFI), use `@_cdecl`:

```rust
// Rust
#[no_mangle]
pub extern "C" fn wallet_address(handle: *mut Wallet) -> *mut c_char {
    let wallet = unsafe { &*handle };
    CString::new(wallet.address()).unwrap().into_raw()
}

#[no_mangle]
pub extern "C" fn wallet_string_free(s: *mut c_char) {
    if !s.is_null() {
        unsafe { let _ = CString::from_raw(s); }
    }
}
```

```swift
// Swift
let cstr = wallet_address(walletHandle)
defer { wallet_string_free(cstr) }
let address = String(cString: cstr!)
```

Always pair allocation with explicit free. Never `free()` Rust-allocated memory from Swift `free()` — call back into Rust.

## Keychain Services

For storing secrets (mnemonic, encryption keys, auth tokens). Persists across app uninstall (unless `kSecAttrAccessible` is `WhenPasscodeSet`).

```swift
import Security
import Foundation

enum KeychainError: Error {
    case unhandledError(status: OSStatus)
    case noData
}

func saveSecret(_ data: Data, account: String, service: String) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service,
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
    ]

    SecItemDelete(query as CFDictionary)        // overwrite if exists
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.unhandledError(status: status)
    }
}

func loadSecret(account: String, service: String) throws -> Data {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne,
    ]

    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
    guard let data = item as? Data else { throw KeychainError.noData }
    return data
}
```

**Accessibility levels** (most→least restrictive):
- `WhenPasscodeSetThisDeviceOnly` — requires passcode + non-portable backup
- `WhenUnlockedThisDeviceOnly` — only when unlocked + non-portable
- `WhenUnlocked` — only when unlocked, included in iCloud Keychain (if user enabled)
- `AfterFirstUnlock` — after first unlock since boot (background access)

For wallet seeds, use **`WhenPasscodeSetThisDeviceOnly`** + biometric access control.

## Secure Enclave

Hardware-backed key storage (P-256 ECC keys). Private key never leaves the SEP.

```swift
import LocalAuthentication
import CryptoKit

func createSEPKey(label: String) throws -> SecureEnclave.P256.Signing.PrivateKey {
    let access = SecAccessControlCreateWithFlags(
        nil,
        kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
        [.privateKeyUsage, .userPresence],     // require Face/TouchID
        nil
    )!

    return try SecureEnclave.P256.Signing.PrivateKey(
        accessControl: access,
        authenticationContext: LAContext()
    )
}

// Sign — prompts biometric
let key = try createSEPKey(label: "com.bhodl.signing")
let signature = try key.signature(for: dataToSign)

// Verify with public key (public is exportable)
let pubKey = key.publicKey
let isValid = pubKey.isValidSignature(signature, for: dataToSign)
```

**Limits**:
- P-256 only (no secp256k1 — Bitcoin keys live elsewhere)
- One signature operation per biometric prompt by default
- Keys cannot be exported, only deleted

For Bitcoin, use SEP for **wrapping/unwrapping** the wallet seed encryption key, not for the wallet keys themselves.

## LocalAuthentication

```swift
import LocalAuthentication

func authenticate() async -> Bool {
    let context = LAContext()
    var error: NSError?
    guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
        return false
    }

    return await withCheckedContinuation { cont in
        context.evaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            localizedReason: "Unlock wallet"
        ) { success, _ in
            cont.resume(returning: success)
        }
    }
}
```

`.deviceOwnerAuthentication` (lowercase 'A') falls back to passcode if biometrics fail. `.deviceOwnerAuthenticationWithBiometrics` is biometric-only.

## NSData ↔ Data ↔ [UInt8]

```swift
// String → Data
let data = "hello".data(using: .utf8)!

// Data → [UInt8]
let bytes = [UInt8](data)

// [UInt8] → Data
let data2 = Data(bytes)

// Hex → Data
extension Data {
    init?(hex: String) {
        let len = hex.count / 2
        var data = Data(capacity: len)
        var idx = hex.startIndex
        for _ in 0..<len {
            let next = hex.index(idx, offsetBy: 2)
            guard let byte = UInt8(hex[idx..<next], radix: 16) else { return nil }
            data.append(byte)
            idx = next
        }
        self = data
    }

    var hex: String {
        map { String(format: "%02x", $0) }.joined()
    }
}
```

## Logging (os.Logger)

```swift
import os

private let log = Logger(subsystem: "com.bhodl.wallet", category: "send")

log.info("Sending \(amount.sats, privacy: .public) sats to \(address, privacy: .private)")
log.error("Send failed: \(error.localizedDescription, privacy: .public)")
```

`privacy: .private` redacts the value in non-debug logs (essential for wallet addresses, balances). View logs in Console.app filtered by subsystem.

## Common Interop Pitfalls

| Pitfall | Fix |
|---|---|
| Holding C pointer past closure | Allocate manually + manage lifetime |
| String passed to C without `withCString` | UTF-8 buffer not guaranteed valid after call |
| Forgetting to free Rust-allocated memory | Always pair `_new` / `_free` from same side |
| Keychain item lost after restore from backup | Use `ThisDeviceOnly` accessibility |
| `NSError` discarded in completion handler | Use `error.code` + `error.domain` for diagnostics |
| Calling SEP key signature off main without LAContext | Pre-authenticate, share LAContext across calls |
| `@objc` on generic class | Not allowed — wrap in non-generic facade |
