在滚动视图上拖动视图:touchchesBegan已收到,但没有touchesEnded或touchesCancelled

作为iOS编程新手,我正在为iPhone的文字游戏而苦苦挣扎。

应用程序结构是: scrollView -> contentView -> imageView -> image 1000 x 1000 (这里是全屏 ):

Xcode截图

我想我终于明白了如何使用在Xcode 5.1中启用自动布局的UIScrollView

我只是为contentView指定了足够的约束(尺寸1000 x 1000以及0到父级),这定义了_scrollView.contentSize (我没有明确地设置它) – 之后我的游戏板滚动和缩放就好了。

但是,我在Tile.m中实现了可拖动的字母拼图我遇到了麻烦 。

我使用touchesBegan, touchesMoved, touchesEnded, touchesCancelled而不是手势识别器(通常由StackOverflow用户建议),因为我在touchesBegan上显示带阴影( bigImage )的较大字母图块图像。

我的拖动按以下方式实现:

  1. touchesBegan我从contentView删除了tile(并将其添加到主app视图中)并显示带阴影的bigImage
  2. touchesMoved我移动瓷砖
  3. touchesEndedtouchesCancelled我再次显示带阴影的smallImage – 将图块添加到contentView或将其保留在主视图中(如果图块位于应用程序的底部)。

我的问题:

大多数情况下这是有效的,但有时(通常)我看到只调用了touchesBegan ,但其他touchesXXXX方法从未被调用:

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

相反, scrollView在大瓷砖下方由手指滚动。

这会导致许多大瓷砖的阴影位于我的应用程序的屏幕上,而滚动视图正在其下方拖动:

问题截图

如何解决这个问题?

我确信我的应用程序结构(可以通过UIScrollView拖入/移出自定义UIView ) – 通过查看流行的文字游戏。

我使用tile.exclusiveTouch = YES和contentView的自定义hitTest方法 – 但这没有帮助:

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

更新1:

我已经尝试将以下代码添加到handleTileTouched

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

然后在ViewController.m的 handleTileReleased中将其设置回YES – 但这没有帮助,看起来更像是对我的黑客攻击。

更新2:

阅读可能与UIScrollView相关的所有东西, hitTest:withEvent:pointInside:withEvent: – 在网上(例如, 黑客攻击链和Matt Neuburg的编程iOS书),StackOverflow和Safari,在我看来,解决方案是是为我的应用程序的主视图实现hitTest:withEvent:方法:

如果击中了Tile对象,则应返回该对象。 否则 – 应返回scrollView

不幸的是,这不起作用 – 我可能遗漏了一些小问题。

我确信存在一个很好的解决方案 – 通过研究iOS的流行文字游戏。 例如,在Zynga的Words withFriends®应用程序中拖动和放置字母拼贴非常流畅,在下面的屏幕截图中,您可以看到它们可能使用UIScrollView (滚动条在角落中可见)并显示拼贴阴影(可能在touchesBegan方法中) ):

应用截图

更新3:

我已经创建了一个新项目来测试TomSwift建议的手势识别器,它显示了我对手势识别器的问题:磁贴大小变化太晚 – 当用户开始移动磁贴时发生,而不是在他触摸它时:

应用截图

这里的问题是从视图层次结构中删除视图会使系统混乱,触摸会丢失。 这是同一个问题(内部手势识别器使用相同的touchesBegan: API)。

https://github.com/LeoNatan/ios-newbie/commit/4cb13ea405d9f959f4d438d08638e1703d6c0c1e (我创建了一个拉取请求。)

我改变的是在触摸开始时不从内容视图中移除图块,而是仅在触摸结束时移动或取消。 但这会产生一个问题 – 当拖动到底部时,图块隐藏在视图下方(由于滚动视图剪切到其边界)。 所以我创建了一个克隆的tile,将其添加为视图控制器视图的子视图,并将其与原始tile一起移动。 当触摸结束时,我删除克隆的瓷砖并将原件放在应该去的地方。

这是因为底栏不是scrollview层次结构的一部分。 如果是,则不需要整个瓷砖克隆。

我也简化了瓷砖的定位。

您可以在拖动切片时将scrollview的userInteractionEnabled设置为NO ,并在切片拖动结束时将其设置回YES

您应该尝试使用手势识别器而不是原始的touchesBegan / touchesMoved。 我之所以这么说是因为UIScrollView正在使用手势识别器,默认情况下这些将放弃到正在运行的任何更高级别的手势识别器。

我把一个带有UIScrollView和嵌入式UIImageView的示例放在一起。 与你的截图一样,在scrollView下面我有一些UIButton“Tiles”,我将其子类化为TSTile对象。 我这样做的唯一原因是暴露一些NSLayoutConstraints来访问/改变它们的高度/宽度(因为你使用的是自动布局与帧操作)。 用户可以将瓷砖从其起始位置拖动到滚动视图中。

这似乎运作良好; 一旦在scrollview中重新设置了父级,我就没有能够拖动它。 但这不应该太难。 为此,您可以考虑在每个磁贴中放置一个长按手势识别器,然后当它触发时,您将关闭滚动视图中的滚动,这样顶级平移手势识别器就会启动。

或者,您可能能够子类化UIScrollView并拦截UIScrollView的pan-gesture-recognitionizer委托回调以阻止用户从磁贴启动时的平移。

 @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 ()  @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 

我还认为你应该使用UIGestureRecognizer ,更准确地说,每个瓷砖上的UILongPressGestureRecognizer一旦被识别就会处理pan。

对于细粒度控制,您仍然可以使用识别器的delegate