Paginação horizontal do UIScrollView como guias do Mobile Safari

O Mobile Safari permite alternar entre páginas, inserindo uma espécie de exibição de paginação horizontal do UIScrollView com um controle de página na parte inferior.

Eu estou tentando replicar esse comportamento específico onde um UIScrollView horizontalmente rolável mostra alguns dos conteúdos da próxima visão.

O exemplo fornecido pela Apple: PageControl mostra como usar um UIScrollView para paginação horizontal, mas todas as exibições ocupam toda a largura da canvas.

Como obtenho um UIScrollView para mostrar algum conteúdo da próxima visualização, como o Safari móvel faz?

Um UIScrollView com paginação ativada será interrompido em múltiplos de sua largura de quadro (ou altura). Então o primeiro passo é descobrir o quão amplo você quer que suas páginas sejam. Faça com que a largura do UIScrollView . Em seguida, defina os tamanhos da sua subvisualização, por maior que você precise, e defina seus centros com base em múltiplos da largura do UIScrollView .

Então, como você quer ver as outras páginas, é claro, defina clipsToBounds como NO como mhjoy afirmou. O truque agora é fazê-lo rolar quando o usuário inicia o arrasto fora do alcance do quadro do UIScrollView . Minha solução (quando tive que fazer isso muito recentemente) foi a seguinte:

Crie uma subclass UIView (ou seja, ClipView ) que conterá o UIScrollView e suas subvisualizações. Essencialmente, deve ter o quadro do que você assumiria que o UIScrollView teria em circunstâncias normais. Coloque o UIScrollView no centro do ClipView . Certifique-se de que ClipView do clipsToBounds esteja definido como YES se sua largura for menor que a de sua exibição pai. Além disso, o ClipView precisa de uma referência ao UIScrollView .

A etapa final é replace - (UIView *)hitTest:withEvent: dentro do ClipView .

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { return [self pointInside:point withEvent:event] ? scrollView : nil; } 

Isso basicamente expande a área de toque do UIScrollView para o quadro de visão de seus pais, exatamente o que você precisa.

Outra opção seria subclassificar o UIScrollView e replace seu método de - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) event , no entanto, você ainda precisará de uma exibição de contêiner para fazer o recorte, e pode ser difícil determinar quando para retornar YES base apenas no quadro do UIScrollView .

NOTA: Você também deve dar uma olhada no hitTest: withEvent: modification do Juri Pakaste: se você está tendo problemas com a interação do usuário subview.

A solução ClipView acima funcionou para mim, mas eu tive que fazer uma implementação diferente -[UIView hitTest:withEvent:] . A versão de Ed Marty não conseguiu interação do usuário trabalhando com scrollviews verticais que eu tenho dentro da horizontal.

A seguinte versão funcionou para mim:

 -(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event { UIView* child = nil; if ((child = [super hitTest:point withEvent:event]) == self) return self.scrollView; return child; } 

Defina o tamanho do quadro de scrollView, pois o tamanho de suas páginas seria:

 [self.view addSubview:scrollView]; [self.view addGestureRecognizer:mainScrollView.panGestureRecognizer]; 

Agora você pode self.view pan em self.view e o conteúdo em scrollView será rolado.
Use também scrollView.clipsToBounds = NO; para impedir o recorte do conteúdo.

Eu acabei indo com o UIScrollView personalizado como era o método mais rápido e simples que me pareceu. No entanto, eu não vi nenhum código exato, então pensei em compartilhar. Minhas necessidades eram para um UIScrollView que tinha pequeno conteúdo e, portanto, o próprio UIScrollView era pequeno para atingir o efeito de paginação. Como diz a postagem, você não pode passar. Mas agora você pode.

Crie uma class CustomScrollView e subclass UIScrollView. Então tudo o que você precisa fazer é include isso no arquivo .m:

 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return (point.y >= 0 && point.y <= self.frame.size.height); } 

Isso permite rolar de um lado para o outro (horizontal). Ajuste os limites de acordo para definir a área de toque / rolagem. Apreciar!

Eu fiz outra implementação que pode retornar o scrollview automaticamente. Portanto, não é necessário ter um IBOutlet que limite a reutilização no projeto.

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if ([self pointInside:point withEvent:event]) { for (id view in [self subviews]) { if ([view isKindOfClass:[UIScrollView class]]) { return view; } } } return nil; } 

Eu tenho outra modificação potencialmente útil para a implementação de hitTest ClipView. Não gostei de ter que fornecer uma referência ao UIScrollView para o ClipView. Minha implementação abaixo permite que você reutilize a class ClipView para expandir a área de teste de ocorrência de qualquer coisa, e não precisa fornecer uma referência.

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1)) { // An extended-hit view should only have one sub-view, or make sure the // first subview is the one you want to expand the hit test for. return [self.subviews objectAtIndex:0]; } return nil; } 

Eu implementei a sugestão acima votada acima, mas o UICollectionView eu estava usando considerou qualquer coisa fora do quadro para ficar fora da canvas. Isso fazia com que as células próximas se processassem apenas quando o usuário estava rolando na direção delas, o que não era ideal.

O que acabei fazendo foi emular o comportamento de um scrollview, adicionando o método abaixo ao delegado (ou UICollectionViewLayout ).

 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { if (velocity.x > 0) { proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize; } else { proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize; } return proposedContentOffset; } 

Isso evita a delegação da ação de furto totalmente, o que também foi um bônus. O UIScrollViewDelegate tem um método semelhante chamado scrollViewWillEndDragging:withVelocity:targetContentOffset: que pode ser usado para a página UITableViews e UIScrollViews.

Ative a ativação de events de toque em exibições secundárias da visualização de rolagem enquanto oferece suporte à técnica dessa pergunta SO. Usa uma referência para a exibição de rolagem (self.scrollView) para legibilidade.

 - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitView = nil; NSArray *childViews = [self.scrollView subviews]; for (NSUInteger i = 0; i < childViews.count; i++) { CGRect childFrame = [[childViews objectAtIndex:i] frame]; CGRect scrollFrame = self.scrollView.frame; CGPoint contentOffset = self.scrollView.contentOffset; if (childFrame.origin.x + scrollFrame.origin.x < point.x + contentOffset.x && point.x + contentOffset.x < childFrame.origin.x + scrollFrame.origin.x + childFrame.size.width && childFrame.origin.y + scrollFrame.origin.y < point.y + contentOffset.y && point.y + contentOffset.y < childFrame.origin.y + scrollFrame.origin.y + childFrame.size.height ){ hitView = [childViews objectAtIndex:i]; return hitView; } } hitView = [super hitTest:point withEvent:event]; if (hitView == self) return self.scrollView; return hitView; } 

Adicione isto à sua visão infantil para capturar o evento de toque:

 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 

(Esta é uma variação da solução do user1856273. Limpada para facilitar a leitura e incorporada na correção de bug do Bartserk. Pensei em editar a resposta do user1856273, mas foi uma mudança muito grande para ser feita.)

minha versão Pressionando o botão deitado no pergaminho – trabalho =)

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView* child = nil; for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) { CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame]; CGRect myframeS =[[[self subviews] objectAtIndex:0] frame]; CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset]; if (myframe.origin.x < point.x && point.x < myframe.origin.x+myframe.size.width && myframe.origin.y+myframeS.origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.origin.y+myframeS.origin.y +myframe.size.height){ child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i]; return child; } } child = [super hitTest:point withEvent:event]; if (child == self) return [[self subviews] objectAtIndex:0]; return child; } 

mas somente [[self subviews] objectAtIndex: 0] deve ser um scroll