Impulse AI Docs
Intern dokumentasjon
Hopp til innhold

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

Text Style Conventions

heading.anchorLabel

All tekststyling i appen er konvensjonsdrevet gjennom 18 modifiers definert i TextStyles.swift. Ingen rå .font() på Text-elementer — all tekst bruker en navngitt konvensjon.

heading.anchorLabel
im + TextRole [+ Variant]

Prefiks im, etterfulgt av tekstens rolle (ikke utseende). Varianter som suffix: .imBodyLight(), ikke .imLightBody().

Innholdshierarki

heading.anchorLabel
ModifierSpesifikasjonRolle
.imHeroTitle(size:)28pt bold, textPrimary, sentrertStore titler (onboarding, splash, completion)
.imSectionTitle()title3 (20pt) bold, textPrimaryPrimære seksjonsoverskrifter, tier-navn
.imCardTitle()subheadline (15pt) semibold, textPrimaryKort-titler (“Your Insight”, “Mental”)
.imSectionLabel()subheadline semibold, textSecondary, leadingSeksjonsetiketter (“Analysis”, “Releases”)
.imBody()16pt regular, textPrimary, 4pt lineSpacingAll lesetekst
.imBodyLight()17pt light, textSecondary, sentrertOnboarding/tour-tekst
.imMetadata()caption (12pt), textMetadataTidsstempler, tall, hints
.imLegalText()caption2 (11pt), textMetadataJuridisk tekst, fine print
ModifierSpesifikasjonRolle
.imToolbarTitle()headline (17pt) semibold, textPrimaryNavigasjonsbar-titler
.imTabLabel(isSelected:)subheadline, semibold/regular basert på stateTabs, pickers, filter chips
.imActionLabel()subheadline mediumInline CTAer, text-linker
.imNavButton()16pt semiboldSolid CTA-knapper

Dekorativ/display

heading.anchorLabel
ModifierSpesifikasjonRolle
.imDisplayNumber()40pt regular, textPrimaryStore metrikk-tall
.imBadgeLabel()caption2 (11pt) mediumPill/badge-tekst
.imProductLabel()footnote (13pt) semibold, uppercaseProdukt-header titler
ModifierSpesifikasjonRolle
.imGuidanceTitle()20pt semibold, expanded trackingInspirasjonskort step-navn
.imGuidanceBody()18pt light, sentrert, spacious lineSpacingMeditasjonstekst
.imErrorBanner()callout (16pt), textPrimaryFeilmeldinger

Regler for lesetekst (.imBody)

heading.anchorLabel

All tekst der formålet er å lese og forstå innholdet bruker .imBody():

  • Alltid textPrimary (hvit). Avvik fra dette er å style tekst for et annet formål enn lesing.
  • Alltid regular vekt. Bold/semibold er reservert for headings, labels og CTAer.
  • 16pt (KMP bodyLarge token). Cross-platform konsistens med Android.
// Lesetekst — bruker alltid .imBody()
Text(insight.narrative)
.imBody()
// Støttetekst — bruker .imMetadata() eller .imSectionLabel()
Text("April 2, 2026")
.imMetadata()

Fargekonvensjon for modifiers

heading.anchorLabel

Noen modifiers setter farge, andre ikke. Konvensjonen:

Setter fargeSetter IKKE farge (sett separat)
.imBody() → textPrimary.imBadgeLabel() — badge-farge varierer
.imMetadata() → textMetadata.imTabLabel() — state-avhengig
.imSectionLabel() → textSecondary.imActionLabel() — typisk .brand
.imHeroTitle() → textPrimary.imNavButton() — typisk .textOnColor
.imErrorBanner() → textPrimary.imGuidanceTitle() — SessionColors
.imLegalText() → textMetadata.imGuidanceBody() — SessionColors
.imToolbarTitle() → textPrimary.imProductLabel() — aktiv/inaktiv

Når du trenger en annen farge enn konvensjonens default, legg til .foregroundColor() etter modifieren.

Line Spacing Tokens

heading.anchorLabel

Definert i Typography.swift som IMLineSpacing:

TokenVerdiBruk
.tight3ptKompakt caption-tekst i korte kort
.body4ptStandard brødtekst
.comfortable6ptOnboarding, lett tekst
.spacious8ptGuidance, affirmations

Tracking Tokens

heading.anchorLabel

Definert i Typography.swift som IMTracking:

TokenVerdiBruk
.subtle0.5ptSeksjon-headere
.wide2ptSplash-undertitler, completion-labels
.expanded4ptSplash-titler, session step-labels
BeskrivelseSti
Text-konvensjoner (18 modifiers)iosApp/Components/Modifiers/TextStyles.swift
Line Spacing + Tracking tokensiosApp/Theme/Typography.swift
Layout-constraintsIMConstraints i TextStyles.swift

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
Typografi + LineSpacing + TrackingiosApp/Theme/Typography.swift
SpacingiosApp/Theme/Spacing.swift
Tekstkonvensjoner (18 modifiers)iosApp/Components/Modifiers/TextStyles.swift
OverflatemodifiersiosApp/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)