Saltar al contenido
K
iOS con SwiftUIPaso a paso40 min de lectura · 6 min de máquina Básico

SwiftUI: primeros pasos

Al terminar: Escribir vistas con struct View, body, modifiers y stacks.

Al terminar, Marcador tendrá su pantalla principal en SwiftUI: el título, una tarjeta con «Rayo FC 2 - 1 Toros» y dos botones de gol; y una pestaña aparte, «Laboratorio», que recoge el botón de UIKit de la lección anterior. Los botones todavía no hacen nada: la pantalla es estática, como lo fue la primera pantalla de Compose.

Importa porque toda la interfaz de iOS que escribas a partir de aquí se hace con estas piezas: una vista es un struct y los stacks colocan los elementos. El orden de los modificadores, que cambia lo que se pinta, es el tema de la lección siguiente.

Ya has visto varias piezas de SwiftUI sin explicación, en la pantalla de La app, sus escenas y el paquete Torneo: VStack apila sus hijos en vertical; Text pinta texto; .font(…) cambia la tipografía de lo que tiene delante; .padding() añade aire alrededor. Los métodos con punto que se escriben detrás de una vista, como .font(…) o .padding(), se llaman modificadores: devuelven una vista nueva con ese cambio. Esta lección explica todo a fondo.

Una vista es un struct que describe

SwiftUI es el sistema de interfaces declarativo de Apple. Declarativo significa que no dices «cambia esta etiqueta a 2-1», sino «la pantalla, para estos datos, se ve así». Cuando los datos cambian, SwiftUI vuelve a pedir la descripción y actualiza lo que cambió.

Una vista es un struct que cumple el protocolo View. Un protocolo es una lista de cosas que un tipo promete tener (en Kotlin, una interfaz; lo viste en Protocolos y extensiones). View pide una sola: la propiedad body, que devuelve la descripción de la pantalla.

some View significa «un tipo concreto que es una vista, que el compilador conoce y tú no quieres escribir». El tipo real de un body con stacks y modificadores es larguísimo; some lo esconde. Como cada vista es un struct, crearla es barato: SwiftUI las crea y las descarta todo el rato.

ejemplo mínimo: una vista (con comentarios)
struct Saludo: View {                  // una vista: un struct que cumple View
    let nombre: String                 // dato de entrada
 
    var body: some View {              // lo único que View exige: qué se ve
        Text("Hola, \(nombre)")        // un texto con el dato dentro
            .font(.title)              // un modificador: tipografía de título
    }
}
ComposeSwiftUI
@Composable fun Saludo(nombre: String)struct Saludo: View { let nombre: String; var body: some View { … } }
Modifiermodificadores: .padding(), .font()
Column / Row / BoxVStack / HStack / ZStack

La vista previa

Una vista se puede ver sin ejecutar la app. Se escribe debajo de ella un bloque #Preview { … }, una vista previa, y Xcode dibuja la vista junto al código:

ejemplo mínimo: una vista previa
#Preview {
    Saludo(nombre: "Ana")     // Xcode dibuja esta vista junto al código
}

Se muestra y se oculta en Editor → Canvas (Opción+Cmd+Intro); si el panel dice que está en pausa, pulsa Resume. Si no la ves, no pasa nada: Cmd+R lo enseña igual.

Stacks y layout

Un stack es un contenedor que coloca a sus hijos en fila: uno tras otro, de arriba abajo, de izquierda a derecha o unos sobre otros.

ContenedorColoca los hijosEquivale en Compose
VStackEn verticalColumn
HStackEn horizontalRow
ZStackUnos encima de otros (el último, delante)Box
  • spacing: el hueco entre hijos, como Arrangement.spacedBy.
  • alignment: cómo se alinean en la otra dirección: en un VStack, en horizontal (.leading es a la izquierda, .center en el centro, .trailing a la derecha).
  • Spacer(): ocupa todo el espacio libre y empuja al resto.
  • .frame(maxWidth: .infinity, alignment: .leading): hace que una vista ocupe todo el ancho disponible, con su contenido pegado a un lado. En una fila con tres elementos, dos con esto y uno en medio, el del medio queda centrado.

Otras piezas que usarás:

  • let partido: Partido = Ejemplo.rayoContraToros: un dato de entrada fijo. Una let en una vista es solo eso: no cambia y no provoca que la pantalla se actualice. Partido y Ejemplo son del paquete Torneo, de ahí el import Torneo.
  • Button("Gol …") {}: un botón con título; la lambda vacía {} es la acción, que todavía no hace nada. .buttonStyle(.borderedProminent) los pinta destacados; al ir después del HStack, se aplica a los dos.
  • .font(.title2.bold()): una tipografía del sistema (.title2, .title3, .headline…) encadenada con la negrita. .font(.system(size: 40, weight: .bold, design: .rounded)) y .monospacedDigit(): una tipografía a medida y cifras del mismo ancho, para que el marcador no baile.
  • .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)): un fondo translúcido (.regularMaterial es un fondo semitransparente del sistema) con esquinas redondeadas (cornerRadius es el radio de la curva).
  • partido.golesLocal y partido.golesVisitante: los goles de cada equipo, propiedades de Partido del paquete.

Una regla que hace falta ya: los modificadores se leen de arriba abajo, y cada uno envuelve lo que tiene delante. Por eso el .background va después del .padding: así el fondo pinta también el aire. En la lección siguiente, El orden de los modificadores, lo ves a fondo.

Todas las piezas en otra pantalla, una del tiempo (no es tu código; solo para leerla o probarla en un fichero aparte):

ejemplo mínimo: stacks, Spacer, frame, una tipografía a medida y un fondo redondeado (con comentarios)
import SwiftUI
 
struct PantallaDelTiempo: View {
    let ciudad = "Cali"                          // un dato de entrada fijo: no cambia
 
    var body: some View {
        VStack(spacing: 16) {                    // en vertical, 16 puntos entre hijos
            Text("El tiempo")
                .font(.title2.bold())            // tipografía de título, en negrita
            HStack {                             // en horizontal: tres textos en fila
                Text(ciudad)
                    .frame(maxWidth: .infinity, alignment: .leading)    // ocupa lo que sobra, pegado a la izquierda
                Text("28°")
                    .font(.system(size: 40, weight: .bold, design: .rounded))   // tipografía a medida
                    .monospacedDigit()           // cifras del mismo ancho
                Text("Soleado")
                    .frame(maxWidth: .infinity, alignment: .trailing)   // pegado a la derecha: el 28° queda en medio
            }
            .padding(20)                         // aire dentro de la tarjeta
            .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))   // fondo translúcido redondeado
            HStack {
                Text("Mín 19°")                  // pegado a la izquierda...
                Spacer()                         // ...el hueco que sobra...
                Text("Máx 31°")                  // ...y esto, pegado a la derecha
            }
            HStack(spacing: 8) {
                Button("Ayer") {}                // {} : la acción, vacía por ahora
                Button("Mañana") {}
            }
            .buttonStyle(.borderedProminent)     // después del HStack: vale para los dos botones
            Spacer()                             // empuja todo lo anterior hacia arriba
        }
        .padding(16)
    }
}

La pantalla se ve así, de arriba abajo: «El tiempo» en negrita; una tarjeta translúcida con «Cali» a la izquierda, «28°» grande en medio y «Soleado» a la derecha; una fila con «Mín 19°» pegado a la izquierda y «Máx 31°» a la derecha; y los botones «Ayer» y «Mañana» destacados, todo arriba de la pantalla.

Reto · Ejercicio: la ficha de un jugador

Crea la subcarpeta FichaDeJugador dentro de Ejercicios (como en El ciclo de vida de UIViewController) y, dentro, el fichero FichaDeJugador.swift. Escribe una vista que reciba un nombre, un dorsal y una posición y muestre:

  • arriba, el nombre en tipografía de título y en negrita;
  • debajo, en una fila, «Dorsal» y el número pegados a la izquierda y la posición pegada a la derecha;
  • todo con 16 puntos de aire dentro, un fondo translúcido de esquinas redondeadas (radio 12) que cubra ese aire, y aire por fuera.

Añade al final una vista previa con Ana, dorsal 9, delantera. Debe verse una tarjeta con:

Ana
Dorsal 9                Delantera
Ver solución
MarcadorApp/Ejercicios/FichaDeJugador/FichaDeJugador.swift
import SwiftUI
 
// La ficha de un jugador: el nombre arriba y, debajo, el dorsal a la izquierda y la posición a la derecha.
struct FichaDeJugador: View {
    let nombre: String
    let dorsal: Int
    let posicion: String
 
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(nombre)
                .font(.title2.bold())
            HStack {
                Text("Dorsal \(dorsal)")
                Spacer()            // empuja la posición a la derecha
                Text(posicion)
            }
        }
        .padding(16)
        .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
        .padding()
    }
}
 
#Preview {
    FichaDeJugador(nombre: "Ana", dorsal: 9, posicion: "Delantera")
}

El VStack alinea a la izquierda el nombre y la fila; el Spacer de la fila se come el hueco y empuja la posición a la derecha. El primer .padding(16) es el aire de dentro, el fondo va después para cubrirlo, y el último .padding() es el aire de fuera.

Trabajas en ~/Marcador-Swift/MarcadorApp. Los ficheros nuevos se crean con clic derecho sobre la carpeta que te indique cada paso (dentro de MarcadorApp, la que contiene MarcadorApp.swift, no el icono azul) → New File from Template… → Swift File → Next, el nombre y Create. Si el menú de tu versión de Xcode se llama distinto, usa la Terminal: mkdir -p de la carpeta y touch del fichero dentro de ~/Marcador-Swift/MarcadorApp/MarcadorApp/, y Xcode lo muestra solo.

Paso 1 · Tu primera vista

Objetivo: una vista propia, PantallaMarcadorEstatica, que ocupa el lugar de la pantalla de la lección anterior y enseña por ahora solo el título.

MarcadorApp (proyecto)
MarcadorApp
Marcador
UI
PantallaMarcadorEstatica.swiftse crea
ContentView.swiftse modifica
En el Project navigator: un fichero nuevo y otro que cambia.

PantallaMarcadorEstatica.swift será la pantalla principal de Marcador, escrita en su propio fichero en vez de dentro de ContentView. «Estática» porque por ahora no cambia con nada, como lo fue la primera pantalla de Compose.

Antes del fichero, sus carpetas. Una funcionalidad es una parte de la app con nombre propio; la del marcador se llama Marcador. Clic derecho sobre la carpeta MarcadorApp (la que contiene MarcadorApp.swift, no el icono azul) → New Folder, de nombre Marcador. Después, clic derecho sobre Marcador → New Folder, de nombre UI: es la carpeta de lo que se ve en pantalla. Por último, clic derecho sobre UI → New File from Template… → Swift File → Next, escribe PantallaMarcadorEstatica y pulsa Create. Desde la Terminal: mkdir -p ~/Marcador-Swift/MarcadorApp/MarcadorApp/Marcador/UI && touch ~/Marcador-Swift/MarcadorApp/MarcadorApp/Marcador/UI/PantallaMarcadorEstatica.swift.

Intenta escribir en ese fichero la vista más pequeña posible: un título. Cuando termines, abre el bloque.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Marcador/UI/PantallaMarcadorEstatica.swift
import SwiftUI   // SwiftUI: el sistema de interfaces moderno de Apple
 
// Una vista es un `struct` que cumple el protocolo `View`.
struct PantallaMarcadorEstatica: View {
    // `body` describe la pantalla para los datos actuales. Es lo único que `View` exige.
    var body: some View {
        Text("Marcador")        // un texto
            .font(.headline)    // un modificador: cambia la tipografía de lo que tiene delante
    }
}
 
// La vista previa: Xcode dibuja la vista junto al código, sin ejecutar la app.
#Preview {
    PantallaMarcadorEstatica()
}

Ahora que la pantalla nueva existe, ContentView deja de dibujar el partido y pasa a mostrarla. Sustituye ContentView.swift entero:

ContentView.swift: muestra la pantalla nueva
import SwiftUI
 
struct ContentView: View {
    var body: some View {
        PantallaMarcadorEstatica()   // la pantalla vive en su propio fichero
    }
}
 
#Preview {
    ContentView()
}

Ejecuta con Cmd+R: la app enseña «Marcador» en tipografía de título, centrado en la pantalla (una vista sola en la raíz se centra). El botón de la hoja de UIKit desaparece de esta pantalla; vuelve, en otra pestaña, al final de esta lección.

Paso 2 · Stacks y la tarjeta del partido

Objetivo: el título, una tarjeta con «Rayo FC 2 - 1 Toros» y dos botones de gol en fila. Los botones todavía no hacen nada.

MarcadorApp (proyecto)
MarcadorApp
Marcador
UI
PantallaMarcadorEstatica.swiftse modifica
Se modifica el fichero del paso anterior.

Intenta escribirlo tú, con el partido de ejemplo del paquete y las piezas de arriba:

  1. PantallaMarcadorEstatica: en columna, el título «Marcador» en tipo de cabecera, la tarjeta del partido, una fila con dos botones destacados, «Gol Rayo FC» y «Gol Toros» (con los nombres de los equipos), que todavía no hacen nada, y abajo todo el espacio libre, para que lo anterior quede arriba.
  2. TarjetaDePartido, una segunda vista, struct, en el mismo fichero y debajo de la primera (un fichero puede tener varias vistas), que recibe el partido: en fila, el nombre del local pegado a la izquierda, el marcador «2 - 1» grande, en negrita, redondeado y con cifras del mismo ancho en el centro, y el visitante pegado a la derecha; con aire alrededor y un fondo translúcido de esquinas redondeadas que cubre también ese aire.

Lo que debes ver: «Marcador», una tarjeta con «Rayo FC», «2 - 1» y «Toros», y debajo los dos botones. Cuando termines, abre el bloque y compara.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
PantallaMarcadorEstatica.swift: versión final
import SwiftUI
import Torneo   // de aquí salen `Partido` y `Ejemplo`
 
struct PantallaMarcadorEstatica: View {
    // Un dato de entrada fijo: el partido de ejemplo del paquete.
    let partido: Partido = Ejemplo.rayoContraToros
 
    var body: some View {
        // `VStack` apila en vertical; `spacing` es el hueco entre hijos.
        VStack(spacing: 24) {
            Text("Marcador")
                .font(.headline)
 
            TarjetaDePartido(partido: partido)   // la tarjeta, definida más abajo
 
            // `HStack` apila en horizontal: dos botones en fila.
            HStack(spacing: 8) {
                // `{}` es la acción del botón: todavía no hace nada.
                Button("Gol \(partido.local.nombre)") {}
                Button("Gol \(partido.visitante.nombre)") {}
            }
            .buttonStyle(.borderedProminent)   // va después del HStack: se aplica a los dos botones
 
            // `Spacer` ocupa todo el espacio libre y empuja a los demás hacia arriba.
            Spacer()
        }
        // Aire alrededor de todo el `VStack`.
        .padding(16)
    }
}
 
// La tarjeta del partido: local, marcador y visitante en una fila.
struct TarjetaDePartido: View {
    let partido: Partido
 
    var body: some View {
        HStack {
            Text(partido.local.nombre)
                .font(.title3)
                // El nombre ocupa el espacio que sobra y se pega a la izquierda.
                .frame(maxWidth: .infinity, alignment: .leading)
            Text("\(partido.golesLocal) - \(partido.golesVisitante)")
                .font(.system(size: 40, weight: .bold, design: .rounded))
                .monospacedDigit()   // cifras del mismo ancho: el marcador no baila
            Text(partido.visitante.nombre)
                .font(.title3)
                // El visitante se pega a la derecha: el marcador queda centrado.
                .frame(maxWidth: .infinity, alignment: .trailing)
        }
        .padding(20)
        // El fondo va DESPUÉS del padding: pinta también el «aire» (la lección siguiente explica por qué).
        .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
    }
}
 
#Preview {
    PantallaMarcadorEstatica()
}

Ejecuta con Cmd+R. Debe verse «Marcador», una tarjeta con «Rayo FC», «2 - 1» y «Toros», y debajo los botones «Gol Rayo FC» y «Gol Toros». Tócalos: no pasa nada.

Paso 3 · El laboratorio y las pestañas

Objetivo: una pestaña «Laboratorio» que recoge el botón y la hoja de UIKit de la lección anterior, y una barra de pestañas con la pantalla del marcador y el laboratorio.

MarcadorApp (proyecto)
MarcadorApp
Laboratorio
LaboratorioView.swiftse crea
ContentView.swiftse modifica
Un fichero nuevo y otro modificado; `LaboratorioView` se lleva el botón de la lección anterior.

Crea LaboratorioView.swift (vacío, por ahora) dentro de Laboratorio: clic derecho sobre la carpeta Laboratorio → New File from Template… → Swift File → Next, nombre LaboratorioView. Va suelto, sin UI dentro: Laboratorio y Ejercicios no son funcionalidades, y por eso no llevan capas.

Las pantallas de aprendizaje que no son parte de la app se agrupan en una pestaña, LaboratorioView. Recoge el botón y la hoja de la lección anterior, que salen de ContentView. Piezas nuevas:

  • ScrollView { … }: contenido que se desplaza si no cabe. Ahora cabe de sobra; la lección siguiente le añade recuadros.
  • TabView { Tab("Marcador", systemImage: "sportscourt") { … } }: una barra de pestañas; systemImage es el nombre de un símbolo del sistema (SF Symbols); los nombres se buscan en la app gratuita SF Symbols de Apple (developer.apple.com/sf-symbols).
  • @State, $ y .sheet(isPresented:): los de la lección anterior, que se explican a fondo en @State y @Binding.

ScrollView y TabView juntos, en una raíz de dos pestañas cualquiera:

ejemplo mínimo: un TabView con dos Tab y un ScrollView (con comentarios)
import SwiftUI
 
struct Raiz: View {
    var body: some View {
        TabView {                                          // la barra de pestañas de abajo
            Tab("Inicio", systemImage: "house") {          // título y símbolo del sistema (SF Symbols)
                Text("Pantalla de inicio")                 // lo que se ve al elegir esta pestaña
            }
            Tab("Lista", systemImage: "list.bullet") {
                ScrollView {                               // si no cabe, se desplaza
                    VStack(alignment: .leading, spacing: 8) {
                        Text("Fila 1")                     // con muchas filas, más de las que caben,
                        Text("Fila 2")                     // el ScrollView deja desplazarlas
                        Text("Fila 3")
                    }
                    .padding()
                }
            }
        }
    }
}

Intenta escribirlo tú: LaboratorioView, con contenido que se desplaza si no cabe: en columna, el botón destacado «Ciclo de vida (UIKit)», que abre en una hoja la pantalla de UIKit (con el mismo dato que antes tenía ContentView para saber si está abierta). Cuando termines, abre el bloque.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Laboratorio/LaboratorioView.swift
import SwiftUI
 
// Pantallas de aprendizaje que no son parte de la app: se agrupan en una pestaña.
struct LaboratorioView: View {
    // Si la hoja del controlador está abierta (el mismo dato que antes tenía `ContentView`).
    @State private var mostrandoControlador = false
 
    var body: some View {
        // Si el contenido no cabe en pantalla, se desplaza.
        ScrollView {
            VStack(alignment: .leading, spacing: 24) {
                Button("Ciclo de vida (UIKit)") { mostrandoControlador = true }
                    .buttonStyle(.borderedProminent)
            }
            .padding()
        }
        // La hoja con el controlador de UIKit de la lección anterior.
        .sheet(isPresented: $mostrandoControlador) {
            CicloDeVidaView()
        }
    }
}
 
#Preview {
    LaboratorioView()
}

Y ContentView.swift pasa a ser la raíz de la app. Intenta escribirlo tú: una barra de pestañas con dos, «Marcador» (con el símbolo sportscourt), que enseña la pantalla del marcador, y «Laboratorio» (con flask), que enseña el laboratorio. Cuando termines, abre el bloque (sustituye el fichero entero).

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
ContentView.swift: la raíz con dos pestañas
import SwiftUI
 
// La raíz de la app. `TabView` es la barra de pestañas de iOS.
struct ContentView: View {
    var body: some View {
        TabView {
            // La primera pestaña: la pantalla del marcador.
            Tab("Marcador", systemImage: "sportscourt") {
                PantallaMarcadorEstatica()
            }
            // La segunda: el laboratorio.
            Tab("Laboratorio", systemImage: "flask") {
                LaboratorioView()
            }
        }
    }
}
 
#Preview {
    ContentView()
}

Ejecuta con Cmd+R. En la pestaña «Marcador» está la pantalla del paso anterior. En «Laboratorio» ves el botón «Ciclo de vida (UIKit)», que sigue abriendo la hoja.

Paso 4 · Las pruebas

Objetivo: que las pruebas de interfaz conozcan la pantalla nueva y las dos pestañas. Son dos cambios en MarcadorAppUITests.swift; las piezas que usan están en la tabla de XCUITest.

Primero, la prueba de arranque. Ahora la pantalla enseña «Rayo FC» y «2 - 1», y no solo el título. Intenta reescribirla tú: que arranque la app, espere a ver el texto «Rayo FC», compruebe que existe el texto «2 - 1» y guarde una captura con capturar, de nombre marcador. Cuando termines, abre el bloque y sustituye testArrancaYMuestraElMarcador entera por esta:

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorAppUITests.swift: testArrancaYMuestraElMarcador, sustituida
    @MainActor
    func testArrancaYMuestraElMarcador() throws {
        let app = XCUIApplication()
        app.launch()
        XCTAssertTrue(app.staticTexts["Rayo FC"].waitForExistence(timeout: 5))
        XCTAssertTrue(app.staticTexts["2 - 1"].exists)
        capturar(app, "marcador")
    }

Después, la prueba de la hoja. El botón «Ciclo de vida (UIKit)» ya no está en la pantalla de arranque, sino en la pestaña «Laboratorio»: la prueba tiene que tocarla antes. En testAbreYCierraElControlador, añade una línea entre app.launch() y la que toca el botón; el resto de la prueba no cambia:

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorAppUITests.swift: testAbreYCierraElControlador, una línea nueva
    @MainActor
    func testAbreYCierraElControlador() throws {
        let app = XCUIApplication()
        app.launch()
        app.tabBars.buttons["Laboratorio"].tap()      // NUEVA: el botón vive ahora en la pestaña «Laboratorio»
        app.buttons["Ciclo de vida (UIKit)"].tap()
        // … el resto de la prueba, sin cambios …

Pasa todas las pruebas con Cmd+U, o desde la Terminal:

cd ~/Marcador-Swift/MarcadorApp
xcodebuild -scheme MarcadorApp -destination 'platform=iOS Simulator,name=iPhone 17' build test

El nombre del simulador tiene que existir en tu Mac; con xcrun simctl list devices available ves los que tienes.

Punto de control

  • Desde ~/Marcador-Swift/MarcadorApp, xcodebuild -scheme MarcadorApp -destination 'platform=iOS Simulator,name=iPhone 17' build test termina con ** TEST SUCCEEDED ** (con el simulador que tengas: cambia iPhone 17 por un nombre de xcrun simctl list devices available): cuatro pruebas, una unitaria y tres de interfaz.
  • La primera pestaña enseña «Marcador», una tarjeta con «Rayo FC», «2 - 1» y «Toros», y los botones «Gol Rayo FC» y «Gol Toros» (no hacen nada).
  • En la pestaña «Laboratorio», el botón «Ciclo de vida (UIKit)» sigue abriendo la hoja.
  • El proyecto tiene Marcador/UI/PantallaMarcadorEstatica.swift y Laboratorio/LaboratorioView.swift dentro de la carpeta MarcadorApp, y Ejercicios/FichaDeJugador con su ejercicio.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué es una vista en SwiftUI?

  2. 2.Un ZStack tiene dos hijos: primero un rectángulo verde y después un Text. ¿Qué se ve?

  3. 3.¿Qué hace Spacer() dentro de un VStack?

  4. 4.¿Qué hace .frame(maxWidth: .infinity, alignment: .leading) sobre un texto de una fila?

  5. 5.¿Qué es un TabView con dos Tab?

Faltan 5 de 5