← Blog Hardware

SwiftUI-Begleit-App für BLE-IoT-Geräte: Muster und Fallstricke

Jedes IoT-Produkt braucht irgendwann eine Begleit-App. Das Funkmodul läuft, die Firmware bootet, die Cloud ist zufrieden — und dann sagt jemand: “Wie konfiguriert der Nutzer dieses Ding eigentlich?” Auf iOS lautet die Antwort fast immer eine SwiftUI-App, die über CoreBluetooth BLE spricht, und fast immer schmerzhafter, als das Team erwartet hat. CoreBluetooth wurde 2011 entworfen, ist älter als Swift überhaupt und stellt eine delegatlastige API bereit, die bei jedem Schritt mit der modernen Swift-Nebenläufigkeit im Clinch liegt. Gut gemacht, ist das Ergebnis eine Begleit-App, die sich nativ und zuverlässig anfühlt. Schlecht gemacht, bekommen Sie App-Store-Prüfer, die fragen, warum Ihre App abstürzt, wenn Bluetooth aus ist, und Kunden, die die Hardware für Probleme verantwortlich machen, die im Telefon leben.

Kurz gesagt: Der Bau einer SwiftUI-Begleit-App für BLE-IoT-Geräte bedeutet, CoreBluetooth in eine beobachtbare State-Machine zu verpacken, die iOS-Hintergrundgrenzen zu respektieren und robuste Abläufe für Kopplung, OTA-Update und Fehlerbehandlung zu entwerfen, damit sich die App nativ anfühlt.

Dieser Beitrag ist ein Praxisleitfaden zum Bau einer produktionsreifen SwiftUI-BLE-Begleit-App für ein Gerät der ESP32-Klasse. Wir behandeln BLE-Grundlagen aus der Sicht von App-Entwicklern, einen CoreBluetooth-Wrapper, der gut mit async/await zusammenspielt, eine Verbindungs-State-Machine, OTA-Firmware-Updates, die Hintergrundmodus-Regeln, die Apple tatsächlich durchsetzt, eine Teststrategie und die App-Store-Einreichungsdetails, die jedes Team mindestens einmal erwischen. Der Code ist Swift 5.9+, iOS 17 als Minimum und darauf ausgelegt, zu kompilieren.

BLE-Grundlagen für App-Entwickler

Bluetooth Low Energy baut auf dem GATT-Modell auf: Generic Attribute Profile. Ein Peripheriegerät (Ihr IoT-Gerät) stellt einen oder mehrere Dienste bereit, jeder durch eine UUID identifiziert. Jeder Dienst enthält Charakteristiken, ebenfalls UUID-identifiziert, welche die eigentlichen Datenendpunkte sind. Eine Charakteristik kann gelesen, geschrieben werden, oder sie kann das Central (Ihr Telefon) benachrichtigen, wenn sich ihr Wert ändert.

Für einen App-Entwickler ist das praktische Modell:

  • Service-UUID = eine logische Gruppe verwandter Endpunkte (“Gerätesteuerung”, “Sensordaten”)
  • Charakteristik-UUID = ein einzelner Endpunkt innerhalb dieser Gruppe (“Farbe setzen”, “Akkuprozent”)
  • Read = den aktuellen Wert einmal abrufen
  • Write = einen Wert übermitteln, mit oder ohne Bestätigung
  • Notify = Wertänderungen abonnieren (die Firmware pusht, wenn etwas passiert)

BLE-Nutzlasten sind standardmäßig winzig. Die MTU beträgt 23 Byte (20 nutzbar), bis Central und Peripheral eine höhere aushandeln. iOS handelt bis zu 185 Byte Nutzlast auf dem iPhone X und neuer aus. Alles Größere muss auf der Anwendungsschicht in Blöcke aufgeteilt werden, was für OTA enorm wichtig ist. Mehr zu den umfassenderen Abwägungen finden Sie in unserem Vergleich von BLE versus Wi-Fi für IoT.

CoreBluetooth-Grundlagen in iOS 17+

CoreBluetooth gibt Ihnen zwei Hauptklassen: CBCentralManager für Scannen und Verbinden und CBPeripheral für die Arbeit mit einem verbundenen Gerät. Beide setzen auf Delegates. Es gibt keine async-API und keine Combine-Integration, die mit dem Framework mitgeliefert wird, also bauen Sie Ihre eigene.

Das Wrapper-Muster, das sich bei uns über mehrere Produktions-Apps hinweg bewährt hat, lautet: CoreBluetooth in einem einzigen Actor halten, async-Methoden bereitstellen, die die Delegate-Callbacks über Continuations überbrücken, und Zustandsänderungen über eine @Observable-Klasse veröffentlichen, an die sich SwiftUI-Views direkt binden können.

Der Wrapper

import CoreBluetooth
import Observation

@Observable final class BLEController: NSObject { enum Status: Equatable { case unknown, poweredOff, unauthorized case scanning, connecting(String) case discovering, ready case reconnecting, failed(String) }

var status: Status = .unknown
var battery: Int? = nil
var discovered: [CBPeripheral] = []

private var central: CBCentralManager!
private var peripheral: CBPeripheral?
private var controlChar: CBCharacteristic?
private var batteryChar: CBCharacteristic?

private var connectContinuation: CheckedContinuation<Void, Error>?
private var writeContinuation: CheckedContinuation<Void, Error>?

static let serviceUUID = CBUUID(string: "4FAFC201-1FB5-459E-8FCC-C5C9C331914B")
static let controlUUID = CBUUID(string: "BEB5483E-36E1-4688-B7F5-EA07361B26A8")
static let batteryUUID = CBUUID(string: "00002A19-0000-1000-8000-00805F9B34FB")

override init() {
    super.init()
    central = CBCentralManager(delegate: self, queue: .main)
}

func startScan() {
    guard central.state == .poweredOn else { return }
    discovered.removeAll()
    status = .scanning
    central.scanForPeripherals(withServices: [Self.serviceUUID])
}

func connect(_ p: CBPeripheral) async throws {
    central.stopScan()
    peripheral = p
    p.delegate = self
    status = .connecting(p.name ?? "Device")
    try await withCheckedThrowingContinuation { cont in
        connectContinuation = cont
        central.connect(p, options: nil)
    }
}

func writeControl(_ data: Data) async throws {
    guard let peripheral, let controlChar else {
        throw BLEError.notReady
    }
    try await withCheckedThrowingContinuation { cont in
        writeContinuation = cont
        peripheral.writeValue(data, for: controlChar, type: .withResponse)
    }
}

}

enum BLEError: Error { case notReady, disconnected, timeout }

Die Delegate-Brücken

extension BLEController: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOff: status = .poweredOff
        case .unauthorized: status = .unauthorized
        case .poweredOn: if status == .unknown { status = .scanning }
        default: status = .unknown
        }
    }
func centralManager(_ c: CBCentralManager,
                    didDiscover p: CBPeripheral,
                    advertisementData: [String : Any], rssi: NSNumber) {
    if !discovered.contains(where: { $0.identifier == p.identifier }) {
        discovered.append(p)
    }
}

func centralManager(_ c: CBCentralManager, didConnect p: CBPeripheral) {
    status = .discovering
    p.discoverServices([Self.serviceUUID])
}

func centralManager(_ c: CBCentralManager, didDisconnectPeripheral p: CBPeripheral,
                    error: Error?) {
    status = .reconnecting
    c.connect(p, options: nil)   // simple auto-reconnect
}

func centralManager(_ c: CBCentralManager, didFailToConnect p: CBPeripheral,
                    error: Error?) {
    connectContinuation?.resume(throwing: error ?? BLEError.disconnected)
    connectContinuation = nil
    status = .failed(error?.localizedDescription ?? "connect failed")
}

}

extension BLEController: CBPeripheralDelegate { func peripheral(_ p: CBPeripheral, didDiscoverServices error: Error?) { guard let svc = p.services?.first(where: { $0.uuid == Self.serviceUUID }) else { return } p.discoverCharacteristics([Self.controlUUID, Self.batteryUUID], for: svc) }

func peripheral(_ p: CBPeripheral, didDiscoverCharacteristicsFor svc: CBService,
                error: Error?) {
    for ch in svc.characteristics ?? [] {
        if ch.uuid == Self.controlUUID { controlChar = ch }
        if ch.uuid == Self.batteryUUID {
            batteryChar = ch
            p.setNotifyValue(true, for: ch)
            p.readValue(for: ch)
        }
    }
    if controlChar != nil {
        status = .ready
        connectContinuation?.resume()
        connectContinuation = nil
    }
}

func peripheral(_ p: CBPeripheral, didUpdateValueFor ch: CBCharacteristic,
                error: Error?) {
    if ch.uuid == Self.batteryUUID, let b = ch.value?.first {
        battery = Int(b)
    }
}

func peripheral(_ p: CBPeripheral, didWriteValueFor ch: CBCharacteristic,
                error: Error?) {
    if let error { writeContinuation?.resume(throwing: error) }
    else { writeContinuation?.resume() }
    writeContinuation = nil
}

}

Das Muster: Jede async-Methode speichert eine Continuation, feuert den entsprechenden CoreBluetooth-Aufruf ab, und der Delegate-Callback setzt die Continuation fort. SwiftUI-Views beobachten BLEController direkt über @Observable und rendern neu, wenn sich Status, Akku oder die Liste der erkannten Geräte ändern.

Die Verbindungs-State-Machine

BLE-Verbindungen scheitern. Telefone gehen schlafen, Geräte verschwinden hinter Wänden, HF-Störungen passieren. Die Begleit-App muss die Verbindung als zustandsbehaftete Ressource behandeln, und die State-Machine muss explizit sein. Impliziter Zustand ist die Hauptursache für “Warum dreht meine App ewig”-Fehlermeldungen.

Die obigen Zustände (scanning, connecting, discovering, ready, reconnecting, failed) sind das Minimum. Jeder Übergang hat wohldefinierte Auslöser und einen Timeout. Wir verwenden einen 10-Sekunden-Timeout für Connecting und Discovering, danach wechselt der Zustand zu failed und dem Nutzer wird ein erneuter Versuch angeboten. Der Auto-Reconnect bei unerwarteter Trennung ist begrenzt: drei Versuche mit exponentiellem Backoff, dann zurück zu failed.

SwiftUI-Bindung

struct ContentView: View {
    @State private var ble = BLEController()
var body: some View {
    VStack(spacing: 24) {
        switch ble.status {
        case .scanning, .unknown:
            List(ble.discovered, id: .identifier) { p in
                Button(p.name ?? "Unknown") {
                    Task { try? await ble.connect(p) }
                }
            }
        case .connecting(let name):
            ProgressView("Connecting to (name)...")
        case .discovering:
            ProgressView("Reading device profile...")
        case .ready:
            DeviceControlView(ble: ble)
        case .reconnecting:
            ProgressView("Reconnecting...")
        case .poweredOff:
            ContentUnavailableView("Bluetooth is off",
                systemImage: "bolt.horizontal.circle")
        case .unauthorized:
            ContentUnavailableView("Bluetooth permission required",
                systemImage: "lock.shield")
        case .failed(let msg):
            ContentUnavailableView("Connection failed",
                systemImage: "exclamationmark.triangle",
                description: Text(msg))
        }
    }
    .onAppear { ble.startScan() }
}

}

Hintergrundmodi und ihre Grenzen

Der Hintergrundmodus bluetooth-central erlaubt Ihrer App, weiter zu scannen und verbunden zu bleiben, wenn sie in den Hintergrund geschoben wird. Er gibt Ihnen keinen Freifahrtschein. Die Regeln, die Apple tatsächlich durchsetzt:

  • Das Scannen im Hintergrund funktioniert nur, wenn Sie Service-UUIDs angeben. Wildcard-Scans liefern nichts zurück.
  • Hintergrund-Scans laufen mit einem viel niedrigeren Duty-Cycle. Die Erkennung kann 10x länger dauern.
  • Benachrichtigungen von einem verbundenen Peripheriegerät wecken Ihre App für kurze Schübe. Nutzen Sie sie, um lokale Benachrichtigungen zu planen, nicht für lange Arbeit.
  • Das System kann Ihre App jederzeit suspendieren. Persistieren Sie jede laufende Operation, damit sie nach einem Neustart fortgesetzt werden kann.
  • Zustandserhaltung und -wiederherstellung ist per CBCentralManagerOptionRestoreIdentifierKey und einem entsprechenden centralManager(_:willRestoreState:)-Handler optional aktivierbar. Nutzen Sie es für jedes Gerät, von dem der Nutzer erwartet, dass es über App-Neustarts hinweg verbunden bleibt.

Für Bedienoberflächen, bei denen der Nutzer ein Always-on-Verhalten erwartet — etwa eine Yacht-Infotainment-Fernbedienung — ist das richtige Muster eine optionale Wiederherstellung plus ein serverseitiger Fallback-Pfad, damit Befehle auch über Wi-Fi fließen können, wenn der Nutzer im selben Netzwerk ist. Wir verwenden diesen hybriden Ansatz in YIS und OMNIYON.

UX-Muster für die Kopplung

Der Kopplungsablauf beim ersten Start ist der Punkt, an dem Nutzer entscheiden, ob Ihre App professionell oder ein Hobbyprojekt ist. Drei Muster, die wir validiert haben:

  1. Näherungs-Gating. Zeigen Sie während des ersten Kopplungs-Scans nur Geräte mit einem RSSI über -60 dBm. Das verhindert, dass der Nutzer acht Geräte der Nachbarn sieht und sich mit dem falschen verbindet.
  2. Visuelle Bestätigung. Senden Sie nach dem Verbinden einen Befehl, der die LED des Geräts in einem bekannten Muster blinken lässt. Fragen Sie den Nutzer “Blinkt Ihr Gerät?”, bevor Sie die Kopplung als abgeschlossen betrachten.
  3. Out-of-Band-Verifizierung für sicherheitskritische Geräte. Für Schlösser oder alles, bei dem ein Fehlkopplungs-Angriff eine Rolle spielt, nutzen Sie die Passkey-Eingabe von BLE mit einem 6-stelligen Code auf einem kleinen Gerätedisplay oder QR-Code-kodierte Kopplungsschlüssel.

OTA-Firmware-Update-Ablauf

Das Over-the-Air-Firmware-Update ist der anspruchsvollste BLE-Ablauf, den Sie bauen werden. Ein 1-MB-ESP32-Firmware-Image bei 185-Byte-MTU und konservativem Durchsatz braucht etwa 90 Sekunden zum Übertragen. Der Ablauf:

  1. Die App lädt einen signierten Firmware-Blob aus der Cloud herunter.
  2. Die App verifiziert die Signatur lokal gegen einen eingebetteten öffentlichen Schlüssel.
  3. Die App schreibt einen Steuerbefehl an das Gerät: “Bereite dich auf OTA vor, erwarte N Byte, Hash X”.
  4. Das Gerät bestätigt, wechselt in den OTA-Modus (Benachrichtigungen auf einer Fortschritts-Charakteristik aktiviert).
  5. Die App schreibt die Firmware in Blöcken, jeder als Write-without-Response, mit periodischem Write-with-Response, um die LE-Link-Layer-Warteschlange zu leeren.
  6. Die App empfängt Fortschrittsbenachrichtigungen und zeigt einen Fortschrittsbalken an.
  7. Das Gerät verifiziert den Hash, wendet das Update an und startet neu.
  8. Die App wartet auf die Wiederverbindung mit der neuen Firmware-Version, bestätigt über eine Versions-Charakteristik und erklärt den Erfolg.

Die beiden Fehlermodi, die am härtesten zubeißen, sind die App-Suspendierung während der Übertragung (der Nutzer schiebt die App in den Hintergrund, um eine Benachrichtigung zu prüfen) und der Signalverlust mitten in der Übertragung. Beide müssen durch fortsetzbare Übertragung behandelt werden: Verfolgen Sie den zuletzt bestätigten Offset, fragen Sie das Gerät bei der Wiederverbindung nach seinem aktuellen Offset und setzen Sie von dort fort. Ohne dies scheitern jeden Samstagmorgen die Firmware-Updates eines ganzen Wohnhauses.

Fehlerbehandlung, die Sie brauchen werden

Jede Fehlerkategorie braucht eine nutzerseitige Meldung und eine Wiederherstellungsaktion. Das Minimum:

  • Bluetooth aus — per UIApplication.openSettingsURLString direkt in die Einstellungen verlinken.
  • Berechtigung verweigert — erklären, warum, und in die Einstellungen verlinken.
  • Gerät im Scan nicht gefunden — einen 30-Sekunden-Wiederholungsversuch und Tipps anbieten (“Ist das Gerät eingeschaltet, innerhalb von 5 Metern?”).
  • Verbindungs-Timeout — erneuten Versuch anbieten; nach drei Fehlversuchen einen Neustart des Geräts vorschlagen.
  • Trennung mitten in der Operation — klar anzeigen, einen Auto-Reconnect versuchen, dann den Nutzer auffordern.
  • Schreiben mit charakteristik-spezifischem Fehlercode fehlgeschlagen — auf eine sinnvolle Meldung abbilden; zeigen Sie Nutzern niemals rohe Error-Beschreibungen.

Teststrategie

CoreBluetooth lässt sich nicht direkt Unit-testen, weil CBCentralManager ein echtes Funkmodul benötigt. Die saubere Lösung ist eine Protokoll-Abstraktion:

protocol Peripheral: AnyObject {
    var name: String? { get }
    func write(_ data: Data) async throws
    func readBattery() async throws -> Int
}

final class MockPeripheral: Peripheral { var name: String? = “Mock Device” var lastWrite: Data? var batteryValue = 87 var writeShouldFail = false

func write(_ data: Data) async throws {
    if writeShouldFail { throw BLEError.disconnected }
    lastWrite = data
}
func readBattery() async throws -> Int { batteryValue }

}

Verpacken Sie das echte CBPeripheral in eine Klasse, die Peripheral entspricht. Injizieren Sie den Protokolltyp in Ihre View-Models. Nun können Sie deterministische Tests für jeden Zustandsübergang, jeden Fehlerpfad und den gesamten OTA-Ablauf ohne echte Hardware schreiben. Nutzen Sie echte Hardware für End-to-End-Smoke-Tests auf physischen Geräten und für die zeitkritischen Teile von OTA.

Stolperfallen bei der App-Store-Einreichung

Apple ist bei Bluetooth streng, und Sie werden mindestens einmal abgelehnt, wenn Sie eines der Folgenden auslassen.

  • NSBluetoothAlwaysUsageDescription in der Info.plist mit einem klaren, spezifischen Grund. “Diese App verwendet Bluetooth” wird abgelehnt. “Diese App verbindet sich mit Ihrem FSS-Gerät, um dessen Einstellungen zu steuern und Sensordaten zu empfangen” besteht.
  • Hintergrundmodi in UIBackgroundModes nur deklariert, wenn Sie sie tatsächlich nutzen. Das Deklarieren von bluetooth-central, wenn Sie nie Hintergrund-BLE verwenden, ist eine Ablehnung.
  • Demo-Konto oder Video für jede geräteabhängige Funktionalität. Der Prüfer hat Ihre Hardware nicht. Eine kurze Bildschirmaufnahme des vollständigen Ablaufs beseitigt dieses Hin und Her.
  • Datenschutz-Nährwert-Etiketten müssen die BLE-abgeleitete Datenerhebung akkurat beschreiben. Der Akkuprozentsatz eines gekoppelten Geräts ist in der Regel unproblematisch; standortabgeleitete Daten benötigen eine Deklaration.
  • Anständiges Verhalten bei ausgeschaltetem Bluetooth — die App darf nicht abstürzen, muss eine klare UI zeigen und darf nicht darauf bestehen, dass Bluetooth zum Start erforderlich ist.

Echtes Beispiel: ESP32-Begleit-App

Das obige Muster ist genau das, was wir für einen kürzlich entwickelten ESP32-basierten Umweltmonitor ausgeliefert haben. Das Gerät stellt einen benutzerdefinierten Dienst mit drei Charakteristiken bereit: Steuerung (Write), Live-Messwerte (Notify), Akku (Read+Notify). Die Kopplung dauert unter guten Bedingungen etwa 4 Sekunden. Die App unterstützt Hintergrund-Scannen, um zu erkennen, wenn der Nutzer nach Hause kommt, und verbindet sich automatisch neu. OTA liefert eine 740-KB-Firmware in etwa 70 Sekunden mit fortsetzbarer Übertragung. Die Zustandserhaltung erlaubt es dem Nutzer, die App zwei Tage später zu öffnen und innerhalb von 200 ms Live-Daten zu sehen, weil die Verbindung vom System erhalten wird.

Dieselbe Architektur skaliert auf Multi-Peripheral-Apps (eine Yacht mit mehreren verbundenen Zonen), Apps, die BLE zur Cloud überbrücken (das Telefon fungiert als Gateway, wenn das Gerät außerhalb der Wi-Fi-Reichweite ist), und Apps, die BLE-Steuerung mit Wi-Fi-Provisionierung mischen. Wir bauen diese Muster in iOS- und Android-Begleit-Apps ein.

Häufig gestellte Fragen

Welches iOS-Framework verwende ich, um mich mit BLE-IoT-Geräten zu verbinden?

Sie verwenden Apples CoreBluetooth-Framework. In einer SwiftUI-App verpacken Sie CBCentralManager und seine Delegate-Callbacks in ein ObservableObject, sodass Verbindungszustand und Charakteristik-Werte Ihre Views reaktiv steuern.

Kann eine SwiftUI-BLE-App im Hintergrund weiterarbeiten?

Nur teilweise. iOS erlaubt begrenztes Hintergrund-BLE über den Hintergrundmodus bluetooth-central, aber das Scannen wird gedrosselt, Service-UUIDs müssen deklariert werden, und Sie können sich nicht auf kontinuierliche, hochfrequente Aktualisierungen verlassen, während die App suspendiert ist.

Wie liefere ich OTA-Firmware-Updates aus einer SwiftUI-Begleit-App aus?

Teilen Sie die Firmware-Binärdatei in Blöcke, streamen Sie sie über eine dedizierte GATT-Charakteristik mit Flusskontrolle und CRC-Validierung, zeigen Sie klaren Fortschritt und behandeln Sie Trennungen mit fortsetzbaren Übertragungen, um das Bricken des Geräts zu vermeiden.

Liefern Sie eine Begleit-App aus, die sich nativ anfühlt

Eine großartige BLE-Begleit-App ist unsichtbar. Der Nutzer tippt auf einen Knopf, das Gerät reagiert, und niemand denkt an Funkmodule, GATT-Profile oder Hintergrundmodi. Dorthin zu gelangen erfordert bewusste Architektur: eine State-Machine, einen async-Wrapper, mockbare Abstraktionen, sorgfältige Hintergrundbehandlung und Respekt vor der Checkliste des App-Store-Prüfers. Jedes Teilstück ist unkompliziert; die Disziplin liegt darin, sich unter Termindruck an alle zu halten.

FSS baut vernetzte Geräte durchgängig: Leiterplatte, Firmware, Cloud und die iOS- und Android-Apps, die Nutzer tatsächlich bedienen. Wenn Sie eine Begleit-App für ein bestehendes Gerät abstecken oder das Gerät vom ersten Tag an gemeinsam mit der App entwerfen, sprechen Sie mit unserem Mobile-Control-Team. Wir teilen Referenzarchitekturen, Zeitplan-Schätzungen und die spezifischen App-Store-Stolperfallen, die für Ihre Kategorie relevant sind.

{“@context”: “https://schema.org”, “@type”: “Article”, “headline”: “SwiftUI-Begleit-App für BLE-IoT-Geräte: Muster und Fallstricke”, “description”: “Der Bau einer SwiftUI-Begleit-App für BLE-IoT-Geräte bedeutet, CoreBluetooth in eine beobachtbare State-Machine zu verpacken, die iOS-Hintergrundgrenzen zu respektieren und robuste Abläufe für Kopplung, OTA-Update und Fehlerbehandlung zu entwerfen, damit sich die App nativ anfühlt.”, “inLanguage”: “de”, “datePublished”: “2026-04-24T08:19:13”, “dateModified”: “2026-06-25T12:00:00”, “author”: {“@type”: “Organization”, “name”: “FSS Technology”, “url”: “https://fss.cc/”}, “publisher”: {“@type”: “Organization”, “name”: “FSS Technology”, “url”: “https://fss.cc/”}, “mainEntityOfPage”: {“@type”: “WebPage”, “@id”: “https://fss.cc/swiftui-ble-companion-app-patterns-pitfalls/”}}
{“@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [{“@type”: “Question”, “name”: “Welches iOS-Framework verwende ich, um mich mit BLE-IoT-Geräten zu verbinden?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Sie verwenden Apples CoreBluetooth-Framework. In einer SwiftUI-App verpacken Sie CBCentralManager und seine Delegate-Callbacks in ein ObservableObject, sodass Verbindungszustand und Charakteristik-Werte Ihre Views reaktiv steuern.”}}, {“@type”: “Question”, “name”: “Kann eine SwiftUI-BLE-App im Hintergrund weiterarbeiten?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Nur teilweise. iOS erlaubt begrenztes Hintergrund-BLE über den Hintergrundmodus bluetooth-central, aber das Scannen wird gedrosselt, Service-UUIDs müssen deklariert werden, und Sie können sich nicht auf kontinuierliche, hochfrequente Aktualisierungen verlassen, während die App suspendiert ist.”}}, {“@type”: “Question”, “name”: “Wie liefere ich OTA-Firmware-Updates aus einer SwiftUI-Begleit-App aus?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Teilen Sie die Firmware-Binärdatei in Blöcke, streamen Sie sie über eine dedizierte GATT-Charakteristik mit Flusskontrolle und CRC-Validierung, zeigen Sie klaren Fortschritt und behandeln Sie Trennungen mit fortsetzbaren Übertragungen, um das Bricken des Geräts zu vermeiden.”}}]}