Перетаскивание представлений в представлении прокрутки: touchesBegan получен, но не touchesEnded или touchesCancelled

Как новичок в программировании iOS, я борюсь с игрой в слова для iPhone .

Структура приложения: scrollView -> contentView -> imageView -> image 1000 x 1000 (здесь полноэкранный режим):

Скриншот Xcode

Я думаю, что наконец-то понял, как использовать UIScrollView с включенным автоматическим макетом в Xcode 5.1:

Я просто указываю достаточно ограничений (размеры 1000 x 1000, а также 0 для родителя) для contentView, и это определяет _scrollView.contentSize (мне не нужно задавать его явно) — после этого игровое поле отлично прокручивается и масштабируется.

Однако у меня возникли проблемы с моими перетаскиваемыми плитками с буквами, реализованными в плитке .

Я использую touchesBegan, touchesMoved, touchesEnded, touchesCancelled, а не распознаватели жестов (как часто предлагают пользователи StackOverflow), потому что я отображаю более крупную буквенную плитку с тенью (bigImage) на touchesBegan.

Мое перетаскивание реализовано следующим образом:

  1. В touchesBegan я удаляю плитку из contentView (и добавляю ее в основной вид приложения) и отображаю bigImage с тенью.
  2. В touchesMoved я перемещаю плитку
  3. В touchesEnded или touchesCancelled я снова отображаю 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 прокручивается пальцем под большой плиткой.

Это приводит к тому, что на экране моего приложения появляется много больших плиток с тенями, в то время как вид прокрутки перетаскивается под ними:

скриншот проблемы

Пожалуйста, как это исправить?

Я точно знаю, что моя структура приложения (с пользовательскими UIViews, перетащенными в / из UIScrollView) возможна - глядя на популярные игры в слова.

Я использую tile.exclusiveTouch = YES и пользовательский метод hitTest для contentView - но это не помогает:

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

а затем снова установите значение YES в handleTileReleased ViewController. m - но это не помогает, а также больше похоже на взлом.

ОБНОВЛЕНИЕ 2:

Прочитав, вероятно, все, что связано с UIScrollView, hitTest:withEvent: и pointInside:withEvent: - в Интернете (например, Взлом цепочки ответчиков и книгу Мэтта Нойбурга по Programming iOS), StackOverflow и Safari, мне кажется, что решением было бы реализовать метод hitTest:withEvent: для основного представления моего приложения:

Если объект Tile поражен, он должен быть возвращен. В противном случае следует вернуть scrollView.

К сожалению, это не работает - я, вероятно, упустил что-то незначительное.

И я уверен, что хорошее решение существует — изучая популярные словесные игры для iOS. Например, перетаскивание и размещение плиток с буквами работает очень гладко в приложении Zynga Words with Friends , и на снимках экрана ниже вы можете см. их, вероятно, используя UIScrollView (полосы прокрутки видны в углу) и отображая тень плитки (вероятно, в методе touchesBegan):

скриншот приложения

ОБНОВЛЕНИЕ 3:

Я создал новый проект для тестирования распознавателя жестов, предложенного TomSwift. и это показывает мою проблему с распознавателями жестов: размер плитки меняется слишком поздно - это происходит, когда пользователь начинает перемещать плитку, а не в тот момент, когда он ее касается:

скриншот приложения


person Alexander Farber    schedule 22.03.2014    source источник
comment
Ах, мистер Фарбер и бесконечные проблемы с игрой в слова. :D   -  person Leo Natan    schedule 25.03.2014
comment
:-) У меня была такая же фаза с карточной игрой (мобильной и настольной), и теперь (с 4 лет) она просто работает: facebook.com/appcenter/video-preferans Мне просто нужно ответить на критическую массу глупых вопросов, и тогда я в порядке. Пожалуйста, проявите терпение к моему новичку в iOS.   -  person Alexander Farber    schedule 25.03.2014
comment
Кажется, я знаю, в чем проблема. Если я прокомментирую сообщение с уведомлением, оно работает нормально, прикосновение не переходит к прокрутке   -  person Leo Natan    schedule 25.03.2014


Ответы (4)


Проблема здесь в том, что удаление представления из иерархии представлений сбивает систему с толку, связь теряется. Это та же проблема (внутренние распознаватели жестов используют тот же touchesBegan: API).

https://github.com/LeoNatan/ios-newbie/commit/4cb13ea405d9f959f4d438d08638c1703d6 запрос.)

Что я изменил, так это не удалять плитку из представления содержимого, когда начинаются касания, а перемещать только после окончания или отмены касаний. Но это создает проблему — при перетаскивании вниз тайл скрывается под видом (из-за обрезки прокрутки до его границ). Итак, я создал клонированный тайл, добавил его как подвид представления контроллера представления и переместил его вместе с исходным тайлом. Когда касания заканчиваются, я убираю клонированный тайл и кладу оригинал туда, где он должен быть.

Это связано с тем, что нижняя панель не является частью иерархии прокрутки. Если бы это было так, клонирование всей плитки не потребовалось бы.

Я также немного упростил расположение тайлов.

person Leo Natan    schedule 24.03.2014
comment
+1 спасибо, это работает хорошо. Я подожду с пулреквестом, пока баунти открыта. Для реальной плитки вы устанавливаете tile.alpha=0. Почему это не мешает ему получать события касания? Из чтения документов у меня сложилось впечатление, что когда alpha < 0.01, то hitTest:withEvent: возвращает nil? - person Alexander Farber; 25.03.2014
comment
@AlexanderFarber Я не уверен в этом. По моему опыту, альфа 0 по-прежнему заставляет все работать нормально, а hidden заставляет их пропускать определенные вещи - поэтому я выбрал альфу. - person Leo Natan; 25.03.2014
comment
@AlexanderFarber Уменьшите масштаб и перетащите плитку за пределы представления содержимого. Это гарантирует, что если плитка окажется за пределами представления содержимого, она будет возвращена в стек. - person Leo Natan; 26.03.2014
comment
@AlexanderFarber Я хочу быть бета-тестером, когда вы почувствуете себя более комфортно с iOS и начнете создавать настоящее приложение. :-) - person Leo Natan; 26.03.2014
comment
Круто, я планирую приложение для iOS + Android + Facebook (для десктопа, во Flex + jQuery)... С уведомлениями через Urban Airship + сокеты... Как вы думаете, стоит ли переместить методы touchesXXX из Tile.m в ViewController.m? - person Alexander Farber; 26.03.2014
comment
Это да. Контроллер должен обрабатывать логику представления. - person Leo Natan; 26.03.2014

вы можете установить userInteractionEnabled прокрутки на NO, пока вы перетаскиваете плитку, и установить ее обратно на YES, когда перетаскивание плитки закончилось.

person andreamazz    schedule 22.03.2014
comment
Я пытался установить _scrollView.userInteractionEnabled на NO в handleTileTouched и на YES в handleTileReleased - это не помогает (поэтому я снова закомментировал: github.com/afarber/ios-newbie/blob/master/ScrollContent/ ) Кроме того, я подозреваю, что это обходной путь. , должно быть лучшее решение (например, доставка сенсорных событий только на перетаскиваемую плитку с буквой). - person Alexander Farber; 23.03.2014
comment
Вместо этого вы можете попробовать использовать UIPanGestureRecognizer. Вы можете быстро внедрить, используя эту - person andreamazz; 23.03.2014
comment
С UIPanGestureRecognizer я не мог показать bigImage с тенью (в настоящее время отображается в touchesBegan - см. мой первоначальный вопрос). - person Alexander Farber; 23.03.2014
comment
Я не вижу причин, по которым вы не могли бы применить тень или изменить размер плитки при использовании UIPanGestureRecognizer. - person TomSwift; 25.03.2014
comment
Поскольку распознавание жестов требует времени (чтобы распознать — это свайп? это долгое касание? и т. д.), а тень должна отображаться сразу после того, как пользователь коснется плитки. - person Alexander Farber; 25.03.2014
comment
Вы можете добавить тень с помощью TapGesture и переместить плитку с помощью PanGesture. Таким образом, вы сразу увидите тень. - person andreamazz; 25.03.2014
comment
Сможет ли распознаватель касаний распознать ситуацию, когда пользователь касается плитки и продолжает касаться ее (не отпуская и не перемещая)? - person Alexander Farber; 25.03.2014
comment
Вам нужно установить контроллер представления в качестве делегата и реализовать это. Возврат YES в gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: позволит распознать оба ваших жеста. - person andreamazz; 25.03.2014

Вы действительно должны попробовать использовать распознаватель жестов вместо необработанных touchesBegan/touchesMoved. Я говорю это, потому что UIScrollView использует распознаватели жестов, и по умолчанию они будут уступать любому работающему распознавателю жестов более высокого уровня.

Я собрал образец с UIScrollView со встроенным UIImageView. Как и на вашем снимке экрана, под scrollView у меня есть несколько «плиток» UIButton, которые я подклассифицировал как объекты TSTile. Единственная причина, по которой я это сделал, заключалась в том, чтобы предоставить некоторым NSLayoutConstraints доступ/изменение их высоты/ширины (поскольку вы используете автоматическую компоновку вместо манипуляций с фреймами). Пользователь может перетаскивать плитки из их начального положения в представление прокрутки.

Кажется, это работает хорошо; Я не подключал возможность перетаскивания плитки после того, как она переопределена в прокрутке. Но это не должно быть слишком сложно. Для этого вы можете подумать о размещении распознавателя жестов с длинным касанием в каждой плитке, а затем, когда он сработает, вы отключите прокрутку в прокрутке, чтобы сработал распознаватель жестов панорамирования верхнего уровня.

Или вы можете создать подкласс UIScrollView и перехватить обратные вызовы делегата распознавания жестов UIScrollView, чтобы воспрепятствовать панорамированию, когда пользователь начинает с плитки.

@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
person TomSwift    schedule 24.03.2014

Я также думаю, что вы должны использовать UIGestureRecognizer, а точнее UILongPressGestureRecognizer для каждой плитки, которая после распознавания будет управлять панорамированием.

Для мелкозернистого управления вы все еще можете использовать распознаватели delegate.

person Rivera    schedule 26.03.2014