Paginación horizontal UIScrollView como pestañas de Safari móvil

88

Mobile Safari le permite cambiar de página ingresando una especie de vista de paginación horizontal UIScrollView con un control de página en la parte inferior.

Estoy tratando de replicar este comportamiento particular en el que un UIScrollView desplazable horizontalmente muestra parte del contenido de la siguiente vista.

El ejemplo proporcionado por Apple: PageControl muestra cómo usar un UIScrollView para la paginación horizontal, pero todas las vistas ocupan todo el ancho de la pantalla.

¿Cómo obtengo un UIScrollView para mostrar algún contenido de la siguiente vista como lo hace Safari móvil?

primer respondedor
fuente
2
Solo un pensamiento ... intente hacer que los límites de la vista de desplazamiento sean más pequeños que la pantalla y juegue con que las vistas se muestren correctamente. (y establezca clipsToBounds de la vista de desplazamiento en NO)
mjhoy
Quería tener páginas más grandes que el ancho de uiscrollview (desplazamiento horizontal). ¡Y el pensamiento de mjhoy realmente me ayudó!
Sasho

Respuestas:

263

A UIScrollViewcon la paginación habilitada se detendrá en múltiplos de su ancho (o alto) de marco. Entonces, el primer paso es averiguar qué tan amplias quieres que sean tus páginas. Que sea el ancho del UIScrollView. Luego, establezca los tamaños de su subvista por muy grandes que necesite, y establezca sus centros en base a múltiplos de la UIScrollViewanchura.

Entonces, ya que desea ver las otras páginas, por supuesto, establecidos clipsToBoundsa NOlo mhjoy declaró. La parte del truco ahora es hacer que se desplace cuando el usuario comience a arrastrar fuera del rango del UIScrollViewmarco de. Mi solución (cuando tuve que hacer esto muy recientemente) fue la siguiente:

Cree una UIViewsubclase (es decir ClipView) que contendrá las UIScrollViewsubvistas y sus. Esencialmente, debe tener el marco de lo que supondría UIScrollViewque tendría en circunstancias normales. Coloque el UIScrollViewen el centro del ClipView. Asegúrese de que el ClipView's clipsToBoundsesté configurado como YESsi su ancho es menor que el de su vista principal. Además, ClipViewnecesita una referencia a UIScrollView.

El último paso es anular - (UIView *)hitTest:withEvent:dentro del ClipView.

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

Básicamente, esto expande el área táctil del UIScrollViewal marco de la vista de su padre, exactamente lo que necesita.

Otra opción sería crear una subclase UIScrollViewy anular su - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventmétodo, sin embargo, aún necesitará una vista de contenedor para hacer el recorte, y puede ser difícil determinar cuándo regresar YESbasándose solo en el UIScrollViewmarco de.

NOTA: También debe echar un vistazo a la modificación hitTest: withEvent: de Juri Pakaste si tiene problemas con la interacción del usuario de la subvista.

Ed Marty
fuente
3
Gracias Ed. Fui con una UIScrollViewsubclase para la carga diferida de vistas (en layoutSubviews) y usé a clippingRectpara hacer las pointInside:withEvent:pruebas. Funciona muy bien y no se requiere una vista de contenedor adicional.
ohhorob
1
Gran respuesta. He utilizado una UIScrollViewsubclase como @ohhorob pero con una vista contenedor para el recorte y el retorno [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];de pointInside:withEvent:. Esto funciona muy bien y no hay problemas con la interacción del usuario mencionados en la respuesta de @ JuriPakaste.
cduck
1
Vaya, esta es una solución realmente buena. Sin embargo, solo una cosa, en mi configuración, las páginas que estoy agregando tienen UIButtons. Cuando anulo el método hitTest, no me permite tocar esos botones. Puedo desplazarme, pero no se puede seleccionar ninguna acción dentro de ninguna página.
A Salcedo
8
Para aquellos que se encuentran con esto más recientemente, tengan en cuenta que - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetagregado a UIScrollViewDelegate en iOS5 significa que ya no tienen que pasar por esta palabrería.
Mike Pollard
1
@MikePollard el efecto no es el mismo silencioso, targetContentOffset no encaja bien con esta situación.
Kyle Fang
70

La ClipViewsolución anterior funcionó para mí, pero tuve que hacer una -[UIView hitTest:withEvent:]implementación diferente . La versión de Ed Marty no logró que la interacción del usuario funcionara con las vistas de desplazamiento verticales que tengo dentro de la horizontal.

La siguiente versión funcionó para mí:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}
Juri Pakaste
fuente
Esto también ayuda a que funcionen los botones y otras subvistas con la interacción del usuario. :) gracias
Thomas Clayson
4
Gracias por este código, ¿cómo puedo usar esta modificación para obtener eventos de subvistas (botones) que no se encuentran en los límites de la vista de desplazamiento? Funciona si un botón, por ejemplo, está dentro de los límites de la vista de desplazamiento central pero no fuera.
DreamOfMirrors
4
Esto realmente ayudó. Debe incluirse como una modificación de la respuesta seleccionada.
Pacu
2
¡Si! ¡Esto también hace que la interacción del usuario dentro de la vista de desplazamiento funcione nuevamente! Tuve que configurar userInteractionEnabled en YES en ClipView para que esto funcione.
Tom van Zummeren
Tuve que iterar a través de self.scrollView.subviews y realizar primero hitTest.
Bao Lei
8

Establezca el tamaño del marco de scrollView como el tamaño de sus páginas:

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

Ahora puede desplazarse self.viewy el contenido de scrollView se desplazará.
También utilícelo scrollView.clipsToBounds = NO;para evitar recortar el contenido.

Nikolay Tabunchenko
fuente
5

Terminé usando el UIScrollView personalizado yo mismo, ya que era el método más rápido y simple que me parecía. Sin embargo, no vi ningún código exacto, así que pensé que lo compartiría. Mis necesidades eran un UIScrollView que tuviera un contenido pequeño y, por lo tanto, el UIScrollView en sí era pequeño para lograr el efecto de paginación. Como dice la publicación, no se puede deslizar. Pero ahora puedes.

Cree una clase CustomScrollView y una subclase UIScrollView. Luego, todo lo que necesita hacer es agregar esto al archivo .m:

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

Esto le permite desplazarse de un lado a otro (horizontal). Ajuste los límites en consecuencia para establecer su área táctil de deslizamiento / desplazamiento. ¡Disfrutar!

djneely
fuente
4

He realizado otra implementación que puede devolver la vista de desplazamiento automáticamente. Por lo tanto, no necesita tener un IBOutlet que limitará la reutilización en el proyecto.

- (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;
}
alvinhu
fuente
1
Este es el que debe usar si ya tiene un xib / storyboard. De lo contrario, no estoy seguro de cómo obtendría la referencia a Paging / Scrollview. ¿Algunas ideas?
mskw
3

Tengo otra modificación potencialmente útil para la implementación de ClipView hitTest. No me gustó tener que proporcionar una referencia UIScrollView a ClipView. Mi implementación a continuación le permite reutilizar la clase ClipView para expandir el área de prueba de impacto de cualquier cosa y no tener que proporcionarle una referencia.

- (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;
}
Rod Reddekopp
fuente
3

Implementé la sugerencia de voto positivo anterior, pero el UICollectionViewque estaba usando consideró que cualquier cosa fuera del marco fuera de la pantalla. Esto hizo que las celdas cercanas solo se mostraran fuera de los límites cuando el usuario se desplazaba hacia ellas, lo que no era ideal.

Lo que terminé haciendo fue emular el comportamiento de una vista de desplazamiento agregando el método a continuación al delegado (o 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;
}

Esto evita la delegación de la acción de deslizar por completo, lo que también fue una ventaja. El UIScrollViewDelegatetiene un método similar llamado scrollViewWillEndDragging:withVelocity:targetContentOffset:que podría ser utilizado para UITableViews de página y UIScrollViews.

Kyle
fuente
2
Dave, estaba luchando por lograr tal comportamiento usando UICollectionView también y terminé usando el mismo enfoque que usted describe aquí. Sin embargo, la experiencia de desplazamiento no es tan buena como lo era una vista de desplazamiento con la paginación habilitada. Cuando pasé el dedo con mayor velocidad, se desplazaron varias páginas y se detuvo en la página propuesta. Lo que quiero lograr es guardar el comportamiento como vista de desplazamiento normal con la paginación habilitada; sea cual sea la velocidad que use, obtendré solo 1 página más. ¿Tienes idea de cómo hacerlo?
Peter Stajger
3

Habilite el disparo de eventos de toque en las vistas secundarias de la vista de desplazamiento mientras apoya la técnica de esta pregunta SO. Utiliza una referencia a la vista de desplazamiento (self.scrollView) para facilitar la lectura.

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

Agregue esto a la vista de su hijo para capturar el evento táctil:

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

(Esta es una variación de la solución de user1856273. Se limpió para facilitar la lectura e incorporó la corrección de errores de Bartserk. Pensé en editar la respuesta de user1856273, pero era un cambio demasiado grande para hacer).

David James
fuente
Para que el tapping funcione en un UIButton incrustado en la subvista, reemplacé el código hitView = [childViews objectAtIndex: i]; con // busque el UIButton para (UIView * paneView en [[childViews objectAtIndex: i] subvistas]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA
2

mi versión Presionando el botón que se encuentra en el pergamino - trabajo =)

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

pero solo [[self subviews] objectAtIndex: 0] debe ser un desplazamiento

Kolbasek
fuente
Esto resolvió mi problema :) Solo tenga en cuenta que no está agregando el desplazamiento de desplazamiento a point.x cuando está comprobando los límites, y esto hace que el código no funcione bien en los desplazamientos horizontales. Después de agregar eso, ¡funcionó bien!
Bartserk
2

Aquí hay una respuesta rápida basada en la respuesta de Ed Marty, pero también incluye la modificación de Juri Pakaste para permitir toques de botones, etc.dentro de la vista de desplazamiento.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}
Ben Packard
fuente
Gracias por actualizar esto :)
Liam Bolling