ViewBuilders

En su documentación, Apple define los ViewBuilders como un elemento personalizado que construye una vista a partir de un closure.

Tampoco es que se demasiado clara esta definición, pero básicamente te permite generar sub-vistas, componentizar el código, vamos.

Para este ejemplo vamos a crear un Grid (compatible con iOS15), utilizando un @ViewBuilder, que pueda elegir hacerlo de 2 x 2 y de 1 x 4, por ejemplo. Los iconos son los mismos, de hecho uno de ellos tiene que ser un botón que permite marcar como favorito y el resto simplemente vistas.

Los intentaré simplificar para centrarnos en el @ViewBuilder.

Todo comienza con el struct Icon, con las propiedades tipo de icono y si se debe mostrar.

struct Icon {
    let type: IconType
    var show: Bool
    var image: Image {
      switch type {<
        case .isFavorite: Image(systemName:"heart")
        case .isOpen: Image(systemName: show ? "door.left.hand.open":"door.left.hand.closed")
        case .horario(let openHours): Image(systemName: openHours.imageName)
        case .guardia: Image(systemName: "cross")
        }
    }
}

La imagen para cada icono es distinta, así que utilicé 2 enums, dado que en el caso de las OpenHours están las opciones de 24horas, 12 horas y 8 horas. 

enum IconType {
    case isFavorite, isOpen, horario(OpenHours), guardia
}

enum OpenHours {
    case _24h, _12h, _8h
    
    var imageName: String {
        switch self {
        case ._24h: "24.circle"
        case ._12h: "12.circle"
        case ._8h: "bag.circle"
        }
    }
}

Del struct Icon cree una extensión con funciones estáticas que devuelven una View, que me permitirán, en el caso del icono favorito añadir un botón, y en el resto solo la View.

extension Icon {
    static func favorite(isFavorite: Bool, action: @escaping  ()->() ) -> some View {
        let icon = Icon(type: .isFavorite, show: isFavorite)
        return Button {
            action()
        } label: {
            IconView(icon: icon)
        }
    }
    
    static func isOpen(_ show: Bool) -> some View {
        let icon = Icon(type: .isOpen, show: show)
        return IconView(icon: icon)
    }
    
    static func horario(_ openHours: OpenHours) -> some View {
        let icon = Icon(type: .horario(openHours), show: true)
        return IconView(icon: icon)
    }
    
    static func guardia(show: Bool) -> some View {
        let icon = Icon(type: .guardia, show: show)
        return IconView(icon: icon)
    }
}

Y ahora si la función que implementa el @ViewBuilder, que también es una función estática de Icon.

 static func Grid<IconGrid: View>(rows: Int,
                                  verticalSpacing: CGFloat = 10,
                                  horizontalSpacing:CGFloat = 10,
                                  @ViewBuilder icons: () -> IconGrid) 
    -> some View {
        let rows = Array.init(repeating: GridItem(.fixed(verticalSpacing * 4)), count: rows)
        return LazyHGrid(rows: rows,spacing: horizontalSpacing) {
            icons()
        }
    }

La función Grid recibe los siguientes parámetros:

rows: Cuántas filas tendrá el grid.
vertical y horizontal spacing: Permitirán ajustar el espacio entre los iconos.
icons: Es cada elemento del closure que será parte del Grid. Se marca con el atributo @ViewBuilder para que Swift sepa que devolveremos una View, anclada en el tipo <IconGrid: View>

En este caso utilizo un LazyHGrid, que funciona similar a un HStack, y que el atributo lazy significa que la vista es creada cuando SwiftUI lo necesite.

Ahora podemos utilizar, a partir de nuestro struct Icon esta vista, indicando las filas que necesitamos y dentro del closure todas las vistas que queremos mostrar.

struct ListIconView: View {
    @State var tiendas = Tienda.samples
    
    var body: some View {
        ForEach(tiendas) { tienda in
        HStack {
            Text("Hello, World!")
            Spacer()
            // Aqui está implementado:
            Icon.Grid(rows: 2) {
                Icon.favorite(isFavorite: tienda.isFavorite) {
                    marcaComoFavorita(tienda: tienda)
                }
                .foregroundColor(Color.red)
                Icon.guardia(show: tienda.guardia)
                    .foregroundColor(Color.blue)
                Icon.isOpen(tienda.isOpen)
                Icon.horario(tienda.horario)
                    .foregroundColor(Color.yellow)
            }
            .font(.largeTitle)
        }
        .padding()
        }
    }
    private func marcaComoFavorita(tienda: Tienda) {
        guard let index = tiendas.firstIndex(where:{ $0.id == tienda.id }) else { return }
        tiendas[index].isFavorite.toggle()
    }
}

¿Que te ha parecido? 
Te animo a implementar una función en la que puedas elegir entre un LazyVStack o LazyHStack y me cuentes.

1 fila
2 filas
3 filas
4 filas

¿Quieres recibir posts, cheatCodes, enlaces y katas en Swift para practicar?

Quincenalmente recibirás en tu correo electrónico la newsletter, solo hace falta tu correo electrónico.