App iOS interna de Cultiva IA · Gestión de campañas de marketing con IA
| Wrapper | Cuándo usarlo |
|---|---|
| @State | Valor local de la vista (toggles, campos de formulario) |
| @Binding | Referencia bidireccional al @State del padre |
| @Observable + @State | Modelo propio con múltiples propiedades (ViewModel) |
| @Observable (sin wrapper) | Referencia de solo lectura pasada desde el padre |
| @Bindable | Binding bidireccional a propiedad @Observable |
| @Environment | Dependencias compartidas (Router, AuthManager) |
@Observable final class CampaignListViewModel { private(set) var campaigns: [Campaign] = [] private(set) var isLoading = false var searchText = "" // Protocolo: testable + mockeable private let repository: any CampaignRepository @MainActor func load() async { isLoading = true defer { isLoading = false } campaigns = (try? await repository.fetchAll()) ?? [] } }
ObservableObject invalida toda la vista ante cualquier cambio.
enum Destination: Hashable { case detail(Campaign.ID) case settings case profile } @Observable final class Router { var path = NavigationPath() func navigate(to dest: Destination) { path.append(dest) } func popToRoot() { path = NavigationPath() } } // RootView NavigationStack(path: $router.path) { CampaignListView() .navigationDestination( for: Destination.self ) { dest in switch dest { case .detail(let id): CampaignDetailView(campaignID: id) case .settings: SettingsView() case .profile: ProfileView() } } }.environment(router)
// Subvistas pequeñas → solo re-render // de la subvista que leyó la propiedad struct CampaignListView: View { @State private var viewModel = CampaignListViewModel() var body: some View { VStack { SummaryBannerView(summary: viewModel.summary) StatusFilterView(selected: $viewModel.selectedStatus) LazyVStack { /* campañas */ } } .task { await viewModel.load() } } } // Equatable para evitar re-renders struct CampaignRowView: View, Equatable { let campaign: Campaign static func == (lhs: Self, rhs: Self) -> Bool { lhs.campaign == rhs.campaign } } // ViewModifier reutilizable struct CardModifier: ViewModifier { func body(content: Content) -> some View { content .padding() .background(.regularMaterial) .clipShape(RoundedRectangle( cornerRadius: 14)) } } extension View { func cardStyle() -> some View { modifier(CardModifier()) } }
// ✅ LazyVStack para listas largas ScrollView { LazyVStack(spacing: 12) { ForEach(viewModel.filtered) { c in CampaignRowView(campaign: c) } } } // ✅ IDs estables (nunca índices de array) ForEach(items, id: \.id) { ... } // ✅ Trabajo async en .task{} // (se cancela al desaparecer la vista) .task { await viewModel.load() } // ❌ NUNCA en body (bloquea render) // var body: some View { // let data = apiCall() // ← PROHIBIDO // }
// Repositorio mock para previews struct MockCampaignRepository: CampaignRepository { let items: [Campaign] func fetchAll() async throws -> [Campaign] { items } } // Previews con datos reales #Preview("Lista activa") { NavigationStack { CampaignListView() } .environment(Router()) .environment(AuthManager()) } #Preview("Fila de campaña") { List { CampaignRowView(campaign: MockData.campaigns[0]) } } #Preview("Detalle") { CampaignDetailView( campaignID: MockData .campaigns[0].id ) .environment(Router()) .environment(AuthManager()) }
ObservableObject / @Published / @StateObject / @EnvironmentObject — migrar a @Observable; invalida toda la vista ante cualquier cambio@EnvironmentObject para dependencias compartidas — reemplazar por @Environment(MyClass.self) con tipo explícito; falla en runtime sin mensajes de error clarosbody o init — usar .task{} o métodos explícitos de carga; bloquea el hilo principal@State en subvistas que no poseen los datos — pasar datos desde el padre; rompe la fuente única de verdadAnyView para borrado de tipos — preferir @ViewBuilder o Group; AnyView dificulta las optimizaciones de SwiftUIForEach(items.indices) — usar id: \.id o Identifiable; los índices generan re-renders innecesarios al reordenarSendable al pasar datos a actores — puede causar data races detectadas en runtime con Swift Concurrency