Swift UI – 自定义单元格实现微信聊天界面
注:代码已升级至Swift4
设计需求
微信聊天界面的消息展示列表,实现的功能有:
(1)消息可以是文本消息也可以是图片消息
(2)消息背景为气泡状图片,同时消息气泡可根据内容自适应大小
(3)每条消息旁边有头像,在左边表示发送方,在右边表示接收方
(4)消息按天分组展示
(5)增加消息发送框,可以发送和展示消息
实现思路
(1)需要定义一个数据结构保存消息内容 MessageItem
(2)继承UITableViewCell实现自定义单元格,这里面放入头像和消息体
(3)继承UITableView实现自定义表格,通过读取数据源,进行页面的渲染
(4)消息体根据内容类型不同,用不同的展示方法
(5)每个单元格的高度需要根据内容计算出来
(6)数据由ViewController来提供初始化数据
主要代码
主页面 ViewController.swift
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
import Foundation
/*
* 用户信息类
*/
class UserInfo:NSObject
{
var username:String = ""
var avatar:String = ""
init(name:String, logo:String)
{
self.username = name
self.avatar = logo
}
}
消息体数据结构 MessageItem.swift
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
import Foundation
/*
数据提供协议
*/
protocol ChatDataSource
{
/*返回对话记录中的全部行数*/
func rowsForChatTable( _ tableView:TableView) -> Int
/*返回某一行的内容*/
func chatTableView(_ tableView:TableView, dataForRow:Int)-> MessageItem
}
自定义单元格 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.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
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
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
}
}
引用素材
版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/02/25/swift-ui-customize-cells-to-realize-wechat-chat-view/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论