DEV Community

GoyesDev
GoyesDev

Posted on

[SUI] ScrollView

Cuando el contenido de la vista supera el tamaño de la pantalla, es necesario hacerlo desplazable ("scrollable"), para lo cual se envuelve la vista en un ScrollView.

ScrollView solo hace que el contenido se pueda desplazar en la pantalla. Para agrupar vistas igual es necesario usar un Stack vertical u horizontal. No obstante, esto no necesariamente significa que hay que usar un VStack o un HStack, sino que se puede optar por sus versiones perezosas: LazyVStack y LazyHStack, respectivamente, que tienen el siguiente constructor:

struct ContentView: View {
  var body: some View {
    ScrollView {
      LazyVStack(pinnedViews: .sectionHeaders) {
        Section {
          ForEach(1..<100) { number in
            Text("Número: \(number)")
          }
        } header: {
          Text("Primera seccion")
            .background(.red)
        }
        Section {
          ForEach(100..<200) { number in
            Text("Número: \(number)")
          }
        } header: {
          Text("Segunda seccion")
            .background(.yellow)
        }
      }
    }
    // ⚠️ Para ocultar el indicador de desplazamiento se usa este modificador
    .scrollIndicators(.hidden)
  }
}
Enter fullscreen mode Exit fullscreen mode

Si no se pasa ningún argumento pinnedViews al LazyVStack, entonces no va a anclar los encabezados de las secciones:

struct ContentView: View {
  var body: some View {
    ScrollView {
      LazyVStack {
        // ...
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Modificadores

  • scrollIndicators(_:axes:): Define una visibilidad, ScrollIndicatorVisibility (que puede ser automatic, never, hidden, visible), para uno o varios ejes (vertical y horizontal).
  • scrollDisabled(_:): Habilita o deshabilita el scroll.
  • scrollDismissesKeyboard(_:): Determina el comportamiento del teclado cuando el gesto de arrastre comienza. El argumento de tipo ScrollDismissesKeyboardMode puede ser automatica, immediately, interactively y never (por defecto).
  • scrollBounceBehavior(_:axes:): Determina si el ScrollView rebota cuando el usuario llega al inicio o final. El primer argumento de tipo ScrollBounceBehavior puede ser automatic, always o basedOnSize (solo si el contenido excede el tamaño de la vista)
  • scrollContentBackground(_:): Muestra u oculta el fondo del scrollview. Las listas y tablas agregan automáticamente un fondo que se puede ocultar. El argumento de tipo Visibility puede tomar valores automatic, visible y hidden.
  • scrollIndicatorsFlash(onAppear:): Determina si los indicadores hacen "flash" (se hacen visibles momentáneamente) cuando la vista aparece.
  • scrollIndicatorsFlash(trigger:): Determina si los indicadores de desplazamiento hacen "flash" cuando un valor Equatable cambia.
  • scrollClipDisabled(_:): Determina si el ScrollView debería cortar el contenido que excede sus límites. Por defecto, un scrollview corta el contenido en sus bordes, pero se puede deshabilitar este comportamiento. Por ejemplo, si las vistas dentro de un ScrollView tienen sombras que se extienden más allá de los bordes del ScrollView, se puede usar este modificador para evitar cortar las sombras.


struct ContentView: View {
  @State var age: String = ""

  var body: some View {
    ScrollView {
      TextField("Edad", text: $age)
        .textFieldStyle(.roundedBorder)
        .keyboardType(.numberPad)
        .padding()
      LazyVStack {
        ForEach(0..<50) { i in
          Text("Item \(i)")
            .frame(maxWidth: .infinity)
        }
      }
    }
    .scrollDismissesKeyboard(.immediately)
    .scrollIndicatorsFlash(onAppear: true)
  }
}
Enter fullscreen mode Exit fullscreen mode

Tener en cuenta que el orden de los componentes importa en el árbol de vistas resultantes. Notar qué pasa cuando se saca el TextField del ScrollView:

struct ContentView: View {
  @State var age: String = ""

  var body: some View {
    VStack {
      TextField("Edad", text: $age)
        .textFieldStyle(.roundedBorder)
        .keyboardType(.numberPad)
        .padding()
      ScrollView {
        LazyVStack {
          ForEach(0..<50) { i in
            Text("Item \(i)")
              .frame(maxWidth: .infinity)
          }
        }
      }
      .scrollDismissesKeyboard(.immediately)
      .scrollIndicatorsFlash(onAppear: true)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

El contenido del ScrollView puede ser horizontal o vertical, así que no basta simplemente con tener el ForEach dentro del ScrollView, sino que:

  1. se tiene que meter dentro de un Stack horizontal o vertical.
  2. Se tiene que definir el eje de desplazamiento del ScrollView.

A continuación se muestra un ejemplo donde no se define el eje de desplazamiento del ScrollView (o sea que por defecto es vertical), ni tampoco se usa un Stack (o sea que por defecto se apila de forma vertical).

struct ContentView: View {
  var body: some View {
    VStack {
      ScrollView {
        // ⚠️ Si no se pone ningún Stack entonces el contenido se pinta de forma vertical por defecto.
        ForEach(0..<50) { i in
          Text("Item \(i)")
            .frame(maxWidth: .infinity)
        }
      }
      .scrollIndicatorsFlash(onAppear: true)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Para mostrar el contenido de forma horizontal es necesario hacer que el eje de desplazamiento del ScrollView sea horizontal y también poner un Stack horizontal.

struct ContentView: View {
  var body: some View {
    VStack {
      ScrollView(.horizontal) {
        LazyHStack {
          ForEach(0..<50) { i in
            Text("Item \(i)")
              .frame(maxWidth: .infinity)
          }
        }
      }
      .scrollIndicatorsFlash(onAppear: true)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Cambiar el tamaño de las celdas

Se puede cambiar el tamaño de las celdas para encajar un cierto número dentro del ScrollView. Para ello se usan los modificadores:

  • containerRelativeFrame(_:count:span:spacing:alignment:): Posiciona la vista dentro de un frame invisible cuyo tamaño es relativo con respecto al contenedor más cercano. axes es el eje con respecto al cual se calcula el tamaño del contenedor. count es el número de elementos que deben entrar dentro del contenedor. span es el número de filas o columnas que debería ocupar (dentro del contenedor) la vista modificada. spacing es la distancia de separación entre elementos. alignment es la alineación dentro del contenedor.
  • safeAreaPadding(_:_:): Agrega un margen al área segura de la vista en cuestión. Si se aplica al ScrollView, las vistas quedan desplazadas con el mismo spacing. Si se aplica a las vistas contenidas, cada una tendrá un nuevo margen.
struct ContentView: View {
  var body: some View {
    VStack {
      ScrollView(.horizontal) {
        LazyHStack {
          ForEach(1..<50) { i in
            Text("Item \(i)")
              .frame(maxWidth: .infinity)
              .containerRelativeFrame(.horizontal, count: 6, span: 1, spacing: 0, alignment: .center)
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Alineando el desplazamiento del ScrollView

ScrollView calcula la velocidad del gesto de desplazamiento y mueve el contenido, según el comportamiento definido.

  • scrollTargetBehavior(_:): Recibe un argumento que conforma ScrollTargetBehavior que puede ser paging o viewAligned.
  • scrollTargetLayout(isEnabled:): Configura un contenedor (como HStack o LazyVStack) para ser un "scrolling target". Esto permite que, al usar viewAligned en scrollTargetBehavior, las vistas contenidas en el ScrollView no se corten, sino que queden perfectamente alineadas con el borde del ScrollView.
struct ContentView: View {
  var body: some View {
    VStack {
      ScrollView(.horizontal) {
        LazyHStack {
          ForEach(1..<50) { i in
            Text("Item \(i)")
              .frame(maxWidth: .infinity)
              .containerRelativeFrame(.horizontal, count: 6, span: 1, spacing: 0, alignment: .center)
          }
        }
        .scrollTargetLayout()
      }
      .scrollTargetBehavior(.viewAligned)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Desplazándose a una posición específica

ScrollPosition lleva registro de la posición del ScrollView.

  • init(idType:): El argumento idType es el tipo de dato del valor usado para rastrear la posición.
  • viewID: Valor Hashable que representa el identificador de la vista en la posición visible.
  • scrollTo(edge:): Desplaza el contenido hacia el extremo definido por parámetro.
  • scrollTo(id:anchor:): Desplaza el ScrollView hacia la vista con el identificador id, con el ancla anchor.
  • scrollTo(x:y:): Desplaza el contenido hacia un punto (x,y).
  • scrollPosition(_:anchor:): Asocia un Binding al ScrollView para almacenar el identificador del item visible, y anchor controla la alineación de la vista cuando se cambia el Binding de forma programática.
struct ContentView: View {
  let data: [Content] = [
    .init(text: "1"),
    // ...
    .init(text: "16"),
  ]
  @State private var position = ScrollPosition(idType: Content.ID.self)

  var body: some View {
    VStack {
      Spacer()

      ScrollView(.horizontal) {
        LazyHStack {
          ForEach(data) { i in
            Text("Item: [\(i.text)]")
              .containerRelativeFrame(.horizontal, count: 8, span: 1, spacing: 0, alignment: .center)
          }
        }
        .scrollTargetLayout()
      }
      .frame(height: 100)
      .scrollPosition($position)
      .scrollTargetBehavior(.viewAligned)
      .onChange(of: position, initial: true) { oldValue, newValue in
        if let visibleId = newValue.viewID as? UUID {
          print(visibleId)
        }
      }

      Button {
        position.scrollTo(edge: .leading)
      } label: {
        Text("De vuelta al inicio")
      }

      Spacer()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Personalizar la transición de las subvistas del ScrollView

  • scrollTransition(_:axis:transition:): Aplica un efecto visual a la transición de la vista. El argumento configuration determina cómo se va a aplicar la transición: puede ser identity (no cambia la apariencia de la vista), animated (anima la transición) e interactive (interpola el efecto a medida que la vista se hace visible). animated(_:) aplica una animación personalizada e interactive(timingCurve:) define una curva personalizada. axis determina el eje al que se aplica el efecto y transition es la acción a ejecutar al finalizar la transición.

Para añadir un efecto de transición, hay que aplicar scrollTransition() a la vista y luego definir el efecto en el closure recibido.

struct ContentView: View {
  let data: [Content] = [
    .init(text: "1"),
    // ...
    .init(text: "16"),
  ]
  @State private var position = ScrollPosition(idType: Content.ID.self)

  var body: some View {
    VStack {
      Spacer()

      ScrollView(.horizontal) {
        LazyHStack {
          ForEach(data) { i in
            Text("Item: [\(i.text)]")
              .containerRelativeFrame(.horizontal, count: 8, span: 1, spacing: 0, alignment: .center)
              .scrollTransition(.interactive, axis: .horizontal) { effect, phase in
                effect
                  .opacity(phase.isIdentity ? 1: 0)
                  .scaleEffect(phase.isIdentity ? 1 : 0.5)
              }
          }
        }
        .scrollTargetLayout()
      }
      .frame(height: 100)
      .scrollPosition($position)
      .scrollTargetBehavior(.viewAligned)
      .onChange(of: position, initial: true) { oldValue, newValue in
        if let visibleId = newValue.viewID as? UUID {
          print(visibleId)
        }
      }

      Button {
        position.scrollTo(edge: .leading)
      } label: {
        Text("De vuelta al inicio")
      }

      Spacer()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

El sistema ejecuta el closure pasado a scrollTransition() a cada vista. Cuando se aplica el efecto, debemos determinar el estado actual de la vista. Si la vista está en un área visible, ScrollTransitionPhse.isIdentity retorna true. De lo contrario, retorna false.

Top comments (0)