Fra online backend til fuld offline: Sådan embedder jeg en opdateret ord-database i min Bedrager-app til iOS og Android
Du kender det sikkert: Du sidder i et tog med “E” på signalet, eller i et fly hvor alt internet er et fjernt minde—og lige dér får du lyst til at spille noget sjovt med vennerne. Det var præcis den situation, der fik mig til at gentænke arkitekturen bag min nye Android/iOS-app Bedrager: et party game, hvor spillere skal gætte et hemmeligt ord, mens impostors prøver at blende ind uden at afsløre sig selv.
I starten byggede jeg en klassisk løsning med en online backend til at hente nye ord. Men jo mere jeg testede og tænkte over brugsscenarierne, jo mere stod én ting klart: offline er ikke en “nice to have”—det er en kernefeature for et party game.
Hvad er Bedrager (kort fortalt)?
Bedrager er et socialt gættespil med en lille twist:
- Alle spillere får et hemmeligt ord
- ... men én spiller er bedragere (impostor) og får enten et andet ord eller et “hint”
- Gruppen forsøger til sidst at afsløre, hvem der lyver...
Det lyder simpelt—men det er super sjovt!
Den første version: Online backend for ord (fordele og ulemper)
Jeg startede med en model, hvor appen ved spilstart kontaktede en backend og hentede ord og spørgsmål.
Fordele
- Nemt at opdatere indhold uden at udgive ny version
- Central styring af ordlister, kategorier, sprog osv.
- Mulighed for A/B-tests og dynamiske events (fx “sommerpakke”, “julepakke”)
Ulemper
- Appen bliver afhængig af internet:
no connection = no fun - Dårlig oplevelse ved spotty signal (tog, festivaler, kældre…)
- Højere kompleksitet omkring caching, sync-fejl og “hvad hvis vi er midt i et spil?”
Tip: Party games bliver ofte spillet netop dér, hvor internet er ustabilt. Offline er ikke bare teknik—det er brugeroplevelse.
Drejningen: Fuldt offline—men stadig opdateret indhold
Jeg ville have to ting på én gang:
- Fuld offline support (alt indhold lokalt)
- En måde at sikre, at hver build stadig har nyeste ord og spørgsmål
Så jeg byggede i stedet:
- En backend, hvor jeg kan administrere ord og content.
- En lille macOS Swift-app, som kører ved build-time, henter data, bygger en offline database og embedder den i appen
Resultatet: Hver gang jeg trykker “build” i Xcode eller Android Studio, ender jeg med en app, der allerede indeholder det nyeste indhold — og er klar-parat til at spille, også uden internet 🦖
Arkitekturen: Backend → Build-step → Embedded offline database
Den overordnede pipeline ser sådan ud:
- Jeg opdaterer indhold i backend (ord, kategorier, spørgsmål, hints osv.)
- En macOS build-helper henter data fra backend via API
- Den bygger en offline database (SQLite) baseret på data fra APIet.
- Appen læser lokalt — ingen Internet for at spille
Hvorfor en macOS Swift-app?
Fordi det passede super godt til mit setup:
- Jeg bygger i forvejen i Xcode på macOS (og i Android Studio på macOS)
- Swift er hurtigt til at lave små værktøjer med
URLSession, filhåndtering og database-generering - Det kan integreres som et build step, så den ikke bliver glemt 🤪
Features
- Instant game start: Ingen ventetid på netværk
- Spil hvor som helst: Fly, metro, sommerhus, udlandet uden roaming osv.
- Forudsigelig oplevelse: Samme data for alle spillere i samme build
Ulemper / trade-offs
- Større app-bundle (ord fylder lidt)
- Indholds-opdateringer kræver en ny build
- Mere build-kompleksitet: generator, validering, integration i iOS/Android
- Risiko for “stale content”, hvis jeg bliver træt af at lave nye builds 🤪
Praktisk eksempel: Hent data fra backend og byg en lokal JSON/DB
Her er et forsimplet eksempel på, hvordan en Swift build-helper kan hente ord fra en backend og skrive dem til en fil, der senere kan konverteres til SQLite.
Swift: Download og skriv til fil
import Foundation
struct WordItem: Codable {
let id: String
let category: String
let word: String
}
func fetchWords(apiURL: URL, outputURL: URL) async throws {
var request = URLRequest(url: apiURL)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw NSError(domain: "BuildTool", code: 1, userInfo: [NSLocalizedDescriptionKey: "Bad response"])
}
let words = try JSONDecoder().decode([WordItem].self, from: data)
// Skriv prettified JSON for nem debugging (kan senere importeres til SQLite)
let encoded = try JSONEncoder().encode(words)
try encoded.write(to: outputURL, options: [.atomic])
}
Eksempel: Build step i Xcode med et script
Hvis din build-helper er en CLI (eller du kan lave en lille wrapper), kan du kalde den i en Run Script Phase:
"${SRCROOT}/Tools/BedragerContentBuilder" \
--api "https://v1.api.local/words" \
--out "${SRCROOT}/App/Resources/offline.db"
Konklusion: Offline-first var den rigtige beslutning for Bedrager
At gå fra “online content” til en løsning hvor en macOS Swift build-app genererer og embedder en offline database har været en kæmpe forbedring for Bedrager. Spillet føles mere stabilt, mere øjeblikkeligt og—vigtigst—det virker der, hvor party games faktisk bliver spillet.
Der er nogle ekstra tandhjul i build-processen, ja. Men når det betyder, at man kan starte et spil i et tog med dårlig forbindelse og stadig have nyeste sjov med i appen, så er det svært at argumentere imod.
/db over-n-out