20 preguntas frecuentes de entrevista sobre iOS. El desarrollo iOS es un área muy demandada centrada en crear apps con Swift, SwiftUI y UIKit, y resolver tareas de lifecycle, concurrencia, red, almacenamiento, rendimiento, pruebas y publicación en App Store. Las preguntas cubren distintos niveles, y puedes practicar respondiéndolas en voz alta en nuestro simulador de entrevistas.
1¿Qué responsabilidades debe tener un UIViewController en una arquitectura típica MVC (Model-View-Controller) en iOS?
En el patrón clásico MVC (Model-View-Controller) de iOS, UIViewController actúa como el controlador que media entre los componentes de la vista y los datos del modelo. Sus responsabilidades principales incluyen gestionar el ciclo de vida de la vista (por ejemplo, viewDidLoad, viewWillAppear), configurar y actualizar los elementos de la interfaz de usuario, manejar las interacciones directas del usuario (como toques de botones, delegados y target-actions) y orquestar transiciones o presentaciones de pantallas. Las responsabilidades ajenas a la interfaz —como el acceso directo a redes, la persistencia y la lógica de negocio pesada— deben delegarse en objetos de servicio o de modelo dedicados para evitar que el controlador de vistas se convierta en un Massive View Controller.
class UserProfileViewController: UIViewController {
private let userService: UserServiceProtocol
private let profileView = UserProfileView()
init(userService: UserServiceProtocol) {
self.userService = userService
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func loadView() {
view = profileView
}
override func viewDidLoad() {
super.viewDidLoad()
profileView.editButton.addTarget(self, action: #selector(didTapEdit), for: .touchUpInside)
fetchProfile()
}
private func fetchProfile() {
userService.fetchCurrentUser { [weak self] result in
DispatchQueue.main.async {
if case .success(let user) = result {
self?.profileView.configure(with: user)
}
}
}
}
@objc private func didTapEdit() {
// Handle user action or trigger navigation
}
}
2¿Cuál es la función de un provisioning profile en la distribución de una aplicación de iOS?
En la distribución de una aplicación de iOS, un provisioning profile actúa como un paquete firmado criptográficamente que vincula los requisitos de seguridad de Apple con el binario de la aplicación y los dispositivos de destino. Su función principal es indicar al sistema operativo iOS y a App Store Connect que la aplicación está autorizada para ejecutarse bajo reglas de distribución específicas. Un provisioning profile agrupa varios elementos críticos: el App ID (Bundle Identifier), el certificado o certificados de firma autorizados que confirman quién compiló la aplicación, los entitlements y capacidades otorgadas a la aplicación (como Push Notifications, Sign in with Apple o iCloud) y, en perfiles que no son de App Store como Development o Ad-Hoc, una lista de UDID (Unique Device Identifier) de dispositivos permitidos. En los perfiles de distribución para App Store se omiten los UDID individuales porque Apple permite la distribución a cualquier dispositivo de usuario a través de App Store.
Provisioning Profile (.mobileprovision)
├── App ID / Bundle Identifier (e.g., com.example.app)
├── Certificates (Public key / Distribution Certificate)
├── Entitlements (e.g., Push Notifications, Associated Domains)
└── Device UDIDs (Present for Development/Ad-Hoc; omitted for App Store distribution)
3¿Qué significa que una aplicación de iOS ejecute tareas en segundo plano y qué límites básicos impone el sistema a ese trabajo?
Ejecutar tareas en segundo plano en iOS significa ejecutar código cuando la aplicación no está activa en pantalla ni visible para el usuario. Cuando un usuario sale de una aplicación, esta pasa del estado activo en primer plano al segundo plano y, poco después, a un estado suspendido donde su ejecución se pausa, aunque su memoria permanece en la RAM. Para preservar la duración de la batería, la capacidad de respuesta del sistema y los recursos del dispositivo, iOS controla estrictamente la ejecución en segundo plano. El sistema limita el tiempo durante el cual una aplicación puede ejecutar código una vez que pasa a segundo plano (normalmente limitado a una ventana breve y finita de aproximadamente 30 segundos, a menos que se utilicen modos específicos en segundo plano o tareas programadas), reduce la prioridad de CPU y red, y puede finalizar aplicaciones suspendidas si el dispositivo experimenta presión de memoria.
4¿Qué es un publisher en Combine y en qué se diferencia de un subscriber en un flujo de datos típico de iOS?
En Combine, un Publisher emite un flujo de valores a lo largo del tiempo y puede finalizar con un evento de finalización (ya sea completándose con éxito o fallando con un error). Declara dos tipos asociados: `Output` (el tipo de datos que produce) y `Failure` (el tipo de error que puede emitir). Un Subscriber recibe valores y eventos del ciclo de vida de un publisher. En un flujo de datos típico de iOS, los publishers actúan como la fuente o los productores de eventos asíncronos (como respuestas de red, notificaciones o entradas del usuario), mientras que los subscribers actúan como los consumidores que reaccionan a los valores emitidos, gestionan errores y responden a la finalización (como actualizar el estado de la interfaz de usuario o escribir en una base de datos). El subscriber se suscribe a un publisher, recibe un token de suscripción y solicita la demanda de elementos.
import Combine
// Publisher: emits a sequence of integers
let numberPublisher = [1, 2, 3].publisher
// Subscriber: consumes the emitted integers
let cancellable = numberPublisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Finished")
case .failure(let error):
print("Error: \(error)")
}
},
receiveValue: { value in
print("Received: \(value)")
}
)
5¿Qué significa async/await en Swift y cómo se utiliza para llamar a una API (Application Programming Interface) asíncrona desde una aplicación de iOS?
`async/await` es la sintaxis nativa de Swift para escribir código asíncrono de manera lineal, legible y secuencial en lugar de usar closures anidados y completion handlers. Marcar una función con `async` le indica al compilador que la función puede suspender su ejecución mientras espera a que finalice un trabajo de larga duración (como solicitudes de red o E/S de archivos). La palabra clave `await` indica un punto de suspensión: la ejecución de la función actual se pausa, liberando el hilo subyacente para realizar otro trabajo, y se reanuda una vez que el resultado esperado o el error está listo. Para llamar a una API asíncrona en una aplicación de iOS, se invoca el método `async` precedido por `await` (y `try await` si la función arroja errores) desde un contexto asíncrono, como dentro de otra función `async` o un bloque `Task`.
6¿Cuándo usarías UserDefaults en una aplicación de iOS y qué tipo de datos no deberían almacenarse allí?
`UserDefaults` está diseñado para almacenar preferencias de usuario, configuraciones e indicadores pequeños y ligeros entre inicios de la aplicación (por ejemplo, preferencias de tema, nivel de volumen o un indicador booleano `hasSeenOnboarding`). Admite tipos de lista de propiedades (plist) como `String`, `Int`, `Double`, `Bool`, `Date`, `Data`, `Array` y `Dictionary`. NO se debe almacenar: 1. Información confidencial o sensible (como contraseñas de usuario, claves privadas o tokens de autenticación), ya que `UserDefaults` almacena los datos sin cifrar en un archivo plist de texto plano. 2. Conjuntos de datos grandes, documentos o archivos multimedia (como imágenes, audio o payloads JSON grandes), ya que el archivo plist completo se carga en memoria al acceder a él, lo que genera una alta sobrecarga de memoria y ralentiza el inicio de la aplicación.
// Saving simple preference values
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
UserDefaults.standard.set("dark", forKey: "preferredTheme")
// Reading values back
let hasCompletedOnboarding = UserDefaults.standard.bool(forKey: "hasCompletedOnboarding")
let theme = UserDefaults.standard.string(forKey: "preferredTheme") ?? "system"
7¿Qué es ARC (Automatic Reference Counting) en Swift y cómo gestiona el ciclo de vida de las instancias de clases en una aplicación iOS?
ARC (Automatic Reference Counting) es el mecanismo de gestión de memoria en tiempo de compilación de Swift para rastrear y administrar el ciclo de vida de las instancias de clases (tipos por referencia). Cada vez que se crea una nueva instancia de una clase y se asigna a una referencia fuerte (strong reference), ARC lleva un recuento interno de referencias para esa instancia. Mientras exista al menos una referencia fuerte hacia una instancia, ARC la mantiene en memoria. Cuando se eliminan todas las referencias fuertes y el recuento de referencias llega a cero, ARC desasigna inmediatamente la instancia para liberar memoria, invocando automáticamente el método `deinit` de la instancia justo antes de la desasignación. A diferencia de la recolección de basura en tiempo de ejecución de entornos como Java o .NET, ARC no ejecuta ciclos de barrido periódicos; el compilador de Swift inserta las llamadas correspondientes de retain y release directamente en el binario durante el tiempo de compilación.
class User {
let name: String
init(name: String) {
self.name = name
print("\(name) initialized")
}
deinit {
print("\(name) deallocated")
}
}
var ref1: User? = User(name: "Alice") // count = 1
var ref2: User? = ref1 // count = 2
ref1 = nil // count = 1
ref2 = nil // count = 0 -> deinit called
8¿Cómo se realiza una solicitud HTTP (Hypertext Transfer Protocol) GET simple con URLSession en una aplicación para iOS?
Para realizar una solicitud HTTP GET simple en una aplicación para iOS, se crea un objeto URL y se pasa a una instancia de URLSession, como URLSession.shared. La solicitud se puede ejecutar utilizando un manejador de finalización de data task (URLSession.shared.dataTask(with: url) { data, response, error in ... }) o mediante la concurrencia moderna de Swift (let (data, response) = try await URLSession.shared.data(from: url)). Cuando se utiliza el enfoque tradicional de data task, se debe llamar a .resume() en la tarea para iniciarla, gestionar cualquier error de transporte, inspeccionar el código de estado de HTTPURLResponse y asegurarse de que cualquier actualización de la interfaz de usuario se despache en el hilo principal.
guard let url = URL(string: "https://api.example.com/items") else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Network error: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
print("Server error or invalid status code")
return
}
if let data = data {
DispatchQueue.main.async {
// Update UI on main thread
}
}
}
task.resume()
9¿Qué es Instruments en Xcode y cómo se utilizaría para investigar un problema de rendimiento simple en una aplicación para iOS?
Instruments es la herramienta de análisis y creación de perfiles de rendimiento de Apple integrada en Xcode. Permite a los desarrolladores monitorizar el comportamiento en tiempo de ejecución, el uso de CPU, las asignaciones de memoria, las fugas de memoria y el rendimiento del renderizado de la interfaz de usuario. Para investigar un problema de rendimiento simple (como caídas de cuadros en la interfaz o un uso elevado de CPU), se inicia Instruments desde Xcode (a través de Product -> Profile), se elige una plantilla adecuada (como Time Profiler para cuellos de botella de CPU o Allocations/Leaks para problemas de memoria) y se graba una traza mientras se reproduce el flujo de usuario problemático en un dispositivo. Tras la grabación, se analiza el árbol de llamadas y las trazas de pila más pesadas para identificar con precisión los métodos exactos que causan retrasos o un consumo excesivo de recursos, lo que permite medir y localizar el cuello de botella antes de realizar cambios en el código.
1. In Xcode, select Product -> Profile (Builds with Release optimizations).
2. Select the 'Time Profiler' template.
3. Click Record and perform the sluggish action in the app.
4. Stop recording.
5. Inspect the Call Tree tab:
- Enable 'Separate by Thread' and 'Hide System Libraries'.
- Trace the heaviest call stack on the Main Thread to identify the blocking function.
10¿Qué es APNs (Apple Push Notification service) y qué rol desempeña en la entrega de notificaciones push a una aplicación de iOS?
APNs (Apple Push Notification service) es el servicio intermediario en la nube de Apple que enruta de forma segura notificaciones push desde los servidores de backend (servidores de proveedor) hacia los dispositivos Apple. Dado que los dispositivos móviles no pueden mantener conexiones socket abiertas y continuas con el backend de cada aplicación individual sin agotar la batería y consumir un ancho de banda de red excesivo, Apple mantiene una única conexión persistente y de bajo consumo entre cada dispositivo y APNs. Cuando tu aplicación se registra para recibir notificaciones remotas, APNs genera un token de dispositivo opaco y único que identifica esa instalación específica de la aplicación en ese dispositivo en particular. La aplicación envía este token al servidor proveedor de tu backend. Cuando el proveedor desea notificar al usuario, construye una carga útil y la envía junto con el token de dispositivo a APNs a través de HTTP/2. Posteriormente, APNs localiza la conexión activa para ese dispositivo y entrega la carga útil de la notificación push directamente a iOS, que muestra la alerta o activa la aplicación según esté configurado.
11¿Cómo refactorizarías un UIViewController extenso que gestiona actualizaciones de UI (User Interface), validación, peticiones de red y navegación?
La refactorización de un Massive View Controller (MVC, Model-View-Controller) debe realizarse de forma incremental para reducir riesgos, separando las distintas responsabilidades en capas dedicadas:
1. **Extraer red y datos:** Mueve las llamadas a la API, la persistencia de datos y el parseo de JSON fuera del view controller hacia clases de servicio o repositorios dedicados con abstracciones mediante protocolos.
2. **Extraer el estado de presentación y validación:** Introduce un ViewModel (o Presenter). Traslada la validación de entradas, el formateo de cadenas y la gestión del estado de la interfaz de usuario al ViewModel, permitiendo que esta lógica sea testeable mediante pruebas unitarias de forma aislada.
3. **Extraer la navegación:** Adopta el patrón Coordinator (o Router / Flow Controller) para eliminar las llamadas a push/present y la lógica del flujo de navegación del view controller.
4. **Mantener el view controller ligero:** Conserva únicamente el diseño de la UI, la configuración de subvistas, los métodos del ciclo de vida y la vinculación de los controles de la UI con el ViewModel.
5. **Ejecución incremental con pruebas:** Añade pruebas unitarias para los nuevos servicios y ViewModels extraídos durante la refactorización para garantizar que el comportamiento existente no se altere.
// 1. Extracted Navigation
protocol LoginCoordinatorProtocol: AnyObject {
func showHomeScreen()
}
// 2. Extracted Business Logic & State
final class LoginViewModel {
private let authService: AuthServiceProtocol
private weak var coordinator: LoginCoordinatorProtocol?
init(authService: AuthServiceProtocol, coordinator: LoginCoordinatorProtocol) {
self.authService = authService
self.coordinator = coordinator
}
func login(email: String, password: String) {
guard email.contains("@") else { return }
authService.login(email: email, password: password) { [weak self] result in
if case .success = result { self?.coordinator?.showHomeScreen() }
}
}
}
// 3. Lean UIViewController: Only handles UI and bindings
final class LoginViewController: UIViewController {
private let viewModel: LoginViewModel
init(viewModel: LoginViewModel) { self.viewModel = viewModel; super.init(nibName: nil, bundle: nil) }
required init?(coder: NSCoder) { fatalError() }
}
12¿Cómo solucionarías un fallo en la subida de un paquete compilado (archive) de iOS provocado por errores de firma o aprovisionamiento (provisioning)?
La resolución de problemas en la subida de un archive de iOS provocada por errores de firma o aprovisionamiento implica revisar tres áreas principales: certificados de distribución, perfiles de aprovisionamiento y entitlements del target.
1. **Inspeccionar los logs de diagnóstico:** Revisa los logs de distribución de Xcode Organizer, los errores detallados de validación de la subida o los logs de consola de CI (Continuous Integration) (`altool`/`notarytool`) para identificar la causa exacta del rechazo.
2. **Verificación de certificados e identidades:** Verifica que exista un certificado válido de Apple Distribution en el Keychain junto con su clave privada correspondiente, y que no haya caducado, no haya sido revocado ni le falte el certificado intermedio Apple WWDR.
3. **Coincidencia del perfil de aprovisionamiento:** Asegúrate de que el perfil utilizado para exportar sea un perfil de App Store Distribution que coincida exactamente con el Bundle Identifier. Si se usan capacidades como Notificaciones Push o Associated Domains, verifica que esté configurado un App ID explícito en lugar de un comodín (wildcard) incompatible.
4. **Alineación de Entitlements:** Comprueba si hay discrepancias entre el archivo `.entitlements` del target y las capacidades habilitadas para el App ID en el Apple Developer Portal. Si los entitlements locales solicitan permisos no registrados en el perfil del portal, la subida fallará.
5. **Configuración de firma:** Si se utiliza firma automática, verifica el Team seleccionado y las credenciales del Apple ID en los ajustes de Xcode. Si se usa firma manual (o exportación en CI), asegúrate de que `ExportOptions.plist` asocie cada bundle ID con el certificado de distribución y el perfil correctos.
# Decode the provisioning profile embedded in the archive
security cms -D -i MyApp.xcarchive/Products/Applications/MyApp.app/embedded.mobileprovision > profile.plist
# Read entitlements embedded in the profile
/usr/libexec/PlistBuddy -c "Print :Entitlements" profile.plist
# Inspect signature and entitlements on the built binary
codesign -d --entitlements :- MyApp.xcarchive/Products/Applications/MyApp.app
13¿Cómo elegirías entre BGAppRefreshTask, BGProcessingTask y URLSession en segundo plano para una funcionalidad de sincronización en segundo plano en producción?
Elegir la API de segundo plano adecuada depende de la duración de la tarea, de si se requieren condiciones específicas de energía o red y de la naturaleza de la carga transferida:
1. **`BGAppRefreshTask`**: Ideal para actualizaciones de contenido breves y ligeras (como refrescar feeds de noticias, líneas de tiempo en redes sociales o paneles de usuario) que tardan aproximadamente 15–30 segundos. El sistema las programa según los patrones de uso del usuario para que el contenido actualizado esté listo justo antes de que este abra habitualmente la aplicación.
2. **`BGProcessingTask`**: Diseñada para operaciones pesadas, no urgentes y de larga duración, tales como indexación de datos, limpieza/migración de bases de datos, entrenamiento de modelos de ML o sincronizaciones masivas de datos. Puede ejecutarse durante minutos y requerir que el dispositivo esté cargando (`requiresExternalPower = true`) y conectado a Wi-Fi/red (`requiresNetworkConnectivity = true`), ejecutándose habitualmente durante la noche.
3. **`URLSession` en segundo plano (`URLSessionConfiguration.background`)**: La opción correcta cuando se transfieren archivos grandes (subida de fotos/vídeos o descarga de paquetes de recursos grandes) que deben continuar fuera de proceso incluso si el sistema operativo suspende o termina la aplicación. Delega la transferencia de red al demonio `nsurlsessiond` en lugar de mantener activo el código de la aplicación en memoria.
14¿Cómo construirías un pipeline de Combine para un campo de texto de búsqueda que aplique debounce a la entrada, ignore duplicados, realice una solicitud de red y actualice la interfaz de usuario de forma segura?
Para construir un pipeline de búsqueda seguro en Combine:
1. **Debounce y deduplicación:** Toma el publisher de la consulta de búsqueda (por ejemplo, `$queryText`), aplica `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` para esperar las pausas al escribir, y `.removeDuplicates()` para ignorar cadenas de búsqueda que no hayan cambiado.
2. **Solicitud de red y cancelación:** Mapea cada consulta a un publisher de solicitud de red y aplánalo mediante `.switchToLatest()`. Esto cancela automáticamente las solicitudes en curso cuando llega una nueva consulta de búsqueda, evitando condiciones de carrera y resultados obsoletos.
3. **Aislar errores:** Captura los errores de red dentro del publisher interno (por ejemplo, `.catch { _ in Just([]) }`) para que los fallos de red no terminen el stream principal de texto de búsqueda.
4. **Entrega en el hilo principal:** Aplica `.receive(on: DispatchQueue.main)` antes de actualizar el estado o asignarlo a las propiedades de la interfaz de usuario.
15¿Cómo utilizarías async/await y la cancelación de `Task` en Swift para implementar una pantalla que cargue datos de la red y evite actualizar la UI (User Interface) si el usuario navega a otra pantalla?
Implementaría la carga de datos de red como una función `async` en un servicio o view model y la ejecutaría desde una `Task` cuyo ciclo de vida esté ligado al de la pantalla. En UIKit, esto suele implicar almacenar una referencia como `var loadTask: Task<Void, Never>?` en el view controller o view model, iniciarla cuando la pantalla deba cargar los datos y cancelarla en `viewWillDisappear`, `deinit` o antes de lanzar una nueva solicitud, según el ciclo de vida deseado. En SwiftUI, optaría por `.task` o `.task(id:)` siempre que sea posible, ya que SwiftUI cancela automáticamente la tarea cuando la vista desaparece o cuando el identificador cambia. Dentro de la tarea, llamaría a la API asíncrona de red mediante `try await`, gestionaría la cancelación de forma independiente a los errores reales y verificaría si la tarea fue cancelada antes de aplicar los resultados si existen múltiples `await` o pasos de procesamiento intermedios. Las mutaciones del estado de la UI deben ejecutarse obligatoriamente en el actor principal, ya sea marcando el view model con `@MainActor` o utilizando `await MainActor.run`. Para evitar actualizaciones obsoletas en la interfaz, cancelaría las tareas previas, evitaría el uso de tareas desconectadas o globales (`Task.detached`) para operaciones vinculadas a una pantalla y, opcionalmente, compararía un identificador de solicitud o el identificador del elemento actual antes de aplicar el resultado.
@MainActor
final class UsersViewController: UIViewController {
private var loadTask: Task<Void, Never>?
private let service: UserService
override func viewDidLoad() {
super.viewDidLoad()
loadUsers()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
loadTask?.cancel()
}
private func loadUsers() {
loadTask?.cancel()
loadingView.isHidden = false
loadTask = Task { [weak self] in
do {
let users = try await self?.service.fetchUsers() ?? []
try Task.checkCancellation()
guard let self else { return }
self.loadingView.isHidden = true
self.tableModel = users
self.tableView.reloadData()
} catch is CancellationError {
// User navigated away or a new load replaced this one; do not update UI.
} catch {
guard let self else { return }
self.loadingView.isHidden = true
self.showError(error)
}
}
}
}
16¿Cómo elegiría entre UserDefaults, Keychain, archivos, SQLite y Core Data para los diferentes tipos de datos de una aplicación?
La elección del mecanismo de almacenamiento adecuado en iOS depende de la confidencialidad de los datos, el tamaño, la complejidad relacional, los requisitos de consulta y el ciclo de vida de las copias de seguridad: 1. **Keychain**: Para datos confidenciales (tokens de autenticación, contraseñas, claves de cifrado, secretos biométricos). Proporciona cifrado respaldado por hardware, persiste tras reinstalar la app y permanece seguro incluso en entornos sandbox comprometidos. 2. **UserDefaults**: Para preferencias y banderas ligeras y no confidenciales (por ejemplo, `hasCompletedOnboarding`, preferencia de tema). Dado que se carga por completo en memoria como un property list, no debe utilizarse para conjuntos de datos grandes, contenido multimedia o tokens sensibles. 3. **Sistema de archivos (Documents / Caches / Application Support)**: Para archivos independientes y objetos binarios grandes (imágenes descargadas, audio, PDFs). `Documents` es accesible para el usuario y se respalda en iCloud; `Caches` es para contenido descartable que puede volver a descargarse; `Application Support` es para archivos persistentes que no están expuestos directamente al usuario. 4. **SQLite**: Para datos estructurados y tabulares que requieren consultas indexadas rápidas, operaciones masivas o uso compartido de la base de datos entre plataformas sin la sobrecarga de un grafo de objetos. 5. **Core Data / SwiftData**: Para grafos de objetos complejos con relaciones, seguimiento de cambios, carga diferida (faulting), gestión de deshacer e integración directa con UIKit (`NSFetchedResultsController`) o SwiftUI (`@Query`).
17En una pantalla de producción en UIKit con callbacks desde un view model, ¿cómo decidirías si capturar self como weak, unowned o strong en los manejadores de clausuras (closures)?
En la arquitectura UIKit, donde un view controller interactúa con un view model mediante callbacks, la estrategia de captura se determina en función de la propiedad (ownership) y el ciclo de vida:
1. `[weak self]`: El enfoque estándar y más seguro para clausuras almacenadas o que escapan de su ámbito de llamada (escaping closures), tales como callbacks de eventos del view model o manejadores asíncronos de finalización de red/datos. Dado que el view controller es propietario del view model, capturar `self` de forma fuerte (strong) en un callback almacenado por el view model crea un ciclo de referencias fuertes (`VC -> VM -> closure -> VC`). `[weak self]` convierte `self` en un opcional (`UIViewController?`), permitiendo que `self` se desasigne limpiamente y evitando fugas de memoria.
2. `[unowned self]`: Asume que `self` nunca será nil cuando la clausura se ejecute. Debe utilizarse con extrema precaución en pantallas de UI. Si una tarea asíncrona o callback se completa después de que el view controller haya sido descartado y desasignado, acceder a `unowned self` provocará un fallo fatal en tiempo de ejecución (crash). Por este motivo, generalmente se prefiere `[weak self]` frente a `unowned` en callbacks de UI en UIKit para producción.
3. **Captura fuerte (por defecto, sin lista de captura):** Apropiada cuando la clausura es no escapatoria (non-escaping, como operaciones estándar de colecciones como `map`/`filter`) o cuando la clausura tiene una vida corta y no crea un ciclo de propiedad de vuelta hacia `self`.
final class ProfileViewController: UIViewController {
private let viewModel: ProfileViewModel
init(viewModel: ProfileViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
// VM stores the callback -> capture weak self to break retain cycle
viewModel.onDataUpdated = { [weak self] state in
guard let self else { return }
self.updateUI(with: state)
}
}
private func updateUI(with state: ProfileState) { /* Update views */ }
}
18Te incorporas a una aplicación iOS madura con cientos de pantallas, múltiples massive view controllers y ciclos de lanzamiento lentos. ¿Cómo planificarías una estrategia de mejora arquitectónica incremental sin detener la entrega de nuevas funcionalidades?
Una estrategia eficaz de migración incremental evita las reescrituras completas y combina la refactorización directamente con la entrega continua de funcionalidades (siguiendo el patrón Strangler Fig). Se comienza definiendo un diseño arquitectónico objetivo acordado (como MVVM o VIPER con inyección de dependencias y coordinadores modulares) y estableciendo líneas base de pruebas (pruebas de caracterización, de snapshot y unitarias) alrededor del código legado antes de modificarlo.
La priorización debe guiarse por la frecuencia de cambios en las funcionalidades y el riesgo: se deben refactorizar las pantallas que los equipos modifican activamente o las áreas con altas tasas de errores, en lugar de código legado estable. Al refactorizar Massive View Controllers, desacopla la lógica de negocio y de presentación en ViewModels/Presenters dedicados e introduce adaptadores o coordinadores basados en protocolos para aislar la interfaz de usuario legada del nuevo código.
Por último, mantén la velocidad y la calidad mediante la gobernanza arquitectónica: proporciona implementaciones de referencia (golden samples), haz cumplir los límites arquitectónicos mediante linters en CI o directrices en los pull requests (PR), y asigna capacidad continua a la deuda técnica (por ejemplo, un 15–20% de capacidad o emparejando refactorizaciones con tareas relacionadas de nuevas funcionalidades) mientras monitoreas métricas como tiempos de compilación, tasas de fallos y cobertura de pruebas.
19Estás lanzando una aplicación iOS de alto tráfico con una migración de base de datos y un cambio en la API (Application Programming Interface) del backend; ¿cómo diseñarías el plan de despliegue para minimizar el impacto en los usuarios?
Diseñar un plan de despliegue para una aplicación iOS de alto tráfico que involucra migraciones de base de datos y cambios en la API del backend requiere desacoplar el despliegue de la activación de funcionalidades, dadas las restricciones del cliente iOS (actualizaciones asíncronas por parte de los usuarios e imposibilidad de forzar rollbacks del binario en el cliente).
1. Compatibilidad en el backend: Implementar una estrategia de «expandir y contraer» (expand-and-contract). Desplegar la API v2 junto con la API v1 para que tanto las versiones heredadas como las actualizadas del cliente funcionen en paralelo sin introducir cambios disruptivos.
2. Migración resiliente de la base de datos: Asegurar que las migraciones de esquemas locales (por ejemplo, SQLite, Core Data o SwiftData) sean idempotentes, no destructivas y gestionen con seguridad saltos entre múltiples versiones (por ejemplo, actualizar de N-3 a N) sin bloquear el hilo principal ni provocar bucles de caídas al iniciar la app.
3. Activación dinámica controlada (dynamic gating): Publicar las nuevas funciones del cliente ocultas tras feature flags o configuración remota gestionada desde el servidor. Mantener el feature flag desactivado durante la distribución inicial.
4. Despliegue gradual y monitorización: Distribuir la aplicación mediante App Store Phased Release (un despliegue escalonado en 7 días). Monitorizar continuamente la telemetría, las tasas de fallos (crashes), las métricas de éxito en las migraciones de base de datos y las tasas de error de la API. Si se detectan anomalías, pausar el despliegue gradual en App Store Connect y desactivar los feature flags sin requerir un rollback binario de emergencia.
Phase 1 (Backend Expand): Deploy API v2 supporting both new and legacy payloads.
Phase 2 (Client Distribution & Migration): Release client v2.0 via App Store Phased Release (1% -> 100%). DB migrates safely on first launch. Feature flag remains OFF.
Phase 3 (Validation & Feature Enablement): Monitor crash rates & migration success. Incrementally ramp Remote Config flag (10% -> 50% -> 100%).
Phase 4 (Backend Contract): Once legacy app adoption drops below deprecation threshold, sunset API v1.
20Estás diseñando la sincronización offline-first para una aplicación de notas donde las ediciones deben sincronizarse eventualmente entre dispositivos, pero iOS puede posponer o cancelar el trabajo en segundo plano. ¿Qué arquitectura de ejecución en segundo plano elegirías?
Una arquitectura de sincronización offline-first considera la base de datos local (como SQLite, Core Data o SwiftData) como la única fuente de verdad inmediata para el estado de la UI, mientras que las mutaciones se agregan a una cola persistente de salida (outbox queue) en disco para que las ediciones pendientes sobrevivan a la finalización del proceso. Para gestionar la ejecución en segundo plano oportunista y no determinista de iOS, el trabajo en segundo plano se estructura en varios niveles:
1. Vaciado de la cola de salida al pasar a segundo plano: Se inicia mediante `UIApplication.shared.beginBackgroundTask(expirationHandler:)` para completar mutaciones en tránsito o pendientes de ejecución rápida.
2. Sincronización periódica programada: Registrada con `BGTaskScheduler` utilizando `BGAppRefreshTask` para sincronizaciones ligeras de metadatos/deltas y `BGProcessingTask` para operaciones de sincronización más pesadas.
3. Sincronización de recursos pesados: Gestionada fuera del proceso principal mediante una `URLSessionConfiguration` en segundo plano utilizando subidas y descargas de archivos.
4. Sincronización activada por el servidor: Despertares oportunistas mediante notificaciones push silenciosas (`content-available: 1`).
Dado que las tareas en segundo plano pueden interrumpirse o reprogramarse en cualquier momento, las operaciones deben utilizar claves de idempotencia generadas en el cliente (por ejemplo, UUIDs) para permitir reintentos seguros sin duplicar datos. Si se invoca el `expirationHandler` de una tarea, el trabajo en curso debe cancelarse de inmediato, las operaciones no confirmadas deben revertirse al estado pendiente y debe llamarse a `setTaskCompleted(success: false)`.
La consistencia eventual se mantiene mediante mecanismos de resolución de conflictos como CRDTs, vectores de versiones o la regla de «la última escritura gana» (last-write-wins) con registros de eliminación (tombstones), asegurando que las deltas remotas no sobrescriban mutaciones locales no confirmadas en la cola de salida.
import BackgroundTasks
import Foundation
final class NoteSyncCoordinator {
static let shared = NoteSyncCoordinator()
private let syncTaskID = "com.notesapp.sync.refresh"
func registerSyncTask() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: syncTaskID, using: nil) { task in
guard let refreshTask = task as? BGAppRefreshTask else { return }
self.handleAppRefresh(task: refreshTask)
}
}
func scheduleNextSync() {
let request = BGAppRefreshTaskRequest(identifier: syncTaskID)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
private func handleAppRefresh(task: BGAppRefreshTask) {
scheduleNextSync()
let syncTask = Task {
do {
try await SyncEngine.shared.flushPersistentOutboxAndFetchDeltas()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
syncTask.cancel()
}
}
}