返回列表

# 223-Drag and Drop with Collection and Table View

# Drag and Drop

# UITableViewDragDelegate

protocol UITableViewDragDelegate : NSObjectProtocol {
  /// 提供 drag 的内容, 看 UIDragItem 详解
  func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]
  /// 在 drag 的过程中,点击添加新 item
  optional func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem]
  /// drag 开始和结束
  optional func tableView(_ tableView: UITableView, dragSessionWillBegin session: UIDragSession)
  optional func tableView(_ tableView: UITableView, dragSessionDidEnd session: UIDragSession)

  /// 移动操作是否允许(默认允许)  
  optional func tableView(_ tableView: UITableView, dragSessionAllowsMoveOperation session: UIDragSession) -> Bool
  
  /// 是否必须 drop 在当前应用(默认否)
  optional func tableView(_ tableView: UITableView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool
  /// 自定义一些参数控制 drag 时 cell 的展示
  /// Mac Catalyst 不起作用
  optional func tableView(_ tableView: UITableView, dragPreviewParametersForRowAt indexPath: IndexPath) -> UIDragPreviewParameters?
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# UITableViewDropDelegate

protocol UITableViewDropDelegate : NSObjectProtocol {
  /// 接受 drop, 看 UITableViewDropCoordinator 详解
  func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator)

  /// tableView 能否接受 drop, 一般是判断 drag item 的数据类型     
  optional func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool
  
  /// 看 UITableViewDropProposal 详解
  optional func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal

  /// 进入、离开以及结束
  optional func tableView(_ tableView: UITableView, dropSessionDidEnter session: UIDropSession)
  optional func tableView(_ tableView: UITableView, dropSessionDidExit session: UIDropSession)
  optional func tableView(_ tableView: UITableView, dropSessionDidEnd session: UIDropSession)

  //// 自定义一些参数控制 drop 时 cell 的展示
  optional func tableView(_ tableView: UITableView, dropPreviewParametersForRowAt indexPath: IndexPath) -> UIDragPreviewParameters?
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# UIDragItem

init(itemProvider: NSItemProvider)
/// 与 item 关联自定义的对象,只能用于本App
var localObject: Any?
1
2
3

# NSItemProvider

drag/drop, copy/paste, app/extension的数据核心

提供四种方式

  • fileUrl
  • 实现 NSItemProviderWriting 协议的对象,NSString, UIImage 实现了这个协议
  • 实现 NSSecureCoding 协议的对象
  • register data

# UITableViewDropProposal

继承 UIDropProposal,有两个属性

  • operation:
    • cancel
    • forbidden
    • move
    • copy
  • indent
    • unspecified
    • insertAtDestinationIndexPath
    • insertIntoDestinationIndexPath
    • automatic

# UITableViewDropCoordinator

取得 drag item, destination indexPath, drop session, proposal 属性,以及执行插入动画

# UIDragSession

继承 UIDragDropSession

/// 自定义数据,只能用于 in-app
var localContext: Any? { get set }
1
2

# UIDropSession

同样继承 UIDragDropSession

/// 取出 drag 数据,completion 运行在主线程上
func loadObjects(ofClass aClass: NSItemProviderReading.Type, completion: @escaping ([NSItemProviderReading]) -> Void) -> Progress

/// 对应的 drag session,只能用于 in-app drag
var localDragSession: UIDragSession? { get }
1
2
3
4
5

# UIDragDropSession

var items: [UIDragItem] { get }
/// drag location

func location(in view: UIView) -> CGPoint

/// 可以 move 
var allowsMoveOperation: Bool { get }
    
/// 只能用于 in-app
var isRestrictedToDraggingApplication: Bool { get }

/// 至少有一个drag item 是 specified UTIs 类型的
func hasItemsConforming(toTypeIdentifiers typeIdentifiers: [String]) -> Bool

/// 至少有一个drag item 是 NSItemProviderReading 类
func canLoadObjects(ofClass aClass: NSItemProviderReading.Type) -> Bool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Demo

// Drag
extension RepoController: UITableViewDragDelegate {
  func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    let repo = getRepo(at: indexPath)
    let itemProvider = NSItemProvider(object: "\(repo.id)" as NSItemProviderWriting)
    let item = UIDragItem(itemProvider: itemProvider)
    item.localObject = repo
    return [item]
  }
    
  func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {  
    let repo = getRepo(at: indexPath)
    let itemProvider = NSItemProvider(object: "\(repo.id)" as NSItemProviderWriting)
    let item = UIDragItem(itemProvider: itemProvider)
    item.localObject = repo
    return [item]
  }
  
  func tableView(_ tableView: UITableView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool {
    return true
  }
}
// Drop
extension GroupCtrlr: UITableViewDropDelegate {
    func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
      guard let dest = coordinator.destinationIndexPath else {
        return
      }
      // 方法一, 使用 UIDropSession
      coordinator.session.loadObjects(ofClass: String.self) { ids in
        // 主线程
        for id in ids {
          let repo = getRepo(id)
    	  group.addToRepo(repo)
        }
        tableview.reloadSections([2], with: .automatic)
      }
      // 方法二,使用 NSItemProvider
      let dragItems = coordinator.items.map { $0.dragItem }
        for item in dragItems {
            item.itemProvider.loadObject(ofClass: String.self) { (value, error) in
              /// 子线程
              if let id = value {
                let repo = getRepo(id)
    	  	    group.addToRepo(repo)
                 coordinator.drop(item, toRowAt: dest)
              }
            }
        }
        /// 方法三,使用 localObject (in-app)
        let repos = coordinator.items.compactMap { $0.dragItem.localObject as? Repo }
        // ...
    }

  func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
    guard let dest = destinationIndexPath, dest.section == 2 else {
      return UITableViewDropProposal(operation: .forbidden)
    }
        
    return UITableViewDropProposal(operation: .copy, intent: .insertIntoDestinationIndexPath)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# PlaceHolder

Drop 时需要异步加载数据,临时插入的占位符

Avoid reloadData , use performBatchUpdates(_:completion:) instead

/// Indicate whether the collection view contains drop placeholders or is reordering its items as part of handling a drop.
var hasUncommittedUpdates: Bool { get }
1
2

# Demo

extension ViewCtrlr: UITableViewDropDelegate {
  func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
  for item in coordinator.items {
    let placeholderContext = coordinator.drop(item.dragItem, toPlaceholderInsertedAt: destinationIndexPath, withReuseIdentifier: "PlaceholderCell") { cell in
	  // Configure the placeholder cell
	}
	item.dragItem.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
	  DispatchQueue.main.async {
	    if let image = object as? UIImage {
	      // 插入真实数据
	      placeholderContext.commitInsertion { insertionIndexPath in
	        self.imagesArray.insert(image, at: insertionIndexPath.item) }
	      } else { 
	        // 删除 place holder 
	        placeholderContext.deletePlaceholder()
	      } 
	    }
	  }
	}
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# Supporting Reordering

# Collection View Reordering Cadence

/// indicate the speed at which collection view items are reorganized during a drop
var reorderingCadence: UICollectionView.ReorderingCadence { get set }
1
2
  • immediate
  • fast
  • slow

# Table View Reordering in iPhone

/*
The default value of this property is true on iPad and false on iPhone. Changing the value to true on iPhone makes it possible to drag content from the table view to another app on iPhone and to receive content from other apps.
*/
tableView.dragInteractionEnabled = true
tableView.dragDelegate = self

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  // 做操作
}
1
2
3
4
5
6
7
8
9

以前 Table View 实现 Reordering 的方式

tableView.isEditing = true 

func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
  return true
}

func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
  return .none
}

func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
  return false
}

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  // 做操作
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Adopting Drag and Drop in a Table View (opens new window)

# Spring Loading

Spring loading is a way to actually navigate and activate controls throughout the system while you're in the middle of a drag session.

在 drag 的过程中能激活控件,响应控件事件

// UITableView 和 UICollectionView 实现了这个协议
protocol UISpringLoadedInteractionSupporting {
  var isSpringLoaded: Bool { get set }
}

// 如果想控制某一行支持Spring loading
protocol UITableViewDelegate {
  func tableView(_ tableView: UITableView, shouldSpringLoadRowAt indexPath: IndexPath, with context: UISpringLoadedInteractionContext) -> Bool
}
1
2
3
4
5
6
7
8
9

# Customizing Cell Appearance

# 跟踪 drag 状态变化,自定义 cell appearance

  • none
  • lifting
  • dragging
func dragStateDidChange(_ dragState: UITableViewCell.DragState)
1

# Preview

optional func tableView(_ tableView: UITableView, dragPreviewParametersForRowAt indexPath: IndexPath) -> UIDragPreviewParameters? {}
1