Fundamentos
Variables y Constantes
Use let por defecto para valores que no cambian; cambie a var solo cuando se necesite mutación. Swift infiere tipos en tiempo de compilación, pero las anotaciones explícitas mejoran la legibilidad para tipos complejos o ambiguos.
var name = "Alice" // mutable
let age = 30 // immutable constant
let pi: Double = 3.14159 // explicit type
var count: Int = 0
count = 10
print(type(of: age)) // IntOptionals
Los optionals representan la ausencia de un valor. Use if-let para desenvolver de forma segura, ?? para valores predeterminados, y ! solo cuando esté seguro de que el valor existe (arriesgado—puede causar crash).
var nickname: String? = nil
nickname = "Al"
if let n = nickname {
print("Hi, \(n)")
}
print(nickname ?? "Anonymous") // nil-coalescing
let force: String = nickname! // force unwrap (crashes if nil)Tuplas
Las tuplas agrupan múltiples valores en un solo valor compuesto. Útiles para devolver múltiples valores de funciones. Los elementos nombrados mejoran la legibilidad.
let person = (name: "Alice", age: 30)
print(person.name) // Alice
print(person.0) // Alice
let (n, a) = person
print("\(n), \(a)") // Alice, 30Conversión de Tipos
Swift no convierte implícitamente entre tipos. Siempre use conversión explícita (por ejemplo, Double(intVal)) para evitar ambigüedad y prevenir bugs sutiles.
let intVal = 42
let doubleVal = Double(intVal)
let strVal = String(intVal)
let fromStr = Int("100")! // 100
let invalid = Int("abc") // nil
print(doubleVal, strVal)Aserciones y Precondiciones
Use assert para verificaciones de depuración (eliminadas en builds de release) y precondition para invariantes críticas verificadas en todos los builds. Ambos ayudan a detectar errores de lógica temprano.
let age = -5
assert(age >= 0, "Age cannot be negative")
precondition(age >= 0, "Age must be non-negative")
// In debug builds, assert crashes if false
// precondition checks in release builds tooCadenas y Caracteres
Interpolación de Cadenas
La interpolación de cadenas con \(expr) incrusta cualquier expresión en una cadena. Es de tipo seguro y evaluada en tiempo de compilación, haciéndola más segura que las cadenas de formato.
let name = "Alice"
let age = 30
let msg = "Name: \(name), Age: \(age)"
let calc = "5 + 3 = \(5 + 3)"
print(msg) // Name: Alice, Age: 30
print(calc) // 5 + 3 = 8Métodos Comunes de Cadena
Las cadenas de Swift son correctas en Unicode, lo que significa que count refleja los caracteres reales (clústeres de grafemas), no bytes. Use estos métodos en lugar de indexación manual para mayor seguridad.
let s = "Hello, World"
print(s.count) // 12
print(s.uppercased()) // HELLO, WORLD
print(s.lowercased()) // hello, world
print(s.hasPrefix("Hello")) // true
print(s.contains("World")) // true
print(s.reversed()) // "dlroW ,olleH"Substring e Indexación
Los índices de cadena no son enteros porque los caracteres pueden tener diferentes tamaños de bytes. Use index(_:offsetBy:) para navegación. Los substrings comparten memoria con el original—convierta a String para almacenamiento a largo plazo.
let s = "Hello, World"
let idx = s.index(s.startIndex, offsetBy: 7)
print(s[idx]) // W
let sub = s[idx...] // "World"
let newStr = String(sub) // convert to String
print(s.prefix(5)) // "Hello"Cadenas Multilínea
Las cadenas con triples comillas preservan saltos de línea e indentación. Las comillas de cierre """ determinan la indentación base. Ideales para HTML, JSON o bloques de texto largo.
let poem = """
Roses are red,
Violets are blue,
Swift is great,
And so are you.
"""
print(poem)
// Use \\(expr) for interpolation in multilineConstrucción y División de Cadenas
Use joined(separator:) para concatenar con un delimitador y split(separator:) para tokenizar. Estos son más eficientes que los bucles manuales con concatenación +.
let parts = ["apple", "banana", "cherry"]
let joined = parts.joined(separator: ", ")
print(joined) // apple, banana, cherry
let csv = "a,b,c"
let fields = csv.split(separator: ",")
print(fields) // ["a", "b", "c"]
let reversed = String(s.reversed())Estructuras de Datos
Array
Los arrays son colecciones ordenadas con indexación base cero. Use append/insert para adiciones, filter/map/reduce para transformaciones. Prefiera tipos de valor (Array es un struct) para seguridad de subprocesos.
var nums = [1, 2, 3]
nums.append(4)
nums.insert(0, at: 0)
nums[1] = 20
print(nums.count) // 5
print(nums.contains(20)) // true
let evens = nums.filter { $0 % 2 == 0 }
print(nums.first, nums.last)Dictionary
Los diccionarios almacenan pares clave-valor con búsqueda O(1) promedio. Use default: para evitar nil al acceder a claves faltantes. Las claves deben ser Hashable.
var ages: [String: Int] = ["Alice": 30]
ages["Bob"] = 25
ages["Alice"] = 31
print(ages["Alice", default: 0]) // 31
for (name, age) in ages {
print("\(name): \(age)")
}
print(ages.keys.sorted())Set
Los sets almacenan valores únicos con prueba de pertenencia O(1). Ideales para desduplicación y operaciones de conjuntos (unión, intersección, diferencia). Los elementos deben ser Hashable.
var a: Set<Int> = [1, 2, 3]
var b: Set<Int> = [3, 4, 5]
print(a.union(b)) // {1,2,3,4,5}
print(a.intersection(b)) // {3}
print(a.subtracting(b)) // {1,2}
print(a.isSubset(of: b)) // false
a.insert(6)Ranges
..< es un rango semi-abierto (excluye el límite superior), ... es un rango cerrado (incluye ambos). Los rangos son útiles en bucles, slicing y coincidencia de patrones con el operador ~=.
for i in 0..<5 { print(i) } // 0,1,2,3,4
for i in 0...5 { print(i) } // 0,1,2,3,4,5
let nums = Array(1...5) // [1,2,3,4,5]
let prefix = nums.prefix(3) // [1,2,3]
if 5...10 ~= 7 { print("in range") }Funciones de Orden Superior
map transforma cada elemento, filter selecciona elementos coincidentes, reduce combina todos en uno. Estos son la base de la programación funcional en Swift y permiten pipelines de datos concisos y legibles.
let nums = [1, 2, 3, 4, 5]
let doubled = nums.map { $0 * 2 } // [2,4,6,8,10]
let evens = nums.filter { $0 % 2 == 0 } // [2,4]
let sum = nums.reduce(0, +) // 15
let strs = nums.map { String($0) } // ["1","2",...]
print(doubled, evens, sum)Flujo de Control
If / Else If / Else
Ramificación condicional estándar. Las condiciones deben ser Booleanas (tipo Bool). Swift requiere llaves incluso para instrucciones únicas para prevenir bugs como la vulnerabilidad goto fail de Apple.
let score = 85
if score >= 90 {
print("A")
} else if score >= 80 {
print("B")
} else if score >= 70 {
print("C")
} else {
print("F")
}Switch (Coincidencia de Patrones)
El switch de Swift es potente: admite vinculación de valores, tuplas, guards where y rangos. Debe ser exhaustivo (default cubre los casos restantes) y no cae en cascada por defecto.
let point = (2, 0)
switch point {
case (0, 0):
print("origin")
case (_, 0):
print("on x-axis")
case (0, _):
print("on y-axis")
case let (x, y) where x == y:
print("on diagonal")
default:
print("elsewhere: \(point.0),\(point.1)")
}Bucles For-In
For-in itera sobre rangos, arrays, diccionarios y cualquier Sequence. Use enumerated() cuando necesite el índice. Use _ para ignorar variables de bucle que no use.
for i in 0..<5 { print(i) }
let fruits = ["apple", "banana"]
for (i, fruit) in fruits.enumerated() {
print("\(i): \(fruit)")
}
let dict = ["a": 1, "b": 2]
for (k, v) in dict { print("\(k)=\(v)") }
for _ in 0..<3 { print("tick") }While y Repeat-While
while verifica la condición antes de cada iteración; repeat-while verifica después (como do-while en C). Use repeat-while cuando el cuerpo deba ejecutarse al menos una vez.
var n = 5
while n > 0 {
print(n)
n -= 1
}
var x = 0
repeat {
x += 1
} while x < 3
print(x) // 3Guard (Salida Temprana)
guard proporciona salida temprana cuando no se cumplen los prerrequisitos. Los optionals desenvueltos permanecen disponibles en el resto del ámbito, reduciendo el anidamiento. Prefiera guard sobre anidamiento profundo if-let.
func greet(_ name: String?) {
guard let n = name, !n.isEmpty else {
print("No name provided")
return
}
print("Hello, \(n)")
// n is unwrapped and available here
}
greet("Alice")
greet(nil)Funciones y Closures
Definición de Funciones
Use _ para omitir etiquetas de argumento para legibilidad. Los valores de parámetros predeterminados hacen que los parámetros sean opcionales. Las funciones con una sola expresión tienen retorno implícito (Swift 5.9+).
func greet(_ name: String, greeting: String = "Hello") -> String {
return "\(greeting), \(name)!"
}
print(greet("Alice")) // Hello, Alice!
print(greet("Bob", greeting: "Hi")) // Hi, Bob!
func add(_ a: Int, _ b: Int) -> Int { a + b }
print(add(3, 4)) // 7Múltiples Valores de Retorno
Las tuplas permiten devolver múltiples valores nombrados. Devolver una tupla optional señala posible fallo. Acceda vía r.min o r.0—los elementos nombrados son más claros.
func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
guard let first = nums.first else { return nil }
var mn = first, mx = first
for n in nums { mn = min(mn, n); mx = max(mx, n) }
return (mn, mx)
}
if let r = minMax([3, 1, 4, 1, 5]) {
print("min=\(r.min), max=\(r.max)")
}Closures
Los closures son bloques autónomos de funcionalidad. Use $0, $1 para nombres de argumento abreviados. La sintaxis de trailing closure (omitiendo la etiqueta del último argumento) es idiomática para map/filter/reduce.
let square: (Int) -> Int = { x in x * x }
print(square(5)) // 25
let add: (Int, Int) -> Int = { $0 + $1 }
print(add(3, 4)) // 7
let nums = [1, 2, 3]
let doubled = nums.map { $0 * 2 }
print(doubled) // [2, 4, 6]Escaping y Auto-Closures
@escaping marca closures almacenados o llamados después de que la función retorna (necesario para callbacks asíncronos). @autoclosure envuelve una expresión en un closure, retrasando su evaluación (usado en assert).
var handlers: [() -> Void] = []
func saveHandler(_ fn: @escaping () -> Void) {
handlers.append(fn)
}
saveHandler { print("done") }
handlers.first?()
// @autoclosure delays evaluation
func logIfTrue(_ cond: @autoclosure () -> Bool) {
if cond() { print("true") }
}
logIfTrue(2 > 1)Parámetros Inout
Los parámetros inout permiten a las funciones modificar la variable del llamador (paso por referencia). Úselos con moderación—prefiera devolver nuevos valores para mayor claridad. El prefijo & marca el sitio de mutación.
func swap(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 10, y = 20
swap(&x, &y)
print(x, y) // 20 10Clases y Structs
Struct (Tipo de Valor)
Los structs son tipos de valor—copiados en la asignación. Úselos para contenedores de datos simples. Las propiedades calculadas (distance) se calculan al acceder. Los métodos mutating modifican self.
struct Point {
var x: Double
var y: Double
var distance: Double {
(x * x + y * y).squareRoot()
}
mutating func moveBy(dx: Double, dy: Double) {
x += dx; y += dy
}
}
var p = Point(x: 3, y: 4)
print(p.distance) // 5.0Class (Tipo de Referencia)
Las clases son tipos de referencia—compartidas vía referencias, admiten herencia y deinit. Use cuando necesite identidad, estado mutable compartido o interoperabilidad con Objective-C. De lo contrario prefiera structs.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
deinit { print("\(name) deallocated") }
}
let p = Person(name: "Alice", age: 30)
print(p.name) // AliceHerencia y Override
Use override para redefinir un método de superclase. Swift usa despacho dinámico para que se llame la versión de subclase. Marque métodos como final para prevenir más sobrescritura por rendimiento.
class Animal {
func speak() { print("...") }
}
class Dog: Animal {
override func speak() { print("Woof!") }
}
class Cat: Animal {
override func speak() { print("Meow!") }
}
let pets: [Animal] = [Dog(), Cat()]
for pet in pets { pet.speak() }Propiedades (Calculadas y Lazy)
Las propiedades calculadas tienen bloques get/set; set usa newValue por defecto. lazy difiere la inicialización hasta el primer acceso—útil para propiedades costosas o raramente necesarias. Debe ser var.
class Circle {
var radius: Double
var area: Double {
get { .pi * radius * radius }
set { radius = (newValue / .pi).squareRoot() }
}
lazy var heavy = loadExpensiveData()
init(radius: Double) { self.radius = radius }
}
func loadExpensiveData() -> String { "data" }Observadores de Propiedades
willSet/didSet observan cambios de propiedad. Use didSet para validar o reaccionar a cambios (por ejemplo, limitar valores, disparar actualizaciones de UI). Los observadores no se disparan durante init.
class Counter {
var count: Int = 0 {
willSet { print("about to set \(newValue)") }
didSet {
print("was \(oldValue), now \(count)")
if count > 10 { count = 10 }
}
}
}
let c = Counter()
c.count = 5
c.count = 20 // clamped to 10Protocolos y Extensiones
Definición de Protocolo
Los protocolos definen un plano de métodos y propiedades. Los tipos conforman implementándolos. Use para abstracción, polimorfismo y desacoplamiento—similar a interfaces en Java/C#.
protocol Greetable {
var name: String { get }
func greet() -> String
}
struct User: Greetable {
let name: String
func greet() -> String { "Hi, \(name)" }
}
let u = User(name: "Alice")
print(u.greet()) // Hi, AliceExtensiones de Protocolo (Impl por Defecto)
Las extensiones de protocolo proporcionan implementaciones por defecto. Esto habilita modelado retroactivo y reutilización de código sin una clase base. Una característica potente para compartir comportamiento entre tipos no relacionados.
protocol Describable {
func describe() -> String
}
extension Describable {
func describe() -> String { "A \(type(of: self))" }
}
struct Box: Describable { }
let b = Box()
print(b.describe()) // A BoxExtensiones
Las extensiones añaden funcionalidad a tipos existentes (incluso los que no posee, como Int). Use para organizar código, añadir propiedades calculadas o conformar a protocolos. No pueden añadir propiedades almacenadas.
extension Int {
var squared: Int { self * self }
func times(_ block: () -> Void) {
for _ in 0..<self { block() }
}
}
print(5.squared) // 25
3.times { print("hi") } // prints hi 3 timesGenéricos
Los genéricos escriben código flexible y reutilizable que funciona con cualquier tipo preservando la seguridad de tipos. Use <T> para parámetros de tipo. Las restricciones (where T: Equatable) limitan los tipos permitidos.
func stackOf<T>(_ items: T...) -> [T] {
var arr: [T] = []
for item in items { arr.append(item) }
return arr
}
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ e: Element) { items.append(e) }
}
let s = stackOf(1, 2, 3) // [Int]Protocolo con Tipo Asociado
Los tipos asociados permiten a los protocolos usar tipos marcador de posición, como genéricos para protocolos. El tipo conforme especifica el tipo real. Use typealias para hacerlo explícito, o deje que Swift lo infiera.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int
private var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}Manejo de Errores
Definir y Lanzar Errores
Los errores conforman al protocolo Error (usualmente enums). throw señala un error. Los valores asociados (coinsNeeded) llevan contexto. Use throws para marcar funciones que pueden fallar.
enum VendingError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
func vend(item: String, coins: Int) throws -> String {
guard item == "Candy" else { throw VendingError.invalidSelection }
guard coins >= 2 else { throw VendingError.insufficientFunds(coinsNeeded: 2) }
return "Dispensing \(item)"
}Do-Catch y Try
do-catch maneja errores lanzados. try marca llamadas que lanzan. Haga coincidencia de patrones en casos específicos para manejo dirigido. Un catch-all maneja errores inesperados. Los errores se propagan hacia arriba en la pila de llamadas.
do {
let result = try vend(item: "Candy", coins: 1)
print(result)
} catch VendingError.insufficientFunds(let needed) {
print("Need \(needed) more coins")
} catch VendingError.outOfStock {
print("Sold out")
} catch {
print("Other error: \(error)")
}Try? / Try!
try? convierte errores a nil (devuelve Optional). try! asume éxito y causa crash en error—use solo cuando el fallo es verdaderamente imposible. Prefiera try? con vinculación optional para degradación elegante.
let result1 = try? vend(item: "Candy", coins: 1)
print(result1) // nil (error converted to optional)
let result2 = try! vend(item: "Candy", coins: 5)
print(result2) // Dispensing Candy (crashes if error)
if let r = try? vend(item: "Candy", coins: 5) {
print(r)
}Tipo Result
Result codifica éxito o fallo como un valor, habilitando manejo de errores asíncrono sin throw. Use para callbacks, APIs asíncronos, o cuando quiera almacenar/encadenar resultados. .get() lanza en fallo.
enum FetchError: Error { case network, parse }
func fetch(_ url: String) -> Result<String, FetchError> {
url.isEmpty ? .failure(.network) : .success("data from \(url)")
}
switch fetch("https://api.com") {
case .success(let data): print(data)
case .failure(let err): print("Error: \(err)")
}
let value = try? fetch("").get()Defer (Limpieza)
defer programa código de limpieza para ejecutarse cuando el ámbito sale, sin importar cómo (retorno normal, throw, error). Use para liberación de recursos (archivos, locks). Múltiples defers se ejecutan en orden LIFO.
func processFile(_ path: String) throws {
let handle = openFile(path)
defer { closeFile(handle) }
// ... work that might throw ...
let data = try read(handle)
// closeFile always runs, even if throw
print("processed \(data)")
}E/S de Archivos y Fecha/Hora
Leer y Escribir Archivos
Foundation proporciona APIs simples de archivos. write(toFile:atomically:) escribe de forma segura (archivo temporal + renombrado). Use String(contentsOfFile:) para texto. Para archivos grandes, use FileHandle para streaming.
import Foundation
let path = "test.txt"
let content = "Hello, File I/O!"
try content.write(toFile: path, atomically: true,
encoding: .utf8)
let read = try String(contentsOfFile: path, encoding: .utf8)
print(read) // Hello, File I/O!
let lines = read.split(separator: "\n")
print(lines.count)URL y FileManager
FileManager maneja operaciones de sistema de archivos (crear, eliminar, verificar existencia). Use URLs (no rutas) para APIs modernas. .documentDirectory es el almacenamiento persistente de la app en iOS/macOS.
import Foundation
let fm = FileManager.default
let url = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("data.json")
try "data".write(to: url, atomically: true, encoding: .utf8)
let exists = fm.fileExists(atPath: url.path)
print("Exists: \(exists)")
try fm.removeItem(at: url) // deleteDate y DateFormatter
Date representa un punto en el tiempo (UTC internamente). DateFormatter convierte entre Date y String—siempre establezca locale a en_US_POSIX para análisis de formato fijo para evitar bugs de configuración regional.
import Foundation
let now = Date()
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
let str = fmt.string(from: now)
print(str) // e.g. 2024-01-15 14:30:00
let parsed = fmt.date(from: "2024-01-01 00:00:00")
print(parsed ?? "invalid")Codificación/Decodificación JSON
Codable automatiza la serialización JSON. JSONEncoder/JSONDecoder manejan la conversión. Conforme sus tipos a Codable—el compilador sintetiza la lógica. Use CodingKeys para personalizar nombres de claves.
struct User: Codable {
let name: String
let age: Int
}
let user = User(name: "Alice", age: 30)
let json = try JSONEncoder().encode(user)
let str = String(data: json, encoding: .utf8)!
print(str) // {"name":"Alice","age":30}
let decoded = try JSONDecoder().decode(User.self, from: json)
print(decoded.name) // AliceCálculos de Fecha
Calendar maneja aritmética de fechas respetando zonas horarias y DST. Use dateComponents para extraer campos o calcular diferencias. Nunca use segundos crudos para matemáticas de fecha—use APIs de Calendar.
import Foundation
let cal = Calendar.current
let now = Date()
let tomorrow = cal.date(byAdding: .day, value: 1, to: now)!
let components = cal.dateComponents([.year, .month, .day], from: now)
print(components.year!, components.month!)
let diff = cal.dateComponents([.hour], from: now, to: tomorrow)
print(diff.hour!) // 24Concurrencia y Async
Async / Await
async/await (Swift 5.5+) hace que el código asíncrono se lea como código síncrono. Task crea un nuevo contexto asíncrono. async let ejecuta concurrentemente y espera todo. Elimina el callback hell—prefiéralo sobre completion handlers.
func fetchUser(_ id: Int) async -> String {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "User \(id)"
}
Task {
let user = await fetchUser(42)
print(user) // User 42
async let u1 = fetchUser(1)
async let u2 = fetchUser(2)
let users = await [u1, u2] // concurrent
print(users)
}Task y Cancelación
Task representa una unidad de trabajo asíncrono. Cancelación cooperativa vía Task.isCancelled—las tareas largas deberían verificar periódicamente. task.value espera el resultado. La cancelación es cooperativa, no forzada.
let task = Task {
for i in 1...100 {
if Task.isCancelled { return }
try? await Task.sleep(nanoseconds: 100_000_000)
print(i)
}
}
Task {
try? await Task.sleep(nanoseconds: 500_000_000)
task.cancel() // request cancellation
}
await task.value // wait for completionActors (Seguridad de Subprocesos)
Los Actors (Swift 5.5+) protegen estado mutable de data races serializando el acceso. Todo el acceso va a través de await. Use en lugar de locks/queues para estado compartido. El compilador verifica la seguridad.
actor Counter {
private var count = 0
func increment() { count += 1 }
func value() -> Int { count }
}
let counter = Counter()
Task {
await counter.increment()
print(await counter.value())
}
// Actors serialize access—no data racesGCD (Grand Central Dispatch)
GCD es la API de concurrencia tradicional. DispatchQueue.global() para trabajo en segundo plano, .main para actualizaciones de UI. QoS (.userInitiated, .background) prioriza tareas. Aún útil para código no async.
import Dispatch
DispatchQueue.global(qos: .userInitiated).async {
let result = heavyComputation()
DispatchQueue.main.async {
print("UI update: \(result)")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("delayed by 2s")
}Async Sequence
AsyncSequence habilita iteración sobre valores asíncronos (como un stream). Use for await para consumir. Ideal para APIs paginadas, server-sent events, o cualquier fuente que produzca valores a lo largo del tiempo.
let urls = ["url1", "url2", "url3"]
func fetchAll() async {
for url in urls {
let data = await fetchData(url)
print(data)
}
}
func fetchData(_ url: String) async -> String {
"data from \(url)"
}
// AsyncSequence: for await item in stream { ... }Protocolos en Profundidad
Protocolo con Tipos Asociados
Los tipos asociados (associatedtype) permiten a los protocolos declarar un tipo marcador de posición que los tipos conformes especifican. Esto es el equivalente en Swift de parámetros de tipo genérico para protocolos. El tipo se infiere de los métodos del tipo conforme. Use cláusulas 'where' para restringir tipos asociados. Los PATs (Protocolos con Tipos Asociados) no pueden usarse como tipos existenciales directamente sin type erasure o (Swift 5.7+) 'any Container'.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
// associatedtype Item inferred as Int
private var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}
// Generic constraint with associated type
func sum<C: Container>(_ c: C) -> Int where C.Item == Int {
var total = 0
for i in 0..<c.count { total += c[i] }
return total
}Extensiones de Protocolo (Implementaciones por Defecto)
Las extensiones de protocolo proporcionan implementaciones por defecto—los tipos conformes obtienen el método gratis pero pueden sobrescribirlo. Esta es la forma de Swift de añadir funcionalidad a tipos retroactivamente. Las extensiones restringidas (where Element: Numeric) añaden métodos solo a tipos que cumplen la restricción. Así es como la biblioteca estándar añade map/filter/reduce a todas las Collections.
protocol Describable {
var description: String { get }
}
// Default implementation via extension
extension Describable {
var description: String { "A \(type(of: self))" }
}
struct Point: Describable {
let x, y: Int
// Uses default description: "A Point"
}
struct Person: Describable {
let name: String
var description: String { "Person named (name)" } // override
}
// Extension with constraints
extension Collection where Element: Numeric {
var sum: Element { reduce(0, +) }
}
[1, 2, 3].sum // 6Composición de Protocolos y Existenciales
La composición de protocolos (A & B) requiere que un valor conforme a múltiples protocolos. Los tipos existenciales (any Protocol) pueden contener cualquier tipo conforme pero tienen sobrecarga de despacho en tiempo de ejecución—una búsqueda de witness table por llamada. Use 'some Protocol' (tipo de retorno opaco) cuando devuelva un tipo específico pero quiera ocultarlo. Prefiera genéricos sobre existenciales por rendimiento; use existenciales cuando necesite colecciones heterogéneas.
protocol Named { var name: String { get } }
protocol Aged { var age: Int { get } }
struct Person: Named, Aged {
let name: String
let age: Int
}
// Protocol composition: requires both protocols
func wishHappyBirthday(to celebrant: Named & Aged) {
print("Happy birthday, (celebrant.name), you're (celebrant.age)!")
}
let p = Person(name: "Alice", age: 30)
wishHappyBirthday(to: p)
// Existential types (any) — type erasure overhead
let items: [any Named] = [p, Pet(name: "Rex")]
for item in items { print(item.name) }
// Swift 5.7: 'any' keyword required for existentials
// 'some' for opaque types (returns one concrete type)Programación Orientada a Protocolos
La Programación Orientada a Protocolos es el cambio de paradigma de Swift desde OOP. En lugar de jerarquías de clases, diseñe alrededor de protocolos con implementaciones por defecto. Beneficios: funciona con tipos de valor (structs/enums), admite conformidad retroactiva (extender tipos que no posee), habilita 'herencia' múltiple (un tipo puede conformar a muchos protocolos). Las clases aún son útiles para semántica de referencia e interop con Objective-C, pero structs+protocolos son preferidos para la mayoría de modelos.
// POP: design around protocols, not inheritance
protocol Drawable {
func draw(on canvas: Canvas)
}
extension Drawable {
// Default implementation + polymorphism
func drawTwice(on canvas: Canvas) {
draw(on: canvas)
draw(on: canvas)
}
}
struct Circle: Drawable { let radius: Double
func draw(on canvas: Canvas) { /* ... */ }
}
struct Square: Drawable { let side: Double
func draw(on canvas: Canvas) { /* ... */ }
}
// Value types + protocols = no inheritance needed
let shapes: [any Drawable] = [Circle(radius: 5), Square(side: 3)]
shapes.forEach { $0.draw(on: canvas) }
// Retroactive modeling (extend types you don't own)
extension Int: Drawable {
func draw(on canvas: Canvas) { /* draw the number */ }
}Testigos de Protocolo Personalizados
Los protocolos como dependencias (Repository) habilitan intercambiar implementaciones (real vs mock) para testing y flexibilidad. Los tipos asociados hacen los protocolos genéricos. Esta es la inyección de dependencias de Swift—inyecte un tipo conforme al protocolo. El 'protocol witness' es el tipo conforme que proporciona comportamiento concreto. Este patrón (repository, data source) es común en arquitectura Swift (VIPER, Clean Architecture).
// Protocol with requirements
protocol Repository {
associatedtype Entity
func get(_ id: Int) -> Entity?
func save(_ entity: Entity)
}
// Concrete implementation
struct UserRepo: Repository {
typealias Entity = User
func get(_ id: Int) -> User? { /* fetch */ nil }
func save(_ user: User) { /* persist */ }
}
// Generic function using protocol
func display<R: Repository>(_ repo: R, id: Int) where R.Entity: CustomStringConvertible {
if let entity = repo.get(id) {
print(entity.description)
}
}
// Test double for testing
struct MockUserRepo: Repository {
typealias Entity = User
var stored: [Int: User] = [:]
func get(_ id: Int) -> User? { stored[id] }
func save(_ user: User) { /* no-op for tests */ }
}Genéricos en Profundidad
Funciones y Tipos Genéricos
Los genéricos escriben código flexible y reutilizable que funciona con cualquier tipo preservando la seguridad de tipos. T es un parámetro de tipo (marcador de posición). El compilador genera versiones especializadas por rendimiento (sin boxing). Los tipos genéricos (Stack<T>) mantienen su parámetro de tipo. Element es el tipo genérico de Array. Los genéricos se resuelven en tiempo de compilación—sin sobrecarga en tiempo de ejecución, a diferencia de los existenciales.
// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a; a = b; b = temp
}
var x = 1, y = 2
swap(&x, &y) // x=2, y=1
// Generic type
struct Stack<T> {
private var items: [T] = []
mutating func push(_ item: T) { items.append(item) }
mutating func pop() -> T? { items.popLast() }
}
var intStack = Stack<Int>()
intStack.push(42)
var strStack = Stack<String>()
strStack.push("hello")
// Generic method on non-generic type
extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}Restricciones de Tipo
Las restricciones de tipo limitan qué tipos se pueden usar: T: Equatable requiere que T conforme a un protocolo, T: SomeClass requiere una jerarquía de clases. La cláusula where añade restricciones más complejas (por ejemplo, coincidir tipos asociados). Las restricciones le permiten usar los métodos del protocolo (== para Equatable, < para Comparable). Sin restricciones, solo puede asignar y pasar T—sin operaciones.
// Constraint: T must conform to Equatable
func indexOf<T: Equatable>(_ item: T, in array: [T]) -> Int? {
for (i, elem) in array.enumerated() {
if elem == item { return i } // needs Equatable
}
return nil
}
// Multiple constraints
func max<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
// Constraint on associated type
protocol Repository {
associatedtype Entity: Identifiable
func find(_ id: Entity.ID) -> Entity?
}
// where clause for complex constraints
func merge<C1: Collection, C2: Collection>(
_ c1: C1, _ c2: C2
) -> [C1.Element] where C1.Element == C2.Element {
Array(c1) + Array(c2)
}Tipos Opacos (some)
Los tipos opacos (some Protocol, Swift 5.1) devuelven un tipo concreto específico que está oculto al llamador. A diferencia de 'any' (existencial), el tipo es fijo y conocido por el compilador—sin boxing, despacho en tiempo de compilación. Esta es la base del 'some View' de SwiftUI. Use 'some' cuando quiera ocultar el tipo concreto pero mantener el rendimiento. El llamador puede usar métodos del protocolo pero no puede depender del tipo específico.
// Opaque return type: returns ONE concrete type
// (hidden from caller, but fixed per call site)
func makeStack() -> some Collection<Int> {
return [1, 2, 3] // concrete type is [Int]
}
let s = makeStack() // type is 'some Collection<Int>'
print(s.count) // 3
// Useful for hiding implementation details
protocol Shape { func draw() }
struct Circle: Shape { func draw() { print("circle") } }
func makeShape() -> some Shape {
return Circle() // caller doesn't know it's Circle
}
// Difference from existential (any):
// - some: one concrete type, no boxing, compile-time dispatch
// - any: can be different types, runtime dispatch, boxing
// Opaque types in properties
struct View {
var body: some View { Text("Hello") } // SwiftUI
}Conformidad Condicional
La conformidad condicional hace que un tipo conforme a un protocolo solo cuando sus parámetros de tipo cumplen ciertas condiciones. Array<Int> es Equatable porque Int lo es; Array<MyStruct> no lo es a menos que MyStruct lo sea. Esto se propaga: [[Int]] es Equatable porque [Int] lo es. La biblioteca estándar usa esto extensivamente—Array, Optional, Dictionary todos conforman condicionalmente a Equatable, Hashable, Codable.
// Array conforms to Equatable only if Element is Equatable
extension Array: Equatable where Element: Equatable {
static func == (lhs: [Element], rhs: [Element]) -> Bool {
return lhs.count == rhs.count && lhs.elementsEqual(rhs)
}
}
[1, 2, 3] == [1, 2, 3] // true (Int: Equatable)
// [SomeStruct] == [SomeStruct] // error if SomeStruct isn't Equatable
// Conditional conformance for Hashable
extension Stack: Equatable where T: Equatable {
static func == (lhs: Stack, rhs: Stack) -> Bool {
return lhs.items == rhs.items
}
}
// Enables: [Stack<Int>] == [Stack<Int>] (transitively)Result y Manejo de Errores Genérico
Result<Success, Failure> es un enum genérico para operaciones que pueden fallar, llevando un valor o un error. Es útil cuando quiere diferir el manejo de errores o almacenar resultados. map transforma el valor de éxito; flatMap encadena operaciones que también pueden fallar. Result { try ... } convierte funciones que lanzan. .get() convierte de vuelta a throwing. Con async/await, Result es menos necesario pero aún útil para errores almacenados/pasados.
// Result: built-in generic for success/failure
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
func fetchUser(_ id: Int) -> Result<User, FetchError> {
if id < 0 { return .failure(.invalidId) }
return .success(User(id: id))
}
switch fetchUser(42) {
case .success(let user): print(user)
case .failure(let err): print(err)
}
// map/flatMap for chaining
let result = fetchUser(1)
.map { $0.name }
.flatMap { name in fetchAvatar(name) }
// Throwing function → Result
let result = Result { try throwingFunction() }
// Result → throwing
let value = try result.get()ARC y Gestión de Memoria
Conteo Automático de Referencias (ARC)
ARC (Automatic Reference Counting) gestiona memoria para instancias de clase (tipos de referencia). Cada referencia fuerte incrementa el conteo; eliminar una lo decrementa. Cuando el conteo llega a 0, el objeto se libera inmediatamente (determinístico, a diferencia del garbage collection). Structs/enums (tipos de valor) no usan ARC—se copian. ARC no puede manejar ciclos de referencia—debe usar weak/unowned para romperlos.
class Person {
let name: String
init(name: String) { self.name = name; print("\(name) initialized") }
deinit { print("\(name) deinitialized") }
}
var p1: Person? = Person(name: "Alice") // "Alice initialized"
var p2 = p1 // strong ref count = 2
p1 = nil // ref count = 1, not deinitialized
p2 = nil // ref count = 0, "Alice deinitialized"
// ARC tracks strong references:
// - +1 on assignment/copy
// - -1 when variable goes out of scope
// - Object freed when count hits 0
// Unlike GC, deterministic (freed immediately when count=0)Referencias Strong, Weak y Unowned
Las referencias strong (predeterminadas) mantienen los objetos vivos. Las referencias weak no mantienen el objeto vivo y se vuelven nil cuando el objeto se libera (debe ser optional var). Las referencias unowned no mantienen el objeto vivo pero son non-optional—use solo cuando esté seguro de que el objeto referenciado sobrevive a la referencia (crash si se accede después de dealloc). Use weak para patrones delegate; unowned para padre-hijo donde el padre siempre sobrevive al hijo.
class Person {
var name: String
var apartment: Apartment? // strong
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class Apartment {
var unit: String
weak var tenant: Person? // weak breaks cycle
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) deinit") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice?.apartment = apt
apt?.tenant = alice // weak, doesn't keep alice alive
alice = nil // "Alice deinit" (no strong cycle)
apt = nil // "Apartment 4A deinit"
// unowned: non-optional, assumed to always have a value
// Use when the referenced object outlives the reference
class Customer {
var card: CreditCard? // strong
}
class CreditCard {
unowned let owner: Customer // non-nil, non-optional
init(owner: Customer) { self.owner = owner }
}Ciclos de Retención y Closures
Los closures capturan referencias fuertemente por defecto. Si una clase almacena un closure que captura self, tiene un ciclo de retención (ambos se mantienen vivos mutuamente → fuga de memoria). Solucione con listas de captura: [weak self] (self se vuelve optional, use guard let) o [unowned self] (non-optional, causa crash si nil). Siempre use weak self en closures almacenados como propiedades o pasados a objetos de larga duración (observadores, tareas asíncronas).
class ViewModel {
var name = "VM"
var onUpdate: (() -> Void)?
func setup() {
// BAD: closure captures self strongly → cycle
onUpdate = {
print(self.name) // strong capture
}
// ViewModel holds closure, closure holds ViewModel → leak!
// GOOD: capture list with weak self
onUpdate = { [weak self] in
guard let self = self else { return }
print(self.name)
}
// GOOD: unowned if self outlives closure
onUpdate = { [unowned self] in
print(self.name)
}
}
deinit { print("VM deinit") }
}Closures Escaping y Non-escaping
Los closures non-escaping (predeterminados) se ejecutan dentro de la función y se descartan—sin riesgo de ciclo de retención, sin necesidad de self. en el closure. Los closures @escaping sobreviven a la función (almacenados en una propiedad, despachados async, etc.)—pueden causar ciclos de retención, así que debe usar self. explícitamente y considerar [weak self]. El compilador lo exige. La mayoría de los completion handlers son @escaping; map/filter/reduce son non-escaping.
class DataLoader {
var storedHandler: (() -> Void)?
// @escaping: closure outlives the function (stored or async)
func load(with handler: @escaping (Data) -> Void) {
storedHandler = { handler(cachedData) } // stored
DispatchQueue.global().async {
handler(self.fetch()) // escapes via async
}
}
// Non-escaping (default): closure runs during the function
func process(_ transform: (Int) -> Int) -> [Int] {
return [1, 2, 3].map(transform) // runs now, no storage
}
// No need for [weak self]—self is alive during the call
}
// @escaping requires explicit self. in closures
class View {
func load() {
DataLoader().load { [self] data in // self. required
print(data)
}
}
}Seguridad de Memoria y Exclusividad
Swift exige exclusividad—una variable no puede ser accedida (leer+escribir o escribir+escribir) simultáneamente desde ámbitos superpuestos. Esto previene data races y comportamiento indefinido. Los parámetros inout obtienen acceso exclusivo por la duración de la llamada. Los métodos mutating de structs mantienen acceso exclusivo a self. Los Actors (Swift 5.5+) exigen exclusividad a nivel de lenguaje para estado mutable compartido, eliminando data races por diseño.
// Exclusivity: Swift prevents overlapping access to a variable
// This is a compile-time or runtime error:
var step = 1
func increment(_ n: inout Int) {
step += 1 // accessing global while n is inout to step
}
// increment(&step) // ERROR: overlapping access
// Fix with explicit copy
func safeIncrement(_ n: inout Int) {
var copy = step
copy += 1
step = copy
}
// Struct mutating methods have exclusive access
struct Counter {
var count = 0
mutating func increment(_ other: inout Counter) {
count += other.count // ok, different instances
}
}
// 'inout' parameters: exclusive access for duration
// 'isolation' (Swift 5.5+): actors enforce exclusivityFundamentos de SwiftUI
Vistas y Modificadores
SwiftUI es el framework de UI declarativa de Apple. Las vistas son structs que conforman a View con una propiedad 'body'. Los modificadores (.font, .padding) devuelven nuevas vistas envueltas—no mutan. Las vistas son tipos de valor ligeros; SwiftUI hace diff para actualizar la UI real. El estilo declarativo describe cómo debería verse la UI para un estado dado, y SwiftUI maneja las transiciones.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Text("Hello, World!")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
Button(action: { print("tapped") }) {
HStack {
Image(systemName: "star.fill")
Text("Tap me")
}
}
.padding()
.background(Color.orange)
.cornerRadius(10)
}
.padding()
}
}
// Views are value types (structs), declarative
// Modifiers return new views (immutability)State y Binding
@State es para estado de vista local y mutable—cuando cambia, SwiftUI re-renderiza la vista. @Binding permite a una vista hija leer/escribir el @State de un padre vía el prefijo $ (crea un binding). State debería ser la única fuente de verdad; pase bindings hacia abajo, no valores a modificar. Para estado compartido/complejo entre vistas, use @StateObject/@ObservedObject/@EnvironmentObject con ObservableObject.
struct CounterView: View {
@State private var count = 0 // local, mutable state
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") { count += 1 }
}
}
}
// @Binding: child receives a reference to parent's state
struct StepperView: View {
@Binding var value: Int
var body: some View {
HStack {
Button("-") { value -= 1 }
Text("\(value)")
Button("+") { value += 1 }
}
}
}
// Parent passes binding with $
struct ParentView: View {
@State var score = 0
var body: some View {
StepperView(value: $score) // $ creates a Binding
}
}ObservableObject y EnvironmentObject
ObservableObject (class) es para estado compartido entre múltiples vistas. Las propiedades @Published disparan actualizaciones de vista cuando cambian. @StateObject crea y posee el objeto (use en la cima de la jerarquía). @EnvironmentObject inyecta el objeto en el árbol de vistas—cualquier descendiente puede acceder sin pasar a través de bindings. @ObservedObject recibe un objeto existente (no lo posee). Use @StateObject para propiedad, @ObservedObject/@EnvironmentObject para recibir.
// Model: ObservableObject for shared state
class AppModel: ObservableObject {
@Published var username: String = "" // @Published triggers UI update
@Published var isLoggedIn: Bool = false
func login() {
// ... auth logic ...
isLoggedIn = true
}
}
// @StateObject: owns the model (created once)
struct RootView: View {
@StateObject var model = AppModel()
var body: some View {
// Inject into environment
ContentView().environmentObject(model)
}
}
// @EnvironmentObject: receives from environment
struct ProfileView: View {
@EnvironmentObject var model: AppModel
var body: some View {
Text("Hello, \(model.username)")
}
}
// @ObservedObject: receives from parent (doesn't own)
struct LoginView: View {
@ObservedObject var model: AppModel
}Listas y Navegación
List renderiza filas desplazables (como UITableView). Los elementos deben conformar a Identifiable (o proporcione id:). NavigationStack (iOS 16+) gestiona navegación; NavigationLink empuja destinos. LazyVStack/LazyHStack cargan contenido perezosamente por rendimiento. ForEach es para repetir vistas dentro de otros contenedores. Use List para datos tabulares, ScrollView+LazyVStack para layouts personalizados.
struct Item: Identifiable {
let id = UUID()
let name: String
}
struct ListView: View {
let items = [Item(name: "Apple"), Item(name: "Banana")]
var body: some View {
NavigationStack {
List(items) { item in
NavigationLink(item.name) {
DetailView(item: item)
}
}
.navigationTitle("Fruits")
}
}
}
struct DetailView: View {
let item: Item
var body: some View {
Text("Detail: \(item.name)")
.navigationTitle(item.name)
}
}
// ForEach for custom layouts
ScrollView {
LazyVStack {
ForEach(items) { item in
Text(item.name)
}
}
}Formularios y Sheets
Form estila automáticamente controles para pantallas de configuración (agrupado, apropiado para la plataforma). Controles comunes: TextField, Toggle, Slider, Picker, Stepper. .sheet presenta un modal; .fullScreenCover para pantalla completa. Los bindings ($) conectan controles al estado. Form se adapta a la plataforma (lista agrupada en iOS, layout de formulario en macOS). Use Form para entrada de datos y configuración; use VStack para layouts personalizados.
struct SettingsView: View {
@State private var name = ""
@State private var notifications = true
@State private var volume: Double = 0.5
@State private var theme = "Light"
@State private var showSheet = false
var body: some View {
Form {
Section("Profile") {
TextField("Name", text: $name)
Toggle("Notifications", isOn: $notifications)
}
Section("Audio") {
Slider(value: $volume, in: 0...1)
}
Section("Appearance") {
Picker("Theme", selection: $theme) {
Text("Light").tag("Light")
Text("Dark").tag("Dark")
}
}
Button("Show Modal") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
Text("Modal content")
}
}
}Closures en Profundidad
Variaciones de Sintaxis de Closures
Los closures tienen muchas formas de sintaxis. Forma completa: { (params) -> Type in body }. La inferencia de tipos le permite omitir tipos. Los argumentos abreviados ($0, $1) reemplazan parámetros nombrados. La sintaxis de trailing closure mueve el closure fuera de () cuando es el último argumento. Múltiples trailing closures (Swift 5.3+) nombran closures adicionales. Use la forma más concisa que aún sea legible—los argumentos abreviados son geniales para closures cortos como map.
// Full closure
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
// Type inference
let add2: (Int, Int) -> Int = { a, b in a + b }
// Shorthand argument names ($0, $1...)
let add3: (Int, Int) -> Int = { $0 + $1 }
// Trailing closure syntax
let result = [1, 2, 3].map { $0 * 2 } // [2, 4, 6]
// Multiple trailing closures (Swift 5.3+)
Button {
print("action") // action
} label: {
Text("Click") // label
}
// No return for Void closures
let log: (String) -> Void = { msg in print(msg) }Captura de Valores
Los closures capturan variables de su ámbito envolvente. Por defecto, capturan por referencia (los cambios a la variable capturada son visibles). Las listas de captura [foo] capturan por valor (una instantánea en la creación del closure). Para tipos de referencia (clases), [weak self] o [unowned self] rompen ciclos de retención. Los tipos de valor (structs) capturados por referencia aún ven actualizaciones porque el closure mantiene una caja. Las listas de captura van antes de la lista de parámetros.
// Closures capture surrounding state
func makeIncrementer(amount: Int) -> () -> Int {
var total = 0
return {
total += amount // captures total and amount
return total
}
}
let inc = makeIncrementer(amount: 5)
inc() // 5
inc() // 10
// total persists between calls (captured by reference)
// Capture list controls how
class Foo { var x = 0 }
let foo = Foo()
let closure = { [foo] in // captures foo by value (snapshot)
print(foo.x)
}
foo.x = 100
closure() // prints 0 (captured the old value)
// Without capture list, foo is captured by reference
let refClosure = { print(foo.x) }
foo.x = 200
refClosure() // prints 200Escaping y Autoclosure
@autoclosure envuelve una expresión en un closure sin argumentos automáticamente, así los llamadores escriben la expresión directamente (sin llaves). Esto habilita evaluación perezosa—assert() lo usa para saltar la condición en builds de release. Combinado con @escaping, puede diferir la evaluación. Los operadores && y || usan @autoclosure para evaluación de cortocircuito. Use @autoclosure con moderación—oculta que el código se difiere, lo que puede confundir a los lectores.
// @autoclosure: wraps an expression in a closure automatically
func log(_ condition: @autoclosure () -> Bool, _ message: String) {
if condition() { // evaluated here, not at call
print(message)
}
}
log(2 > 1, "two is greater") // no { } needed
// assert uses @autoclosure to avoid evaluating in release
assert(isValid, "Invalid state") // isValid not checked in release
// @autoclosure + @escaping
func deferred(_ block: @autoclosure @escaping () -> Int) {
DispatchQueue.main.async { print(block()) }
}
deferred(expensiveComputation()) // runs later, on main
// Short-circuit evaluation with @autoclosure
func &&(lhs: @autoclosure () -> Bool, rhs: @autoclosure () -> Bool) -> Bool {
return lhs() ? rhs() : false // rhs not evaluated if lhs is false
}Funciones de Orden Superior
Las funciones de orden superior de Swift (map, filter, reduce, flatMap, compactMap) habilitan programación funcional en colecciones. map transforma, filter selecciona, reduce agrega, flatMap aplana, compactMap elimina nils. Estas devuelven nuevas colecciones (inmutabilidad). El encadenamiento compone operaciones. Para conjuntos de datos grandes, use .lazy para evitar arrays intermedios. Estas son la base del Swift funcional—prefiéralas sobre bucles for imperativos al transformar datos.
let nums = [1, 2, 3, 4, 5, 6]
// map: transform each element
let doubled = nums.map { $0 * 2 } // [2, 4, 6, 8, 10, 12]
// filter: keep elements matching predicate
let evens = nums.filter { $0 % 2 == 0 } // [2, 4, 6]
// reduce: combine all into one
let sum = nums.reduce(0, +) // 21
let product = nums.reduce(1, *) // 720
// flatMap: transform + flatten
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 } // [1, 2, 3, 4]
// compactMap: transform + filter nils
let strings = ["1", "abc", "3"]
let ints = strings.compactMap { Int($0) } // [1, 3]
// Chaining (lazy evaluation)
let result = nums
.filter { $0 % 2 == 0 }
.map { $0 * $0 }
.reduce(0, +) // 4 + 16 + 36 = 56Closures como Completion Handlers
Los completion handlers eran el patrón async estándar antes de async/await (Swift 5.5). Son @escaping (se ejecutan después) y a menudo usan Result para manejo de errores. La desventaja son los callbacks anidados ('pirámide de perdición'). async/await hace esto lineal y legible. El código nuevo debería usar async/await; los completion handlers permanecen para APIs delegate e interop con Objective-C. Puede envolver completion handlers con withCheckedContinuation para usarlos con async/await.
// Completion handler pattern (pre-async/await)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let user = try? JSONDecoder().decode(User.self, from: data) else {
completion(.failure(DecodeError()))
return
}
completion(.success(user))
}.resume()
}
// Usage (pyramid of doom)
fetchUser(id: 42) { result in
switch result {
case .success(let user):
fetchAvatar(user) { avatarResult in
// nested...
}
case .failure(let error):
print(error)
}
}
// Modern: async/await replaces this
func fetchUserAsync(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}Property Wrappers
Definir un Property Wrapper
Los property wrappers (@propertyWrapper) encapsulan comportamiento de propiedad reutilizable. La propiedad wrappedValue es con la que interactúan los usuarios. El init recibe el valor inicial y cualquier argumento personalizado. SwiftUI usa property wrappers extensivamente: @State, @Binding, @Published, @AppStorage. Defina los suyos para validación, caché, logging o valores predeterminados. El wrapper es un struct/class que gestiona el almacenamiento.
@propertyWrapper
struct Clamped<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
self.range = range
}
}
// Usage
struct Slider {
@Clamped(0...100) var progress: Int = 50
}
var s = Slider()
s.progress = 150 // clamped to 100
s.progress = -10 // clamped to 0
print(s.progress) // 0Valor Proyectado
El projectedValue (accedido vía $) es una interfaz secundaria a la propiedad. SwiftUI usa esto extensivamente: el $ de @State da un Binding para vistas hijas, el $ de @Published da un Publisher. Defina projectedValue cuando el wrapper deba exponer algo más allá del valor envuelto (un binding, un publisher, el wrapper mismo). La sintaxis de prefijo $ hace esto ergonómico. No todos los wrappers necesitan projectedValue—es opcional.
@propertyWrapper
struct Observable<T> {
var value: T
var wrappedValue: T {
get { value }
set { value = newValue; onChange() }
}
var onChange: () -> Void = {}
// projectedValue: accessed via $ (like @State's binding)
var projectedValue: Observable<T> { self }
}
struct View {
@Observable var count = 0
// count: the wrapped value (Int)
// $count: the projected value (Observable<Int>)
}
// SwiftUI's @State projects a Binding
struct Counter: View {
@State var value = 0
var body: some View {
Stepper("Value: \(value)", value: $value) // Binding<Int>
}
}Property Wrappers Integrados
SwiftUI proporciona muchos property wrappers: @State (estado local), @Binding (estado del padre), @ObservedObject/@StateObject (modelo externo), @EnvironmentObject (modelo inyectado), @AppStorage (UserDefaults), @SceneStorage (restauración de escena), @FocusState (foco de teclado), @ScaledMetric (Dynamic Type), @Namespace (animaciones). Cada uno gestiona almacenamiento y ciclo de vida. El @Published de Combine dispara actualizaciones de UI. Saber cuándo usar cuál es clave para SwiftUI.
import SwiftUI
import Combine
struct ContentView: View {
@State var count = 0 // local view state
@Binding var externalValue: Int // parent-owned
@EnvironmentObject var model: AppModel // injected
@AppStorage("username") var username = "" // UserDefaults
@SceneStorage("draft") var draft = "" // per-scene storage
@FocusState var isFocused: Bool // focus management
@ScaledMetric var size = 16 // Dynamic Type scaling
@Namespace var animationNamespace // matched geometry
// Combine
@Published var data = [Item]() // in ObservableObject
}
// @AppStorage auto-syncs with UserDefaults
// @SceneStorage restores state per scene (iPad multitasking)Wrappers Personalizados para Validación
Los property wrappers brillan para preocupaciones transversales. NonEmpty valida al establecer. Cached calcula perezosamente y memoiza. Otras ideas: Logged (registra cambios), Trimmed (recorta espacios en blanco), Formatted (formatea al obtener), Clamped (límites de rango), UserDefaults-backed. Los wrappers reducen código repetitivo—escribe la lógica una vez y aplícala con @. Se componen: @Logged @Clamped(0...100) var value. Mantenga los wrappers enfocados en una sola preocupación.
@propertyWrapper
struct NonEmpty {
private var value: String = ""
var wrappedValue: String {
get { value }
set {
guard !newValue.isEmpty else {
print("Warning: cannot set to empty")
return
}
value = newValue
}
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
@propertyWrapper
struct Cached<T> {
private var cached: T?
private var generator: () -> T
var wrappedValue: T {
mutating get {
if cached == nil { cached = generator() }
return cached!
}
}
init(wrappedValue: @autoclosure @escaping () -> T) {
self.generator = wrappedValue
}
}
struct Config {
@NonEmpty var name: String = "default"
@Cached var expensiveValue: Int = computeExpensive()
}Property Wrappers en Protocolos y Genéricos
Los property wrappers pueden ser genéricos y usarse en protocolos. Sin embargo, tienen limitaciones: un struct con propiedades envueltas no obtiene inicializadores memberwise (debe escribir init manualmente), y la copia puede ser complicada (el wrapper se copia, incluyendo su estado). Para clases, esto es menos un problema. A pesar de las limitaciones, los wrappers son potentes para comportamiento de propiedad declarativo y reutilizable. El diseño de SwiftUI depende en gran medida de ellos.
// Apply to protocol requirements
protocol Configurable {
@NonEmpty var name: String { get set }
}
// Generic property wrapper
@propertyWrapper
struct Validated<T> {
private var value: T?
let validator: (T) -> Bool
var wrappedValue: T? {
get { value }
set {
if let v = newValue, validator(v) { value = v }
}
}
init(wrappedValue: T?, validator: @escaping (T) -> Bool) {
self.validator = validator
self.wrappedValue = wrappedValue
}
}
struct Form {
@Validated(validator: { $0.count >= 8 }) var password: String? = nil
}
// Limitation: property wrappers in structs can't be
// initialized from another instance easily (copy issues).
// They work best as stored properties, not computed.Framework Combine
Publishers y Subscribers
Combine es el framework reactivo de Apple (como RxSwift). Los Publishers emiten valores; los Subscribers los reciben. sink crea un subscriber con closures. assign vincula la salida a una propiedad. Los operadores (map, filter, reduce) transforman publishers declarativamente. Combine es declarativo—describa el pipeline, y los valores fluyen a través. Se usa para operaciones asíncronas, bindings de UI y manejo de eventos. El Swift moderno prefiere async/await, pero Combine permanece para pipelines complejos.
import Combine
// Publisher: emits values over time
let publisher = [1, 2, 3].publisher
// Subscriber: receives values
publisher.sink { value in
print(value) // 1, 2, 3
}
// With completion
publisher.sink(receiveCompletion: { completion in
print("done: \(completion)")
}, receiveValue: { value in
print(value)
})
// Assign to a property
class ViewModel {
var data: String = ""
}
let vm = ViewModel()
Just("hello").assign(to: &vm.data)
// Operators transform publishers
[1, 2, 3, 4].publisher
.map { $0 * 2 }
.filter { $0 > 4 }
.sink { print($0) } // 6, 8@Published y ObservableObject
@Published envuelve una propiedad y emite su nuevo valor a los suscriptores cuando cambia. El prefijo $ accede al publisher. Esto tiende un puente entre Combine y SwiftUI—@Published en un ObservableObject dispara actualizaciones de vista. Las suscripciones se cancelan cuando el AnyCancellable se desasigna, así que almacénelos (por ejemplo, en un Set). Use Combine para debouncing, throttling, combinar múltiples fuentes asíncronas—cosas que async/await maneja menos elegantemente.
class UserStore: ObservableObject {
@Published var name: String = "" // publishes on change
@Published var age: Int = 0
// $name is a Publisher<String, Never>
func demo() {
$name.sink { print("name changed to \($0)") }
name = "Alice" // prints "name changed to Alice"
}
}
// Combine with SwiftUI
struct ProfileView: View {
@ObservedObject var store: UserStore
var body: some View {
Text(store.name) // updates when name changes
}
}
// Cancellable: store subscriptions to keep them alive
class Service {
var cancellables = Set<AnyCancellable>()
let store = UserStore()
init {
store.$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { name in saveToServer(name) }
.store(in: &cancellables) // keep alive
}
}Operadores
Los operadores de Combine transforman, filtran, combinan y controlan el tiempo de publishers. map/scan transforman; filter/selectors eligen; merge/zip/combineLatest combinan múltiples streams; debounce/throttle controlan el tiempo. Los operadores devuelven nuevos publishers (inmutabilidad, encadenamiento). debounce espera por un período de quietud (search-as-you-type); throttle limita la tasa (taps de botón). collect reúne todos los valores en un array. Estos habilitan pipelines reactivos declarativos.
let publisher = [1, 2, 3, 4, 5].publisher
// Transforming
publisher.map { $0 * 2 } // [2, 4, 6, 8, 10]
publisher.scan(0, +) // [1, 3, 6, 10, 15] (running sum)
// Filtering
publisher.filter { $0 % 2 == 0 } // [2, 4]
publisher.removeDuplicates() // consecutive dupes
publisher.first() // just 1
// Combining
let p1 = [1, 2].publisher
let p2 = [3, 4].publisher
p1.merge(with: p2) // interleaved: 1, 2, 3, 4 (or any order)
p1.zip(p2) // pairs: (1,3), (2,4)
p1.combineLatest(p2) // latest of each
// Timing
publisher.delay(for: 1, scheduler: RunLoop.main)
publisher.throttle(for: 1, scheduler: .main, latest: true)
publisher.debounce(for: 0.5, scheduler: .main)
publisher.collect() // gather all into [1,2,3,4,5]Subjects (Publicación Manual)
Los Subjects son publishers mutables a los que puede enviar valores manualmente—tienden un puente entre código imperativo y reactivo. PassthroughSubject transmite sin almacenar (stream de eventos). CurrentValueSubject almacena el último valor (estado). Los nuevos suscriptores a CurrentValueSubject obtienen inmediatamente el valor actual. Use Subjects para envolver delegates, notificaciones o eventos de UI en pipelines de Combine. @Published es esencialmente un CurrentValueSubject integrado con ObservableObject.
import Combine
// PassthroughSubject: broadcasts to subscribers, no current value
let subject = PassthroughSubject<Int, Never>()
subject.sink { print($0) } // subscriber 1
subject.send(1) // prints 1
subject.send(2) // prints 2
// CurrentValueSubject: holds current value, new subs get it
let current = CurrentValueSubject<Int, Never>(0)
current.sink { print($0) } // prints 0 (current)
current.send(1) // prints 1
current.sink { print($0) } // prints 1 (current)
current.value // 1
// Use cases:
// - PassthroughSubject: events (button taps, notifications)
// - CurrentValueSubject: state (like @Published but more control)
// - Bridge imperative code to Combine
// Finish a subject
subject.send(completion: .finished)Manejo de Errores
Los publishers de Combine tienen un tipo Failure (Never para los que no pueden fallar). catch reemplaza un error con un publisher alternativo. retry re-suscribe en fallo (bueno para redes inestables). mapError transforma el tipo de error. assertNoFailure causa crash si ocurre un error (use cuando esté seguro). Los publishers con Failure == Never se pueden usar en cualquier lugar; los que tienen errores necesitan manejo. Esto hace que el manejo de errores sea explícito y componible en pipelines reactivos.
enum APIError: Error { case network, parse }
// Publishers can fail with an error
let publisher = Fail<Int, APIError>(error: .network)
// catch: replace error with fallback publisher
publisher
.catch { _ in Just(0) } // fallback to 0
.sink { print($0) } // 0
// retry: retry on failure
URLSession.shared.dataTaskPublisher(for: url)
.retry(3) // retry up to 3 times
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
// mapError: transform error type
publisher
.mapError { _ in NSError(domain: "x", code: 1) }
// assertNoFailure: crash if error (when you're sure it won't)
Just(1).assertNoFailure().sink { print($0) }
// Result publisher
let resultPublisher = [1, 2, 3].publisher.setFailureType(to: Error.self)Control de Acceso y Organización de Código
Niveles de Acceso
Swift tiene cinco niveles de acceso. private (ámbito más pequeño, dentro de la declaración), fileprivate (dentro del archivo fuente), internal (dentro del módulo, predeterminado), public (cualquiera que importe), open (public + subclaseable/sobrescribible, solo clases). Swift 5.9 añade package para módulos SPM. Comience restrictivo y amplíe según sea necesario. Use public para APIs de biblioteca, internal para código de app, private para detalles de implementación. open es para clases de framework diseñadas para subclasear.
// public: accessible from any module that imports this
public class PublicAPI {
public func connect() {}
}
// internal (default): accessible within the module
class InternalClass {
func helper() {} // internal by default
}
// fileprivate: accessible within the source file
fileprivate func fileHelper() {}
// private: accessible within the enclosing declaration
class Counter {
private var count = 0 // only Counter can access
fileprivate func reset() { count = 0 } // same file
}
// Swift 5.9: package (for Swift Package Manager)
package struct PackageType {
package var value: Int
}
// Guidelines:
// - Start restrictive (private), open up as needed
// - public for framework API, internal for implementation
// - private for encapsulation within a typeExtensiones y Organización
Las extensiones le permiten dividir la funcionalidad de un tipo entre archivos (String+Validation.swift, String+Parsing.swift) y añadir conformidad retroactivamente. Esto mantiene los archivos enfocados y manejables. La conformidad de protocolo puede estar en una extensión/archivo separado de la definición del tipo principal. La conformidad condicional (where Element: ...) añade conformidad condicionalmente. Use extensiones para organizar: mantenga el tipo central mínimo, añada funcionalidad en archivos de extensión enfocados.
// Split a type across files with extensions
// String+Validation.swift
extension String {
var isValidEmail: Bool {
contains("@") && contains(".")
}
}
// String+Parsing.swift
extension String {
func toURL() -> URL? { URL(string: self) }
}
// Conform to protocols in extensions
extension String: Comparable { // already conforms, just showing
// protocol methods
}
// Conditional conformance
extension Array: MyProtocol where Element: MyProtocol {}
// Protocol conformance in a separate file
// (keeps the main type definition clean)
struct User { let id: Int; let name: String }
extension User: Codable {} // synthesized
extension User: Equatable {} // synthesized
extension User: CustomStringConvertible {
var description: String { "User(\(id), \(name))" }
}Módulos y Paquetes
Swift Package Manager (SPM) define módulos vía Package.swift. Cada target es un módulo—el acceso internal es dentro de un target, public cruza módulos. Las dependencias son otros paquetes (URLs de git). Los products son lo que su paquete expone. SPM es la forma moderna de gestionar código Swift (reemplazando CocoaPods/Carthage). Xcode integra SPM nativamente. Estructura: Sources/MyLibrary/ para código, Tests/MyLibraryTests/ para pruebas. Use módulos para separar preocupaciones y controlar el acceso.
// Package.swift (Swift Package Manager)
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLibrary",
products: [
.library(name: "MyLibrary", targets: ["MyLibrary"]),
],
dependencies: [
.package(url: "https://github.com/.../Logging", from: "1.0.0"),
],
targets: [
.target(name: "MyLibrary", dependencies: ["Logging"]),
.testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
]
)
// Importing modules
import Foundation
import MyLibrary // your module
import Logging // dependency
// Each target is a module
// internal access is within a target/module
// public access crosses module boundariesInicializadores y Designated/Convenience
Las clases tienen inicializadores designated (inicializan completamente, deben llamar al designated del super) e inicializadores convenience (delegan a otro init en la misma clase). Este sistema de dos niveles asegura inicialización completa. Los structs obtienen un inicializador memberwise gratis. Los inicializadores failable (init?) devuelven nil en fallo. Los inicializadores required (required init) deben ser implementados por subclases. Para la mayoría del código, prefiera structs (inicialización más simple) sobre clases.
class Person {
let name: String
let age: Int
// Designated initializer: fully initializes
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class Employee: Person {
let employeeId: String
// Designated init must call super's designated init
init(name: String, age: Int, employeeId: String) {
self.employeeId = employeeId
super.init(name: name, age: age) // must call super
}
// Convenience init must call another init (same class)
convenience init(name: String) {
self.init(name: name, age: 0, employeeId: "TEMP")
}
}
// Structs: memberwise initializer (free)
struct Point { let x, y: Int }
let p = Point(x: 1, y: 2) // synthesized
// Failable initializers
struct Config {
let port: Int
init?(port: Int) {
guard port > 0 else { return nil }
self.port = port
}
}Optionals en Profundidad
Los optionals son el mecanismo de seguridad de nulos de Swift—un enum que es .some(value) o .none (nil). Debe desenvolver para acceder al valor. El encadenamiento optional (?.) navega de forma segura; la fusión nil (??) proporciona un valor predeterminado; if let/guard let desenvuelven de forma segura. Evite ! (force unwrap) a menos que esté seguro—causa crash en nil. Los optionals le obligan a manejar la ausencia explícitamente, eliminando las excepciones de referencia nula comunes en otros lenguajes.
// Optional: enum with .some(Value) and .none
let maybeInt: Int? = 42
let nilInt: Int? = nil
// Optional chaining (short-circuits on nil)
let name: String? = user?.profile?.name // nil if any link is nil
// Nil-coalescing (default value)
let value = maybeInt ?? 0 // 42
let value2 = nilInt ?? 0 // 0
// Forced unwrapping (crashes if nil)
let forced = maybeInt! // 42
// let crash = nilInt! // RUNTIME CRASH
// Optional binding
if let actual = maybeInt {
print(actual) // 42, actual is Int (not optional)
}
// Guard for early exit
func process(_ data: Data?) {
guard let data = data else { return }
// data is non-optional here
}
// Optional try
let result = try? parse(json) // nil on error
let result2 = try! parse(json) // crashes on error
// Multiple binding
if let a = optA, let b = optB, a < b { /* ... */ }Gestión de Memoria (ARC/weak)
Fundamentos de ARC
El Conteo Automático de Referencias (ARC) rastrea referencias fuertes. Cuando el conteo llega a cero, el objeto se desasigna. ARC es determinístico (a diferencia del garbage collection): deinit se ejecuta inmediatamente cuando se libera la última referencia. La mayoría del tiempo, ARC simplemente funciona. Los problemas surgen con ciclos de referencia: dos objetos se referencian fuertemente mutuamente, impidiendo la desasignación. Use weak o unowned para romper ciclos.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
reference1 = Person(name: "Alice")
// Prints: Alice is being initialized
reference2 = reference1 // Strong reference count: 2
reference1 = nil // Count: 1
reference2 = nil // Count: 0, deinit runs
// Prints: Alice is being deinitializedReferencias Weak
Las referencias weak no mantienen vivo el objeto referenciado. Cuando el objeto se desasigna, las referencias weak se vuelven nil automáticamente. weak debe ser optional (var tenant: Person?). Use weak cuando el objeto referenciado tenga un ciclo de vida más corto (el inquilino puede dejar el apartamento). El caso de uso clásico es el patrón delegate: el delegate es weak para evitar ciclos. weak tiene ligera sobrecarga (registrado con el runtime para nil-setting).
class Person {
var name: String
var apartment: Apartment? // Strong (person owns apartment)
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class Apartment {
var unit: String
weak var tenant: Person? // Weak (breaks cycle)
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) deinit") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice!.apartment = apt
apt!.tenant = alice // Weak reference, no cycle
alice = nil // Alice deinit (apartment's tenant becomes nil)
apt = nil // Apartment deinit
// weak must be optional (becomes nil when deallocated)Referencias Unowned
Las referencias unowned, como weak, no mantienen vivo el objeto. A diferencia de weak, unowned es non-optional y no se vuelve nil. Acceder a una referencia unowned después de la desasignación causa crash. Use unowned cuando pueda garantizar que el objeto referenciado sobrevive a la referencia (por ejemplo, una tarjeta de crédito no puede existir sin su cliente). unowned tiene menos sobrecarga que weak. Elija: weak para ciclos de vida inciertos, unowned para ciclos de vida garantizados.
class Customer {
let name: String
var card: CreditCard? // Strong
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class CreditCard {
let number: UInt
unowned let customer: Customer // Non-optional, no cycle
init(number: UInt, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card \(number) deinit") }
}
var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234, customer: john!)
john = nil
// Customer deinit, then CreditCard deinit
// unowned: non-optional, assumed to always have a value
// CRASH if accessed after deallocation
// Use when the referenced object has same or longer lifetimeClosures y Listas de Captura
Los closures capturan referencias fuertemente por defecto, causando ciclos cuando el closure se almacena en self. Las listas de captura ([weak self] o [unowned self]) rompen el ciclo. weak self requiere manejo optional (guard let self = self). Capture valores específicos (let name = self.name) para evitar capturar self enteramente. Los closures pasados a funciones (no almacenados) no causan ciclos. Siempre use listas de captura para closures almacenados que referencian self.
class ViewController {
var name = "MyVC"
var onComplete: (() -> Void)?
// BAD: Strong reference cycle
func setupBad() {
onComplete = {
print("\(self.name) completed") // Captures self strongly
}
}
// GOOD: Capture list breaks cycle
func setupGood() {
onComplete = { [weak self] in
guard let self = self else { return }
print("\(self.name) completed")
}
}
// unowned for guaranteed lifetime
func setupUnowned() {
onComplete = { [unowned self] in
print("\(self.name) completed")
}
}
// Capture specific values
func setupCapture() {
let name = self.name // Capture value, not self
onComplete = {
print("\(name) completed")
}
}
deinit { print("VC deinit") }
}Detección y Solución de Fugas
Fuentes comunes de fugas: delegates almacenados strong (use weak), Timer reteniendo target (invalidate en deinit), observadores de NotificationCenter (elimine en deinit). El Xcode Memory Graph Debugger visualiza grafos de objetos y resalta fugas. Ejecute con MallocStackLogging para obtener trazas de asignación. Pruebe deinit estableciendo referencias a nil y verificando que deinit se ejecuta. Instruments (herramienta Leaks) detecta fugas en tiempo de ejecución. Siempre empareje setup con teardown.
// Xcode memory graph debugger: visualize reference cycles
// Product > Debug > View Memory Graph Hierarchy
// Common leak patterns:
// 1. Delegate stored as strong
class View {
weak var delegate: ViewDelegate? // MUST be weak
}
// 2. Timer retains target
class TimerHolder {
var timer: Timer?
func start() {
// BAD: timer retains self via #selector
timer = Timer.scheduledTimer(timeInterval: 1,
target: self, selector: #selector(fire),
userInfo: nil, repeats: true)
}
@objc func fire() {}
deinit { timer?.invalidate() } // MUST invalidate
}
// 3. NotificationCenter observer
class Observer {
init() {
NotificationCenter.default.addObserver(
self, selector: #selector(handle),
name: .someNotification, object: nil)
}
@objc func handle() {}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// Use Xcode's Debug Memory Graph to find leaksActors y async/await
Fundamentos de async/await
async/await (Swift 5.5+) hace que el código asíncrono parezca síncrono. async marca funciones que pueden suspender; await marca puntos de suspensión. Tasks tienden un puente de contextos síncronos a asíncronos. async let inicia una tarea concurrente; await recoge el resultado. Use async let para ejecución paralela, await regular para secuencial. Los errores se propagan con throws/try. El compilador exige await en puntos de suspensión. async/await reemplaza completion handlers y Combine para muchos casos de uso.
// Async function
func fetchUser(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Calling async functions
func loadProfile() async {
do {
let user = try await fetchUser(id: 1)
print("Loaded: \(user.name)")
} catch {
print("Error: \(error)")
}
}
// Task: bridge async to sync context
Task {
await loadProfile()
}
// Concurrent execution
func loadMultiple() async {
async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
async let user3 = fetchUser(id: 3)
let users = try await [user1, user2, user3]
print("Loaded \(users.count) users")
}
// Sequenced vs concurrent
let sequential = try await fetchUser(id: 1) // Waits
let concurrent = try await fetchUser(id: 2) // Then waits againActors
Los Actors (Swift 5.5+) son tipos de referencia con acceso serializado, previniendo data races. Solo un método se ejecuta en un actor a la vez—sin locking manual necesario. Los métodos de actor deben llamarse con await (pueden suspender). Las propiedades están aisladas y no pueden accederse directamente desde fuera. Los actors son el valor predeterminado seguro para estado mutable compartido en código concurrente. Use actors en lugar de clases con locks para la mayoría de estado concurrente.
// Actor: reference type with serialized access
actor BankAccount {
private var balance: Decimal
init(balance: Decimal) {
self.balance = balance
}
func deposit(_ amount: Decimal) {
balance += amount
}
func withdraw(_ amount: Decimal) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
func getBalance() -> Decimal { balance }
}
// Usage (must be async)
let account = BankAccount(balance: 100)
Task {
await account.deposit(50)
let balance = await account.getBalance()
print("Balance: \(balance)") // 150
}
// Actors prevent data races automatically
// Only one method runs at a time per actor instance
// No need for locks or queuesAislamiento de Actor
El aislamiento de actor protege el estado. Los miembros nonisolated pueden accederse sin await (para valores puros o métodos que no tocan estado aislado). @MainActor es un actor global para aislamiento de subproceso principal (actualizaciones de UI). @globalActor crea actors globales personalizados. Cruzar límites de actor requiere await. El compilador verifica el aislamiento en tiempo de compilación, previniendo data races. Use @MainActor para ViewModels y código de UI; actors personalizados para aislamiento específico de dominio.
actor Counter {
private var count = 0
// Isolated method (default)
func increment() { count += 1 }
// Nonisolated: can be called without await
nonisolated var description: String { "Counter" }
// nonisolated function
nonisolated func id() -> String { "counter-\(ObjectIdentifier(self))" }
}
// Global actor: @MainActor runs on main thread
@MainActor
class ViewModel: ObservableObject {
@Published var data: [Item] = []
func loadData() async {
// Always runs on main thread
data = try await fetchItems()
}
}
// Mark specific functions
class Service {
@MainActor
func updateUI(_ result: Result) {
// UI updates on main thread
}
}
// globalActor lets you apply to types, functions, properties
@globalActor
actor MyActor {
static let shared = MyActor()
}Concurrencia Estructurada
Los task groups habilitan computación paralela dinámica. addTask genera tareas hijas; for await group recoge resultados a medida que completan. Los task groups exigen concurrencia estructurada: todas las hijas completan antes de que el padre continúe. La cancelación se propaga de padre a hijas automáticamente. Verifique Task.isCancelled en tareas largas. withThrowingTaskGroup propaga errores, cancelando hermanos al lanzar. Use task groups para patrones fan-out/fan-in.
// TaskGroup: dynamic parallel computation
func fetchAllUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await fetchUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
// Task tree: parent-child relationship
func process() async {
await withTaskGroup(of: Void.self) { group in
for i in 1...5 {
group.addTask {
await processItem(i)
}
}
// All tasks complete before group returns
}
print("All done")
}
// Cancellation
func longRunning() async {
await withTaskGroup(of: Void.self) { group in
group.addTask {
for i in 1...100 {
if Task.isCancelled { return }
await doWork(i)
}
}
}
}
// Cancel from outside
let task = Task { await longRunning() }
task.cancel()AsyncSequence y Streams
AsyncSequence es la versión asíncrona de Sequence, habilitando bucles for await. AsyncStream tiende un puente de APIs basados en callback o delegate a async/await. yield emite valores, finish termina. onTermination limpia recursos. AsyncThrowingStream admite errores. Use AsyncSequence para APIs paginadas, datos en tiempo real, o cualquier stream de valores a lo largo del tiempo. La biblioteca estándar proporciona .lines en URL para lectura de archivos línea por línea. AsyncSequence se integra con la cancelación de tareas.
// AsyncSequence: iterate asynchronously
struct Counter: AsyncSequence {
struct AsyncIterator: AsyncIteratorProtocol {
var current: Int
let max: Int
mutating func next() async -> Int? {
guard current <= max else { return nil }
defer { current += 1 }
try? await Task.sleep(nanoseconds: 100_000_000)
return current
}
}
let start: Int
let max: Int
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(current: start, max: max)
}
}
// Usage
for await num in Counter(start: 1, max: 5) {
print(num) // 1 2 3 4 5 (with delays)
}
// AsyncStream: bridge callbacks to async
func urlLines(_ url: URL) -> AsyncThrowingStream<String, Error> {
AsyncThrowingStream { continuation in
let task = Task {
do {
for try await line in url.lines {
continuation.yield(line)
}
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}Testing
Fundamentos de XCTest
XCTest es el framework de testing estándar. XCTestCase agrupa pruebas relacionadas. setUp/tearDown se ejecutan antes/después de cada prueba. Aserciones: XCTAssertEqual, XCTAssertNil, XCTAssertThrowsError, XCTAssertTrue. @testable import accede a símbolos internal. Las pruebas se ejecutan en el subproceso principal por defecto. Use XCTAssertNotNil para optionals. Ejecute pruebas con Cmd+U. Xcode muestra resultados de prueba en línea. Escriba pruebas junto al código para feedback rápido.
import XCTest
@testable import MyApp
class UserTests: XCTestCase {
var user: User!
override func setUp() {
super.setUp()
user = User(name: "Alice", age: 30)
}
override func tearDown() {
user = nil
super.tearDown()
}
func testName() {
XCTAssertEqual(user.name, "Alice")
}
func testAgeAfterBirthday() {
user.haveBirthday()
XCTAssertEqual(user.age, 31)
}
func testValidation() {
XCTAssertThrowsError(try User(name: "", age: -1)) { error in
guard let error = error as? UserError else {
XCTFail("Wrong error type")
return
}
XCTAssertEqual(error, .invalidName)
}
}
func testOptional() {
XCTAssertNotNil(user.email)
XCTAssertNil(user.address)
}
}
// Run: Cmd+U in XcodeTesting Asíncrono
Swift 5.5+ admite métodos de prueba async directamente (async throws func test...). Use try await dentro de las pruebas. Para código más antiguo, use XCTestExpectation: cree expectation, fulfill() en callback, wait(for:timeout:). Establezca timeouts razonables para detectar cuelgues. Para probar secuencias async, use bucles for await. Simule llamadas de red con URLProtocol o inyección de dependencias. Pruebe tanto rutas de éxito como de fallo. Las pruebas async se ejecutan concurrentemente por defecto—use el trait .serialized si el orden importa.
class APITests: XCTestCase {
func testFetchUser() async throws {
let user = try await fetchUser(id: 1)
XCTAssertEqual(user.name, "Alice")
}
func testFetchFailure() async {
do {
_ = try await fetchUser(id: -1)
XCTFail("Should have thrown")
} catch {
// Expected
}
}
// Old style with expectations (still works)
func testAsyncWithExpectation() {
let expectation = XCTestExpectation(description: "Fetch completes")
fetchUserAsync { result in
switch result {
case .success(let user):
XCTAssertEqual(user.name, "Alice")
case .failure:
XCTFail("Should succeed")
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
}Mocking y Stubbing
La inyección de dependencias habilita el testing: defina protocolos para dependencias, inyecte mocks en pruebas. MockNetworkService devuelve resultados predeterminados. Pruebe casos de éxito y fallo estableciendo mock.fetchUserResult. Este patrón desacopla las pruebas de la red, haciéndolas rápidas y deterministas. Para mocks complejos, use bibliotecas como Cuckoo o Mockingbird. Pruebe la API pública de sus tipos, no detalles de implementación. Los mocks deberían ser simples y enfocados.
// Protocol for dependency injection
protocol NetworkService {
func fetchUser(id: Int) async throws -> User
}
// Production implementation
class APIService: NetworkService {
func fetchUser(id: Int) async throws -> User {
// Real network call
}
}
// Mock for testing
class MockNetworkService: NetworkService {
var fetchUserResult: Result<User, Error>?
func fetchUser(id: Int) async throws -> User {
switch fetchUserResult {
case .success(let user): return user
case .failure(let error): throw error
case .none: throw URLError(.badURL)
}
}
}
class ViewModelTests: XCTestCase {
func testLoadUser() async throws {
let mock = MockNetworkService()
mock.fetchUserResult = .success(User(name: "Alice", age: 30))
let vm = ViewModel(service: mock)
await vm.load()
XCTAssertEqual(vm.userName, "Alice")
}
}Testing de UI
Las pruebas de UI automatizan interacciones de usuario vía identificadores de accesibilidad. XCUIApplication lanza la app. Encuentre elementos por identificador de accesibilidad (establecido en código con .accessibilityIdentifier). Acciones: tap, typeText, swipeLeft/Right. Aserciones: waitForExistence, exists, hittable. Las pruebas de UI son más lentas que las unitarias pero detectan bugs de integración. Establezca identificadores de accesibilidad en SwiftUI con .accessibilityIdentifier("Email"). Ejecute pruebas de UI en múltiples dispositivos/simuladores para cobertura.
import XCTest
class AppUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
let app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
let app = XCUIApplication()
let emailField = app.textFields["Email"]
let passwordField = app.secureTextFields["Password"]
let loginButton = app.buttons["Log In"]
emailField.tap()
emailField.typeText("[email protected]")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
// Verify navigation
XCTAssertTrue(app.staticTexts["Welcome, Alice!"].waitForExistence(timeout: 5))
}
func testSwipeAndTap() {
let cell = app.cells["Item 5"]
cell.swipeLeft()
app.buttons["Delete"].tap()
XCTAssertFalse(cell.exists)
}
}Rendimiento y Cobertura de Código
measure evalúa el rendimiento del código, comparando con líneas base. Establezca líneas base en Xcode; las pruebas fallan si la regresión excede el umbral. measureMetrics rastrea métricas específicas (memoria pico, CPU). Habilite la cobertura de código en el scheme para ver rutas de código no probadas. Apunte a 80%+ de cobertura en lógica de negocio. Snapshot testing (swift-snapshot-testing) captura UI para regresión visual. Perfile con Instruments (Time Profiler, Allocations) para análisis más profundo. Las pruebas de rendimiento detectan regresiones temprano.
class PerformanceTests: XCTestCase {
func testSortingPerformance() {
let data = (1...10000).shuffled()
measure {
_ = data.sorted()
}
}
func testMemoryUsage() {
measureMetrics([XCTPerformanceMetric.peakMemory]) {
_ = (0..<10000).map { _ in [Int](repeating: 0, count: 100) }
}
}
// Baseline comparison
func testWithBaselines() {
let options = XCTMeasureOptions()
options.defaultOptions.measurementUnits = .seconds
measure(options) {
// Code to benchmark
}
}
}
// Code coverage: enable in Edit Scheme > Test > Options
// View coverage: Cmd+Shift+Y (Report Navigator) > Coverage
// Snapshot testing (third-party)
// func testAppearance() {
// assertSnapshot(of: view, as: .image)
// }Concurrencia de Swift
async/await
async/await (Swift 5.5+) simplifica el código async. async marca funciones que pueden suspender. await marca puntos de suspensión. Task crea un contexto async. Mucho más limpio que los completion handlers. Los errores se propagan con try.
func fetchData() async throws -> String {
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? ""
}
// Call from async context
Task {
let result = try await fetchData()
print(result)
}Actors
actor (Swift 5.5+) es un tipo de referencia con exclusión mutua automática. Solo una tarea accede a su estado a la vez. Los métodos son implícitamente async cuando se llaman desde fuera. Reemplaza locks y dispatch queues para estado mutable compartido. Seguro para subprocesos por diseño.
actor Counter {
private var count = 0
func increment() {
count += 1
}
func getValue() -> Int {
count
}
}
// Usage
Task {
let counter = Counter()
await counter.increment()
let value = await counter.getValue()
}Async Sequence
AsyncSequence es el equivalente async de Sequence. for await itera asíncronamente. Útil para streaming de datos (red, líneas de archivo). Implemente AsyncIteratorProtocol para crear secuencias async personalizadas. El compilador maneja la suspensión y cancelación.
for try await item in asyncSequence {
print(item)
}
// Custom AsyncSequence
struct Counter: AsyncSequence {
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
mutating func next() async -> Int? {
current += 1
return current <= 5 ? current : nil
}
}
func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}Task Groups
Los task groups ejecutan múltiples tareas concurrentemente y recogen resultados. addTask añade una tarea hija. for await itera resultados a medida que completan. Todas las tareas deben completar antes de que el grupo retorne. Concurrencia estructurada: la cancelación se propaga a las hijas. Los resultados se recogen de forma segura.
let results = await withTaskGroup(of: Int.self) { group in
for i in 1...5 {
group.addTask { i * i }
}
var sum = 0
for await result in group {
sum += result
}
return sum
}Continuation
withCheckedContinuation tiende un puente de APIs de completion-handler a async/await. resume(returning:) reanuda la función async. Debe llamarse exactamente una vez. withCheckedThrowingContinuation admite errores. Útil para adoptar async/await con APIs existentes.
func fetchWithCompletion(_ completion: @escaping (String) -> Void) {
// Legacy completion handler API
completion("data")
}
// Wrap in async
func fetchAsync() async -> String {
await withCheckedContinuation { continuation in
fetchWithCompletion { result in
continuation.resume(returning: result)
}
}
}SwiftUI en Profundidad
Modificadores de Vista
Los modificadores envuelven vistas para cambiar apariencia o comportamiento. El orden importa: los modificadores posteriores envuelven a los anteriores. .padding antes de .background pone el padding dentro del background. Los modificadores devuelven nuevas instancias de View. Encadénelos para estilo complejo.
Text("Hello")
.font(.title)
.foregroundColor(.blue)
.padding()
.background(Color.gray)
.cornerRadius(8)
.shadow(radius: 4)Listas
List muestra filas desplazables. ForEach genera filas a partir de datos. onDelete habilita swipe-para-eliminar. Otros modificadores: onMove, onInsert. Los datos Identifiable (propiedad id) son requeridos para ForEach. Estilo de List: .plain, .insetGrouped, .sidebar.
List {
ForEach(items) { item in
HStack {
Text(item.name)
Spacer()
Text("\(item.price)")
}
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.listStyle(.insetGrouped)Navegación
NavigationStack gestiona una pila de vistas. NavigationLink empuja un destino. navigationTitle establece el título. toolbar añade botones de barra de navegación. Para iOS 16+. Use NavigationView para versiones más antiguas. Sheets y alerts usan modificadores .sheet y .alert.
NavigationStack {
List(items) { item in
NavigationLink(item.name) {
DetailView(item: item)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add") { addItem() }
}
}
}Gestión de Estado
@State para estado de vista local (tipos de valor). @Binding para estado pasado. @StateObject para objetos observables propios (creados una vez). @ObservedObject para objetos observables externos. @EnvironmentObject para estado de toda la app. Los cambios de @State disparan actualizaciones de vista.
struct CounterView: View {
@State private var count = 0
@Binding var selected: Int
@StateObject var viewModel = ViewModel()
var body: some View {
Button("\(count)") { count += 1 }
}
}Animaciones
withAnimation anima cambios de estado. .spring, .easeInOut son curvas de animación. .animation(value:) anima cuando el valor cambia. .scaleEffect, .opacity, .offset son animables. Las animaciones implícitas usan el modificador .animation. Las explícitas usan withAnimation.
struct AnimatedView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Button("Tap") {
withAnimation(.spring(response: 0.3)) {
scale = scale == 1 ? 1.5 : 1
}
}
.scaleEffect(scale)
.animation(.easeInOut(duration: 0.3), value: scale)
}
}Framework Combine
Publishers y Subscribers
Combine es el framework reactivo de Apple. Los Publishers emiten valores a lo largo del tiempo. Los Subscribers los reciben. sink crea un subscriber. Just emite un solo valor. PassthroughSubject es un publisher manual. Los Cancellables deben retenerse o la suscripción se cancela. Similar a RxSwift.
import Combine
let publisher = Just(42)
let cancellable = publisher.sink { value in
print("Received: \(value)")
}
// Just emits one value then finishes
// PassthroughSubject: manual values
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)Operadores
Los operadores transforman la salida del publisher. map transforma valores. filter selecciona valores. debounce retrasa hasta quietud. throttle limita la tasa. combineLatest fusiona streams. flatMap encadena publishers. Los operadores son perezosos: nada pasa hasta que sink se suscribe.
let publisher = (1...10).publisher
.map { $0 * 2 }
.filter { $0 > 10 }
.sink { print($0) } // 12, 14, 16, 18, 20
// Debounce
let debounced = subject
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { print($0) }@Published
@Published expone una propiedad como un publisher de Combine. El prefijo $ accede al publisher. Los cambios emiten nuevos valores. Funciona con ObservableObject para SwiftUI. El publisher emite el valor actual en la suscripción. Útil para actualizaciones reactivas de UI sin notificaciones manuales.
class ViewModel: ObservableObject {
@Published var count = 0
}
let vm = ViewModel()
let cancellable = vm.$count.sink { newCount in
print("Count changed: \(newCount)")
}
vm.count = 1 // Prints "Count changed: 1"
vm.count = 2 // Prints "Count changed: 2"Future
Future envuelve una API de completion-handler como un publisher. promise(.success) emite un valor. promise(.failure) emite un error. Future emite exactamente una vez. Útil para tender un puente de APIs de callback a Combine. El closure se ejecuta eagerly a menos que se envuelva con .delay o deferred.
func fetchUser() -> Future<User, Error> {
Future { promise in
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { promise(.failure(error)) }
else { promise(.success(parse(data!))) }
}.resume()
}
}
// Usage
fetchUser().sink(receiveCompletion: { _ in }, receiveValue: { user in print(user) })Manejo de Errores
Fail emite un error inmediatamente. catch reemplaza un publisher fallido con otro. retry re-suscribe en fallo. assertNoFailure causa crash en error (para depuración). El evento de completion señala que no habrá más valores. Los errores se propagan hacia abajo a menos que se capturen.
let publisher = Fail<Int, MyError>(error: .notFound)
.catch { error in
Just(0) // Fallback value
}
.sink { print($0) } // 0
// retry on failure
let retried = requestPublisher
.retry(3)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })Gestión de Memoria
Fundamentos de ARC
ARC (Automatic Reference Counting) rastrea referencias. Cuando el conteo llega a cero, deinit se ejecuta y la memoria se libera. Las referencias strong incrementan el conteo. Los ciclos de referencia impiden la desasignación. ARC es determinístico (a diferencia del GC). deinit se ejecuta sincrónicamente cuando se suelta la última referencia.
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) is being deinitialized") }
}
var p1: Person? = Person(name: "Alice")
p1 = nil // Prints "Alice is being deinitialized"Referencias Weak
Las referencias weak no incrementan el conteo de retención. Se auto-establecen a nil cuando el objeto se desasigna. Deben ser optional var. Use para delegates y observadores para romper ciclos. Las referencias weak son zeroing: seguras de acceder después de que el objeto se ha ido. La solución más común para ciclos de retención.
class View {
weak var delegate: Delegate?
}
class Delegate {
var view: View? // strong
}
// weak: does not keep object alive, auto-set to nil
// Use for delegate patterns to avoid cyclesReferencias Unowned
Las referencias unowned no incrementan el conteo de retención. A diferencia de weak, son non-optional y no zeroing. Acceder después de dealloc causa crash. Use cuando el objeto referenciado sobrevive o muere con el referenciador. Más rápido que weak (sin verificación de nil). Común en closures que capturan self cuando self sobrevive al closure.
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer
init(customer: Customer) { self.customer = customer }
}
// unowned: does not keep alive, but not optional
// CRASHES if accessed after dealloc
// Use when the other object has same or shorter lifetimeCiclos de Retención
Los ciclos de retención ocurren cuando los objetos se referencian fuertemente mutuamente. Ninguno se desasigna, fugando memoria. Solucione con weak o unowned. Común en delegates, observadores y closures. Los closures capturan self fuertemente por defecto. [weak self] o [unowned self] rompe el ciclo. Use Memory Graph Debugger para encontrar ciclos.
// BAD: retain cycle
class Node {
var next: Node?
var prev: Node? // strong -> cycle
}
// GOOD: break with weak
class Node {
var next: Node?
weak var prev: Node?
}
// Closures: use [weak self]
{ [weak self] in self?.update() }Closures y Captura
Los closures capturan variables por referencia. Los closures almacenados (escaping) crean ciclos de retención si capturan self. [weak self] hace la captura optional y weak. @escaping marca closures que sobreviven a la llamada de función. Los closures non-escaping (predeterminados) no pueden causar ciclos. El compilador advierte sobre ciclos potenciales.
class Timer {
var handler: (() -> Void)?
func start() {
handler = { [weak self] in
self?.tick() // weak to avoid cycle
}
}
func tick() { /* ... */ }
}
// Escaping closure: stored for later
// @escaping marks escaping closuresManejo de Errores en Profundidad
Errores Personalizados
Los errores personalizados conforman a Error. LocalizedError proporciona errorDescription. Los valores asociados llevan contexto. Los enums son la forma idiomática de definir errores. Cada caso representa un fallo distinto. La exhaustividad del switch asegura que todos los errores se manejen. Conforme a CustomStringConvertible para salida de depuración.
enum APIError: Error, LocalizedError {
case invalidURL
case unauthorized
case serverError(Int)
case decodingFailed(Error)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .unauthorized: return "Unauthorized"
case .serverError(let code): return "Server error: \(code)"
case .decodingFailed(let err): return "Decode failed: \(err)"
}
}
}Tipo Result
Result es un enum tipado de éxito/fallo. Útil para operaciones síncronas que pueden fallar. map transforma el valor de éxito. flatMap encadena operaciones. get() lanza para convertir a try/catch. Result se prefiere sobre funciones que lanzan para resultados almacenados o pasados. Combina manejo de errores con semántica de valor.
func fetchUser(id: Int) -> Result<User, APIError> {
guard id > 0 else { return .failure(.invalidURL) }
return .success(User(name: "Alice"))
}
// Usage
switch fetchUser(id: 1) {
case .success(let user): print(user)
case .failure(let error): print(error)
}
// map and flatMap
let result = fetchUser(id: 1).map { $0.name }Rethrows
rethrows propaga errores de un parámetro de closure. Si el closure no lanza, la función tampoco lanza. Esto evita forzar try en llamadores que pasan closures que no lanzan. Usado por map, filter y otras funciones de orden superior. La función misma no puede lanzar independientemente del closure.
func withLock<T>(_ lock: NSLock, _ body: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try body()
}
// Only throws if body throws
let x = withLock(lock) { 5 } // no try needed
let y = try withLock(lock) { try throwingFunc() }Propagación de Errores
try propaga errores al llamador. try? convierte errores a nil (devuelve Optional). try! causa crash en error (use cuando esté seguro). Los errores suben por la pila de llamadas hasta ser capturados. Las funciones que lanzan deben marcarse con throws. El compilador exige marcadores try, previniendo errores no manejados. defer se ejecuta sin importar los errores lanzados.
func process() throws {
let data = try fetchData() // may throw
let user = try parse(data) // may throw
save(user) // does not throw
}
// try?: converts to optional, nil on error
let user = try? fetchUser()
// try!: asserts no error, crashes if thrown
let user = try! fetchUser()Limpieza con defer
defer programa limpieza para ejecutarse cuando el ámbito sale. Se ejecuta sin importar cómo sale el ámbito (return, throw o fall-through). Múltiples defers se ejecutan en orden LIFO. Útil para cerrar archivos, liberar locks, liberar recursos. defer no puede lanzar ni break/continue. El closure captura variables por referencia.
func processFile() throws {
let file = open("file.txt")
defer { close(file) } // runs on scope exit
let data = try read(file)
// ... process ...
// close() runs here even if read throws
}
// Multiple defers run in reverse order (LIFO)
defer { print("1") }
defer { print("2") } // prints 2 then 1Fragmentos de Swift relacionados
Copy-paste ready code for common tasks.
Optionals
Use optional binding, nil-coalescing, chaining, and guard for safe unwrapping.
Closures
Define closure expressions, capture state, and pass escaping callbacks.
Protocols
Define contracts, conform with structs, and add default behavior via extensions.
Generics
Write type-parameterized functions and types with protocol constraints.
Structs and Classes
Compare value-type structs with reference-type classes and inheritance.
Error Handling
Throw and catch typed errors with do-catch, try?, try!, and rethrows.
Concurrency (async/await)
Run async functions, parallelize with async let, and fan out with task groups.
String Manipulation
Trim, split, join, replace, and index strings using Swift's Unicode API.
Was this helpful?