上一篇文章swift实现一个与智能机器人聊天的app(一)实现了聊天appUI的输入框部分,接下来我会教大家如何实现聊天窗口部分,也就是下图的第二部分:
你可以在这里下载上一篇文章的源代码:
上一篇文章源代码
首先打开我们的项目,你可以找到用于实现该部分的文件:
MessageBubbleTableViewCell.swift和MessageSentDateTableViewCell.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种图片:发送消息气泡的正常和高亮(被点击后)图片,接收消息气泡的正常和高亮图片,以供调用。
这是图片的原型,不难理解这是发送消息对应的聊天气泡,所以直接调用即可
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的值互相交换即可。
然后通过
UIImage
的resizableImageWithCapInsets()
方法,获取可拉伸图片:
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运行一下,应该能出现下面的效果:
消息是正常显示出来了,但是消息的发送时间看起来很别扭,所以我们需要对其进行格式化,在类中添加如下方法:
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)
然后再次运行:
看!这样就很顺眼了吧?
到这里我们的第二部分教程就完成了,第三部分将会实现发送消息、用Alamofire网络请求进行聊天信息的反馈,从Parse服务器接收和保存聊天信息,真正实现和智能机器人聊天!敬请期待!
本篇文章源代码放在了百度网盘里:
下载地址