← Blog Hardware

Aplikacja towarzysząca SwiftUI dla urządzeń BLE IoT: wzorce i pułapki

Każdy produkt IoT prędzej czy później potrzebuje aplikacji towarzyszącej. Radio działa, firmware się uruchamia, chmura jest zadowolona — a potem ktoś pyta: “jak użytkownik ma właściwie skonfigurować tę rzecz?”. Na iOS odpowiedzią jest niemal zawsze aplikacja SwiftUI komunikująca się z BLE przez CoreBluetooth i niemal zawsze bardziej bolesna, niż zespół się spodziewał. CoreBluetooth zaprojektowano w 2011 roku, jest starszy niż sam Swift i udostępnia API mocno oparte na delegatach, które na każdym kroku walczy z nowoczesną współbieżnością Swifta. Zrobiona dobrze, aplikacja towarzysząca sprawia wrażenie natywnej i niezawodnej. Zrobiona źle, otrzymujesz recenzentów App Store pytających, dlaczego Twoja aplikacja się zawiesza, gdy Bluetooth jest wyłączony, oraz klientów obwiniających sprzęt za problemy, które tkwią w telefonie.

W skrócie: Budowa aplikacji towarzyszącej SwiftUI dla urządzeń BLE IoT oznacza opakowanie CoreBluetooth w obserwowalną maszynę stanów, respektowanie ograniczeń pracy w tle na iOS oraz zaprojektowanie solidnych przepływów parowania, aktualizacji OTA i obsługi błędów, aby aplikacja sprawiała wrażenie natywnej.

Ten wpis to przewodnik terenowy po budowie produkcyjnej aplikacji towarzyszącej SwiftUI BLE dla urządzenia klasy ESP32. Omówimy podstawy BLE z perspektywy programistów aplikacji, wrapper CoreBluetooth współgrający z async/await, maszynę stanów połączenia, aktualizacje firmware'u OTA, reguły trybów pracy w tle, które Apple faktycznie egzekwuje, strategię testowania oraz szczegóły zgłoszenia do App Store, które łapią każdy zespół co najmniej raz. Kod jest w Swift 5.9+, minimum iOS 17 i ma się kompilować.

Podstawy BLE dla programistów aplikacji

Bluetooth Low Energy jest zbudowany wokół modelu GATT: Generic Attribute Profile. Urządzenie peryferyjne (Twoje urządzenie IoT) udostępnia jedną lub więcej usług, z których każda identyfikowana jest przez UUID. Każda usługa zawiera charakterystyki, również identyfikowane przez UUID, które są faktycznymi punktami końcowymi danych. Charakterystykę można odczytać, zapisać albo może ona powiadamiać centralę (Twój telefon), gdy jej wartość się zmienia.

Dla programisty aplikacji praktyczny model wygląda tak:

  • UUID usługi = logiczna grupa powiązanych punktów końcowych (“sterowanie urządzeniem”, “dane czujnika”)
  • UUID charakterystyki = pojedynczy punkt końcowy w tej grupie (“ustaw kolor”, “procent baterii”)
  • Odczyt (Read) = jednorazowe pobranie bieżącej wartości
  • Zapis (Write) = wysłanie wartości, z potwierdzeniem lub bez
  • Powiadomienie (Notify) = subskrypcja zmian wartości (firmware wysyła, gdy coś się dzieje)

Ładunki BLE są domyślnie maleńkie. MTU wynosi 23 bajty (20 użytecznych), dopóki centrala i urządzenie peryferyjne nie wynegocjują wyższego. iOS negocjuje ładunek do 185 bajtów na iPhone X i nowszych. Cokolwiek większego musi być dzielone na fragmenty w warstwie aplikacji, co ma ogromne znaczenie dla OTA. Więcej o szerszych kompromisach w naszym porównaniu BLE kontra Wi-Fi dla IoT.

Podstawy CoreBluetooth w iOS 17+

CoreBluetooth daje Ci dwie główne klasy: CBCentralManager do skanowania i łączenia oraz CBPeripheral do pracy z podłączonym urządzeniem. Obie opierają się na delegatach. Framework nie dostarcza żadnego API async ani integracji z Combine, więc budujesz własną.

Wzorzec wrappera, który sprawdził się u nas w wielu aplikacjach produkcyjnych, to: trzymaj CoreBluetooth w pojedynczym aktorze, udostępnij metody async mostkujące wywołania zwrotne delegatów przez kontynuacje i publikuj zmiany stanu przez klasę @Observable, do której widoki SwiftUI mogą się bezpośrednio podpiąć.

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 }

Mostki delegatów

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
}

}

Wzorzec: każda metoda async zapisuje kontynuację, wywołuje odpowiadające jej wywołanie CoreBluetooth, a wywołanie zwrotne delegata wznawia kontynuację. Widoki SwiftUI obserwują BLEController bezpośrednio przez @Observable i renderują się ponownie, gdy zmienia się status, bateria lub lista wykrytych urządzeń.

Maszyna stanów połączenia

Połączenia BLE zawodzą. Telefony zasypiają, urządzenia znikają za ścianami, zdarzają się zakłócenia RF. Aplikacja towarzysząca musi traktować połączenie jako zasób ze stanem, a maszyna stanów musi być jawna. Ukryty stan to główna przyczyna zgłoszeń błędów typu “dlaczego moja aplikacja kręci się w nieskończoność”.

Powyższe stany (scanning, connecting, discovering, ready, reconnecting, failed) to minimum. Każde przejście ma dobrze zdefiniowane wyzwalacze i limit czasu. Używamy 10-sekundowego limitu czasu dla łączenia i wykrywania, po którym stan przechodzi w failed, a użytkownikowi oferowana jest ponowna próba. Automatyczne ponowne łączenie po nieoczekiwanym rozłączeniu jest ograniczone: trzy próby z wykładniczym odczekiwaniem, potem powrót do failed.

Podpięcie w SwiftUI

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() }
}

}

Tryby pracy w tle i ich ograniczenia

Tryb pracy w tle bluetooth-central pozwala aplikacji kontynuować skanowanie i pozostawać połączoną, gdy działa w tle. Nie daje jednak wolnej ręki. Reguły, które Apple faktycznie egzekwuje:

  • Skanowanie w tle działa tylko wtedy, gdy podasz UUID-y usług. Skany z symbolem wieloznacznym nie zwracają nic.
  • Skany w tle działają z dużo niższym cyklem pracy. Wykrywanie może trwać 10x dłużej.
  • Powiadomienia od podłączonego urządzenia peryferyjnego wybudzają aplikację na krótkie chwile. Używaj ich do planowania powiadomień lokalnych, a nie do wykonywania długiej pracy.
  • System może zawiesić Twoją aplikację w dowolnym momencie. Utrwalaj każdą trwającą operację, aby mogła zostać wznowiona po świeżym uruchomieniu.
  • Zachowywanie i przywracanie stanu jest opcjonalne, przez CBCentralManagerOptionRestoreIdentifierKey i odpowiadający handler centralManager(_:willRestoreState:). Używaj go dla każdego urządzenia, które użytkownik oczekuje mieć połączone między ponownymi uruchomieniami aplikacji.

Dla powierzchni sterowania, gdzie użytkownik oczekuje działania zawsze włączonego — na przykład pilot systemu infotainment na jachcie — właściwym wzorcem jest opcjonalne przywracanie plus ścieżka awaryjna po stronie serwera, tak aby polecenia mogły płynąć również przez Wi-Fi, gdy użytkownik jest w tej samej sieci. Stosujemy to podejście hybrydowe w YIS i OMNIYON.

Wzorce UX parowania

Przepływ parowania przy pierwszym uruchomieniu to moment, w którym użytkownicy decydują, czy Twoja aplikacja jest profesjonalna, czy to projekt hobbystyczny. Trzy wzorce, które zweryfikowaliśmy:

  1. Bramkowanie bliskością. Pokazuj tylko urządzenia z RSSI powyżej -60 dBm podczas początkowego skanu parowania. To zapobiega temu, by użytkownik widział osiem urządzeń sąsiadów i połączył się z niewłaściwym.
  2. Potwierdzenie wizualne. Po połączeniu wyślij polecenie, które sprawi, że dioda LED urządzenia zamruga w znanym wzorze. Zapytaj użytkownika “czy Twoje urządzenie miga?” zanim uznasz parowanie za zakończone.
  3. Weryfikacja poza pasmem dla urządzeń wrażliwych na bezpieczeństwo. Dla zamków lub czegokolwiek, gdzie atak przez błędne parowanie ma znaczenie, użyj wprowadzania klucza dostępu BLE z 6-cyfrowym kodem wyświetlanym na małym ekranie urządzenia albo kluczy parowania zakodowanych w kodzie QR.

Przepływ aktualizacji firmware'u OTA

Aktualizacja firmware'u przez powietrze (OTA) to najbardziej wymagający przepływ BLE, jaki zbudujesz. Obraz firmware'u ESP32 o wielkości 1 MB przy MTU 185 bajtów i konserwatywnej przepustowości przesyła się około 90 sekund. Przepływ:

  1. Aplikacja pobiera podpisany blob firmware'u z chmury.
  2. Aplikacja weryfikuje podpis lokalnie względem osadzonego klucza publicznego.
  3. Aplikacja wysyła do urządzenia polecenie sterujące: “przygotuj się na OTA, oczekuj N bajtów, hash X”.
  4. Urządzenie potwierdza, przełącza się w tryb OTA (powiadomienia włączone na charakterystyce postępu).
  5. Aplikacja zapisuje firmware fragmentami, każdy jako zapis bez odpowiedzi, z okresowym zapisem z odpowiedzią, aby opróżnić kolejkę warstwy łącza LE.
  6. Aplikacja odbiera powiadomienia o postępie, wyświetla pasek postępu.
  7. Urządzenie weryfikuje hash, stosuje aktualizację, uruchamia się ponownie.
  8. Aplikacja czeka na ponowne połączenie na nowej wersji firmware'u, potwierdza przez charakterystykę wersji, ogłasza sukces.

Dwa tryby awarii, które gryzą najmocniej, to zawieszenie aplikacji podczas transferu (użytkownik przenosi aplikację w tło, aby sprawdzić powiadomienie) oraz utrata sygnału w połowie transferu. Oba trzeba obsłużyć transferem wznawialnym: śledź ostatni potwierdzony offset, po ponownym połączeniu zapytaj urządzenie o jego bieżący offset i wznów od tego miejsca. Bez tego aktualizacje firmware'u dla całego bloku mieszkalnego zawodzą co sobotni poranek.

Obsługa błędów, której będziesz potrzebować

Każda kategoria błędu potrzebuje komunikatu dla użytkownika i akcji naprawczej. Zestaw minimalny:

  • Bluetooth wyłączony — przekieruj głębokim linkiem do Ustawień przez UIApplication.openSettingsURLString.
  • Odmowa uprawnień — wyjaśnij dlaczego i podlinkuj do Ustawień.
  • Urządzenie nieznalezione w skanie — zaoferuj 30-sekundową ponowną próbę i wskazówki (“czy urządzenie jest włączone, w promieniu 5 metrów”).
  • Przekroczenie limitu czasu połączenia — zaoferuj ponowną próbę; po trzech niepowodzeniach zasugeruj ponowne uruchomienie urządzenia.
  • Rozłączenie w połowie operacji — wyraźnie zasygnalizuj, spróbuj jednego automatycznego ponownego połączenia, potem poproś użytkownika.
  • Zapis nieudany z kodem błędu specyficznym dla charakterystyki — zmapuj na zrozumiały komunikat; nigdy nie pokazuj użytkownikom surowych opisów Error.

Strategia testowania

CoreBluetooth nie da się testować jednostkowo bezpośrednio, bo CBCentralManager wymaga prawdziwego radia. Czystym rozwiązaniem jest abstrakcja protokołu:

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 }

}

Opakuj prawdziwy CBPeripheral w klasę zgodną z Peripheral. Wstrzyknij typ protokołu do swoich modeli widoków. Teraz możesz pisać deterministyczne testy dla każdego przejścia stanu, każdej ścieżki błędu i całego przepływu OTA bez prawdziwego sprzętu. Prawdziwego sprzętu używaj do end-to-endowych testów dymnych na fizycznych urządzeniach oraz do wrażliwych na czas części OTA.

Pułapki zgłoszenia do App Store

Apple jest surowe w kwestii Bluetootha i zostaniesz odrzucony co najmniej raz, jeśli pominiesz cokolwiek z poniższych.

  • NSBluetoothAlwaysUsageDescription w Info.plist z jasnym, konkretnym powodem. “Ta aplikacja używa Bluetootha” zostaje odrzucone. “Ta aplikacja łączy się z Twoim urządzeniem FSS, aby sterować jego ustawieniami i odbierać dane czujnika” przechodzi.
  • Tryby pracy w tle zadeklarowane w UIBackgroundModes tylko jeśli faktycznie ich używasz. Zadeklarowanie bluetooth-central, gdy nigdy nie używasz BLE w tle, to powód odrzucenia.
  • Konto demonstracyjne lub film dla każdej funkcjonalności zależnej od urządzenia. Recenzent nie ma Twojego sprzętu. Krótkie nagranie ekranu pokazujące cały przepływ eliminuje tę wymianę zdań.
  • Etykiety prywatności (privacy nutrition labels) muszą dokładnie opisywać zbieranie danych pochodzących z BLE. Procent baterii sparowanego urządzenia jest zwykle w porządku; dane pochodne z lokalizacji wymagają deklaracji.
  • Uprzejme zachowanie przy wyłączonym Bluetooth — aplikacja nie może się zawieszać, musi pokazywać czytelny interfejs i nie może upierać się, że Bluetooth jest wymagany do uruchomienia.

Realny przykład: aplikacja towarzysząca ESP32

Powyższy wzorzec to dokładnie to, co dostarczyliśmy dla niedawnego monitora środowiskowego opartego na ESP32. Urządzenie udostępnia jedną niestandardową usługę z trzema charakterystykami: sterowanie (zapis), odczyty na żywo (powiadomienie), bateria (odczyt + powiadomienie). Parowanie zajmuje około 4 sekund w dobrych warunkach. Aplikacja obsługuje skanowanie w tle, aby wykryć powrót użytkownika do domu, i automatycznie łączy się ponownie. OTA dostarcza firmware o wielkości 740 KB w około 70 sekund z transferem wznawialnym. Zachowywanie stanu pozwala użytkownikowi otworzyć aplikację dwa dni później i zobaczyć dane na żywo w ciągu 200 ms, bo połączenie jest utrzymywane przez system.

Ta sama architektura skaluje się do aplikacji wielourządzeniowych (jacht z kilkoma połączonymi strefami), aplikacji mostkujących BLE do chmury (telefon działa jako brama, gdy urządzenie jest poza zasięgiem Wi-Fi) oraz aplikacji łączących sterowanie BLE z konfiguracją Wi-Fi. Wbudowujemy te wzorce zarówno w aplikacje towarzyszące iOS, jak i Android.

Najczęściej zadawane pytania

Którego frameworka iOS używam, aby łączyć się z urządzeniami BLE IoT?

Używasz frameworka CoreBluetooth firmy Apple. W aplikacji SwiftUI opakuj CBCentralManager i jego wywołania zwrotne delegatów w ObservableObject, aby stan połączenia i wartości charakterystyk reaktywnie napędzały Twoje widoki.

Czy aplikacja SwiftUI BLE może działać w tle?

Tylko częściowo. iOS zezwala na ograniczone BLE w tle przez tryb pracy w tle bluetooth-central, ale skanowanie jest ograniczane, UUID-y usług muszą być zadeklarowane i nie możesz polegać na ciągłych, częstych aktualizacjach, gdy aplikacja jest zawieszona.

Jak dostarczyć aktualizacje firmware'u OTA z aplikacji towarzyszącej SwiftUI?

Podziel binaria firmware'u na fragmenty, przesyłaj strumieniowo przez dedykowaną charakterystykę GATT z kontrolą przepływu i walidacją CRC, pokazuj czytelny postęp i obsługuj rozłączenia transferami wznawialnymi, aby uniknąć zablokowania (bricking) urządzenia.

Dostarcz aplikację towarzyszącą, która sprawia wrażenie natywnej

Świetna aplikacja towarzysząca BLE jest niewidoczna. Użytkownik dotyka przycisku, urządzenie odpowiada i nikt nie myśli o radiach, profilach GATT czy trybach pracy w tle. Dotarcie tam wymaga przemyślanej architektury: maszyny stanów, wrappera async, abstrakcji podatnych na mockowanie, starannej obsługi tła i szacunku dla listy kontrolnej recenzenta App Store. Każdy element jest prosty; dyscypliną jest trzymanie się ich wszystkich w terminie.

FSS buduje urządzenia podłączone kompleksowo: PCB, firmware, chmura oraz aplikacje na iOS i Android, których użytkownicy faktycznie dotykają. Jeśli szacujesz aplikację towarzyszącą dla istniejącego urządzenia lub projektujesz urządzenie wraz z aplikacją od pierwszego dnia, porozmawiaj z naszym zespołem sterowania mobilnego. Podzielimy się architekturami referencyjnymi, szacunkami harmonogramu i konkretnymi pułapkami App Store istotnymi dla Twojej kategorii.

{“@context”: “https://schema.org”, “@type”: “Article”, “headline”: “Aplikacja towarzysząca SwiftUI dla urządzeń BLE IoT: wzorce i pułapki”, “description”: “Budowa aplikacji towarzyszącej SwiftUI dla urządzeń BLE IoT oznacza opakowanie CoreBluetooth w obserwowalną maszynę stanów, respektowanie ograniczeń pracy w tle na iOS oraz zaprojektowanie solidnych przepływów parowania, aktualizacji OTA i obsługi błędów, aby aplikacja sprawiała wrażenie natywnej.”, “inLanguage”: “pl”, “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”: “Którego frameworka iOS używam, aby łączyć się z urządzeniami BLE IoT?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Używasz frameworka CoreBluetooth firmy Apple. W aplikacji SwiftUI opakuj CBCentralManager i jego wywołania zwrotne delegatów w ObservableObject, aby stan połączenia i wartości charakterystyk reaktywnie napędzały Twoje widoki.”}}, {“@type”: “Question”, “name”: “Czy aplikacja SwiftUI BLE może działać w tle?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Tylko częściowo. iOS zezwala na ograniczone BLE w tle przez tryb pracy w tle bluetooth-central, ale skanowanie jest ograniczane, UUID-y usług muszą być zadeklarowane i nie możesz polegać na ciągłych, częstych aktualizacjach, gdy aplikacja jest zawieszona.”}}, {“@type”: “Question”, “name”: “Jak dostarczyć aktualizacje firmware'u OTA z aplikacji towarzyszącej SwiftUI?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Podziel binaria firmware'u na fragmenty, przesyłaj strumieniowo przez dedykowaną charakterystykę GATT z kontrolą przepływu i walidacją CRC, pokazuj czytelny postęp i obsługuj rozłączenia transferami wznawialnymi, aby uniknąć zablokowania (bricking) urządzenia.”}}]}