Благодаря технологии Drag and Drop, iOS 11 превратила iOS, в частности для iPad, в настоящую многозадачную платформу. Это обещает размыть границы между приложениями, позволяя легко обмениваться контентом. Используя мультисенсорное управление, iOS 11 позволяет перемещать контент естественным и интуитивно понятным способом, приближая мобильные устройства Apple к паритету с богатством, которым пользуются пользователи настольных компьютеров и ноутбуков.
Эта долгожданная функция позволяет перетаскивать элементы в другое место в том же приложении или в другое приложение. Это работает либо с помощью разделения экрана, либо через док, используя один непрерывный жест. Более того, пользователи не ограничиваются простым выбором отдельных элементов, но могут перетаскивать несколько элементов одновременно. Многие приложения, в том числе системные приложения, такие как «Фотографии» и «Файлы», используют множественный выбор и перетаскивание нескольких файлов.
Цели этого урока
Из этого туториала Вы узнаете, как перетаскивать объекты, а затем глубже погрузиться в архитектуру и стратегию использования нового Drag and Drop SDK в приложении с поддержкой табличного представления. Я хочу помочь таким разработчикам, как вы, привести свои приложения в соответствие с новым поведением пользовательского интерфейса, которое станет стандартом в будущих приложениях для iOS.
В этом уроке мы рассмотрим следующее:
- Понимание Drag and Drop
- Реализация перетаскивания в табличном представлении
- Drag and Drop Best Practices
Во второй половине этого учебного руководства мы пройдем практические шаги, позволяющие приложению простого табличного представления использовать преимущества перетаскивания, начиная с одного из стандартных шаблонов табличного представления Apple, доступных при создании нового проекта в Xcode. 9. Идите вперед и клонируйте репозиторий GitHub из руководства, если хотите следовать.
Предполагаемые знания
В этом руководстве предполагается, что у вас есть опыт работы в качестве разработчика iOS и вы использовали библиотеки UIKit в Swift или Objective-C, включая UITableView
, и что у вас есть некоторое представление о делегатах и протоколах.
Понимание Drag and Drop
Используя номенклатуру Apple, визуальный элемент перетаскивается из исходного местоположения и помещается в конечное местоположение. Это называется действием перетаскивания, когда действие выполняется либо в одном приложении (поддерживается iPad и iPhone), либо в двух приложениях (доступно только на iPad).
Во время сеанса перетаскивания исходное и конечное приложения все еще активны и работают в обычном режиме, поддерживая взаимодействие с пользователем. Фактически, в отличие от macOS, iOS поддерживает несколько одновременных операций перетаскивания с использованием нескольких пальцев.
Но давайте сосредоточимся на одном элементе перетаскивания и на том, как он использует обещание в качестве контракта для своих представлений данных.
Перетащите предметы как обещания
Каждый элемент перетаскивания можно рассматривать как обещание, содержащее представление данных, которое будет перетаскиваться из источника к месту назначения. Элемент перетаскивания использует провайдера элемента, заполняя его registeredTypeIdentifiers
унифицированными идентификаторами типов , которые представляют собой отдельные представления данных, которые он обязуется доставить по назначению, вместе с предварительным изображением (которое визуально закреплено под точкой касания пользователя), как показано на рисунке. ниже:
Элемент перетаскивания UIDragInteractionDelegate
через UIDragInteractionDelegate
из исходного местоположения и обрабатывается в месте назначения через UIDropInteractionDelegate
. Местоположение источника должно соответствовать протоколу NSItemProviderWriting
, а местоположение назначения должно соответствовать протоколу NSItemProviderReading
.
Это основной обзор назначения вида в качестве элемента перетаскивания через обещания. Давайте посмотрим, как мы реализуем источник перетаскивания из вида, прежде чем устанавливать пункт назначения.
Реализация Drag Source
Сосредоточив наше внимание на первой части перетаскивания — источнике перетаскивания — нам нужно выполнить следующие шаги, когда пользователь инициирует операцию перетаскивания:
- Соответствует представлению протоколу
UIDragInterationDelegate
. - Объявите элементы данных, которые будут формировать обещание элемента, с помощью
dragInteraction(_:itemsForBeginning:)
. - Заполните сеанс перетаскивания элементами перетаскивания, чтобы пользователь мог перетащить элементы в место назначения перетаскивания.
Первое, что вам нужно сделать, это согласовать назначенное представление с протоколом UIDragInterationDelegate
, создав новый экземпляр UIDragInteraction
и связав его со ViewController
представления addInteraction
а также его делегатом, следующим образом:
1
2
|
let dragInteraction = UIDragInteraction(delegate: dragInteractionDelegate)
view.addInteraction(dragInteraction)
|
После объявления источника перетаскивания вы приступаете к созданию элемента перетаскивания, по сути, обещания представления данных, реализуя метод делегата dragInteraction(_:itemsForBeginning:)
, который система вызывает для возврата массива из одного или нескольких элементов перетаскивания для заполнения. свойство items сеанса перетаскивания. В следующем примере NSItemProvider
из обещания изображения перед возвратом массива элементов данных:
1
2
3
4
5
6
7
8
|
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
guard let imagePromise = imageView.image else {
return [] //By returning an empty array you disable dragging.
}
let provider = NSItemProvider(object: imagePromise)
let item = UIDragItem(itemProvider: provider)
return [item]
}
|
Вышеуказанный метод делегата отвечает на запрос перетаскивания, инициируемый, когда пользователь начинает перетаскивание элемента, с помощью средства распознавания UIGestureRecognizer
( UIGestureRecognizer
), отправляющего сообщение «начало перетаскивания» обратно в систему. Это то, что по сути инициализирует «сеанс перетаскивания».
Далее мы приступаем к реализации пункта назначения для обработки массива элементов перетаскивания, инициированных в сеансе.
Реализация назначения отбрасывания
Аналогично, чтобы согласовать назначенный вами вид с тем, чтобы принимать и использовать данные как часть места назначения, вам необходимо выполнить следующие шаги:
-
DropInteraction
. - Объявите типы элементов данных (если они есть), которые вы примете, используя
dropInteraction(_:canHandle:)
. -
dropInteraction(_:sessionDidUpdate:)
предложение удаления, используя метод протоколаdropInteraction(_:sessionDidUpdate:)
, определяющий, будете ли вы копировать, перемещать, отказываться или отменять сеанс. - Наконец, используйте элементы данных, используя метод протокола
dropInteraction(_:performDrop:)
.
Так же, как мы настроили наше представление для включения перетаскивания, мы будем симметрично настраивать наше назначенное представление для приема отброшенных элементов из сеанса перетаскивания, используя UIDropinteractionDelegate
и реализуя его DropInteraction
делегата DropInteraction
:
1
2
|
let dropInteraction = UIDropInteraction(delegate: dropInteractionDelegate)
view.addInteraction(dropInteraction)
|
Чтобы указать, способен ли View принимать элементы перетаскивания или нет, мы реализуем метод протокола dropInteraction(_:canHandle:)
. Следующий метод позволяет нашему представлению сообщить системе, может ли она принимать элементы, указав тип объектов, которые он может получить — в данном случае UIImages.
1
2
3
4
|
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
// Explicitly state the acceptable drop item type here
return session.canLoadObjects(ofClass: UIImage.self)
}
|
Если объект представления не принимает никаких взаимодействий отбрасывания, вы должны вернуть false из этого метода.
Затем свяжите предложение удаления, чтобы принять данные из сеанса удаления. Хотя это необязательный метод, настоятельно рекомендуется реализовать этот метод, поскольку он предоставляет визуальные подсказки относительно того, приведет ли удаление к копированию, перемещению элемента или вообще к отказу в отбрасывании. dropInteraction(_:sessionDidUpdate:)
метод протокола dropInteraction(_:sessionDidUpdate:)
, который возвращает UIDropProposal
, вы указываете тип предложения, используя определенный тип перечисления операций ( UIDropOperation
). Допустимые типы, которые вы можете вернуть:
-
cancel
-
forbidden
-
copy
-
move
1
2
3
4
|
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
// Signal to the system that you will move the item from the source app (you could also state .copy to copy as opposed to move)
return UIDropProposal(operation: .move)
}
|
И, наконец, чтобы использовать содержимое элемента данных в месте назначения, вы реализуете метод протокола dropInteraction(_:performDrop:)
в фоновой очереди (а не в основной очереди — это обеспечивает отзывчивость). Это показано ниже:
1
2
3
4
5
6
7
|
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
// Consume UIImage drag items
session.loadObjects(ofClass: UIImage.self) { items in
let images = items as!
self.imageView.image = images.first
}
}
|
Мы продемонстрировали, как можно реализовать перетаскивание в настраиваемом представлении, поэтому теперь давайте перейдем к практической части этого руководства и реализуем перетаскивание в приложении с табличным представлением.
Реализация перетаскивания в табличном представлении
До сих пор мы обсуждали, как реализовать перетаскивание в пользовательских представлениях, но Apple также упростила возможность дополнения табличных и коллекционных представлений перетаскиванием. В то время как текстовые поля и представления автоматически поддерживают перетаскивание из окна, представления таблиц и коллекций предоставляют конкретные методы, делегаты и свойства для настройки поведения перетаскивания. Мы посмотрим на это в ближайшее время.
Начните с создания нового проекта в Xcode 9, убедившись, что вы выбрали Master-Detail App из окна шаблона:
Прежде чем приступить к работе над остальными шагами, соберите и запустите проект и поэкспериментируйте с ним. Вы увидите, что при создании кнопки «плюс» ( + ) она генерирует новую дату. Мы собираемся улучшить это приложение, позволяя пользователю перетаскивать и заказывать метки времени.
Перетаскивание поддерживается в табличных представлениях (а также в коллекциях) через специализированные API-интерфейсы, которые позволяют перетаскивание со строками, в соответствии с нашим табличным представлением для принятия протоколов UITableViewDragDelegate
и UITableViewDropDelegate
. Откройте файл MasterViewController.swift и добавьте следующее в метод viewDidLoad()
:
1
2
3
4
5
6
|
override func viewDidLoad() {
super.viewDidLoad()
…
self.tableView.dragDelegate = self
self.tableView.dropDelegate = self
…
|
Как и в случае с пользовательскими представлениями, нам нужно обрабатывать новый сеанс перетаскивания, когда пользователь перетаскивает выбранную строку или несколько строк / выделений. Мы делаем это с помощью метода делегата tableView(_:itemsForBeginning:at:)
. В этом методе вы либо возвращаете заполненный массив, который начинает перетаскивание выбранных строк, либо пустой массив, чтобы пользователь не мог перетаскивать контент из этого определенного индекса.
Добавьте следующий метод в ваш файл MasterViewController.swift :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let dateItem = self.objects[indexPath.row] as!
let data = dateItem.data(using: .utf8)
let itemProvider = NSItemProvider()
itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypePlainText as String, visibility: .all) { completion in
completion(data, nil)
return nil
}
return [
UIDragItem(itemProvider: itemProvider)
]
}
|
Некоторый из добавленного кода уже должен быть вам знаком из предыдущего раздела, но по сути мы делаем то, что создаем элемент данных из выбранного объекта, оборачиваем его в NSItemProvider
и возвращаем его в DragItem
.
Обращая наше внимание на включение сеанса удаления, добавьте следующие два метода:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
return session.canLoadObjects(ofClass: NSString.self)
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if tableView.hasActiveDrag {
if session.items.count > 1 {
return UITableViewDropProposal(operation: .cancel)
} else {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
} else {
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
}
}
|
Первый метод сообщает системе, что он может обрабатывать типы данных String как часть своего сеанса удаления. Второй метод делегата, tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)
, отслеживает место потенциального удаления, уведомляя метод о каждом изменении. Он также отображает визуальную обратную связь, чтобы сообщить пользователю, является ли конкретное место запрещенным или приемлемым, с помощью небольшой визуальной пиктограммы.
Наконец, мы обрабатываем tableView(_:performDropWith:)
и потребляем элемент данных, вызывая tableView(_:performDropWith:)
, выбирая строку перетаскиваемого элемента данных, обновляя источник данных нашего табличного представления и вставляя необходимые строки в таблицу.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
// Get last index path of table view.
let section = tableView.numberOfSections — 1
let row = tableView.numberOfRows(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
coordinator.session.loadObjects(ofClass: NSString.self) { items in
// Consume drag items.
let stringItems = items as!
var indexPaths = [IndexPath]()
for (index, item) in stringItems.enumerated() {
let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
self.objects.insert(item, at: indexPath.row)
indexPaths.append(indexPath)
}
tableView.insertRows(at: indexPaths, with: .automatic)
}
}
|
Дальнейшее чтение
Для получения дополнительной информации о поддержке перетаскивания в табличных представлениях обратитесь к собственной документации Apple для разработчиков по поддержке перетаскивания в табличных представлениях .
Drag and Drop Best Practices
Содержимое, которое мы рассмотрели, должно помочь вам реализовать перетаскивание в ваших приложениях, позволяя пользователям визуально и интерактивно перемещаться по контенту в своих существующих приложениях, а также между приложениями.
Однако, наряду с техническими знаниями о том, как реализовать перетаскивание, необходимо уделить время тому, чтобы подумать о том, как реализовать перетаскивание, следуя рекомендациям Apple, изложенным в их Руководствах по взаимодействию с персоналом (HIG), в для того, чтобы предоставить пользователям наилучший пользовательский опыт iOS 11.
В заключение мы коснемся некоторых наиболее важных аспектов, которые следует рассмотреть, начиная с визуальных подсказок. Согласно HIG, фундаментальный опыт работы с перетаскиванием заключается в том, что когда пользователь взаимодействует с некоторым контентом, визуальные подсказки указывают пользователю на активный сеанс перетаскивания, обозначаемый подъемом элемента контента, вместе со значком, указывающим, когда удаление или не возможно.
Мы уже использовали эту tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)
практику в наших предыдущих примерах, когда мы включили метод tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)
, является ли назначение удаления перемещением, копированием или запретом. С помощью пользовательских представлений и взаимодействий вы должны поддерживать ожидаемый набор поведений, поддерживаемый другими приложениями iOS 11, особенно системными приложениями.
Другим важным аспектом, который следует учитывать, является решение о том, приведет ли ваш сеанс перетаскивания к перемещению или копированию. Как правило, Apple рекомендует, чтобы при работе в одном и том же приложении это, как правило, приводило к перемещению, тогда как более целесообразно копировать элемент данных при перетаскивании между различными приложениями. Хотя есть исключения, конечно, основной принцип заключается в том, что это должно иметь смысл для пользователя, и то, что они ожидают, должно произойти.
Вам также следует подумать об источниках и направлениях, а также о том, имеет ли смысл что-то перетаскивать или нет.
Давайте посмотрим на некоторые из системных утилит Apple. Например, Notes позволяет выбирать и перетаскивать текстовое содержимое в другие места в приложении или в другие приложения на iPad с помощью разделенного экрана. Приложение «Напоминания» позволяет перемещать элементы напоминания из одного списка в другой. Подумайте о функциональности, выбирая, как пользователи используют ваш контент.
Согласно рекомендациям Apple, весь контент, который можно редактировать, должен поддерживать прием отброшенного контента, а любой контент, который можно выбрать, должен принимать перетаскиваемый контент, в дополнение к поддержке копирования и вставки для этих типов элементов. Используя стандартные системные текстовые представления и текстовые поля, вы получите поддержку перетаскивания из поля.
Вы также должны поддерживать перетаскивание нескольких элементов, в отличие от поддержки только отдельных элементов, в результате чего пользователи могут использовать несколько пальцев для одновременного выбора нескольких элементов, складывая выбранные элементы в группу, которую необходимо перетаскивать в места назначения. Примером такого действия является выбор нескольких изображений в приложении «Фотографии» или нескольких файлов в приложении «Файлы».
И последнее правило — предоставить пользователям возможность отменить действие или «отменить» сброс. Пользователи очень давно привыкли к концепции отмены действий в большинстве популярных приложений, и перетаскивание не должно быть исключением. Пользователи должны быть уверены в том, что смогут инициировать перетаскивание и иметь возможность отменить это действие, если они уронят элемент в неправильном месте назначения.
Дальнейшее чтение
Помимо того, что мы рассмотрели, есть еще много рекомендаций по перетаскиванию, включая способы поддержки пропущенных визуальных сигналов индикаторов, отображения сбойных пропущенных действий и индикаторов прогресса для сеансов не мгновенного перетаскивания, таких как передача данных. Обратитесь к руководству Apple по человеческому интерфейсу iOS по перетаскиванию для получения полного списка лучших практик.
Вывод
В этом руководстве вы узнали, как обогатить свои приложения iOS перетаскиванием с помощью iOS 11. Попутно мы рассмотрели, как включить как пользовательские представления, так и представления таблиц в качестве источников перетаскивания и пунктов назначения перетаскивания.
Как часть эволюции iOS в направлении более ориентированного на жест пользовательского интерфейса, без сомнения, перетаскивание быстро станет ожидаемой функцией для пользователей в масштабе всей системы, и поэтому все сторонние приложения также должны соответствовать. И так же важно, как реализация перетаскивания, вам нужно правильно его реализовать, чтобы он стал второй натурой для пользователей, включая простоту и функциональность.
И пока вы здесь, ознакомьтесь с некоторыми другими нашими публикациями по разработке приложений для iOS!