09-自定义 cell

原创微博内容的 View

  • HMStatusOriginalView
class HMStatusOriginalView: UIView {

    /// 微博视图模型
    var statusViewModel: HMStatusViewModel?

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupUI() {
        backgroundColor = UIColor.redColor()
    }
}
  • 定义懒加载控件
// MARK: - 懒加载控件
private lazy var originalView: HMStatusOriginalView = HMStatusOriginalView()
  • 添加顶部视图
private func setupUI() {
    // 1. 添加控件
    contentView.addSubview(originalView)

    // 2. 添加约束
    // 添加原创微博内容的约束
    originalView.snp_makeConstraints { (make) -> Void in
        make.top.equalTo(contentView.snp_top)
        make.width.equalTo(self.snp_width)
        make.height.equalTo(47)
    }
}

原创微博内容布局

  • 设置数据
/// 微博视图模型
var statusViewModel: HMStatusViewModel? {
    didSet {
        originalView.statusViewModel = statusViewModel
    }
}
  • 懒加载控件
// MARK: - 懒加载控件
/// 头像
private lazy var iconView = UIImageView()
/// 姓名
private lazy var nameLabel = UILabel(color: UIColor.darkGrayColor(), fontSize: 14)
/// 微博认证
private lazy var verifiedIconView: UIImageView = UIImageView(image: UIImage(named: "avatar_vip"))
/// VIP图标
private lazy var memberIconView: UIImageView = UIImageView(image: UIImage(named: "common_icon_membership_level1"))
/// 时间
private lazy var timeLabel = UILabel(color: UIColor.orangeColor(), fontSize: 10)
/// 来源
private lazy var sourceLabel = UILabel(color: UIColor.darkGrayColor(), fontSize: 10)
  • 抽取部分常量到 HMStatusCell
// 头像的大小与宽度
let HMStatusHeadImageWH: CGFloat = 35
// 昵称字体大小
let HMStatusNameFontSize: CGFloat = 14
// 来源与时间的字体大小
let HMStatusSourceFontSize: CGFloat = 10
// 微博正文字体大小
let HMStatusContentFontSize: CGFloat = 15
  • 定义间距常量 (定义到 HMStatusCell 里)
/// 控件间距
let HMStatusCellMargin: CGFloat = 10
  • 添加控件 & 自动布局
private func setupUI() {
    // 1. 添加控件
    addSubview(iconView)
    addSubview(nameLabel)
    addSubview(verifiedIconView)
    addSubview(memberIconView)
    addSubview(timeLabel)
    addSubview(sourceLabel)

    // 2. 添加约束
    // 头像
    iconView.snp_makeConstraints { (make) -> Void in
        make.leading.equalTo(HMStatusCellMargin)
        make.top.equalTo(HMStatusCellMargin)
        make.size.equalTo(CGSizeMake(35, 35))
    }
    // 名称
    nameLabel.snp_makeConstraints { (make) -> Void in
        make.leading.equalTo(self.iconView.snp_trailing).offset(HMStatusCellMargin)
        make.top.equalTo(self.iconView.snp_top)
    }
    // 认证图标
    verifiedIconView.snp_makeConstraints { (make) -> Void in
        make.centerX.equalTo(self.iconView.snp_trailing)
        make.centerY.equalTo(self.iconView.snp_bottom)
    }
    // 会员图标
    memberIconView.snp_makeConstraints { (make) -> Void in
        make.centerY.equalTo(self.nameLabel.snp_centerY)
        make.leading.equalTo(self.nameLabel.snp_trailing).offset(HMStatusCellMargin)
    }

    // 时间
    timeLabel.snp_makeConstraints { (make) -> Void in
        make.leading.equalTo(self.nameLabel.snp_leading)
        make.bottom.equalTo(self.iconView.snp_bottom)
    }
    // 来源
    sourceLabel.snp_makeConstraints { (make) -> Void in
        make.leading.equalTo(self.timeLabel.snp_trailing).offset(HMStatusCellMargin)
        make.centerY.equalTo(self.timeLabel.snp_centerY)
    }

}
  • 设置原创微博数据
/// 微博视图模型
var statusViewModel: HMStatusViewModel? {
    didSet{
        // 昵称
        nameLabel.text = statusViewModel?.status?.user?.name

        // TODO: 需要处理细节
        timeLabel.text = "刚刚"
        sourceLabel.text = "来自 weibo.com"
    }
}
  • 设置 tableView 的行高为200
// TODO: 测试行高
tableView.rowHeight = 200

运行测试

设置顶部数据

  • HMStatusViewModel 模型中添加 userProfileUrl 属性
/// 用户头像URL
var userProfileUrl: NSURL? {
    return NSURL(string: status?.user?.profile_image_url ?? "")
}
  • HMStatusOriginalView 中设置头像
iconView.sd_setImageWithURL(statusViewModel?.userProfileUrl, placeholderImage: UIImage(named: "avatar_default_small"))
  • HMStatusViewModel 模型中添加 userVerifiedImage 属性
/// 用户认证图像
/// 认证类型 -1:没有认证,1,认证用户,2,3,5: 企业认证,220: 达人
var userVerifiedImage: UIImage? {
    switch status?.user?.verified ?? 0 {
    case 1:
        return UIImage(named: "avatar_vip")
    case 2,3,5:
        return UIImage(named: "avatar_enterprise_vip")
    case 220:
        return UIImage(named: "avatar_grassroot")
    default:
        return nil
    }
}
  • HMStatusViewModel 模型中添加 userMemberImage
/// 会员图像
var userMemberImage: UIImage? {
    if status?.user?.mbtype > 2 && status?.user?.mbrank > 0 && status?.user?.mbrank < 7 {
        return UIImage(named: "common_icon_membership_level\(status!.user!.mbrank)")
    }
    return nil
}
  • 调整后的设置数据方法
/// 微博视图模型
var statusViewModel: StatusViewModel? {
    didSet {
        iconView.sd_setImageWithURL(statusViewModel?.userProfileUrl)
        nameLabel.text = statusViewModel?.status?.user?.name
        vipIconView.image = statusViewModel?.userVipImage
        memberIconView.image = statusViewModel?.userMemberImage

        // TODO: - 设置文字细节
        timeLabel.text = "刚刚"
        sourceLabel.text = "来自 皮皮时光机"
    }
}

正文文字

添加正文文字 label 到 HMStatusOriginalView

  • 扩展 便利构造函数
/// 遍历构造函数
///
/// - parameter color:    颜色
/// - parameter fontSize: 字体大小
///
/// - returns: UILabel
convenience init(color: UIColor, fontSize: CGFloat, layoutWidth: CGFloat = 0) {
    self.init()

    textColor = color
    font = UIFont.systemFontOfSize(fontSize)

    if layoutWidth > 0 {
        numberOfLines = 0
        preferredMaxLayoutWidth = layoutWidth
    }
}
  • 懒加载方法
/// 微博正文
private lazy var contentLabel: UILabel = UILabel(color: UIColor.darkGrayColor(), fontSize: 15, layoutWidth: UIScreen.mainScreen().bounds.width - 2 * HMStatusCellMargin)
  • 自动布局
addSubview(contentLabel)

//  微博文字
contentLabel.snp_makeConstraints { (make) -> Void in
    make.leading.equalTo(self.iconView.snp_leading)
    make.top.equalTo(self.iconView.snp_bottom).offset(HMStatusCellMargin)
}
  • 关键:添加底部约束
// 约束当前 View 的底部与正文内容的底部一样
snp_makeConstraints { (make) -> Void in
    make.bottom.equalTo(contentLabel.snp_bottom)
}
  • 更改 HMStatusCell 中 原创微博View 的约束
// 添加原创微博内容的约束
originalView.snp_makeConstraints { (make) -> Void in
    // 关键:约束原创微博整体 View 的顶部
    make.top.equalTo(contentView.snp_top)
    make.width.equalTo(contentView.snp_width)
}

// 约束当前 contenView 关键:底部等于 originalView的底部
contentView.snp_makeConstraints { (make) -> Void in
    make.width.equalTo(self.snp_width)
    make.top.equalTo(self.snp_top)
    make.bottom.equalTo(originalView.snp_bottom)
}
  • 更改 HMHomeTableViewController 中 tableView 的行高计算方式
// 跟据 AutoLayout 约束的高度自动计算
tableView.rowHeight = UITableViewAutomaticDimension
// 预估行高
tableView.estimatedRowHeight = 200

运行测试 --> 添加原创微博 View 的底部约束,可以让cell的高度以原创微博 View 最大的Y值来计算

底部ToolBar

  • 数据格式
属性名 类型 说明
reposts_count int 转发数
comments_count int 评论数
attitudes_count int 表态数

定义 HMStatusToolBar

class HMStatusToolBar: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.redColor()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}
  • HMStatusCell 添加懒加载
// 底部工具条
private lazy var statusToolBar: HMStatusToolBar = HMStatusToolBar()
  • 添加约束(并且更改底部约束)
// 添加原创微博内容的约束
originalView.snp_makeConstraints { (make) -> Void in
    // 关键:约束原创微博整体 View 的顶部
    make.top.equalTo(contentView.snp_top)
    make.width.equalTo(contentView.snp_width)
}

// 底部toolBar
statusToolBar.snp_makeConstraints { (make) -> Void in
    make.top.equalTo(originalView.snp_bottom).offset(HMStatusCellMargin)
    make.width.equalTo(originalView.snp_width)
    make.height.equalTo(35)
}

// 约束当前 contenView 关键:底部等于 statusToolBar 的底部
contentView.snp_makeConstraints { (make) -> Void in
    make.width.equalTo(self.snp_width)
    make.top.equalTo(self.snp_top)
    make.bottom.equalTo(statusToolBar.snp_bottom)
}

运行测试

添加子控件

  • UIButton extension
extension UIButton {

    /// 便利构造一个Button
    ///
    /// - parameter title:     标题文字
    /// - parameter fontSize:  字体大小
    /// - parameter color:     文字颜色
    /// - parameter imageName: 图片名称
    ///
    convenience init(title: String, fontSize: CGFloat, color: UIColor, imageName: String? = nil){
        self.init()
        setTitle(title, forState: UIControlState.Normal)
        titleLabel?.font = UIFont.systemFontOfSize(fontSize)
        setTitleColor(color, forState: UIControlState.Normal)
        // 如果有图片,则设置image
        if let imageN = imageName {
            imageView?.image = UIImage(named: imageN)
        }
    }
}
  • 新增一个添加子控件的方法
/// 添加子控件
///
/// - parameter title: 显示的文字
/// - parameter image: 显示的图片
///
/// - returns: 将添加的 button 返回
private func addChildButton(title: String, image: String) -> UIButton {

    let button = UIButton(title: title, fontSize: 14, color: UIColor.darkGrayColor())

    // 设置不同状态的背景颜色
    button.setBackgroundImage(UIImage(named: "timeline_card_bottom_background_highlighted"), forState: UIControlState.Highlighted)
    button.setBackgroundImage(UIImage(named: "timeline_card_bottom_background"), forState: UIControlState.Normal)

    button.setImage(UIImage(named: image), forState: UIControlState.Normal)
    addSubview(button)
    return button
}
  • 定义按钮属性
/// 转发按钮
var retweetButton: UIButton?
/// 评论按钮
var commentButton: UIButton?
/// 表态按钮
var attituedButton: UIButton?
  • 添加子控件
private func setupUI(){
    // 1.添加子控件
    retweetButton = addChildButton("转发", image: "timeline_icon_retweet")
    commentButton = addChildButton("评论", image: "timeline_icon_comment")
    attituedButton = addChildButton("赞", image: "timeline_icon_unlike")
}

设置约束

  • 设置思路:转发按钮 -> A,评论按钮 -> B,赞按钮 -> C
    • A 按钮的左边紧贴父控件的左边,顶部和高度与父控件对齐
    • B 按钮的左边紧贴 A 按钮的右边,右边紧贴 C 按钮的左边, 顶部和高度与父控件对齐
    • C 按钮的右边紧贴父控件的右边,顶部和高度与父控件对齐
    • A 按钮的宽度 等于 B按钮的宽度,C 按钮的宽度等于 B 按钮的宽度
// 2.添加约束
retweetButton!.snp_makeConstraints { (make) -> Void in
    make.top.equalTo(self.snp_top)
    make.leading.equalTo(self.snp_leading)
    make.height.equalTo(self.snp_height)
    make.width.equalTo(commentButton!.snp_width)
}
commentButton!.snp_makeConstraints { (make) -> Void in
    make.leading.equalTo(retweetButton!.snp_trailing)
    make.trailing.equalTo(attituedButton!.snp_leading)
    make.top.equalTo(retweetButton!.snp_top)
    make.height.equalTo(self.snp_height)
}
attituedButton!.snp_makeConstraints { (make) -> Void in
    make.trailing.equalTo(self.snp_trailing)
    make.height.equalTo(self.snp_height)
    make.top.equalTo(retweetButton!.snp_top)
    make.width.equalTo(commentButton!.snp_width)
}

运行测试

  • 添加分割线
/// 添加分割线
private func addSpliteView() -> UIImageView {
    let image = UIImageView(image: UIImage(named: "timeline_card_bottom_line"))
    addSubview(image)
    return image
}
  • 设置约束
// 3.添加分割线
let sp1 = addSpliteView()
let sp2 = addSpliteView()

// 4.设置分割线的约束

sp1.snp_makeConstraints { (make) -> Void in
    make.centerX.equalTo(self.retweetButton!.snp_trailing)
    make.centerY.equalTo(self.retweetButton!.snp_centerY)
}
sp2.snp_makeConstraints { (make) -> Void in
    make.centerX.equalTo(self.commentButton!.snp_trailing)
    make.centerY.equalTo(self.commentButton!.snp_centerY)
}

设置数据

  • HMStatus 模型中添加以下属性
/// 转发数
var reposts_count: Int = 0
/// 评论数
var comments_count: Int = 0
/// 表态数
var attitudes_count: Int = 0
  • 添加视图模型到 HMStatusToolBar
/// 视图模型
var statusViewModel: HMStatusViewModel?
  • HMStatusCell 里面设置此属性
/// 微博视图模型
var statusViewModel: HMStatusViewModel? {
    didSet{
        // 设置原创微博内容的视图模型
        originalView.statusViewModel = statusViewModel
        // 设置底部 ToolBar 的视图模型
        statusToolBar.statusViewModel = statusViewModel
    }
}
  • 数量显示逻辑

    • 小于 10000,直接显示数字
    • 大于 10000,小于 11000,显示 1万
    • 大于 11000,小于 20000,显示 1.x万
    • 其他同理
  • 视图模型 里面添加 repostsCountString 属性

/// 转发数量
var repostsCountString: String {
    // 默认显示 `转发`
    var result = "转发"
    let count = status?.reposts_count ?? 0

    // 如果数量为 0,直接显示 `转发`
    if count == 0 {
        return result
    }

    // 如果数量大于10000,再做处理
    if count > 10000 {
        // 先除以1000返回一个整数,再除以10,返回一个小数
        let res = Float(count / 1000) / 10
        // 拼接字符串
        result = "\(res)万"
    }else{
        result = "\(status!.reposts_count)"
    }
    return result
}
  • 添加测试数据(在 HMStatus 中添加 reposts_count 的 didSet 方法)
/// 转发数
var reposts_count: Int = 0 {
    didSet{
        reposts_count = 10009
    }
}

测试发现显示 1.0万

  • 添加判断小数为 0 的逻辑
// 查看是否包含 .0 万
if result.containsString(".0万") {
    result = result.stringByReplacingOccurrencesOfString(".0", withString: "")
}
  • 视图模型 里面添加 repostsCountString 和 `` 属性
/// 评论数量
var commentsCountString: String {
    return "评论"
}
/// 点赞数量
var attitudesCountString: String {
    return "赞"
}
  • 抽取处理数量逻辑的方法
/// 处理 转发\评论\赞 数量逻辑
///
/// - parameter count:          对应数量
/// - parameter defaultString:  默认显示的文字
///
private func countString(count: Int, defaultString: String) -> String {

    var result = defaultString
    if count == 0 {
        return result
    }

    // 如果数量大于10000,再做处理
    if count > 10000 {
        let res = Float(count / 1000) / 10
        result = "\(res)万"
        // 查看是否包含 .0 万
        if result.containsString(".0万") {
            result = result.stringByReplacingOccurrencesOfString(".0", withString: "")
        }
    }else{
        result = "\(count)"
    }
    return result
}
  • 更改三个属性的 get 方法
/// 转发数量
var repostsCountString: String {
    let count = status?.reposts_count ?? 0
    return countString(count, defaultString: "转发")
}
/// 评论数量
var commentsCountString: String {
    let count = status?.comments_count ?? 0
    return countString(count, defaultString: "评论")
}
/// 点赞数量
var attitudesCountString: String {
    let count = status?.attitudes_count ?? 0
    return countString(count, defaultString: "赞")
}

运行测试(也可以将这三个属性设置成存储型属性)

转发微博内容

数据模型准备

  • 添加转发微博属性
/// 转发微博
var retweeted_status: HMStatus?
  • setValue(value: AnyObject?, forKey key: String) 函数中增加一下代码
// 2. 转发微博
if key == "retweeted_status" {
    retweeted_status = Status(dict: value as! [String: AnyObject])
    return
}

新建 HMStatusRetweetView

class HMStatusRetweetView: UIView {

    /// 微博视图模型
    var statusViewModel: HMStatusViewModel?

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupUI(){
        backgroundColor = UIColor(white: 0.95, alpha: 1)
    }
}
  • 懒加载控件
// MARK: - 懒加载控件
/// 转发微博正文内容
private lazy var contentLabel = UILabel(color: UIColor.darkGrayColor(), fontSize: 15, layoutWidth: UIScreen.mainScreen().bounds.width - 2 * HMStatusCellMargin)
  • 添加控件设置约束
private func setupUI(){
    backgroundColor = UIColor(white: 0.9, alpha: 1)

    // 添加子控件
    addSubview(contentLabel)

    // 设置约束
    contentLabel.snp_makeConstraints { (make) -> Void in
        make.top.equalTo(self.snp_top).offset(HMStatusCellMargin)
        make.leading.equalTo(self.snp_leading).offset(HMStatusCellMargin)
    }
    // 设置当前View的底部为内容的底部加上间距
    snp_makeConstraints { (make) -> Void in
        make.bottom.equalTo(contentLabel.snp_bottom).offset(HMStatusCellMargin)
    }
}
  • HMStatusViewModel 添加 retweetText 属性
// 转发微博内容
var retweetText: String? {
    if let retStatus = self.status?.retweeted_status where retStatus.text != nil  {
        return "@\(retStatus.user!.name!):\(retStatus.text!)"
    }
}
  • 设置数据
/// 微博视图模型
var statusViewModel: HMStatusViewModel?{
    didSet{
        contentLabel.text = statusViewModel!.retweetText
    }
}

更新 HMStatusCell

  • 添加转发微博控件
// 转发微博
private lazy var retweetView: HMStatusRetweetView = HMStatusRetweetView()
...
private func setupUI(){
    // 添加控件
    contentView.addSubview(originalView)
    contentView.addSubview(retweetView)
    contentView.addSubview(statusToolBar)


    // 添加原创微博内容的约束
    originalView.snp_makeConstraints { (make) -> Void in
        // 关键:约束原创微博整体 View 的顶部
        make.top.equalTo(contentView.snp_top)
        make.width.equalTo(contentView.snp_width)
    }
    // 添加转发微博内容的约束
    retweetView.snp_makeConstraints { (make) -> Void in
        make.top.equalTo(originalView.snp_bottom)
        make.leading.equalTo(originalView.snp_leading)
        make.width.equalTo(originalView.snp_width)
    }
    // 底部toolBar的约束
    statusToolBar.snp_makeConstraints { (make) -> Void in
        make.top.equalTo(retweetView.snp_bottom).constraint
        make.width.equalTo(originalView.snp_width)
        make.height.equalTo(35)
        make.bottom.equalTo(contentView.snp_bottom)
    }
}
  • 设置数据(需要判断是否有转发微博)
/// 微博视图模型
var statusViewModel: HMStatusViewModel? {
    didSet{
        // 设置视图模型
        originalView.statusViewModel = statusViewModel
        statusToolBar.statusViewModel = statusViewModel

        // 如果有转发微博
        if statusViewModel?.status?.retweeted_status != nil {
            retweetView.hidden = false
            // 设置转发微博的视图模型
            retweetView.statusViewModel = statusViewModel
            // TODO:需要更新约束,statusToolBar的顶部要与转发微博底部对齐
        }else{
            // 没有转发微博,隐藏转发微博的View
            retweetView.hidden = true
            // TODO:需要更新约束,statusToolBar的顶部要与原创微博底部对齐

        }
    }
}
  • 定义变量记住 statusToolBar 的顶部约束
// toolBar 顶部约束
var toolBarTopConstraints: Constraint?

...
// 在约束toolBar顶部约束的时候记录
// 底部toolBar
statusToolBar.snp_makeConstraints { (make) -> Void in
    self.toolBarTopConstraints = make.top.equalTo(retweetView.snp_bottom).constraint
    make.width.equalTo(originalView.snp_width)
    make.height.equalTo(35)
    make.bottom.equalTo(contentView.snp_bottom)
}
  • 根据是否有转发微博更新约束
// 先让之前记录的约束失效 -> 约束的时候重新记录
toolBarTopConstraints?.uninstall()
// 如果有转发微博
if statusViewModel?.status?.retweeted_status != nil {
    retweetView.hidden = false
    // 设置转发微博的视图模型
    retweetView.statusViewModel = statusViewModel
    statusToolBar.snp_updateConstraints(closure: { (make) -> Void in
        toolBarTopConstraints = make.top.equalTo(retweetView.snp_bottom).constraint
    })
}else{
    // 没有转发微博,隐藏转发微博的View
    retweetView.hidden = true
    // 更新约束
    statusToolBar.snp_updateConstraints(closure: { (make) -> Void in
        toolBarTopConstraints = make.top.equalTo(originalView.snp_bottom).constraint
    })
}

运行测试

微博配图

数据

配图数据对应的字段 pic_urls,格式为:

pic_urls: [
    {
        thumbnail_pic: "http://ww2.sinaimg.cn/thumbnail/005Ko17Djw1exjar89996j30b40b440s.jpg"
    },
    {
        thumbnail_pic: "http://ww2.sinaimg.cn/thumbnail/005Ko17Djw1exjar89996j30b40b440s.jpg"
    }
]
  • 定义 pic_urls 内部的数据模型 HMStatusPhotoInfo
class HMStatusPhotoInfo: NSObject {

    /// 约略图地址
    var thumbnail_pic: String?

    init(dictionary: [String: AnyObject]){
        super.init()
        setValuesForKeysWithDictionary(dictionary)
    }

    override func setValue(value: AnyObject?, forUndefinedKey key: String) {}
}
  • HMStatus 模型中增加配图数组模型
/// 配图模型数组
var pic_urls: [HMStatusPhotoInfo]?
  • setValue(value: AnyObject?, forKey key: String) 函数中增加一下代码
if key == "pic_urls" {
    var tempArray = [HMStatusPhotoInfo]()
    // 遍历字典转模型
    for value in value as! [[String: AnyObject]] {
        tempArray.append(HMStatusPhotoInfo(dictionary: value))
    }
    pic_urls = tempArray
}

思路

  • 图片可以有多张可以使用 UICollectionView 实现
  • 根据原创微博(转发微博)是否有配图去显示或者隐藏控件

控件显示实现

  • 定义 HMStatusPictureView
class HMStatusPictureView: UICollectionView {

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: UICollectionViewFlowLayout())
        // 为了测试,设置背景颜色为随机色
        backgroundColor = RandomColor()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

原创微博

  • HMStatusOriginalView 中懒加载控件
/// 配图视图
private lazy var pictureView: HMStatusPictureView = HMStatusPictureView()
  • 添加控件并设置约束
// 配图视图
pictureView.snp_makeConstraints { (make) -> Void in
    // 先写死一个宽高
    make.size.equalTo(CGSizeMake(100, 100))
    make.leading.equalTo(contentLabel.snp_leading)
    make.top.equalTo(contentLabel.snp_bottom).offset(HMStatusCellMargin)
}

下一步就需要根据原创微博是否有配图去动态更新当前原创微博View的高度

  • 如果有:原创微博 View 的底部是相对于配图控件来说的

  • 如果没有:原创微博 View 的底部是相对于微博内容控件来说的

  • 所以需要在初始化控件的时候记录当前 View 底部的约束

  • 记录底部的约束

/// 当前 View 的底部约束
private var bottomConstraint: Constraint?
...
// 约束当前 View 的底部与正文内容的底部一样 并 记录该约束
snp_makeConstraints { (make) -> Void in
    self.bottomConstraint = make.bottom.equalTo(contentLabel.snp_bottom).offset(HMStatusCellMargin).constraint
}
  • 在设置视图模型的时候判断是否有配图去更新约束
// 先移除之前的约束
bottomConstraint?.uninstall()
// 配图视图
if let picUrls = statusViewModel?.status?.pic_urls where picUrls.count > 0 {
    pictureView.hidden = false
    // 有配图,更新约束 -> 更新当前 View 底部的约束
    self.snp_updateConstraints(closure: { (make) -> Void in
        self.bottomConstraint = make.bottom.equalTo(pictureView.snp_bottom).offset(HMStatusCellMargin).constraint
    })
}else{
    // 没有配图,隐藏配图控件
    pictureView.hidden = true
    self.snp_updateConstraints(closure: { (make) -> Void in
        self.bottomConstraint = make.bottom.equalTo(contentLabel.snp_bottom).offset(HMStatusCellMargin).constraint
    })
}

运行测试:原创微博有配图,就会显示配图控件

  • 根据配图的张数计算控件大小
    • 添加方法 calcViewSize()HMStatusPictureView
/// 根据图片个数计算当前View的大小
private func calcViewSize() -> CGSize {

    // 获取到配图张数
    let count = pic_urls?.count ?? 0
    // 计算出每一个条目的宽高

    // 每一个条目之间的间距
    let HMStatusPictureItemMargin: CGFloat = 5
    // 每一个Item的宽高
    let HMStatusPictureItemWH = (SCREENW - 2 * HMStatusCellMargin - 2 * HMStatusPictureItemMargin) / 3

    // 计算出多少列
    let col = count == 4 ? 2 : (count > 3 ? 3 : count)
    let row = count == 4 ? 2 : ((count - 1) / 3 + 1)

    // 计算出当前控件的宽度
    let width = HMStatusPictureItemWH * CGFloat(col) + CGFloat(col - 1) * HMStatusPictureItemMargin;
    let height = HMStatusPictureItemWH * CGFloat(row) + CGFloat(row - 1) * HMStatusPictureItemMargin;

    return CGSizeMake(width, height)
}
  • 定义配图数据的属性
/// 配图
var pic_urls: [HMStatusPhotoInfo]?
  • HMStatusOriginalView 设置数据的时候给配图 View 设置数据
// 设置数据
pictureView.pic_urls = picUrls
  • 在设置数据的时候去更新当前配图控件的大小约束
/// 配图
var pic_urls: [HMStatusPhotoInfo]? {
    didSet{

        // 在设置配图的时候计算当前 View 的大小
        snp_updateConstraints { (make) -> Void in
            make.size.equalTo(calcViewSize())
        }
    }
}
  • 为了测试方便,添加一个测试 label 到配图控件里面,显示当前配图控件里面需要展示几张图片
/// 测试:用于显示张数的label
private lazy var label: UILabel = {
    let label = UILabel()
    label.textColor = UIColor.blackColor()
    label.font = UIFont.systemFontOfSize(30)
    return label
}()
...
// 添加控件以及添加约束
addSubview(label);
label.snp_makeConstraints { (make) -> Void in
    make.center.equalTo(self.snp_center)
}
...
// 在设置数据的时候,让 label 显示配图张数
var pic_urls: [HMStatusPhotoInfo]? {
    didSet{
        ...
        label.text = "\(pic_urls!.count)"
    }
}

运行测试

转发微博配图

  • 添加控件思路与原创微博一样

注意:设置数据的时候一定要设置成转发微博的数据

图片显示

  • 定义可重用 ID
// 可重用ID
private let HMStatusPictureCellId = "HMStatusPictureCellId"
  • 设置数据源以及代理,设置每一个 item 的宽度
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
    super.init(frame: frame, collectionViewLayout: UICollectionViewFlowLayout())
    ...
    // 设置代理与数据源都是自己
    self.delegate = self
    self.dataSource = self

    // 设置layout
    let layout = collectionViewLayout as! UICollectionViewFlowLayout
    layout.itemSize = CGSizeMake(HMStatusPictureItemWH, HMStatusPictureItemWH)
    // 设置间隔
    layout.minimumInteritemSpacing = HMStatusPictureItemMargin
    layout.minimumLineSpacing = HMStatusPictureItemMargin
    // 注册cell
    self.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: HMStatusPictureCellId)
}
  • 实现两个数据源方法
extension HMStatusPictureView {

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return pic_urls?.count ?? 0
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(HMStatusPictureCellId, forIndexPath: indexPath)
        // 为了查看出效果,设置 cell 的背景颜色为随机色
        cell.backgroundColor = RandomColor()
        return cell
    }
}

运行测试

  • 自定义 Cell HMStatusPictureCell
private class HMStatusPictureCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)
        // 设置背景颜色为随机颜色
        backgroundColor = RandomColor()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
  • 更改注册的cell
// 注册cell
self.registerClass(HMStatusPictureCell.self, forCellWithReuseIdentifier: HMStatusPictureCellId)

运行测试

  • 添加图片控件
// 懒加载控件
private lazy var imageView: UIImageView = {
    let imageView = UIImageView()
    // 设置imageView的显示模式
    imageView.contentMode = UIViewContentMode.ScaleAspectFill
    // 切掉多余部分
    imageView.clipsToBounds = true
    return imageView;
}()
...
// 添加控件并设置约束
// 添加子控件
contentView.addSubview(imageView)
// 添加约束
imageView.snp_makeConstraints { (make) -> Void in
    make.size.equalTo(contentView.snp_size)
    make.leading.equalTo(contentView.snp_leading)
    make.top.equalTo(contentView.snp_top)
}
  • 添加属性 photoInfo
/// 设置数据模型
var photoInfo: HMStatusPhotoInfo?
  • 在数据源方法里面设置数据
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(HMStatusPictureCellId, forIndexPath: indexPath) as! HMStatusPictureCell
    // 设置数据
    cell.photoInfo = pic_urls![indexPath.row]
    return cell
}
  • 显示图片
var photoInfo: HMStatusPhotoInfo? {
    didSet{
        if let urlString = photoInfo?.thumbnail_pic{
            imageView.sd_setImageWithURL(NSURL(string: urlString), placeholderImage: UIImage(named: "timeline_image_placeholder"))
        }
    }
}
  • 添加 gif 图标
/// gif 图标
private lazy var gifIcon: UIImageView = UIImageView(image: UIImage(named: "timeline_image_gif"))
...
/// 添加控件
contentView.addSubview(gifIcon)
...
/// 添加约束
gifIcon.snp_makeConstraints { (make) -> Void in
    make.trailing.equalTo(contentView.snp_trailing)
    make.bottom.equalTo(contentView.snp_bottom)
}
...
/// 显示逻辑
if let urlString = photoInfo?.thumbnail_pic{
    imageView.sd_setImageWithURL(NSURL(string: urlString), placeholderImage: UIImage(named: "timeline_image_placeholder"))
    gifIcon.hidden = !urlString.hasSuffix(".gif");
}
  • 设置配图控件的背景颜色
/// 原创微博配图控件
private lazy var pictureView: HMStatusPictureView = {
    let pictureView = HMStatusPictureView()
    pictureView.backgroundColor = UIColor.whiteColor();
    return pictureView;
}()
...
/// 转发微博配图控件
private lazy var pictureView: HMStatusPictureView = {
    let pictureView = HMStatusPictureView()
    pictureView.backgroundColor = UIColor(white: 0.95, alpha: 1);
    return pictureView;
}()

其他细节

  • 取消分隔线
tableView.separatorStyle = UITableViewCellSeparatorStyle.None
  • 增加 cell 分隔视图
// 设置 cell 的contentView的背景颜色
// 设置背景颜色
contentView.backgroundColor = UIColor(white: 240 / 255, alpha: 1)

// 设置原创微博背景色为白色 在 `HMStatusOriginalView`
backgroundColor = UIColor.whiteColor()

// 更改原创微博距离顶部的间距 在 `HMStatusCell`
originalView.snp_makeConstraints { (make) -> Void in
    // 距离顶部有间距
    make.top.equalTo(contentView.snp_top).offset(HMStatusCellMargin)
    ...
}
  • 设置 tableView 的背景色
tableView.backgroundColor = UIColor(white: 240 / 255, alpha: 1)
  • 抽取颜色的方法 -> CommonTools.swift
    • RGBColor & 随机颜色
/// RGB颜色
func RGB(r r: CGFloat, g: CGFloat, b: CGFloat) -> UIColor {
    return UIColor(red: r / 256, green: g / 256, blue: b / 256, alpha: 1)
}

/// 随机颜色
func RandomColor() -> UIColor {
    return RGB(r: CGFloat(random()) % 256, g: CGFloat(random()) % 256, b: CGFloat(random()) % 256)
}
  • 抽取屏幕宽度/高度 -> CommonTools.swift
/// 屏幕宽高
let SCREENW = UIScreen.mainScreen().bounds.size.width
let SCREENH = UIScreen.mainScreen().bounds.size.height

单张图片

目标

  • 将单张图片提前缓存到本地,以便判断大小
  • 复习 gcd 的 dispatch_group
  • 熟悉 SDWebImage 的其他函数应用

预先加载图片说明

  • 新浪微博的数据接口并没有返回每一张图片的尺寸
  • 而对于保存在远程服务器的图片而言,客户端是无法获知服务器上的图片大小的
  • 因此要实现单图都效果,需要先将图片缓存到本地

代码实现

缓存单张图片

  • HMStatusListViewModel 中增加 cacheSingleImage 函数
  • 调整 loadData 函数,调用 cacheSingleImage 函数缓存单张图片
// 拼接数据
self.statusList = dataList + self.statusList

// 缓存图片
self.cacheSingleImage(dataList)
  • 缓存图片并且回调
private func cacheSingleImage(array: [StatusViewModel]) {

    // 1. 遍历数组
    for vm in array {
        // 1> 只缓存单张图片
        if vm.thumbnailUrls?.count != 1 {
            continue
        }

        // 2> 获取 url
        let url = vm.thumbnailUrls![0]

        print("要缓存的 \(url)")

        // 3> 下载图片
        SDWebImageManager.sharedManager().downloadImageWithURL(
            url,                                // URL
            options: [],  // 选项
            progress: nil)                      // 进度
            { (image, error, _, _, _)  in       // 完成回调
                if let img = image,
                    data = UIImagePNGRepresentation(img) {
                        print(data.length)
                }
        }
    }
}
  • 添加 dispatch_group 和数据长度
// 0. 调度组
let group = dispatch_group_create()
// 缓存数据长度
var dataLength = 0
  • 下载图像之前入组,下载图像最后一行出组
// 3> 下载图片
dispatch_group_enter(group)
SDWebImageManager.sharedManager().downloadImageWithURL(
    url,                                // URL
    options: [],  // 选项
    progress: nil)                      // 进度
    { (image, error, _, _, _)  in       // 完成回调

        // 不是每次图像都能下载成功
        if let img = image,
            data = UIImagePNGRepresentation(img) {

                // 累加长度
                dataLength += data.length
        }

        // 出组
        dispatch_group_leave(group)
}
  • 修改函数定义,增加完成回调参数
private func cacheSingleImage(dataArray: [HMStatusViewModel],completion: (isSuccessed: Bool)->()){
  • 完成回调
// 2. 监听调度组完成
dispatch_group_notify(group, dispatch_get_main_queue()) {
    print("缓存图像大小 \(dataLength / 1024) K")
    completion(isSuccessed: true)
}
  • 修改函数调用
// 3. 拼接数据
self.statusList = tempArray + self.statusList

// 4. 缓存图片
self.cacheSingleImage(tempArray, completion: finished)

修改单张图片显示

  • 修改 calcViewSize 函数
// 2. 单图
if count == 1 {
    // 临时设置单图大小
    var size = CGSize(width: 150, height: 120)

    // 提取单图
    if let key = pic_urls.first?.thumbnail_pic {
        size = SDWebImageManager.sharedManager().imageCache.imageFromDiskCacheForKey(key).size
    }

    layout.itemSize = size
    return size
}
  • 细节处理,防止图片过窄或者太宽
// 过窄处理 - 针对长图
if size.width < 150 {
    let w: CGFloat = 150
    let h = size.height * w / size.width
    size = CGSize(width: w, height: h > 230 ? 230 : h)
}else if size.width > 200 {
    // 过宽的图片
    let w: CGFloat = 200
    let h = size.height * w / size.width
    size = CGSize(width: w, height: h)
}

小结

  • dispatch_group

    • dispatch_group_enter 后续的 block 执行会受 group 监听
    • block 的最后一句必须是 dispatch_group_leave,通知 group 该任务完成
    • dispatch_group_enterdispatch_group_leave 无比成对出现
  • SDWebImage

    • 下载图像时一定注意图像不一定都会被正确下载
    • SDWebImageManager.sharedManager().downloadImageWithURL 是 SDWebImage 的核心下载函数
    • SDWebImageManager.sharedManager().imageCache.imageFromDiskCacheForKey 使用图像完整的 URL字符串 检查是否存在图像的磁盘缓存
    • SDWebImage 使用 MD5URL 字符串编码并作为缓存图像的文件名

你可能感兴趣的:(09-自定义 cell)