美文网首页iOS解惑SwiftiOS 新新手
swift实现一个与智能机器人聊天的app(二)

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

作者: codeGlider | 来源:发表于2015-09-05 23:08 被阅读4628次

    上一篇文章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服务器接收和保存聊天信息,真正实现和智能机器人聊天!敬请期待!
    本篇文章源代码放在了百度网盘里:
    下载地址

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

    相关文章

      网友评论

      • 616569607277:不知博主是否还在活跃,我按照这个方法写了输入框,可不知为何输入框不显示呢
      • 巴图鲁:不错
      • 宫城_:mark
      • 76b26b6a5767:谢谢作者
      • DannyYanQingW:第三部分什么时候写呢,期待。。。
      • 影痕残碎:你好,我想问下,那个生成图片的代码应该能优化一下吧,不用在Cell每次使用图片的时候都去创建一下吧
        codeGlider:@影痕残碎 这个确实不能优化了😂😂因为每个cell的大小都是根据文字多少计算出的长宽,所以都是唯一的大小
      • 361d41057faa:关于 autolayout 不是很熟 问下 作者 messageLabel换作 自定义的控件 tableView还可否自适应高度 (比如 说 我自己做个view drawRect文字)
        codeGlider:@灬灬ck 但是文字还是得用UIlabel,或者uitextview
        codeGlider:@灬灬ck 应该是可以的,只要autolayout设置对
      • SlimMan:能告诉我cell的高度是在哪弄的吗,它是怎么根据字数自动变高的啊,我找了半天没找到
        codeGlider:@codeGlider messageBubblecell里面messageLabel.preferredMaxLayoutWidth=218这里设置了最宽宽度,高度可变,所以文字就自动适应高度了
        codeGlider:@SlimMan 不需要设置的,因为它是用的autolayout,高度可变的
      • e743d0a2acfb: return (constraint.firstItem as! UIView).tag == bubbleTag && (constraint.firstAttribute == NSLayoutAttribute.Left || constraint.firstAttribute == NSLayoutAttribute.Right)
        里面constraint是AnyObject!类型,它的firstItem和firstAttribute属性在文件帮助里查不到啊
        codeGlider:@445809317 firstItem指的就是约束的对象,那就是一个uiview啊
        e743d0a2acfb:@codeGlider 为什么constraint.firstItem as! UIView可以把constraint.firstItem转化为UIView?constraint不是约束吗,怎么可以变成View
        codeGlider:@445809317 确实没有。。虽然他是个anyobject 但是实际上它是NSloutConstraint类型。。很是奇怪
      • e743d0a2acfb:CGContextSetBlendMode(context, kCGBlendModeSourceAtop)里的kCGBlendModeSourceAtop我看是在Objective-c里的常数嘛,为什么可以直接用在swift里?
        codeGlider:@445809317 因为他们用的库是同一个啊😂
      • angelababa:支持!!很少见到这么好的干货了!
        codeGlider:@angelababa 感谢支持!
      • Boy_iOS:学习
      • 流河:坐等博主更新第七章
        流河:其实就想看看你怎么做机器人那块 :stuck_out_tongue_closed_eyes:
        codeGlider:@流河 😂😂
        codeGlider:@流河 第七章。。你想得太远了吧
      • 4f6a729e4398:下载完代码不能正常运行,少snapkit,然后pod search 也找不到,这是什么原因啊?
        4f6a729e4398:@codeGlider 嗯 这个clean一下也不行!
        codeGlider:还有我的Xcode版本是6.4,SnapKit要注意大小写,pod是对大小写敏感的
        codeGlider:@枫鸣凯 先clean一下,然后在build一遍试试
      • 4a974d65969c:能发一下最新代码吗 我这边pod install一直都不成功 406823054@qq.com 谢谢了 :joy:
        codeGlider:@YouNeed4P 文章最后有下载地址
      • 5dc20149d87e:有时候看见代码就头晕,看见小编的这个文章我的阅读速度就不由得快了起来
        codeGlider:@本分 谢谢支持😊😊我会继续努力的
      • 576e71a8a8ac:文科生表示逼格好高完全不懂
        576e71a8a8ac:@codeGlider 虽然也系统的学过计算机 不过这种都是考完就忘光光 😞不是这块料
        codeGlider:@Linlin酱 谁说文科生不能学编程的😁😁

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

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