diciembre 21, 2025
20 min de lectura

Preguntas para Entrevistas de Desarrollador Mobile Senior (iOS): Guía Completa

interview
career-advice
job-search
Preguntas para Entrevistas de Desarrollador Mobile Senior (iOS): Guía Completa
Milad Bonakdar

Milad Bonakdar

Autor

Domina el desarrollo avanzado de iOS con preguntas esenciales para entrevistas que cubren patrones de arquitectura, optimización del rendimiento, concurrencia, Core Data y diseño de sistemas para desarrolladores senior.


Introducción

Se espera que los desarrolladores senior de iOS diseñen aplicaciones robustas y escalables, manteniendo al mismo tiempo una alta calidad de código y rendimiento. Este rol requiere un profundo conocimiento de los frameworks de iOS, patrones de diseño, gestión de memoria y la capacidad de tomar decisiones arquitectónicas informadas.

Esta guía completa cubre preguntas esenciales de entrevistas para Desarrolladores Senior de iOS, abarcando conceptos avanzados de Swift, patrones arquitectónicos, optimización del rendimiento, concurrencia y diseño de sistemas. Cada pregunta incluye respuestas detalladas, evaluación de rareza y niveles de dificultad.


Swift Avanzado y Características del Lenguaje (6 Preguntas)

1. Explica la gestión de memoria de Swift y ARC (Automatic Reference Counting).

Respuesta: ARC gestiona automáticamente la memoria rastreando y gestionando las referencias a las instancias de clase.

  • Cómo funciona: Cada instancia de clase tiene un contador de referencias. Cuando el contador llega a cero, la instancia se desasigna.
  • Referencias Fuertes: Predeterminado. Aumenta el contador de referencias.
  • Referencias Débiles: No aumentan el contador de referencias. Automáticamente se convierten en nil cuando la instancia se desasigna.
  • Referencias No Poseídas: No aumentan el contador de referencias, pero asumen que la instancia siempre existe.
  • Ciclos de Retención: Ocurren cuando dos objetos mantienen referencias fuertes entre sí, impidiendo la desasignación.
class Person {
    var name: String
    var apartment: Apartment?
    
    init(name: String) { self.name = name }
    deinit { print("\(name) está siendo desasignado") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // débil para romper el ciclo de retención
    
    init(unit: String) { self.unit = unit }
    deinit { print("Apartamento \(unit) está siendo desasignado") }
}

Rareza: Muy Común Dificultad: Difícil


2. ¿Qué son los Genéricos en Swift y por qué son útiles?

Respuesta: Los genéricos te permiten escribir funciones y tipos flexibles y reutilizables que pueden funcionar con cualquier tipo.

  • Beneficios: Reutilización de código, seguridad de tipos, rendimiento (sin sobrecarga en tiempo de ejecución)
  • Restricciones de Tipo: Restringen los tipos genéricos a protocolos o clases específicas
  • Tipos Asociados: Se utilizan en protocolos para definir tipos de marcadores de posición
// Función genérica
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

// Tipo genérico
struct Stack<Element> {
    private var items: [Element] = []
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
}

// Restricción de tipo
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

Rareza: Común Dificultad: Media


3. Explica la diferencia entre closures escaping y non-escaping.

Respuesta:

  • Non-escaping (predeterminado): El closure se ejecuta antes de que la función regrese. El compilador puede optimizar mejor.
  • Escaping (@escaping): El closure sobrevive a la función (se almacena en una propiedad, se llama asíncronamente). Debe capturar explícitamente self.
class NetworkManager {
    var completionHandlers: [() -> Void] = []
    
    // Closure de escape - almacenado para más tarde
    func fetchData(completion: @escaping (Data?) -> Void) {
        URLSession.shared.dataTask(with: url) { data, _, _ in
            completion(data)  // Llamado después de que la función regrese
        }.resume()
    }
    
    // Closure no de escape - ejecutado inmediatamente
    func processData(_ data: Data, transform: (Data) -> String) -> String {
        return transform(data)  // Llamado antes de que la función regrese
    }
}

Rareza: Común Dificultad: Media


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

Respuesta: Estas son funciones de orden superior para transformar colecciones:

  • map: Transforma cada elemento y devuelve un array de resultados
  • compactMap: Similar a map pero filtra los valores nil
  • flatMap: Aplana arrays anidados en un único array
let numbers = [1, 2, 3, 4, 5]

// map
let doubled = numbers.map { $0 * 2 }
// [2, 4, 6, 8, 10]

// compactMap - elimina nil
let strings = ["1", "2", "three", "4"]
let validNumbers = strings.compactMap { Int($0) }
// [1, 2, 4]

// flatMap - aplana arrays
let nested = [[1, 2], [3, 4], [5]]
let flattened = nested.flatMap { $0 }
// [1, 2, 3, 4, 5]

Rareza: Común Dificultad: Fácil


5. Explica los Property Wrappers en Swift.

Respuesta: Los property wrappers añaden una capa de separación entre el código que gestiona cómo se almacena una propiedad y el código que define una propiedad.

  • Ejemplos Incorporados: @State, @Published, @AppStorage en SwiftUI
  • Wrappers Personalizados: Definen comportamientos de propiedad reutilizables
@propertyWrapper
struct Capitalized {
    private var value: String = ""
    
    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
}

struct User {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
}

var user = User()
user.firstName = "john"
print(user.firstName)  // "John"

// Wrapper de UserDefaults
@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

struct Settings {
    @UserDefault(key: "username", defaultValue: "Guest")
    static var username: String
}

Rareza: Media Dificultad: Difícil


6. ¿Qué es el tipo Result y cómo se utiliza?

Respuesta: Result es un enum que representa éxito o fracaso, haciendo que el manejo de errores sea más explícito.

  • Definición: enum Result<Success, Failure: Error>
  • Beneficios: Manejo de errores con seguridad de tipos, contratos de API más claros, mejor que lanzar funciones para código asíncrono
enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

func fetchUser(id: Int, completion: @escaping (Result<User, NetworkError>) -> Void) {
    guard let url = URL(string: "https://api.example.com/users/\(id)") else {
        completion(.failure(.invalidURL))
        return
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        guard let data = data else {
            completion(.failure(.noData))
            return
        }
        
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(.decodingError))
        }
    }.resume()
}

// Uso
fetchUser(id: 1) { result in
    switch result {
    case .success(let user):
        print("Usuario: \(user.name)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

Rareza: Común Dificultad: Media


Patrones de Arquitectura (5 Preguntas)

7. Explica el patrón MVVM (Model-View-ViewModel).

Respuesta: MVVM separa la lógica de la interfaz de usuario de la lógica de negocio, haciendo que el código sea más fácil de probar y mantener.

Loading diagram...
  • Model: Datos y lógica de negocio
  • View: UI (UIViewController, SwiftUI View)
  • ViewModel: Lógica de presentación, transforma los datos del modelo para la vista
  • Beneficios: Testeable (ViewModel no tiene dependencias de la UI), ViewModels reutilizables, clara separación de responsabilidades
// Model
struct User {
    let id: Int
    let firstName: String
    let lastName: String
}

// ViewModel
class UserViewModel {
    private let user: User
    
    var fullName: String {
        "\(user.firstName) \(user.lastName)"
    }
    
    var displayText: String {
        "Usuario #\(user.id): \(fullName)"
    }
    
    init(user: User) {
        self.user = user
    }
}

// View
class UserViewController: UIViewController {
    private let viewModel: UserViewModel
    private let label = UILabel()
    
    init(viewModel: UserViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        label.text = viewModel.displayText
    }
}

Rareza: Muy Común Dificultad: Media


8. ¿Qué es el patrón Coordinator y por qué usarlo?

Respuesta: El patrón Coordinator separa la lógica de navegación de los controladores de vista.

  • Problema: Controladores de Vista Masivos con lógica de navegación mezclada con lógica de UI
  • Solución: Los Coordinators manejan el flujo de navegación
  • Beneficios: Controladores de vista reutilizables, navegación testeable, flujo de aplicación claro
protocol Coordinator {
    var navigationController: UINavigationController { get }
    func start()
}

class AppCoordinator: Coordinator {
    let navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        showLogin()
    }
    
    func showLogin() {
        let loginVC = LoginViewController()
        loginVC.coordinator = self
        navigationController.pushViewController(loginVC, animated: true)
    }
    
    func showHome() {
        let homeVC = HomeViewController()
        homeVC.coordinator = self
        navigationController.pushViewController(homeVC, animated: true)
    }
}

Rareza: Media Dificultad: Difícil


9. Explica la Inyección de Dependencias en iOS.

Respuesta: La Inyección de Dependencias es un patrón de diseño donde las dependencias se proporcionan a un objeto en lugar de crearse internamente.

  • Beneficios: Testeabilidad (inyectar mocks), flexibilidad, bajo acoplamiento
  • Tipos:
    • Inyección de Constructor: Pasar dependencias a través del inicializador (más común)
    • Inyección de Propiedad: Establecer dependencias después de la inicialización
    • Inyección de Método: Pasar dependencias como parámetros de método
// Protocolo para la abstracción
protocol NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}

// Implementación concreta
class URLSessionNetworkService: NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        // Llamada de red real
    }
}

// Mock para pruebas
class MockNetworkService: NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        completion(.success(Data()))
    }
}

// ViewModel con inyección de dependencias
class UserViewModel {
    private let networkService: NetworkService
    
    // Inyección de constructor
    init(networkService: NetworkService) {
        self.networkService = networkService
    }
    
    func loadUsers() {
        networkService.fetchData { result in
            // Manejar el resultado
        }
    }
}

// Uso
let viewModel = UserViewModel(networkService: URLSessionNetworkService())

// Pruebas
let testViewModel = UserViewModel(networkService: MockNetworkService())

Rareza: Común Dificultad: Media


10. ¿Qué es el patrón Repository?

Respuesta: El patrón Repository abstrae la lógica de acceso a datos, proporcionando una API limpia para las operaciones de datos.

  • Beneficios: Lógica de datos centralizada, fácil de cambiar las fuentes de datos (API, base de datos, caché), testeable
  • Implementación: El repositorio coordina entre múltiples fuentes de datos
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func saveUser(_ user: User) async throws
}

class UserRepositoryImpl: UserRepository {
    private let apiService: APIService
    private let cacheService: CacheService
    
    init(apiService: APIService, cacheService: CacheService) {
        self.apiService = apiService
        self.cacheService = cacheService
    }
    
    func getUser(id: Int) async throws -> User {
        // Verificar la caché primero
        if let cachedUser = try? await cacheService.getUser(id: id) {
            return cachedUser
        }
        
        // Obtener de la API
        let user = try await apiService.fetchUser(id: id)
        
        // Actualizar la caché
        try? await cacheService.saveUser(user)
        
        return user
    }
    
    func saveUser(_ user: User) async throws {
        try await apiService.updateUser(user)
        try await cacheService.saveUser(user)
    }
}

Rareza: Media Dificultad: Media


11. Explica las diferencias entre MVC, MVP y MVVM.

Respuesta:

  • MVC (Model-View-Controller):
    • Patrón predeterminado de Apple
    • El controlador media entre el Modelo y la Vista
    • Problema: Controladores de Vista Masivos
  • MVP (Model-View-Presenter):
    • El Presentador maneja toda la lógica de la UI
    • La Vista es pasiva (solo muestra datos)
    • Mejor testeabilidad que MVC
  • MVVM (Model-View-ViewModel):
    • El ViewModel expone flujos de datos
    • La Vista se enlaza al ViewModel
    • Mejor para la programación reactiva (Combine, RxSwift)

Rareza: Común Dificultad: Difícil


Rendimiento y Optimización (5 Preguntas)

12. ¿Cómo optimizas el rendimiento de la vista de tabla y la vista de colección?

Respuesta: Múltiples estrategias mejoran el rendimiento del desplazamiento:

  1. Reutilización de Celdas: Utiliza dequeueReusableCell correctamente
  2. Evitar Operaciones Pesadas: No realices cálculos costosos en cellForRowAt
  3. Optimización de Imágenes:
    • Redimensiona las imágenes al tamaño de visualización
    • Utiliza hilos en segundo plano para el procesamiento de imágenes
    • Almacena en caché las imágenes decodificadas
  4. Prefetching: Implementa UITableViewDataSourcePrefetching
  5. Almacenamiento en Caché de Altura: Almacena en caché las alturas de las celdas calculadas
  6. Evitar la Transparencia: Las vistas opacas se renderizan más rápido
class ImageTableViewCell: UITableViewCell {
    static let identifier = "ImageCell"
    private let customImageView = UIImageView()
    
    override func prepareForReuse() {
        super.prepareForReuse()
        customImageView.image = nil  // Limpiar la imagen antigua
    }
}

// En el controlador de vista
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.identifier, for: indexPath) as! ImageTableViewCell
    
    // Cargar la imagen asíncronamente
    DispatchQueue.global(qos: .userInitiated).async {
        let image = self.loadAndResizeImage(at: indexPath)
        DispatchQueue.main.async {
            cell.imageView?.image = image
        }
    }
    
    return cell
}

Rareza: Muy Común Dificultad: Media


13. Explica Instruments y cómo lo utilizas para el perfilado del rendimiento.

Respuesta: Instruments es la herramienta de análisis de rendimiento de Xcode.

  • Instruments Comunes:
    • Time Profiler: Identifica el código intensivo en CPU
    • Allocations: Rastrea las asignaciones de memoria y las fugas
    • Leaks: Detecta fugas de memoria
    • Network: Supervisa la actividad de la red
    • Energy Log: Analiza el uso de la batería
  • Flujo de Trabajo:
    1. Perfilar la aplicación (Cmd+I)
    2. Elegir el instrumento
    3. Grabar e interactuar con la aplicación
    4. Analizar el árbol de llamadas y la línea de tiempo
    5. Identificar los cuellos de botella

Rareza: Común Dificultad: Media


14. ¿Cómo detectas y corriges las fugas de memoria?

Respuesta: Las fugas de memoria ocurren cuando los objetos no se desasignan cuando ya no son necesarios.

  • Causas Comunes:
    • Ciclos de retención (ciclos de referencia fuertes)
    • Closures que capturan self fuertemente
    • Delegados no marcados como weak
  • Detección:
    • Herramienta Instruments Leaks
    • Depurador de gráficos de memoria en Xcode
    • Observar el aumento del uso de memoria
  • Correcciones:
    • Utiliza weak o unowned para los delegados
    • Utiliza [weak self] o [unowned self] en los closures
    • Romper los ciclos de retención
class ViewController: UIViewController {
    var closure: (() -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // MAL - ciclo de retención
        closure = {
            self.view.backgroundColor = .red
        }
        
        // BIEN - weak self
        closure = { [weak self] in
            self?.view.backgroundColor = .red
        }
        
        // BIEN - unowned self (si self siempre existe)
        closure = { [unowned self] in
            self.view.backgroundColor = .red
        }
    }
}

Rareza: Muy Común Dificultad: Media


15. ¿Qué técnicas utilizas para la optimización del inicio de la aplicación?

Respuesta: Un inicio de aplicación más rápido mejora la experiencia del usuario:

  1. Carga Perezosa: Inicializar objetos solo cuando sea necesario
  2. Reducir la Carga de Dylib: Minimizar las bibliotecas dinámicas
  3. Optimizar application:didFinishLaunching:
    • Mover el trabajo no crítico al fondo
    • Aplazar la inicialización pesada
  4. Tamaño Binario: Un binario más pequeño se carga más rápido
  5. Evitar Operaciones Pesadas: No bloquear el hilo principal
  6. Medir: Utiliza la plantilla App Launch de Instruments
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    // Crítico: Configurar la ventana y el controlador de vista raíz
    setupWindow()
    
    // Aplazar el trabajo no crítico
    DispatchQueue.main.async {
        self.setupAnalytics()
        self.setupCrashReporting()
    }
    
    // Trabajo en segundo plano
    DispatchQueue.global(qos: .background).async {
        self.preloadData()
    }
    
    return true
}

Rareza: Común Dificultad: Media


16. ¿Cómo gestionas el almacenamiento en caché y la carga de imágenes?

Respuesta: El manejo eficiente de las imágenes es crucial para el rendimiento:

  • Estrategias:
    • Caché de Memoria: Acceso rápido, tamaño limitado
    • Caché de Disco: Persistente, mayor capacidad
    • Descarga: Obtener de la red
  • Librerías: SDWebImage, Kingfisher (gestionan el almacenamiento en caché automáticamente)
  • Implementación Personalizada:
class ImageCache {
    static let shared = ImageCache()
    
    private let memoryCache = NSCache<NSString, UIImage>()
    private let fileManager = FileManager.default
    private let cacheDirectory: URL
    
    private init() {
        cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("ImageCache")
        try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
    }
    
    func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
        let key = url.absoluteString as NSString
        
        // Verificar la caché de memoria
        if let cachedImage = memoryCache.object(forKey: key) {
            completion(cachedImage)
            return
        }
        
        // Verificar la caché de disco
        let fileURL = cacheDirectory.appendingPathComponent(url.lastPathComponent)
        if let diskImage = UIImage(contentsOfFile: fileURL.path) {
            memoryCache.setObject(diskImage, forKey: key)
            completion(diskImage)
            return
        }
        
        // Descargar
        URLSession.shared.dataTask(with: url) { data, _, _ in
            guard let data = data, let image = UIImage(data: data) else {
                completion(nil)
                return
            }
            
            // Guardar en las cachés
            self.memoryCache.setObject(image, forKey: key)
            try? data.write(to: fileURL)
            
            DispatchQueue.main.async {
                completion(image)
            }
        }.resume()
    }
}

Rareza: Común Dificultad: Difícil


Concurrencia y Programación Asíncrona (4 Preguntas)

17. Explica async/await en Swift.

Respuesta: El modelo de concurrencia moderno de Swift introducido en Swift 5.5.

  • Beneficios: Sintaxis más limpia que los completion handlers, manejo de errores más fácil, seguridad de hilos impuesta por el compilador
  • Palabras Clave:
    • async: Marca una función que puede suspenderse
    • await: Marca un punto de suspensión
    • Task: Crea un nuevo contexto asíncrono
    • actor: Tipo de referencia seguro para hilos
// Antigua forma con completion handlers
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        // Manejar el resultado
    }.resume()
}

// Nueva forma con async/await
func fetchUser(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    let user = try JSONDecoder().decode(User.self, from: data)
    return user
}

// Uso
Task {
    do {
        let user = try await fetchUser(id: 1)
        print(user.name)
    } catch {
        print("Error: \(error)")
    }
}

// Ejecución paralela
async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
let users = try await [user1, user2]

Rareza: Muy Común Dificultad: Difícil


18. ¿Qué son los Actors en Swift?

Respuesta: Los Actors son tipos de referencia que protegen su estado mutable de las carreras de datos.

  • Seguridad de Hilos: Solo una tarea puede acceder al estado mutable del actor a la vez
  • Sincronización Automática: El compilador impone el acceso seguro
  • Main Actor: Actor especial para las actualizaciones de la UI
actor BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
    
    func getBalance() -> Double {
        return balance
    }
}

// Uso
let account = BankAccount()

Task {
    await account.deposit(amount: 100)
    let balance = await account.getBalance()
    print("Balance: \(balance)")
}

// Main Actor para actualizaciones de la UI
@MainActor
class ViewModel: ObservableObject {
    @Published var data: [String] = []
    
    func loadData() async {
        let newData = await fetchDataFromAPI()
        // Automáticamente en el hilo principal
        self.data = newData
    }
}

Rareza: Media Dificultad: Difícil


19. Explica el framework Combine.

Respuesta: Combine es el framework de programación reactiva de Apple.

  • Conceptos Centrales:
    • Publisher: Emite valores a lo largo del tiempo
    • Subscriber: Recibe valores
    • Operator: Transforma valores
  • Beneficios: Declarativo, composable, operadores incorporados
  • Casos de Uso: Redes, manejo de entrada del usuario, enlace de datos
import Combine

class UserViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var users: [User] = []
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $searchText
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .filter { !$0.isEmpty }
            .flatMap { query in
                self.searchUsers(query: query)
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] users in
                self?.users = users
            }
            .store(in: &cancellables)
    }
    
    func searchUsers(query: String) -> AnyPublisher<[User], Never> {
        // Devolver publisher
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [User].self, decoder: JSONDecoder())
            .replaceError(with: [])
            .eraseToAnyPublisher()
    }
}

Rareza: Común Dificultad: Difícil


20. ¿Cuál es la diferencia entre las colas Serial y Concurrent?

Respuesta: Las colas de despacho ejecutan tareas de forma serial o concurrente:

  • Cola Serial: Ejecuta una tarea a la vez en orden FIFO. Las tareas esperan a que la tarea anterior se complete.
  • Cola Concurrent: Ejecuta múltiples tareas simultáneamente. Las tareas comienzan en orden FIFO pero pueden terminar en cualquier orden.
  • Cola Principal: Cola serial especial para las actualizaciones de la UI
// Cola serial
let serialQueue = DispatchQueue(label: "com.app.serial")
serialQueue.async { print("Tarea 1") }
serialQueue.async { print("Tarea 2") }
// Salida: Tarea 1, Tarea 2 (siempre en orden)

// Cola concurrente
let concurrentQueue = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent)
concurrentQueue.async { print("Tarea A") }
concurrentQueue.async { print("Tarea B") }
// Salida: Tarea A, Tarea B O Tarea B, Tarea A (orden no garantizado)

// Barrera para las operaciones de escritura
concurrentQueue.async(flags: .barrier) {
    // Acceso exclusivo - ninguna otra tarea se ejecuta simultáneamente
    print("Operación de escritura")
}

Rareza: Común Dificultad: Media


Core Data y Persistencia (3 Preguntas)

21. Explica la arquitectura de Core Data y sus componentes principales.

Respuesta: Core Data es el framework de persistencia y gráfico de objetos de Apple.

Loading diagram...
  • NSManagedObjectModel: Definición del esquema (entidades, atributos, relaciones)
  • NSPersistentStoreCoordinator: Coordina entre el contexto y el almacén
  • NSManagedObjectContext: Memoria de trabajo para los objetos (como un bloc de notas)
  • NSPersistentStore: Almacenamiento real (SQLite, binario, en memoria)

Rareza: Común Dificultad: Media


22. ¿Cómo gestionas la concurrencia en Core Data?

Respuesta: Los contextos de Core Data no son seguros para hilos. Utiliza patrones de concurrencia adecuados:

  • Tipos de Contexto:
    • Contexto de Cola Principal: Para las operaciones de la UI
    • Contexto de Cola Privada: Para las operaciones en segundo plano
  • Buenas Prácticas:
    • Nunca pases objetos gestionados entre hilos
    • Utiliza perform o performAndWait para las operaciones de contexto
    • Pasa los IDs de objeto entre los contextos
class CoreDataManager {
    let persistentContainer: NSPersistentContainer
    
    var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    
    func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
        persistentContainer.performBackgroundTask { context in
            block(context)
            
            if context.hasChanges {
                try? context.save()
            }
        }
    }
}

// Uso
coreDataManager.performBackgroundTask { context in
    let user = User(context: context)
    user.name = "John"
    // El contexto se guarda automáticamente
}

Rareza: Media Dificultad: Difícil


23. ¿Qué es NSFetchedResultsController y cuándo lo utilizas?

Respuesta: NSFetchedResultsController gestiona eficientemente los resultados de Core Data para las vistas de tabla/colección.

  • Beneficios:
    • Seguimiento automático de cambios
    • Eficiente en memoria (batching)
    • Gestión de secciones
    • Actualizaciones automáticas de la UI
  • Caso de Uso: Mostrar objetos de Core Data en vistas de tabla/colección
class UsersViewController: UITableViewController, NSFetchedResultsControllerDelegate {
    var fetchedResultsController: NSFetchedResultsController<User>!
    
    func setupFetchedResultsController() {
        let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        
        fetchedResultsController = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: context,
            sectionNameKeyPath: nil,
            cacheName: nil
        )
        
        fetchedResultsController.delegate = self
        try? fetchedResultsController.performFetch()
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return fetchedResultsController.sections?[section].numberOfObjects ?? 0
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.reloadData()
    }
}

Rareza: Media Dificultad: Media


Diseño de Sistemas y Mejores Prácticas (2 Preguntas)

24. ¿Cómo diseñarías una aplicación móvil offline-first?

Respuesta: Las aplicaciones offline-first funcionan sin Internet y se sincronizan cuando están conectadas.

  • Arquitectura:
    • Base de datos local como fuente de verdad (Core Data, Realm)
    • Capa de sincronización para reconciliar con el servidor
    • Estrategia de resolución de conflictos
  • Estrategias:
    • UI Optimista: Mostrar los cambios inmediatamente, sincronizar en segundo plano
    • Operaciones de Cola: Almacenar las solicitudes fallidas, reintentar cuando esté en línea
    • Timestamp/Versión: Rastrear la frescura de los datos
  • Desafíos:
    • Resolución de conflictos (última escritura gana, fusión, elección del usuario)
    • Consistencia de los datos
    • Límites de almacenamiento

Rareza: Media Dificultad: Difícil


25. ¿Cuáles son tus estrategias para la seguridad de la aplicación en iOS?

Respuesta: Múltiples capas protegen la aplicación y los datos del usuario:

  1. Protección de Datos:
    • Keychain para datos sensibles (contraseñas, tokens)
    • Cifrar los datos en reposo
    • Utilizar la API de Protección de Datos
  2. Seguridad de la Red:
    • Solo HTTPS (App Transport Security)
    • Certificate pinning para las APIs críticas
    • Validar los certificados SSL
  3. Seguridad del Código:
    • Ofuscación para la lógica sensible
    • Detección de jailbreak
    • No tener secretos codificados
  4. Autenticación:
    • Aut
Newsletter subscription

Consejos de carrera semanales que realmente funcionan

Recibe las últimas ideas directamente en tu bandeja de entrada

Deja de Postularte. Comienza a Ser Contratado.

Transforma tu currículum en un imán de entrevistas con optimización impulsada por IA confiada por buscadores de empleo en todo el mundo.

Comienza gratis

Compartir esta publicación

Duplica tus Llamadas para Entrevistas

Los candidatos que adaptan sus currículums a la descripción del trabajo obtienen 2.5 veces más entrevistas. Usa nuestra IA para personalizar tu CV automáticamente para cada solicitud al instante.