# Gap Android → iOS: allineamento app nativa Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un’app **allineata ad Android `2.0.5-native`**. **Aggiornato:** 24 luglio 2026 **Riferimento Android (produzione / telefono / Play):** `2.0.5-native` (`versionCode` **26**), API `https://www.matchlivetv.it` **Stato iOS attuale:** marketing `2.0.0`, build `21` — i18n **solo Login parziale** Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast) + bump versione, **senza** i bug di sessione/crash già risolti su Android 2.0.5. --- ## Contesto Su Android (rilasciato) la lingua scelta dall’utente vale su **tutta l’app** usabile: - Persistenza + apply live (`AppLocale` / SharedPreferences `mltv_locale` + `ProvideAppLocale`) - Icona **globo** su Login (TopAppBar) e Matches - Hub Partite localizzato - Bottom sheet e dialog Matches (es. «Choose team», nuova partita, programma, elimina, riprendi) - Wizard 3 step + branding colori - Broadcast overlay, snackbar, dialog fine set/partita - ~230+ chiavi × 5 lingue in `res/values*` Su iOS esistono già `AppLanguage` + `LanguagePickerView` + Login localizzato, ma **Matches/sheet/wizard/broadcast restano in italiano hardcoded**. Va replicato il lavoro Android in SwiftUI. API produzione: entrambe le app puntano già a `https://www.matchlivetv.it` — **nessun gap di endpoint**. --- ## Lezioni Android 2.0.5 (obbligatorie su iOS) Queste sono regressioni/bug già visti su Android; **non ripeterli** su iOS. | Problema | Sintomo | Fix Android (da replicare in spirito) | |----------|---------|----------------------------------------| | Recreate Activity al cambio lingua | Torna al Login / perde nav | **Niente recreate** obbligatorio: aggiorna UI via stato lingua + refresh stringhe | | Context “nudo” per le stringhe | Crash wizard: `No ActivityResultRegistryOwner` / picker foto | Mantieni **Activity** come host; localizza solo Resources / environment | | `findActivity()` su context configurazione | Cambio lingua non applica (apply silent no-op) | Applica lingua con `applicationContext` / store globale, non dipendere da Activity dal dialog | | Globe sotto lo scroll del form Login | Tap ignorato | Globe in **toolbar/top bar**, non overlay sotto `ScrollView`/`Column` | | Splash che richiede rete per restare loggato | Dopo cambio lingua → Login se API lenta | Se token locale presente → resta autenticato; refresh rete best-effort | ### Pattern Android di riferimento (Compose) 1. `AppLocale.tagFlow` + prefs `mltv_locale` 2. `ProvideAppLocale`: `LocalConfiguration` + `LocalizedContextWrapper(Activity, localizedResources)` → stringhe aggiornate **e** `LocalActivityResultRegistryOwner` integro 3. `LanguagePickerDialog` chiama `AppLocale.apply(context, tag)` (applicationContext), non `findActivity()` obbligatorio 4. Splash: `currentSession() != null` → Matches; `validateOrRefresh()` in background ### Equivalente SwiftUI consigliato 1. `AppLanguage` / `@AppStorage` + `NotificationCenter` (già presente) o `environment(\.locale, …)` 2. Root: `.environment(\.locale, locale)` **e** `.id(languageTick)` sulle schermate che mostrano testo 3. **Non** distruggere `NavigationStack` / session store al cambio lingua 4. Photo picker / `PhotosPicker` / `UIImagePicker`: restano agganciati alla `View` host, non a un context “fake” 5. Cold start: se token in Keychain/UserDefaults → vai a Matches anche se `/me` fallisce temporaneamente --- ## Stato a confronto (target = Android 2.0.5) | Area | Android 2.0.5 | iOS oggi | Da fare su iOS | |------|---------------|----------|----------------| | Persistenza lingua | SharedPreferences + `ProvideAppLocale` (no recreate nav) | UserDefaults + notification | Refresh UI su **tutte** le schermate **senza** reset sessione/nav | | Picker Login | Icona globo in **TopAppBar** | Testo / picker presente | Allineare a **icona globo** in toolbar | | Picker Matches | Icona globo top bar | Presente | OK se già icona; altrimenti allineare | | Login stringhe | Sì | Sì (`L10n`) | OK | | Slogan wordmark | `app_slogan` | Chiave `app.slogan` **non usata** | Collegare `L10n.t("app.slogan")` | | Hub Matches | Sì | Quasi tutto IT fisso | Localizzare con chiavi `matches.*` | | Sheet/dialog Matches | Sì (`sheet_*`) | IT fisso | Localizzare (priorità: scegli squadra) | | Status badge partita | `match_status_*` | IT | Localizzare | | Snackbar hub | `matches_msg_*` | IT | Localizzare | | Wizard 3 step | Sì (`wizard_*`) | IT | Localizzare | | Branding / color picker | Sì | IT | Localizzare | | Broadcast + score dialog | Sì (`broadcast_*`, `score_*`) | IT | Localizzare | | Catalogo stringhe | `values` + `values-en/fr/de/es` (~230 chiavi) | ~14 chiavi in `AppLanguage.swift` | Espandere `L10n` (o migrare a `Localizable.xcstrings`) | | Logout UI | Icona | Testo | Icona SF Symbol | | Versione | `2.0.5-native` / **26** | `2.0.0` / **21** | Bump a **2.0.5** / **26** (o build successiva se conflitto TestFlight) | | RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK in prod; allineare solo semantica dev se serve | --- ## File di riferimento ### Android (sorgente di verità — già fatto) | Ruolo | Path | |-------|------| | Locale + ProvideAppLocale | `native/android/.../core/AppLocale.kt` | | Picker | `native/android/.../ui/components/LanguagePickerDialog.kt` | | Activity | `native/android/.../MainActivity.kt` | | Splash (sessione locale) | `native/android/.../ui/splash/SplashScreen.kt` | | Stringhe | `native/android/app/src/main/res/values/strings.xml` + `values-{en,fr,de,es}/` | | Login | `.../ui/login/LoginScreen.kt` | | Hub | `.../ui/matches/MatchesScreen.kt` | | Sheet/dialog | `.../ui/matches/MatchSheets.kt` | | Wizard | `.../ui/wizard/*.kt` | | Broadcast | `.../ui/broadcast/*.kt` | | Versione | `native/android/app/build.gradle.kts` | ### iOS (da aggiornare) | Ruolo | Path | |-------|------| | Lingua + L10n | `native/ios/MatchLiveTv/Core/AppLanguage.swift` | | Login | `native/ios/MatchLiveTv/UI/Login/LoginScreen.swift` | | Hub | `native/ios/MatchLiveTv/UI/Matches/MatchesScreen.swift` | | Sheet/dialog Matches | file sheet/alert equivalenti sotto `UI/Matches/` (cercare stringhe IT) | | Wizard | `UI/Wizard/` (o path equivalente) | | Broadcast | `UI/Broadcast/` (o path equivalente) | | Wordmark | `native/ios/MatchLiveTv/UI/Components/MatchLiveWordmark.swift` | | Auth / splash equivalent | entry root / session restore | | Versioni | `generate_xcodeproj.py`, `project.pbxproj`, `Info.plist` | | Build | `scripts/build_ios_release.sh` | --- ## Piano di lavoro iOS (replicare Android — ordine obbligato) ### Fase A — Infrastruttura stringhe (prima di tutto) 1. Aprire Android `values/strings.xml` e `values-en|fr|de|es/strings.xml` come catalogo. 2. Portare in `L10n.table` (o meglio `Localizable.xcstrings`) **tutte** le chiavi usate da: - `matches_*`, `sheet_*`, `match_status_*`, `matches_msg_*` - `wizard_*`, `wizard_color_hue` - `broadcast_*`, `score_*` - `login_*`, `language_*`, `action_*`, `app_slogan`, `common_*` se presenti 3. Convenzione chiavi iOS: `matches.hello` ↔ Android `matches_hello` (punto al posto di `_`). 4. Placeholder: Android `%1$s` → Swift `"Ciao, \(name)"` o `String(format: L10n.t("matches.hello"), name)`. ### Fase B — Login + wordmark (rapido) 5. Login: icona globo in toolbar (come Android TopAppBar), non overlay che lo scroll può coprire. 6. `MatchLiveWordmark`: `L10n.t("app.slogan")`. 7. Cambio lingua: aggiorna stringhe **senza** buttare fuori l’utente (se già loggato altrove, stessa regola). ### Fase C — Hub Matches + sheet/dialog (priorità UX — screenshot «Scegli squadra») 8. `MatchesScreen.swift`: ogni `Text("…")` hardcoded → `L10n.t(...)`. 9. Sheet/dialog equivalenti a `MatchSheets.kt`: - scegli squadra → `sheet.choose_team` - nuova partita / programma / avvia subito - scegli partita, badge BOZZA/PROGRAMMATA - riprendi diretta, continua setup - configura ora?, elimina partita 10. Snackbar/alert hub → `matches.msg_*`. 11. Badge status riga partita → `match.status_*`. 12. Assicurare `.id(languageTick)` (o environment) su root Matches **e** sheet presentati. ### Fase D — Wizard 13. Localizzare step Partita / Trasmissione / Test rete + branding editor + color hue (`wizard.*`). 14. Errori validazione e CTA AVANTI/INDIETRO/INIZIA. 15. Verificare photo/logo picker dopo cambio lingua (su Android crashava senza Activity host). ### Fase E — Broadcast 16. Overlay controlli, contentDescription, snackbar pausa/ripresa/regia. 17. Dialog termina diretta, set concluso, chiudi set, partita terminata (`broadcast.*`, `score.*`). ### Fase F — Sessione + versione e release 18. Cold start / cambio lingua: token locale → resta in Matches (non forzare Login se `/me` fallisce). 19. Bump `MARKETING_VERSION` → `2.0.5`, `CURRENT_PROJECT_VERSION` → `26` (o successivo se conflitto TestFlight). 20. Aggiornare `generate_xcodeproj.py` **e** `Info.plist`, poi `python3 generate_xcodeproj.py`. 21. `./scripts/build_ios_release.sh` (API default già produzione). --- ## Mapping chiavi — gruppi da copiare da Android Sorgente: `native/android/app/src/main/res/values*/strings.xml`. | Prefisso Android | Uso | Priorità | |------------------|-----|----------| | `language_*`, `login_*`, `app_slogan`, `action_*` | Login / chrome | Alta | | `matches_*` | Hub | Alta | | `sheet_*`, `match_status_*`, `matches_msg_*` | Sheet/dialog hub | **Alta** (bug «Scegli squadra» in EN) | | `wizard_*` | Wizard | Alta (stessa sessione utente dopo hub) | | `broadcast_*`, `score_*` | Diretta | Alta | | `streaming_notification_*` | Solo Android FGS | N/A su iOS | Non lasciare fuori scope wizard/broadcast: su Android **sono già tradotti**; iOS deve seguirli per parità. --- ## Verifica manuale su Mac (acceptance) 1. `cd native/ios && python3 generate_xcodeproj.py` 2. Build simulatore; login produzione. 3. Login: globo → English → label/errori/slogan in EN. 4. **Loggato:** cambio lingua da Matches → resti su Matches, testi EN (niente ritorno al Login). 5. Matches: saluto, CTA, empty state in EN. 6. Apri «Change» / scegli squadra: titolo **«Choose team»** (non «Scegli squadra»). 7. Nuova partita → Avvia ora → wizard si apre **senza crash**; step in EN. 8. Wizard 3 step in EN; logo picker funziona. 9. (Se possibile) overlay diretta: dialog/termina in EN. 10. Kill app → riapri: lingua persistita **e** sessione resta se token valido. 11. Spot-check FR/DE/ES. 12. Release: `./scripts/build_ios_release.sh` Test: ```bash cd native/ios python3 generate_xcodeproj.py xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \ -destination 'platform=iOS Simulator,name=iPhone 17' \ -derivedDataPath build/DerivedData \ ONLY_ACTIVE_ARCH=YES ARCHS=arm64 EXCLUDED_ARCHS=x86_64 \ test -only-testing:MatchLiveTvTests ``` --- ## Note API / produzione | | Android | iOS | |--|---------|-----| | Default | BuildConfig / `-PAPI_BASE_URL` | `API_BASE_URL` → Info.plist | | Fallback | `https://www.matchlivetv.it` | idem | | REST / Cable | `/api/v1`, `wss://…/cable` | idem | Nessuna modifica server richiesta. ### Artefatti Android Play (riferimento) ```bash # APK installazione diretta ./scripts/build_native_android_apk_prod.sh # AAB per Google Play Console cd native/android && ./gradlew bundleRelease -PAPI_BASE_URL=https://www.matchlivetv.it # → app/build/outputs/bundle/release/app-release.aab ``` Versione corrente store: **`2.0.5-native` / versionCode `26`**. --- ## Sintesi priorità (ordine di esecuzione) 1. **A** Catalogo stringhe completo (copiare da Android `values*`) 2. **C** Hub + sheet/dialog (fix «Scegli squadra» in EN) 3. **B** Slogan + globo Login (toolbar) 4. **D** Wizard (+ no crash picker) 5. **E** Broadcast 6. **F** Sessione stabile al cambio lingua + versione `2.0.5` / `26` + build release Quando A–F sono chiusi, iOS è allineato ad Android **2.0.5-native** per UX multilingua, stabilità sessione e versioning store.