Como dimensionar automaticamente um UIScrollView para caber em seu conteúdo

130

Existe uma maneira de fazer um UIScrollViewajuste automático à altura (ou largura) do conteúdo que está rolando?

Algo como:

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

Respostas:

306

O melhor método que eu já encontrei para atualizar o tamanho do conteúdo de um com UIScrollViewbase em suas subvisões contidas:

Objetivo-C

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ã
fonte
9
Eu estava executando isso viewDidLayoutSubviewspara que o autolayout terminasse, no iOS7 funcionou bem, mas testar o iOS6 por algum motivo não terminou o trabalho, então eu tinha alguns valores de altura errados, então mudei para viewDidAppearagora funciona bem .. apenas para apontar que talvez alguém precise disso. obrigado
Hatem Alimam
1
No iPad iOS6, havia um misterioso UIImageView (7x7) no canto inferior direito. Eu filtrei isso aceitando apenas componentes da interface do usuário (visíveis e alfa), e funcionou. Pensando se esse imageView estava relacionado ao scrollBars, definitivamente não é o meu código.
JOM
5
Tenha cuidado quando os Indicadores de rolagem estiverem ativados! um UIImageView será criado automaticamente para cada um pelo SDK. você deve dar conta disso ou o tamanho do seu conteúdo estará errado. (veja stackoverflow.com/questions/5388703/… )
AmitP
Pode confirmar que esta solução funciona perfeitamente para mim em Swift 4
Ethan Humphries
Na verdade, não podemos começar a união do CGRect.zero, ele funciona apenas se você exibir algumas subvisões em coordenadas negativas. Você deve iniciar os quadros de subvisão de união a partir da primeira subvisão, não do zero. Por exemplo, você tem apenas uma subvisão que é uma UIView com quadro (50, 50, 100, 100). O tamanho do seu conteúdo será (0, 0, 150, 150), não (50, 50, 100, 100).
Evgeny
74

O UIScrollView não conhece a altura do seu conteúdo automaticamente. Você deve calcular a altura e a largura para si mesmo

Faça isso com algo como

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

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

Mas isso só funciona se as visualizações estiverem uma abaixo da outra. Se você tiver uma visualização próxima, precisará adicionar a altura de uma se não quiser definir o conteúdo do scroller maior do que realmente é.

Montenegro
fonte
Você acha que algo assim também funcionaria? CGFloat scrollViewHeight = scrollView.contentSize.height; [scrollView setContentSize: (CGSizeMake (320, scrollViewHeight))]; Aliás, não se esqueça de pedir o tamanho e a altura. scrollViewHeight + = view.frame.size.height
jwerre
A inicialização do scrollViewHeight na altura torna o scroller maior que seu conteúdo. Se você fizer isso, não adicione a altura das visualizações visíveis na altura inicial do conteúdo do scroller. Embora talvez você precise de uma lacuna inicial ou outra coisa.
emenegro
21
Ou apenas:int y = CGRectGetMaxY(((UIView*)[_scrollView.subviews lastObject]).frame); [_scrollView setContentSize:(CGSizeMake(CGRectGetWidth(_scrollView.frame), y))];
Gal
@saintjab, obrigado, eu adicionei-lo como uma resposta formal de :)
Gal
40

Solução se você estiver usando o layout automático:

  • Defina translatesAutoresizingMaskIntoConstraintscomo NOem todas as visualizações envolvidas.

  • Posicione e dimensione sua exibição de rolagem com restrições externas à exibição de rolagem.

  • Use restrições para organizar as subvisões na visualização de rolagem, certificando-se de que as restrições estejam vinculadas às quatro arestas da visualização de rolagem e não se baseiem na visualização de rolagem para obter seu tamanho.

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

Adam Waite
fonte
10
Mas esta é a solução real no mundo em que tudo acontece com o layout automático. Em relação às outras soluções: o que você pensa sério em calcular a altura e a largura de cada visualização?
Fawkes
Obrigado por compartilhar isso! Essa deve ser a resposta aceita.
SePröbläm 26/03
2
Isso não funciona quando você deseja que a altura da visualização de rolagem seja baseada na altura do conteúdo. (por exemplo, um pop-up centralizado na tela em que você não deseja rolar até uma certa quantidade de conteúdo).
LeGom 27/09
Isso não responder à pergunta
Igor Vasilev
Ótima solução. No entanto, eu adicionaria mais um marcador ... "Não restrinja a altura de uma exibição de pilha filho, se ela crescer verticalmente. Apenas fixe-a nas bordas da exibição de rolagem".
Andrew Kirna 19/07/19
35

Eu adicionei isso à resposta de Espuz e JCC. Ele usa a posição y das subvisões e não inclui as barras de rolagem. Editar Usa a parte inferior da subvisualização mais baixa visível.

+ (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
fonte
1
Maravilhoso! Apenas o que eu precisava.
Richard
2
Estou surpreso por um método como este não fazer parte da classe.
João Portela
Por que você fez self.scrollView.showsHorizontalScrollIndicator = NO; self.scrollView.showsVerticalScrollIndicator = NO;no começo e self.scrollView.showsHorizontalScrollIndicator = YES; self.scrollView.showsVerticalScrollIndicator = YES;no final do método?
João Portela
Obrigado richy :) +1 pela resposta.
Shraddha
1
@ richy você está certo Os indicadores de rolagem são subvisões se visíveis, mas mostrar / ocultar a lógica está errado. Você precisa adicionar dois locais de BOOL: restoreHorizontal = NO, restoreVertical = NO. Se mostrarHorizontal, oculte-o, defina restoreHorizontal como YES. O mesmo para vertical. Em seguida, após a inserção de loop for / in, if: if restoreHorizontal, defina para mostrá-lo, o mesmo para vertical. Seu código forçará apenas a mostrar os dois indicadores
imigrante ilegal
13

Aqui está a resposta aceita rapidamente para quem é muito preguiçoso para convertê-lo :)

var contentRect = CGRectZero
for view in self.scrollView.subviews {
    contentRect = CGRectUnion(contentRect, view.frame)
}
self.scrollView.contentSize = contentRect.size
Josh
fonte
e actualizada para Swift3: var contentRect = CGRect.zero para vista em self.scrollView.subviews {contentRect = contentRect.union (view.frame)} self.scrollView.contentSize = contentRect.size
desenhou ..
Isso precisa ser chamado no thread principal? e no didLayoutSubViews?
Maksim Kniazev
8

Aqui está uma adaptação do Swift 3 da resposta de @ leviatan:

EXTENSÃO

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

Muito fácil de usar!

fredericdnd
fonte
Muito útil. Depois de tantas iterações, encontrei esta solução e é perfeita.
Hardik Shah
Encantador! Apenas implementou.
Arvidurs 17/09/17
7

A extensão a seguir seria útil no 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
    }
}

A lógica é a mesma com as respostas dadas. No entanto, omite as visualizações ocultas UIScrollViewe o cálculo é realizado após os indicadores de rolagem serem ocultos.

Além disso, há um parâmetro de função opcional e você pode adicionar um valor de deslocamento passando o parâmetro para a função.

gokhanakkurt
fonte
Esta solução contém muitas suposições. Por que você está mostrando os indicadores de rolagem em um método que define o tamanho do conteúdo?
Kappe 25/03
5

Excelente e melhor solução de @leviathan. Apenas traduzindo para rápido usando a abordagem FP (programação funcional).

self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect(), { 
  CGRectUnion($0, $1.frame) 
}.size
othierry42
fonte
Se você deseja ignorar as visualizações ocultas, pode filtrar as subvisões antes de passar para reduzir.
Andy
Atualizado para Swift 3:self.contentSize = self.subviews.reduce(CGRect(), { $0.union($1.frame) }).size
John Rogers
4

Você pode obter a altura do conteúdo no UIScrollView calculando qual filho "alcança mais longe". Para calcular isso, é necessário levar em consideração a origem Y (início) e a altura do item.

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

Fazendo assim, os itens (subvisões) não precisam ser empilhados diretamente um sob o outro.

paxx
fonte
Você também pode usar este um forro dentro do loop: maxHeight = MAX (maxHeight, child.frame.origin.y + child.frame.size.height) ;)
Thomas CG de Vilhena
3

Eu vim com outra solução baseada na solução da @ Montenegro

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

Basicamente, descobrimos qual elemento está mais abaixo na visualização e adicionamos um preenchimento de 10px na parte inferior

Chris
fonte
3

Como um scrollView pode ter outros scrollViews ou uma árvore inDepth subViews diferente, é preferível executar em profundidade recursivamente. insira a descrição da imagem aqui

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
fonte
2

Ou apenas faça:

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

(Esta solução foi adicionada por mim como um comentário nesta página. Depois de receber 19 votos positivos para esse comentário, decidi adicionar essa solução como uma resposta formal para o benefício da comunidade!)

Garota
fonte
2

Para swift4 usando reduzir:

self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect.zero, {
   return $0.union($1.frame)
}).size
mm282
fonte
Também vale a pena verificar se pelo menos uma subvisualização tem origem == CGPoint.zero ou apenas atribuir a origem de uma subvisualização específica (maior, por exemplo) a zero.
Paul B
Isso funciona apenas para pessoas que colocam seus pontos de vista com CGRect (). Se você estiver usando restrições, isso não está funcionando.
Linus_hologram 5/08/19
1

O tamanho depende do conteúdo carregado dentro dele e das opções de recorte. Se for uma visualização de texto, também depende da disposição, quantas linhas de texto, tamanho da fonte e assim por diante. Quase impossível para você se calcular. A boa notícia é que é calculada após o carregamento da visualização e no viewWillAppear. Antes disso, é tudo desconhecido e o tamanho do conteúdo será igual ao tamanho do quadro. Mas, no método viewWillAppear e depois (como o viewDidAppear), o tamanho do conteúdo será o real.

PapaSmurf
fonte
1

Envolvendo o código de Richy, criei uma classe UIScrollView personalizada que automatiza o redimensionamento completo do conteúdo!

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

Como usar:
Simplesmente importe o arquivo .h para o seu controlador de exibição e declare uma instância do SBScrollView em vez da instância normal do UIScrollView.

AmitP
fonte
2
layoutSubviews parece ser o lugar certo para esse código relacionado ao layout. Mas em um layout UIScrollView, o Subview é chamado toda vez que o deslocamento do conteúdo é atualizado durante a rolagem. E como o tamanho do conteúdo geralmente não muda durante a rolagem, isso é um desperdício.
Sven
Isso é verdade. Uma maneira de evitar essa ineficiência pode ser verificar [self isDragging] e [self is desacelerating] e executar o layout apenas se ambos forem falsos.
Greg Brown
1

Defina o tamanho do conteúdo dinâmico como este.

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

depende realmente do conteúdo: content.frame.height pode dar o que você deseja? Depende se o conteúdo é uma coisa única ou uma coleção de coisas.

Andiih
fonte
0

Também achei a resposta do leviatã para funcionar melhor. No entanto, estava calculando uma altura estranha. Ao percorrer as subvisões, se a visualização de rolagem estiver configurada para mostrar indicadores de rolagem, eles estarão na matriz de subvisões. Nesse caso, a solução é desativar temporariamente os indicadores de rolagem antes de fazer o loop e, em seguida, restabelecer sua configuração de visibilidade anterior.

-(void)adjustContentSizeToFit é um método público em uma subclasse personalizada do 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
fonte
0

Eu acho que essa pode ser uma maneira interessante de atualizar o tamanho da visualização de conteúdo do 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
fonte
0

por que não uma única linha de código?

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