Cómo usar PreviewModifier para crear Previews eficientes

Misael Cuevas
09 octubre, 20247min de lectura
PreviewModifier es un protocolo que permite compartir datos entre previews, eliminando la duplicidad de código. Lo interesante de usar este protocolo es que el sistema de previews cachea los datos utilizados en un PreviewModifier, permitiendo que sean reutilizados en las demás previews que lo implementen. Esto hace que tus previews sean mucho más eficientes.
PreviewModifier es especialmente útil para mockear servicios en vistas que realizan peticiones a una API o acceso a bases de datos a través de dicho servicio. Esto asegura que la preview se cargue mucho más rápido.
App de ejemplo
Para demostrar las capacidades de PreviewModifier, comienza con este simple ejemplo:
import Foundation@Observable final class SearchManager {private(set) var results = [String]()func update(_ results: [String]) {self.results = results}}
SearchManager es una clase que administra una lista de resultados contenida en la variable results y posee la función update para reemplazar la lista de resultados. Además, la clase está marcada como @Observable, lo que notifica a cualquier vista de SwiftUI que use SearchManager sobre los cambios en results.
Si quieres conocer más sobre
@Observable, te recomiendo nuestro artículo sobre Observation en SwiftUI.
Para utilizar SearchManager, crea la siguiente vista:
import SwiftUI// 1struct ContentView: View {// 2@Environment(SearchManager.self) private var searchManagervar body: some View {// 3List(searchManager.results, id: \.self) { result inText(result)}}}// 4#Preview {ContentView().environment(SearchManager())}
- Se define una vista llamada
ContentView. - Se crea la variable
searchManagerpara acceder aSearchManagerdesde el entorno mediante@Environment. - Se muestra una lista de resultados provenientes de
SearchManager. - Se muestra una preview de
ContentViewen Xcode, asignando una instancia deSearchManageral entorno de la vista.
Hasta este punto, la preview de ContentView se muestra en blanco porque no se han agregado resultados con la función update, por lo que la lista no tiene datos para mostrar.
Usando PreviewModifier
Una vez creado el ejemplo de la app, es hora de utilizar PreviewModifier para configurar los datos de SearchManager y usarlos en ContentView. Crea la estructura PreviewSearchManager que implemente el protocolo PreviewModifier:
import SwiftUIstruct PreviewSearchManager: PreviewModifier {}
El protocolo requiere definir cuál será el tipo de datos a utilizar en las previews a través del alias Context, en este caso SearchManager. Esto permite usar protocolos de una manera genérica. Añade el siguiente código dentro de PreviewSearchManager:
typealias Context = SearchManager
Con esto, ya puedes conformar las dos funciones del protocolo. La primera es makeSharedContext, que sirve para crear el contexto que utilizarán las previews, además de cachear los datos. Esto significa que el contexto se creará una sola vez para todas las previews que usen este modificador. Su firma es la siguiente:
@MainActor static func makeSharedContext() async throws -> Self.Context
La segunda función es body, que aplica el contexto con el parámetro context al contenido de la vista mediante el parámetro content. También puedes aplicar otros modificadores al contenido de la vista. Su firma es la siguiente:
@ViewBuilder @MainActor func body(content: Self.Content, context: Self.Context) -> Self.Body
Debajo del alias Context, añade el siguiente código:
static func makeSharedContext() async throws -> SearchManager {// 1let results = ["Swift", "SwiftUI", "UIKit", "Combine", "MapKit"]// 2let searchManager = SearchManager()// 3searchManager.update(results)// 4return searchManager}func body(content: Content, context: SearchManager) -> some View {// 5content.environment(context)}
- Se define una lista de resultados.
- Se crea una instancia de
SearchManager. - Se actualizan los resultados de
searchManagercon los valores predefinidos. - Se retorna la instancia de
SearchManagercon los valores establecidos. - Se incluye el
SearchManagercreado previamente en la funciónmakeSharedContexta la vista.
Ahora estás listo para utilizar PreviewSearchManager en la preview de ContentView. Sustituye todo el código de la macro #Preview con:
#Preview(traits: .modifier(PreviewSearchManager())) {ContentView()}
La diferencia con el antiguo #Preview es que ahora se utiliza el parámetro traits para aplicar el modificador PreviewSearchManager, y se ha eliminado la asignación del entorno de la vista en ContentView porque ya se está realizando en la función body de PreviewSearchManager.
Puedes simplificar aún más el uso del modificador dentro del parámetro traits de la preview creando una variable estática en la extensión PreviewTrait. Por ejemplo:
extension PreviewTrait where T == Preview.ViewTraits {@MainActor static var previewSearchManager: Self = .modifier(PreviewSearchManager())}
De esta manera, podrás usar tus previews de la siguiente forma:
#Preview(traits: .previewSearchManager) {ContentView()}
Si has seguido todas las instrucciones, tu código completo debería verse así:
import SwiftUI@Observable final class SearchManager {private(set) var results = [String]()func update(_ results: [String]) {self.results = results}}struct ContentView: View {@Environment(SearchManager.self) private var searchManagervar body: some View {List(searchManager.results, id: \.self) { result inText(result)}}}#Preview(traits: .previewSearchManager) {ContentView()}struct PreviewSearchManager: PreviewModifier {typealias Context = SearchManagerstatic func makeSharedContext() async throws -> SearchManager {let results = ["Swift", "SwiftUI", "UIKit", "Combine", "MapKit"]let searchManager = SearchManager()searchManager.update(results)return searchManager}func body(content: Content, context: SearchManager) -> some View {content.environment(context)}}extension PreviewTrait where T == Preview.ViewTraits {@MainActor static var previewSearchManager: Self = .modifier(PreviewSearchManager())}
PreviewModifier + SwiftData
Otro caso donde PreviewModifier es muy útil es para mockear datos de SwiftData. Mira un ejemplo:
import SwiftData@Model final class SearchResult {var text: Stringinit(text: String) {self.text = text}static var mockData: [SearchResult] = [SearchResult(text: "Swift"),SearchResult(text: "SwiftUI"),SearchResult(text: "Apple"),]}
Se define el modelo SearchResult, que representa los resultados de una búsqueda. Contiene una propiedad text y una variable mockData con datos de ejemplo.
import SwiftUIimport SwiftData// 1struct MockModelContainer: PreviewModifier {// 2typealias Context = ModelContainer// 3static func makeSharedContext() async throws -> ModelContainer {let configuration = ModelConfiguration(isStoredInMemoryOnly: true)let container = try ModelContainer(for: SearchResult.self, configurations: configuration)SearchResult.mockData.forEach { container.mainContext.insert($0) }return container}// 4func body(content: Content, context: ModelContainer) -> some View {content.modelContainer(context)}}// 5extension PreviewTrait where T == Preview.ViewTraits {@MainActor static var mockModelContainer: Self = .modifier(MockModelContainer())}
- Se define la estructura
MockModelContainer, que conforma al protocoloPreviewModifierpara mockear los datos de SwiftData. - Se establece el
ModelContainercomo el contexto que utilizarán las previews. - Se crea una instancia de
ModelContainercon una configuración específica, utilizando la variablemockDatadeSearchResultpara insertar datos en el modelo. - Se aplica el contenedor de SwiftData proveniente del contexto
ModelContaineral contenido de la vista. - Se simplifica el uso del modificador en los
traitsde la preview creando la variablemockModelContaineren la extensiónPreviewTrait.
import SwiftUIimport SwiftData// 1struct SearchResultsView: View {@Query private var searchResults: [SearchResult]var body: some View {List(searchResults) { result inText(result.text)}}}// 2#Preview(traits: .mockModelContainer) {SearchResultsView()}
- Se define la vista
SearchResultsView, que muestra una lista de resultados de búsqueda a partir de una consulta al modeloSearchResult. - Se define la preview de
SearchResultsViewcon el mock del contenedor de SwiftData.
Si quieres conocer más sobre SwiftData te recomiendo nuestra guía esencial para comenzar con SwiftData.
Compatibilidad
PreviewModifier puede utilizarse a partir de las siguientes versiones de los sistemas operativos de Apple:
- iOS 18.0+.
- iPadOS 18.0+.
- Mac Catalyst 13.0+.
- macOS 15.0+.
- tvOS 18.0+.
- visionOS 2.0+.
- watchOS 11.0+.