Swift 自定义UITableView

github demo:https://github.com/LINGLemon/LXFSwiftApp

UITableView是我们开发过程中比较常用的,用于显示一系列对象,UITableView继承自UIScrollViewUIScrollView可以在任意方向滑动,而UITableView只在垂直方向上滑动。UITableView中的内容是由UITableViewCell负责显示的。

1. UITableViewCell

  • UITableViewUITableViewCell组成,UITabelViewCell负责显示数据。
  • UITableView的每一行,即每一个UITableViewCell显示一条项目。
  • UITableViewCell对象的数量不受限制,仅由设备内存决定。
  • UITableViewCell类定义了单元格在UITableView中的属性和行为。

创建 UITableViewCell 的时候,你可以自定义一个 cell ,或者使用系统预定义的几种格式。系统预定义的 cell 提供了 textLabeldetailTextLabel属性和imageView属性用来设置cell的内容和图片。样式由UITableViewCellStyle枚举来控制:

枚举类型描述
.default包含一个左侧的可选图像视图,和一个左对齐的标签对象。
.value1包含一个左侧的可选视图和一个左对齐的标签对象,在单元格右侧还有一个灰色、右对齐的标签对象。
.value2包含一个左侧、右对齐的蓝色文字标签对象和一个右侧的左对齐的标签对象。
.subtitle包含一个左侧的可选图像视图,和一个左对齐的标签对象,在这个标签对象下方,还有一个字体较小的标签对象。

2. 创建一个UITableView

创建UITableView,首先是实例化一个UITableView对象,还要涉及到它的代理UITabelViewDataSource、UITableViewDelegate,在UITableViewDataSource代理方法中定义UITableViewCell的样式。

import UIKit

class ViewController:UIViewController,UITableViewDataSource,UITableViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let tableView = UITableView(frame: view.bounds, style: .grouped)
        tableView.backgroundColor = UIColor.white;
        view.addSubview(tableView)
        tableView.dataSource = self
        tableView.delegate = self
    }

//MARK: UITableViewDataSource
    // cell的个数
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    // UITableViewCell
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellid = "testCellID"
        var cell = tableView.dequeueReusableCell(withIdentifier: cellid)
        if cell==nil {
            cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellid)
        }
        
        cell?.textLabel?.text = "这个是标题~"
        cell?.detailTextLabel?.text = "这里是内容了油~"
        cell?.imageView?.image = UIImage(named:"Expense_success")
        return cell!
    }
  
//MARK: UITableViewDelegate
    // 设置cell高度
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 44.0
    }
    // 选中cell后执行此方法
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print(indexPath.row)
    }  
}

  • 添加了代理协议UITableViewDataSource,主要用来给UITableView提供数据来源,并用来处理数据源的变化。
    它的主要带你方法:

    • tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath):
      初始化和复用指定索引位置的UITableViewCell必须实现
    • tableView(_ tableView: UITableView, numberOfRowsInSection section: Int):
      设置某一章节(section)中的单元格数量,必须实现
    • numberOfSections(in tableView: UITableView):
      设置表格中的章节(section)个数。
    • tableView(_ tableView: UITableView, titleForHeaderInSection section: Int):
      设置指定章节的标题文字,如果不设置或代理返回值为nil,不显示。
    • tableView(_ tableView: UITableView, titleForFooterInSection section: Int):
      设置章节脚部标题文字,如果不设置或代理返回值为nil,不显示。
    • tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath):
      设置表格中指定索引位置的cell是否可编辑,可编辑的cell会显示插入和删除的图标。
    • tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath):
      当完成插入或删除操作时会调用此方法。
    • tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath):
      设置指定索引位置的cell是否可以通过拖动的方式,改变它的位置。
    • tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath):
      cell从一个位置拖动到另一个位置时调用此方法。
  • 然后我们进行了实例化,设置位置和尺寸,然后设置UITableView的数据源为当前视图控制器对象,即设置代理UITableViewDataSource

  • 实现数据源协议定义中的方法,从而设置章节中cell的个数,以及对cell进行初始化和复用设置。

  • indexPathNSIndexPath类用来描述在嵌套数列的树种指定节点的路径,即索引路径。索引路径中的每一个索引,都用来表示一个节点的子数组中的指定索引位置。事实上,NSIndexPath描述了一整个数列,表示在表格视图中指定的章节中的指定行。
    UITableView中的索引路径包括两个元素,第一个元素section是表格的章节序号,第二个元素row表示章节中的行序号。

  • 还添加了UITableViewDelegate代理协议,它的主要作用是提供一些可选的方法,用来控制表格的选择、指定章节的头和尾的显示、单元格内容的复制和粘贴以及协助完成单元格的排序等功能。
    主要代理方法有:

    • tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath):
      设置单元格高度,每当表格需要显示时,都会调用此方法。
    • tableView(_ tableView: UITableView, heightForHeaderInSection section: Int)
      设置某一索引下的章节头部的高度。
    • tableView(_ tableView: UITableView, heightForFooterInSection section: Int):
      设置某一索引下的章节尾部的高度。
    • tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath):
      当指定索引位置上的单元格即将显示时,调用此方法。此方法是委托对象有机会在单元格显示之前重写其状态属性,如背景颜色等。
    • tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath):
      当用户点击选择指定索引位置的单元格时,调用此方法。
    • tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath):
      当用户点击一个已经被选中的单元格时,调用此方法。

3. UITableView 复用机制

复用机制在很多地方都有应用,比如去饭店吃饭,盘子、碗都不是一次性的,当客人使用过之后,会经过消毒、清洗。然后再给下一批客人使用。如果每个客人使用过之后都换一批新的话,那成本太高了。

UITableView也采用复用机制,一个UITableView可能需要显示100条数据,但屏幕尺寸有限,假设一次只能显示9条,如果滑动一下的话,会显示出第10条的一部分,所以当前屏幕在这种情况下最多只能显示出10条。
所以系统只需要创建10个UITableViewCell对象就好,当手指从下往上滑动时,回收处于屏幕之外的最上方单元格,并放置到表格最下方,作为将要显示的11个单元格。当UITableView对象从上往下滑动时,也是同样的服用机制。

在上面的代码中:

        let cellid = "testCellID"
        var cell = tableView.dequeueReusableCell(withIdentifier: cellid)

dequeueReusableCell方法的作用是从单元格对象池中获取指定类型并可复用的单元格对象。

        if cell==nil {
            cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellid)
        }

如果从对象池中没有获得可复用的单元格,就调用实例化方法实例一个某一类型的、可复用的单元格。

  • style参数: 枚举常量,用于表示单元格的样式。
  • reuseIdentifier: 作为一个字符串类型的参数,它用来标识具有相同类型的、可复用的单元格。对于相同类型的单元格,需要使用相同的reuseIdentifier参数。

4. 自定义UITableViewCell

一般对于相对复杂一些的显示内容,我们会创建一个UITableViewCell的类文件。

 


Subclass of 写UITableViewCell

 

上代码:


import UIKit

class NewTableViewCell: UITableViewCell {

    let width:CGFloat = UIScreen.main.bounds.width
    var userLabel:UILabel!      // 名字
    var birthdayLabel:UILabel!  // 出生日期
    var sexLabel:UILabel!       // 性别
    var iconImv:UIImageView!    // 头像
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        // 头像
        iconImv = UIImageView(frame: CGRect(x: 20, y: 15, width: 44, height: 44))
        iconImv.layer.masksToBounds = true
        iconImv.layer.cornerRadius = 22.0
        
        // 名字
        userLabel = UILabel(frame: CGRect(x: 74, y: 18, width: 70, height: 15))
        userLabel.textColor = UIColor.black
        userLabel.font = UIFont.boldSystemFont(ofSize: 15)
        userLabel.textAlignment = .left
        
        // 性别
        sexLabel = UILabel(frame: CGRect(x: 150, y: 20, width: 50, height: 13))
        sexLabel.textColor = UIColor.black
        sexLabel.font = UIFont.systemFont(ofSize: 13)
        sexLabel.textAlignment = .left
        
        // 出生日期
        birthdayLabel = UILabel(frame: CGRect(x: 74, y: 49, width: width-94, height: 13))
        birthdayLabel.textColor = UIColor.gray
        birthdayLabel.font = UIFont.systemFont(ofSize: 13)
        birthdayLabel.textAlignment = .left
        
        contentView.addSubview(iconImv)
        contentView.addSubview(userLabel)
        contentView.addSubview(sexLabel)
        contentView.addSubview(birthdayLabel)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    } 
}
  • 上面代码中,首先给NewTableViewCell添加了五个属性:width屏幕宽度、iconImv头像、userLabel用户名、sexLabel性别和birthdayLabel生日。
  • 然后添加实例化方法:init(style: UITableViewCellStyle, reuseIdentifier: String?)并在方法中实例化定义的4个属性,将他们添加到屏幕上。
  • 最后实现继承自UITableViewCell类所必须的init?(coder aDecoder: NSCoder)构造函数。

现在我们完成了NewTableViewCell的创建,再到ViewController.swift类文件中,调用这个自定义单元格类。

import UIKit

class ViewController:UIViewController,UITableViewDataSource,UITableViewDelegate {

    var dataSource = [[String:String]()]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let tableView = UITableView(frame: view.bounds, style: .grouped)
        tableView.backgroundColor = UIColor.white;
        view.addSubview(tableView)
        tableView.dataSource = self
        tableView.delegate = self
        
        dataSource = [
          ["name":"王小明","sex":"男","icon":"my_def_photo","birthday":"2017-10-11"],
          ["name":"李磊","sex":"男","icon":"my_def_photo","birthday":"2011-12-30"],
          ["name":"韩梅","sex":"女","icon":"my_def_photo","birthday":"2014-9-10"],
          ["name":"JIM","sex":"男","icon":"my_def_photo","birthday":"2008-10-1"]]
        tableView.reloadData()
    }

//MARK: UITableViewDataSource
    // cell的个数
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }
    // UITableViewCell
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellid = "testCellID"
        var cell:NewTableViewCell? = tableView.dequeueReusableCell(withIdentifier: cellid) as? NewTableViewCell
        if cell==nil {
            cell = NewTableViewCell(style: .subtitle, reuseIdentifier: cellid)
        }
        let dict:Dictionary = dataSource[indexPath.row]
        cell?.iconImv.image = UIImage(named: dict["icon"]!)
        cell?.userLabel.text = dict["name"]
        cell?.sexLabel.text = dict["sex"]
        cell?.birthdayLabel.text = dict["birthday"]
        return cell!
    }
    
//MARK: UITableViewDelegate
    // 设置cell高度
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 74.0
    }
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 20
    }
    // 选中cell后执行此方法
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print(indexPath.row)
    }
}
  • 这里实例了一个数组,数组内的元素是字典,用来存放需要展示的数据。
  • 然后注意tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)代理方法,返回参数是dataSource.count,意思是数组中有几条数据就展示几个Cell。
  • 接下来就是修改tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)方法中的Cell了。并根据dataSource数组中的数据对cell的元素进行赋值。
  • 后面我们还修改了cell的高度,和header的高度。跑一下项目:

5. 添加索引和章节(Section)

最常见的带有索引的TableView就是通讯录了吧,在TableView的右侧有一个垂直的索引序列,点击索引序列的元素可在表格中迅速定位到指定的位置,尤其是拥有大量数据的时候。

先来看一下索引需要用到的代理方法:

  • numberOfSections(in tableView: UITableView)
    设置TableView中章节(Section的数量)不设置默认为1。
  • tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)
    在指定章节中,cell的个数。
    -tableView(_ tableView: UITableView, titleForHeaderInSection section: Int)
    设置章节标题文字,返回结果为字符串,如果返回为nil,则不显示标题。
  • sectionIndexTitles(for tableView: UITableView)
    设置在表格右侧显示的索引序列的内容,返回结果为一个字符串数组
  • tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
    TableViewCell初始化和复用

开始之前,需要先创建索引表格所需的数据源,刚才的例子中是添加了一个 数组 作为数据源,这里索引的话需要一个 字典 来作为数据源。
开发中需要数据源通常各种各样,不如加载本地文本文件和plist文件,或者从服务器请求书院,通过返回的JSON或XML作为数据源。这里我们仅创建一个字典作为数据源。

代码搞完了,上代码:

import UIKit

class IndexsViewController: UIViewController,UITableViewDataSource {

    let contents:Dictionary<String,[String]> =
        ["A":["安其拉"],
         "B":["步惊云","不知火舞","白起","扁鹊"],
         "C":["程咬金","成吉思汗","蔡文姬","曹操"],
         "D":["妲己","狄仁杰","典韦","貂蝉","达摩","大乔","东皇太一"],
         "G":["高渐离","关羽","宫本武藏","干将莫邪","鬼谷子"],
         "H":["韩信","后羿","花木兰","黄忠"],
         "J":["荆轲","姜子牙"],
         "L":["老夫子","刘邦","刘婵","鲁班七号","兰陵王","露娜","廉颇","李元芳","刘备","李白","吕布"],
         "M":["墨子","芈月"],
         "N":["牛魔","娜可露露","哪吒","女娲"],
         "P":["庞统",""],
         "S":["孙膑","孙尚香","孙悟空"],
         "W":["王昭君","武则天"],
         "X":["项羽","小乔"],
         "Y":["亚瑟","虞姬","嬴政"],
         "Z":["周瑜","庄周","甄姬","钟无艳","张飞","张良","钟馗","赵云","诸葛亮"]]
    var keys:[String] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationController?.isNavigationBarHidden = false
        // 把字典里的key拿出来放到一个数组中,备用,作为章节的标题
        keys = contents.keys.sorted()
        
        let tableView = UITableView(frame: view.bounds, style: .plain)
        tableView.dataSource = self
        view.addSubview(tableView)
    }
//MARK: UITableViewDataSource
    //MARK: 章节的个数
    func numberOfSections(in tableView: UITableView) -> Int {
        return keys.count
    }
    //MARK: 某一章节cell个数
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let arr = contents[keys[section]]
        return (arr?.count)!
    }
    //MARK: 初始化cell
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCell(withIdentifier: "indexsCellId")
        if cell==nil {
            cell = UITableViewCell(style: .default, reuseIdentifier: "indexsCellId")
        }
        let arr = contents[keys[indexPath.section]]
        cell?.textLabel?.text = arr?[indexPath.row]
        return cell!
    }
    //MARK: 每一个章节的标题
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return keys[section]
    }
    //MARK: 设置索引序列内容
    func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        return keys
    }  
}
  • 首先我们来确定一下数据源,本来是想用人名的,不过想想万一暴露了什么被发现~~,然后就用了农药里的英雄,劳逸结合,劳逸结合。这不是重点
  • 然后我们定义了一个数组keys,用来存放数据源里面的key
  • 实例化TableView,设置代理,实现需要用到的代理方法。这还不是重点。
  • 实现代理方法,都有注释,就不细说了,设置章节个数、设置每个章节的cell个数,初始化cell、设置每一个章节的头部标题。这也不是重点
  • 然后实现代理,设置索引内容:sectionIndexTitles(for tableView: UITableView)这才是重点,添加了这个方法,右侧才会出现索引序列。
    点击索引条目,会迅速的到达点击索引内容的部分。

需要注意的是: 实例化的时候,init方法第二个参数有两个值:.plain.grouped
如果不添加章节头部的话,基本看不出这两个值给tableView带来的变化。
但在这里,是有区别的:

  • .plain:如果传的是这个参数,向上滑动,当章节头部滑动到UITableVeiw的上方边界时,章节头部会停在边界位置,知道下一个章节头部到达它的位置,它才会继续向上滑动,下一个章节头部会占据它的位置。
  • . grouped:就正常滑动,没啥影响。

哦,除了这个还是有别的区别的,当设置的是plain,如果cell的个数不够扑满屏幕,系统会一直创建空的cell来扑满,可以试一下,能看到一条一条的横线,cell的分割线,如果是设置的grouped就不会有这种情况。

上张图:

6. cell的选择和取消选择

本来想等着看WWDC的,结果睡着了,早上看了新闻,又特么要做适配了,还特么这么贵。

这个有很多场景会遇到的,比如说,我们之前项目里有支付功能,需要设置一下默认支付方式,默认微信还是支付宝,产品给的UI就是一表格。

需求:三种支付方式,只能单选。

import UIKit

class SelectViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
    
    // 数据源,
    var dataSource = [["微信支付":"select"],["支付宝支付":"on"],["银联支付":"no"]]
    
    override func viewDidLoad() {
        super.viewDidLoad()

        let tableView = UITableView(frame: view.bounds, style: .grouped)
        tableView.backgroundColor = UIColor.white
        tableView.delegate = self
        tableView.dataSource = self
        view.addSubview(tableView)
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 0.01
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCell(withIdentifier: "selectCell")
        if cell == nil {
            cell = UITableViewCell(style: .default, reuseIdentifier: "selectCell")
            cell?.selectionStyle = .none
        }
        let dic = dataSource[indexPath.row] as Dictionary
        cell?.textLabel?.text = dic.keys.first
        if dic.values.first == "select" {
            cell?.accessoryType = .checkmark
        } else {
            cell?.accessoryType = .none
        }
        return cell!
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
       
        var i = 0
        for var dict in dataSource {
            
            if i == indexPath.row {
                dict[dict.keys.first!] = "select"
                dataSource[i] = dict
            } else {
                dict[dict.keys.first!] = "no"
                dataSource[i] = dict
            }
            i = i+1
        }
        tableView.reloadData()
    }
    
}
  • 先说一下思路:首先定义了一个数据源,因为是单选,所以用的是数组嵌套字典,key就是支付方式,value是代表是否选中个状态的string,如果选中,就把数据源里这个位置的value变成select,但是只能单选,所以还需要把其他的都变成no

  • 直接说重点:cell的实例化和复用代理方法中,可以看到,如果数据源里的valueselecrt,就把 accessoryType属性设置成checkmark

  • tableView(_:, didSelectRowAt:)方法中,可以看到,用For循环来修改元数据中选中状态的value,然后调用reloadData()方法刷新cell。

  • cellaccessoryType属性的值是枚举UITableViewCellAccessoryType:

枚举类型说明
none没有任何的样式
detailButton右侧蓝色的圆圈,中间带叹号
detailDisclosureButton右侧蓝色圆圈带叹号,它的右侧还有一个灰色向右的箭头
disclosureIndicator右侧一个灰色的向右箭头
checkmark右侧蓝色对号

7. cell的插入和删除

插入和删除设计到的两个代理方法:

  • tableView(_ tableView:, editingStyleForRowAt indexPath:)
    确定编辑模式,Add or Delete
  • tableView(_ tableView:, commit editingStyle:, forRowAt indexPath:)
    当执行编辑操作时,调用此方法

和一个开启TableView编辑模式的方法:

  • setEditing(_ editing:, animated:)
    • editing: 是否开启编辑状态
    • animated: 是否有动画效果
import UIKit

class AddViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {

    var dataSource = [["微信","支付宝","银联"],["微信","支付宝","银联"]]
    var tableView:UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "支付方式"
        
        let rightBar = UIBarButtonItem.init(barButtonSystemItem: .add, target: self, action: #selector(addButtonClick))
        navigationItem.rightBarButtonItem = rightBar
        
        tableView = UITableView(frame: view.bounds, style: .grouped)
        tableView.delegate = self
        tableView.dataSource = self
        view.addSubview(tableView)
        
    }
    
    //MARK: 导航栏右侧按钮,点击开启或关闭编辑模式
    func addButtonClick() {
        tableView.setEditing(!tableView.isEditing, animated: true)
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return dataSource.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource[section].count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCell(withIdentifier: "addCell")
        if cell == nil {
            cell = UITableViewCell(style: .default, reuseIdentifier: "addCell")
            cell?.selectionStyle = .none
        }
        let arr = dataSource[indexPath.section] as Array
        cell?.textLabel?.text = arr[indexPath.row] as String
        return cell!
    }
    
    //MARK: 编辑模式,增加还是删除
    func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
        if indexPath.section == 1 {
            return .delete
        }
        return .insert
    }
    //MARK: 执行编辑操作时,调用此方法
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        var arr = dataSource[indexPath.section] as Array
        if editingStyle == .insert {
            arr.insert("Apple Pay", at: indexPath.row)
            dataSource[indexPath.section] = arr
            tableView.insertRows(at: [indexPath], with: .right)
        } else {
            arr.remove(at: indexPath.row)
            dataSource[indexPath.section] = arr
            tableView.deleteRows(at: [indexPath], with: .left)
        }
    }
    
}
  • 说下思路,为了方便举例,我设置了两个Section,第一个Section做增加操作,第二个Section做删除操作,所以数据源里放的是两个数组。
  • 在导航条右侧添加了一个按钮,用来确定编辑状态是否开启。
  • setEditing方法上面说过了,就不说了。看下面两个代理方法。
  • tableView(_: ,editingStyleForRowAt:)方法返回参数是UITableViewCellEditingStyle:
    • insert: 添加操作
    • delete: 删除操作
    • none: 没有任何操作
  • tableView(_:, commit editingStyle:, forRowAt:),当执行了编辑操作,就会调起这个方法,你可以通过编辑状态对TableView和数据源进行操作。注意一定要把数据源和视图显示操作保持一致,不然很容易数组越界导致崩溃。
  • insertRows(at indexPaths: [IndexPath], with animation: UITableViewRowAnimation)deleteRows(at indexPaths: [IndexPath], with animation: UITableViewRowAnimation)方法的作用都是对TableViewCell的条数进行操作,一个增加一个删除
  • UITableViewRowAnimation枚举的作用是控制操作的动画:
属性说明
fade以淡入淡出的方式显示或移除
right添加或删除时,从右侧滑入或划出
left添加或删除时,从左侧滑入或划出
top添加或删除时,从上方滑入或划出
bottom添加或删除时,从底部滑入或划出
middle表格视图将尽量使新旧cell居中显示在曾经或将要显示的位置
automatic自动选择适合自身的动画方式
none采用默认动画方式

8. cell位置移动功能

支持重新排序(Reordering)功能的TableView,允许用户拖动位于单元格右侧的排序图标,来重新排序TableView中的单元格。
排序功能一般用到的还是比较多的,我曾经做过一个类似PPT的功能,要求可以更换演示版页的排列方式,就是用的这个功能。

移动功能同样设计到了两个代理方法:

  • tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath)
    设置cell是否可移动
  • tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
    每次移动结束后会调用此方法

移动功能同样需要开启编辑模式setEditing(_: ,animated:)

import UIKit

class ReorderViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {

    var dataSource = ["微信","支付宝","银联","易宝"]
    var tableView:UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let rightBar = UIBarButtonItem.init(barButtonSystemItem: .add, target: self, action: #selector(ReorderButtonClick))
        navigationItem.rightBarButtonItem = rightBar
        
        tableView = UITableView(frame: view.bounds, style: .grouped)
        tableView.delegate = self
        tableView.dataSource = self
        view.addSubview(tableView)
    }
    
    func ReorderButtonClick() {
        tableView.setEditing(!tableView.isEditing, animated: true)
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 0.01
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCell(withIdentifier: "addCell")
        if cell == nil {
            cell = UITableViewCell(style: .default, reuseIdentifier: "addCell")
            cell?.selectionStyle = .none
        }
        cell?.textLabel?.text = dataSource[indexPath.row] as String
        return cell!
    }
    //MARK: 选择编辑模式,不删除也不添加就设置为none
    func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
        return .none
    }
    //MARK: 设置cell是否可移动
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    //MARK: 移动结束后调用此方法
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        
        let data = dataSource[sourceIndexPath.row]
        dataSource.remove(at: sourceIndexPath.row)
        dataSource.insert(data, at: destinationIndexPath.row)
    }
}

  • 说下思路,先开启TableView的编辑模式,但我们现在不需要添加或删除,只需要移动功能。 直接看后面三个代理方法:
  • tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath):
    因为我们不需要添加或删除操作,所以调用此方法,并设置返回none
  • tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath):
    移动返回true
  • tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath):
    主要说这个,这个方法的作用是移动结束后调用,一般我们用它来同步数据源中的数据保持与视图同步,从代码里可以看出,三个参数的作用:
    • 第一个就是移动的tableView了,如果当前视图只有一个tableView不用管他。
    • 第二个参数是移动的cell曾经的位置。
    • 第三个参数是移动的最后位置。

没有PS工具,我是把图片放到word里面截图出来的。

 

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好,关于 Swift 封装 UITableView 和 UICollectionView 的问题,我可以给你一些基本的介绍。 首先,UITableView 和 UICollectionView 都是 iOS 平台中常用的视图控件,用于展示列表和网格等数据。在 Swift 中,我们可以通过继承 UITableView 和 UICollectionView 类来自定义我们自己的表格和网格视图。 下面是一个简单的示例代码,展示如何封装 UITableView: ```swift import UIKit class MyTableView: UITableView, UITableViewDataSource, UITableViewDelegate { var data: [String] = [] override init(frame: CGRect, style: UITableView.Style) { super.init(frame: frame, style: style) self.dataSource = self self.delegate = self // 注册单元格 self.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // UITableViewDataSource 协议方法 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.data.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = self.data[indexPath.row] return cell } // UITableViewDelegate 协议方法 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("选中了第 \(indexPath.row) 行") } } ``` 在这个示例代码中,我们自定义了一个名为 `MyTableView` 的类,继承自 `UITableView`。我们在 `init` 方法中设置了数据源和代理,并注册了一个单元格类型。在 `UITableViewDataSource` 和 `UITableViewDelegate` 协议方法中,我们实现了表格的行数、单元格内容和选中事件的处理。 类似地,我们也可以使用类似的方式封装 UICollectionView。需要注意的是,UICollectionViewDelegate 和 UICollectionViewDataSource 两个协议方法和 UITableView 中的函数名和实现方式略有不同,需要根据实际情况来进行调整。 希望这个简单的示例代码可以对你有所帮助。如果你有其他关于 Swift 的问题,欢迎随时提出!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值