Comment dimensionner automatiquement un UIScrollView pour l'adapter à son contenu

130

Existe-t-il un moyen d'ajuster UIScrollViewautomatiquement la hauteur (ou la largeur) du contenu qu'il fait défiler?

Quelque chose comme:

[scrollView setContentSize:(CGSizeMake(320, content.height))];
Jwerre
la source

Réponses:

306

La meilleure méthode que j'ai jamais rencontrée pour mettre à jour la taille du contenu d'un en UIScrollViewfonction de ses sous-vues contenues:

Objectif c

CGRect contentRect = CGRectZero;

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

Rapide

let contentRect: CGRect = scrollView.subviews.reduce(into: .zero) { rect, view in
    rect = rect.union(view.frame)
}
scrollView.contentSize = contentRect.size
Léviathan
la source
9
viewDidLayoutSubviewsJ'exécutais ceci pour que la mise en page automatique se termine, dans iOS7 cela fonctionnait bien, mais tester os iOS6 la mise en page automatique pour une raison quelconque n'a pas terminé le travail, donc j'avais des valeurs de hauteur erronées, donc je suis passé à viewDidAppearmaintenant fonctionne bien. juste pour souligner que quelqu'un en aurait peut-être besoin. merci
Hatem Alimam
1
Avec iOS6 iPad, il y avait une mystérieuse UIImageView (7x7) dans le coin inférieur droit. J'ai filtré cela en n'acceptant que les composants d'interface utilisateur (visibles et alpha), puis cela a fonctionné. Vous vous demandez si cette imageView était liée à scrollBars, certainement pas mon code.
JOM
5
Soyez prudent lorsque les indicateurs de défilement sont activés! un UIImageView sera automatiquement créé pour chacun d'eux par le SDK. vous devez en tenir compte, sinon la taille de votre contenu sera erronée. (voir stackoverflow.com/questions/5388703/… )
AmitP
Peut confirmer que cette solution fonctionne parfaitement pour moi dans Swift 4
Ethan Humphries
En fait, nous ne pouvons pas commencer l'union à partir de CGRect.zero, cela ne fonctionne que si vous hébergez certaines sous-vues en coordonnées négatives. Vous devez démarrer les cadres de sous-vue d'union à partir de la première sous-vue, et non à partir de zéro. Par exemple, vous n'avez qu'une seule sous-vue qui est une vue UIV avec cadre (50, 50, 100, 100). La taille de votre contenu sera (0, 0, 150, 150) et non (50, 50, 100, 100).
Evgeny
74

UIScrollView ne connaît pas automatiquement la hauteur de son contenu. Vous devez calculer vous-même la hauteur et la largeur

Faites-le avec quelque chose comme

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

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

Mais cela ne fonctionne que si les vues sont l'une en dessous de l'autre. Si vous avez une vue côte à côte, vous n'avez qu'à ajouter la hauteur d'une seule si vous ne voulez pas définir le contenu du défilement plus grand qu'il ne l'est réellement.

emenegro
la source
Pensez-vous que quelque chose comme ça fonctionnerait aussi? CGFloat scrollViewHeight = scrollView.contentSize.height; [scrollView setContentSize: (CGSizeMake (320, scrollViewHeight))]; BTW, n'oubliez pas de demander la taille puis la hauteur. scrollViewHeight + = view.frame.size.height
jwerre
L'initialisation de scrollViewHeight à la hauteur rend le défilement plus grand que son contenu. Si vous faites cela, n'ajoutez pas la hauteur des vues visibles sur la hauteur initiale du contenu du défilement. Bien que vous ayez peut-être besoin d'un écart initial ou de quelque chose d'autre.
emenegro
21
Ou faites simplement:int y = CGRectGetMaxY(((UIView*)[_scrollView.subviews lastObject]).frame); [_scrollView setContentSize:(CGSizeMake(CGRectGetWidth(_scrollView.frame), y))];
Gal
@saintjab, merci, je viens de l'ajouter comme réponse formelle :)
Gal
41

Solution si vous utilisez la mise en page automatique:

  • Mettre translatesAutoresizingMaskIntoConstraintsà NOtous les points de vue en cause.

  • Positionnez et redimensionnez votre vue de défilement avec des contraintes externes à la vue de défilement.

  • Utilisez des contraintes pour disposer les sous-vues dans la vue de défilement, en vous assurant que les contraintes sont liées aux quatre bords de la vue de défilement et ne comptez pas sur la vue de défilement pour obtenir leur taille.

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

Adam Waite
la source
10
A mon humble avis, c'est la vraie solution au monde où tout se passe avec la mise en page automatique. Concernant les autres solutions: qu'est-ce que ... êtes-vous sérieux dans le calcul de la hauteur et parfois de la largeur de chaque vue?
Fawkes
Merci d'avoir partagé ça! Cela devrait être la réponse acceptée.
SePröbläm
2
Cela ne fonctionne pas lorsque vous souhaitez que la hauteur de la vue de défilement soit basée sur la hauteur de son contenu. (par exemple une fenêtre contextuelle centrée sur l'écran où vous ne voulez pas faire défiler jusqu'à une certaine quantité de contenu).
Cela ne répond pas à la question
Igor Vasilev
Excellente solution. Cependant, j'ajouterais une puce supplémentaire ... "Ne restreignez pas la hauteur d'une vue de pile enfant si elle doit croître verticalement. Épinglez-la simplement sur les bords de la vue de défilement."
Andrew Kirna
35

J'ai ajouté ceci à la réponse d'Espuz et JCC. Il utilise la position y des sous-vues et n'inclut pas les barres de défilement. Modifier Utilise le bas de la sous-vue la plus basse 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;
}
Richy
la source
1
Merveilleux! Juste ce dont j'avais besoin.
Richard
2
Je suis surpris qu'une méthode comme celle-ci ne fasse pas partie de la classe.
João Portela
Pourquoi avez-vous fait self.scrollView.showsHorizontalScrollIndicator = NO; self.scrollView.showsVerticalScrollIndicator = NO;au début et self.scrollView.showsHorizontalScrollIndicator = YES; self.scrollView.showsVerticalScrollIndicator = YES;à la fin de la méthode?
João Portela
Merci richy :) +1 pour la réponse.
Shraddha le
1
@richy you're right les indicateurs de défilement sont des sous-vues si visibles, mais afficher / masquer la logique est erronée. Vous devez ajouter deux locaux BOOL: restoreHorizontal = NO, restoreVertical = NO. Si showsHorizontal, masquez-le, définissez restoreHorizontal sur YES. Idem pour la verticale. Ensuite, après l'instruction for / in loop insert if: if restoreHorizontal, set to show it, same for vertical. Votre code forcera simplement à afficher les deux indicateurs
immigrant illégal
13

Voici la réponse acceptée rapidement pour quiconque est trop paresseux pour le convertir :)

var contentRect = CGRectZero
for view in self.scrollView.subviews {
    contentRect = CGRectUnion(contentRect, view.frame)
}
self.scrollView.contentSize = contentRect.size
Josh
la source
et mis à jour pour Swift3: var contentRect = CGRect.zero pour l'affichage dans self.scrollView.subviews {contentRect = contentRect.union (view.frame)} self.scrollView.contentSize = contentRect.size
dessiné ..
Cela doit-il être appelé sur le thread principal? et dans didLayoutSubViews?
Maksim Kniazev
8

Voici une adaptation Swift 3 de la réponse de @ leviatan:

EXTENSION

import UIKit


extension UIScrollView {

    func resizeScrollViewContentSize() {

        var contentRect = CGRect.zero

        for view in self.subviews {

            contentRect = contentRect.union(view.frame)

        }

        self.contentSize = contentRect.size

    }

}

USAGE

scrollView.resizeScrollViewContentSize()

Très simple d'utilisation!

fredericdnd
la source
Très utile. Après tant d'itérations, j'ai trouvé cette solution et elle est parfaite.
Hardik Shah
Charmant! Je viens de le mettre en œuvre.
arvidurs
7

L'extension suivante serait utile dans 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 logique est la même avec les réponses données. Cependant, il omet les vues cachées à l'intérieur UIScrollViewet le calcul est effectué après que les indicateurs de défilement sont masqués.

En outre, il existe un paramètre de fonction facultatif et vous pouvez ajouter une valeur de décalage en passant le paramètre à function.

gokhanakkurt
la source
Cette solution contient trop d'hypothèses, pourquoi affichez-vous les indicateurs de défilement dans une méthode qui définit la taille du contenu?
Kappe
5

Excellente et meilleure solution de @leviathan. Il suffit de traduire rapidement en utilisant l'approche FP (programmation fonctionnelle).

self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect(), { 
  CGRectUnion($0, $1.frame) 
}.size
othierry42
la source
Si vous souhaitez ignorer les vues masquées, vous pouvez filtrer les sous-vues avant de passer à la réduction.
Andy
Mise à jour pour Swift 3:self.contentSize = self.subviews.reduce(CGRect(), { $0.union($1.frame) }).size
John Rogers
4

Vous pouvez obtenir la hauteur du contenu dans UIScrollView en calculant quel enfant "atteint plus loin". Pour calculer cela, vous devez prendre en compte l'origine Y (début) et la hauteur de l'élément.

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))];

En faisant les choses de cette façon, les éléments (sous-vues) n'ont pas à être empilés directement les uns sous les autres.

paxx
la source
Vous pouvez également utiliser cette doublure à l'intérieur de la boucle: maxHeight = MAX (maxHeight, child.frame.origin.y + child.frame.size.height) ;)
Thomas CG de Vilhena
3

J'ai trouvé une autre solution basée sur la solution 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)];

Fondamentalement, nous déterminons quel élément est le plus bas dans la vue et ajoutons un rembourrage de 10 pixels en bas.

Chris
la source
3

Étant donné qu'un scrollView peut avoir d'autres scrollViews ou une arborescence de sous-vues inDepth différente, il est préférable d'exécuter en profondeur de manière récursive. entrez la description de l'image ici

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)
    }
}

Usage

scrollView.recalculateVerticalContentSize_synchronous()
iluvatar_GR
la source
2

Ou faites simplement:

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

(Cette solution a été ajoutée par moi en tant que commentaire sur cette page. Après avoir obtenu 19 votes pour ce commentaire, j'ai décidé d'ajouter cette solution comme réponse formelle au profit de la communauté!)

Fille
la source
2

Pour swift4 en utilisant réduire:

self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect.zero, {
   return $0.union($1.frame)
}).size
mm282
la source
Il vaut également la peine de vérifier si au moins une sous-vue a une origine == CGPoint.zero ou attribuer simplement l'origine d'une sous-vue particulière (la plus grande par exemple) à zéro.
Paul B
Cela ne fonctionne que pour les personnes qui placent leurs vues avec CGRect (). Si vous utilisez des contraintes, cela ne fonctionne pas.
linus_hologram
1

La taille dépend du contenu chargé à l'intérieur de celui-ci et des options de détourage. S'il s'agit d'une vue de texte, cela dépend également de l'habillage, du nombre de lignes de texte, de la taille de la police, etc. Presque impossible pour vous de vous calculer. La bonne nouvelle est qu'il est calculé après le chargement de la vue et dans le viewWillAppear. Avant cela, tout est inconnu et la taille du contenu sera la même que la taille du cadre. Mais, dans la méthode viewWillAppear et après (comme viewDidAppear), la taille du contenu sera la taille réelle.

Grand Schtroumpf
la source
1

Enveloppant le code de Richy, j'ai créé une classe UIScrollView personnalisée qui automatise complètement le redimensionnement du contenu!

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

Comment l'utiliser:
importez simplement le fichier .h dans votre contrôleur de vue et déclarez une instance SBScrollView au lieu de l'instance UIScrollView normale.

AmitP
la source
2
layoutSubviews semble être le bon endroit pour un tel code lié à la mise en page. Mais dans une mise en page UIScrollView, Subview est appelée à chaque fois que le décalage de contenu est mis à jour pendant le défilement. Et comme la taille du contenu ne change généralement pas lors du défilement, c'est plutôt un gaspillage.
Sven
C'est vrai. Une façon d'éviter cette inefficacité pourrait être de vérifier [self isDragging] et [self isDecelerating] et de n'effectuer la mise en page que si les deux sont faux.
Greg Brown
1

Définissez la taille du contenu dynamique comme ceci.

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

cela dépend vraiment du contenu: content.frame.height peut vous donner ce que vous voulez? Cela dépend si le contenu est une seule chose ou un ensemble de choses.

Andiih
la source
0

J'ai également trouvé que la réponse de Léviathan fonctionnait le mieux. Cependant, il calculait une hauteur étrange. Lorsque vous parcourez les sous-vues en boucle, si la vue de défilement est définie pour afficher des indicateurs de défilement, ceux-ci seront dans le tableau des sous-vues. Dans ce cas, la solution consiste à désactiver temporairement les indicateurs de défilement avant de boucler, puis à rétablir leur paramètre de visibilité précédent.

-(void)adjustContentSizeToFit est une méthode publique sur une sous-classe personnalisée 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
la source
0

Je pense que cela peut être un bon moyen de mettre à jour la taille de la vue du contenu d'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
la source
0

pourquoi pas une seule ligne de code ??

_yourScrollView.contentSize = CGSizeMake(0, _lastView.frame.origin.y + _lastView.frame.size.height);
Ali
la source
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)
    }

}
user6127890
la source