本篇文章中你将会学到
- 从Parse服务器下载聊天数据并显示到TableView中
- 实现发送消息功能,并加载到TableView中
- 使用Alamofire网络请求库,调用图灵机器人api获得回复信息
- 保存聊天信息到Parse服务器中
首先下载本篇文章的初始项目,也就是上一篇文章完成的项目,如果你跟着我的文章做了,也可以直接打开上一篇文章的完成项目:
百度网盘下载地址
从Parse服务器下载聊天数据并显示到TableView中
打开** AppDelegate.swift**文件,解除方法func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
中以下这行代码的注释:
//Parse.setApplicationId("CYdFL9mvG8jHqc4ZA5PJsWMInBbMMun0XCoqnHgf", clientKey: "6tGOC1uIKeYp5glvJE6MXZOWG9pmLtMuIUdh2Yzo")
与Parse服务器建立连接。
然后打开ChatViewController.swift,找到viewDidLoad()方法,删除其中的假数据:
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))
],
]
替换为函数调用,这个函数用于从Parse数据库加载聊天记录
initData()
新建initData()方法:
func initData(){
var index = 0
var section = 0
var currentDate:NSDate?
//1
var query:PFQuery = PFQuery(className:"Messages")
query.orderByAscending("sentDate")
//2
for object in query.findObjects() as! [PFObject]{
let message = Message(incoming: object["incoming"] as! Bool, text: object["text"] as! String, sentDate: object["sentDate"] as! NSDate)
if index == 0{
currentDate = message.sentDate
}
let timeInterval = message.sentDate.timeIntervalSinceDate(currentDate!)
//3
if timeInterval < 120{
messages[section].append(message)
}else{
section++
messages.append([message])
}
currentDate = message.sentDate
index++
}
}
代码解释:
//1
新建查询,设置查询类为我们定义的Messages类,关于怎么在Parse服务器上新建这个类在我的第一篇文章有对应的讲解,点击查看。
然后设置查询的顺序,按发送时间升序排列,也就是按照正常的聊天顺序排列。
//2这里是一个循环,遍历所有接收的数据,也就是query.findObjects()
返回的数据,按照时间间隔(如果两个消息前后间隔小于120秒将存入同一区)进行聊天消息的分区,就是//3所示部分。
由于可能存在消息已经获取的情况,所以将第二部分用一个if判断包裹起来,如果消息数小于1时才执行(消息数组默认情况下含一个空的数组元素):
if messages.count <= 1{
...
...
...
}
如果没有错误,运行一下,应该能正常显示出聊天记录
iOS Simulator Screen Shot 2015年9月7日 上午10.42.14.png实现发送消息功能,并加载到TableView中
我们的发送按钮sendButton已经添加了事件监控,只要实现监控方法即可:
sendButton.addTarget(self, action: "sendAction", forControlEvents: UIControlEvents.TouchUpInside)
新建sendAction()
方法:
func sendAction() {
//1
messages.append([Message(incoming: false, text: textView.text, sentDate: NSDate())])
textView.text = nil
updateTextViewHeight()
sendButton.enabled = false
//2
let lastSection = tableView.numberOfSections()
tableView.beginUpdates()
tableView.insertSections(NSIndexSet(index: lastSection), withRowAnimation:.Automatic)
tableView.insertRowsAtIndexPaths([
NSIndexPath(forRow: 0, inSection: lastSection),
NSIndexPath(forRow: 1, inSection: lastSection)
], withRowAnimation: .Automatic)
tableView.endUpdates()
tableViewScrollToBottomAnimated(true)
}
//1将新的消息添加到消息数组
//2我们虽然只有一个消息需要添加,但是我们要增加两行cell,因为我们要留出一行来显示发送时间。
tableView.beginUpdates()
告诉我们的系统tableView
开始执行更新
tableView.insertSections(NSIndexSet(index: lastSection), withRowAnimation:.Automatic)
插入一个新分区,这个分区将有两行cell,第一行显示发送时间,第二行显示消息。
tableView.insertRowsAtIndexPaths([
NSIndexPath(forRow: 0, inSection: lastSection),
NSIndexPath(forRow: 1, inSection: lastSection)
], withRowAnimation: .Automatic)
tableView.endUpdates()
执行行插入,并告诉系统我们的TableView
更新完成!
tableViewScrollToBottomAnimated(true)
将TableView
滚动到我们新添加消息的位置,当然这个函数还没有定义,还有updateTextViewHeight()
方法也是,这个函数是为了更新输入框的高度来适应tableView的高度,在sendAction()
方法下方添加这两个帮助方法:
func tableViewScrollToBottomAnimated(animated: Bool) {
let numberOfSections = messages.count
let numberOfRows = messages[numberOfSections - 1].count
if numberOfRows > 0 {
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow:numberOfRows, inSection: numberOfSections - 1), atScrollPosition: .Bottom, animated: animated)
}
}
func updateTextViewHeight() {
let oldHeight = textView.frame.height
let maxHeight = UIInterfaceOrientationIsPortrait(interfaceOrientation) ? textViewMaxHeight.portrait : textViewMaxHeight.landscape
var newHeight = min(textView.sizeThatFits(CGSize(width: textView.frame.width, height: CGFloat.max)).height, maxHeight)
#if arch(x86_64) || arch(arm64)
newHeight = ceil(newHeight)
#else
newHeight = CGFloat(ceilf(newHeight.native))
#endif
if newHeight != oldHeight {
toolBar.frame.size.height = newHeight+8*2-0.5
}
}
我们还需要实现一个textView的代理方法,用来根据输入框有文字与否来决定sendButton是否可以被点击:
func textViewDidChange(textView: UITextView) {
updateTextViewHeight()
sendButton.enabled = textView.hasText()
}
然后运行一下,现在应该可以发送消息了,当然我们是收不到回复的,下面我们就要使用Alamofire实现这部分功能!
使用Alamofire网络请求库,调用图灵机器人api获得回复信息
首先注册图灵机器人,来得到apikey:
图灵机器人注册
ps:这里有一点小私心啦,点这个链接注册可以给我的api升级1000/天的权限,谢谢大家的支持!
注册完成并登陆之后,点击个人中心:
找到apikey:
屏幕快照 2015-09-07 下午3.37.13.png
接下来在AppDelegate.swift中添加一些全局常量,在import下方,
@UIApplicationMain
上方:
let api_key = "替换为你的apikey"
let api_url = "http://www.tuling123.com/openapi/api"
let userId = "eaew233aswq"
api_url是api的调用地址,userId可以随便起,这里的作用是告诉api,聊天的是同一个人,只是为了连接上下文的语义。
然后返回ChatViewController.swift的sendAction()方法中:
在
tableViewScrollToBottomAnimated(true)
之后添加如下代码:
Alamofire.request(.GET, NSURL(string: api_url)!, parameters: ["key":api_key,"info":question,"userid":userId]).responseJSON(options: NSJSONReadingOptions.MutableContainers) { (_,_,data,error) -> Void in
if error == nil{
if let text = data!.objectForKey("text") as? String{
self.messages[lastSection].append(Message(incoming: true, text:text, sentDate: NSDate()))
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([
NSIndexPath(forRow: 2, inSection: lastSection)
], withRowAnimation: .Automatic)
self.tableView.endUpdates()
self.tableViewScrollToBottomAnimated(true)
}
}else{
println("Error occured! \(error?.userInfo)")
}
}
看起来很复杂是吗?没关系我们来详细讲解一下Alamofire的方法:
其实这里有两步,首先创建Alamofire的request对象:
var request = Alamofire.request(.GET, NSURL(string: api_url)!, parameters: ["key":api_key,"info":question,"userid":userId])
- 第一个参数是确定HTTP请求的方法,这里我们用GET方法
- 第二个参数是HTTP请求的地址,是一个NSURL类型的对象,用我们的api请求地址来创建它
- 第三个参数是HTTP请求的参数,是一个[String:String]类型的字典类型
这里我们要求三个参数apikey,发送的消息和uerid
request.responseJSON(options: NSJSONReadingOptions.MutableContainers) { (_,_,data,error) -> Void in
}
然后调用responseJSON函数,因为我们的api返回一个json格式的数据。
该函数有两个参数,一个是读取JSON的选项,.MutableContainers
是指定返回的对象序列化为可变的字典对象,第二个参数是一个尾随闭包(NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void
,用来对返回内容进行处理,这个闭包有四个参数,URL请求, URL反馈, JSON对象,最后一个是错误。
我们只需要返回的JSON对象,和错误信息,所以前两个参数可以省略,用_
表示。
在处理时,首先判断是否有错误,若有错误就打印错误信息:
if error == nil{
...
...
}else{
println("Error occured! \(error?.userInfo)")
}
在用if let语法对对象进行强制拆包,然后就可以用返回的消息构造我们的Message对象了。
填充到tableView的过程和上面对发送消息的处理类似,要注意的一点是我们这次不用插入新的分区了,因为发送和接收消息几乎是同时发送的,间隔肯定小于2分钟,而且只需要插入一行到当前分区的第三行,第一行和第二行分别是时间标签和发送消息。
然后运行一下,应该就能接收到来自萌蠢机器人的消息了!( ⊙ o ⊙ )
不过还有一些消息并不能显示出来,比如你问他"今天北京到上海的航班":
额,航班信息在哪?难道是我们的机器人傻到忘记发给我们了??/(ㄒoㄒ)/~~
当然不是,一定是哪里出了错,我们来打印一下返回的json数据:
{
code = 200000;
text = "亲,已帮你找到航班信息";
url = "http://touch.qunar.com/h5/flight/flightlist?bd_source=chongdong&startCity=%E5%8C%97%E4%BA%AC&destCity=%E4%B8%8A%E6%B5%B7&startDate=2015-09-07&backDate=&flightType=oneWay&priceSortType=1";
}
噢!原来它还返回了一个网址!那么我们怎么处理他呢,我们这里用一个简单的方法,如果json数据里存在这个url的时候,点击气泡就会打开这个url,在MessageBubbleTableViewCell
和Message
类中各添加一个属性:
var url = ""
然后稍稍修改一下sendAction
方法中Alamofire函数调用,将这一行
self.messages[lastSection].append(Message(incoming: true, text:text, sentDate: NSDate()))
修改为:
if let url = data!.objectForKey("url") as? String{
var message = Message(incoming: true, text:text+"\n(点击该消息打开查看)", sentDate: NSDate())
message.url = url
self.messages[lastSection].append(message)
}else{
var message = Message(incoming: true, text:text, sentDate: NSDate())
self.messages[lastSection].append(message)
}
增加一个tableView的代理方法:
override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
var selectedCell = tableView.cellForRowAtIndexPath(indexPath) as! MessageBubbleTableViewCell
if selectedCell.url != ""{
var url = NSURL(string: selectedCell.url)
UIApplication.sharedApplication().openURL(url!)
}
return nil
}
返回nil是因为我们只想知道哪个cell被按了,但是并不想让它变成高亮状态。
这样应该会起作用了!再运行一下看看效果:
(O_O)?看来机票都卖完了。。不过细想也对,都晚上11点了怎么还会有机票卖。。。O__O"…
其实还有其他更复杂的情况,比如你对他说红绕肉怎么做,返回的则是这样一个json数据:
{
"code":308000,
"text":"********",
"list":[{
"name":"",
"info":"",
"detailurl":""
"icon":""
}]
}
参数 | 说明 |
---|---|
code | 状态码 |
text | 文字内容 |
name | 名称 |
info | 详情 |
detailurl | 详情链接 |
icon | 图标地址 |
这些数据我们暂时先不处理,以后会进行一些改进。
保存聊天信息到Parse服务器中
这相对来说就简单一些,新建一个保存消息的方法:
func saveMessage(message:Message){
var saveObject = PFObject(className: "Messages")
saveObject["incoming"] = message.incoming
saveObject["text"] = message.text
saveObject["sentDate"] = message.sentDate
saveObject.saveEventually { (success, error) -> Void in
if success{
println("消息保存成功!")
}else{
println("消息保存失败! \(error)")
}
}
}
然后再每次创建Message类的实例后调用这个方法,具体是sendAction方法中开头位置还有Alamofire函数调用的闭包内。
sendAction方法的完整代码现修改如下:
func sendAction() {
var message = Message(incoming: false, text: textView.text, sentDate: NSDate())
saveMessage(message)
messages.append([message])
question = textView.text
textView.text = nil
updateTextViewHeight()
sendButton.enabled = false
let lastSection = tableView.numberOfSections()
tableView.beginUpdates()
tableView.insertSections(NSIndexSet(index: lastSection), withRowAnimation:.Automatic)
tableView.insertRowsAtIndexPaths([
NSIndexPath(forRow: 0, inSection: lastSection),
NSIndexPath(forRow: 1, inSection: lastSection)
], withRowAnimation: .Automatic)
tableView.endUpdates()
tableViewScrollToBottomAnimated(true)
Alamofire.request(.GET, NSURL(string: api_url)!, parameters: ["key":api_key,"info":question,"userid":userId]).responseJSON(options: NSJSONReadingOptions.MutableContainers) { (_,_,data,error) -> Void in
println(data!)
if error == nil{
if let text = data!.objectForKey("text") as? String{
if let url = data!.objectForKey("url") as? String{
var message = Message(incoming: true, text:text+"\n(点击该消息打开查看)", sentDate: NSDate())
message.url = url
self.saveMessage(message)
self.messages[lastSection].append(message)
}else{
var message = Message(incoming: true, text:text, sentDate: NSDate())
self.saveMessage(message)
self.messages[lastSection].append(message)
}
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([
NSIndexPath(forRow: 2, inSection: lastSection)
], withRowAnimation: .Automatic)
self.tableView.endUpdates()
self.tableViewScrollToBottomAnimated(true)
}
}else{
println("Error occured! \(error?.userInfo)")
}
}
}
这里告诉大家一个小技巧,由于swift需要靠换行来区分语句,所以有的时候swift代码写多了看起来层次很不清楚,但是手动一行一行调整缩进很麻烦,所以选中需要调节缩进的部分,当然你也可以cmd+A全选( ⊙ o ⊙ ),然后control+I。duang~,一切是那么地层次分明你如果觉得太靠左边了,可以设置缩进的大小:
屏幕快照 2015-09-08 下午3.38.16.png 屏幕快照 2015-09-08 下午3.37.21.png然后我们再运行一下app,细心的同学会发现,含有链接的聊天气泡点击后并没有反应,这是因为服务器上的Messages类并没有储存url这个属性,所以我们要调整一下数据库的类,打开Parse的控制面板,(关于Parse的注册和使用在我的第一篇教程里可以找到): 屏幕快照 2015-09-08 下午3.58.30.png
屏幕快照 2015-09-08 下午3.58.48.png
然后修改initData函数,在创建Message对象代码的下方添加如下代码,以便从数据库中取出url属性:
if let url = object["url"] as? String{
message.url = url
}
同样地,在我们保存消息的方法中也将url存入数据库对象,增加箭头所指的代码:
屏幕快照 2015-09-08 下午4.11.40.png
目前我们的app一切都好,只是在键盘弹出时有一些问题:
-
在我们点出键盘时会遮挡消息:
iOS Simulator Screen Shot 2015年9月8日 下午4.14.55.png -
键盘弹出时把tableView拉到底部会有一个很难看的空白:
iOS Simulator Screen Shot 2015年9月8日 下午4.15.21.png
网友评论