Saltar al contenido
K
Favoritos con RoomPaso a paso40 min Intermedio

Favoritos en iOS

Al terminar: Añadir la barra de pestañas y el corazón consumiendo el mismo Flow de favoritos.

Ahora le toca a iOS, y el mismo FavoritesViewModel que alimenta Android va a alimentar SwiftUI sin tocar una línea de Kotlin de lógica. Al terminar tendrás dos pestañas, el corazón en la tarjeta y una pantalla de favoritos con los gestos de Apple: deslizar para quitar y un diálogo para vaciar.

La mayor parte es Swift que ya conoces. Lo propio de Kotlin Multiplatform es el puente: una pieza de Kotlin pequeña y un conjunto de números (Set<Int>) que llega a Swift envuelto y hay que desenvolver una vez. Y lo nuevo de SwiftUI son dos gestos del sistema, .swipeActions y .confirmationDialog.

El concepto · Un conjunto de números que llega en cajas

Android consulta favoriteIds, un Set<Int> que FavoritesViewModel ya tiene: el conjunto de los números de los Pokémon marcados («saber si el 25 está» es una pregunta que un conjunto contesta al instante). En Swift ese conjunto llega como un Set<KotlinInt>: no es un Set<Int32>, es un conjunto de objetos KotlinInt, una clase que Kotlin inventa para envolver cada número.

Un ejemplo solo para leer (no lo escribas en el proyecto), con datos ajenos a la Pokédex: los números de los libros prestados de una biblioteca. KotlinInt solo existe dentro del framework de tu proyecto, así que aquí se usa NSNumber, la caja del sistema de la que KotlinInt hereda: se comporta igual.

ejemplo mínimo: abrir las cajas de un conjunto (con comentarios)
import Foundation
 
// Lo que llega de Kotlin: un conjunto de cajas, cada una con un número dentro.
let prestados: Set<NSNumber> = [NSNumber(value: Int32(3)), NSNumber(value: Int32(7)), NSNumber(value: Int32(9))]
 
// El número del libro que buscamos, como lo guarda Swift: un `Int32` suelto.
let libro: Int32 = 7
 
// Se desenvuelve UNA vez: de cada caja se saca su número (`int32Value`)
// y se arma un conjunto nuevo, ya de números sueltos.
let enSwift: Set<Int32> = Set(prestados.map { $0.int32Value })
 
print(enSwift.contains(libro))   // ¿está el 7?
print(enSwift.contains(5))       // ¿está el 5?
print(enSwift.sorted())          // los tres, de menor a mayor

Al ejecutarlo verás:

true
false
[3, 7, 9]

Si preguntaras al conjunto de cajas con el número suelto, prestados.contains(libro), Swift se negaría a compilar (cannot convert value of type 'Int32' to expected argument type 'NSNumber'): un Int32 no es una caja. Lo razonable es desenvolver el conjunto una sola vez, cuando llega: es lo que hará el paso 3.

El concepto · Dos gestos que pone el sistema

En Android, quitar deslizando obligó a dibujar el fondo de la fila, y las opciones de «Quitar todos» a montar una hoja inferior. En iOS son dos modificadores:

  • .swipeActions se pone sobre una fila de una List y dice qué botones asoman al deslizarla. El botón sale con su icono mientras se desliza, y puedes poner varios por lado.
  • .confirmationDialog es un diálogo que pide confirmar antes de algo que no se puede deshacer. Se muestra cuando un Bool se pone en verdadero. Lleva dos bloques de llaves: el primero, los botones (cada uno con su role), y el segundo, el que empieza por message:, el texto que va bajo el título. Un botón con role: .destructive es más que un color: el sistema lo pinta en rojo, lo coloca donde toca en cada tamaño de pantalla y lo anuncia como destructivo.

Otro ejemplo solo para leer, con una lista de tareas ajena a la Pokédex:

ejemplo mínimo: deslizar para borrar y confirmar antes de vaciar (con comentarios)
import SwiftUI
 
struct TareasView: View {
    @State private var tareas = ["Comprar pan", "Llamar al médico", "Regar las plantas"]
    @State private var confirmando = false   // ¿se ve el diálogo de «Borrar todas»?
 
    var body: some View {
        NavigationStack {
            List {
                ForEach(tareas, id: \.self) { tarea in
                    Text(tarea)
                        // Al deslizar la fila hacia la izquierda asoma este botón,
                        // con su icono. `edge: .trailing` es el borde final (a la derecha
                        // en español) y `role: .destructive` lo pinta en rojo.
                        .swipeActions(edge: .trailing) {
                            Button("Borrar", systemImage: "trash", role: .destructive) {
                                tareas.removeAll { $0 == tarea }
                            }
                        }
                }
            }
            .navigationTitle("Tareas")
            // `.toolbar` rellena la barra superior con botones.
            .toolbar {
                Button("Borrar todas", systemImage: "trash") { confirmando = true }
            }
            // Un diálogo que pide confirmar antes de algo que no se puede deshacer.
            // Se ve mientras `confirmando` sea verdadero; el sistema lo apaga al elegir.
            .confirmationDialog("¿Borrar todas las tareas?", isPresented: $confirmando, titleVisibility: .visible) {
                Button("Borrar todas", role: .destructive) { tareas.removeAll() }
                Button("Cancelar", role: .cancel) { }
            } message: {
                Text("No se pueden recuperar.")
            }
        }
    }
}
 
#Preview { TareasView() }

En la vista previa, deslizar una fila hacia la izquierda enseña el botón rojo «Borrar», y el icono de la papelera de la barra abre el diálogo con «Borrar todas» en rojo. En iOS 26 el diálogo sale como un globo flotante arriba, cerca de la papelera, sin «Cancelar» (se cierra tocando fuera); en versiones anteriores sale abajo como una hoja, con «Cancelar» debajo. Es lo que montará el paso 5, con los favoritos.

Los ficheros de hoy

shared/src/iosMain/…/feature/favorites/FavoritesIos.ktse crea (paso 1)
iosApp/iosApp
ContentView.swiftse modifica (paso 4)
List
PokemonCardView.swiftse modifica (paso 2)
PokemonListView.swiftse modifica (paso 3)
Favorites/FavoritesView.swiftse crea (paso 5)
Uno de Kotlin y otro de Swift se crean; tres de Swift cambian. El ViewModel compartido no cambia.

Paso 1 · El puente, en Kotlin

El archivo. FavoritesIos.kt, en iosMain (el source set que solo compila para iOS). Es el equivalente, para favoritos, de los puentes de El mismo ViewModel en iOS: una función que pide el ViewModel a Koin y una propiedad anotada por cada flujo. Créalo: clic derecho sobre la carpeta shared → New → File y escribe la ruta desde la carpeta del módulo (si las carpetas no existen, el diálogo las crea):

src/iosMain/kotlin/com/sebastiannarvaez/pokedex/feature/favorites/FavoritesIos.kt

Kotlin mete las funciones sueltas de un fichero en una clase con el nombre del fichero más Kt: por eso, desde Swift, esta función será FavoritesIosKt.favoritesViewModel(…). Escribe primero la función y el flujo del estado:

shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/feature/favorites/FavoritesIos.kt
package com.sebastiannarvaez.pokedex.feature.favorites
 
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
import kotlinx.coroutines.flow.StateFlow
import org.koin.mp.KoinPlatform
 
fun favoritesViewModel(owner: ViewModelStoreOwner): FavoritesViewModel {
    val koin = KoinPlatform.getKoin()   // el contenedor de Koin ya arrancado
    return ViewModelProvider.create(
        owner = owner,              // quien guarda el ViewModel (lo pasa SwiftUI)
        factory = viewModelFactory { initializer { koin.get<FavoritesViewModel>() } },
    )[FavoritesViewModel::class]
}
 
@NativeCoroutinesState   // genera uiStateForIos (valor) y uiStateForIosFlow (para for await)
val FavoritesViewModel.uiStateForIos: StateFlow<FavoritesUiState>
    get() = uiState

Y a continuación, al final del mismo fichero, la segunda pieza, la que sirve los identificadores:

shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/feature/favorites/FavoritesIos.kt
/**
 * Los identificadores de los favoritos. Swift los recibe como `Set<KotlinInt>`
 * y los convierte a `Set<Int32>` una vez por cambio.
 */
@NativeCoroutinesState
val FavoritesViewModel.favoriteIdsForIos: StateFlow<Set<Int>>
    get() = favoriteIds

No hay conversión en Kotlin: el conjunto cruza en cajas y se abre en Swift (paso 3).

Paso 2 · El corazón, que iOS anima solo

El archivo. PokemonCardView.swift es la tarjeta de la rejilla, de La lista en iOS. Ábrelo en Xcode (en el Project navigator, Cmd+1: iosApp → iosApp → List → PokemonCardView.swift). Añade dos propiedades debajo de let pokemon: Pokemon:

iosApp/iosApp/List/PokemonCardView.swift
    let esFavorito: Bool
    let alMarcar: () -> Void

La tarjeta recibe el estado (esFavorito) y avisa hacia fuera cuando tocan el corazón (alMarcar), igual que la de Android: no sabe nada de favoritos. Después, dentro del ZStack de la tarjeta, justo debajo del .padding(14) de la imagen, hay ahora mismo este bloque, el número:

Text(String(format: "N.º %04d", pokemon.id))
    .font(.caption2.weight(.semibold))
    .foregroundStyle(.secondary)
    .padding(12)

Bórralo entero (el Text y sus tres modificadores, .padding(12) incluido) y, en su lugar, pon esta fila, que lleva el número a la izquierda y el corazón a la derecha, y que ya trae su propio .padding(12) al final:

iosApp/iosApp/List/PokemonCardView.swift
                HStack {
                    Text(String(format: "N.º %04d", pokemon.id))
                        .font(.caption2.weight(.semibold))
                        .foregroundStyle(.secondary)
                    Spacer()
                    // El corazón con su propia zona táctil: marcar no debe
                    // abrir la ficha.
                    Button(action: alMarcar) {
                        Image(systemName: esFavorito ? "heart.fill" : "heart")
                            .font(.subheadline)
                            .foregroundStyle(esFavorito ? Color.red : Color.secondary)
                            .contentTransition(.symbolEffect(.replace))
                    }
                    .buttonStyle(.plain)
                    .accessibilityLabel(esFavorito ? "Quitar de favoritos" : "Añadir a favoritos")
                }
                .padding(12)

.contentTransition(.symbolEffect(.replace)) anima el paso de contorno a relleno con la transición de SF Symbols; en Compose habría que escribir la animación a mano.

.buttonStyle(.plain) quita el aspecto de botón del sistema; sin él, el corazón heredaría el azul y el resalte del botón que envuelve la tarjeta.

.accessibilityLabel cambia con el estado, como la contentDescription de Android: un símbolo que pasa de contorno a relleno no dice nada a quien no lo ve (VoiceOver, el lector de pantalla de iOS, lee esa etiqueta). En el simulador no puedes encenderlo; se comprueba con Accessibility Inspector, en el paso 6.

Pero hay un detalle. La tarjeta entera lleva .accessibilityElement(children: .ignore) (lo pusiste en La lista en iOS): VoiceOver la ve como un único elemento, «Bulbasaur, tipo Planta y Veneno», y no llega al corazón de dentro. Para que alguien que no ve la pantalla pueda marcar favoritos, la tarjeta ofrece el corazón como una acción de accesibilidad: una orden con nombre que VoiceOver enseña al deslizar arriba o abajo sobre el elemento y que se activa con doble toque. Añádela al final de la tarjeta, justo debajo de su .accessibilityLabel(…) (el que lee el nombre y los tipos, al cierre del body):

iosApp/iosApp/List/PokemonCardView.swift
        .accessibilityLabel(
            // … el nombre y los tipos, tal como está …
        )
        // El corazón, como acción de la tarjeta: llama a lo mismo que el botón.
        .accessibilityAction(named: Text(esFavorito ? "Quitar de favoritos" : "Añadir a favoritos")) { alMarcar() }

.accessibilityAction(named:) recibe el nombre de la orden (el texto que se oye) y, entre llaves, lo que hace al activarla: aquí, alMarcar(), lo mismo que el botón.

Mientras no actualices la lista (paso 3), Xcode marcará error en la llamada a PokemonCardView: es lo esperado.

Paso 3 · La lista: Swift abre las cajas una sola vez

El archivo. PokemonListView.swift (iosApp → iosApp → List). Tres cambios. Primero, dos estados nuevos junto a los que ya tiene (@State es el estado propio de una vista, como en @State). favoritos guarda el ViewModel una vez pedido, y idsFavoritos el conjunto ya convertido:

iosApp/iosApp/List/PokemonListView.swift
    @State private var favoritos: FavoritesViewModel?
    @State private var idsFavoritos: Set<Int32> = []

Segundo, el camino de la pila (la lista de fichas abiertas) deja de ser propio de la lista y se recibe de fuera. Quien lo posee ahora es ContentView (paso 4), que es la vista que tiene las dos pestañas, igual que en Android la pila la posee PokedexApp y no cada pantalla; la lista lo comparte con un @Binding. La línea @State private var camino: [Int32] = [] pasa a ser:

iosApp/iosApp/List/PokemonListView.swift
    @Binding var camino: [Int32]

Tercero, una .task más (junto a las que ya hay: cada una recoge un flujo distinto), con la conversión en la línea que importa:

iosApp/iosApp/List/PokemonListView.swift
        .task {
            // `owner` ya lo tiene esta vista desde La lista en iOS: guarda el ViewModel.
            let vm = favoritos ?? FavoritesIosKt.favoritesViewModel(owner: owner)
            favoritos = vm
            do {
                for try await ids in asyncSequence(for: vm.favoriteIdsForIosFlow) {
                    idsFavoritos = Set(ids.map { $0.int32Value })
                }
            } catch {
                // Cancelar no es fallar.
            }
        }

FavoritesIosKt es la clase del paso 1. ids es un Set<KotlinInt>, y ids.map { $0.int32Value } es el desempaquetado del ejemplo de la biblioteca. Ocurre una vez por emisión, no una vez por celda; después idsFavoritos es un Set<Int32> nativo. Y la tarjeta recibe lo mismo que en Android, con nombres de Swift (sustituye PokemonCardView(pokemon: pokemon)):

iosApp/iosApp/List/PokemonListView.swift
                            PokemonCardView(
                                pokemon: pokemon,
                                esFavorito: idsFavoritos.contains(pokemon.id),
                                alMarcar: { favoritos?.toggle(id: pokemon.id, name: pokemon.name) }
                            )

Qué pasaría si no lo abrieras. Con el conjunto tal cual (Set<KotlinInt>), la asignación compilaría, pero fallaría idsFavoritos.contains(pokemon.id) con el error del ejemplo de la biblioteca (cannot convert value of type 'Int32' to expected argument type 'KotlinInt'). La regla general: los números que viajan dentro de colecciones como Set o List llegan envueltos; desenvuélvelos una sola vez al entrar en Swift.

Paso 4 · TabView, y cada pestaña con su pila

El archivo. ContentView.swift (iosApp → iosApp) hasta ahora era una línea: PokemonListView(). Como la lista ahora pide su camino, ContentView lo guarda y monta las dos pestañas con el TabView que ya conoces. Sustituye el struct ContentView por:

iosApp/iosApp/ContentView.swift
/// Las dos secciones de la app.
///
/// `TabView` es el contenedor de iOS: la barra la pinta el sistema. En Android
/// hay que colocar la barra, decidir cuándo se ve y gestionar la pila a mano.
///
/// Cada pestaña lleva **su propio camino** (un array de identificadores que
/// guarda este `ContentView`), que es el comportamiento que el usuario de iOS
/// espera: volver a Favoritos te devuelve donde estabas.
struct ContentView: View {
 
    @State private var caminoLista: [Int32] = []      // fichas abiertas en la pestaña Pokédex
    @State private var caminoFavoritos: [Int32] = []  // fichas abiertas en la pestaña Favoritos
 
    var body: some View {
        TabView {
            PokemonListView(camino: $caminoLista)
                .tabItem { Label("Pokédex", systemImage: "square.grid.2x2") }
 
            FavoritesView(camino: $caminoFavoritos)
                .tabItem { Label("Favoritos", systemImage: "heart.fill") }
        }
    }
}

Hasta el paso 5, Xcode marcará error en FavoritesView, porque aún no existe: es lo esperado.

Paso 5 · Los favoritos, con los gestos de Apple

El archivo. FavoritesView.swift es la pantalla de la pestaña Favoritos. Va en una carpeta nueva, Favorites, junto a List. Créala desde la Terminal, en la carpeta de tu proyecto: mkdir iosApp/iosApp/Favorites (mkdir crea una carpeta). Xcode la muestra sola (la carpeta iosApp está sincronizada con el disco, como viste en Ejecutar en iOS). Después, en el Project navigator, clic derecho sobre Favorites → New File from Template… → Swift File → Next, nombre FavoritesView y Create.

Es una lista como las que ya hiciste en Marcador, con el estado que publica el ViewModel de Kotlin. Escribe primero la cabecera y el cuerpo, que montan el NavigationStack, la barra con el botón de la papelera, «Vaciar» (el .confirmationDialog del ejemplo de las tareas) y la tarea que recoge el estado:

iosApp/iosApp/Favorites/FavoritesView.swift
import SwiftUI
import Shared
import KMPNativeCoroutinesAsync
 
/// Los favoritos en iOS.
///
/// Los dos gestos son los de Apple y no se parecen a los de Android: acciones
/// al deslizar con `.swipeActions`, que además dejan asomar el botón con su
/// icono, y un diálogo de confirmación en vez de una hoja inferior, porque en
/// iOS lo destructivo se confirma con un `confirmationDialog`.
struct FavoritesView: View {
 
    // Quien guarda el ViewModel de Kotlin mientras la vista viva (ver
    // «El mismo ViewModel en iOS»): @StateObject lo crea una sola vez.
    @StateObject private var owner = IosViewModelStoreOwner()
    @State private var estado = FavoritesUiState(favoritos: [], cargando: true, quitado: nil)
    @State private var viewModel: FavoritesViewModel?
    @State private var confirmandoVaciar = false   // ¿se ve el diálogo de «Vaciar»?
    @Binding var camino: [Int32]                    // el camino de esta pestaña, de ContentView
 
    var body: some View {
        NavigationStack(path: $camino) {
            contenido
                .navigationDestination(for: Int32.self) { id in
                    PokemonDetailView(pokemonId: id)
                }
                .navigationTitle("Favoritos")
                .toolbar {                          // los botones de la barra superior
                    if !estado.favoritos.isEmpty {   // solo hay «Vaciar» si hay algo que vaciar
                        ToolbarItem(placement: .topBarTrailing) {   // a la derecha
                            Button("Vaciar", systemImage: "trash") { confirmandoVaciar = true }
                        }
                    }
                }
                // Un diálogo que pide confirmar antes de algo destructivo.
                .confirmationDialog(
                    "¿Quitar todos los favoritos?",
                    isPresented: $confirmandoVaciar,
                    titleVisibility: .visible
                ) {
                    Button("Quitar todos", role: .destructive) { viewModel?.vaciar() }
                    Button("Cancelar", role: .cancel) { }
                } message: {
                    Text("Se guardan solo en este dispositivo y no se pueden recuperar.")
                }
        }
        .task {
            let vm = viewModel ?? FavoritesIosKt.favoritesViewModel(owner: owner)
            viewModel = vm
            estado = vm.uiStateForIos
            do {
                for try await nuevo in asyncSequence(for: vm.uiStateForIosFlow) {
                    estado = nuevo
                }
            } catch {
                // Cancelar no es fallar.
            }
        }
    }

estado = vm.uiStateForIos lee de golpe el valor actual del StateFlow (la propiedad sin Flow, que @NativeCoroutinesState también genera) para que la pantalla no empiece en blanco; después el for try await va recibiendo los cambios. El FavoritesUiState de Swift se construye con todos los parámetros, también con quitado: nil: los valores por defecto de Kotlin no cruzan (se explica en el paso «La trampa: los argumentos por defecto no cruzan» de Corrutinas en commonMain), y sin ese argumento Xcode no compila. El campo quitado lo creaste en Quitar con deshacer para el aviso de Android; en iOS no se usa, y la nota de más abajo explica por qué.

Sigue, dentro del mismo struct, el contenido: carga, estado vacío (sin dibujarlo a mano) o la lista. @ViewBuilder es el atributo que permite que una propiedad devuelva vistas distintas según un if.

iosApp/iosApp/Favorites/FavoritesView.swift
    @ViewBuilder
    private var contenido: some View {
        if estado.cargando {
            ProgressView()
        } else if estado.vacia {
            ContentUnavailableView(
                "Todavía no hay favoritos",
                systemImage: "heart",
                description: Text("Toca el corazón de cualquier Pokémon para guardarlo aquí.")
            )
        } else {
            List {
                ForEach(estado.favoritos, id: \.id) { favorito in
                    Button {
                        camino.append(favorito.id)
                    } label: {
                        HStack(spacing: 16) {
                            AsyncImage(url: URL(string: favorito.artworkUrl)) { imagen in
                                imagen.resizable().scaledToFit()
                            } placeholder: {
                                Color.clear
                            }
                            .frame(width: 52, height: 52)
 
                            VStack(alignment: .leading, spacing: 2) {
                                Text(favorito.name.capitalized)   // `capitalized`: primera letra en mayúscula
                                    .font(.system(.headline, design: .rounded))
                                Text(String(format: "N.º %04d", favorito.id))
                                    .font(.caption2)
                                    .foregroundStyle(.secondary)
                            }
                        }
                    }
                    .buttonStyle(.plain)
                    // `.swipeActions` deja ver el botón con su icono mientras
                    // se desliza, y permite varias acciones por lado. En
                    // Android el equivalente pinta un fondo y hay que dibujarlo.
                    .swipeActions(edge: .trailing) {
                        Button("Quitar", systemImage: "heart.slash", role: .destructive) {
                            viewModel?.toggle(id: favorito.id, name: favorito.name)
                        }
                    }
                }
            }
            .listStyle(.plain)
        }
    }
}

El role: .destructive es el de los dos ejemplos. En Android, la hoja inferior de Pestañas en Android tuvo que declarar el color a mano.

Paso 6 · Comprobar

Antes de ejecutar, compila una vez (Product → Build, Cmd+B) para que Xcode regenere el framework Shared con FavoritesIosKt; si no, FavoritesIosKt saldrá como «cannot find». Después ejecuta (Cmd+R) en el simulador. Marca dos Pokémon con el corazón, ve a Favoritos y desliza una fila hacia la izquierda: aparece «Quitar». Toca la papelera de la barra: en iOS 26 sale un globo flotante arriba, cerca de la papelera, con el título «¿Quitar todos los favoritos?», el mensaje y «Quitar todos» en rojo, y sin «Cancelar» (se cierra tocando fuera); en iOS 17 y 18 sale abajo como una hoja, con «Cancelar» debajo. En iOS 26 la barra enseña «Vaciar» solo como el icono de la papelera.

VoiceOver no existe en el simulador. El corazón se comprueba con Accessibility Inspector: en Xcode, Xcode → Open Developer Tool → Accessibility Inspector; arriba a la izquierda elige el simulador, pulsa el botón del cuadrado con la flecha (inspeccionar) y pasa el ratón sobre una tarjeta. La tarjeta es un solo elemento y el corazón llega como acción, así que en la sección Actions debe verse «Añadir a favoritos» o «Quitar de favoritos» según esté. En un iPhone real, VoiceOver se enciende en Ajustes → Accesibilidad.

Punto de control

  • Las dos pestañas se ven y la barra la pinta el sistema.
  • Marcar un favorito en la lista lo hace aparecer en la pestaña de Favoritos, y quitarlo allí deslizando lo desmarca en la lista.
  • El corazón anima el cambio de contorno a relleno, y en Accessibility Inspector, sobre la tarjeta, la sección Actions dice «Quitar de favoritos» cuando está marcado (VoiceOver no está en el simulador).
  • En Favoritos, toca una fila (se abre su ficha), cambia a la pestaña Pokédex y vuelve a Favoritos: la ficha sigue abierta.
  • Deslizar una fila de Favoritos la quita (sin aviso de «Deshacer»: eso es de Android), y la papelera de la barra («Vaciar») pide confirmación antes de quitar todos.
  • Marcados dos o tres, detén la app (Stop) y vuelve a ejecutarla (Cmd+R): siguen marcados, porque la base se guardó en el iPhone simulado.
  • Los tests siguen igual (./gradlew :shared:jvmTest :shared:testAndroidHostTest :shared:iosSimulatorArm64Test, sin contar los de la carpeta ejercicios): 51 en la JVM, 40 en Android host y 42 en iOS. Esta lección no añade ninguno.

Comprueba lo que entendiste

7 preguntas · necesitas 5 para completar la lección

  1. 1.¿Cómo llega a Swift un StateFlow<Set<Int>> de Kotlin?

  2. 2.Para consultar idsFavoritos.contains(pokemon.id) con un Int32, ¿qué se hace en la lección?

  3. 3.En Swift, ¿dónde ocurre Set(ids.map { $0.int32Value })?

  4. 4.¿Qué diferencia hay entre el TabView de iOS y la barra inferior que se montó en Android?

  5. 5.¿Qué aporta role: .destructive en el botón del confirmationDialog?

  6. 6.En Marcador cada pestaña era Tab("…", systemImage: …) { … }. ¿Por qué esta app las marca con .tabItem { Label(…) }?

  7. 7.En Android, al quitar un favorito sale un aviso con «Deshacer». ¿Por qué la pantalla de iOS no lo enseña?

Faltan 7 de 7