Preguntas de entrevista de iOS y Swift — 40 con código
Si estás preparando una entrevista para un puesto de iOS developer, sabés que el panel técnico no se improvisa. Las preguntas van desde los fundamentos del lenguaje hasta arquitectura, concurrencia, y el proceso de publicación en el App Store. Acá juntamos 40 preguntas reales con respuestas detalladas y código Swift que podés estudiar, adaptar y practicar en voz alta.
Una aclaración antes de empezar: memorizar respuestas no alcanza. Lo que buscan los entrevistadores es que entiendas el *por qué* detrás de cada decisión. Por eso cada respuesta acá incluye el razonamiento, no solo la definición.
Swift: fundamentos del lenguaje
1. ¿Qué es un Optional en Swift y por qué existe?
Un Optional representa un valor que puede existir o puede ser nil. Existe porque Swift tiene un sistema de tipos estricto — no podés tener nil en una variable que no está declarada como opcional.
var nombre: String? = nil
nombre = "María"
// Unwrap seguro con if let
if let n = nombre {
print("Hola, \(n)")
}
// Unwrap con guard (preferido en funciones)
func saludar(nombre: String?) {
guard let n = nombre else {
print("Sin nombre")
return
}
print("Hola, \(n)")
}
// Nil coalescing
let display = nombre ?? "Invitado"Lo que busca el entrevistador: que entiendas la diferencia entre if let, guard let, ! (force unwrap — peligroso), y ??. El force unwrap es la causa #1 de crashes en producción con optionals.
2. Value types vs reference types: ¿cuándo usás cada uno?
En Swift, struct y enum son value types (se copian en cada asignación), mientras que class es un reference type (se comparte una referencia al mismo objeto en memoria).
struct Punto {
var x: Double
var y: Double
}
class Nodo {
var valor: Int
var siguiente: Nodo?
init(_ valor: Int) { self.valor = valor }
}
var p1 = Punto(x: 1.0, y: 2.0)
var p2 = p1 // copia independiente
p2.x = 99.0
print(p1.x) // 1.0 — p1 no cambió
var n1 = Nodo(10)
var n2 = n1 // misma referencia
n2.valor = 99
print(n1.valor) // 99 — n1 SÍ cambióRegla práctica: usá struct por defecto. Usá class cuando necesitás identidad compartida (delegates, managers de estado, objetos que se pasan entre módulos y deben mantener su identidad).
3. ¿Qué son los Generics y para qué sirven?
Los generics permiten escribir código flexible y reutilizable que funciona con cualquier tipo, sin perder type safety.
// Sin generics — repetición horrible
func primerElementoInt(_ arr: [Int]) -> Int? { arr.first }
func primerElementoString(_ arr: [String]) -> String? { arr.first }
// Con generics — una sola función
func primerElemento<T>(_ arr: [T]) -> T? { arr.first }
let n = primerElemento([1, 2, 3]) // Int?
let s = primerElemento(["a", "b", "c"]) // String?
// Constraint: T debe ser Comparable
func maximo<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}Uso avanzado — associated types en protocolos:
protocol Contenedor {
associatedtype Item
var items: [Item] { get }
mutating func agregar(_ item: Item)
}4. ¿Qué son los Property Wrappers?
Son un mecanismo para encapsular lógica que se repite en propiedades. Declarás una vez la lógica y la aplicás con un @ en cualquier propiedad.
@propertyWrapper
struct Clamped {
private var valor: Int
let rango: ClosedRange<Int>
var wrappedValue: Int {
get { valor }
set { valor = min(max(newValue, rango.lowerBound), rango.upperBound) }
}
init(wrappedValue: Int, _ rango: ClosedRange<Int>) {
self.rango = rango
self.valor = min(max(wrappedValue, rango.lowerBound), rango.upperBound)
}
}
struct Config {
@Clamped(0...100) var volumen: Int = 50
}
var c = Config()
c.volumen = 200
print(c.volumen) // 100 — se clampea automáticoLos property wrappers más conocidos en iOS: @State, @Binding, @Published, @AppStorage, @UserDefault.
5. ¿Cuál es la diferencia entre `map`, `flatMap` y `compactMap`?
let numeros = [1, 2, 3, 4]
// map: transforma cada elemento
let dobles = numeros.map { $0 * 2 } // [2, 4, 6, 8]
// compactMap: transforma y elimina nil
let strings = ["1", "dos", "3", "cuatro"]
let enteros = strings.compactMap { Int($0) } // [1, 3]
// flatMap: aplaná un nivel de anidamiento
let pares = [[1, 2], [3, 4], [5, 6]]
let plano = pares.flatMap { $0 } // [1, 2, 3, 4, 5, 6]Error frecuente en entrevistas: confundir flatMap con compactMap en arrays de opcionales. A partir de Swift 4.1, flatMap sobre opcionales fue renombrado a compactMap para mayor claridad.
ARC y gestión de memoria
6. ¿Cómo funciona ARC en Swift?
ARC (Automatic Reference Counting) cuenta las referencias a cada objeto en el heap. Cuando el contador llega a 0, libera la memoria. No es un garbage collector — ocurre de forma determinista en tiempo de compilación.
class Persona {
let nombre: String
init(_ nombre: String) {
self.nombre = nombre
print("Creando \(nombre)")
}
deinit {
print("Liberando \(nombre)")
}
}
var p1: Persona? = Persona("Ana") // RC = 1
var p2 = p1 // RC = 2
p1 = nil // RC = 1
p2 = nil // RC = 0 → deinit7. ¿Qué es un retain cycle y cómo lo resolvés?
Un retain cycle ocurre cuando dos objetos se referencian mutuamente con referencias strong, y ninguno llega a RC = 0.
class Empleado {
var jefe: Jefe?
deinit { print("Empleado liberado") }
}
class Jefe {
var empleado: Empleado?
deinit { print("Jefe liberado") }
}
var e: Empleado? = Empleado()
var j: Jefe? = Jefe()
e?.jefe = j
j?.empleado = e
e = nil // ⚠️ NO se llama deinit — cycle
j = nil // ⚠️ NO se llama deinit — cycleLa solución: weak o unowned
class JefeCorrecto {
weak var empleado: Empleado? // no incrementa RC
deinit { print("Jefe liberado") }
}8. ¿Cuándo usás `weak` y cuándo `unowned`?
weak: cuando la referencia puede sernildurante su vida útil. Siempre es opcional.unowned: cuando la referencia NUNCA va a sernilmientras exista el objeto que la tiene. No es opcional, y si el objeto ya fue liberado, crashea.
class Tarjeta {
let numero: String
unowned let titular: Cliente // una tarjeta siempre tiene titular
init(numero: String, titular: Cliente) {
self.numero = numero
self.titular = titular
}
}
class Cliente {
let nombre: String
var tarjeta: Tarjeta?
init(nombre: String) { self.nombre = nombre }
}Caso clásico de weak en closures:
viewModel.fetch { [weak self] resultado in
guard let self else { return }
self.actualizar(resultado)
}9. ¿Qué problema causa un retain cycle en una closure?
class ViewController: UIViewController {
var timer: Timer?
var contador = 0
func iniciar() {
// ⚠️ self retiene timer, timer retiene self
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.contador += 1 // strong capture
}
}
// ✅ Corrección
func iniciarBien() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.contador += 1
}
}
}Lo que busca el entrevistador: que identifiques la causa (el closure captura self con strong por defecto) y que conozcas la capture list [weak self].
Protocolos
10. ¿Qué ventaja tiene un protocolo frente a herencia de clase?
Los protocolos permiten composición múltiple, no hay acoplamiento de implementación, y funcionan con structs y enums (que no soportan herencia).
protocol Volable {
func volar()
}
protocol Nadable {
func nadar()
}
struct Pato: Volable, Nadable {
func volar() { print("Pato volando") }
func nadar() { print("Pato nadando") }
}
// Protocol extensions — implementación por defecto
extension Volable {
func despegar() {
print("Preparando para volar...")
volar()
}
}11. ¿Qué es Protocol-Oriented Programming (POP)?
Es el paradigma central de Swift donde los protocolos (con extensions) reemplazan a la herencia de clase como mecanismo principal de reutilización.
protocol Persistible {
var id: UUID { get }
func guardar()
}
extension Persistible {
func guardar() {
// implementación default — por ejemplo, con UserDefaults
print("Guardando \(id)")
}
}
struct Usuario: Persistible {
let id = UUID()
let nombre: String
// guardar() viene del extension — no hace falta redefinirla
}Ventaja clave: podés testear con mocks fácilmente porque el tipo concreto no importa, solo que conforme al protocolo.
12. ¿Qué es un `@objc` protocol y para qué se usa?
Los protocolos @objc exponen Swift al runtime de Objective-C. Se usan cuando necesitás métodos opcionales (@objc optional) o cuando el delegate va a ser usado por código ObjC.
@objc protocol MiDelegate: AnyObject {
func didLoad()
@objc optional func didFail(error: Error)
}
class MiClase {
weak var delegate: MiDelegate?
func proceso() {
delegate?.didLoad()
delegate?.didFail?(error: NSError()) // nota el ?
}
}13. ¿Qué es `some` y `any` en Swift?
some Protocol(opaque type): el tipo concreto es uno solo y lo conoce el compilador. Permite optimizaciones.any Protocol(existential): el tipo puede ser cualquiera que conforme al protocolo. Implica un overhead (existential container).
// some: el compiler sabe el tipo concreto
func hacerVista() -> some View {
Text("Hola")
}
// any: puede ser cualquier tipo que sea Hashable
func imprimir(valores: [any Hashable]) {
valores.forEach { print($0) }
}A partir de Swift 5.7, el compilador te exige any para protocolos usados como tipo (existentials), haciendo el costo explícito.
Concurrencia Swift — async/await y actores
14. ¿Qué problema resuelve async/await respecto a completion handlers?
Los completion handlers generan "callback hell", errores de manejo inconsistente, y fácil olvido de llamar el completion en todos los paths. async/await hace el código asíncrono que parece síncrono, con manejo de errores nativo.
// Antes — callback hell
func cargarDatos(completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error { completion(.failure(error)); return }
guard let data else { completion(.failure(MiError.sinDatos)); return }
completion(.success(data))
}.resume()
}
// Con async/await
func cargarDatos() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Uso
Task {
do {
let datos = try await cargarDatos()
print(datos)
} catch {
print("Error: \(error)")
}
}15. ¿Qué es un `Task` y cuándo usás `Task.detached`?
Un Task crea una unidad de trabajo asíncrona. Hereda el contexto del actor actual (por ej., @MainActor). Task.detached no hereda contexto — útil para trabajo de fondo que no debería bloquear el actor principal.
// Task hereda contexto del actor actual
func cargar() {
Task {
let datos = try await api.fetch()
// corre en el mismo actor que cargar()
self.mostrar(datos)
}
}
// Task.detached — sin herencia de contexto
Task.detached(priority: .background) {
// procesamiento pesado, off main thread
let resultado = procesarImagen(data)
await MainActor.run {
self.imagen = resultado
}
}16. ¿Qué es un Actor y qué problema resuelve?
Un actor es un tipo de referencia que protege su estado mutable de accesos concurrentes. Solo permite un acceso a la vez — no necesitás locks ni DispatchQueues manuales.
actor Contador {
private var valor = 0
func incrementar() {
valor += 1
}
func obtenerValor() -> Int {
valor
}
}
let c = Contador()
async let t1: () = c.incrementar()
async let t2: () = c.incrementar()
await (t1, t2) // no hay data race@MainActor: es el actor del hilo principal. Todo lo que lo lleva ejecuta en el main thread — ideal para actualizar UI.
@MainActor
class ViewModel: ObservableObject {
@Published var texto = ""
func cargar() async {
let datos = await api.fetch()
texto = datos.titulo // seguro: estamos en @MainActor
}
}17. ¿Qué es `async let` y cuándo usarlo?
async let arranca múltiples tareas asíncronas en paralelo y espera ambas con await.
func cargarPerfil() async throws -> Perfil {
// arranca ambas peticiones al mismo tiempo
async let usuario = api.fetchUsuario()
async let fotos = api.fetchFotos()
// espera los dos resultados
return Perfil(usuario: try await usuario, fotos: try await fotos)
}Sin async let, si usaras dos await separados, se ejecutarían en secuencia — el doble de tiempo innecesariamente.
18. ¿Qué es un `AsyncSequence`?
Es el equivalente asíncrono de Sequence — producís y consumís elementos uno a uno de forma asíncrona.
struct ContadorAsync: AsyncSequence {
typealias Element = Int
let limite: Int
struct AsyncIterator: AsyncIteratorProtocol {
var actual = 0
let limite: Int
mutating func next() async -> Int? {
guard actual < limite else { return nil }
try? await Task.sleep(nanoseconds: 500_000_000)
defer { actual += 1 }
return actual
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(limite: limite)
}
}
// Uso con for await
for await numero in ContadorAsync(limite: 5) {
print(numero)
}UIKit vs SwiftUI
19. ¿Cuáles son las diferencias fundamentales entre UIKit y SwiftUI?
| Aspecto | UIKit | SwiftUI |
|---|---|---|
| Paradigma | Imperativo | Declarativo |
| Ciclo de vida | viewDidLoad, viewWillAppear, etc. | onAppear, .task, modificadores |
| Estado | Propiedades + reloadData() manual | @State, @Binding, @StateObject |
| Preview | Simulador | Canvas en tiempo real |
| Mínimo iOS | iOS 2+ | iOS 13+ |
20. Explicá el ciclo de vida de un UIViewController
class MiViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Se llama UNA vez. Configurar la view, constraints, delegates.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Antes de que aparezca. Actualizar datos que pueden haber cambiado.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Ya está visible. Iniciar animaciones, tracking de analytics.
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// A punto de irse. Guardar estado, pausar reproducción.
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// Ya no es visible. Detener tareas costosas.
}
}21. ¿Cómo funciona el sistema de gestión de estado en SwiftUI?
// @State: estado local de la vista, privado
struct Contador: View {
@State private var cuenta = 0
var body: some View {
Button("Contar: \(cuenta)") {
cuenta += 1
}
}
}
// @Binding: recibe una referencia al estado del padre
struct Boton: View {
@Binding var activo: Bool
var body: some View {
Toggle("Activo", isOn: $activo)
}
}
// @StateObject: crea y posee el ViewModel
struct PantallaMain: View {
@StateObject private var vm = MainViewModel()
var body: some View {
Text(vm.titulo)
.task { await vm.cargar() }
}
}
// @ObservedObject: recibe un ViewModel ya creado
struct SubVista: View {
@ObservedObject var vm: MainViewModel
// ...
}22. ¿Cómo integrás un UIViewController en SwiftUI?
Usando UIViewControllerRepresentable:
struct MapaView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MKMapViewController {
MKMapViewController()
}
func updateUIViewController(_ uiViewController: MKMapViewController, context: Context) {
// sincronizar cambios de SwiftUI → UIKit
}
}
// Uso en SwiftUI
var body: some View {
MapaView()
.frame(height: 300)
}23. ¿Qué es el ciclo de vida de una escena en SwiftUI?
@main
struct MiApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// Reaccionar a cambios de fase
struct ContentView: View {
@Environment(\.scenePhase) private var fase
var body: some View {
Text("Hola")
.onChange(of: fase) { nuevaFase in
switch nuevaFase {
case .active: print("App activa")
case .inactive: print("App inactiva")
case .background: print("App en background")
@unknown default: break
}
}
}
}Core Data y SwiftData
24. ¿Qué es el NSManagedObjectContext y cuántos usarías en una app?
El NSManagedObjectContext es el "espacio de trabajo" donde creás, modificás y borrás objetos antes de persistirlos. En apps simples usás uno (el viewContext en el main thread). En apps con operaciones pesadas, usás un contexto de background para no bloquear la UI.
// Context principal (main thread)
let contexto = persistenceController.container.viewContext
// Crear entidad
let usuario = Usuario(context: contexto)
usuario.nombre = "Ana"
usuario.fecha = Date()
// Guardar
do {
try contexto.save()
} catch {
print("Error guardando: \(error)")
}
// Context de background para imports masivos
persistenceController.container.performBackgroundTask { bgContext in
// import de 10k registros sin freezar la UI
for item in datos {
let obj = Item(context: bgContext)
obj.valor = item.valor
}
try? bgContext.save()
}25. ¿Qué es SwiftData y cómo se diferencia de Core Data?
SwiftData (iOS 17+) es la evolución moderna de Core Data con una API declarativa basada en macros Swift.
// Core Data — requires .xcdatamodeld + NSManagedObject subclass
class Tarea: NSManagedObject {
@NSManaged var titulo: String
@NSManaged var completada: Bool
}
// SwiftData — puro Swift, sin archivo de modelo
import SwiftData
@Model
class Tarea {
var titulo: String
var completada: Bool
var fechaCreacion: Date
init(titulo: String) {
self.titulo = titulo
self.completada = false
self.fechaCreacion = Date()
}
}
// Uso en SwiftUI
struct ListaTareas: View {
@Query(sort: \Tarea.fechaCreacion) private var tareas: [Tarea]
@Environment(\.modelContext) private var contexto
var body: some View {
List(tareas) { tarea in
Text(tarea.titulo)
}
.toolbar {
Button("Agregar") {
contexto.insert(Tarea(titulo: "Nueva tarea"))
}
}
}
}Diferencias clave: SwiftData usa macros (@Model, @Query) en lugar de subclases de NSManagedObject; no necesita archivo .xcdatamodeld; tiene integración nativa con SwiftUI.
26. ¿Cómo harías un fetch con predicado en Core Data?
let request = NSFetchRequest<Tarea>(entityName: "Tarea")
request.predicate = NSPredicate(format: "completada == %@ AND prioridad > %d",
NSNumber(value: false),
2)
request.sortDescriptors = [NSSortDescriptor(key: "fechaCreacion", ascending: false)]
request.fetchLimit = 50
do {
let tareas = try contexto.fetch(request)
print("Encontradas: \(tareas.count)")
} catch {
print("Error: \(error)")
}Networking con URLSession
27. ¿Cómo hacés una petición GET con URLSession y async/await?
struct APIClient {
enum APIError: Error {
case invalidURL
case httpError(Int)
case decodingError
}
func fetch<T: Decodable>(_ endpoint: String) async throws -> T {
guard let url = URL(string: endpoint) else {
throw APIError.invalidURL
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
throw APIError.httpError(code)
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw APIError.decodingError
}
}
}
// Uso
struct Usuario: Decodable {
let id: Int
let nombre: String
}
let cliente = APIClient()
let usuario: Usuario = try await cliente.fetch("https://api.ejemplo.com/usuario/1")28. ¿Cómo implementás un interceptor de autenticación con URLSession?
Usando URLSessionDelegate o, más modernamente, URLProtocol:
class AuthInterceptor: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
return request.url?.host == "api.miapp.com"
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
var modificado = request
if let token = KeychainManager.shared.token {
modificado.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return modificado
}
override func startLoading() {
let task = URLSession(configuration: .default).dataTask(with: request) { data, response, error in
if let response { self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) }
if let data { self.client?.urlProtocol(self, didLoad: data) }
if let error { self.client?.urlProtocol(self, didFailWithError: error) }
self.client?.urlProtocolDidFinishLoading(self)
}
task.resume()
}
override func stopLoading() {}
}29. ¿Cómo cancelás una tarea asíncrona de red?
class BuscadorViewModel: ObservableObject {
@Published var resultados: [Item] = []
private var tareaActual: Task<Void, Never>?
func buscar(query: String) {
tareaActual?.cancel() // cancela la búsqueda anterior
tareaActual = Task {
do {
let items: [Item] = try await api.buscar(query: query)
guard !Task.isCancelled else { return }
await MainActor.run { resultados = items }
} catch is CancellationError {
// ignorar — cancelación esperada
} catch {
print("Error: \(error)")
}
}
}
}Testing con XCTest
30. ¿Cómo estructurás un unit test en Swift?
import XCTest
@testable import MiApp
final class CalculadoraTests: XCTestCase {
var calculadora: Calculadora!
override func setUp() {
super.setUp()
calculadora = Calculadora()
}
override func tearDown() {
calculadora = nil
super.tearDown()
}
func test_suma_dosNumerosPositivos_retornaSumaCorrecta() {
// Given
let a = 3.0
let b = 4.0
// When
let resultado = calculadora.sumar(a, b)
// Then
XCTAssertEqual(resultado, 7.0, accuracy: 0.001)
}
func test_dividir_porcero_lanzaError() {
XCTAssertThrowsError(try calculadora.dividir(10, por: 0)) { error in
XCTAssertEqual(error as? CalculadoraError, .divisionPorCero)
}
}
}Convención de nombres: test_[método]_[condición]_[resultadoEsperado]. Hace los tests autodocumentados.
31. ¿Cómo testeás código async/await?
final class APIClientTests: XCTestCase {
func test_fetch_respuestaExitosa_retornaUsuario() async throws {
// Given
let mockSession = MockURLSession()
mockSession.respuesta = Data("""
{"id": 1, "nombre": "Ana"}
""".utf8)
let cliente = APIClient(session: mockSession)
// When
let usuario: Usuario = try await cliente.fetch("https://api.test/usuario/1")
// Then
XCTAssertEqual(usuario.id, 1)
XCTAssertEqual(usuario.nombre, "Ana")
}
}A partir de Xcode 13, los tests pueden ser async throws directamente — no necesitás XCTestExpectation para código async.
32. ¿Cómo usás mocks para aislar dependencias?
// Protocolo que abstrae la dependencia
protocol ServicioClima {
func fetchClima(ciudad: String) async throws -> Clima
}
// Implementación real
class ServicioClimaReal: ServicioClima {
func fetchClima(ciudad: String) async throws -> Clima {
// llamada real a la API
}
}
// Mock para tests
class MockServicioClima: ServicioClima {
var climaARetornar: Clima?
var errorALanzar: Error?
func fetchClima(ciudad: String) async throws -> Clima {
if let error = errorALanzar { throw error }
return climaARetornar ?? Clima(temp: 20, descripcion: "Soleado")
}
}
// ViewModel testeable
class ClimaViewModel {
let servicio: ServicioClima // inyección de dependencia
init(servicio: ServicioClima) { self.servicio = servicio }
}Patrones de arquitectura
33. ¿Cómo implementás MVVM en una pantalla real?
// Model
struct Producto: Codable {
let id: Int
let nombre: String
let precio: Double
}
// ViewModel
@MainActor
class ProductosViewModel: ObservableObject {
@Published var productos: [Producto] = []
@Published var cargando = false
@Published var error: String?
private let repositorio: ProductoRepositorio
init(repositorio: ProductoRepositorio = ProductoRepositorioReal()) {
self.repositorio = repositorio
}
func cargar() async {
cargando = true
defer { cargando = false }
do {
productos = try await repositorio.fetchProductos()
} catch {
self.error = error.localizedDescription
}
}
}
// View
struct ProductosView: View {
@StateObject private var vm = ProductosViewModel()
var body: some View {
Group {
if vm.cargando {
ProgressView()
} else {
List(vm.productos, id: \.id) { producto in
Text("\(producto.nombre) — $\(producto.precio, specifier: "%.2f")")
}
}
}
.task { await vm.cargar() }
}
}34. ¿Qué es el patrón Coordinator y por qué se usa?
El Coordinator extrae la lógica de navegación fuera del ViewController, eliminando el acoplamiento entre pantallas.
protocol Coordinator: AnyObject {
var navigationController: UINavigationController { get }
func start()
}
class AppCoordinator: Coordinator {
let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = HomeViewController()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
func mostrarDetalle(item: Item) {
let vc = DetalleViewController(item: item)
vc.coordinator = self
navigationController.pushViewController(vc, animated: true)
}
func mostrarPerfil() {
let childCoordinator = PerfilCoordinator(navigationController: navigationController)
childCoordinator.start()
}
}Sin Coordinator: el HomeVC conoce al DetalleVC — cambiar la navegación requiere tocar múltiples VCs. Con Coordinator: el HomeVC solo avisa "quiero ver el detalle" y el Coordinator decide cómo.
35. ¿Qué es Combine y cuándo lo usás sobre async/await?
Combine es un framework reactivo para manejar flujos de valores en el tiempo. Con async/await disponible, Combine sigue siendo útil para:
- Combinar múltiples publishers (
combineLatest,zip,merge) - Debounce y throttle (ej: búsqueda en tiempo real)
- Cadenas declarativas de transformaciones
// Búsqueda con debounce — más limpio con Combine
class BuscadorViewModel: ObservableObject {
@Published var query = ""
@Published var resultados: [Item] = []
private var cancellables = Set<AnyCancellable>()
init() {
$query
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.sink { [weak self] texto in
Task { await self?.buscar(texto) }
}
.store(in: &cancellables)
}
}Publicación en App Store
36. ¿Cuál es el proceso completo de publicación en App Store?
- 1Certificados y perfiles: generás un Distribution Certificate en el Apple Developer Portal y un Provisioning Profile de tipo "App Store Distribution".
- 2Configuración en Xcode:
Bundle IDdebe coincidir con App Store Connect, version y build number correctos. - 3Archive:
Product → Archiveen Xcode. - 4Validación: Xcode valida el binario contra las reglas de Apple.
- 5Upload:
Distribute App → App Store Connect. - 6TestFlight: probás internamente (o con testers externos) antes del submit.
- 7Submit for Review: en App Store Connect completás metadata, screenshots, privacy policy, y enviás a revisión.
- 8Revisión: típicamente 24-48hs (puede extenderse).
37. ¿Qué son los App Capabilities y cuándo los configurás?
Los capabilities habilitan servicios que requieren entitlements especiales: Push Notifications, HealthKit, Sign in with Apple, iCloud, App Groups, etc.
// Para Push Notifications — registrar el device token
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("Device token: \(token)")
// enviar token al backend
}
// Pedir permiso
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}38. ¿Cómo manejás el versionado semántico en iOS?
- Version (CFBundleShortVersionString): formato
MAJOR.MINOR.PATCHvisible al usuario. Cambiá MAJOR en cambios breaking, MINOR en features nuevas, PATCH en bugfixes. - Build number (CFBundleVersion): entero que incrementa con cada upload a App Store Connect. Puede reiniciarse por version.
En CI/CD suele automatizarse:
# Con agvtool
agvtool new-marketing-version 2.1.0
agvtool next-version -all # incrementa build number
# Con fastlane
fastlane run increment_build_number
fastlane run increment_version_number bump_type:minor39. ¿Qué son los App Groups y para qué se usan?
Los App Groups permiten compartir datos entre tu app principal y extensiones (widgets, Share Extension, Notification Extension) o entre apps del mismo developer.
// Escribir en el shared container
let defaults = UserDefaults(suiteName: "group.com.miempresa.miapp")
defaults?.set("valor compartido", forKey: "clave")
// Acceder desde un Widget Extension
let defaults = UserDefaults(suiteName: "group.com.miempresa.miapp")
let valor = defaults?.string(forKey: "clave")
// Shared files con App Group
let containerURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.com.miempresa.miapp")
let archivoURL = containerURL?.appendingPathComponent("datos.json")40. ¿Cómo usás Instruments para detectar memory leaks?
- 1
Xcode → Product → Profile (⌘I)para abrir Instruments. - 2Elegís el template Leaks o Allocations.
- 3Ejecutás los flujos más complejos de la app.
- 4Leaks marca con una X roja si detecta objetos no liberados.
- 5En Allocations buscás objetos cuya cuenta crece sin límite.
En código — cómo detectar leaks sin Instruments:
// Agregar print en deinit para confirmar que el objeto se libera
class MiViewController: UIViewController {
deinit {
print("✅ MiViewController liberado")
}
}
// Si este mensaje no aparece al cerrar la pantalla → retain cycleTips para la entrevista técnica iOS
Antes de responder, contextualizá. Si te preguntan "¿usarías weak o unowned?" no respondas con una sola palabra. Explicá cuándo aplica cada uno y dá un ejemplo.
Conocé los trade-offs. No existe la respuesta "siempre usá X". MVVM no es mejor que MVC para una pantalla de dos botones. async/await no reemplaza Combine en todos los casos. Demostrá que pensás en contexto.
Errores que cuestan el puesto:
- Forzar unwrap (
!) sin explicar por qué es seguro en ese contexto - Desconocer la diferencia entre
weakyunowned - Confundir
@StateObjectcon@ObservedObject(el primero crea y posee; el segundo observa uno que ya existe) - No saber que los tests de async/await son simplemente
async throws— sin expectativas ni waiters
Lo que diferencia a un senior: no solo responde qué hace algo, sino cuándo NO usarlo y qué problemas puede causar.
*¿Querés practicar estas respuestas en voz alta, recibir feedback y detectar los puntos débiles antes de la entrevista real? InterviewHack.ai genera preguntas personalizadas basadas en la vacante concreta a la que aplicás — no preguntas genéricas, sino las que haría el panel de esa empresa.*