# Apple Platform Integration Guide (iOS/macOS)

This guide covers integrating `peat-btle` into iOS and macOS applications using CoreBluetooth.

## Requirements

| Platform | Minimum Version |
|----------|-----------------|
| iOS | 13.0 |
| macOS | 10.15 (Catalina) |
| Xcode | 15.0+ |

### Hardware Requirements

- iPhone 8 or newer for best BLE 5.0 support
- Any modern Mac with Bluetooth

## Architecture

```
┌─────────────────────────────────────────┐
│       SwiftUI / UIKit Application       │
├─────────────────────────────────────────┤
│         UniFFI Swift Bindings           │
├─────────────────────────────────────────┤
│      CoreBluetoothAdapter (Rust)        │
├─────────────────────────────────────────┤
│  CentralManager    │  PeripheralManager │
│   (scanning,       │   (advertising,    │
│    connecting)     │    GATT server)    │
├─────────────────────────────────────────┤
│           Objective-C Delegates          │
├─────────────────────────────────────────┤
│           CoreBluetooth Framework        │
└─────────────────────────────────────────┘
```

## Project Setup

### Option 1: Pure Swift with Native BLE

For simpler integration, use Swift's CoreBluetooth directly and call Rust for mesh logic only.

### Option 2: Full Rust Integration (UniFFI)

Use UniFFI to expose the entire Rust API to Swift.

---

## Info.plist Configuration

Add required permissions:

```xml
<!-- Bluetooth usage description -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Peat uses Bluetooth to sync data with nearby devices</string>

<!-- For iOS 13+ -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Peat uses Bluetooth to sync data with nearby devices</string>

<!-- Background modes (iOS) -->
<key>UIBackgroundModes</key>
<array>
    <string>bluetooth-central</string>
    <string>bluetooth-peripheral</string>
</array>
```

### macOS Sandbox Entitlements

For sandboxed Mac apps, add to `*.entitlements`:

```xml
<key>com.apple.security.device.bluetooth</key>
<true/>
```

## Swift Implementation

### PeatManager Class

```swift
import CoreBluetooth
import Combine

class PeatManager: NSObject, ObservableObject {
    // Published state
    @Published var peers: [PeatPeer] = []
    @Published var isScanning = false
    @Published var isAdvertising = false
    @Published var emergencyActive = false

    // CoreBluetooth managers
    private var centralManager: CBCentralManager!
    private var peripheralManager: CBPeripheralManager!

    // Peat UUIDs
    private let peatServiceUUID = CBUUID(string: "F47AC10B-58CC-4372-A567-0E02B2C3D479")
    private let documentCharUUID = CBUUID(string: "F47AC10B-58CC-4372-A567-0E02B2C30003")

    // Connections
    private var connectedPeripherals: [UUID: CBPeripheral] = [:]
    private var documentCharacteristics: [UUID: CBCharacteristic] = [:]

    // Rust bridge
    private var meshBridge: PeatMeshBridge?

    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
        peripheralManager = CBPeripheralManager(delegate: self, queue: nil)

        // Initialize Rust mesh
        initializeMesh()
    }

    private func initializeMesh() {
        let nodeId = generateNodeId()
        meshBridge = PeatMeshBridge(
            nodeId: nodeId,
            callsign: "IOS-\(UIDevice.current.name.prefix(4))",
            meshId: "DEMO"
        )
    }

    private func generateNodeId() -> UInt32 {
        // Use a stable identifier derived from device
        let id = UIDevice.current.identifierForVendor ?? UUID()
        let bytes = id.uuid
        return UInt32(bytes.12) << 24 |
               UInt32(bytes.13) << 16 |
               UInt32(bytes.14) << 8 |
               UInt32(bytes.15)
    }

    // MARK: - Public API

    func startScanning() {
        guard centralManager.state == .poweredOn else { return }
        centralManager.scanForPeripherals(
            withServices: [peatServiceUUID],
            options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
        )
        isScanning = true
    }

    func stopScanning() {
        centralManager.stopScan()
        isScanning = false
    }

    func startAdvertising() {
        guard peripheralManager.state == .poweredOn else { return }

        let advertisementData: [String: Any] = [
            CBAdvertisementDataServiceUUIDsKey: [peatServiceUUID],
            CBAdvertisementDataLocalNameKey: meshBridge?.deviceName ?? "Peat"
        ]

        peripheralManager.startAdvertising(advertisementData)
        isAdvertising = true
    }

    func stopAdvertising() {
        peripheralManager.stopAdvertising()
        isAdvertising = false
    }

    func sendEmergency() {
        guard let data = meshBridge?.sendEmergency() else { return }
        broadcastToAllPeers(data: data)
        emergencyActive = true
    }

    func sendAck() {
        guard let data = meshBridge?.sendAck() else { return }
        broadcastToAllPeers(data: data)
    }

    func clearEmergency() {
        meshBridge?.clearEvent()
        emergencyActive = false
    }

    private func broadcastToAllPeers(data: Data) {
        for (uuid, char) in documentCharacteristics {
            if let peripheral = connectedPeripherals[uuid] {
                peripheral.writeValue(data, for: char, type: .withResponse)
            }
        }
    }

    func tick() {
        if let data = meshBridge?.tick() {
            broadcastToAllPeers(data: data)
        }
    }
}

// MARK: - CBCentralManagerDelegate

extension PeatManager: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            startScanning()
            setupGattService()
        case .poweredOff:
            isScanning = false
            isAdvertising = false
        default:
            break
        }
    }

    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String: Any],
                        rssi RSSI: NSNumber) {
        let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String

        // Parse mesh ID from name
        var meshId: String?
        if let name = name, name.hasPrefix("PEAT_") {
            let parts = name.dropFirst(5).split(separator: "-")
            if parts.count >= 1 {
                meshId = String(parts[0])
            }
        }

        // Notify Rust layer
        if let nodeId = meshBridge?.onDiscovered(
            identifier: peripheral.identifier.uuidString,
            name: name,
            rssi: RSSI.int8Value,
            meshId: meshId
        ) {
            // Add to peers list
            let peer = PeatPeer(
                nodeId: nodeId,
                name: name ?? "Unknown",
                rssi: RSSI.intValue
            )

            if !peers.contains(where: { $0.nodeId == nodeId }) {
                DispatchQueue.main.async {
                    self.peers.append(peer)
                }
            }

            // Connect if not already
            if connectedPeripherals[peripheral.identifier] == nil {
                central.connect(peripheral, options: nil)
            }
        }
    }

    func centralManager(_ central: CBCentralManager,
                        didConnect peripheral: CBPeripheral) {
        connectedPeripherals[peripheral.identifier] = peripheral
        peripheral.delegate = self
        peripheral.discoverServices([peatServiceUUID])

        meshBridge?.onConnected(identifier: peripheral.identifier.uuidString)
    }

    func centralManager(_ central: CBCentralManager,
                        didDisconnectPeripheral peripheral: CBPeripheral,
                        error: Error?) {
        connectedPeripherals.removeValue(forKey: peripheral.identifier)
        documentCharacteristics.removeValue(forKey: peripheral.identifier)

        meshBridge?.onDisconnected(identifier: peripheral.identifier.uuidString)

        // Reconnect
        central.connect(peripheral, options: nil)
    }
}

// MARK: - CBPeripheralDelegate

extension PeatManager: CBPeripheralDelegate {
    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }

        for service in services {
            if service.uuid == peatServiceUUID {
                peripheral.discoverCharacteristics([documentCharUUID], for: service)
            }
        }
    }

    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverCharacteristicsFor service: CBService,
                    error: Error?) {
        guard let characteristics = service.characteristics else { return }

        for char in characteristics {
            if char.uuid == documentCharUUID {
                documentCharacteristics[peripheral.identifier] = char

                // Enable notifications
                peripheral.setNotifyValue(true, for: char)

                // Initial read
                peripheral.readValue(for: char)
            }
        }
    }

    func peripheral(_ peripheral: CBPeripheral,
                    didUpdateValueFor characteristic: CBCharacteristic,
                    error: Error?) {
        guard let data = characteristic.value else { return }

        if let result = meshBridge?.onDataReceived(
            identifier: peripheral.identifier.uuidString,
            data: data
        ) {
            if result.isEmergency {
                DispatchQueue.main.async {
                    self.emergencyActive = true
                    self.triggerHapticFeedback()
                }
            }
        }
    }

    private func triggerHapticFeedback() {
        #if os(iOS)
        let generator = UINotificationFeedbackGenerator()
        generator.notificationOccurred(.warning)
        #endif
    }
}

// MARK: - CBPeripheralManagerDelegate

extension PeatManager: CBPeripheralManagerDelegate {
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        if peripheral.state == .poweredOn {
            setupGattService()
            startAdvertising()
        }
    }

    private func setupGattService() {
        let characteristic = CBMutableCharacteristic(
            type: documentCharUUID,
            properties: [.read, .write, .notify],
            value: nil,
            permissions: [.readable, .writeable]
        )

        let service = CBMutableService(type: peatServiceUUID, primary: true)
        service.characteristics = [characteristic]

        peripheralManager.add(service)
    }

    func peripheralManager(_ peripheral: CBPeripheralManager,
                           didReceiveRead request: CBATTRequest) {
        if request.characteristic.uuid == documentCharUUID {
            if let data = meshBridge?.buildDocument() {
                request.value = data
                peripheral.respond(to: request, withResult: .success)
            } else {
                peripheral.respond(to: request, withResult: .attributeNotFound)
            }
        }
    }

    func peripheralManager(_ peripheral: CBPeripheralManager,
                           didReceiveWrite requests: [CBATTRequest]) {
        for request in requests {
            if request.characteristic.uuid == documentCharUUID,
               let data = request.value {
                meshBridge?.onDataReceived(
                    identifier: request.central.identifier.uuidString,
                    data: data
                )
            }
        }

        if let first = requests.first {
            peripheral.respond(to: first, withResult: .success)
        }
    }
}
```

### SwiftUI View

```swift
import SwiftUI

struct ContentView: View {
    @StateObject private var peatManager = PeatManager()

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                // Status
                HStack {
                    StatusIndicator(
                        label: "Scanning",
                        active: peatManager.isScanning
                    )
                    StatusIndicator(
                        label: "Advertising",
                        active: peatManager.isAdvertising
                    )
                }

                // Peer list
                List(peatManager.peers) { peer in
                    PeerRow(peer: peer)
                }

                // Action buttons
                HStack(spacing: 20) {
                    Button("EMERGENCY") {
                        peatManager.sendEmergency()
                    }
                    .buttonStyle(EmergencyButtonStyle())

                    Button("ACK") {
                        peatManager.sendAck()
                    }
                    .buttonStyle(AckButtonStyle())

                    Button("RESET") {
                        peatManager.clearEmergency()
                    }
                    .buttonStyle(ResetButtonStyle())
                }
            }
            .navigationTitle("Peat Mesh")
            .onAppear {
                // Start tick timer
                Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                    peatManager.tick()
                }
            }
        }
    }
}
```

## UniFFI Integration (Optional)

For full Rust API exposure, use UniFFI bindings.

### 1. Create FFI Crate

```toml
# peat-apple-ffi/Cargo.toml
[package]
name = "peat-apple-ffi"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["staticlib", "cdylib"]
name = "peat_apple_ffi"

[dependencies]
peat-btle = { path = ".." }
uniffi = "0.25"

[build-dependencies]
uniffi = { version = "0.25", features = ["build"] }
```

### 2. Define UDL Interface

```udl
// peat-apple-ffi/src/peat.udl
namespace peat_apple_ffi {
    PeatMeshBridge create_mesh(u32 node_id, string callsign, string mesh_id);
};

interface PeatMeshBridge {
    constructor(u32 node_id, string callsign, string mesh_id);

    string device_name();

    u32? on_discovered(string identifier, string? name, i8 rssi, string? mesh_id);
    u32? on_connected(string identifier);
    void on_disconnected(string identifier);

    DataResult? on_data_received(string identifier, bytes data);

    bytes send_emergency();
    bytes send_ack();
    void clear_event();

    bytes? tick();
    bytes build_document();
};

dictionary DataResult {
    u32 source_node;
    boolean is_emergency;
    boolean is_ack;
};
```

### 3. Build Script

```bash
#!/bin/bash
# build-apple.sh

set -e

# Build for all Apple platforms
for TARGET in \
    aarch64-apple-ios \
    aarch64-apple-ios-sim \
    x86_64-apple-ios \
    aarch64-apple-darwin \
    x86_64-apple-darwin
do
    echo "Building for $TARGET..."
    cargo build --release --target $TARGET
done

# Create XCFramework
mkdir -p build

# Generate Swift bindings
cargo run --bin uniffi-bindgen generate \
    src/peat.udl --language swift --out-dir build/

# Create fat libraries
lipo -create \
    target/aarch64-apple-ios-sim/release/libpeat_apple_ffi.a \
    target/x86_64-apple-ios/release/libpeat_apple_ffi.a \
    -output build/libpeat_apple_ffi_sim.a

# Create XCFramework
xcodebuild -create-xcframework \
    -library target/aarch64-apple-ios/release/libpeat_apple_ffi.a \
    -headers build/ \
    -library build/libpeat_apple_ffi_sim.a \
    -headers build/ \
    -library target/aarch64-apple-darwin/release/libpeat_apple_ffi.a \
    -headers build/ \
    -output build/PeatFFI.xcframework

echo "XCFramework created at build/PeatFFI.xcframework"
```

## Background Execution

### iOS Background Handling

```swift
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Check if launched from BLE event
        if let centralOptions = launchOptions?[.bluetoothCentrals] as? [String] {
            // Restore central manager state
        }
        if let peripheralOptions = launchOptions?[.bluetoothPeripherals] as? [String] {
            // Restore peripheral manager state
        }
        return true
    }
}
```

### State Restoration

```swift
// In PeatManager init
centralManager = CBCentralManager(
    delegate: self,
    queue: nil,
    options: [CBCentralManagerOptionRestoreIdentifierKey: "PeatCentral"]
)

peripheralManager = CBPeripheralManager(
    delegate: self,
    queue: nil,
    options: [CBPeripheralManagerOptionRestoreIdentifierKey: "PeatPeripheral"]
)

// Handle restoration
func centralManager(_ central: CBCentralManager,
                    willRestoreState dict: [String: Any]) {
    if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
        for peripheral in peripherals {
            connectedPeripherals[peripheral.identifier] = peripheral
            peripheral.delegate = self
        }
    }
}
```

## Troubleshooting

### Common Issues

| Issue | Cause | Solution |
|-------|-------|----------|
| Scan returns nothing | No BLE permission | Check Info.plist |
| Background stops | Missing background mode | Add UIBackgroundModes |
| Mac sandbox error | Missing entitlement | Add bluetooth entitlement |
| Discovery fails | Wrong UUID format | Use uppercase UUID |

### Debug Logging

```swift
// Enable CoreBluetooth debug logging
// Add to scheme environment variables:
// CBUUID_DEBUG=1
```

## References

- [CoreBluetooth Programming Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/)
- [WWDC: What's New in Core Bluetooth](https://developer.apple.com/videos/play/wwdc2019/901/)
- [UniFFI Swift Bindings](https://mozilla.github.io/uniffi-rs/swift/overview.html)
- [Background Execution](https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518696-restoredstate)
