Rendimiento SwiftUI: Optimización de LazyVStack y Listas Complejas
Técnicas de optimización para LazyVStack y listas SwiftUI. Reducir el consumo de memoria, mejorar el rendimiento del scroll y evitar errores comunes.

Las listas representan uno de los componentes más utilizados en las aplicaciones iOS. LazyVStack y List de SwiftUI ofrecen soluciones eficientes para mostrar colecciones de datos, pero un uso incorrecto puede degradar rápidamente la experiencia del usuario. Comprender el funcionamiento interno de estos componentes ayuda a evitar errores frecuentes y a construir interfaces fluidas.
Este artículo presenta las técnicas esenciales de optimización para listas SwiftUI: lazy loading, reciclaje de vistas, gestión de identificadores y patrones avanzados para grandes conjuntos de datos. Actualizado para iOS 27 con las nuevas APIs de reordenamiento y acciones de deslizamiento.
Comprender el Lazy Loading en SwiftUI
El lazy loading instancia las vistas únicamente cuando se vuelven visibles en la pantalla. A diferencia de VStack que crea todas las vistas hijas de inmediato, LazyVStack retrasa esta creación, reduciendo drásticamente el consumo de memoria y el tiempo de renderizado inicial.
import SwiftUI
// ❌ Problem: VStack instantiates all 10,000 views immediately
struct NonLazyListView: View {
let items = (1...10000).map { "Item (\$0)" }
var body: some View {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
// Each view is created at launch
ExpensiveRowView(title: item)
}
}
}
}
}
// ✅ Solution: LazyVStack creates views on demand
struct LazyListView: View {
let items = (1...10000).map { "Item (\$0)" }
var body: some View {
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
// Only visible views are created
ExpensiveRowView(title: item)
}
}
}
}
}La diferencia de rendimiento se vuelve significativa con apenas unos cientos de elementos. Con 10.000 elementos, VStack puede tardar varios segundos en arrancar mientras que LazyVStack permanece instantáneo.
Medir el Impacto del Lazy Loading
Instruments permite medir con precisión el uso de memoria y CPU. Aquí una vista de prueba que ilustra la diferencia:
struct ExpensiveRowView: View {
let title: String
// Simulating expensive initialization
init(title: String) {
self.title = title
// Log to visualize when the view is created
print("Creating row: (title)")
}
var body: some View {
HStack {
// Image with processing
Circle()
.fill(
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text(title)
.font(.headline)
Text("Subtitle with computation")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding()
}
}Al ejecutar con VStack, los 10.000 logs aparecen de inmediato. Con LazyVStack, solo los elementos visibles (aproximadamente 15-20 según el tamaño de pantalla) se registran al inicio, y aparecen más al hacer scroll.
LazyVStack mantiene en memoria las vistas creadas después de que aparezcan. A diferencia de List que recicla activamente las celdas, las vistas en un LazyVStack persisten hasta que el componente padre se destruye.
La Importancia de los Identificadores Estables
Los identificadores constituyen el mecanismo central de las actualizaciones de listas SwiftUI. Un identificador inestable provoca recreaciones innecesarias de vistas y puede generar bugs visuales como animaciones incorrectas o pérdida de la posición del scroll.
// ❌ Problem: using index as identifier
struct UnstableIdentifierView: View {
@State private var items = ["A", "B", "C", "D"]
var body: some View {
List {
// Index changes if an element is deleted
ForEach(items.indices, id: \.self) { index in
Text(items[index])
}
}
}
}
// ❌ Problem: using UUID() in ForEach
struct RegeneratedIdentifierView: View {
let items = ["A", "B", "C", "D"]
var body: some View {
List {
// UUID() generates a new ID on each render
ForEach(items, id: \.self) { item in
// Subtle issue if items contain duplicates
Text(item)
}
}
}
}
// ✅ Solution: model with stable identifier
struct Item: Identifiable {
let id: UUID // Created once
var name: String
init(name: String) {
self.id = UUID()
self.name = name
}
}
struct StableIdentifierView: View {
@State private var items = [
Item(name: "A"),
Item(name: "B"),
Item(name: "C"),
Item(name: "D")
]
var body: some View {
List {
// id is stable for the item's lifetime
ForEach(items) { item in
Text(item.name)
}
}
}
}Usar un identificador único y persistente garantiza que SwiftUI pueda diferenciar correctamente los elementos durante actualizaciones, animaciones y comparaciones.
Optimización de Celdas con Equatable
SwiftUI compara las vistas para determinar si es necesario un nuevo renderizado. Por defecto, esta comparación utiliza reflection, lo que puede resultar costoso. Implementar Equatable permite una comparación optimizada y explícita.
// Data model
struct Contact: Identifiable, Equatable {
let id: UUID
var name: String
var email: String
var avatarURL: URL?
var lastActivity: Date
// Custom comparison: ignore lastActivity
// if other properties are identical
static func == (lhs: Contact, rhs: Contact) -> Bool {
lhs.id == rhs.id &&
lhs.name == rhs.name &&
lhs.email == rhs.email &&
lhs.avatarURL == rhs.avatarURL
// lastActivity intentionally excluded
}
}
// Optimized cell view
struct ContactRow: View, Equatable {
let contact: Contact
// Explicit comparison to avoid unnecessary re-renders
static func == (lhs: ContactRow, rhs: ContactRow) -> Bool {
lhs.contact == rhs.contact
}
var body: some View {
HStack(spacing: 12) {
// Async avatar
AsyncImage(url: contact.avatarURL) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure:
Image(systemName: "person.circle.fill")
.foregroundStyle(.gray)
default:
ProgressView()
}
}
.frame(width: 44, height: 44)
.clipShape(Circle())
// Contact information
VStack(alignment: .leading, spacing: 2) {
Text(contact.name)
.font(.body.weight(.medium))
Text(contact.email)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.vertical, 4)
}
}
// List using EquatableView
struct ContactListView: View {
let contacts: [Contact]
var body: some View {
List {
ForEach(contacts) { contact in
// EquatableView prevents re-renders if contact unchanged
EquatableView(content: ContactRow(contact: contact))
}
}
}
}Esta optimización reduce significativamente la carga de CPU durante el scroll rápido, especialmente con celdas que contienen cálculos o imágenes.
¿Listo para aprobar tus entrevistas de iOS?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Gestión de la Carga Asíncrona de Imágenes
Las imágenes suelen convertirse en el cuello de botella de rendimiento en las listas. iOS 27 introdujo caché HTTP estándar para AsyncImage por defecto, eliminando la necesidad de caché manual en muchos escenarios. Para aplicaciones que apuntan a iOS 26 o versiones anteriores, o que requieren control detallado sobre el comportamiento del caché, una implementación personalizada sigue siendo valiosa.
import SwiftUI
// Singleton image cache (for iOS 26 or custom caching needs)
actor ImageCache {
static let shared = ImageCache()
private var cache = NSCache<NSString, UIImage>()
private init() {
// Memory limit: 50 MB
cache.totalCostLimit = 50 * 1024 * 1024
}
func image(for url: URL) -> UIImage? {
cache.object(forKey: url.absoluteString as NSString)
}
func setImage(_ image: UIImage, for url: URL) {
// Cost estimation: image bytes
let cost = Int(image.size.width * image.size.height * 4)
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
}
}
// Optimized image view with caching
struct CachedAsyncImage: View {
let url: URL?
let size: CGSize
@State private var image: UIImage?
@State private var isLoading = false
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else if isLoading {
Rectangle()
.fill(Color.gray.opacity(0.2))
.overlay(ProgressView())
} else {
Rectangle()
.fill(Color.gray.opacity(0.2))
}
}
.frame(width: size.width, height: size.height)
.clipped()
.task(id: url) {
await loadImage()
}
}
private func loadImage() async {
guard let url else { return }
// Check cache
if let cached = await ImageCache.shared.image(for: url) {
self.image = cached
return
}
isLoading = true
defer { isLoading = false }
// Download and resize
do {
let (data, _) = try await URLSession.shared.data(from: url)
// Resize to save memory
if let original = UIImage(data: data),
let resized = await resizeImage(original, to: size) {
await ImageCache.shared.setImage(resized, for: url)
self.image = resized
}
} catch {
// Handle error silently
}
}
private func resizeImage(_ image: UIImage, to size: CGSize) async -> UIImage? {
// Use screen scale
let scale = await UIScreen.main.scale
let targetSize = CGSize(
width: size.width * scale,
height: size.height * scale
)
return await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
let renderer = UIGraphicsImageRenderer(size: targetSize)
let resized = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
}
continuation.resume(returning: resized)
}
}
}
}Para iOS 27 y versiones posteriores, el AsyncImage estándar respeta los headers de caché HTTP automáticamente. Se puede aplicar un URLCache personalizado mediante el modificador asyncImageURLSession(_:) para requisitos específicos.
Prefetching Inteligente para Anticipar
Para listas muy largas, el prefetching carga imágenes antes de que se vuelvan visibles:
// Prefetching coordinator
@Observable
final class ImagePrefetcher {
private var prefetchTasks: [URL: Task<Void, Never>] = [:]
private let prefetchDistance = 10 // Number of items ahead
func prefetchImages(for items: [Contact], visibleRange: Range<Int>) {
// Calculate prefetch range
let prefetchStart = max(0, visibleRange.lowerBound - prefetchDistance)
let prefetchEnd = min(items.count, visibleRange.upperBound + prefetchDistance)
// Launch prefetch for items in range
for index in prefetchStart..<prefetchEnd {
guard let url = items[index].avatarURL else { continue }
// Avoid duplicates
guard prefetchTasks[url] == nil else { continue }
prefetchTasks[url] = Task {
// Check if already cached
if await ImageCache.shared.image(for: url) != nil {
return
}
// Prefetch
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
await ImageCache.shared.setImage(image, for: url)
}
} catch {
// Ignore prefetch errors
}
}
}
// Cancel out-of-range prefetches
cancelOutOfRangePrefetches(validRange: prefetchStart..<prefetchEnd, items: items)
}
private func cancelOutOfRangePrefetches(validRange: Range<Int>, items: [Contact]) {
let validURLs = Set(
items[validRange].compactMap { $0.avatarURL }
)
for (url, task) in prefetchTasks {
if !validURLs.contains(url) {
task.cancel()
prefetchTasks.removeValue(forKey: url)
}
}
}
}Elegir entre List y LazyVStack
iOS 27 redujo significativamente la brecha entre List y LazyVStack. Anteriormente, List era la única opción para acciones de deslizamiento nativas y arrastrar para reordenar. Las nuevas APIs .swipeActionsContainer() y .reorderable() llevan estas capacidades a cualquier contenedor.
// ✅ List: automatic cell recycling, built-in styles
// Best for: standard contacts, settings, data tables
struct ContactsWithSwipeActions: View {
@State private var contacts: [Contact] = []
var body: some View {
List {
ForEach(contacts) { contact in
ContactRow(contact: contact)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteContact(contact)
} label: {
Label("Delete", systemImage: "trash")
}
}
.swipeActions(edge: .leading) {
Button {
favoriteContact(contact)
} label: {
Label("Favorite", systemImage: "star")
}
.tint(.yellow)
}
}
}
.listStyle(.plain)
}
private func deleteContact(_ contact: Contact) {
contacts.removeAll { $0.id == contact.id }
}
private func favoriteContact(_ contact: Contact) {
// Favorite logic
}
}
// ✅ LazyVStack with iOS 27 swipe actions
// Best for: custom layouts, cards, feeds
struct CustomFeedWithSwipeActions: View {
@State private var posts: [Post] = []
var body: some View {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(posts) { post in
PostCard(post: post)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deletePost(post)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
.padding(.horizontal)
.swipeActionsContainer()
}
}
private func deletePost(_ post: Post) {
posts.removeAll { $0.id == post.id }
}
}La elección ahora depende de los requisitos visuales más que de la disponibilidad de funcionalidades:
| Requisito | Contenedor recomendado |
|---|---|
| Reciclaje de celdas para eficiencia de memoria | List |
| Espaciado y padding personalizados | LazyVStack |
| Separadores e insets nativos | List |
| Layouts tipo tarjeta o no estándar | LazyVStack |
| Headers de sección con comportamiento fijo | Ambos (usar pinnedViews) |
| Acciones de deslizamiento | Ambos (iOS 27+) |
| Arrastrar para reordenar | Ambos (iOS 27+) |
List recicla activamente las celdas, lo que puede causar problemas con el estado local (@State). Los valores @State en celdas de List pueden reutilizarse de forma inesperada. Conviene almacenar el estado en el modelo de datos o en un ViewModel.
Reordenar Elementos en LazyVStack (iOS 27)
Antes de iOS 27, arrastrar para reordenar requería List con onMove(perform:). Las nuevas APIs reorderable funcionan con cualquier contenedor, incluyendo LazyVStack y LazyVGrid.
import SwiftUI
struct ReorderableCardList: View {
@State private var cards: [Card] = Card.sampleData
var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(cards) { card in
CardView(card: card)
}
.reorderable() // Mark content as draggable
}
.padding()
.reorderContainer(for: Card.self) { difference in
// Apply the reorder to the model
cards.apply(difference: difference)
}
}
}
}
struct Card: Identifiable {
let id: UUID
var title: String
var color: Color
static var sampleData: [Card] {
[
Card(id: UUID(), title: "Design Review", color: .blue),
Card(id: UUID(), title: "Code Sprint", color: .green),
Card(id: UUID(), title: "Testing Phase", color: .orange)
]
}
}
struct CardView: View {
let card: Card
var body: some View {
Text(card.title)
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(card.color.opacity(0.2))
.cornerRadius(12)
}
}
// Extension to apply ReorderDifference
extension Array where Element: Identifiable {
mutating func apply(difference: ReorderDifference<Element.ID>) {
// Move items from sources to destination
let movedItems = difference.sources.compactMap { id in
self.first { $0.id == id }
}
// Remove from original positions
self.removeAll { item in
difference.sources.contains(item.id)
}
// Insert at destination
if let destinationIndex = self.firstIndex(where: { $0.id == difference.destination }) {
self.insert(contentsOf: movedItems, at: destinationIndex)
} else {
self.append(contentsOf: movedItems)
}
}
}SwiftUI maneja la vista previa del arrastre, el marcador de inserción y la animación de soltar. El objeto ReorderDifference proporciona los IDs de origen y el destino, dejando las actualizaciones del modelo al código de la aplicación.
Secciones y Encabezados Optimizados
Organizar el contenido en secciones mejora la legibilidad pero puede impactar el rendimiento si se implementa mal. Los encabezados fijados y la gestión de secciones requieren atención particular.
// Grouped data model
struct GroupedContacts {
let letter: String
let contacts: [Contact]
}
// View with optimized sections
struct SectionedContactList: View {
let groupedContacts: [GroupedContacts]
var body: some View {
ScrollView {
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
ForEach(groupedContacts, id: \.letter) { group in
Section {
// Section content
ForEach(group.contacts) { contact in
ContactRow(contact: contact)
.padding(.horizontal)
.padding(.vertical, 8)
// Custom separator
if contact.id != group.contacts.last?.id {
Divider()
.padding(.leading, 68)
}
}
} header: {
// Optimized pinned header
SectionHeader(title: group.letter)
}
}
}
}
}
}
// Lightweight header for performance
struct SectionHeader: View {
let title: String
var body: some View {
Text(title)
.font(.headline)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.padding(.vertical, 8)
.background(.ultraThinMaterial)
}
}
// Optimized grouping function
extension Array where Element == Contact {
func groupedByFirstLetter() -> [GroupedContacts] {
// Dictionary for O(n) grouping
var groups: [String: [Contact]] = [:]
for contact in self {
let letter = String(contact.name.prefix(1)).uppercased()
groups[letter, default: []].append(contact)
}
// Sort groups alphabetically
return groups
.map { GroupedContacts(letter: $0.key, contacts: $0.value) }
.sorted { $0.letter < $1.letter }
}
}Los encabezados fijados (pinnedViews: [.sectionHeaders]) permanecen visibles durante el scroll, mejorando la navegación en listas largas.
Paginación y Scroll Infinito
Para grandes conjuntos de datos, la paginación evita cargar todos los datos en memoria. La implementación debe ser transparente para el usuario.
// ViewModel handling pagination
@Observable
final class PaginatedListViewModel {
private(set) var items: [Contact] = []
private(set) var isLoading = false
private(set) var hasMorePages = true
private var currentPage = 0
private let pageSize = 20
private let dataService: ContactDataService
init(dataService: ContactDataService) {
self.dataService = dataService
}
func loadInitialData() async {
guard items.isEmpty else { return }
await loadNextPage()
}
func loadMoreIfNeeded(currentItem: Contact) async {
// Trigger loading when approaching the end
guard let index = items.firstIndex(where: { $0.id == currentItem.id }) else {
return
}
// Load 5 items before the end
let thresholdIndex = items.count - 5
if index >= thresholdIndex {
await loadNextPage()
}
}
private func loadNextPage() async {
guard !isLoading, hasMorePages else { return }
isLoading = true
defer { isLoading = false }
do {
let newItems = try await dataService.fetchContacts(
page: currentPage,
limit: pageSize
)
items.append(contentsOf: newItems)
currentPage += 1
hasMorePages = newItems.count == pageSize
} catch {
// Handle error
}
}
}
// View with infinite scroll
struct InfiniteContactList: View {
@State private var viewModel: PaginatedListViewModel
init(dataService: ContactDataService) {
_viewModel = State(initialValue: PaginatedListViewModel(dataService: dataService))
}
var body: some View {
List {
ForEach(viewModel.items) { contact in
ContactRow(contact: contact)
.task {
// Check if more loading needed
await viewModel.loadMoreIfNeeded(currentItem: contact)
}
}
// Loading indicator at end of list
if viewModel.isLoading {
HStack {
Spacer()
ProgressView()
Spacer()
}
.padding()
}
}
.task {
await viewModel.loadInitialData()
}
}
}
// Protocol for data service
protocol ContactDataService {
func fetchContacts(page: Int, limit: Int) async throws -> [Contact]
}Este patrón garantiza una carga fluida sin bloquear la interfaz y permite una gestión eficiente de la memoria.
¿Listo para aprobar tus entrevistas de iOS?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Profiling con Instruments
Identificar problemas de rendimiento exige herramientas de medición precisas. Instruments ofrece varias plantillas adaptadas a SwiftUI.
// Measurement points for debugging
struct PerformanceMonitor {
// Measure view creation time
static func measureViewCreation<T: View>(
_ name: String,
@ViewBuilder content: () -> T
) -> T {
let start = CFAbsoluteTimeGetCurrent()
let view = content()
let elapsed = CFAbsoluteTimeGetCurrent() - start
#if DEBUG
if elapsed > 0.016 { // More than 16ms = frame drop
print("[(name)] View creation took (elapsed * 1000)ms")
}
#endif
return view
}
}
// Extension to trace renders
extension View {
func debugRender(_ label: String) -> some View {
#if DEBUG
let _ = Self._printChanges()
print("Rendering: (label)")
#endif
return self
}
func measureRender(_ label: String) -> some View {
modifier(RenderMeasureModifier(label: label))
}
}
struct RenderMeasureModifier: ViewModifier {
let label: String
@State private var renderCount = 0
func body(content: Content) -> some View {
content
.onAppear {
renderCount += 1
#if DEBUG
print("[(label)] Render count: (renderCount)")
#endif
}
}
}Checklist de Optimización con Instruments
Para un profiling efectivo de listas SwiftUI:
- Time Profiler: identificar las funciones que más CPU consumen
- Allocations: verificar el crecimiento de memoria durante el scroll
- SwiftUI Instrument: visualizar las evaluaciones de body
- Core Animation: detectar caídas de frames
// Instrumented view for profiling
struct ProfiledContactList: View {
let contacts: [Contact]
var body: some View {
let _ = Self._printChanges() // Shows changes triggering re-render
List {
ForEach(contacts) { contact in
ContactRow(contact: contact)
.measureRender("ContactRow-(contact.id)")
}
}
}
}Esta API de debugging de SwiftUI imprime en consola qué propiedades cambiaron y dispararon una reevaluación del body. Esencial para identificar renderizados innecesarios.
Optimizaciones Avanzadas con drawingGroup
Para vistas complejas con muchos efectos visuales, drawingGroup() puede mejorar significativamente el rendimiento al rasterizar la vista en una capa Metal.
// Cell with complex visual effects
struct ComplexVisualRow: View {
let item: Item
var body: some View {
HStack(spacing: 16) {
// Circle with gradient and shadow
Circle()
.fill(
RadialGradient(
colors: [.blue, .purple, .pink],
center: .center,
startRadius: 0,
endRadius: 25
)
)
.frame(width: 50, height: 50)
.shadow(color: .purple.opacity(0.5), radius: 8, y: 4)
VStack(alignment: .leading, spacing: 4) {
Text(item.name)
.font(.headline)
// Progress bar with gradient
GeometryReader { geometry in
Capsule()
.fill(Color.gray.opacity(0.2))
.overlay(alignment: .leading) {
Capsule()
.fill(
LinearGradient(
colors: [.green, .yellow, .orange],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: geometry.size.width * item.progress)
}
}
.frame(height: 8)
}
}
.padding()
// Rasterization for performance
.drawingGroup()
}
}
// List using optimized cells
struct OptimizedComplexList: View {
let items: [Item]
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items) { item in
ComplexVisualRow(item: item)
}
}
.padding()
}
}
}
struct Item: Identifiable {
let id: UUID
let name: String
let progress: Double
}drawingGroup() resulta especialmente eficaz para vistas que combinan gradientes, sombras y efectos blur.
Sources
- WWDC26 SwiftUI Guide - Documentación oficial de Apple sobre las actualizaciones de SwiftUI en iOS 27 incluyendo caché de AsyncImage y APIs reorderable
- New SwiftUI APIs for Reordering and Drag and Drop on iOS 27 - Explicación detallada de los modificadores
.reorderable()y.reorderContainer(for:) - What's New in SwiftUI for iOS 27 - Resumen completo de los cambios de SwiftUI en WWDC26
Conclusión
La optimización de listas SwiftUI se basa en una comprensión profunda de los mecanismos de lazy loading, reciclaje y comparación de vistas. iOS 27 trajo mejoras significativas con caché nativo de AsyncImage y APIs de acciones de deslizamiento y reordenamiento independientes del contenedor.
Checklist de Rendimiento SwiftUI
- Usar
LazyVStackoListen lugar deVStackpara colecciones - Implementar
Identifiablecon IDs estables y únicos - Adoptar
Equatablepara celdas complejas - Aprovechar el caché nativo de AsyncImage en iOS 27, o implementar caché personalizado para versiones anteriores
- Precargar datos con prefetching inteligente
- Elegir
Listpara reciclaje de celdas oLazyVStackpara layouts personalizados (ambos ahora soportan acciones de deslizamiento y reordenamiento) - Usar
pinnedViewspara encabezados de sección - Implementar paginación para grandes conjuntos de datos
- Hacer profiling regular con Instruments
- Aplicar
drawingGroup()a vistas con efectos visuales complejos
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
¿Sabrías detectar el bug en iOS?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador de SharpSkill
Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.
Actualizado el 19 de agosto de 2026
Etiquetas
Compartir
Artículos relacionados

ViewModifiers personalizados en SwiftUI: patrones reutilizables para Design Systems
Construye ViewModifiers personalizados en SwiftUI para un design system coherente. Patrones, mejores prácticas y ejemplos prácticos para estilizar vistas iOS de forma eficiente.

SwiftUI @Observable vs @State: Cuándo Usar Cada Uno en 2026
Domina las diferencias entre @Observable y @State en SwiftUI para elegir la herramienta de gestión de estado adecuada en aplicaciones iOS.

CloudKit con SwiftUI en 2026: patrones de sincronización entre dispositivos
Guía completa para implementar la sincronización CloudKit con SwiftUI: CKSyncEngine, integración con SwiftData, resolución de conflictos y mejores prácticas para iOS 2026.