InterviewHack.ai
Start free
Blog/Preguntas de entrevista de iOS y Swift — 40 con código

Preguntas de entrevista de iOS y Swift — 40 con código

16 de septiembre de 2026

iosswift

40 preguntas de iOS/Swift que hacen en entrevistas reales: ARC, protocolos, concurrencia Swift (async/await, actors), UIKit vs SwiftUI, Core Data. Con código Swift.

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.

swift
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).

swift
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.

swift
// 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:

swift
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.

swift
@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ático

Los property wrappers más conocidos en iOS: @State, @Binding, @Published, @AppStorage, @UserDefault.


5. ¿Cuál es la diferencia entre `map`, `flatMap` y `compactMap`?

swift
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.

swift
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 → deinit

7. ¿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.

swift
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 — cycle

La solución: weak o unowned

swift
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 ser nil durante su vida útil. Siempre es opcional.
  • unowned: cuando la referencia NUNCA va a ser nil mientras exista el objeto que la tiene. No es opcional, y si el objeto ya fue liberado, crashea.
swift
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:

swift
viewModel.fetch { [weak self] resultado in
    guard let self else { return }
    self.actualizar(resultado)
}

9. ¿Qué problema causa un retain cycle en una closure?

swift
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).

swift
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.

swift
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.

swift
@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).
swift
// 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.

swift
// 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.

swift
// 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.

swift
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.

swift
@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.

swift
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.

swift
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

swift
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?

swift
// @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:

swift
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?

swift
@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.

swift
// 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.

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?

swift
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?

swift
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:

swift
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?

swift
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?

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?

swift
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?

swift
// 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?

swift
// 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.

swift
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
swift
// 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?

  1. 1Certificados y perfiles: generás un Distribution Certificate en el Apple Developer Portal y un Provisioning Profile de tipo "App Store Distribution".
  2. 2Configuración en Xcode: Bundle ID debe coincidir con App Store Connect, version y build number correctos.
  3. 3Archive: Product → Archive en Xcode.
  4. 4Validación: Xcode valida el binario contra las reglas de Apple.
  5. 5Upload: Distribute App → App Store Connect.
  6. 6TestFlight: probás internamente (o con testers externos) antes del submit.
  7. 7Submit for Review: en App Store Connect completás metadata, screenshots, privacy policy, y enviás a revisión.
  8. 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.

swift
// 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.PATCH visible 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:

bash
# 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:minor

39. ¿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.

swift
// 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. 1Xcode → Product → Profile (⌘I) para abrir Instruments.
  2. 2Elegís el template Leaks o Allocations.
  3. 3Ejecutás los flujos más complejos de la app.
  4. 4Leaks marca con una X roja si detecta objetos no liberados.
  5. 5En Allocations buscás objetos cuya cuenta crece sin límite.

En código — cómo detectar leaks sin Instruments:

swift
// 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 cycle

Tips 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 weak y unowned
  • Confundir @StateObject con @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.*

FAQ

¿Cuáles son las preguntas más comunes en entrevistas de iOS developer?+

Las más frecuentes cubren ARC y retain cycles (strong/weak/unowned), la diferencia entre value types y reference types (struct vs class), el sistema de optionals en Swift, concurrencia con async/await y actores, y la gestión de estado en SwiftUI (@State, @Binding, @StateObject). También es muy común que te pidan implementar MVVM o explicar el patrón Coordinator.

¿Cuál es la diferencia entre weak y unowned en Swift?+

Ambas evitan retain cycles, pero difieren en si la referencia puede ser nil. `weak` siempre es opcional — si el objeto referenciado se libera, la variable queda en nil. `unowned` asume que el objeto referenciado siempre existe mientras viva el que lo referencia — si se libera y accedés a la referencia, la app crashea. Usá `weak` cuando la relación de vida es incierta; `unowned` cuando tenés garantía de que el objeto vive tanto como el que lo retiene.

¿Cuál es la diferencia entre @StateObject y @ObservedObject en SwiftUI?+

@StateObject crea y posee el ViewModel — la vista es dueña del objeto y lo mantiene vivo durante toda su vida. @ObservedObject solo observa un ViewModel que alguien más creó (normalmente el padre). Si usás @ObservedObject en la vista que debería ser dueña, el ViewModel se puede recrear inesperadamente al redibujar la vista, perdiendo el estado.

¿Cómo funciona ARC y por qué es importante entenderlo para una entrevista de iOS?+

ARC (Automatic Reference Counting) lleva la cuenta de cuántos objetos apuntan a cada instancia en el heap. Cuando la cuenta llega a cero, libera la memoria automáticamente. Es importante entenderlo porque los retain cycles (cuando dos objetos se retienen mutuamente con referencias strong) evitan que la cuenta llegue a cero, provocando memory leaks. El entrevistador espera que identifiques estos ciclos y que sepas resolverlos con weak o unowned.

¿Qué diferencia hay entre async/await y Combine en Swift?+

async/await es ideal para operaciones puntuales (una petición de red, leer un archivo) y hace el código asíncrono que parece síncrono. Combine es un framework reactivo para flujos de valores en el tiempo: si necesitás debounce en una búsqueda, combinar múltiples publishers, o transformaciones declarativas en cadena, Combine suele ser más expresivo. En proyectos nuevos se usa async/await como base y Combine solo donde agrega claridad.

¿Qué preguntan sobre SwiftUI en entrevistas de iOS senior?+

Las más comunes: diferencias entre @State, @Binding, @StateObject y @ObservedObject; cómo integrar UIKit en SwiftUI con UIViewControllerRepresentable; el ciclo de vida de una app SwiftUI con @main y ScenePhase; uso de .task vs onAppear para operaciones async; y cómo implementar navegación con NavigationStack. En puestos senior también preguntan sobre performance: evitar re-renders innecesarios con Equatable, uso de LazyVStack/LazyHStack, y manejo de estado con @EnvironmentObject.

Related articles

Preguntas de entrevista de Android y Kotlin — 40 con código

40 preguntas de Android/Kotlin para entrevistas: corrutinas, Jetpack Compose, ViewModel, LiveData vs Flow, Room, Hilt. Con código Kotlin real.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY