上一篇文章swift實現一個與智慧機器人聊天的app(1)實現了聊天appUI的輸入框部分,接下來我會教大家如何實現聊天視窗部分,也就是下圖的第二部分:
你可以在這裡下載上一篇文章的原始碼:
上一篇文章原始碼
首先開啟我們的專案,你可以找到用於實現該部分的檔案:
MessageBubbleTableViewCell.swift和MessageSentDateTableViewCell.swift,分別用來實現訊息傳送時間的cell和聊天氣泡的cell
首先實現訊息傳送時間的cell,開啟MessageBubbleTableViewCell.swift檔案,增加對SnapKit第三方庫的引用:
1 |
import SnapKit |
在類裡增加一個UILabel的屬性,用來顯示時間:
1 |
let sentDateLabel: UILabel |
在override init()方法中新增程式碼:
1 2 3 4 5 |
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) |
設定時間標籤的背景色、字型,文字居中對齊、文字顏色。
1 2 3 |
super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .None contentView.addSubview(sentDateLabel) |
呼叫父類的構造方法。
我們將該cell設定為不可選,因為我們僅僅需要顯示時間而已。
最後將標籤新增到cell的檢視
1 2 3 4 5 6 |
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的使用我在上一篇文章提到了一些,真的十分地好用,上手也很快,只要你想出一個公式,比如上面這段程式碼可以轉化為:
1 2 3 |
sentDateLabel.centerX = contentView.centerX sentDateLabel.top = contentView.top + 13 sentDateLabel.bottom = contentView.bottom - 4.5 |
ok,顯示訊息傳送時間的cell就設定好了。
接下來開啟MessageBubbleTableViewCell.swift檔案,增加新的屬性:
1 2 |
let bubbleImageView: UIImageView let messageLabel: UILabel |
在import下面增加全域性變數,用來標示cell的型別(接受或傳送的訊息):
1 2 |
let incomingTag = 0, outgoingTag = 1 let bubbleTag = 8 |
在類外增加一些方法,在檔案結尾新增以下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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種圖片:傳送訊息氣泡的正常和高亮(被點選後)圖片,接收訊息氣泡的正常和高亮圖片,以供呼叫。
這是圖片的原型,不難理解這是傳送訊息對應的聊天氣泡,所以直接呼叫即可
1 |
let maskOutgoing = UIImage(named: "MessageBubble")! |
然而接受訊息的氣泡和它的關係是水平映象,所以我們要用一個方法獲得它的水平映象圖片:
1 |
let maskIncoming = UIImage(CGImage: maskOutgoing.CGImage, scale: 2, orientation: .UpMirrored)! |
然而這兩個圖片並不能用,因為它的大小是固定的,但是我們的訊息的長度是不定的,所以,要把它們做成大小可變的圖片,首先設定可拉伸區域:
1 2 |
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) |
那麼它是怎麼確定可拉伸區域的呢,這個示意圖可以解釋一切:
實際上這個可拉伸區域只有1×1畫素,但是也夠我們用了,因為這一部分可以無限地橫向或縱向拉伸,接收訊息氣泡和傳送訊息氣泡可拉伸區域唯一的區別就是水平方向上,所以把right和left的值互相交換即可。
然後通過UIImage
的resizableImageWithCapInsets()
方法,獲取可拉伸圖片:
1 2 3 4 |
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()
進行染色處理,就是下面的這個方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 } |
獲取圖片大小
1 |
let rect = CGRect(origin: CGPointZero, size: image.size) |
建立點陣圖繪圖上下文
1 |
UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale) |
獲取點陣圖繪圖上下文,並開始進行渲染操作
1 2 3 4 5 |
let context = UIGraphicsGetCurrentContext() image.drawInRect(rect) CGContextSetRGBFillColor(context, red, green, blue, alpha) CGContextSetBlendMode(context, kCGBlendModeSourceAtop) CGContextFillRect(context, rect) |
獲取到繪圖結果,結束點陣圖繪圖上下文並返回繪圖結果
1 2 3 |
let result = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return result |
輔助方法寫完,下面開始進行cell的配置,在init()方法中新增以下程式碼:
1 2 3 4 5 6 7 8 |
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 |
設定氣泡檢視和訊息標籤
1 2 3 4 5 |
super.init(style: .Default, reuseIdentifier: reuseIdentifier) selectionStyle = .None contentView.addSubview(bubbleImageView) bubbleImageView.addSubview(messageLabel) |
初始化cell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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,在類中新增如下程式碼:
1 2 3 4 5 6 7 8 9 |
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,新增以下的配置方法:
1 2 3 4 5 6 7 |
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 |
//1
設定訊息內容。
//2
刪除聊天氣泡的left或right約束,以便於根據訊息型別重新進行設定。
//3
根據訊息型別進行對應的設定,包括使用的圖片還有約束條件。由於傳送訊息的聊天氣泡是靠右的,而接受訊息的聊天氣泡是靠左的,所以傳送訊息的聊天氣泡距離cell右邊緣10點:
1 |
make.right.equalTo(contentView.snp_right).offset(-10) |
接受訊息的聊天氣泡距離cell左邊緣10點:
1 |
make.left.equalTo(contentView.snp_left).offset(10) |
對應地,訊息內容的Label也相應右移或左移3點:
1 2 3 |
messageLabel.snp_updateConstraints { (make) -> Void in make.centerX.equalTo(bubbleImageView.snp_centerX).offset(3) } |
1 2 3 |
messageLabel.snp_updateConstraints { (make) -> Void in make.centerX.equalTo(bubbleImageView.snp_centerX).offset(-3) } |
ok,到目前為止我們已經實現了兩種tableViewCell,下面我們來看看如何顯示出來這些訊息!
將聊天內容顯示到主介面
這裡我們將使用假資料,只是為了演示如何實現,我們將在下一篇文章著重介紹怎麼將真實的資料顯示出來!
開啟ChatViewController.swift檔案,在類裡新增如下屬性,用於存放我們的聊天資料:
1 |
var messages:[[Message]] = [[]] |
這是一個Message型別的陣列,陣列的元素也是一個Message型別的陣列。為什麼要這樣定義呢,這是為了區分聊天發生的時間,同一段時間發生的聊天打包到一起組成一個陣列元素,超過這一段時間的聊天放到新開闢的陣列元素中,這樣做也便於我們的tableView確定分割槽(section)和行(row),同一段時間的聊天放在同一個section,超過這段時間的聊天放在下一個section,每一分割槽(section)中有幾個訊息,就有幾行(row)。
找到viewDidLoad()方法,在super.viewDidLoad()
這行程式碼下新增如下程式碼:
1 |
tableView.registerClass(MessageSentDateTableViewCell.self, forCellReuseIdentifier: NSStringFromClass(MessageSentDateTableViewCell)) |
註冊tableViewCell
1 2 3 4 |
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增加一些底部位移:
1 |
self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom:toolBarMinHeight, right: 0) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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的分割槽數和行數:
1 2 3 4 5 6 7 8 9 |
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的代理方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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執行一下,應該能出現下面的效果:
訊息是正常顯示出來了,但是訊息的傳送時間看起來很彆扭,所以我們需要對其進行格式化,在類中新增如下方法:
1 2 3 4 5 6 |
func formatDate(date: NSDate) -> String { let calendar = NSCalendar.currentCalendar() var dateFormatter = NSDateFormatter() dateFormatter.locale = NSLocale(localeIdentifier: "zh_CN") let last18hours = (-18*60*60 |
你會感覺看到了一些奇怪的東西,所以我來解釋一下這些程式碼:
1 |
let calendar = NSCalendar.currentCalendar() |
獲取當前的日曆,我們要使用其中的一些方法
1 2 |
var dateFormatter = NSDateFormatter() dateFormatter.locale = NSLocale(localeIdentifier: "zh_CN") |
新建日期格式化器,設定地區為中國大陸
1 |
let last18hours = (-18*60*60 |
設定一些布林變數用來判斷訊息傳送時間相對於當前時間有多久
1 2 3 4 5 6 7 8 |
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
現在,在給日期賦值前,呼叫該方法進行格式化,修改下面這一行程式碼:
1 |
cell.sentDateLabel.text = "(message.sentDate)" |
為
1 |
cell.sentDateLabel.text = formatDate(message.sentDate) |
然後再次執行:
看!這樣就很順眼了吧?
到這裡我們的第二部分教程就完成了,第三部分將會實現傳送訊息、用Alamofire網路請求進行聊天資訊的反饋,從Parse伺服器接收和儲存聊天資訊,真正實現和智慧機器人聊天!敬請期待!
本篇文章原始碼放在了百度網盤裡:
下載地址