La pagination horizontale UIScrollView comme les tabs Mobile Safari

Mobile Safari vous permet de changer de page en entrant une sorte de vue de pagination horizontale UIScrollView avec un contrôle de page en bas.

J’essaie de reproduire ce comportement particulier où un UIScrollView à défilement horizontal affiche une partie du contenu de la vue suivante.

L’exemple fourni par Apple: PageControl montre comment utiliser un UIScrollView pour la pagination horizontale, mais toutes les vues occupent toute la largeur de l’écran.

Comment puis-je obtenir un UIScrollView pour afficher le contenu de la vue suivante, comme le fait Safari?

Un UIScrollView avec pagination activée s’arrêtera à des multiples de sa largeur (ou hauteur). La première étape consiste à déterminer la largeur de vos pages. Faites-en la largeur de UIScrollView . Ensuite, définissez les tailles de vos sous-vues, quelle que soit leur taille, et définissez leurs centres en fonction des multiples de la largeur de UIScrollView .

Puis, comme vous voulez voir les autres pages, bien sûr, définissez clipsToBounds sur NO comme indiqué par mhjoy. La partie astuce consiste maintenant à la faire défiler lorsque l’utilisateur lance le glisser en dehors de la plage du cadre de UIScrollView . Ma solution (quand je devais le faire très récemment) était la suivante:

Créez une sous-classe UIView (c.-à-d. ClipView ) qui contiendra UIScrollView et ses sous-vues. Essentiellement, il devrait avoir le cadre de ce que vous supposeriez que UIScrollView aurait dans des circonstances normales. Placez le UIScrollView au centre de ClipView . Assurez-vous que les ClipView de clipsToBounds sont définis sur YES si leur largeur est inférieure à celle de leur vue parent. En outre, le ClipView nécessite une référence à UIScrollView .

L’étape finale consiste à remplacer - (UIView *)hitTest:withEvent: à l’intérieur de ClipView .

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

Cela étend essentiellement la zone tactile de UIScrollView au cadre de la vue de son parent, exactement ce dont vous avez besoin.

Une autre option consisterait à sous- UIScrollView et à remplacer sa méthode d’ - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) event , mais vous aurez toujours besoin d’une vue conteneur pour effectuer le découpage. pour retourner YES basé uniquement sur la trame de UIScrollView .

NOTE: Vous devriez également jeter un œil à hitTest: withEvent: modification de Juri Pakaste si vous rencontrez des problèmes avec l’interaction de l’utilisateur de la sous-vue.

La solution ClipView ci-dessus a fonctionné pour moi, mais j’ai dû faire une -[UIView hitTest:withEvent:] différente -[UIView hitTest:withEvent:] . La version d’Ed Marty n’a pas interagi avec les utilisateurs avec les aperçus verticaux que j’ai à l’intérieur de l’horizontal.

La version suivante a fonctionné pour moi:

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

Définissez la taille d’image de scrollView en fonction de la taille de vos pages:

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

Vous pouvez maintenant effectuer un panoramique sur self.view , et le contenu de scrollView défile.
Utilisez également scrollView.clipsToBounds = NO; pour empêcher le découpage du contenu.

Je me suis retrouvé avec le UIScrollView personnalisé moi-même car c’était la méthode la plus rapide et la plus simple qu’il me semblait. Cependant, je n’ai pas vu de code précis si bien pensé que je partagerais. Mes besoins étaient pour un UIScrollView qui avait un petit contenu et par conséquent, le UIScrollView lui-même était petit pour obtenir l’effet de pagination. Comme le post le déclare, vous ne pouvez pas glisser à travers. Mais maintenant tu peux.

Créez une classe CustomScrollView et une sous-classe UIScrollView. Ensuite, il vous suffit d’append ceci dans le fichier .m:

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

Cela vous permet de faire défiler d'un côté à l'autre (horizontal). Ajustez les limites en conséquence pour définir votre zone tactile de glissement / défilement. Prendre plaisir!

J’ai fait une autre implémentation qui peut retourner automatiquement la défilement. Il n’est donc pas nécessaire d’avoir un IBOutlet qui limitera la réutilisation dans le projet.

 - (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; } 

J’ai une autre modification potentiellement utile pour l’implémentation de HitTest ClipView. Je n’ai pas aimé avoir à fournir une référence UIScrollView à ClipView. Mon implémentation ci-dessous vous permet de réutiliser la classe ClipView pour étendre la zone de test de hit et ne pas avoir à fournir de référence.

 - (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; } 

J’ai implémenté la suggestion votée ci-dessus, mais le UICollectionView j’utilisais considérait que tout ce qui sortait du cadre était hors de l’écran. Cela a empêché les cellules voisines de se rendre hors limites lorsque l’utilisateur les faisait défiler, ce qui n’était pas idéal.

J’ai fini par émuler le comportement d’une scrollview en ajoutant la méthode ci-dessous au délégué (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; } 

Cela évite la délégation de l’action par coup, ce qui était également un bonus. UIScrollViewDelegate a une méthode similaire appelée scrollViewWillEndDragging:withVelocity:targetContentOffset: qui pourrait être utilisé pour référencer UITableViews et UIScrollViews.

Activer les événements de prise de vue sur les vues enfants de la vue de défilement tout en prenant en charge la technique de cette question SO. Utilise une référence à la vue de défilement (self.scrollView) pour une meilleure lisibilité.

 - (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; } 

Ajoutez ceci à votre vue enfant pour capturer l'événement tactile:

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

(Ceci est une variante de la solution de user1856273. Nettoyée pour la lisibilité et incorporant le correctif de Bartserk. J'ai pensé à éditer la réponse de user1856273 mais c'était un changement trop important à faire.)

ma version En appuyant sur le bouton situé sur le rouleau – travail =)

 - (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; } 

mais seulement [[self sous-vues] objectAtIndex: 0] doit être un défilement