¿Cómo dimensiono automáticamente un UIScrollView para adaptarlo a su contenido?

130

¿Hay alguna manera de hacer un UIScrollViewajuste automático a la altura (o ancho) del contenido que se desplaza?

Algo como:

[scrollView setContentSize:(CGSizeMake(320, content.height))];
jwerre
fuente

Respuestas:

306

El mejor método que he encontrado para actualizar el tamaño del contenido de un UIScrollViewbasado en sus subvistas contenidas:

C objetivo

CGRect contentRect = CGRectZero;

for (UIView *view in self.scrollView.subviews) {
    contentRect = CGRectUnion(contentRect, view.frame);
}
self.scrollView.contentSize = contentRect.size;

Rápido

let contentRect: CGRect = scrollView.subviews.reduce(into: .zero) { rect, view in
    rect = rect.union(view.frame)
}
scrollView.contentSize = contentRect.size
leviatán
fuente
9
Estaba ejecutando esto viewDidLayoutSubviewspara que el autolayout terminara, en iOS7 funcionó bien, pero probar os iOS6 el autolayout por alguna razón no terminó el trabajo, por lo que tenía algunos valores de altura incorrectos, así que cambié a viewDidAppearahora funciona bien ... solo para señalar que tal vez alguien necesitaría esto. gracias
Hatem Alimam
1
Con el iPad iOS6 había un misterioso UIImageView (7x7) en la esquina inferior derecha. Lo filtré y acepté solo los componentes de la interfaz de usuario (visible && alpha), luego funcionó. Preguntándome si esa imageView estaba relacionada con scrollBars, definitivamente no es mi código.
JOM
55
¡Tenga cuidado cuando los indicadores de desplazamiento estén habilitados! El SDK creará automáticamente un UIImageView para cada uno. debe tenerlo en cuenta o de lo contrario su tamaño de contenido será incorrecto. (ver stackoverflow.com/questions/5388703/… )
AmitP
Puedo confirmar que esta solución funciona perfectamente para mí en Swift 4
Ethan Humphries
En realidad, no podemos comenzar a unirnos desde CGRect.zero, solo funciona si se guardan algunas subvistas en coordenadas negativas. Debe comenzar los marcos de subvista de unión desde la primera subvista, no desde cero. Por ejemplo, solo tiene una subvista que es una UIView con marco (50, 50, 100, 100). El tamaño de su contenido será (0, 0, 150, 150), no (50, 50, 100, 100).
Evgeny
74

UIScrollView no conoce la altura de su contenido automáticamente. Debes calcular la altura y el ancho por ti mismo

Hazlo con algo como

CGFloat scrollViewHeight = 0.0f;
for (UIView* view in scrollView.subviews)
{
   scrollViewHeight += view.frame.size.height;
}

[scrollView setContentSize:(CGSizeMake(320, scrollViewHeight))];

Pero esto solo funciona si las vistas son una debajo de la otra. Si tiene una vista una al lado de la otra, solo tiene que agregar la altura de una si no desea establecer el contenido del desplazador más grande de lo que realmente es.

emenegro
fuente
¿Crees que algo como esto funcionaría también? CGFloat scrollViewHeight = scrollView.contentSize.height; [scrollView setContentSize: (CGSizeMake (320, scrollViewHeight))]; Por cierto, no te olvides de preguntar el tamaño y la altura. scrollViewHeight + = view.frame.size.height
jwerre
La inicialización de scrollViewHeight a la altura hace que el desplazamiento sea más grande que su contenido. Si hace esto, no agregue la altura de las vistas visibles en la altura inicial del contenido del desplazador. Aunque tal vez necesites una brecha inicial u otra cosa.
emenegro 01 de
21
O simplemente hazlo:int y = CGRectGetMaxY(((UIView*)[_scrollView.subviews lastObject]).frame); [_scrollView setContentSize:(CGSizeMake(CGRectGetWidth(_scrollView.frame), y))];
Gal
@saintjab, gracias, lo acabo de agregar como respuesta formal :)
Gal
40

Solución si está utilizando diseño automático:

  • Establecer translatesAutoresizingMaskIntoConstraintsa NOen todas las vistas involucradas.

  • Coloque y dimensione su vista de desplazamiento con restricciones externas a la vista de desplazamiento.

  • Use restricciones para diseñar las subvistas dentro de la vista de desplazamiento, asegurándose de que las restricciones estén vinculadas a los cuatro bordes de la vista de desplazamiento y no confíe en la vista de desplazamiento para obtener su tamaño.

Fuente: https://developer.apple.com/library/ios/technotes/tn2154/_index.html

Adam Waite
fuente
10
En mi opinión, esta es la solución real en el mundo donde todo sucede con el diseño automático. Con respecto a las otras soluciones: ¿Qué ... en serio calculas la altura y, a veces, el ancho de cada vista?
Fawkes
¡Gracias por compartir esto! Esa debería ser la respuesta aceptada.
SePröbläm
2
Esto no funciona cuando desea que la altura de la vista de desplazamiento se base en su altura de contenido. (por ejemplo, una ventana emergente centrada en la pantalla donde no desea desplazarse hasta una cierta cantidad de contenido).
LeGom
Esto no responde la pregunta
Igor Vasilev
Gran solución Sin embargo, agregaría una viñeta más ... "No restrinja la altura de una vista de pila secundaria si creciera verticalmente. Simplemente fíjela a los bordes de la vista de desplazamiento".
Andrew Kirna
35

Agregué esto a la respuesta de Espuz y JCC. Utiliza la posición y de las subvistas y no incluye las barras de desplazamiento. Editar Utiliza la parte inferior de la vista secundaria más baja que está visible.

+ (CGFloat) bottomOfLowestContent:(UIView*) view
{
    CGFloat lowestPoint = 0.0;

    BOOL restoreHorizontal = NO;
    BOOL restoreVertical = NO;

    if ([view respondsToSelector:@selector(setShowsHorizontalScrollIndicator:)] && [view respondsToSelector:@selector(setShowsVerticalScrollIndicator:)])
    {
        if ([(UIScrollView*)view showsHorizontalScrollIndicator])
        {
            restoreHorizontal = YES;
            [(UIScrollView*)view setShowsHorizontalScrollIndicator:NO];
        }
        if ([(UIScrollView*)view showsVerticalScrollIndicator])
        {
            restoreVertical = YES;
            [(UIScrollView*)view setShowsVerticalScrollIndicator:NO];
        }
    }
    for (UIView *subView in view.subviews)
    {
        if (!subView.hidden)
        {
            CGFloat maxY = CGRectGetMaxY(subView.frame);
            if (maxY > lowestPoint)
            {
                lowestPoint = maxY;
            }
        }
    }
    if ([view respondsToSelector:@selector(setShowsHorizontalScrollIndicator:)] && [view respondsToSelector:@selector(setShowsVerticalScrollIndicator:)])
    {
        if (restoreHorizontal)
        {
            [(UIScrollView*)view setShowsHorizontalScrollIndicator:YES];
        }
        if (restoreVertical)
        {
            [(UIScrollView*)view setShowsVerticalScrollIndicator:YES];
        }
    }

    return lowestPoint;
}
rico
fuente
1
¡Maravilloso! Justo lo que necesitaba.
Richard
2
Me sorprende que un método como este no sea parte de la clase.
João Portela
¿Por qué lo hiciste self.scrollView.showsHorizontalScrollIndicator = NO; self.scrollView.showsVerticalScrollIndicator = NO;al principio y self.scrollView.showsHorizontalScrollIndicator = YES; self.scrollView.showsVerticalScrollIndicator = YES;al final del método?
João Portela
Gracias richy :) +1 por la respuesta.
Shraddha
1
@richy tienes razón, los indicadores de desplazamiento son subvistas si son visibles, pero mostrar / ocultar la lógica es incorrecto. Debe agregar dos locales BOOL: restoreHorizontal = NO, restoreVertical = NO. Si muestraHorizontal, escóndelo, establezca restoreHorizontal en YES. Lo mismo para vertical. Luego, después de for / in loop insert if if: if restoreHorizontal, configúrelo para mostrarlo, lo mismo para vertical. Su código solo obligará a mostrar ambos indicadores
inmigrante ilegal
13

Aquí está la respuesta aceptada rápidamente para cualquier persona que sea demasiado perezosa para convertirla :)

var contentRect = CGRectZero
for view in self.scrollView.subviews {
    contentRect = CGRectUnion(contentRect, view.frame)
}
self.scrollView.contentSize = contentRect.size
Josh
fuente
y actualizado para Swift3: var contentRect = CGRect.zero para ver en self.scrollView.subviews {contentRect = contentRect.union (view.frame)} self.scrollView.contentSize = contentRect.size
drew ..
¿Esto tiene que ser llamado en el hilo principal? y en didLayoutSubViews?
Maksim Kniazev
8

Aquí hay una adaptación de Swift 3 de la respuesta de @leviatan:

EXTENSIÓN

import UIKit


extension UIScrollView {

    func resizeScrollViewContentSize() {

        var contentRect = CGRect.zero

        for view in self.subviews {

            contentRect = contentRect.union(view.frame)

        }

        self.contentSize = contentRect.size

    }

}

USO

scrollView.resizeScrollViewContentSize()

Muy facil de usar!

fredericdnd
fuente
Muy útil. Después de tantas iteraciones, encontré esta solución y es perfecta.
Hardik Shah
¡Encantador! Acabo de implementarlo.
arvidurs
7

La siguiente extensión sería útil en Swift .

extension UIScrollView{
    func setContentViewSize(offset:CGFloat = 0.0) {
        // dont show scroll indicators
        showsHorizontalScrollIndicator = false
        showsVerticalScrollIndicator = false

        var maxHeight : CGFloat = 0
        for view in subviews {
            if view.isHidden {
                continue
            }
            let newHeight = view.frame.origin.y + view.frame.height
            if newHeight > maxHeight {
                maxHeight = newHeight
            }
        }
        // set content size
        contentSize = CGSize(width: contentSize.width, height: maxHeight + offset)
        // show scroll indicators
        showsHorizontalScrollIndicator = true
        showsVerticalScrollIndicator = true
    }
}

La lógica es la misma con las respuestas dadas. Sin embargo, omite las vistas ocultas UIScrollViewy el cálculo se realiza después de que los indicadores de desplazamiento se ocultan.

Además, hay un parámetro de función opcional y puede agregar un valor de desplazamiento pasando el parámetro a la función.

gokhanakkurt
fuente
Esta solución contiene demasiados supuestos, ¿por qué muestra los indicadores de desplazamiento en un método que establece el tamaño del contenido?
Kappe
5

Gran y mejor solución de @leviathan. Simplemente traduciendo a Swift usando el enfoque FP (programación funcional).

self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect(), { 
  CGRectUnion($0, $1.frame) 
}.size
othierry42
fuente
Si desea ignorar las vistas ocultas, puede filtrar las subvistas antes de pasar para reducir.
Andy
Actualizado para Swift 3:self.contentSize = self.subviews.reduce(CGRect(), { $0.union($1.frame) }).size
John Rogers
4

Puede obtener la altura del contenido dentro de UIScrollView calculando qué hijo "llega más lejos". Para calcular esto, debe tener en cuenta el origen Y (inicio) y la altura del artículo.

float maxHeight = 0;
for (UIView *child in scrollView.subviews) {
    float childHeight = child.frame.origin.y + child.frame.size.height;
    //if child spans more than current maxHeight then make it a new maxHeight
    if (childHeight > maxHeight)
        maxHeight = childHeight;
}
//set content size
[scrollView setContentSize:(CGSizeMake(320, maxHeight))];

Al hacer las cosas de esta manera, los elementos (subvistas) no tienen que apilarse directamente uno debajo del otro.

paxx
fuente
También puede usar este forro dentro del bucle: maxHeight = MAX (maxHeight, child.frame.origin.y + child.frame.size.height) ;)
Thomas CG de Vilhena
3

Se me ocurrió otra solución basada en la solución de @emenegro

NSInteger maxY = 0;
for (UIView* subview in scrollView.subviews)
{
    if (CGRectGetMaxY(subview.frame) > maxY)
    {
        maxY = CGRectGetMaxY(subview.frame);
    }
}
maxY += 10;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, maxY)];

Básicamente, descubrimos qué elemento está más abajo en la vista y agregamos un relleno de 10px en la parte inferior

Chris
fuente
3

Debido a que scrollView puede tener otras scrollViews o diferentes inDepth subViews tree, es preferible ejecutarlo en profundidad de forma recursiva. ingrese la descripción de la imagen aquí

Swift 2

extension UIScrollView {
    //it will block the mainThread
    func recalculateVerticalContentSize_synchronous () {
        let unionCalculatedTotalRect = recursiveUnionInDepthFor(self)
        self.contentSize = CGRectMake(0, 0, self.frame.width, unionCalculatedTotalRect.height).size;
    }

    private func recursiveUnionInDepthFor (view: UIView) -> CGRect {
        var totalRect = CGRectZero
        //calculate recursevly for every subView
        for subView in view.subviews {
            totalRect =  CGRectUnion(totalRect, recursiveUnionInDepthFor(subView))
        }
        //return the totalCalculated for all in depth subViews.
        return CGRectUnion(totalRect, view.frame)
    }
}

Uso

scrollView.recalculateVerticalContentSize_synchronous()
iluvatar_GR
fuente
2

O simplemente hazlo:

int y = CGRectGetMaxY(((UIView*)[_scrollView.subviews lastObject]).frame); [_scrollView setContentSize:(CGSizeMake(CGRectGetWidth(_scrollView.frame), y))];

(Esta solución fue agregada por mí como un comentario en esta página. ¡Después de obtener 19 votos positivos para este comentario, he decidido agregar esta solución como una respuesta formal para el beneficio de la comunidad!)

Galón
fuente
2

Para swift4 usando reduce:

self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect.zero, {
   return $0.union($1.frame)
}).size
mm282
fuente
También vale la pena verificar si al menos una subvista tiene origen == CGPoint.zero o simplemente asigna a cero el origen de una subvista particular (la más grande, por ejemplo).
Paul B
Esto solo funciona para las personas que colocan sus puntos de vista con CGRect (). Si está utilizando restricciones, esto no está funcionando.
linus_hologram
1

El tamaño depende del contenido cargado dentro de él y las opciones de recorte. Si se trata de una vista de texto, entonces también depende del ajuste, cuántas líneas de texto, el tamaño de fuente, etc. Casi imposible para usted computarse. La buena noticia es que se calcula después de cargar la vista y en viewWillAppear. Antes de eso, todo es desconocido y el tamaño del contenido será el mismo que el tamaño del marco. Pero, en el método viewWillAppear y después (como viewDidAppear), el tamaño del contenido será el real.

Papá Pitufo
fuente
1

Envolviendo el código de Richy, ¡creé una clase UIScrollView personalizada que automatiza completamente el cambio de tamaño del contenido!

SBScrollView.h

@interface SBScrollView : UIScrollView
@end

SBScrollView.m:

@implementation SBScrollView
- (void) layoutSubviews
{
    CGFloat scrollViewHeight = 0.0f;
    self.showsHorizontalScrollIndicator = NO;
    self.showsVerticalScrollIndicator = NO;
    for (UIView* view in self.subviews)
    {
        if (!view.hidden)
        {
            CGFloat y = view.frame.origin.y;
            CGFloat h = view.frame.size.height;
            if (y + h > scrollViewHeight)
            {
                scrollViewHeight = h + y;
            }
        }
    }
    self.showsHorizontalScrollIndicator = YES;
    self.showsVerticalScrollIndicator = YES;
    [self setContentSize:(CGSizeMake(self.frame.size.width, scrollViewHeight))];
}
@end

Cómo usarlo:
simplemente importe el archivo .h a su controlador de vista y declare una instancia de SBScrollView en lugar de la instancia normal de UIScrollView.

AmitP
fuente
2
layoutSubviews parece que es el lugar adecuado para dicho código relacionado con el diseño. Pero en un diseño UIScrollView se llama a Subview cada vez que se actualiza el desplazamiento de contenido durante el desplazamiento. Y dado que el tamaño del contenido generalmente no cambia durante el desplazamiento, esto es un desperdicio.
Sven
Es verdad. Una forma de evitar esta ineficiencia podría ser verificar [self isDragging] y [self isDecelerating] y solo realizar el diseño si ambos son falsos.
Greg Brown
1

Establezca un tamaño de contenido dinámico como este.

 self.scroll_view.contentSize = CGSizeMake(screen_width,CGRectGetMaxY(self.controlname.frame)+20);
Monika Patel
fuente
0

realmente depende del contenido: content.frame.height podría darte lo que quieres. Depende de si el contenido es una sola cosa, o una colección de cosas.

Andiih
fuente
0

También encontré la respuesta del leviatán para trabajar mejor. Sin embargo, estaba calculando una altura extraña. Al recorrer las subvistas, si la vista de desplazamiento está configurada para mostrar indicadores de desplazamiento, estarán en la matriz de subvistas. En este caso, la solución es desactivar temporalmente los indicadores de desplazamiento antes de realizar un bucle y luego restablecer su configuración de visibilidad anterior.

-(void)adjustContentSizeToFit es un método público en una subclase personalizada de UIScrollView.

-(void)awakeFromNib {    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self adjustContentSizeToFit];
    });
}

-(void)adjustContentSizeToFit {

    BOOL showsVerticalScrollIndicator = self.showsVerticalScrollIndicator;
    BOOL showsHorizontalScrollIndicator = self.showsHorizontalScrollIndicator;

    self.showsVerticalScrollIndicator = NO;
    self.showsHorizontalScrollIndicator = NO;

    CGRect contentRect = CGRectZero;
    for (UIView *view in self.subviews) {
        contentRect = CGRectUnion(contentRect, view.frame);
    }
    self.contentSize = contentRect.size;

    self.showsVerticalScrollIndicator = showsVerticalScrollIndicator;
    self.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;
}
Clayzar
fuente
0

Creo que esta puede ser una buena manera de actualizar el tamaño de la vista de contenido de UIScrollView.

extension UIScrollView {
    func updateContentViewSize() {
        var newHeight: CGFloat = 0
        for view in subviews {
            let ref = view.frame.origin.y + view.frame.height
            if ref > newHeight {
                newHeight = ref
            }
        }
        let oldSize = contentSize
        let newSize = CGSize(width: oldSize.width, height: newHeight + 20)
        contentSize = newSize
    }
}
Furqan Khan
fuente
0

¿Por qué no una sola línea de código?

_yourScrollView.contentSize = CGSizeMake(0, _lastView.frame.origin.y + _lastView.frame.size.height);
Ali
fuente
0
import UIKit

class DynamicSizeScrollView: UIScrollView {

    var maxHeight: CGFloat = UIScreen.main.bounds.size.height
    var maxWidth: CGFloat = UIScreen.main.bounds.size.width

    override func layoutSubviews() {
        super.layoutSubviews()
        if !__CGSizeEqualToSize(bounds.size,self.intrinsicContentSize){
            self.invalidateIntrinsicContentSize()
        }
    }

    override var intrinsicContentSize: CGSize {
        let height = min(contentSize.height, maxHeight)
        let width = min(contentSize.height, maxWidth)
        return CGSize(width: width, height: height)
    }

}
usuario6127890
fuente