swift详解之二十三------------UICollectionView基础用法和简单自定义

UICollectionView基础用法和简单自定义

注:本文通过几个实例来讲讲UICollectionView基本用法

本次要实现的两个效果。感谢猫神提供的教程 OneV’s Den

一个界面是一个普通的流布局 UICollectionViewFlowLayout, 第二个界面是自定义一个圆形布局。加了点手势操作和动画。老规矩。后面会附上源码

首先来看下基本用法

1、UICollectionView基础用法

简单的UICollectionView 相当于GridView一个多列的UItableView,然而UICollectionViewUItableView 的操作也非常相似 。都是会设置有一个DataSource一个delegate 标准的UICollectionView包含三个部分,它们都是UIView的子类:

  • cells 单元格用来展示内容的,可以设置所有的大小 也可以指定不同尺寸和不同的内容

  • Supplementary Views 追加视图 如果你对UITableView比较熟悉的话,可以理解为每个Section的Header或者Footer,用来标记每个section的view

  • decoration Views 装饰视图 这是每个section的背景

UICollectionView和UITableView最大的不同就是UICollectionViewLayout,UICollectionViewLayout可以说是UICollectionView的大脑和中枢,它负责了将各个cell、Supplementary View和decoration Views进行组织,为它们设定各自的属性包括位置、尺寸、层级、形状等等 。。

Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性

下面我们实现一个最简单的Demo

let layout = UICollectionViewFlowLayout()
layout.scrollDirection = UICollectionViewScrollDirection.Vertical  //滚动方向
layout.itemSize = CGSizeMake(60,75) //设置所有cell的size 太重要了 找了半天。(自学就是辛苦呀!!)
layout.minimumLinespacing = 10.0  //上下间隔
layout.minimumInteritemSpacing = 5.0 //左右间隔
layout.headerReferenceSize = CGSizeMake(20,20)
layout.footerReferenceSize = CGSizeMake(20,20)

这里创建了基本的流布局 设置了一些基本属性

然后其他的设置和UITableView差不多

let collect:UICollectionView = UICollectionView(frame: self.view.frame,collectionViewLayout:layout)
collect.backgroundColor = UIColor.whiteColor()
collect.delegate = self
collect.dataSource = self
self.view.addSubview(collect)

因为初始的背景色是黑色的,这里指定了背景色

然后实现下面三个基本的方法,就能正常跑了 。最要是cell的显示方法

//设置分区个数
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
//设置每个分区元素个数
func collectionView(collectionView: UICollectionView,numberOfItemsInSection section: Int) -> Int {
        return  10
    }

//设置元素内容
func collectionView(collectionView: UICollectionView,cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        //这里创建cell  
        return cell
 }

为了得到高效的View,对于cell的重用是必须的,避免了不断生成和销毁对象的操作,在UICollectionView中使用以下方法进行注册

registerClass:forCellWithReuseIdentifier:
registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
registerNib:forCellWithReuseIdentifier:
registerNib:forSupplementaryViewOfKind:withReuseIdentifier:

注册 ,使用一个Identifier ,加入重用队列。要是在重用队列里没有可用的cell的话,runtime将自动帮我们生成并初始化一个可用的cell。

我们这里是自己用xib 画了个cell

一个很简单的cell ,把它的class 设置成我们自定义MyCellContent,MyCellContent 继承自UICollectionViewCell ,把这两个拖成它的成员属性

import UIKit
class MyCellContent: UICollectionViewCell {
    @IBOutlet var contentimage: UIImageView!
    @IBOutlet var contentLabel: UILabel! 
}

然后,在我们的视图控制器中的viewDidLoad进行注册

let nib = UINib(nibName: "MyCollectionCell",bundle: NSBundle.mainBundle())
collect.registerNib(nib,forCellWithReuseIdentifier: "designviewCell")

然后在cellForItemAtIndexPath 里面就能这样取了

let identify:String = "designviewCell"
 let cell =collectionView.dequeueReusableCellWithReuseIdentifier(identify,forIndexPath: indexPath) as! MyCellContent

我们事先创建了个结构体,用来存放cell的img和name

struct CellContent{
    var img:String
    var name:String
}

然后在控制器中声明了一个var dic = Array<CellContent>()

viewDidLoad中初始化。

for i in 1...9{
            dic.append(CellContent(img: "f"+String(i),name: "歪脖子"+String(i)))
}

图片存放的名字就是f1-----f9

func collectionView(collectionView: UICollectionView,numberOfItemsInSection section: Int) -> Int {
        return  self.dic.count
}

这里返回元素个数就可以这么写了

//设置元素内容
func collectionView(collectionView: UICollectionView,cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let identify:String = "designviewCell"
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(identify,forIndexPath: indexPath) as! MyCellContent
    cell.contentView.backgroundColor = UIColor.grayColor()
    cell.contentView.alpha = 0.2
    let img = UIImage(named: (self.dic[indexPath.row] as CellContent).img)
    cell.contentimage.image = img
    cell.contentLabel.text = (self.dic[indexPath.row] as CellContent).name

    return cell
}

这个就可以很简单的设置了 。现在运行,第一个页面效果就有了

但让还有向UITableView中样很多的方法去设置别的,比如单个cell的大小

func collectionView(collectionView: UICollectionView!,layout collectionViewLayout: UICollectionViewLayout!,sizeforItemAtIndexPath indexPath: NSIndexPath!) -> CGSize {
            return CGSizeMake(150,150)
    }

点击cell

//点击元素
    func collectionView(collectionView: UICollectionView,didSelectItemAtIndexPath indexPath: NSIndexPath){
      print("点击了第\(indexPath.section) 分区,第\(indexPath.row) 个元素")
    }

还有很多,自己慢慢玩吧 。下面看看自定义

2、自定义UICollectionViewLayout

UICollectionViewLayoutAttributes一个非常重要的类,先来看看property列表:

@property (nonatomic) CGRect frame
@property (nonatomic) CGPoint center
@property (nonatomic) CGSize size
@property (nonatomic) CATransform3D transform3D
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
@property (nonatomic,getter=isHidden) BOOL hidden

可以看到,UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和DataSource的行为十分类似,当UICollectionView获取布局时将针对每一个indexPath的部件(包括cell,追加视图和装饰视图),向其上的UICollectionViewLayout实例询问该部件的布局信息,这个布局信息,就以UICollectionViewLayoutAttributes的实例的方式给出。

UICollectionViewLayout功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法

-(CGSize)collectionViewContentSize //返回collectionView的内容的尺寸

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
//返回rect中的所有的元素的布局属性

-(UICollectionViewLayoutAttributes )layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath
//返回对应于indexPath的位置的cell的布局属性

-(UICollectionViewLayoutAttributes )layoutAttributesForSupplementaryViewOfKind:(Nsstring )kind atIndexPath:(NSIndexPath *)indexPath
//返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载

-(UICollectionViewLayoutAttributes * )layoutAttributesFordecorationViewOfKind:(Nsstring)decorationViewKind atIndexPath:(NSIndexPath )indexPath
//返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载

-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
//当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。

在初始化一个UICollectionViewLayout实例后,会有一系列准备方法自动调用,以保证layout实例的正确。

  • 首先,-(void)prepareLayout将被调用认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。

  • 之后,-(CGSize) collectionViewContentSize将被调用,以确定collection应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionView的本质是一个scrollView,因此需要这个尺寸来配置滚动行为。

  • 接下来-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被调用,这个没什么值得多说的。初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。

  • 另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIViewsetNeedsLayout方法分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect生成更新后的布局。

以上都是猫神的巨作,他写的很好直接拿来用了

下面看下demo

首先创建一个类继承自UICollectionViewLayou 然后声明一些基本的属性

private var _cellCount:Int?
    private var _collectSize:CGSize?
    private var _center:CGPoint?
    private var _radius:CGFloat?

按照上面的步骤

//一般在该方法中设定一些必要的layout的结构和初始需要的参数等
override func prepareLayout() {
    super.prepareLayout()
    _collectSize = self.collectionView?.frame.size
    _cellCount = self.collectionView?.numberOfItemsInSection(0)
    _center = CGPointMake(_collectSize!.width / 2.0,_collectSize!.height / 2.0);
    _radius = min(_collectSize!.width,_collectSize!.height)/2.5
}

这个方法初始化了一些基本信息

//内容区域的总大小 (不是可见区域)
    override func collectionViewContentSize() -> CGSize {
        return _collectSize!  //这里不用可见区域吧
    }

可见区域

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        var attributesArray = [UICollectionViewLayoutAttributes]()
        if let count = self._cellCount {
            for i in 0 ..< count{
                //这里利用了-layoutAttributesForItemAtIndexPath:来获取attributes
                let indexPath = NSIndexPath(forItem: i,inSection: 0)
                let attributes =  self.layoutAttributesForItemAtIndexPath(indexPath)
                attributesArray.append(attributes!)
            }
        }
        return attributesArray
    }

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attrs = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
        attrs.size = CGSizeMake(ITEM_SIZE,ITEM_SIZE)
        let x = Double(_center!.x) + Double(_radius!) * cos(Double(2 * indexPath.item) * M_PI/Double(_cellCount!))
        let y = Double(_center!.y) + Double(_radius!) * sin(Double(2 * indexPath.item) * M_PI/Double(_cellCount!))
        attrs.center = CGPointMake( CGFloat(x),CGFloat(y));
        return attrs
    }

这个方法layoutAttributesForItemAtIndexPathUICollectionViewLayoutAttributes 的一些属性进行设置 ,前面列出过 ,然后layoutAttributesForElementsInRect 方法返回所有UICollectionViewLayoutAttributes , 以数组的方式

然后再使用的时候把基本用法里的layout换掉

layout = MyCollectionViewLayout()

  collect = UICollectionView(frame: self.view.frame,collectionViewLayout:layout)
  collect.backgroundColor = UIColor.whiteColor()
  collect.delegate = self
  collect.dataSource = self

这样运行 , 圆就出现了

if(layout is MyCollectionViewLayout){
            layout = UICollectionViewFlowLayout()
            (layout as! UICollectionViewFlowLayout).scrollDirection = UICollectionViewScrollDirection.Vertical  //滚动方向
            (layout as! UICollectionViewFlowLayout).itemSize = CGSizeMake(60,75)
        }else{
            layout = MyCollectionViewLayout()
        }
        self.collect.setCollectionViewLayout(layout,animated: true)

可以通过setCollectionViewLayout 方法来切换layout

然后给这个界面添加手势

//注册tap手势事件
let tapRecognizer = UITapGestureRecognizer(target: self,action: "handleTap:")
collect.addGestureRecognizer(tapRecognizer)
func handleTap(sender:UITapGestureRecognizer){
        if sender.state == UIGestureRecognizerState.Ended{
            let tapPoint = sender.locationInView(self.collect)
            if let  indexPath = self.collect.indexPathForItemAtPoint(tapPoint)
            {
                //点击了cell
                //这个方法可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。
                print("------")
                self.collect.performBatchUpdates({ () -> Void in self.collect.deleteItemsAtIndexPaths([indexPath]) self.dic.removeAtIndex(indexPath.row) },completion: nil) }else{ let val = arc4random_uniform(8)+1 self.dic.append(CellContent(img: "f"+String(val),name: "歪脖子"+String(val))) self.collect.insertItemsAtIndexPaths([NSIndexPath(forItem: Int(val),inSection: 0)]) // dispatch_async(dispatch_get_global_queue(0,0),{ () -> Void in // let val = arc4random_uniform(9) // self.dic.append(CellContent(img: "f"+String(val),name: "歪脖子"+String(val))) // dispatch_async(dispatch_get_main_queue()) { // self.collect.reloadData() // // } // }) //点击了不是cell的区域 print("+++++++") } } }

我注释掉这段GCD的代码也是可以执行的 ,就是没有动画 。
这个方法performBatchUpdates:completion 可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。相应的动画由layout中的下列四个方法来定义:

initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingdecorationElementOfKind:atIndexPath:
finalLayoutAttributesFordisappearingItemAtIndexPath:
finalLayoutAttributesFordisappearingdecorationElementOfKind:atIndexPath:

认的动画是这样的

我们可以自定义动画

每次重新给出layout时都会调用prepareLayout,这样在以后如果有collectionView大小变化的需求时也可以自动适应变化。

override func initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {

        // Must call super
        var attributes = super.initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath)

        if self.insertIndexPaths.contains(itemIndexPath) {

            if let _ = attributes{
                attributes = self.layoutAttributesForItemAtIndexPath(itemIndexPath)
            }

            // Configure attributes ...
            attributes!.alpha = 0.0;
            attributes!.center =  CGPointMake(_center!.x,_center!.y);
            //attributes?.size = CGSizeMake(1000,1000)
        }

        return attributes;
    }

    override func prepareForCollectionViewUpdates(updateItems: [UICollectionViewUpdateItem]) {
        super.prepareForCollectionViewUpdates(updateItems)
        self.insertIndexPaths = [NSIndexPath]()
        for update in updateItems{
            if update.updateAction == UICollectionUpdateAction.Insert{
                self.insertIndexPaths.append(update.indexPathAfterUpdate)
            }
    }

首先会调用prepareForCollectionViewUpdates,我们在这里拿到那个新增的NSIndexPath ,然后initialLayoutAttributesForAppearingItemAtIndexPath 在这方法中设置一些初始位置。

看下效果

这个是从中间散出去的 ,同理也可以搞一些别的效果 。大概就这些吧。当然UICollectionView可以玩的还很多,期待大家一起探索。多分享哦!
(本实例使用xcode 7 bate , swift 2.0)
最后附上源码:https://github.com/smalldu/SwiftStudy

相关文章

软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘...
现实生活中,我们听到的声音都是时间连续的,我们称为这种信...
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿...
【Android App】实战项目之仿抖音的短视频分享App(附源码和...
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至...
因为我既对接过session、cookie,也对接过JWT,今年因为工作...