添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接


可以設定畫面上最多會有幾張卡片,卡片向做滑動可以滑出畫面,向右可以將卡片滑入。

CardCell

https://ithelp.ithome.com.tw/upload/images/20180103/20107329K9pjhhJ9Dl.png
CardCell 只有放一張圖片,在 loadContent 後載入圖片、設定基本樣式。

class CardCell: UICollectionViewCell {
    @IBOutlet weak var imageView: UIImageView!
    var imageName:String?
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    func loadContent() {
        if imageName == nil { return }
        if let image = UIImage(named:imageName!) {
            imageView.image = image
        layer.cornerRadius = 20
        layer.borderColor = UIColor.white.cgColor
        layer.borderWidth = 2

CardCollectionViewLayout

通過自定義 UICollectionViewLayout 來定義出卡片滑動的佈局效果。

可以對 CardCollectionViewLayout 設定 itemSize / spacing / maximumVisibleItems

並且在設定的時候調用 invalidateLayout 方法,觸發畫面重新 render 機制,這裡會檢查 collectionView 是否存在。

public var itemSize: CGSize = CGSize(width: 250, height: 400) {
    didSet{
        if collectionView != nil {
            invalidateLayout()
public var spacing: CGFloat = 16.0 {
    didSet{
        if collectionView != nil {
            invalidateLayout()
public var maximumVisibleItems: Int = 4 {
    didSet{
        if collectionView != nil {
            invalidateLayout()

attributes.center - 每張卡片在畫面上的位置,根據設定的 Spacing 讓卡片在 y 軸上有間距。

根據移動 UICollectionViewCell 的比例 (percentageDeltaOffset) 來改變即將登場卡片的 Alpha 值

// MARK: - compute layout
extension CardCollectionViewLayout {
    func computeLayoutAttributesForItem(indexPath: IndexPath,
                                        minVisibleIndex: Int,
                                        contentCenterX: CGFloat,
                                        deltaOffset: CGFloat,
                                        percentageDeltaOffset: CGFloat) -> UICollectionViewLayoutAttributes {
        if collectionView == nil { return UICollectionViewLayoutAttributes(forCellWith:indexPath)}
        let attributes = UICollectionViewLayoutAttributes(forCellWith:indexPath)
        let cardIndex = indexPath.row - minVisibleIndex
        attributes.size = itemSize
        attributes.center = CGPoint(x: contentCenterX + spacing * CGFloat(cardIndex),
                                    y: collectionView!.bounds.midY + spacing * CGFloat(cardIndex))
        attributes.zIndex = maximumVisibleItems - cardIndex
        switch cardIndex {
        case 0:
            attributes.center.x -= deltaOffset
        case 1..<maximumVisibleItems:
            attributes.center.x -= spacing * percentageDeltaOffset
            attributes.center.y -= spacing * percentageDeltaOffset
            if cardIndex == maximumVisibleItems - 1 {
                attributes.alpha = percentageDeltaOffset
        default: break
        return attributes

返回佈局屬性

這次的佈局只有支持單一 section 所以在 prepare 的地方會先檢查 section 的數量。

  • collectionViewContentSize 需要回傳 UICollectionView 內容的大小(不只是可視範圍)類似於UIScrollView 的 contentSize
  • layoutAttributesForElements 可視 (in rect) 範圍內所有單元格的屬性
  • // MARK: UICollectionViewLayout
    extension CardCollectionViewLayout {
        override open func prepare() {
            super.prepare()
            assert(collectionView?.numberOfSections == 1, "Multiple sections aren't supported!")
        override open var collectionViewContentSize: CGSize {
            if collectionView == nil { return CGSize.zero }
            let itemsCount = CGFloat(collectionView!.numberOfItems(inSection: 0))
            return CGSize(width: collectionView!.bounds.width * itemsCount,
                          height: collectionView!.bounds.height)
        override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            if collectionView == nil { return nil }
            let totalItemsCount = collectionView!.numberOfItems(inSection: 0)
            let minVisibleIndex = max(0, Int(collectionView!.contentOffset.x) / Int(collectionView!.bounds.width))
            let maxVisibleIndex = min(totalItemsCount, minVisibleIndex + maximumVisibleItems)
            let contentCenterX = collectionView!.contentOffset.x + collectionView!.bounds.width / 2
            let deltaOffset = Int(collectionView!.contentOffset.x) % Int(collectionView!.bounds.width)
            let percentageDeltaOffset = CGFloat(deltaOffset) / collectionView!.bounds.width
            var attributes = [UICollectionViewLayoutAttributes]()
            for i in minVisibleIndex..<maxVisibleIndex {
                let attribute = computeLayoutAttributesForItem(indexPath: IndexPath(item: i, section: 0),
                                                               minVisibleIndex: minVisibleIndex,
                                                               contentCenterX: contentCenterX,
                                                               deltaOffset: CGFloat(deltaOffset),
                                                               percentageDeltaOffset: percentageDeltaOffset)
                attributes.append(attribute)
            return attributes
        override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
            return true
    

    layoutAttributesForElements 的計算是根據 in rect 範圍推算目前在畫面上的 index 然後計算對應的 attributes 後返回。

    可以進一步的將計算過的 attribute 存起來,在這個方法中直接返回,這樣可以避免一直重複計算。

    layoutAttributesForElements 以及 layout AttributesForItem 的關係?

    在拖動 UICollectionViewCell 的過程中

    collectionView.bounds 的大小是隨著往左滑動的卡片數量增長的嗎?

    Reference

  • 官方文件 - UICollectionViewLayout
  • 這次的學習對象 CardLayout
  • 在 Github 上可以下載本文的 Source Code
  • 本文同步轉載自 CardLayout - 卡片佈局 「Custom UICollectionViewLayout」
  •