UIScrollView ao resgate, não tão rápido…
Para entender como uma scroll view funciona, precisamos entender o que significam 3 propriedades: contentOffset, contentSize e contentInset.
A cada ano aumenta a variedade de dispositivos iOS↳iOS38 conteúdosO X do Xamarin Forms – O guia das funcionalidades nativas – Parte 01: iOSDev (Back & Front) · abr 2019Desenvolvedores: Apple libera terceiro beta do iOS 12.4Dev (Back & Front) · mai 201910 bibliotecas que todo desenvolvedor iOS deve substituir ainda em 2024Dev (Back & Front) · out 2024Ver tudo em Dev (Back & Front) → com tamanhos de telas diferentes e assim o Auto Layout se torna cada vez mais importante no desenvolvimento. Já usei muito autoresizingMask e calculei muito frame na mão, mas tenho apreciado cada vez mais o Auto Layout e tenho feito dele minha principal ferramenta de layout. Entretanto, essa semana me deparei com um problema imune ao Auto Layout – talvez devido minha falta de habilidade.
A ideia é ter uma View2 que pertence a um view controller 2 dentro de uma View1, pertencente a um view controller 1. A View2 pode ter um tamanho arbitrário definido pelo view controller 1. A View2 irá conter uma imagem que deve ser centralizada. O tamanho dessa imagem é arbitrário e deve ser redimensionado de maneira que a distância do lado maior mais próximo da borda seja de x pontos.
Até aí nada de complicado, mas também é necessário que seja possível fazer zoom e scroll da imagem e a margem de x pontos seja mantida independente da ampliação.
Bom, nossa boa e velha amiga UIScrollView parece ser uma ótima candidata para salvar o dia, mas para isso, precisamos entender melhor com ela funciona. Uma ótima referência é um artigo da edição sobre views do objc.io, Understanding Scroll Views. Vou resumir alguns conceitos básicos e colocar um pouco da minha visão, mas o recomendo a leitura, assim como dos outros artigos dessa edição sobre views. Para entender como uma scroll view funciona, precisamos entender o que significam 3 propriedades: contentOffset, contentSize e contentInset.
contentOffsetO contentOffset define a posição do scroll, isto é, o deslocamento das sub views da scroll view. Na prática, ela é a origin do bounds da scroll view, mas alguém poderia perguntar: a origem não é sempre {0,0}? Nem sempre. Um ponto qualquer ({xS,yS}) de uma subView é convertido para o sistema de coordenadas ({x,y}) de sua super view (view) da seguinte forma:
x = xS + subView.frame.origin.x + view.bounds.origin.x y = yS + subView.frame.origin.y + view.bounds.origin.y
Como normalmente view.bounds.origin = {0,0} para calcular a posição de um ponto qualquer da subView na view é só somar a origem do frame da subView. Isso significa que quando mudamos a origin do bounds de uma view, todas as suas sub views vão ser deslocadas pela mesma quantidade. Truque maroto!
Se consideramos que a UIImageView tem origin = {0,0}, o contentOffset é a distância entre o canto superior esquerdo da image view e o da scroll view, como ilustrado na figura acima.
contentSizeÉ o tamanho do conteúdo apresentado. No caso da figura, o contentSize é igual o frame.size da image view. Num caso geral, ele só depende do tamanho e disposição das sub views; nunca da scroll view.
contentInsetUsado para definir uma margem para apresentação do conteúdo, por padrão, seu valor é {0,0,0,0}, e portanto o tamanho da área que pode apresentar conteúdo é igual à scrollView.frame.
Quando o contentSize for menor que o tamanho da scroll view, isso significa que as sub views irão ficar no canto superior da esquerda, fixas. Uma maneira de centralizar o conteúdo é colocar um inset como metade da diferença de tamanho entre a scroll view e o contentSize:
GFloat xInset = (CGRectGetWidth(scrollView.frame) - scrollView.contentSize.width)/2.; CGFloat yInset = (CGRectGetHeight(scrollView.frame) - scrollView.contentSize.height)/2.; scrollView.contentInset = UIEdgeInsetsMake(yInset, xInset, yInset, xInset);
Quando o contentSize for maior que que a scroll view, o contentInset define os limites máximos de scroll. Por exemplo no caso da UIImageView que tem a frame.origin = {0,0}, os limites da UIImageView não podem “entrar” na área definida pelo frame da scrool view descontado o contentInset. A figura abaixo deve deixar isso mais claro:
Ok, agora fica fácil escrever o código que adiciona uma imagem à uma scroll view e define essas propriedades corretamente:
- (void)updateWithImage:(UIImage *)image
{
UIScrollView *scrollView = self.scrollView;
UIImageView *imageView = self.imageView;
imageView.image = image;
CGSize imageSize = image.size;
CGRect imageFrame = CGRectMake(0, 0, imageSize.width, imageSize.height);
imageView.frame = imageFrame;
scrollView.contentSize = imageSize;
CGRect scrollViewFrame = scrollView.frame;
// Set Inset
UIEdgeInsets insets = self.scrollViewDefaultInset;
insets = [self insetsForContentFrame:imageFrame
insideScrollViewWithFrame:scrollViewFrame
withDefaultInsets:insets];
scrollView.contentInset = insets;
// Set Zoom
CGSize scrollViewSize = CGSizeMake(CGRectGetWidth(scrollViewFrame) - insets.left - insets.right,
CGRectGetHeight(scrollViewFrame) - insets.top - insets.bottom);
CGFloat xMinZoomScale = scrollViewSize.width/(imageSize.width + 2. * kMargin);
CGFloat yMinZoomScale = scrollViewSize.height/(imageSize.height + 2. * kMargin);
CGFloat minimumZoomScale = MIN(xMinZoomScale, yMinZoomScale);
scrollView.minimumZoomScale = minimumZoomScale;
scrollView.maximumZoomScale = minimumZoomScale * kMaxZoomFactor;
// Fit on screen
scrollView.zoomScale = minimumZoomScale;
}
Sendo que função que calcula os insets é:
- (UIEdgeInsets)insetsForContentFrame:(CGRect)contentFrame
insideScrollViewWithFrame:(CGRect)scrollViewFrame
withDefaultInsets:(UIEdgeInsets)insets
{
CGSize contentSize = contentFrame.size;
CGSize scrollViewSize = CGSizeMake(CGRectGetWidth(scrollViewFrame) - insets.left - insets.right,
CGRectGetHeight(scrollViewFrame) - insets.top - insets.bottom);
CGFloat margin = kMargin;
CGFloat xInset = MAX((scrollViewSize.width - contentSize.width)/2., margin);
CGFloat yInset = MAX((scrollViewSize.height - contentSize.height)/2., margin);
insets.left += xInset;
insets.right += xInset;
insets.top += yInset;
insets.bottom += yInset;
return insets;
}
Para habilitar o zoom só falta implementar um método do UIScrollViewDelegate:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
Esse método apenas retorna qual a view que será aplicado o zoom. Da primeira vez que vi achei muito estranho, mas entender como a UIScrollView faz o zoom tudo ficou muito mais claro.
Bônus: zoomScaleA UIScrollView faz zoom aplicando uma transformação na view retornada pelo método do delagate descrito acima. Isto é, aplica uma CGAffineTransform do tipo CGAffineTransformMakeScale(zoomScale, zoomScale) na subview. Isso faz com que o frame da subView seja alterado! E portanto, o contentSize da scrollView; por isso sempre que ocontentSize e, consequentemente, o zoomScale forem alterados, o contentInset deve ser recalculado. Isso pode ser feito facilmente implementando mais um método do delegate:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
UIEdgeInsets insets = self.scrollViewDefaultInset;
insets = [self insetsForContentFrame:self.imageView.frame
insideScrollViewWithFrame:scrollView.frame
withDefaultInsets:insets];
scrollView.contentInset = insets;
}
A UIScrollView é uma classe muito importante no UIKit. Seu funcionamento é muito simples, mas entender como ela funciona exatamente pode não ser uma tarefa muito simples.
Um exemplo dessa solução funcionando pode ser encontrada no repositório UIScrollView-Center. Qualquer dúvida, críticas e comentários são bem vindos, a maneira mais fácil de me encontrar é no Twitter.










