iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 6
2
Software Development

iOS 三十天上架記帳 APP系列 第 6

Money Mom - 實作標籤輸入 Part 2

前言

Part 1 時我們實作了標籤輸入的基本功能,但是我們的每個標籤,因為沒有給 UICollectionViewCell 寬度,所以被擠壓到剩下一個小方塊,原因是因為 UICollectionViewFlowLayout 預設每一個 Cell 的大小為 50x50。

但是我們理想的畫面,應該是每個標籤應該要有足夠空間顯示內容,並從左邊開始往右排版,盡可能塞滿寬度,但又保有固定的間距,如下圖所示:

實作上,我們可以給標籤足夠寬度顯示,然後限制其最大寬度,避免因為標籤字數太多破壞排版,因此每個標籤排版須符合下列規則:

  • 高度固定為 50
  • 最小寬度為標籤文字寬度
  • 最大寬度為螢幕寬度的一半
  • 字數過多時,隱藏中段文字,用「...」代替

計算 UICollectionViewCell 長寬

首先,我們要調整每個 UICollectionViewCell 的長寬,我們可以實作 sizeForItemAt 告訴 UICollectionView 我們的 Cell 有多大,如下:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    if indexPath.row == tags.count {
        // 最後的輸入框先固定大小(之後會調整)
        return CGSize(width: 100, height: 50)
    } else {
        let cell = TagCollectionViewCell()
        cell.label.text = tags[indexPath.row]
        cell.label.sizeToFit()

        // 標籤寬度則是內文加上左右兩邊的 Margin
        // 過長時,限制其最大寬度為螢幕的一半
        return CGSize(width: min(cell.label.frame.width + cell.layoutMargins.right + cell.layoutMargins.left, tagCollectionView.frame.width / 2), height: 50);
    }
}

我們依據 Cell 的類型給不同的寬度,在此我們先給最後的輸入框固定長寬,稍後會針對這個元件處理,因為每次使用者打字,輸入框的寬度都必須跟著調整。

而標籤的部分,我們透過計算 TagCollectionViewCell 的標籤文字大小,加上標籤的左右 Margin,得到標籤的寬度,但是要特別注意的是,如果標籤太寬,就要自動縮成螢幕寬的一半(在此也就是 UICollectionView 的一半寬)。

標籤過長時,限制標籤的寬度後,理所當然標籤的文字會被切斷,在這邊我們可以將標籤的文字設定成 byTruncatingMiddle,也就是將文字的中段用「...」取代,我們可以將這個設定加入到 TagCollectionViewCell,如下:

private func addSubview(label: UILabel) {
    contentView.addSubview(label)

    let margin = contentView.layoutMarginsGuide

    // ...略

    // 將文字中斷以「...」取代
    label.lineBreakMode = .byTruncatingMiddle
}

新增標籤後,自動捲動至最後一個 Cell

前段我們新增了計算長寬的邏輯後,我們可以發現在新增標籤時,長寬已經可以自動幫我們計算,並適時地換行,但是換行造成了一個問題,UICollectionView 不會自動幫我們捲動到最下面,也因此我們最後的輸入框,會被往下擠然後消失在畫面中,使用者就不知道自己打什麼字,還得自己手動往下捲才能繼續輸入,相當麻煩!

因此,我們必須在新標籤建立後,也就是可能造成換行的情形時,請 UICollectionView 幫我們捲動到最後的輸入框,以便使用者持續地新增標籤。

我們可以在 QuickCreateViewController 監控標籤新增的 didAdd(tag:) 方法內加入這段程式,如下:

extension QuickCreateViewController: TagTextFieldDelegate {
    func didAdd(tag: String) {
        tags.append(tag)
        tagCollectionView.reloadData()

        // 請 UICollectionView 幫我們捲動到最後的輸入框
        tagCollectionView.scrollToItem(at: IndexPath(item: tags.count, section: 0), at: .bottom, animated: true)
    }
}

備註:捲動的部分,在後面有發現一些問題,之後會專門針對這個問題寫一篇。

讓所有的 Cell 靠左

至此,我們已經完成大部分 UICollectionView 介面的邏輯了,可是 UICollectionView 預設會幫我們盡量地平均分佈這些 Cell,如下圖所示:

可是我們希望所有標籤要靠左,並保持固定的距離,直到寬度不夠時,才自動換行。因此,我們需要調整一下 UICollectionViewFlowLayout 的排版邏輯,所以我們必須繼承 UICollectionViewFlowLayout 進行一些調整。

首先,我們先建立一個 UICollectionViewFlowLayout 的小孩,如下:

class TagCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        // 稍後會針對 attributes 調整

        return attributes
    }
}

透過 layoutAttributesForElements(in:) 這個方法,我們可以調整 UICollectionView 的排版方式(為什麼?),調整的概念是:「先產生原生 UICollectionViewFlowLayout 的排版,然後調整 x 軸的位置,使其靠左」,如下所示:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)

    var leftMargin = sectionInset.left
    var maxY: CGFloat = -1.0

    attributes?.forEach { layoutAttribute in
        if layoutAttribute.frame.origin.y >= maxY {
            leftMargin = sectionInset.left
        }

        layoutAttribute.frame.origin.x = leftMargin

        leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
        maxY = max(layoutAttribute.frame.maxY, maxY)
    }

    return attributes
}

最後,再請 UICollectionView 使用我們修改後的 Layout,如下:

let tagCollectionView: UICollectionView = {
    return UICollectionView(frame: CGRect.zero, collectionViewLayout: TagCollectionViewFlowLayout())
}()

最後,經過本次調整,我們的標籤輸入從原本的:

進化成:

程式碼:GitHub

下一篇會針對最後「紅色」那塊,也就是標籤輸入框做調整,目的是讓輸入框的寬度可以隨著輸入的字調整,這樣就不會造成最後明明還有位置,紅色卻還是被往下擠的狀況。


上一篇
Money Mom - 實作標籤輸入 Part 1
下一篇
Money Mom - 實作標籤輸入 Part 3
系列文
iOS 三十天上架記帳 APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
frank61003
iT邦新手 5 級 ‧ 2020-08-20 18:49:45

請問如何在tableView Cell內放入collectionView

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DraftNameCollectionViewCell", for: indexPath) as! DraftNameCollectionViewCell

}
使用Cell時會報錯
libc++abi.dylib: terminating with uncaught exception of type NSException

Thread 1: Exception: "-[NSConcreteValue setSizeHasBeenSet:]: unrecognized selector sent to instance 0x6000015bd640"

我要留言

立即登入留言