美文网首页iOS解惑Swift带我飞
swift实现一个与智能机器人聊天的app(三)

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

作者: codeGlider | 来源:发表于2015-09-08 16:43 被阅读2855次

    本篇文章中你将会学到

    • 从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

    本篇文章源代码下载

    相关文章

      网友评论

      • jk2K:messages 数组定义可以改成这样 `var messages = [[Message]]()` 这样就不存在空的数组元素了
        codeGlider:@jk2K ok,我去改一下👌
      • angelababa:请问一下。有遇到过一个问题,就是在聊天界面时,快速的收到对方的消息,可能一秒3条,这样一秒就调用3次tableViewScrollToBottomAnimated。我在项目中发现。要是对方持续刷屏。会造成主界面卡死。
        codeGlider:@angelababa 好吧。。话说你的对方刷屏是怎么做到的😳你是用了一个聊天服务器吗?
        angelababa:@codeGlider 你能够测试下吗。机器人回复多条。。
        codeGlider:@angelababa 我这里默认的对方就是聊天机器人,所以只会是你发一条收到一条,所没有考虑多条消息的情况😳
      • coolma:简书里面代码格式怎么写?我在编辑器里没找到啊,楼主你怎么用的?
        codeGlider:@coolma 用的markdown,```包裹代码就行了
      • ruiaylin:很不错
        codeGlider:@ruiayLin 谢谢支持!
      • 4a974d65969c:好像现在每一个app用得图灵机器人都是一个key啊 所有人的消息都能看到 :smirk:
        codeGlider:@YouNeed4P 我发现了。。总是发现新消息😂😂😂
      • MJGA:感觉可以给简书安一个小简妹子给大家调戏……
        codeGlider:@简叔 可以啊可以用这个api搞起来😁😁
      • Jieqiudedede:期待优化篇
        codeGlider:@Jieqiudedede 好的,谢谢支持!
      • 庞大不小: :smile: 写得非常详细呢.
        codeGlider:@九夕 是啊,心血之作😁

      本文标题:swift实现一个与智能机器人聊天的app(三)

      本文链接:https://www.haomeiwen.com/subject/twlycttx.html