Performance SwiftUI: Otimização de LazyVStack e Listas Complexas
Técnicas de otimização para LazyVStack e listas SwiftUI. Reduzir o consumo de memória, melhorar a performance de scroll e evitar armadilhas comuns.

As listas representam um dos componentes mais utilizados em aplicativos iOS. LazyVStack e List do SwiftUI oferecem soluções performáticas para exibir coleções de dados, mas o uso incorreto pode degradar rapidamente a experiência do usuário. Compreender o funcionamento interno desses componentes ajuda a evitar armadilhas frequentes e construir interfaces fluidas.
Este artigo apresenta as técnicas essenciais de otimização para listas SwiftUI: lazy loading, reciclagem de views, gestão de identificadores e padrões avançados para grandes conjuntos de dados. Atualizado para iOS 27 com as novas APIs de reordenamento e ações de deslizamento.
Compreender o Lazy Loading no SwiftUI
O lazy loading instancia as views apenas quando elas se tornam visíveis na tela. Diferente do VStack que cria todas as views filhas imediatamente, o LazyVStack adia essa criação, reduzindo drasticamente o consumo de memória e o tempo de renderização 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)
}
}
}
}
}A diferença de performance se torna significativa com apenas algumas centenas de elementos. Com 10.000 itens, o VStack pode levar vários segundos para iniciar enquanto o LazyVStack permanece instantâneo.
Medir o Impacto do Lazy Loading
O Instruments permite medir com precisão o uso de memória e CPU. Aqui está uma view de teste que ilustra a diferença:
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()
}
}Ao executar com VStack, todos os 10.000 logs aparecem imediatamente. Com LazyVStack, apenas os elementos visíveis (cerca de 15-20 dependendo do tamanho da tela) são registrados inicialmente, com mais aparecendo conforme o scroll.
O LazyVStack mantém as views criadas em memória após elas aparecerem. Diferente do List que recicla ativamente as células, as views em um LazyVStack persistem até que o componente pai seja destruído.
A Importância dos Identificadores Estáveis
Os identificadores formam o mecanismo central das atualizações de listas SwiftUI. Um identificador instável causa recriações desnecessárias de views e pode gerar bugs visuais como animações incorretas ou perda da posição do 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 um identificador único e persistente garante que o SwiftUI possa diferenciar corretamente os elementos durante atualizações, animações e comparações.
Otimização de Células com Equatable
O SwiftUI compara as views para determinar se uma nova renderização é necessária. Por padrão, essa comparação utiliza reflection, o que pode ser custoso. Implementar Equatable permite uma comparação otimizada e 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))
}
}
}
}Essa otimização reduz significativamente a carga de CPU durante o scroll rápido, particularmente com células que contêm cálculos ou imagens.
Pronto para mandar bem nas entrevistas de iOS?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Gestão do Carregamento Assíncrono de Imagens
As imagens frequentemente se tornam o gargalo de performance em listas. O iOS 27 introduziu cache HTTP padrão para AsyncImage por padrão, eliminando a necessidade de cache manual em muitos cenários. Para aplicativos que visam iOS 26 ou versões anteriores, ou que requerem controle detalhado sobre o comportamento do cache, uma implementação personalizada continua sendo 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 e versões posteriores, o AsyncImage padrão respeita os headers de cache HTTP automaticamente. Um URLCache personalizado pode ser aplicado via modificador asyncImageURLSession(_:) para requisitos específicos.
Prefetching Inteligente para Antecipação
Para listas muito longas, o prefetching carrega imagens antes que elas se tornem visíveis:
// 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)
}
}
}
}Escolher entre List e LazyVStack
O iOS 27 reduziu significativamente a lacuna entre List e LazyVStack. Anteriormente, List era a única opção para ações de deslizamento nativas e arrastar para reordenar. As novas APIs .swipeActionsContainer() e .reorderable() trazem essas capacidades para qualquer contêiner.
// ✅ 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 }
}
}A escolha agora depende dos requisitos visuais mais do que da disponibilidade de funcionalidades:
| Requisito | Contêiner recomendado |
|---|---|
| Reciclagem de células para eficiência de memória | List |
| Espaçamento e padding personalizados | LazyVStack |
| Separadores e insets nativos | List |
| Layouts tipo cartão ou não padrão | LazyVStack |
| Headers de seção com comportamento fixo | Ambos (usar pinnedViews) |
| Ações de deslizamento | Ambos (iOS 27+) |
| Arrastar para reordenar | Ambos (iOS 27+) |
O List recicla ativamente as células, o que pode causar problemas com estado local (@State). Os valores @State em células de List podem ser reutilizados de forma inesperada. Convém armazenar o estado no modelo de dados ou em um ViewModel.
Reordenar Elementos em LazyVStack (iOS 27)
Antes do iOS 27, arrastar para reordenar requeria List com onMove(perform:). As novas APIs reorderable funcionam com qualquer contêiner, incluindo LazyVStack e 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)
}
}
}O SwiftUI gerencia a visualização de arrasto, o marcador de inserção e a animação de soltar. O objeto ReorderDifference fornece os IDs de origem e o destino, deixando as atualizações do modelo para o código do aplicativo.
Seções e Cabeçalhos Otimizados
Organizar o conteúdo em seções melhora a legibilidade mas pode impactar a performance se mal implementado. Os cabeçalhos fixados e a gestão de seções exigem atenção 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 }
}
}Os cabeçalhos fixados (pinnedViews: [.sectionHeaders]) permanecem visíveis durante o scroll, melhorando a navegação em listas longas.
Paginação e Scroll Infinito
Para grandes conjuntos de dados, a paginação evita carregar todos os dados em memória. A implementação deve ser transparente para o usuário.
// 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]
}Esse padrão garante um carregamento fluido sem bloquear a interface e permite uma gestão eficiente da memória.
Pronto para mandar bem nas entrevistas de iOS?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Profiling com Instruments
Identificar problemas de performance exige ferramentas de medição precisas. O Instruments oferece diversos templates adequados ao 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 Otimização com Instruments
Para um profiling efetivo de listas SwiftUI:
- Time Profiler: identificar as funções que mais consomem CPU
- Allocations: verificar o crescimento de memória durante o scroll
- SwiftUI Instrument: visualizar as avaliações de body
- Core Animation: detectar quedas 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)")
}
}
}
}Essa API de debugging do SwiftUI imprime no console quais propriedades mudaram e dispararam uma reavaliação do body. Essencial para identificar renderizações desnecessárias.
Otimizações Avançadas com drawingGroup
Para views complexas com muitos efeitos visuais, o drawingGroup() pode melhorar significativamente a performance ao rasterizar a view em uma camada 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
}O drawingGroup() é particularmente eficaz para views que combinam gradientes, sombras e efeitos blur.
Sources
- WWDC26 SwiftUI Guide - Documentação oficial da Apple sobre as atualizações do SwiftUI no iOS 27 incluindo cache de AsyncImage e APIs reorderable
- New SwiftUI APIs for Reordering and Drag and Drop on iOS 27 - Explicação detalhada dos modificadores
.reorderable()e.reorderContainer(for:) - What's New in SwiftUI for iOS 27 - Resumo completo das mudanças do SwiftUI na WWDC26
Conclusão
A otimização de listas SwiftUI se baseia em uma compreensão profunda dos mecanismos de lazy loading, reciclagem e comparação de views. O iOS 27 trouxe melhorias significativas com cache nativo de AsyncImage e APIs de ações de deslizamento e reordenamento independentes do contêiner.
Checklist de Performance SwiftUI
- Usar
LazyVStackouListem vez deVStackpara coleções - Implementar
Identifiablecom IDs estáveis e únicos - Adotar
Equatablepara células complexas - Aproveitar o cache nativo de AsyncImage no iOS 27, ou implementar cache personalizado para versões anteriores
- Pré-carregar dados com prefetching inteligente
- Escolher
Listpara reciclagem de células ouLazyVStackpara layouts personalizados (ambos agora suportam ações de deslizamento e reordenamento) - Usar
pinnedViewspara cabeçalhos de seção - Implementar paginação para grandes conjuntos de dados
- Fazer profiling regular com Instruments
- Aplicar
drawingGroup()em views com efeitos visuais complexos
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Você saberia encontrar o bug em iOS?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador da SharpSkill
Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.
Atualizado em 19 de agosto de 2026
Tags
Compartilhar
Artigos relacionados

ViewModifiers customizados em SwiftUI: padrões reutilizáveis para Design Systems
Construa ViewModifiers customizados em SwiftUI para um design system consistente. Padrões, melhores práticas e exemplos práticos para estilizar views iOS de forma eficiente.

SwiftUI @Observable vs @State: Quando Usar Cada Um em 2026
Domine as diferenças entre @Observable e @State no SwiftUI para escolher a ferramenta certa de gerenciamento de estado em aplicações iOS.

CloudKit com SwiftUI em 2026: padrões de sincronização entre dispositivos
Guia completo para implementar a sincronização CloudKit com SwiftUI: CKSyncEngine, integração com SwiftData, resolução de conflitos e melhores práticas para iOS 2026.