可以設定畫面上最多會有幾張卡片,卡片向做滑動可以滑出畫面,向右可以將卡片滑入。
CardCell
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」