
Una tarea común en el desarrollo de iOS son las secciones expandibles / plegables en un UITableView. Hoy nos estamos dando cuenta de esta tarea usando SwiftUI. Como un pequeño giro, agregue un triángulo animado en el encabezado de la sección y haga que las celdas también se expandan.
El desarrollo tuvo lugar en Xcode 11.2 para macOS Catalina 10.15.1
Inicia el proyecto
Inicie Xcode, File - New Project - Single View App. En el cuadro de diálogo, especifique el lenguaje de desarrollo de Swift, formaremos la interfaz de usuario utilizando SwiftUI.

Datos
Como datos de demostración, utilizaremos varias expresiones aladas divertidas en latín con su traducción al ruso.
Agregue un nuevo archivo Swift al proyecto,
llámelo Data.swift y escriba lo siguiente allí:
struct QuoteDataModel : Identifiable { var id: String { return latin } var latin : String var russian : String var expanded = false } struct SectionDataModel : Identifiable { var id: Character { return letter } var letter : Character var quotes : [QuoteDataModel] var expanded = false }
QuoteDataModel es un modelo de una sola expresión, en el futuro se convertirá en el contenido de cada celda individual. En él almacenamos el texto original de la expresión, su traducción y el signo de la celda "expandida" (por defecto está "minimizada")
SectionDataModel es un modelo de cada sección separada, aquí almacenamos la "letra" de la sección, una serie de citas que comienzan con esta letra y también un signo de la sección "expandida" (por defecto también "colapsado")
En el futuro, mostraremos todo esto en una vista de Lista, que requiere que los datos para ello cumplan con el protocolo
Identificable . Para hacer esto, definimos la propiedad
id , que debe ser única para cada elemento de la Lista.
Además, en el mismo archivo Data.swift, formamos nuestros datos:
var latinities : [SectionDataModel] = [ SectionDataModel(letter: "C", quotes: [ QuoteDataModel(latin: "Calvitium non est vitium, sed prudentiae indicium.", russian: " , ."), QuoteDataModel(latin: "Conjecturalem artem esse medicinam.", russian: " ."), QuoteDataModel(latin: "Crede firmiter et pecca fortiter!", russian: " !")]), SectionDataModel(letter: "H", quotes: [ QuoteDataModel(latin: "Homo sine religione sicut equus sine freno.", russian: " ."), QuoteDataModel(latin: "Habet et musca splenem.", russian: " .")]), SectionDataModel(letter: "M", quotes: [ QuoteDataModel(latin: "Malum est mulier, sed necessarium malum.", russian: " , ."), QuoteDataModel(latin: "Mulierem ornat silentium.", russian: " .")])]
Tratemos con la interfaz
Ahora determinaremos cómo se verá el encabezado de sección y cada celda.
Elija Archivo - Nuevo - Archivo - SwiftUI View en el menú. Asigne un nombre al archivo
HeaderView.swift y reemplace su contenido con lo siguiente:
import SwiftUI struct HeaderView : View { var section : SectionDataModel var body: some View { HStack() { Spacer() Text(String(section.letter)) .font(.largeTitle) .foregroundColor(Color.black) Spacer() } .background(Color.yellow) } } struct HeaderView_Previews: PreviewProvider { static var previews: some View { HeaderView(section: latinities[0]) } }

Ahora nuevamente Archivo - Nuevo - Archivo - SwiftUI View. Denomine el archivo
QuoteView.swift y reemplace su contenido con lo siguiente:
import SwiftUI struct QuoteView: View { var quote : QuoteDataModel var body: some View { VStack(alignment: .leading, spacing: 5) { Text(quote.latin) .font(.title) if quote.expanded { Group() { Divider() Text(quote.russian).font(.body) }.transition(.move(edge: .top)).animation(.default) } } } } struct QuoteView_Previews: PreviewProvider { static var previews: some View { QuoteView(quote: latinities[0].quotes[0]) } }

Ahora abra el archivo ContentView.swift y cambie la estructura de ContentView de la siguiente manera:
struct ContentView: View { var body: some View { List { ForEach(latinities) { section in Section(header: HeaderView(section: section), footer: EmptyView()) { if section.expanded { ForEach(section.quotes) { quote in QuoteView(quote: quote) } } } } } .listStyle(GroupedListStyle()) } }
¡Enhorabuena, acaba de completar la Lista con datos actualizados! Para cada elemento de la matriz de
latinidades, creamos una sección con un encabezado basado en
HeaderView y con un pie de página vacío. Si la sección está "expandida", entonces para cada expresión en la matriz de citas formamos una celda basada en
QuoteView . En nuestros datos, todas las secciones y todas las celdas están "contraídas", por lo que si hace que Canvas sea visible, verá solo encabezados de sección:

Como comprenderá, ahora la aplicación está completamente "muerta" y aún lejos de nuestro objetivo final. ¡Pero pronto lo arreglaremos!
Modificar el encabezado de la sección ligeramente
Volver al archivo HeaderView.swift. Dentro de la estructura HeaderView, inmediatamente después del cuerpo, agregue esto:
struct Triangle : Shape { func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: 0, y: 0)) path.addLine(to: CGPoint(x: 0, y: rect.height - 1)) path.addLine(to: CGPoint(x: sqrt(3)*(rect.height)/2, y: rect.height/2)) path.closeSubpath() return path } }
Esta estructura devuelve un triángulo equilátero. Ahora agregue nuestro triángulo al encabezado. Dentro de
HStack , antes del primer
espaciador agregue esto:
Triangle() .fill(Color.black) .overlay( Triangle() .stroke(Color.red, lineWidth: 5) ) .frame(width : 50, height : 50) .padding() .rotationEffect(.degrees(section.expanded ? 90 : 0), anchor: .init(x: 0.5, y: 0.5)).animation(.default))

Modificar datos
De vuelta a nuestros datos. Abra Data.swift y ajuste nuestra matriz de
latinidades en una nueva clase UserData, como esta:
class UserData : ObservableObject { @Published var latinities : [SectionDataModel] = [ SectionDataModel(letter: "C", quotes: [ QuoteDataModel(latin: "Calvitium non est vitium, sed prudentiae indicium.", russian: " , ."), QuoteDataModel(latin: "Conjecturalem artem esse medicinam.", russian: " ."), QuoteDataModel(latin: "Crede firmiter et pecca fortiter!", russian: " !")]), SectionDataModel(letter: "H", quotes: [ QuoteDataModel(latin: "Homo sine religione sicut equus sine freno.", russian: " ."), QuoteDataModel(latin: "Habet et musca splenem.", russian: " .")]), SectionDataModel(letter: "M", quotes: [ QuoteDataModel(latin: "Malum est mulier, sed necessarium malum.", russian: " , ."), QuoteDataModel(latin: "Mulierem ornat silentium.", russian: " .")])] }
Recuerde marcar también las latinidades como
@Published .
Que hemos hechoObservableObject es un objeto especial para nuestros datos que se puede "enlazar" a alguna Vista. SwiftUI "monitorea" todos los cambios que pueden afectar la Vista y, después de que los datos han cambiado, cambia la Vista.
Después del "ajuste" de las latinidades, tuvimos muchos errores, los corregiremos. Abra
HeaderView.swift y corrija la estructura de
HeaderView_Previews de la siguiente manera:
struct HeaderView_Previews: PreviewProvider { static var previews: some View { HeaderView(section: UserData().latinities[0]) } }
Ahora realice cambios similares a
QuoteView.swift :
struct QuoteView_Previews: PreviewProvider { static var previews: some View { QuoteView(quote: UserData().latinities[0].quotes[0]) } }
Abra el archivo ContentView.swift y agregue esto antes de la declaración del
cuerpo @ObservedObject var userData = UserData()
Revive el paisaje
Volver al archivo ContentView.swift. Dentro de la estructura ContenView, inmediatamente después de la definición de UserData, agregue dos funciones:
func sectionIndex(section : SectionDataModel) -> Int { userData.latinities.firstIndex(where: {$0.letter == section.letter})! } func quoteIndex(section : Int, quote : QuoteDataModel) -> Int { return userData.latinities[section].quotes.firstIndex(where: {$0.latin == quote.latin})! }
Agregue modificadores
onTapGesture al encabezado de sección y celda que estamos
creando . Vista final del contenido
corporal :
var body: some View { List { ForEach(userData.latinities) { section in Section(header: HeaderView(section: section) .onTapGesture { self.userData.latinities[self.sectionIndex(section: section)].expanded.toggle() }, footer: EmptyView()) { if section.expanded { ForEach(section.quotes) { quote in QuoteView(quote: quote) .onTapGesture { let sectionIndex = self.sectionIndex(section: section) let quoteIndex = self.quoteIndex(section: sectionIndex, quote: quote) self.userData.latinities[sectionIndex].quotes[quoteIndex].expanded.toggle() } } } } } } .listStyle(GroupedListStyle()) }
Las
funciones sectionIndex y
quoteUndex devuelven el índice de las secciones y expresiones que se les pasan. Después de recibir estos índices, cambiamos los valores de las propiedades
expandidas en nuestra matriz de
latinidades , lo que conduce al plegamiento / despliegue de la sección o expresión.

Conclusión
El proyecto terminado se puede
descargar aquí .
Algunos enlaces útiles:
¡Espero que la publicación te sea útil!