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

96
codeGlider
0.1 2015.09.08 16:43* 字数 2372

本篇文章中你将会学到

  • 从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/天的权限,谢谢大家的支持!

注册完成并登陆之后,点击个人中心:

屏幕快照 2015-09-07 下午3.34.26.png

找到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.swiftsendAction()方法中:

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 ⊙ )
不过还有一些消息并不能显示出来,比如你问他"今天北京到上海的航班":

iOS Simulator Screen Shot 2015年9月7日 下午9.53.38.png

额,航班信息在哪?难道是我们的机器人傻到忘记发给我们了??/(ㄒ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,在MessageBubbleTableViewCellMessage类中各添加一个属性:

 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被按了,但是并不想让它变成高亮状态。
这样应该会起作用了!再运行一下看看效果:

打开url

(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

我们将在下一篇文章详细讲解如何优化这些细节!希望这篇文章对您有帮助!如果觉得有帮助请点一下喜欢,或者打赏!谢谢支持!

还有一点需要说明,这个app没有更新到swift2.0还是使用Xcode6.4进行开发的,今后会更新到swift2.0

本篇文章源代码下载

iOS技术分享