iOS

Aprende todo el stack

Guía de Estudio

Cada tema central de iOS, explicado desde los primeros principios y planteado como lo pide una entrevista. Lee el concepto, luego la nota de nivel para saber si es lo mínimo indispensable o algo que te diferencia.

Cómo leer esto Cada tema comienza con qué es, luego los puntos que una entrevista realmente sondea, y una nota de nivel para que sepa si es requisito básico (junior/intermedio) o diferenciador (senior/arquitecto). Léalo de arriba a abajo una vez, luego regrese a través de las flashcards y las prompts para que se fije.

01 · Fundamentos del Lenguaje Swift

Qué es. Swift es un lenguaje orientado a valores y tipado de forma segura. La idea más importante para interiorizar temprano es tipos por valor vs tipos por referencia: struct y enum se copian al asignar (semántica de valor), mientras que las instancias de class se comparten por referencia. SwiftUI se apoya fuertemente en tipos por valor, así que use struct por defecto y recurrirá a class solo cuando necesite identidad o semántica de referencia.

Fluidez básica que la entrevista asume: optionals (y desenvoltura segura con if let / guard let en lugar de desenvoltura forzada con !), enums con valores asociados, closures y cómo capturan, protocolos y diseño orientado a protocolos, genéricos y manejo de errores con do/try/catch.

// Value vs reference struct Point { var x = 0 } // copied final class Box { var x = 0 } // shared var a = Point(); var b = a; b.x = 9 // a.x still 0 let p = Box(); let q = p; q.x = 9 // p.x is now 9 // Optionals, no force-unwrap func first(_ xs: [Int]) -> Int { guard let head = xs.first else { return 0 } return head }
Memoria Las clases tienen conteo de referencias (ARC). Un closure que captura self fuertemente dentro de una propiedad almacenada crea un retain cycle — rompalo con [weak self]. Esta es una pregunta clásica de seguimiento para senior.
Nivel Junior: optionals, structs, closures. Senior: diseño orientado a protocolos, genéricos con restricciones, y razonamiento sobre ARC y rendimiento de semántica de valor (copy-on-write).

02 · Fundamentos de SwiftUI

Qué es. SwiftUI es un framework de UI declarativo: describe cómo debería verse la UI para un estado dado, y el framework diff y actualiza la pantalla cuando el estado cambia. Una vista es un struct ligero que cumple con View con un body que retorna más vistas.

Domine los modificadores (y que el orden importa.padding().background() difiere de .background().padding()), el layout con VStack/HStack/ ZStack, Spacer, frame y alineación, y las listas con List y ForEach con clave por identidad estable.

struct Greeting: View { let name: String var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Hello, \(name)").font(.title.bold()) Text("Welcome back").foregroundStyle(.secondary) } .padding() } }
Identidad SwiftUI rastrea las vistas por identidad. En ForEach, use un id estable (un id real del modelo, no el índice del array) o las animaciones y el estado se adherirán a la fila incorrecta.
Nivel Junior/intermedio: construir pantallas, componer vistas, usar modificadores correctamente. Senior: razonar sobre identidad de vistas, diffing y cuándo un re-render es innecesario (véase Rendimiento, tema 09).

03 · Estado y Flujo de Datos en SwiftUI

Qué es. SwiftUI está impulsado por una única fuente de verdad. Elija el wrapper de propiedad adecuado para quién posee el estado:

  • @State — valor poseído por esta vista (privado, estado UI transitorio).
  • @Binding — una referencia bidireccional al estado poseído en otro lugar.
  • @Observable (el framework de Observación, iOS 17+) — modelo de tipo referencia cuyas propiedades las vistas observan automáticamente; combine con @State para poseerlo y @Bindable para vincularse a él.
  • @Environment — dependencias inyectadas hacia abajo en el árbol de vistas.

Antes de iOS 17 esto era ObservableObject + @Published + @StateObject/ @ObservedObject; conozca ambos, porque mucho código todavía usa la API anterior.

@Observable final class CounterModel { var count = 0 } struct CounterView: View { @State private var model = CounterModel() var body: some View { Button("Tapped \(model.count)") { model.count += 1 } } }
Por qué importa El framework de Observación rastrea las lecturas a nivel de propiedad, por lo que solo las vistas que realmente leen una propiedad modificada se vuelven a renderizar — una ganancia real de rendimiento sobre el modelo anterior @Published, que invalidaba todos los observadores.

04 · UIKit e Interoperabilidad

Qué es. UIKit es el framework de UI imperativo y maduro que impulsó iOS durante una década. Incluso en una app SwiftUI-first lo necesita para APIs que SwiftUI no cubre completamente, personalización profunda y bases de código existentes grandes. Conozca el ciclo de vida de UIViewController (viewDidLoad → viewWillAppear → viewDidAppear → …), las restricciones de Auto Layout, y UITableView/UICollectionView con diffable data sources.

El puente funciona en ambas direcciones: envuelva UIKit en SwiftUI con UIViewRepresentable / UIViewControllerRepresentable, y aloje SwiftUI dentro de UIKit con UIHostingController.

struct MapView: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { MKMapView() } func updateUIView(_ view: MKMapView, context: Context) { /* sync state */ } }
Nivel Intermedio: usar componentes UIKit y los wrappers representable. Senior: dirigir una migración UIKit ↔ SwiftUI, gestionar el patrón Coordinator para delegados, y saber qué framework posee el stack de navegación.

05 · Concurrencia de Swift

Qué es. La concurrencia asíncrona moderna de iOS se construye sobre async/await y la concurrencia estructurada. Las funciones async se suspenden sin bloquear un hilo; Task inicia trabajo concurrente; async let y TaskGroup ejecutan hijos en paralelo y los unen. Los actors protegen el estado mutable de carreras de datos serializando el acceso, y @MainActor garantiza que el código se ejecute en el hilo principal (requerido para UI).

Swift 6 convierte esto en seguridad contra carreras de datos en tiempo de compilación: los tipos que cruzan límites de concurrencia deben ser Sendable, y el compilador aplica el aislamiento de actors. Entender el aislamiento, Sendable y @MainActor es la conversación de concurrencia para senior.

func loadProfile() async throws -> Profile { async let user = api.user() // these two run async let posts = api.posts() // concurrently return try await Profile(user: user, posts: posts) } @MainActor func show(_ p: Profile) { self.profile = p } // safe UI update
Trampa No recurrir a DispatchQueue.main.async por hábito — use @MainActor. Y nunca bloquee el actor principal con trabajo síncrono; muévalo fuera con un Task o un actor de fondo.
Nivel Intermedio: usar async/await y @MainActor. Senior: explicar el aislamiento de actors, Sendable, cancelación y cómo migrar una API basada en callbacks a async con withCheckedContinuation.

06 · Red y Codable

Qué es. El stack estándar es la API asíncrona de URLSession más Codable para JSON. Codable (= Encodable & Decodable) mapea JSON a tipos Swift automáticamente; use CodingKeys para renombrar campos y estrategias de decodificador (por ejemplo .convertFromSnakeCase, decodificación de fechas personalizada) para manejar payloads del mundo real.

struct User: Codable, Identifiable { let id: Int let fullName: String enum CodingKeys: String, CodingKey { case id, fullName = "full_name" } } func fetchUser(id: Int) async throws -> User { let url = URL(string: "https://api.example.com/users/\(id)")! let (data, response) = try await URLSession.shared.data(from: url) guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) } return try JSONDecoder().decode(User.self, from: data) }
Argumento Envuelva la red detrás de un protocolo (APIClient) para que los view models dependan de una abstracción que puede mockear en pruebas. Agregue caché en capas con URLCache o su tienda de persistencia, y centralice la lógica de reintento/renovación de auth.

07 · Persistencia

Qué es. Elija el almacenamiento según los datos: SwiftData (iOS 17+) para modelos de app — anote una clase con @Model, guarde a través de un ModelContext y consulte reactivamente con @Query; Core Data para objetivos más antiguos o control avanzado; UserDefaults para preferencias pequeñas; el Keychain para secretos (tokens, contraseñas — nunca UserDefaults); y el sistema de archivos para blobs.

@Model final class Task { var title: String var isDone = false init(title: String) { self.title = title } } struct TaskList: View { @Query(sort: \Task.title) private var tasks: [Task] @Environment(\.modelContext) private var context var body: some View { List(tasks) { Text($0.title) } } }
No haga Almacene tokens de autenticación o PII en UserDefaults — es un plist sin cifrar. Use el Keychain (véase Seguridad, tema 14).
Nivel Intermedio: modelar datos y hacer CRUD con SwiftData/ Core Data. Senior: diseñar migraciones, razonar sobre el modelo de contexto/hilos y sincronizar con un backend (offline-first — véase Arquitectura).

08 · Navegación y Estructura de la App

Qué es. El punto de entrada de una app es un struct que cumple con App con @main, componiendo Scenes (usualmente un WindowGroup). Para la navegación, NavigationStack con navigationDestination reemplazó al antiguo NavigationView y habilita la navegación programática basada en valores: vincule un array path y haga push/pop mutándolo. TabView maneja las secciones de nivel superior, y los deep links mapean un URL sobre esa ruta.

@main struct MyApp: App { var body: some Scene { WindowGroup { RootView() } } } struct RootView: View { @State private var path: [Route] = [] var body: some View { NavigationStack(path: $path) { HomeView() .navigationDestination(for: Route.self) { route in DetailView(route: route) } } } }
Argumento La navegación basada en valores hace que el deep linking y la restauración de estado sean sencillos: un URL o notificación push se descodifica en valores Route que se agregan a path. Ese es el encuadre senior — navegación como datos.

09 · Rendimiento e Instruments

Qué es. El trabajo de rendimiento se trata principalmente de hacer menos: menos re-renders, menos asignaciones, menos trabajo en el hilo principal. En SwiftUI eso significa mantener body ligero y puro (sin cálculos pesados, sin efectos secundarios), dar a las vistas identidad estable, usar LazyVStack/LazyHStack y List para contenido largo, y dejar que el framework de Observación re-renderice solo las vistas que leen una propiedad modificada.

Mida con Instruments: Time Profiler para puntos calientes de CPU, Allocations/Leaks para memoria y retain cycles, el instrumento SwiftUI para conteo de view-body, y Hangs para bloqueos del hilo principal. Regla uno: perfile antes de optimizar — las suposiciones suelen estar equivocadas.

Errores comunes Descodificar u ordenar dentro de body; cargar imágenes de resolución completa en miniaturas pequeñas (reduzca la resolución primero); retain cycles de closures que capturan self; y bloquear el actor principal con E/S síncrona.
Nivel Senior: leer un trace de Instruments, encontrar el punto caliente y explicar la solución. Arquitecto: establecer presupuestos de rendimiento (lanzamiento en frío, scroll, memoria) y conectarlos a CI y observabilidad.

10 · Pruebas

Qué es. Hoy se shippean dos frameworks de pruebas unitarias: el establecido XCTest y el más nuevo Swift Testing (funciones @Test con macros #expect/#require, pruebas parametrizadas y soporte asíncrono), introducido con Xcode 16. Los flujos de UI se cubren con XCUITest. Lo que las entrevistas sondean es la testeabilidad: inyecte dependencias detrás de protocolos para poder sustituir fakes, y mantenga la lógica en view models en lugar de vistas.

import Testing @Test func totalsAreSummed() { let cart = Cart(items: [2, 3]) #expect(cart.total == 5) } @Test func loadsUser() async throws { let client = MockAPIClient(user: .stub) let model = ProfileModel(api: client) try await model.load() #expect(model.user?.id == 1) }
Argumento "Dependo de un protocolo APIClient, no de URLSession, por lo que el view model toma un mock en pruebas y se ejecuta sin red." Esa una oración señala que usted diseña para testeabilidad.

11 · Dependencias y Swift Package Manager

Qué es. Swift Package Manager (SPM) es la forma nativa de agregar dependencias y — igualmente importante — de modularizar su propia app en paquetes locales. Un manifiesto Package.swift declara targets (código) y productos (lo que expone). Dividir una funcionalidad en su propio paquete fuerza límites, acelera compilaciones incrementales y permite que las funcionalidades se prueben de forma aislada. CocoaPods y Carthage todavía existen en proyectos legacy, pero SPM es el predeterminado para trabajo nuevo.

// Package.swift let package = Package( name: "Feature", products: [.library(name: "Feature", targets: ["Feature"])], dependencies: [], targets: [ .target(name: "Feature"), .testTarget(name: "FeatureTests", dependencies: ["Feature"]), ] )
Perspectiva de Arquitecto Los paquetes locales son la forma más barata de obtener arquitectura modular: una dirección de dependencia clara, sin importaciones accidentales entre funcionalidades y compilación en paralelo. Esta es una respuesta recurrente de diseño de sistemas (véase Arquitectura).

12 · CI/CD y Lanzamiento

Qué es. Un pipeline repetible que construye, prueba, firma y distribuye cada cambio. Xcode Cloud es el CI alojado de Apple integrado con App Store Connect; Fastlane es el estándar de automatización multiplataforma (y se ejecuta en GitHub Actions, etc.). La parte que complica a todos es el firma de código: los certificados identifican al equipo, los provisioning profiles vinculan un app id + dispositivos + certificado juntos. El firmado automático maneja el caso común; los equipos suelen gestionarlo explícitamente (por ejemplo, Fastlane match) para credenciales compartidas y reproducibles.

# Fastlane: build & ship a TestFlight beta lane :beta do increment_build_number build_app(scheme: "App") upload_to_testflight end
Argumento Distinga la versión (marketing, CFBundleShortVersionString) del número de build (CFBundleVersion, debe incrementarse por carga). Respuesta senior: los PR ejecutan pruebas unitarias + de UI en CI; merges a main envían automáticamente un build de TestFlight; los lanzamientos se promueven con un phased rollout.

13 · Revisión de App Store y Distribución

Qué es. El lanzamiento se realiza a través de App Store Connect: cargue un build, complete los metadatos y capturas de pantalla, complete App Privacy (la "etiqueta de nutrición" de privacidad) y envíe para revisión según las App Store Review Guidelines. Las pruebas beta se realizan a través de TestFlight. Conozca las razones de rechazo comunes para poder evitarlas: funcionalidad rota/incompleta, problemas de privacidad (cadenas de descripción de uso faltantes, recopilación de datos no revelada), metadatos engañosos y uso de APIs privadas.

  • Phased release implementa una actualización a un % creciente de usuarios durante ~7 días — pause si las tasas de crash aumentan.
  • Los privacy manifests y las APIs de razón requerida deben declararse (véase Seguridad, tema 14).
  • StoreKit maneja las compras en app y suscripciones; Apple requiere IAP para bienes digitales.
Realidad La revisión son principalmente verificaciones automáticas más un pase humano; la mayoría de rechazos se pueden evitar con metadatos completos, respuestas honestas de privacidad y un build que no falle en el primer lanzamiento. Use TestFlight para atrapar lo obvio primero.

14 · Seguridad y Privacidad

Qué es. Almacene secretos en el Keychain (cifrado, respaldado por hardware), nunca en UserDefaults. Proteja flujos sensibles con biometría a través del framework LocalAuthentication (Face ID / Touch ID). Mantenga App Transport Security (solo HTTPS) activado. Use clases de protección de datos para que los archivos estén cifrados en reposo cuando el dispositivo esté bloqueado. Respete la privacidad: declare un privacy manifest (PrivacyInfo.xcprivacy) y el uso de APIs de razón requerida, y solicite permiso de rastreo a través de App Tracking Transparency antes de usar el IDFA.

import LocalAuthentication func authenticate() async -> Bool { let ctx = LAContext() guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return false } return (try? await ctx.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Unlock your vault")) ?? false }
Nivel Senior: Keychain, biometría, ATS y privacy manifests son lo esperado. Arquitecto: posea la postura de privacidad de la app — minimización de datos, procesamiento en el dispositivo (véase tema 15) y una respuesta defendible a "qué datos salen del dispositivo y por qué".

15 · IA en el Dispositivo y Machine Learning

Qué es. La ML de Apple se ejecuta en el dispositivo por privacidad, baja latencia y uso sin conexión, acelerada por el Neural Engine. La caja de herramientas: Core ML (ejecutar modelos entrenados), Create ML (entrenar sin salir de Swift) y frameworks de tareas — Vision (imágenes), Natural Language (texto), Speech (transcripción) y Sound Analysis. Apple Intelligence lleva esto más lejos con funciones generativas a nivel de sistema y modelos fundamentales en el dispositivo. Convierta modelos de terceros a Core ML con coremltools, y optimice (cuantice, poda) para ajustar a presupuestos de memoria y latencia.

import Vision func detectText(in image: CGImage) async throws -> [String] { let request = VNRecognizeTextRequest() request.recognitionLevel = .accurate let handler = VNImageRequestHandler(cgImage: image) try handler.perform([request]) return request.results?.compactMap { $0.topCandidates(1).first?.string } ?? [] }
Encuadre de frontera "Ejecute inferencia en el dispositivo cuando pueda — es privado, funciona sin conexión y no tiene costo por llamada; vuelva a un modelo de servidor solo cuando la tarea exceda la capacidad del dispositivo." La propia búsqueda de esta guía ejecuta un pequeño modelo de incrustación en su navegador como demostración funcional.