当前位置: > > > Swift - 时间轴效果的实现(附样例)

Swift - 时间轴效果的实现(附样例)

时间轴展示也是一种比较常见的展现形式。一般用于展示以时间为主线的一连串事件,比如日常活动、企业发展历程、历史事件等等。时间轴可以运用于不同领域,最大的作用就是把过去的事物系统化、完整化、精确化。本文演示如何使用 Swift 实现一个垂直时间轴。

1,效果图

(1)这里做一个日常消费记录的时间轴列表。按日期从远到近纵向排列。
(2)时间线上每一个小图标代表一条消费记录,图标旁会显示具体的消费详情。
(3)消费详情包括:消费日期、金额、内容、以及备注信息(灰色区域)。其中备注信息不是必须的。
(4)由于备注信息不是必填项,且内容长度不固定。所以每个消费记录条目高度都是自适应的。

2,实现原理

(1)由于页面需要进行大量的约束设置以及修改操作,所以我这里使用了一个第三方的约束库 SnapKitSnapKit 详细使用方法,可以参考我过去写的文章(点此查看)。
(2)整个时间轴展示页面使用的仍是 tableView,不过隐藏单元格分隔线,并设置行高自适应。
(3)通过自定义单元格来实现每条消费记录。其中单元格内时间线是使用背景色为灰色,宽度为1的 UIView 实现。
(4)时间轴上图标图片原来是矩形的,这里通过设置 UIImageView 圆角半径,将其显示成圆形。

3,样例代码

(1)自定义单元格(TimeLineCell.swift
import UIKit
import SnapKit

class TimeLineCell: UITableViewCell {
    //时间轴线上的图标
    var timeLineIcon: UIImageView!
    //费用显示文本标签
    var costLabel: UILabel!
    //消费时间文本标签
    var dateTimeLabel: UILabel!
    //消费条目文本标签
    var titleLabel: UILabel!
    //备注标签容器
    var containView: UIView!
    //备注显示文本标签
    var appendixLabel: UILabel!
    //图标上半部分的时间线
    var forepartTimeLineLabel: UIView!
    //图标下半部分的时间线
    var backpartTimeLineLabel: UIView!
    //备注标签容器的高度约束(将高度设为0)
    var heightContraint: Constraint?
    //时间线离左右的横向间距
    let horizontalGap: CGFloat = 25
    
    //是否有备注
    var hasAppendix:Bool = false {
        didSet
        {
            if hasAppendix {
                //有备注则高度约束实效,备注容器高度更具内容自适应
                self.heightContraint?.deactivate()
            }else{
                //没有备注高度约束生效,高度变成0
                self.heightContraint?.activate()
            }
        }
    }
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        setSubviews()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setSubviews(){
        //图标上半部分的时间线初始化。并设置相关约束。
        forepartTimeLineLabel = UIView()
        forepartTimeLineLabel.backgroundColor = UIColor.lightGray
        contentView.addSubview(forepartTimeLineLabel)
        forepartTimeLineLabel.snp.makeConstraints { (make) -> Void in
            make.left.equalTo(contentView).offset(horizontalGap)
            make.height.equalTo(25)
            make.width.equalTo(1)
            make.top.equalTo(contentView)
        }
        
        //时间线上图标初始化。并设置相关约束。
        timeLineIcon = UIImageView()
        timeLineIcon.layer.masksToBounds = true
        //设置圆角半径,显示成圆形。
        timeLineIcon.layer.cornerRadius = 12
        contentView.addSubview(timeLineIcon)
        timeLineIcon.snp.makeConstraints { (make) -> Void in
            make.top.equalTo(forepartTimeLineLabel.snp.bottom)
            make.centerX.equalTo(forepartTimeLineLabel.snp.centerX)
            make.width.height.equalTo(25)
        }
        
        //费用显示文本标签初始化。并设置相关约束。
        costLabel = UILabel()
        costLabel.font = UIFont.systemFont(ofSize: 16)
        costLabel.sizeToFit()
        contentView.addSubview(costLabel)
        costLabel.snp.makeConstraints { (make) -> Void in
            make.top.equalTo(timeLineIcon)
            make.left.equalTo(forepartTimeLineLabel.snp.right).offset(horizontalGap)
        }
        
        //消费时间文本标签初始化。并设置相关约束。
        dateTimeLabel = UILabel()
        dateTimeLabel.font = UIFont.systemFont(ofSize: 12)
        dateTimeLabel.textColor = UIColor.lightGray
        dateTimeLabel.sizeToFit()
        contentView.addSubview(dateTimeLabel)
        dateTimeLabel.snp.makeConstraints { (make) -> Void in
            make.left.equalTo(costLabel.snp.right).offset(5)
            make.centerY.equalTo(costLabel)
        }
        
        //消费条目文本标签初始化。并设置相关约束。
        titleLabel = UILabel()
        titleLabel.font = UIFont.systemFont(ofSize: 16)
        titleLabel.sizeToFit()
        contentView.addSubview(titleLabel)
        titleLabel.snp.makeConstraints { (make) -> Void in
            make.left.equalTo(costLabel)
            make.top.equalTo(costLabel.snp.bottom).offset(10)
        }
        
        //备注便签容器初始化。并设置相关约束。
        containView = UIView()
        containView.backgroundColor = UIColor(red: 200/255, green: 200/255,
                                              blue: 200/255, alpha: 0.3)
        containView.layer.cornerRadius = 3
        contentView.addSubview(containView)
        containView.snp.makeConstraints { (make) -> Void in
            make.top.equalTo(titleLabel.snp.bottom).offset(10)
            make.left.equalTo(forepartTimeLineLabel.snp.right).offset(10)
            make.right.equalTo(contentView).offset(-10)
        }
        
        //备注文本标签初始化。并设置相关约束。
        appendixLabel = UILabel()
        appendixLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
        appendixLabel.font = UIFont.systemFont(ofSize: 16)
        appendixLabel.numberOfLines = 4
        appendixLabel.sizeToFit()
        containView.addSubview(appendixLabel)
        appendixLabel.snp.makeConstraints { (make) -> Void in
            make.edges.equalTo(containView).inset(UIEdgeInsetsMake(10, 10, 10, 10))
        }
        
        containView.snp.makeConstraints { (make) -> Void in
            self.heightContraint = make.height.equalTo(0).constraint
            make.bottom.equalTo(contentView.snp.bottom).offset(-10)
        }
        
        //图标下半部分的时间线初始化。并设置相关约束。
        backpartTimeLineLabel = UIView()
        backpartTimeLineLabel.backgroundColor = UIColor.lightGray
        contentView.addSubview(backpartTimeLineLabel)
        backpartTimeLineLabel.snp.makeConstraints { (make) -> Void in
            make.left.width.equalTo(forepartTimeLineLabel)
            make.top.equalTo(timeLineIcon.snp.bottom)
            make.bottom.equalTo(contentView)
        }
    }
}

(2)主页面(ViewController.swift
import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    //所有消费记录
    var consumptions:[Consumption]?
    
    //使用时间轴形式的表格
    var tableView:UITableView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //初始化数据,这一次数据,我们放在属性列表文件里
        consumptions = [
            Consumption(title:"充了手机话费", cost:100.0, datetime:"2016.10.10 12:10",
                        appendix:""),
            Consumption(title:"超市购物", cost:810.0, datetime:"2016.10.11 12:10",
                        appendix:"买了台豆浆机,一袋大米,一桶油,两斤苹果,一包饼干,两只牙刷。"),
            Consumption(title:"同事结婚随礼", cost:500, datetime:"2016.10.11 17:10",
                        appendix:""),
            Consumption(title:"办健身卡", cost:1000, datetime:"2016.10.15 11:00",
                        appendix:"有效期至2016年10月")]
        
        //创建表视图
        self.tableView = UITableView(frame: self.view.frame, style:UITableViewStyle.plain)
        self.tableView!.delegate = self
        self.tableView!.dataSource = self
        //创建一个重用的单元格
        self.tableView!.register(TimeLineCell.self,
                                      forCellReuseIdentifier: "SwiftCell")
        self.view.addSubview(self.tableView!)
        
        //让单元格自适应
        tableView!.rowHeight = UITableViewAutomaticDimension
        tableView!.estimatedRowHeight = 100
        tableView!.separatorStyle = UITableViewCellSeparatorStyle.none
        tableView!.tableFooterView = UIView()
    }
    
    //在本例中,只有一个分区
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1;
    }

    //返回表格行数(也就是返回控件数)
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.consumptions!.count
    }
    
    //创建各单元显示内容(创建参数indexPath指定的单元)
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
        -> UITableViewCell {
        let identifier = "SwiftCell"
        var cell = tableView.dequeueReusableCell(withIdentifier: identifier)
            as? TimeLineCell
        if cell == nil{
            cell = TimeLineCell(style: .default, reuseIdentifier: identifier)
            cell?.selectionStyle = .none
        }
        
        //获取记录
        let consumption = self.consumptions![indexPath.row]
        //设置时间轴上的图标
        cell!.timeLineIcon.image = UIImage(named: "money")
        //设置消费金额
        cell!.costLabel.text = "\(consumption.cost) 元"
        //设置消费时间
        cell?.dateTimeLabel.text = consumption.datetime
        //设置消费内容
        cell?.titleLabel.text = consumption.title
        //设置备注信息
        cell?.appendixLabel.text = consumption.appendix
        //设置是否有备注(自动更新备注标签容器相关约束)
        cell?.hasAppendix = (cell?.appendixLabel.text != "")
        return cell!
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

//消费记录
struct Consumption {
    var title:String //消费条目
    var cost:Double //费用
    var datetime:String //时间
    var appendix:String //备注
}
源码下载hangge_1383.zip
评论4
  • 4楼
    2017-06-07 14:25
    510738319@qq.com

    赞.写的很棒.受教了

    站长回复

    谢谢你的夸奖,欢迎常来看看,我会持续更新下去的。

  • 3楼
    2016-11-30 17:21
    七夕猪

    明白了。谢谢你的解答

    站长回复

    不客气的。

  • 2楼
    2016-11-16 09:59
    七夕猪

    辛苦了。又想起来一个疑问,在 TimeLineCell.swift 中的 setSubviews 方法中设置 containView 时为什么不写containView.layer.masksToBounds = true 这句代码,只写 containView.layer.cornerRadius = 3 这句代码也能实现圆角效果,我记得需要这两句都写才能实现的啊?

    站长回复

    masksToBounds是为了让子元素不会溢出。比如设了圆角,但masksToBounds未设置为true。那么其内部元素可能会超出圆角部分。
    而样例中containView里面没有其它元素,所以不需要设置masksToBounds。

  • 1楼
    2016-11-16 09:37
    七夕猪

    有几点疑问:
    1.初始化 tableView 的时候,设置 tableView!.tableFooterView = UIView() 这句代码我去掉之后,发现展示的效果一样,这句代码是否有必要;
    2. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 方法中在 if cell == nil 代码块中设置 cell?.selectionStyle = .none ,运行之后发现点击 cell 还是有点击的效果,这个句代码没起作用啊,在if 代码块之后再设置才有效果;
    3.设置 cell 属性时,前两个设置是 cell!.xx = xxx ,为什么后面的写成 cell?.xx = xx,到底用哪种啊?

    站长回复

    1,tableFooterView可以不需要设置,我这里设了UIView()但里面没东西自然也看不出效果。
    2,如果不要点击选择效果,可以直接设置:tableView?.allowsSelection = false
    3,如果确定cell存在的话,用!就可以了。如果不确定cell存在,就用?,否则!会报错。这里cell肯定不为nil,所以!和?都没问题。