Swift UI - 自定义单元格实现微信聊天界面

注:代码已升级至Swift4

设计需求

微信聊天界面的消息展示列表,实现的功能有:

(1)消息可以是文本消息也可以是图片消息
(2)消息背景为气泡状图片,同时消息气泡可根据内容自适应大小
(3)每条消息旁边有头像,在左边表示发送方,在右边表示接收方
(4)消息按天分组展示
(5)增加消息发送框,可以发送和展示消息

实现思路

(1)需要定义一个数据结构保存消息内容 MessageItem
(2)继承UITableViewCell实现自定义单元格,这里面放入头像和消息体
(3)继承UITableView实现自定义表格,通过读取数据源,进行页面的渲染
(4)消息体根据内容类型不同,用不同的展示方法
(5)每个单元格的高度需要根据内容计算出来
(6)数据由ViewController来提供初始化数据

主要代码

主页面 ViewController.swift

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import UIKit

class ViewController: UIViewController, ChatDataSource, UITextFieldDelegate {

var Chats:NSMutableArray!
var tableView:TableView!
var me:UserInfo!
var you:UserInfo!
var txtMsg:UITextField!

override func viewDidLoad() {
super.viewDidLoad()

setupChatTable()
setupSendPanel()
}

func setupSendPanel()
{
let screenWidth = UIScreen.main.bounds.width
let sendView = UIView(frame:CGRect(x: 0,y: self.view.frame.size.height - 56,width: screenWidth,height: 56))

sendView.backgroundColor = UIColor.lightGray
sendView.alpha = 0.9

txtMsg = UITextField(frame:CGRect(x: 7,y: 10,width: screenWidth - 95,height: 36))
txtMsg.backgroundColor = UIColor.white
txtMsg.textColor = UIColor.black
txtMsg.font = UIFont.boldSystemFont(ofSize: 12)
txtMsg.layer.cornerRadius = 10.0
txtMsg.returnKeyType = UIReturnKeyType.send

//Set the delegate so you can respond to user input
txtMsg.delegate = self
sendView.addSubview(txtMsg)
self.view.addSubview(sendView)

let sendButton = UIButton(frame:CGRect(x: screenWidth - 80,y: 10,width: 72,height: 36))
sendButton.backgroundColor = UIColor(red: 0x37/255, green: 0xba/255, blue: 0x46/255, alpha: 1)
sendButton.addTarget(self, action:#selector(ViewController.sendMessage) ,
for:UIControlEvents.touchUpInside)
sendButton.layer.cornerRadius = 6.0
sendButton.setTitle("发送", for:UIControlState())
sendView.addSubview(sendButton)
}

func textFieldShouldReturn(_ textField:UITextField) -> Bool
{
sendMessage()
return true
}

func sendMessage()
{
//composing=false
let sender = txtMsg
let thisChat = MessageItem(body:sender!.text! as NSString, user:me, date:Date(), mtype:ChatType.mine)
let thatChat = MessageItem(body:"你说的是:\(sender!.text!)" as NSString, user:you, date:Date(), mtype:ChatType.someone)

Chats.add(thisChat)
Chats.add(thatChat)
self.tableView.chatDataSource = self
self.tableView.reloadData()

//self.showTableView()
sender?.resignFirstResponder()
sender?.text = ""
}

/*创建表格及数据*/
func setupChatTable()
{
self.tableView = TableView(frame:CGRect(x: 0, y: 20, width: self.view.frame.size.width, height: self.view.frame.size.height - 76), style: .plain)

//创建一个重用的单元格
self.tableView!.register(TableViewCell.self, forCellReuseIdentifier: "ChatCell")
me = UserInfo(name:"Xiaoming" ,logo:("xiaoming.png"))
you = UserInfo(name:"Xiaohua", logo:("xiaohua.png"))

let zero = MessageItem(body:"最近去哪玩了?", user:you, date:Date(timeIntervalSinceNow:-90096400), mtype:.someone)

let zero1 = MessageItem(body:"去了趟杭州,明天发照片给你哈?", user:me, date:Date(timeIntervalSinceNow:-90086400), mtype:.mine)

let first = MessageItem(body:"你看这风景怎么样,我周末去杭州拍的!", user:me, date:Date(timeIntervalSinceNow:-90000600), mtype:.mine)

let second = MessageItem(image:UIImage(named:"example.png")!, user:me, date:Date(timeIntervalSinceNow:-90000290), mtype:.mine)

let third = MessageItem(body:"太赞了,我也想去那看看呢!", user:you, date:Date(timeIntervalSinceNow:-90000060), mtype:.someone)

let fouth = MessageItem(body:"嗯,下次我们一起去吧!", user:me, date:Date(timeIntervalSinceNow:-90000020), mtype:.mine)

let fifth = MessageItem(body:"三年了,我终究没能看到这个风景", user:you, date:Date(timeIntervalSinceNow:0), mtype:.someone)


Chats = NSMutableArray()
Chats.addObjects(from: [first,second, third, fouth, fifth, zero, zero1])

//set the chatDataSource
self.tableView.chatDataSource = self

//call the reloadData, this is actually calling your override method
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] as! MessageItem
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

用户信息类 UserInfo.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Foundation

/*
* 用户信息类
*/
class UserInfo:NSObject
{
var username:String = ""
var avatar:String = ""

init(name:String, logo:String)
{
self.username = name
self.avatar = logo
}
}

消息体数据结构 MessageItem.swift

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import UIKit

//消息类型,我的还是别人的
enum ChatType {
case mine
case someone
}

class MessageItem {
//用户信息
var user:UserInfo
//消息时间
var date:Date
//消息类型
var mtype:ChatType
//内容视图,标签或者图片
var view:UIView
//边距
var insets:UIEdgeInsets

//设置我的文本消息边距
class func getTextInsetsMine() -> UIEdgeInsets {
return UIEdgeInsets(top:9, left:10, bottom:9, right:17)
}

//设置他人的文本消息边距
class func getTextInsetsSomeone() -> UIEdgeInsets {
return UIEdgeInsets(top:9, left:15, bottom:9, right:10)
}

//设置我的图片消息边距
class func getImageInsetsMine() -> UIEdgeInsets {
return UIEdgeInsets(top:9, left:10, bottom:9, right:17)
}

//设置他人的图片消息边距
class func getImageInsetsSomeone() -> UIEdgeInsets {
return UIEdgeInsets(top:9, left:15, bottom:9, right:10)
}

//构造文本消息体
convenience init(body:NSString, user:UserInfo, 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(user:user, date:date, mtype:mtype, view:label, insets:insets)
}

//可以传入更多的自定义视图
init(user:UserInfo, date:Date, mtype:ChatType, view:UIView, insets:UIEdgeInsets) {
self.view = view
self.user = user
self.date = date
self.mtype = mtype
self.insets = insets
}

//构造图片消息体
convenience init(image:UIImage, user:UserInfo, 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(user:user, date:date, mtype:mtype, view:imageView, insets:insets)
}
}

表格数据协议 ChatDataSource.swift

1
2
3
4
5
6
7
8
9
10
11
12
import Foundation

/*
数据提供协议
*/
protocol ChatDataSource
{
/*返回对话记录中的全部行数*/
func rowsForChatTable( _ tableView:TableView) -> Int
/*返回某一行的内容*/
func chatTableView(_ tableView:TableView, dataForRow:Int)-> MessageItem
}

自定义单元格 TableViewCell.swift

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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.user.username != "")
{
let thisUser = self.msgItem.user
//self.avatarImage.removeFromSuperview()

let imageName = thisUser.avatar != "" ? thisUser.avatar : "noAvatar.png"
self.avatarImage = UIImageView(image:UIImage(named:imageName))

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:CGFloat = 0
//set the frame correctly
self.avatarImage.frame = CGRect(x: avatarX, y: avatarY, width: 50, height: 50)
self.addSubview(self.avatarImage)

//如果只有一行消息(消息框高度不大于头像)则将消息框居中于头像位置
let delta = (50 - (self.msgItem.insets.top
+ self.msgItem.insets.bottom + self.msgItem.view.frame.size.height))/2
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:("left_bubble.png"))!
.stretchableImage(withLeftCapWidth: 21, topCapHeight:25)
} else {
self.bubbleImage.image = UIImage(named:"right_bubble.png")!
.stretchableImage(withLeftCapWidth: 15, topCapHeight:25)
}
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
}
}
}

自定义单元格头部 TableHeaderViewCell.swift

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import UIKit

class TableHeaderViewCell: UITableViewCell
{
var height:CGFloat = 30.0
var label:UILabel!

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

init(reuseIdentifier cellId:String)
{
super.init(style: UITableViewCellStyle.default, reuseIdentifier:cellId)
}

class func getHeight() -> CGFloat
{
return 30.0
}

func setDate(_ value:Date)
{
self.height = 30.0
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy年MM月dd日"
let text = dateFormatter.string(from: value)

if (self.label != nil)
{
self.label.text = text
return
}
self.selectionStyle = UITableViewCellSelectionStyle.none
self.label = UILabel(frame:CGRect(x: CGFloat(0), y: CGFloat(0), width: self.frame.size.width, height: height))

self.label.text = text
self.label.font = UIFont.boldSystemFont(ofSize: 12)

self.label.textAlignment = NSTextAlignment.center
self.label.shadowOffset = CGSize(width: 0, height: 1)
self.label.shadowColor = UIColor.white

self.label.textColor = UIColor.darkGray

self.label.backgroundColor = UIColor.clear

self.addSubview(self.label)
}
}

自定义表格 TableView.swift

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import UIKit

enum ChatBubbleTypingType
{
case nobody
case me
case somebody
}

class TableView: UITableView, UITableViewDelegate, UITableViewDataSource
{
//用于保存所有消息
var bubbleSection:NSMutableArray!
//数据源,用于与 ViewController 交换数据
var chatDataSource:ChatDataSource!

var snapInterval:TimeInterval!
var typingBubble:ChatBubbleTypingType!

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

override init(frame: CGRect, style: UITableViewStyle) {
//the snap interval in seconds implements a headerview to seperate chats
self.snapInterval = TimeInterval(60 * 60 * 24) //one day
self.typingBubble = ChatBubbleTypingType.nobody
self.bubbleSection = NSMutableArray()

super.init(frame:frame, style:style)

self.backgroundColor = UIColor.clear
self.separatorStyle = UITableViewCellSeparatorStyle.none
self.delegate = self
self.dataSource = self
}

override func reloadData()
{
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
self.bubbleSection = NSMutableArray()
var count = 0
if ((self.chatDataSource != nil))
{
count = self.chatDataSource.rowsForChatTable(self)

if(count > 0)
{
let bubbleData = NSMutableArray(capacity:count)

for i in 0 ..< count
{
let object = self.chatDataSource.chatTableView(self, dataForRow:i)
bubbleData.add(object)
}
bubbleData.sort(comparator: sortDate)

var last = ""

var currentSection = NSMutableArray()
// 创建一个日期格式器
let dformatter = DateFormatter()
// 为日期格式器设置格式字符串
dformatter.dateFormat = "dd"

for i in 0 ..< count
{
let data = bubbleData[i] as! MessageItem
// 使用日期格式器格式化日期,日期不同,就新分组
let datestr = dformatter.string(from: data.date as Date)
if (datestr != last)
{
currentSection = NSMutableArray()
self.bubbleSection.add(currentSection)
}
(self.bubbleSection[self.bubbleSection.count-1] as AnyObject).add(data)

last = datestr
}
}
}
super.reloadData()

//滑向最后一部分
let secno = self.bubbleSection.count - 1
let indexPath = IndexPath(row:(self.bubbleSection[secno] as AnyObject).count,section:secno)

self.scrollToRow(at: indexPath, at:UITableViewScrollPosition.bottom,animated:true)
}

//按日期排序方法
func sortDate(_ m1: Any, m2: Any) -> ComparisonResult {
if((m1 as! MessageItem).date.timeIntervalSince1970 < (m2 as! MessageItem).date.timeIntervalSince1970)
{
return ComparisonResult.orderedAscending
}
else
{
return ComparisonResult.orderedDescending
}
}

//第一个方法返回分区数
func numberOfSections(in tableView:UITableView)->Int {
var result = self.bubbleSection.count
if (self.typingBubble != ChatBubbleTypingType.nobody)
{
result += 1;
}
return result;
}

//返回指定分区的行数
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section >= self.bubbleSection.count)
{
return 1
}

return (self.bubbleSection[section] as AnyObject).count + 1
}

//用于确定单元格的高度,如果此方法实现得不对,单元格与单元格之间会错位
func tableView(_ tableView:UITableView, heightForRowAt indexPath:IndexPath)
-> CGFloat {
// Header
if (indexPath.row == 0)
{
return TableHeaderViewCell.getHeight()
}
let section = self.bubbleSection[indexPath.section] as! NSMutableArray
let data = section[indexPath.row - 1]

let item = data as! MessageItem
let height = max(item.insets.top + item.view.frame.size.height + item.insets.bottom, 52) + 17
print("height:\(height)")
return height
}

//返回自定义的 TableViewCell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
// Header based on snapInterval
if (indexPath.row == 0)
{
let cellId = "HeaderCell"

let hcell = TableHeaderViewCell(reuseIdentifier:cellId)
let section = self.bubbleSection[indexPath.section] as! NSMutableArray
let data = section[indexPath.row] as! MessageItem

hcell.setDate(data.date)
return hcell
}
// Standard
let cellId = "ChatCell"

let section = self.bubbleSection[indexPath.section] as! NSMutableArray
let data = section[indexPath.row - 1]

let cell = TableViewCell(data:data as! MessageItem, reuseIdentifier:cellId)

return cell
}
}

引用素材

left_bubble

right_bubble

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :