Impulse AI Docs
Intern dokumentasjon
skipLink.label

iOS/KMP-utvikling

Denne guiden dekker iOS-utvikling i Impulse AI: KMP shared module, SwiftUI-moonstre, design-tokens og lokalisering.

KMP shared module

heading.anchorLabel

Kotlin Multiplatform (KMP) deler forretningslogikk mellom iOS og Android. Shared-modulen inneholder domenemodeller, store-er, API-klient og i18n.

shared/src/commonMain/kotlin/com/im/shared/
├── domain/ # Domenemodeller (Impulse, Entry, Insight, etc.)
├── store/ # FlowMVI stores (HomeStore, InsightsStore, etc.)
├── data/ # Repository-implementasjoner + API-klient
├── i18n/ # Strings.kt (moko-wrappere)
└── util/ # Delte verktoy

Bygg-kommandoer

heading.anchorLabel
Terminal window
# Rask kompileringssjekk (13s) -- bruk dette for validering
cd apps/mobile-kmp && ./gradlew :shared:compileKotlinIosSimulatorArm64
# Full shared-modul build (3+ min)
cd apps/mobile-kmp && ./gradlew :shared:build
# Rens og bygg pa nytt
cd apps/mobile-kmp && ./gradlew clean

Gradle-daemon er aktivert og gir 10-15s raskere bygg. Incremental compilation er konfigurert i gradle.properties:

ByggemodusTid
Forste build~15-20s
Inkrementelt~8-10s
Shared-kode~10-12s

Device-testing mot staging/production

heading.anchorLabel

iOS-appen bruker DEVELOPMENT (localhost) som standard. For a teste mot staging eller production fra fysisk enhet eller simulator, bruk Xcode scheme environment variables.

  1. Xcode → Product → Scheme → Edit Scheme (⌘<)
  2. Run → Arguments → Environment Variables
  3. Legg til:
NameValueResultat
APP_ENVIRONMENTSTAGINGapi-stg.impulseai.app + Supabase staging
APP_ENVIRONMENTPRODUCTIONapi.impulseai.app + Supabase production
APP_ENVIRONMENTDEVELOPMENTTilbake til localhost (fjerner override)
  1. Bygg og kjor (⌘R)

Slik fungerer det

heading.anchorLabel

Valget persisteres i UserDefaults — appen husker miljoet selv etter at du kobler fra Xcode eller stopper debuggeren. Du trenger kun a sette variabelen en gang.

Tilbakestilling: Sett APP_ENVIRONMENT=DEVELOPMENT og kjor en gang, eller slett appen fra enheten.

Fil: ImpulseApp.swift (init) — leser ProcessInfo → fallback til UserDefaults.

Miljo-oversikt

heading.anchorLabel
MiljoAPI URLSupabase
Developmenthttp://localhost:3001Lokal (localhost:54331)
Staginghttps://api-stg.impulseai.appconxpuqktkeiphnnpfiv.supabase.co
Productionhttps://api.impulseai.appjksqfeutntcvukrisrno.supabase.co

Forutsetninger for staging-autentisering

heading.anchorLabel

For at Apple Sign In skal fungere mot staging:

  1. Supabase staging → Authentication → Providers → Apple:

    • Client ID: com.digiteers.impulseai
    • Secret Key: Gyldig Apple JWT (samme som production)
    • Callback URL: https://conxpuqktkeiphnnpfiv.supabase.co/auth/v1/callback
  2. Doppler stg ma ha:

    • SUPABASE_URL — Supabase staging URL
    • SUPABASE_SECRET_KEY — Service role key
    • API_CLIENT_KEY — Matcher verdien i Environment.kt
    • NODE_ENV=production — Aktiverer API-key middleware
  3. Railway staging custom domain aktiv: api-stg.impulseai.app

SwiftUI + @Observable-regler

heading.anchorLabel

Kritisk regel: aldri .map KMP-typer

heading.anchorLabel

Bruk KMP-typer (shared.Impulse, shared.Entry, etc.) direkte i SwiftUI views. Aldri transformer med .map { SwiftWrapper(...) } — dette bryter @Observable-tracking og forhindrer UI-oppdateringer.

// FEIL -- bryter observation:
private var entries: [EntryDisplayData] {
entriesStore.entries.map { EntryDisplayData(...) }
}
// RIKTIG -- observation fungerer:
private var entries: [shared.Entry] {
entriesStore.entries
}

Tillatte operasjoner pa KMP-typer:

  • .filter { } — returnerer samme objekter
  • .prefix() / .suffix() — OK
  • .sorted { } — OK
  • Direkte tilgang til store-properties — OK

Forbudte operasjoner:

  • .map { NewType(...) } — bryter observation
  • Wrapper-typer som EntryDisplayData — unnga

KMP-typer med id-property trenger extension for sheet(item:):

extension shared.Entry: Identifiable {}

State Sync Pattern

heading.anchorLabel

Nar en view holder en lokal @State-kopi av et store-objekt, synkroniser med en enkelt onChange:

// EN onChange -- updatedAt endres ved ALLE server-side felt-mutasjoner
.onChange(of: storeObject?.updatedAt) { _, _ in
if let updated = storeObject {
localCopy = updated
}
}

Aldri legg til separate onChange-handlere for individuelle felter. updatedAt-timestampet dekker alle mutasjoner.

Derived View State Pattern

heading.anchorLabel

Nar en SwiftUI-views rendering avhenger av flere @Observable-stores, bruk en computed enum — aldri if-else-kjeder i view body.

Monster (2 filer per skjerm)

heading.anchorLabel
// 1. FooState.swift -- enum med alle mulige display-tilstander
enum FooContentState {
case loading
case empty
case content
case error(String)
}
// 2. FooView.swift -- computed property utleder tilstand fra stores
private var contentState: FooContentState {
if store.isLoading && !store.hasData { return .loading }
if let error = store.error, !store.hasData { return .error(error) }
if !store.hasData { return .empty }
return .content
}
// Exhaustive switch -- kompilatoren hjelper
switch contentState {
case .loading: ProgressView()
case .empty: EmptyState()
case .content: ContentView()
case .error(let msg): ErrorView(msg)
}

Never-Fetched = Loading-konvensjonen

heading.anchorLabel
if store.isLoading || (!store.hasData && store.lastFetched == nil) {
return .loading
}

lastFetched == nil betyr at data aldri ble hentet — vis spinner, ikke tom tilstand. KMP RetryableLoadingState-grensesnittet gir lastFetched: Long? pa tvers av alle stores.

Eksisterende bruk

heading.anchorLabel
ViewEnumFil
ImpulseDetailViewSessionSectionStateFeatures/Impulses/SessionSectionState.swift
InsightsViewInsightsContentStateFeatures/Insights/InsightsContentState.swift
JourneyViewJourneyContentStateFeatures/Journey/JourneyContentState.swift
UnifiedSessionViewSessionLayerDefinert inline

Accent Surface Token System

heading.anchorLabel

Designsystemet bygger pa en aksent-farge med fire opasitetsniva:

Fargehjelpere (Colors.swift)

heading.anchorLabel
extension Color {
func accentSurface() -> Color { self.opacity(0.08) } // Kort-bakgrunner
func accentInteractive() -> Color { self.opacity(0.10) } // Ghost CTA-er
func accentBadge() -> Color { self.opacity(0.15) } // Status-piller, tags
// 1.00 -- direkte farge for tekst/ikoner
}

ViewModifiers (AccentSurface.swift)

heading.anchorLabel
ModifierFunksjon
.imCard(accent:, padding:, cornerRadius:)Kort med padding + maxWidth + accentSurface-bakgrunn. nil accent -> backgroundSecondary
.imGhostCTA(accent:)Ghost-CTA med foregroundColor + accentInteractive-bakgrunn
.imBadge(accent:)Status-pille med accentBadge-bakgrunn
.imSheet(dismiss:)Sheet med backgroundPrimary + toolbar xmark.circle.fill

Aksentfarge-valg

heading.anchorLabel
KontekstAksent
Kontraksjons-impuls/sesjon.classContraction
Ekspansjons-impuls/sesjon.classExpansion
Brand/noytral (insights, stats, home).brand
Ingen kontekst (profil, tab bar)nil -> backgroundSecondary

CTA-hierarki (2 niva)

heading.anchorLabel
TierStilJobbBruk
Invite.imGhostCTA(accent:)BrukerhandlingerStart sesjon, fang impuls, fortsett, los
ProgressSolid SessionColors.primaryTextSesjonsnavigasjonSesjon-intro, steg-knapper, Done

Toolbar Close-knapper

heading.anchorLabel

Alle sheets og modale skjermer bruker:

Button { dismiss() } label: {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.textMetadata)
}

Eksklusjonsliste (forblir backgroundSecondary)

heading.anchorLabel
  • ProfileView list-rader
  • CustomTabBar
  • JourneyFilterChips / GroupingToggle
  • Editor-overflater (ContractionEditor, ExpansionEditor, NoteEditor)
  • AIClassificationLoadingView gradient
  • HomeEmptyStates

Session Design Tokens

heading.anchorLabel

Sesjons-spesifikke tokens bygger pa accent surface-systemet:

  • SessionColors.cardBackground(for:) — returnerer aksenttintet kortfarge basert pa sesjonsklassifisering
  • SessionColors.backgroundGradient — sesjonsgradient-bakgrunn
  • Sesjonskomponenter mottar en accent: Color-parameter (standard: .brand)

Fil: iosApp/Features/Sessions/Shared/SessionDesignTokens.swift

Store-kommunikasjonsarkitektur

heading.anchorLabel

Appen bruker tre distinkte kommunikasjonsmekanismer:

MekanismeJobbRetningEksempel
@Observable stateReaktive UI-oppdateringerStore -> View (automatisk)Loading, lister, feil
CallbacksNavigasjonsside-effekterStore -> View (imperativ)Apne fullScreenCover, dismiss
AppEventBusCross-store-koordineringStore -> Store (KMP)“Analyse ferdig” -> HomeStore refresher

Callback-regler

heading.anchorLabel

Callbacks pa store-wrappere er strengt for navigasjonseffekter som ikke kan uttrykkes via @Observable-state.

  1. Tilordne i onAppear, rydd i onDisappear — forhindre foreldede callbacks
  2. Aldri legg til callbacks for state-endringer — bruk @Observable store-properties
  3. Singleton = siste skriver vinner — kun en view kan eie en callback om gangen

Gold standard: UnifiedSessionView.setupStoreCallbacks() + cleanupIfNeeded().

Moko Strings — trippel oppdatering

heading.anchorLabel

Ny lokalisert streng krever oppdatering i 3 filer:

1. strings.xml (base + nb) -- moko source of truth (snake_case)
2. Strings.kt -- KMP camelCase wrapper
3. LocalizedString.swift -- iOS Swift wrapper (L.propertyName)
FilSti
Engelskshared/src/commonMain/moko-resources/base/strings.xml
Norskshared/src/commonMain/moko-resources/nb/strings.xml
KMP-wrappershared/src/commonMain/kotlin/com/im/shared/i18n/Strings.kt
iOS-wrapperiosApp/Utilities/LocalizedString.swift
// iOS
L.sessionHelloWorld
// Android
mokoStringResource(MR.strings.session_hello_world)
BeskrivelseSti
FargetokensiosApp/Theme/Colors.swift
TypografiiosApp/Theme/Typography.swift
SpacingiosApp/Theme/Spacing.swift
ViewModifiersiosApp/Components/Modifiers/AccentSurface.swift
Press-effektiosApp/Components/Modifiers/PressEffect.swift
SesjonstokensiosApp/Features/Sessions/Shared/SessionDesignTokens.swift
Lokaliseringstrings.xml -> Strings.kt -> LocalizedString.swift
Store-wrappereiosApp/Stores/
KMP store provideriosApp/Stores/KMPStoreProvider.swift

SKIE og SwiftUI Preview

heading.anchorLabel

SKIE (Swift/Kotlin Interface Enhancer) genererer forskjellige Swift-typer for simulator vs device:

TargetKotlin Double?Kotlin String i ForEach
Simulator (arm64-simulator)Double?Fungerer direkte
Device (arm64)KotlinDouble?Kan gi generics-feil

1. #Preview ma wrappes i simulator-guard:

#if targetEnvironment(simulator)
#Preview {
MyView(data: shared.SomeKMPType(...))
}
#endif

Uten dette feiler device-builds fordi preview-data bruker simulator-spesifikke typer.

2. ForEach med KMP-lister — bruk enumerated():

// Robust — fungerer pa tvers av targets
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
ItemView(item: item)
}
// Fragilt — kan feile pa device
ForEach(items, id: \.self) { item in ... }

3. KotlinDouble i preview-data:

// Simulator: bruk plain Double
shared.AreaAssignment(relevanceScore: 0.95)
// Device: krever KotlinDouble
shared.AreaAssignment(relevanceScore: KotlinDouble(double: 0.95))
// Losning: wrap preview i #if targetEnvironment(simulator)