iOS UICollectionView記錄

UICollectionView 在項目中是出現很高頻的一個空間,它能靈活的展示各類佈局。平時,咱們經常使用的水平、垂直及網格的效果基本上均可以使用系統提供的給咱們的 Layout 進行完成,最近恰好作了一個自定義佈局的需求,這裏將過程稍做記錄,後面也說起了一些 DragAndDrop 的簡單使用。git

  • 準備知識
  • Basic Layout
  • Custom Layout
  • Drag And Drop

準備知識

UICollectionView 的核心概念有三點:github

  • Layout(佈局)
  • Data Source(數據源)
  • Delegate(代理)

UICollectionViewLayout

一個抽象基類,用於生成 UICollectionView 的佈局信息,每個 cell 的佈局信息由 UICollectionViewLayoutAttributes 進行管理。緩存

系統爲咱們提供了一個流式佈局的類——UICollectionViewFlowLayout,咱們能夠利用這個定義咱們經常使用的佈局。它能夠定義佈局方向、cell大小、間距等信息。bash

UICollectionViewDataSource

數據源協議,遵照協議的 delegate 爲 UICollectionView 提供數據的各類信息:分組狀況、Cell 的數量、每一個 Cell 的內容等。session

經常使用代理方法:app

// 分組信息
optional func numberOfSections(in collectionView: UICollectionView) -> Int

// 每一個分組中,cell 的數量
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int

// 返回須要顯示的 cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell

// 返回 UICollectionView 的 Header 或 Footer
optional func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
複製代碼

UICollectionViewDelegate

UICollectionViewDelegate 爲咱們提供了 cell 點擊的事件以及一些視圖的顯示事件。dom

經常使用代理方法:ide

// cell 點擊事件
optional func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
    
// cell 視圖即將顯示
optional func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
複製代碼

Basic Layout

  • 準備工做,後面的幾個示例中,數據源的代理方法也是以下實現:
extension LayoutViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 14
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCell
        
        let random = dataSource[indexPath.row]
        
        cell.showImage.image = UIImage(named: "\(random)")

        return cell
    }
}
複製代碼
  • 設置 Basic Layout 的佈局
let layout = UICollectionViewFlowLayout()
// 垂直滾動
layout.scrollDirection = .vertical
// cell 大小
layout.itemSize = CGSize(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.width)
self.collectionView.collectionViewLayout = layout
複製代碼

這是最基本的使用,就很少贅述了,基本的使用若是不瞭解能夠去看看官方文檔。佈局

Custom Layout

系統提供給咱們的佈局只是簡單的流式佈局,當咱們須要一些特殊的佈局的時候,只能本身繼承自 UICollectionViewLayout 來自定義 Layout。動畫

自定義 Layout

class CustomCollectionViewLayout: UICollectionViewLayout {
    private var itemWidth: CGFloat = 0   // cell 寬度
    private var itemHeight: CGFloat = 0  // cell 高度

    private var currentX: CGFloat = 0    // 當前 x 座標
    private var currentY: CGFloat = 0    // 當前 y 座標
	
	 // 存儲每一個 cell 的佈局信息
    private var attrubutesArray = [UICollectionViewLayoutAttributes]()
}
複製代碼

佈局相關準備工做

// 佈局相關準備工做
// 爲每一個 invalidateLayout 調用
// 緩存 UICollectionViewLayoutAttributes
// 計算 collectionViewContentSize
override func prepare() {
    super.prepare()

    guard let count = self.collectionView?.numberOfItems(inSection: 0) else { return }
    // 獲得每一個 item 的屬性並存儲
    for i in 0..<count {
        let indexPath = IndexPath(row: i, section: 0)

        guard let attributes = self.layoutAttributesForItem(at: indexPath) else { break }
        attrubutesArray.append(attributes)
    }
}
複製代碼

提供佈局屬性對象

// 提供佈局對象
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    return attrubutesArray
}

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    // 獲取寬度
    let contentWidth = self.collectionView!.frame.size.width

    // 經過 indexpath 建立一個 item 屬性
    let temp = UICollectionViewLayoutAttributes(forCellWith: indexPath)
    
    // 計算 item 的寬高
    let typeOne: (Int) -> () = { index in
        self.itemWidth = contentWidth / 2
        self.itemHeight = self.itemWidth
        
        temp.frame = CGRect(x: self.currentX, y: self.currentY, width: self.itemWidth, height: self.itemHeight)
        
        if index == 0 {
            self.currentX += self.itemWidth
        } else {
            self.currentX += self.itemWidth
            self.currentY += self.itemHeight
        }
    }
    
    let typeTwo: (Int) -> () = { index in
        if index == 2 {
            self.itemWidth = (contentWidth * 2) / 3.0
            self.itemHeight = (self.itemWidth * 2) / 3.0
            
            temp.frame = CGRect(x: self.currentX, y: self.currentY, width: self.itemWidth, height: self.itemHeight)
            
            self.currentX += self.itemWidth
        } else {
            self.itemWidth = contentWidth / 3.0
            self.itemHeight = (self.itemWidth * 2) / 3.0
            
            temp.frame = CGRect(x: self.currentX, y: self.currentY, width: self.itemWidth, height: self.itemHeight)
            
            self.currentY += self.itemHeight
            if index == 4 {
                self.currentX = 0
            }
        }
    }
    
    let typeThree: (Int) -> () = { index in
        if index == 7 {
            self.itemWidth = (contentWidth * 2) / 3.0
            self.itemHeight = (self.itemWidth * 2) / 3.0
            
            temp.frame = CGRect(x: self.currentX, y: self.currentY, width: self.itemWidth, height: self.itemHeight)
            
            self.currentX += self.itemWidth
            self.currentY += self.itemHeight
        } else {
            self.itemWidth = contentWidth / 3.0
            self.itemHeight = (self.itemWidth * 2) / 3.0
            
            temp.frame = CGRect(x: self.currentX, y: self.currentY, width: self.itemWidth, height: self.itemHeight)
            
            if index == 5 {
                self.currentY += self.itemHeight
            } else {
                self.currentX += self.itemWidth
                self.currentY -= self.itemHeight
            }
        }
    }
    
    // 這下面是我模擬的根據不一樣的 indexPath 的信息來提供不一樣的 cell 的顯示類型。
    // 實際項目中,一把根據利用 Block 或者 Delegate,在 controller 中根據 indexPath 的值進行計算
    // 並根據計算結果肯定其具體的顯示類型。
    // Custom Layout 再根據顯示類型進行計算顯示位置。
    
    let judgeNum = indexPath.row % 8
    
    switch judgeNum {
    case 0, 1:
        typeOne(judgeNum)
    case 2, 3, 4:
        typeTwo(judgeNum)
    case 5, 6, 7:
        typeThree(judgeNum)
    default:
        break
    }
    
    // 當 currentX 到屏幕最右邊時,換行到下一行顯示。
    if currentX >= contentWidth {
        currentX = 0
    }

    return temp
}
複製代碼

滾動範圍

// 提供滾動範圍
override var collectionViewContentSize: CGSize {
    return CGSize(width: UIScreen.main.bounds.size.width, height: currentY + 20)
}
複製代碼

邊界更改

// 處理自定義佈局中的邊界修改
// 返回 true 使集合視圖從新查詢佈局
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
    return true
}
複製代碼

使用

let layout = CustomCollectionViewLayout()
self.collectionView.collectionViewLayout = layout
複製代碼

Drag And Drop

以前爲了在 UICollectionView 中拖動 cell 須要本身定義手勢處理拖動,在 iOS11 增長了 UICollectionViewDragDelegate 及 UICollectionViewDropDelegate 兩個協議方便咱們進行這個操做。

開啓拖放手勢,設置代理

// 開啓拖放手勢,設置代理。
self.collectionView.dragInteractionEnabled = true
self.collectionView.dragDelegate = self
self.collectionView.dropDelegate = self
複製代碼

實現代理

UICollectionViewDragDelegate

UICollectionViewDragDelegate 只有一個必須實現的方法。

  1. 建立一個或多個 NSItemProvider ,使用 NSItemProvider 傳遞集合視圖item內容。
  2. 將每一個 NSItemProvider 封裝在對應 UIDragItem 對象中。
  3. 返回 dragItem。
extension LayoutViewController: UICollectionViewDragDelegate {
	func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
	
	    let imageName = self.dataSource[indexPath.row]
	
	    let image = UIImage(named: imageName)!
	
	    let provider = NSItemProvider(object: image)
	
	    let dragItem = UIDragItem(itemProvider: provider)
	
	    return [dragItem]
	}
}

複製代碼

若是須要支持一次拖動多個,還須要實現下面這個方法,其實現步驟與上方大體相同。

func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem]
複製代碼

若是拖動以後,想自定義拖動視圖的樣式,能夠實現:

/*
    自定義拖動過程當中 cell 外觀。返回 nil 則以 cell 原樣式呈現。
 */
func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters? {
    return nil
}
複製代碼

UICollectionViewDropDelegate

實現上方的協議以後,我們在程序中已經能夠實現拖動了,可是如今當放開時,cell 並不會按照咱們預想的到達對應的位置,此時,須要實現 UICollectionViewDropDelegate 協議,來處理拖動內容的接收。

extension LayoutViewController: UICollectionViewDropDelegate {
	 // 返回一個 UICollectionViewDropProposal 對象,告知 cell 該怎麼送入新的位置。
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        if session.localDragSession != nil {
            // 拖動手勢源自同一app。
            return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        } else {
            // 拖動手勢源自其它app。
            return UICollectionViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
        }
    }

    /*
        當手指離開屏幕時,UICollectionView 會調用。必須實現該方法以接收拖動的數據。
     */
    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(item: 0, section: 0)

        switch coordinator.proposal.operation {
        case .move:
            let items = coordinator.items

            if items.contains(where: { $0.sourceIndexPath != nil }) {
                if items.count == 1, let item = items.first {
						// 找到操做的數據
                    let temp = dataSource[item.sourceIndexPath!.row]
						
						// 數據源將操做的數據在原位置刪除,以及插入到新的位置。
                    dataSource.remove(at: item.sourceIndexPath!.row)
                    dataSource.insert(temp, at: destinationIndexPath.row)

                    // 將 collectionView 的多個操做合併爲一個動畫。
                    collectionView.performBatchUpdates({
                        collectionView.deleteItems(at: [item.sourceIndexPath!])
                        collectionView.insertItems(at: [destinationIndexPath])
                    })

                    coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
                }
            }
        default:
            return
        }
    }
}
複製代碼

關於 Drag 和 Drap 還能夠不少使用的協議咱們沒有使用,這裏只是實現了一個基本的效果,更多的實現方式仍是須要多研讀研讀官方的文檔。

Demo在此

參考連接:Drag and Drop with Collection and Table View

參考連接:A Tour Of UICollectionView

相關文章
相關標籤/搜索