Перетаскивание элементов интерфейса пальцем стало настолько естественной составляющей жестового управления, что редкое приложение обходится без него. Тем не менее, должным образом прописать подобный функионал в коде — не всегда тривиальная задача. О некоторых специфических моментах технической реализации drag&drop на Objective C поведает читателям партнерская компания Music Breath.

«Один из проектов, над которым у нас сейчас ведется активная работа — это Song Writer — Lyrics Memo Pad, своеобразная записная книжка для музыкантов, в которую можно в любой момент занести какую-нибудь идею, удачную строку или даже понравившиеся аккорды. Для последних требовалось внедрить в приложение функцию вставки и перетаскивания изображения, которую мы решили выполнить классическим методом — Drag’&’Drop. Сегодня мы расскажем, как реализовывали его, переписывая приложение с Unity на Native, и с какими трудностями столкнулись в процессе.


Начали мы, естественно, с поиска готового решения: перечитали много статей, провели несколько дней в гугле и перебрали массу вариантов реализации данной фичи. Однако все они создавались для перенесения объектов из UIView в UIScrollView или из UIView в UIView. В нашу же задачу на тот момент входило перемещение из UIScrollView в UIScrollView (позже выяснилось, что перенос будет происходить из UIScrollView в UIView, которая находится в UIScrollView). Это оказалось не так просто: перед нами возник ряд проблем, о преодолении которых и пойдет речь ниже.

Проблемы с Drag’&’Drop


Для реализации Drag’&’Drop мы выбрали UIPanGestureRecognizer. Сложности с этим методом возникли следующие:

  1. UIPanGestureRecognizer перехватывает события UIScrollView.
  2. Нам требовалось не перенести объект, а скопировать его. Строго говоря, эта проблема не связана напрямую с Drag’&’Drop, но мы с ней столкнулись в процессе реализации.
  3. Необходимо конвертировать систему координат из одной UIScrollView в другую.

Пункты 2 и 3 можно объединить. Итак, разберем оба блока проблем подробнее.

1. UIPanGestureRecognizer перехватывает события UIScrollView.

Суть проблемы:

Так как в UIScrollView, из которого мы хотим перенести UIImageView, больше элементов чем отображается на экране (иными словами, contentSize оказывается больше, чем frame), а UIPanGestureRecognizer перехватывает события UIScrollView, то при скроллинге вправо/влево мы не можем добраться до других элементов — все время вызывается UIPanGestureRecognizer.


Решение:

Чтобы решить данную проблему мы использовали категорию для UIPanGestureRecognizer, которую нашли на просторах stackoverflow. Она была нам нужна для того, чтобы задать направление для UIPanGestureRecognizer – сделать так, чтобы элемент перемещался только вверх/вниз.

Как это выглядело в коде:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    if (self.state == UIGestureRecognizerStateFailed) return;
    CGPoint nowPoint = [[touches anyObject] locationInView:self.view];
    CGPoint prevPoint = [[touches anyObject] previousLocationInView:self.view];
    _moveX += prevPoint.x - nowPoint.x;
    _moveY += prevPoint.y - nowPoint.y;
    if (!_drag) {
        if (abs(_moveX) > kDirectionPanThreshold) {
            if (_direction == DirectionPangestureRecognizerVertical) {
                self.state = UIGestureRecognizerStateFailed;
            }else {
                _drag = YES;
            }
        }else if (abs(_moveY) > kDirectionPanThreshold) {
            if (_direction == DirectionPanGestureRecognizerHorizontal) {
                self.state = UIGestureRecognizerStateFailed;
            }else {
                _drag = YES;
            }
        }
    }
}

Теперь мы можем задать определенное направление для UIPanGestureRecognizer. Вот таким образом:

  for (int i =0; i < _scrollChords.imageArray.count; i++)
    {
        DirectionPanGestureRecognizer *panRecg = [[DirectionPanGestureRecognizer alloc]initWithTarget:self action:@selector(labelDragged:)];
        panRecg.direction = DirectionPangestureRecognizerVertical;
        
        [[_scrollChords.imageArray objectAtIndex:i] addGestureRecognizer:panRecg];
      
    }

где panRecg.direction = DirectionPangestureRecognizerVertical как раз и определяет направление для нашего Drag’&’Drop.



Немного о UIPanGestureRecognizer:

У UIPanGestureRecognizer есть три события для перемещения объекта:

  1. UIGestureRecognizerStateBegan – событие возвращается при начале передвижения, когда мы еще только тапнули на объект.
  2. UIGestureRecognizerStateChanged – событие возвращается в процессе перемещения объекта.
  3. UIGestureRecognizerStateEnded – событие возвращается, когда перемещение закончено, то есть когда мы уже отпустили палец.

Чтобы осуществить перенос объекта, мы отлавливаем все события.

2) Конвертирование системы координат из одной UIScrollView в другую. Копирование объекта

Суть проблемы:

В нашем случае для реализации Drag’&’Drop требовался перерасчет систем координат. Сложность заключалась в том, что внутри UIScrollView координаты у нас не статичные: при скроллинге вверх, вправо, влево и вниз они меняются.

Решение:

Для начала нам необходимо было определить, что мы перемещаем UIImageView и что мы перемещаем определенную UIImageView, то есть нужна была информация о её координатах, ширине и высоте и собственно картинке (UIImage). И тут же требовалось определить центр, где находится UIImageView в self.view

UIImageView *imageView=(UIImageView *)[recognizer view];
    
CGPoint newCenter = [recognizer translationInView:self.view];

Эта процедура выполняется перед каждым из событий UIPanGestureRecognizer.

Напомним, что мы хотели не просто передвинуть объект, а скопировать его, чтобы он появился в одной UIScrollView, не исчезнув при этом из другой. Кроме того, процесс перемещения должен быть видимым для пользователя.

Рассмотрим все события UIPanGestureRecognizer.

2.1. UIGestureRecognizerStateBegan

if (recognizer.state==UIGestureRecognizerStateBegan)
    {
                
        viewImage = [[UIImageView alloc] init];
        CGRect rect = [self.view convertRect:[imageView frame] fromView:[imageView superview]];
        viewImage.frame = CGRectMake(rect.origin.x, rect.origin.y, 28, 28);
        viewImage.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@copy",imageView.accessibilityLabel]];
        viewImage.contentMode = UIViewContentModeScaleAspectFill;
        viewImage.userInteractionEnabled = YES;
        
        [self.view addSubview:viewImage];
        
        beginX = imageView.center.x;
        beginY = imageView.center.y;
  }

Магия здесь в том, что мы создаем картинку на нашей self.view. Но это только начало. Теперь нам требуется перерассчитать координаты UIImageView, которую мы передвигаем в self.view, и добавить ее именно туда, откуда мы начинаем перемещение. На прежнем месте она должна появиться затем, чтобы у пользователя сохранялась иллюзия, что он перетаскивает именно эту картинку.

Разобьем весь код на несколько частей:

a) Конвертирование

CGRect rect = [self.view convertRect:[imageView frame] fromView:[imageView superview]];

Мы конвертим размеры нашего передвигаемого объекта из его superview в self.view и получаем координаты в self.view.

viewImage.frame = CGRectMake(rect.origin.x, rect.origin.y, 28, 28);
        viewImage.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@copy",imageView.accessibilityLabel]];

Далее задаем frame новой UIImageView, которую мы добавляем на self.view. При этом не забываем, что размеры нужны будут строго определенные, поэтому берем статичные значения, 28х28.

b) Задание картинки

Еще одна сложность состояла в том, что просто так взять название картинки мы не можем: нам требуется перемещать UIImage отличную от той, которую мы перетаскиваем (другой цвет, закругленные края и т. д.). Для этого мы при построении массива с UIImageView задаем каждой accessibilityLabel, чтобы можно было легко получить название.

        viewImage.contentMode = UIViewContentModeScaleAspectFill;
        viewImage.userInteractionEnabled = YES;
        
        [self.view addSubview:viewImage];



c) Добавление необходимого пункта

Здесь главное — не забыть присвоить userInteractionEnabled = YES, иначе события не будут распознаваться при тапе на UIImageVIew. Сначала мы это реализовали, когда строили массив с UIImageView. Затем, так как в дальнейшем будет возможность перемещать и то, что добавляется в другую UIScrollView, мы и для нее проделали то же самое:

viewImage.userInteractionEnabled = YES;

d) Задание начальной точки

Ну и наконец, нам нужно знать, с какой точки мы хотим начать перемещение.

beginX = imageView.center.x;
beginY = imageView.center.y;

Это центр нашего нового объекта. Берем именно imageView: таким образом когда мы начнем двигать элемент, он будет находиться прямо под пальцем. Если бы мы использовали viewImage.center, то передвигаемый объект оказался бы выше и левее (возможно тут не доработка, и можно проще, но мы в дальнейшем просто перерасчитываем координаты).

Итак, всё, что требовалось сделать для начала передвижения, мы сделали. Теперь можно идти дальше.

2.2. UIGestureRecognizerStateChanged

Теперь нам необходимо реализовать сам процесс движения, причем так, чтобы он происходил на глазах пользователя. Для этого при обработке события UIGestureRecognizerStateChanged мы делаем следующее:

  newCenter.x = ( newCenter.x + beginX );
  newCenter.y = ( newCenter.y + beginY );
  newCenter = [self.view convertPoint:newCenter fromView:imageView.superview];
  viewImage.center = newCenter;

Здесь повторяется магический трюк с перерасчетом систем координат. На выходе имеем объект, который движется прямо вслед за пальцем: картинка никуда не смещается и на каждом этапе пути, находится там, где и должна быть.


Теперь осталось самое сложное – добавить объект туда, где мы бы хотели его видеть в конечном итоге. Усложняло дело то, что мы перемещаем не просто в UIScrollView, а в UIView, которая находится внутри нее.

Все происходит в событии UIGestureRecognizerStateEnded.

2.3. UIGestureRecognizerStateEnded

Для начала мы находим 2 CGRect’а: передвигаемой картинки и UIView, и вставляем туда:

        CGRect rect = [self.view convertRect:[viewImage frame] fromView:[viewImage superview]];
 	 CGRect rectNewView = [self.view convertRect:[[_customfield.viewArray objectAtIndex:j] frame] fromView:[[_customfield.viewArray objectAtIndex:j] superview]];

При отпускании пальца (то есть в момент, когда перемещение закончено) мы конвертим нашу viewImage и аналогичным образом конвертим ту локацию, куда мы хотим ее вставить. По сути, здесь мы работаем по шаблону: раньше вставляли в UIScrollView, теперь с тем же успехом вставляем в UIView, которая находится в UIScrollView.

Получив два конвертированных CGRect’а, мы можем проверить, попали ли мы туда, куда следует.

if (CGRectIntersectsRect(rect,rectNewView))
{

}
else
{
     [viewImage removeFromSuperview];
}

Если выяснится, что нет, то мы просто удалим созданную UIImageView для перемещения с нашей self.view


Если же все прошло успешно, делаем следующее:

newCenter = [[_customText.viewArray objectAtIndex:j] convertPoint:newCenter fromView:viewImage.superview];
 viewImage.center = CGPointMake(newCenter.x,[[_customText.viewArray objectAtIndex:j] frame].size.height/2);                
[[_customText.viewArray objectAtIndex:j] addSubview:viewImage];

Здесь мы находим точку, уже внутри UIView, в которую будем перемещать объект.
Далее нужно поставить UIImageVew четко по центру по оси Y, для этого мы задаем новую координату. Смотрим, что получилось, и видим, что картинка продублировалась туда, куда мы и хотели, при этом оставшись и в старой UIScrollView.

На этом сегодняшний сеанс черной магии Drag’&’Drop заканчивается.


Подытоживая сказанное, вся сложность состояла в том, чтобы понять, что и где конвертить. Возможно, со временем мы выработаем более простой алгоритм, но на данный момент просто будем радоваться, что все работает и красиво копируется из одного место в другое, как и замышлялось».
Поделиться с друзьями
-->

Комментарии (1)


  1. Madmess
    18.05.2017 13:46

    Разработчикам Inbox эту статью бы почитать и реализовать наконец-то перетаскивание адресатов между To, Cc etc