Swift - 自定义单元格实现微信聊天界面
(本文代码已升级至Swift3)
1,下面是一个放微信聊天界面的消息展示列表,实现的功能有:

(2)消息体数据结构 MessageItem.swift
(3)表格数据协议 ChatDataSource.swift
(4)自定义表格 TableView.swift
(5)自定义单元格 TableViewCell.swift
6,源码下载:
hangge_559.zip
1,效果图
这里将用户头像位置做个调整,不管消息高度如何,头像总在最上方。这样看起来消息气泡就是向下扩张。

2,源码下载:
hangge_559.zip
1,下面是一个放微信聊天界面的消息展示列表,实现的功能有:
(1)消息可以是文本消息也可以是图片消息
(2)消息背景为气泡状图片,同时消息气泡可根据内容自适应大小
(3)每条消息旁边有头像,在左边表示发送方,在右边表示接收方
2,实现思路
(1)需要定义一个数据结构保存消息内容 MessageItem
(2)继承UITableViewCell实现自定义单元格,这里面放入头像和消息体
(3)继承UITableView实现自定义表格,通过读取数据源,进行页面的渲染
(4)消息体根据内容类型不同,用不同的展示方法
(5)每个单元格的高度需要根据内容计算出来
(6)数据由ViewController来提供初始化数据
3,效果图

4,代码结构

5,主要代码
(1)主页面 ViewController.swift
import UIKit class ViewController: UIViewController, ChatDataSource { var Chats:Array<MessageItem>! var tableView:TableView! override func viewDidLoad() { super.viewDidLoad() setupChatTable() } /*创建表格及数据*/ func setupChatTable() { self.tableView = TableView(frame:self.view.bounds, style: .plain) //创建一个重用的单元格 self.tableView!.register(TableViewCell.self, forCellReuseIdentifier: "MsgCell") let me = "xiaoming.png" let you = "xiaohua.png" let first = MessageItem(body:"你看这风景怎么样,我周末去苏州拍的!", logo:me, date:Date(timeIntervalSinceNow:-600), mtype:.mine) let second = MessageItem(image:UIImage(named:"sz.png")!,logo:me, date:Date(timeIntervalSinceNow:-290), mtype:.mine) let third = MessageItem(body:"太赞了,我也想去那看看呢!",logo:you, date:Date(timeIntervalSinceNow:-60), mtype:.someone) let fouth = MessageItem(body:"嗯,下次我带你去吧!",logo:me, date:Date(timeIntervalSinceNow:-20), mtype:.mine) let fifth = MessageItem(body:"三年了,我终究没能看到这个风景",logo:you, date:Date(timeIntervalSinceNow:0), mtype:.someone) Chats = [first,second, third, fouth, fifth] self.tableView.chatDataSource = self self.tableView.reloadData() self.view.addSubview(self.tableView) } /*返回对话记录中的全部行数*/ func rowsForChatTable(_ tableView:TableView) -> Int { return self.Chats.count } /*返回某一行的内容*/ func chatTableView(_ tableView:TableView, dataForRow row:Int) -> MessageItem { return Chats[row] } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }
(2)消息体数据结构 MessageItem.swift
import UIKit //消息类型,我的还是别人的 enum ChatType { case mine case someone } class MessageItem { //头像 var logo:String //消息时间 var date:Date //消息类型 var mtype:ChatType //内容视图,标签或者图片 var view:UIView //边距 var insets:UIEdgeInsets //设置我的文本消息边距 class func getTextInsetsMine() -> UIEdgeInsets { return UIEdgeInsets(top:5, left:10, bottom:11, right:17) } //设置他人的文本消息边距 class func getTextInsetsSomeone() -> UIEdgeInsets { return UIEdgeInsets(top:5, left:15, bottom:11, right:10) } //设置我的图片消息边距 class func getImageInsetsMine() -> UIEdgeInsets { return UIEdgeInsets(top:11, left:13, bottom:16, right:22) } //设置他人的图片消息边距 class func getImageInsetsSomeone() -> UIEdgeInsets { return UIEdgeInsets(top:11, left:13, bottom:16, right:22) } //构造文本消息体 convenience init(body:NSString, logo:String, date:Date, mtype:ChatType) { let font = UIFont.boldSystemFont(ofSize: 12) let width = 225, height = 10000.0 let atts = [NSFontAttributeName: font] let size = body.boundingRect(with: CGSize(width: CGFloat(width), height: CGFloat(height)), options: .usesLineFragmentOrigin, attributes:atts, context:nil) let label = UILabel(frame:CGRect(x: 0, y: 0, width: size.size.width, height: size.size.height)) label.numberOfLines = 0 label.lineBreakMode = NSLineBreakMode.byWordWrapping label.text = (body.length != 0 ? body as String : "") label.font = font label.backgroundColor = UIColor.clear let insets:UIEdgeInsets = (mtype == ChatType.mine ? MessageItem.getTextInsetsMine() : MessageItem.getTextInsetsSomeone()) self.init(logo:logo, date:date, mtype:mtype, view:label, insets:insets) } //可以传入更多的自定义视图 init(logo:String, date:Date, mtype:ChatType, view:UIView, insets:UIEdgeInsets) { self.view = view self.logo = logo self.date = date self.mtype = mtype self.insets = insets } //构造图片消息体 convenience init(image:UIImage, logo:String, date:Date, mtype:ChatType) { var size = image.size //等比缩放 if (size.width > 220) { size.height /= (size.width / 220); size.width = 220; } let imageView = UIImageView(frame:CGRect(x: 0, y: 0, width: size.width, height: size.height)) imageView.image = image imageView.layer.cornerRadius = 5.0 imageView.layer.masksToBounds = true let insets:UIEdgeInsets = (mtype == ChatType.mine ? MessageItem.getImageInsetsMine() : MessageItem.getImageInsetsSomeone()) self.init(logo:logo, date:date, mtype:mtype, view:imageView, insets:insets) } }
(3)表格数据协议 ChatDataSource.swift
import Foundation /* 数据提供协议 */ protocol ChatDataSource { /*返回对话记录中的全部行数*/ func rowsForChatTable( _ tableView:TableView) -> Int /*返回某一行的内容*/ func chatTableView(_ tableView:TableView, dataForRow:Int)-> MessageItem }
(4)自定义表格 TableView.swift
import UIKit class TableView:UITableView,UITableViewDelegate, UITableViewDataSource { //用于保存所有消息 var bubbleSection:Array<MessageItem>! //数据源,用于与 ViewController 交换数据 var chatDataSource:ChatDataSource! required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override init(frame: CGRect, style: UITableViewStyle) { super.init(frame:frame, style:style) self.bubbleSection = Array<MessageItem>() self.backgroundColor = UIColor.clear self.separatorStyle = UITableViewCellSeparatorStyle.none self.delegate = self self.dataSource = self } override func reloadData() { self.showsVerticalScrollIndicator = false self.showsHorizontalScrollIndicator = false var count = 0 if ((self.chatDataSource != nil)) { count = self.chatDataSource.rowsForChatTable(self) if(count > 0) { for i in 0 ..< count { let object = self.chatDataSource.chatTableView(self, dataForRow:i) bubbleSection.append(object) } //按日期排序方法 bubbleSection.sort(by: { $0.date.timeIntervalSince1970 < $1.date.timeIntervalSince1970 }) } } super.reloadData() } //第一个方法返回分区数,在本例中,就是1 func numberOfSections(in tableView:UITableView)->Int { return 1 } //返回指定分区的行数 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if (section >= self.bubbleSection.count) { return 0 } return self.bubbleSection.count } //用于确定单元格的高度,如果此方法实现得不对,单元格与单元格之间会错位 func tableView(_ tableView:UITableView,heightForRowAt indexPath:IndexPath) -> CGFloat { let data = self.bubbleSection[indexPath.row] return max(data.insets.top + data.view.frame.size.height + data.insets.bottom, 52) } //返回自定义的 TableViewCell func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellId = "MsgCell" let data = self.bubbleSection[indexPath.row] let cell = TableViewCell(data:data, reuseIdentifier:cellId) return cell } }
(5)自定义单元格 TableViewCell.swift
import UIKit class TableViewCell:UITableViewCell { //消息内容视图 var customView:UIView! //消息背景 var bubbleImage:UIImageView! //头像 var avatarImage:UIImageView! //消息数据结构 var msgItem:MessageItem! required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } //- (void) setupInternalData init(data:MessageItem, reuseIdentifier cellId:String) { self.msgItem = data super.init(style: UITableViewCellStyle.default, reuseIdentifier:cellId) rebuildUserInterface() } func rebuildUserInterface() { self.selectionStyle = UITableViewCellSelectionStyle.none if (self.bubbleImage == nil) { self.bubbleImage = UIImageView() self.addSubview(self.bubbleImage) } let type = self.msgItem.mtype let width = self.msgItem.view.frame.size.width let height = self.msgItem.view.frame.size.height var x = (type == ChatType.someone) ? 0 : self.frame.size.width - width - self.msgItem.insets.left - self.msgItem.insets.right var y:CGFloat = 0 //显示用户头像 if (self.msgItem.logo != "") { let logo = self.msgItem.logo self.avatarImage = UIImageView(image:UIImage(named:(logo != "" ? logo : "noAvatar.png"))) self.avatarImage.layer.cornerRadius = 9.0 self.avatarImage.layer.masksToBounds = true self.avatarImage.layer.borderColor = UIColor(white:0.0 ,alpha:0.2).cgColor self.avatarImage.layer.borderWidth = 1.0 //别人头像,在左边,我的头像在右边 let avatarX = (type == ChatType.someone) ? 2 : self.frame.size.width - 52 //头像居于消息底部 let avatarY = height //set the frame correctly self.avatarImage.frame = CGRect(x: avatarX, y: avatarY, width: 50, height: 50) self.addSubview(self.avatarImage) let delta = self.frame.size.height - (self.msgItem.insets.top + self.msgItem.insets.bottom + self.msgItem.view.frame.size.height) if (delta > 0) { y = delta } if (type == ChatType.someone) { x += 54 } if (type == ChatType.mine) { x -= 54 } } self.customView = self.msgItem.view self.customView.frame = CGRect(x: x + self.msgItem.insets.left, y: y + self.msgItem.insets.top, width: width, height: height) self.addSubview(self.customView) //如果是别人的消息,在左边,如果是我输入的消息,在右边 if (type == ChatType.someone) { self.bubbleImage.image = UIImage(named:("yoububble.png"))! .stretchableImage(withLeftCapWidth: 21,topCapHeight:14) } else { self.bubbleImage.image = UIImage(named:"mebubble.png")! .stretchableImage(withLeftCapWidth: 15, topCapHeight:14) } self.bubbleImage.frame = CGRect(x: x, y: y, width: width + self.msgItem.insets.left + self.msgItem.insets.right, height: height + self.msgItem.insets.top + self.msgItem.insets.bottom) } //让单元格宽度始终为屏幕宽 override var frame: CGRect { get { return super.frame } set (newFrame) { var frame = newFrame frame.size.width = UIScreen.main.bounds.width super.frame = frame } } }
6,源码下载:

功能改进一:消息进行分组、添加消息发送栏
1,效果图
(1)消息按天分组展示
(2)增加消息发送框,可以发送和展示消息

功能改进二:聊天气泡向下扩展
从前面两个样例的效果图可以看到,如果某条消息内容多的话,该消息用户头像都是处于最下方,聊天气泡看起像是向上扩张。1,效果图
这里将用户头像位置做个调整,不管消息高度如何,头像总在最上方。这样看起来消息气泡就是向下扩张。

2,源码下载:

太棒了,感谢!
我加到项目里后发现每次只能显示一个分组,很是无奈,不知道要改哪里啊
切换到横屏的时候
TableView的宽度没有更新,导致mine的用户留言被白色区域遮挡,看不到,代码如下:
func setupChatTable()
{
self.tableView = TableView(frame:CGRect(x: 5, y: 20, width: self.view.frame.size.width, height: self.view.frame.size.height - 76), style: .plain)
#width: self.view.frame.size.width 这个宽度没更新,横屏的时候,用的还是竖屏的宽度
航哥你好,我在模拟器中,把屏幕横过来,聊天的窗口的宽度还是保持竖屏的宽度
航哥你好,我是一名刚学ios的菜鸟,您写的swift 代码真的是太好了,对我来说帮助很大。现在有个问题想要请教一下,我研究了半天没弄明白,我想更改聊天内容的字体和字体颜色,请问怎么修改呢?谢谢。
下载了demo学习一下,看到聊天的时间和内容突然觉得是一个伤感的故事呢~还是很感谢站长
请教一下站长,我聊天时,用户的头像需要异步加载,不知道怎么做,我的聊天界面按照你的做的。头像异步不知道怎么加载?能告知一下吗?谢谢
楼主,有没有swift2.2的demo,这是我的邮箱:flyyison7890@qq.com,谢谢!
谢谢回复!好像是因为我的工程名是中文导致的,改成英文名可以正常运行了!谢谢!
楼主您好,我是新手,临摹您这个代码,出了一个问题,已经找了好几天了还是没找出来哪错了。
错误是:在模拟器运行时到Chats = [first,second, third, fouth, fifth]这一句时出现thread1:EXC_BAD_ACCESS错误。然后let first = MessageItem(body:"你看这风景怎么样,我周末去苏州拍的!", logo:me,date:Date(timeIntervalSinceNow:-600), mtype:.mine) 这句代码运行时我通过打印发现它输出的是<<< invalid type >>>
我新建一个工程然后复制您的代码过来也会出现这个问题,我是新手,是不是我忽略了什么很低级的小细节,或者忘了一些别的设置呢,希望楼主能帮忙想一下,谢谢!
楼主 请问在class TableViewCell:UITableViewCell {这个类 继承了UITableViewCell
怎么在这个类点击头像 跳转页面或者 弹一个窗体 谢谢告知一下
头像哪个我已经看到了谢谢
楼主 就是在 TableViewCell 这个类下 继承了UITableViewCell之后 怎么在这个类里实现点击头像 弹一个窗体出来呀 ,我试过UIAlertController之后总是报不在一个层 。 要怎么做请楼主讲一下 谢谢
楼主你好! 怎么把气泡向下一直扩张 不是向上 请告知一下
楼主能不能联系你下 我有关于聊天这个问题
楼主您好!感谢您的代码。做为菜鸟程序员想请教您一下,我想把从后台加载的图片添加到右侧用户对话框的imageView里当做头像,不知道要怎样修改,望指导,谢谢!!
你好!楼主。很喜欢能用上你提供的代码。
我想请教一下:我现在用[消息推送],如何动态新增到聊天窗口中?
感谢……^_^
大神能把聊天记录存在硬盘里面吗,现在这个只是存在内存里面无法实用,新手看了半天代码也不知道该怎么修改法,求帮助谢谢
你好,真机无法运行报错,模拟器却不报错
let data = section[indexPath.row - 1]
let data = section[indexPath.row] as! MessageItem
let data = section[indexPath.row - 1]
这3行报错提示ambiguous use of 'subscript'
另外界面宽度似乎是固定的,怎么添加约束让它适应各种设备
刚好需要,写的很赞,感谢!
self.tableView.chatDataSource = self 这个是什么意思 为什么要 = self 看不懂 多谢指教
sendButton.addTarget(self, action:#selector(self.sendMessage) ,forControlEvents:UIControlEvents.TouchUpInside) 提示我改成这样,不知道对不对,但是功能还是好的
请教问题 真的很着急 就是swift里面根据label上文字返回 返回label高度的方法?能不能具体点~表示这个网站当教科书用了几个月.
好多网站看到过这个,因该是复制楼主的吧。
hangge功能改进版本运行有两个错误,我的是xcode7.1.1
撸主表生气,我是看你做的整个网站是大多数都是精品资源.所以对这个上面的很多期望值都很高...
刚学iOS,demo非常好,谢谢!
写的也忒难看了一点。。这样式...