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)
}
}
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 {
// ...
}
}
}
}
Modificadores
-
scrollIndicators(_:axes:): Define una visibilidad,ScrollIndicatorVisibility(que puede serautomatic,never,hidden,visible), para uno o varios ejes (verticalyhorizontal). -
scrollDisabled(_:): Habilita o deshabilita el scroll. -
scrollDismissesKeyboard(_:): Determina el comportamiento del teclado cuando el gesto de arrastre comienza. El argumento de tipoScrollDismissesKeyboardModepuede serautomatica,immediately,interactivelyynever(por defecto). -
scrollBounceBehavior(_:axes:): Determina si elScrollViewrebota cuando el usuario llega al inicio o final. El primer argumento de tipoScrollBounceBehaviorpuede serautomatic,alwaysobasedOnSize(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 tipoVisibilitypuede tomar valoresautomatic,visibleyhidden. -
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 valorEquatablecambia. -
scrollClipDisabled(_:): Determina si elScrollViewdeberí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 unScrollViewtienen sombras que se extienden más allá de los bordes delScrollView, 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)
}
}
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)
}
}
}
El contenido del ScrollView puede ser horizontal o vertical, así que no basta simplemente con tener el ForEach dentro del ScrollView, sino que:
- se tiene que meter dentro de un
Stackhorizontal o vertical. - 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)
}
}
}
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)
}
}
}
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.axeses el eje con respecto al cual se calcula el tamaño del contenedor.countes el número de elementos que deben entrar dentro del contenedor.spanes el número de filas o columnas que debería ocupar (dentro del contenedor) la vista modificada.spacinges la distancia de separación entre elementos.alignmentes la alineación dentro del contenedor. -
safeAreaPadding(_:_:): Agrega un margen al área segura de la vista en cuestión. Si se aplica alScrollView, las vistas quedan desplazadas con el mismospacing. 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)
}
}
}
}
}
}
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 conformaScrollTargetBehaviorque puede serpagingoviewAligned. -
scrollTargetLayout(isEnabled:): Configura un contenedor (comoHStackoLazyVStack) para ser un "scrolling target". Esto permite que, al usarviewAlignedenscrollTargetBehavior, las vistas contenidas en elScrollViewno se corten, sino que queden perfectamente alineadas con el borde delScrollView.
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)
}
}
}
Desplazándose a una posición específica
ScrollPosition lleva registro de la posición del ScrollView.
-
init(idType:): El argumentoidTypees el tipo de dato del valor usado para rastrear la posición. -
viewID: ValorHashableque 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 elScrollViewhacia la vista con el identificadorid, con el anclaanchor. -
scrollTo(x:y:): Desplaza el contenido hacia un punto(x,y). -
scrollPosition(_:anchor:): Asocia unBindingalScrollViewpara almacenar el identificador del item visible, yanchorcontrola la alineación de la vista cuando se cambia elBindingde 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()
}
}
}
Personalizar la transición de las subvistas del ScrollView
-
scrollTransition(_:axis:transition:): Aplica un efecto visual a la transición de la vista. El argumentoconfigurationdetermina cómo se va a aplicar la transición: puede seridentity(no cambia la apariencia de la vista),animated(anima la transición) einteractive(interpola el efecto a medida que la vista se hace visible).animated(_:)aplica una animación personalizada einteractive(timingCurve:)define una curva personalizada.axisdetermina el eje al que se aplica el efecto ytransitiones 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()
}
}
}
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)