Arrastrar vistas en una vista de desplazamiento: se recibe touchBegan, pero no toca Fineado o toca Cancelado

Como novato en la progtwigción de iOS, estoy luchando con un juego de palabras para iPhone .

La estructura de la aplicación es: scrollView -> contentView -> imageView -> image 1000 x 1000 (aquí en pantalla completa ):

Xcode captura de pantalla

Creo que finalmente entendí cómo usar un UIScrollView con Auto Layout habilitado en Xcode 5.1:

Acabo de especificar suficientes restricciones (dimensiones de 1000 x 1000 y también 0 para el padre) para el contenido de contentView y esto define el _scrollView.contentSize (no tengo que configurarlo explícitamente), después de que mi tablero de juego se desplaza y zoom bien.

Sin embargo, tengo problemas con mis baldosas arrastrables implementadas en Tile.m.

Uso touchesBegan, touchesMoved, touchesEnded, touchesCancelled y no reconocedores de gestos (como lo sugieren a menudo los usuarios de StackOverflow), porque bigImage image de mosaico de letras más grande con sombra (the bigImage ) en touchesBegan .

Mi arrastre se implementa de la siguiente manera:

  1. En touchesBegan , touchesBegan el mosaico de contentView (y lo agrego a la vista principal de la aplicación) y bigImage con sombra.
  2. En los touchesMoved muevo los azulejos
  3. En touchesEnded o touchesCancelled muestra smallImage una image smallImage con sombra y agrega la ficha a la contentView o contentView en la vista principal (si la ficha está en la parte inferior de la aplicación).

Mi problema:

Sobre todo esto funciona, pero a veces (a menudo) veo que solo se llama a touchesBegan , pero los otros methods touchesXXXX nunca se llaman:

 2014-03-22 20:20:20.244 ScrollContent[8075:60b] -[Tile touchesBegan:withEvent:]: Tile J 10 {367.15002, 350.98877} {57.599998, 57.599998} 

En cambio, el scrollView se desplaza por el dedo, debajo del azulejo grande.

Esto da como resultado muchos azulejos grandes con sombras en la pantalla de mi aplicación, mientras que la vista de desplazamiento se arrastra debajo de ellos:

problema de captura de pantalla

¿Cómo solucionar esto por favor?

Sé con certeza que mi estructura de la aplicación (con UIView personalizadas arrastrada hacia adentro / afuera de un UIScrollView ) es posible – mirando juegos de palabras populares.

Utilizo tile.exclusiveTouch = YES y un método personalizado hitTest para el contenidoView , pero esto no ayuda:

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

ACTUALIZACIÓN 1:

He intentado agregar el siguiente código para handleTileTouched :

 _contentView.userInteractionEnabled = NO; _scrollView.userInteractionEnabled = NO; _scrollView.scrollEnabled = NO; 

y luego volver a establecerlo en YES en handleTileReleased de ViewController.m , pero esto no ayuda y también se parece más a un hack para mí.

ACTUALIZACIÓN 2:

Después de haber leído probablemente todo lo relacionado con UIScrollView , hitTest:withEvent: y pointInside:withEvent: – en la web (por ejemplo, piratería de la cadena de respuesta y el libro de iOS de Progtwigción de Matt Neuburg), StackOverflow y Safari, me parece que una solución ser para implementar el hitTest:withEvent: método para la vista principal de mi aplicación:

Si se golpea un object Tile , debería devolverse. De lo contrario, se debe devolver scrollView .

Desafortunadamente, esto no funciona, probablemente me falte algo menor.

Y estoy seguro de que existe una buena solución, estudiando juegos de palabras populares para iOS. Por ejemplo, arrastrar y colocar mosaicos de letras funciona muy bien en la aplicación Zynga's Words with Friends® y en las capturas de pantalla a continuación puede verlos probablemente usando UIScrollView (las barras de desplazamiento son visibles en la esquina) y mostrar una sombra de mosaico (probablemente en touchesBegan método de touchesBegan ):

app screenshot

ACTUALIZACIÓN 3:

He creado un nuevo proyecto para probar el reconocimiento de gestos sugerido por TomSwift y muestra el problema que tengo con los reconocedores de gestos: el tamaño del mosaico cambia demasiado tarde, sucede cuando el usuario comienza a mover el mosaico y no en el momento en que lo toca :

app screenshot

El problema aquí es que eliminar una vista de la jerarquía de vista confunde el sistema, el toque se pierde. Es el mismo problema (los reconocedores de gestos internos usan los mismos touchesBegan: API).

https://github.com/LeoNatan/ios-newbie/commit/4cb13ea405d9f959f4d438d08638e1703d6c0c1e (creé una request de extracción)

Lo que cambié fue no eliminar el mosaico de la vista de contenido cuando los toques comienzan, pero solo se mueven al tocar los extremos o cancelar. Pero esto crea un problema: al arrastrarlo al background, el mosaico se oculta debajo de la vista (debido al recorte de la vista de desplazamiento a sus límites). Así que creé un mosaico clonado, lo agregué como una subvista de la vista del controller de vista y lo muevo junto con el mosaico original. Cuando toque, elimino el mosaico clonado y coloco el original donde debería ir.

Esto se debe a que la barra inferior no es parte de la jerarquía de scrollview. Si lo fuera, no sería necesario el clonado completo.

También optimicé bastante el posicionamiento de los azulejos.

podría configurar userInteractionEnabled de la vista de desplazamiento en NO mientras arrastra el mosaico y volverlo a colocar en YES cuando userInteractionEnabled el userInteractionEnabled de mosaico.

Realmente debería tratar de usar un reconocimiento de gestos en lugar de los toques sin procesar de Began / touchesMoved. Digo esto porque UIScrollView está utilizando reconocedores de gestos y, por defecto, ceden a cualquier gestor de reconocimiento de nivel superior que se esté ejecutando.

Elaboré una muestra que tiene un UIScrollView con un UIImageView integrado. Al igual que con su captura de pantalla, debajo del scrollView tengo algunos UIB "Tiles", que subclaqué como objects TSTile. La única razón por la que hice esto fue exponer algunas NSLayoutConstraints para acceder / alterar su alto / ancho (ya que está usando la disposition automática frente a la manipulación de ttwigs). El usuario puede arrastrar fichas desde su lugar de inicio a la vista de desplazamiento.

Esto parece funcionar bien; No conecté la capacidad de arrastrar un mosaico una vez que se volvió a generar en la vista de desplazamiento. Pero eso no debería ser muy difícil. Para eso, podría considerar colocar un reconocimiento de gesto de pulsación larga en cada mosaico, luego, cuando se dispara, se desactivaría el desplazamiento en la vista de desplazamiento, de modo que el reconocedor de gestos de nivel superior entrara en acción.

O bien, puede subclasificar UIScrollView e interceptar las devoluciones de llamada delegadas de delegado de gestos-reconocimiento de UIScrollView para impedir el desplazamiento cuando el usuario parte de un mosaico.

 @interface TSTile : UIButton //$hook these up to width/height constraints in your storyboard! @property (nonatomic, readonly) IBOutlet NSLayoutConstraint* widthConstraint; @property (nonatomic, readonly) IBOutlet NSLayoutConstraint* heightConstraint; @end @implementation TSTile @synthesize widthConstraint,heightConstraint; @end @interface TSViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate> @end @implementation TSViewController { IBOutlet UIImageView* _imageView; TSTile* _dragTile; } - (void)viewDidLoad { [super viewDidLoad]; UIPanGestureRecognizer* pgr = [[UIPanGestureRecognizer alloc] initWithTarget: self action: @selector( pan: )]; pgr.delegate = self; [self.view addGestureRecognizer: pgr]; } - (UIView*) viewForZoomingInScrollView:(UIScrollView *)scrollView { return _imageView; } - (BOOL) gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { CGPoint pt = [gestureRecognizer locationInView: self.view]; UIView* v = [self.view hitTest: pt withEvent: nil]; return [v isKindOfClass: [TSTile class]]; } - (void) pan: (UIGestureRecognizer*) gestureRecognizer { CGPoint pt = [gestureRecognizer locationInView: self.view]; switch ( gestureRecognizer.state ) { case UIGestureRecognizerStateBegan: { NSLog( @"pan start!" ); _dragTile = (TSTile*)[self.view hitTest: pt withEvent: nil]; [UIView transitionWithView: self.view duration: 0.4 options: UIViewAnimationOptionAllowAnimatedContent animations:^{ _dragTile.widthConstraint.constant = 70; _dragTile.heightConstraint.constant = 70; [self.view layoutIfNeeded]; } completion: nil]; } break; case UIGestureRecognizerStateChanged: { NSLog( @"pan!" ); _dragTile.center = pt; } break; case UIGestureRecognizerStateEnded: { NSLog( @"pan ended!" ); pt = [gestureRecognizer locationInView: _imageView]; // reparent: [_dragTile removeFromSuperview]; [_imageView addSubview: _dragTile]; // animate: [UIView transitionWithView: self.view duration: 0.25 options: UIViewAnimationOptionAllowAnimatedContent animations:^{ _dragTile.widthConstraint.constant = 40; _dragTile.heightConstraint.constant = 40; _dragTile.center = pt; [self.view layoutIfNeeded]; } completion:^(BOOL finished) { _dragTile = nil; }]; } break; default: NSLog( @"pan other!" ); break; } } @end 

También creo que deberías usar un UIGestureRecognizer , y más precisamente un UILongPressGestureRecognizer en cada mosaico que una vez reconocido manejará pan.

Para el control de grano fino, aún puede usar el delegate los reconocedores.