Una de las grandes características de desarrollar en SwiftUI es Vista previa de Xcode que permiten una rápida iteración de la interfaz de usuario mediante la representación de los cambios de código casi en tiempo real junto con el código SwiftUI. En DoorDash hacemos un uso intensivo de Xcode Previews junto con el SnapshotTesting biblioteca de Sin puntos para asegurarnos de que las pantallas tienen el aspecto que esperamos al desarrollarlas y de que no cambian de forma inesperada con el tiempo. SnapshotTesting puede utilizarse para capturar una imagen renderizada de una View
y crear un XCTest
si la nueva imagen no coincide con la imagen de referencia en disco. Las vistas previas de Xcode en combinación con SnapshotTesting se pueden utilizar para proporcionar iteraciones rápidas al tiempo que se garantiza que las vistas siguen teniendo el aspecto previsto sin temor a cambios inesperados.
El reto de utilizar Xcode Previews y SnapshotTesting juntos es que puede resultar en una gran cantidad de repeticiones y duplicación de código entre las vistas previas y las pruebas. Para resolver este problema, DoorDash engineering ha desarrollado PreviewSnapshots, una herramienta de snapshot de código abierto que se puede utilizar para compartir fácilmente configuraciones entre las vistas previas de Xcode y las pruebas de snapshot. En este artículo, vamos a profundizar en este tema proporcionando primero algunos antecedentes sobre cómo funcionan las vistas previas de Xcode y SnapshotTesting y luego explicando cómo utilizar la nueva herramienta de código abierto con ejemplos ilustrativos de cómo eliminar la duplicación de código compartiendo configuraciones de vista entre vistas previas e instantáneas.
Cómo funcionan las vistas previas de Xcode
Vista previa de Xcode permiten a los desarrolladores devolver una o más versiones de un View
de un PreviewProvider
y Xcode mostrará una versión en vivo del archivo View
junto con el código de aplicación.
A partir de Xcode 14, las vistas con múltiples previsualizaciones se presentan como pestañas seleccionables en la parte superior del lienzo de previsualización, como se muestra en la Figura 1.
Cómo funciona SnapshotTesting
La biblioteca SnapshotTesting permite a los desarrolladores escribir afirmaciones de prueba sobre la apariencia de sus vistas. Al afirmar que una vista coincide con las imágenes de referencia en disco, los desarrolladores pueden estar seguros de que las vistas no cambian de forma inesperada con el tiempo.
El código de ejemplo de la Figura 2 comparará las versiones corta y larga de MessageView
con las imágenes de referencia almacenadas en disco como testSnapshots.1
y testSnapshots.2
respectivamente. Las instantáneas fueron grabadas originalmente por SnapshotTesting
y se le asigna automáticamente el nombre de la función de prueba junto con la posición de la aserción dentro de la función.
El problema de utilizar Xcode Previews y SnapshotTesting juntos
Hay mucho en común entre el código utilizado para las vistas previas de Xcode y para la creación de pruebas instantáneas. Esta similitud puede dar lugar a la duplicación de código y el esfuerzo adicional para los desarrolladores al tratar de adoptar ambas tecnologías. Lo ideal sería que los desarrolladores pudieran escribir código para previsualizar una vista en una variedad de configuraciones y luego reutilizar ese código para realizar pruebas instantáneas de la vista en esas mismas configuraciones.
Presentación de PreviewSnapshots
Resolver este reto de duplicación de código es donde PreviewSnapshots puede ayudar. PreviewSnapshots permite a los desarrolladores crear un único conjunto de estados de vista para las vistas previas de Xcode y crear casos de prueba de instantáneas para cada uno de los estados con una única aserción de prueba. A continuación veremos cómo funciona con un ejemplo sencillo.
Uso de PreviewSnapshots para una vista simple
Supongamos que tenemos una vista que recibe una lista de nombres y los muestra de alguna forma interesante.
Tradicionalmente querríamos crear una vista previa para algunos estados interesantes de la vista. Tal vez vacía, un solo nombre, una lista corta de nombres y una lista larga de nombres.
struct NameList_Previews: PreviewProvider {
static var previews: some View {
NameList(names: [])
.previewDisplayName("Empty")
.previewLayout(.sizeThatFits)
NameList(names: [“Alice”])
.previewDisplayName("Single Name")
.previewLayout(.sizeThatFits)
NameList(names: [“Alice”, “Bob”, “Charlie”])
.previewDisplayName("Short List")
.previewLayout(.sizeThatFits)
NameList(names: [
“Alice”,
“Bob”,
“Charlie”,
“David”,
“Erin”,
//...
])
.previewDisplayName("Long List")
.previewLayout(.sizeThatFits)
}
}
Luego escribiríamos un código muy similar para las pruebas de instantáneas.
final class NameList_SnapshotTests: XCTestCase {
func test_snapshotEmpty() {
let view = NameList(names: [])
assertSnapshot(matching: view, as: .image)
}
func test_snapshotSingleName() {
let view = NameList(names: [“Alice”])
assertSnapshot(matching: view, as: .image)
}
func test_snapshotShortList() {
let view = NameList(names: [“Alice”, “Bob”, “Charlie”])
assertSnapshot(matching: view, as: .image)
}
func test_snapshotLongList() {
let view = NameList(names: [
“Alice”,
“Bob”,
“Charlie”,
“David”,
“Erin”,
//...
])
assertSnapshot(matching: view, as: .image)
}
}
La larga lista de nombres podría compartirse potencialmente entre las vistas previas y las pruebas instantáneas utilizando una propiedad estática, pero no se puede evitar escribir manualmente una prueba instantánea individual para cada estado que se esté previsualizando.
PreviewSnapshots permite a los desarrolladores definir una única colección de configuraciones interesantes, y luego reutilizarlas trivialmente entre vistas previas y pruebas de instantáneas.
Este es el aspecto de una vista previa de Xcode usando PreviewSnapshots:
struct NameList_Previews: PreviewProvider {
static var previews: some View {
snapshots.previews.previewLayout(.sizeThatFits)
}
static var snapshots: PreviewSnapshots<[String]> {
PreviewSnapshots(
configurations: [
.init(name: "Empty", state: []),
.init(name: "Single Name", state: [“Alice”]),
.init(name: "Short List", state: [“Alice”, “Bob”, “Charlie”]),
.init(name: "Long List", state: [
“Alice”,
“Bob”,
“Charlie”,
“David”,
“Erin”,
//...
]),
],
configure: { names in NameList(names: names) }
)
}
}
Para crear una colección de PreviewSnapshots construimos una instancia de PreviewSnapshots con un array de configuraciones junto con una variable configure
para establecer la vista de una configuración determinada. Una configuración consta de un nombre, junto con una instancia de State
que se utilizará para configurar la vista. En este caso, el tipo de estado es [String]
para la matriz de nombres.
Para generar las previsualizaciones devolvemos snapshots.previews
de la norma previews
como se ilustra en la Figura 3. snapshots.previews
generará una vista previa con el nombre adecuado para cada configuración del PreviewSnapshots
.
Para una vista pequeña que es fácil de construir, PreviewSnapshots proporciona alguna estructura adicional pero no hace mucho para reducir las líneas de código dentro de las vistas previas. El mayor beneficio para las vistas pequeñas viene cuando es el momento de escribir pruebas de instantáneas para la vista.
final class NameList_SnapshotTests: XCTestCase {
func test_snapshot() {
NameList_Previews.snapshots.assertSnapshots()
}
}
Esa única aserción probará instantáneamente cada configuración en el PreviewSnapshots. La Figura 4 muestra el código de ejemplo junto a las imágenes de referencia en Xcode. Además, si se añaden nuevas configuraciones a las vistas previas, se comprobarán automáticamente sin necesidad de cambiar el código de prueba.
Para vistas más complejas con muchos argumentos, las ventajas son aún mayores.
Uso de PreviewSnapshots para una vista más compleja
En nuestro segundo ejemplo veremos un FormView
que lleva varios Binding
s , un mensaje de error opcional y un cierre de acción como argumentos de su inicializador. Esto demostrará el aumento de los beneficios de PreviewSnapshots a medida que aumenta la complejidad de la construcción de la vista.
struct FormView: View {
init(
firstName: Binding<String>,
lastName: Binding<String>,
email: Binding<String>,
errorMessage: String?,
submitTapped: @escaping () -> Void
) { ... }
// ...
}
Desde PreviewSnapshots
es genérico sobre el estado de entrada, podemos agrupar los distintos parámetros de entrada en una pequeña estructura de ayuda para pasarla al método configure
bloque y sólo tienen que hacer el trabajo de construir un FormView
una vez. Para mayor comodidad PreviewSnapshots
proporciona una NamedPreviewState
para simplificar la construcción de configuraciones de entrada agrupando el nombre de la vista previa junto con el estado de la vista previa.
struct FormView_Previews: PreviewProvider {
static var previews: some View {
snapshots.previews
}
static var snapshots: PreviewSnapshots<PreviewState> {
PreviewSnapshots(
states: [
.init(name: "Empty"),
.init(
name: "Filled",
firstName: "John", lastName: "Doe", email: "[email protected]"
),
.init(
name: "Error",
firstName: "John", lastName: "Doe", errorMessage: "Email Address is required"
),
],
configure: { state in
NavigationView {
FormView(
firstName: .constant(state.firstName),
lastName: .constant(state.lastName),
email: .constant(state.email),
errorMessage: state.errorMessage,
submitTapped: {}
)
}
}
)
}
struct PreviewState: NamedPreviewState {
let name: String
var firstName: String = ""
var lastName: String = ""
var email: String = ""
var errorMessage: String?
}
}
En el código de ejemplo creamos un PreviewState
que se ajuste a NamedPreviewState
para guardar el nombre de la vista previa junto con el nombre, apellidos, dirección de correo electrónico y un mensaje de error opcional para construir la vista. A continuación, en el configure
creamos una única instancia de FormView
basándose en el estado de configuración introducido. Al devolver snapshots.preview
de PreviewProvider.previews
PreviewSnapshots hará un bucle sobre los estados de entrada y construirá una vista previa Xcode con el nombre adecuado para cada estado, como se ve en la Figura 5.
Una vez que hemos definido un conjunto de PreviewSnapshots para las vistas previas, podemos crear de nuevo un conjunto de pruebas de instantáneas con una única aserción de prueba unitaria.
final class FormView_SnapshotTests: XCTestCase {
func test_snapshot() {
FormView_Previews.snapshots.assertSnapshots()
}
}
Al igual que en el ejemplo más sencillo anterior, este caso de prueba comparará cada uno de los estados de vista previa definidos en FormView_Previews.snapshots
contra la imagen de referencia grabada en disco y generar un fallo de prueba si las imágenes no coinciden con lo esperado.
Conclusión
Este artículo ha discutido algunos de los beneficios del uso de Xcode Previews y SnapshotTesting cuando se desarrolla con SwiftUI. También ha demostrado algunos de los puntos de dolor y la duplicación de código que puede resultar de la utilización de esas dos tecnologías juntas y cómo PreviewSnapshots permite a los desarrolladores ahorrar tiempo mediante la reutilización del esfuerzo que ponen en la escritura de vistas previas de Xcode para las pruebas de instantáneas.
Las instrucciones para incorporar PreviewSnapshots a tu proyecto, así como una aplicación de ejemplo que hace uso de PreviewSnapshots, están disponibles en GitHub.