swift实现一个与智能机器人聊天的app(二)

上一篇文章swift实现一个与智能机器人聊天的app(一)实现了聊天appUI的输入框部分,接下来我会教大家如何实现聊天窗口部分,也就是下图的第二部分:

UI结构

你可以在这里下载上一篇文章的源代码:
上一篇文章源代码

首先打开我们的项目,你可以找到用于实现该部分的文件:
MessageBubbleTableViewCell.swiftMessageSentDateTableViewCell.swift,分别用来实现消息发送时间的cell和聊天气泡的cell
首先实现消息发送时间的cell,打开MessageBubbleTableViewCell.swift文件,增加对SnapKit第三方库的引用:

import SnapKit

在类里增加一个UILabel的属性,用来显示时间:

  let sentDateLabel: UILabel

在override init()方法中添加代码:

        sentDateLabel = UILabel(frame: CGRectZero)
        sentDateLabel.backgroundColor = UIColor.clearColor()
        sentDateLabel.font = UIFont.systemFontOfSize(10)
        sentDateLabel.textAlignment = .Center
        sentDateLabel.textColor = UIColor(red: 142/255, green: 142/255, blue: 147/255, alpha: 1)

设置时间标签的背景色、字体,文字居中对齐、文字颜色。

        super.init(style: style, reuseIdentifier: reuseIdentifier)
        selectionStyle = .None
        contentView.addSubview(sentDateLabel)

调用父类的构造方法。
我们将该cell设置为不可选,因为我们仅仅需要显示时间而已。
最后将标签添加到cell的视图

   sentDateLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
        sentDateLabel.snp_makeConstraints { (make) -> Void in
            make.centerX.equalTo(contentView.snp_centerX)
            make.top.equalTo(contentView.snp_top).offset(13)
            make.bottom.equalTo(contentView.snp_bottom).offset(-4.5)
        }

将标签左右居中,顶部距离cell视图顶部13点,底部距离cell视图底部4.5点。关于SnapKit的使用我在上一篇文章提到了一些,真的十分地好用,上手也很快,只要你想出一个公式,比如上面这段代码可以转化为:

sentDateLabel.centerX = contentView.centerX
sentDateLabel.top = contentView.top + 13
sentDateLabel.bottom = contentView.bottom - 4.5

ok,显示消息发送时间的cell就设置好了。

接下来打开MessageBubbleTableViewCell.swift文件,增加新的属性:

    let bubbleImageView: UIImageView
    let messageLabel: UILabel

在import下面增加全局变量,用来标示cell的类型(接受或发送的消息):

let incomingTag = 0, outgoingTag = 1
let bubbleTag = 8

在类外增加一些方法,在文件结尾添加以下代码:

let bubbleImage = bubbleImageMake()

func bubbleImageMake() -> (incoming: UIImage, incomingHighlighed: UIImage, outgoing: UIImage, outgoingHighlighed: UIImage) {
   let maskOutgoing = UIImage(named: "MessageBubble")!
   let maskIncoming = UIImage(CGImage: maskOutgoing.CGImage, scale: 2, orientation: .UpMirrored)!

   let capInsetsIncoming = UIEdgeInsets(top: 17, left: 26.5, bottom: 17.5, right: 21)
   let capInsetsOutgoing = UIEdgeInsets(top: 17, left: 21, bottom: 17.5, right: 26.5)

   let incoming = coloredImage(maskIncoming, 229/255, 229/255, 234/255, 1).resizableImageWithCapInsets(capInsetsIncoming)
   let incomingHighlighted = coloredImage(maskIncoming, 206/255, 206/255, 210/255, 1).resizableImageWithCapInsets(capInsetsIncoming)
   let outgoing = coloredImage(maskOutgoing,  0.05 ,0.47,0.91,1.0).resizableImageWithCapInsets(capInsetsOutgoing)
   let outgoingHighlighted = coloredImage(maskOutgoing, 32/255, 96/255, 200/255, 1).resizableImageWithCapInsets(capInsetsOutgoing)

   return (incoming, incomingHighlighted, outgoing, outgoingHighlighted)
}

返回一个结构体包含4种图片:发送消息气泡的正常和高亮(被点击后)图片,接收消息气泡的正常和高亮图片,以供调用。


MessageBubble.png

这是图片的原型,不难理解这是发送消息对应的聊天气泡,所以直接调用即可

let maskOutgoing = UIImage(named: "MessageBubble")!

然而接受消息的气泡和它的关系是水平镜像,所以我们要用一个方法获得它的水平镜像图片:

 let maskIncoming = UIImage(CGImage: maskOutgoing.CGImage, scale: 2, orientation: .UpMirrored)!

然而这两个图片并不能用,因为它的大小是固定的,但是我们的消息的长度是不定的,所以,要把它们做成大小可变的图片,首先设置可拉伸区域:

 let capInsetsIncoming = UIEdgeInsets(top: 17, left: 26.5, bottom: 17.5, right: 21)
 let capInsetsOutgoing = UIEdgeInsets(top: 17, left: 21, bottom: 17.5, right: 26.5)

那么它是怎么确定可拉伸区域的呢,这个示意图可以解释一切:

可拉伸区域

实际上这个可拉伸区域只有1x1像素,但是也够我们用了,因为这一部分可以无限地横向或纵向拉伸,接收消息气泡和发送消息气泡可拉伸区域唯一的区别就是水平方向上,所以把right和left的值互相交换即可。
然后通过UIImageresizableImageWithCapInsets()方法,获取可拉伸图片:

 let incoming = coloredImage(maskIncoming, 229/255, 229/255, 234/255, 1).resizableImageWithCapInsets(capInsetsIncoming)
    let incomingHighlighted = coloredImage(maskIncoming, 206/255, 206/255, 210/255, 1).resizableImageWithCapInsets(capInsetsIncoming)
    let outgoing = coloredImage(maskOutgoing,  0.05 ,0.47,0.91,1.0).resizableImageWithCapInsets(capInsetsOutgoing)
    let outgoingHighlighted = coloredImage(maskOutgoing, 32/255, 96/255, 200/255, 1).resizableImageWithCapInsets(capInsetsOutgoing)

当然这些图片还调用了一个方法coloredImage()进行染色处理,就是下面的这个方法:

func coloredImage(image: UIImage, red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) -> UIImage! {
    let rect = CGRect(origin: CGPointZero, size: image.size)
    UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
    let context = UIGraphicsGetCurrentContext()
    image.drawInRect(rect)
    CGContextSetRGBFillColor(context, red, green, blue, alpha)
    CGContextSetBlendMode(context, kCGBlendModeSourceAtop)
    CGContextFillRect(context, rect)
    let result = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return result
}

获取图片大小

  let rect = CGRect(origin: CGPointZero, size: image.size)

创建位图绘图上下文

  UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)

获取位图绘图上下文,并开始进行渲染操作

   let context = UIGraphicsGetCurrentContext()
    image.drawInRect(rect)
    CGContextSetRGBFillColor(context, red, green, blue, alpha)
    CGContextSetBlendMode(context, kCGBlendModeSourceAtop)
    CGContextFillRect(context, rect)

获取到绘图结果,结束位图绘图上下文并返回绘图结果

    let result = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return result

辅助方法写完,下面开始进行cell的配置,在init()方法中添加以下代码:

        bubbleImageView = UIImageView(image: bubbleImage.incoming, highlightedImage: bubbleImage.incomingHighlighed)
        bubbleImageView.tag = bubbleTag
        bubbleImageView.userInteractionEnabled = true // #CopyMesage

        messageLabel = UILabel(frame: CGRectZero)
        messageLabel.font = UIFont.systemFontOfSize(messageFontSize)
        messageLabel.numberOfLines = 0
        messageLabel.userInteractionEnabled = false   // #CopyMessage

设置气泡视图和消息标签

        super.init(style: .Default, reuseIdentifier: reuseIdentifier)
        selectionStyle = .None

        contentView.addSubview(bubbleImageView)
        bubbleImageView.addSubview(messageLabel)

初始化cell

        bubbleImageView.setTranslatesAutoresizingMaskIntoConstraints(false)
        messageLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
        bubbleImageView.snp_makeConstraints { (make) -> Void in
            make.left.equalTo(contentView.snp_left).offset(10)
            make.top.equalTo(contentView.snp_top).offset(4.5)
            make.width.equalTo(messageLabel.snp_width).offset(30)
            make.bottom.equalTo(contentView.snp_bottom).offset(-4.5)
        
            
        }
        messageLabel.snp_makeConstraints { (make) -> Void in
            make.centerX.equalTo(bubbleImageView.snp_centerX).offset(3)
            make.centerY.equalTo(bubbleImageView.snp_centerY).offset(-0.5)
            messageLabel.preferredMaxLayoutWidth = 218
            make.height.equalTo(bubbleImageView.snp_height).offset(-15)
    
        }

进行autolayout设置

然而这样只是一种聊天气泡,而且没有设置消息内容,我们要根据消息内容和类型对cell进行配置,在这之前我们首先完善我们的消息模型Message,打开Message.swift,在类中添加如下代码:

    let incoming: Bool
    let text: String
    let sentDate: NSDate

    init(incoming: Bool, text: String, sentDate: NSDate) {
        self.incoming = incoming
        self.text = text
        self.sentDate = sentDate
    }

然后回到我们的MessageBubbleTableViewCell.swift,添加以下的配置方法:


    func configureWithMessage(message: Message) {
           //1
            messageLabel.text = message.text
           //2
            let constraints: NSArray = contentView.constraints()
            let indexOfConstraint = constraints.indexOfObjectPassingTest { (var constraint, idx, stop) in
                return (constraint.firstItem as! UIView).tag == bubbleTag && (constraint.firstAttribute == NSLayoutAttribute.Left || constraint.firstAttribute == NSLayoutAttribute.Right)
            }
            contentView.removeConstraint(constraints[indexOfConstraint] as! NSLayoutConstraint)
            //3
            bubbleImageView.snp_makeConstraints({ (make) -> Void in
                if message.incoming {
                    tag = incomingTag
                    bubbleImageView.image = bubbleImage.incoming
                    bubbleImageView.highlightedImage = bubbleImage.incomingHighlighed
                    messageLabel.textColor = UIColor.blackColor()
                  make.left.equalTo(contentView.snp_left).offset(10)
                    messageLabel.snp_updateConstraints { (make) -> Void in
                        make.centerX.equalTo(bubbleImageView.snp_centerX).offset(3)
                    }
                 
                } else { // outgoing
                    tag = outgoingTag
                    bubbleImageView.image = bubbleImage.outgoing
                    bubbleImageView.highlightedImage = bubbleImage.outgoingHighlighed
                    messageLabel.textColor = UIColor.whiteColor()
                     make.right.equalTo(contentView.snp_right).offset(-10)
                    messageLabel.snp_updateConstraints { (make) -> Void in
                        make.centerX.equalTo(bubbleImageView.snp_centerX).offset(-3)
                    }

                   
                }
            })
        
       
    }

//1
设置消息内容。
//2
删除聊天气泡的left或right约束,以便于根据消息类型重新进行设置。
//3
根据消息类型进行对应的设置,包括使用的图片还有约束条件。由于发送消息的聊天气泡是靠右的,而接受消息的聊天气泡是靠左的,所以发送消息的聊天气泡距离cell右边缘10点:

 make.right.equalTo(contentView.snp_right).offset(-10)

接受消息的聊天气泡距离cell左边缘10点:

make.left.equalTo(contentView.snp_left).offset(10)

对应地,消息内容的Label也相应右移或左移3点:

  messageLabel.snp_updateConstraints { (make) -> Void in
                        make.centerX.equalTo(bubbleImageView.snp_centerX).offset(3)
                    }
   messageLabel.snp_updateConstraints { (make) -> Void in
                        make.centerX.equalTo(bubbleImageView.snp_centerX).offset(-3)
                    }

ok,到目前为止我们已经实现了两种tableViewCell,下面我们来看看如何显示出来这些消息!

将聊天内容显示到主界面

这里我们将使用假数据,只是为了演示如何实现,我们将在下一篇文章着重介绍怎么将真实的数据显示出来!
打开ChatViewController.swift文件,在类里添加如下属性,用于存放我们的聊天数据:

var messages:[[Message]] = [[]]

这是一个Message类型的数组,数组的元素也是一个Message类型的数组。为什么要这样定义呢,这是为了区分聊天发生的时间,同一段时间发生的聊天打包到一起组成一个数组元素,超过这一段时间的聊天放到新开辟的数组元素中,这样做也便于我们的tableView确定分区(section)和行(row),同一段时间的聊天放在同一个section,超过这段时间的聊天放在下一个section,每一分区(section)中有几个消息,就有几行(row)。
找到viewDidLoad()方法,在super.viewDidLoad()这行代码下添加如下代码:

        tableView.registerClass(MessageSentDateTableViewCell.self, forCellReuseIdentifier: NSStringFromClass(MessageSentDateTableViewCell))

注册tableViewCell

        self.tableView.keyboardDismissMode = .Interactive
        self.tableView.estimatedRowHeight = 44
        self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom:toolBarMinHeight, right: 0)
        self.tableView.separatorStyle = .None

对tableView进行一些必要的设置,由于tableView底部有一个输入框,因此会遮挡cell,所以要将tableView的内容inset增加一些底部位移:

self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom:toolBarMinHeight, right: 0)
       messages = [
            [
                Message(incoming: true, text: "你叫什么名字?", sentDate: NSDate(timeIntervalSinceNow: -12*60*60*24)),
                Message(incoming: false, text: "我叫灵灵,聪明又可爱的灵灵", sentDate: NSDate(timeIntervalSinceNow:-12*60*60*24))
            ],
            [
                Message(incoming: true, text: "你爱不爱我?", sentDate: NSDate(timeIntervalSinceNow: -6*60*60*24 - 200)),
                Message(incoming: false, text: "爱你么么哒", sentDate: NSDate(timeIntervalSinceNow: -6*60*60*24 - 100))
            ],
            [
                Message(incoming: true, text: "北京今天天气", sentDate: NSDate(timeIntervalSinceNow: -60*60*18)),
                Message(incoming: false, text: "北京:08/30 周日,19-27° 21° 雷阵雨转小雨-中雨 微风小于3级;08/31 周一,18-26° 中雨 微风小于3级;09/01 周二,18-25° 阵雨 微风小于3级;09/02 周三,20-30° 多云 微风小于3级", sentDate: NSDate(timeIntervalSinceNow: -60*60*18))
            ],
            [
                Message(incoming: true, text: "你在干嘛", sentDate: NSDate(timeIntervalSinceNow: -60)),
                Message(incoming: false, text: "我会逗你开心啊", sentDate: NSDate(timeIntervalSinceNow: -65))
            ],
        ]

填充假的聊天数据

重写tableView的代理方法,设置tableView的分区数和行数:

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {

        return messages.count
        
    }
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        return messages[section].count + 1
    }

重写tableView设置cell的代理方法

   override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        if indexPath.row == 0{
            
            let cellIdentifier = NSStringFromClass(MessageSentDateTableViewCell)
            var cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier,forIndexPath: indexPath) as! MessageSentDateTableViewCell
            let message = messages[indexPath.section][0]
            
            
            cell.sentDateLabel.text = "\(message.sentDate)"
            
            return cell
            
        }else{
            let cellIdentifier = NSStringFromClass(MessageBubbleTableViewCell)
            var cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! MessageBubbleTableViewCell!
            if cell == nil {
                
                cell = MessageBubbleTableViewCell(style: .Default, reuseIdentifier: cellIdentifier)
            }
            
            
            
            let message = messages[indexPath.section][indexPath.row - 1]
            
            cell.configureWithMessage(message)
            
            
            
            
            return cell
        }
        
    }

如果没有错误,cmd+R运行一下,应该能出现下面的效果:


iOS Simulator Screen Shot 2015年9月5日.png

消息是正常显示出来了,但是消息的发送时间看起来很别扭,所以我们需要对其进行格式化,在类中添加如下方法:

    func formatDate(date: NSDate) -> String {
        let calendar = NSCalendar.currentCalendar()
        var dateFormatter = NSDateFormatter()
        dateFormatter.locale = NSLocale(localeIdentifier: "zh_CN")
        
        let last18hours = (-18*60*60 < date.timeIntervalSinceNow)
        let isToday = calendar.isDateInToday(date)
        let isLast7Days = (calendar.compareDate(NSDate(timeIntervalSinceNow: -7*24*60*60), toDate: date, toUnitGranularity: .CalendarUnitDay) == NSComparisonResult.OrderedAscending)
        
        if last18hours || isToday {
            dateFormatter.dateFormat = "a HH:mm"
        } else if isLast7Days {
            dateFormatter.dateFormat = "MM月dd日 a HH:mm EEEE"
        } else {
            dateFormatter.dateFormat = "YYYY年MM月dd日 a HH:mm"
            
        }
        return dateFormatter.stringFromDate(date)
    }

你会感觉看到了一些奇怪的东西,所以我来解释一下这些代码:

let calendar = NSCalendar.currentCalendar()

获取当前的日历,我们要使用其中的一些方法

    var dateFormatter = NSDateFormatter()
        dateFormatter.locale = NSLocale(localeIdentifier: "zh_CN")

新建日期格式化器,设置地区为中国大陆

        let last18hours = (-18*60*60 < date.timeIntervalSinceNow)
        let isToday = calendar.isDateInToday(date)
        let isLast7Days = (calendar.compareDate(NSDate(timeIntervalSinceNow: -7*24*60*60), toDate: date, toUnitGranularity: .CalendarUnitDay) == NSComparisonResult.OrderedAscending)

设置一些布尔变量用来判断消息发送时间相对于当前时间有多久

 if last18hours || isToday {
            dateFormatter.dateFormat = "a HH:mm"
        } else if isLast7Days {
            dateFormatter.dateFormat = "MM月dd日 a HH:mm EEEE"
        } else {
            dateFormatter.dateFormat = "YYYY年MM月dd日 a HH:mm"

        }

根据消息新旧来设置日期格式,这些格式由一些占位符和UTF-8字符构成,以下是常用占位符表:

占位符 含义
YYYY 年份
MM 月份
dd
HH 小时
mm 分钟
ss
a 表示上午、下午等
EEEE 星期几

所以在这里日期就被表示为(以2015年9月3日上午10点为例):
a HH:mm 对应上午 10:10
MM月dd日 a HH:mm EEEE对应 9月3日 上午 10:00 星期四
YYYY年MM月dd日 a HH:mm对应2015年9月3日 上午 10:00
现在,在给日期赋值前,调用该方法进行格式化,修改下面这一行代码:

cell.sentDateLabel.text = "\(message.sentDate)"

 cell.sentDateLabel.text = formatDate(message.sentDate)

然后再次运行:

iOS Simulator Screen Shot 2015年9月5日.png

看!这样就很顺眼了吧?
到这里我们的第二部分教程就完成了,第三部分将会实现发送消息、用Alamofire网络请求进行聊天信息的反馈,从Parse服务器接收和保存聊天信息,真正实现和智能机器人聊天!敬请期待!
本篇文章源代码放在了百度网盘里:
下载地址

如果该文章对你有帮助,请点一下喜欢!您的支持是我继续写作的动力!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,551评论 4 58
  • 李智彬阅读 198评论 0 0
  • 第一 ,进入一家公司之前,登陆目标公司网站考察公司规模、 企业文化 、价值观 、找几位资深人士访谈目标公司所处行业...
    教练成长咖啡屋阅读 849评论 0 3
  • 做人大方可以, 但要遇到知恩图报的人, 不然你的一片好心就是白费; 做人善良可以, 但要碰到通情达理的人, 不然你...
    创业中踩的坑阅读 310评论 0 0