先上效果图
忽略粗糙的UI和表情😊
一、准备数据
这里的数据结构是以plist文件的形式,包含图片名称,图片服务器名称以及图片路径。每一个图片文件夹都包含一个
info.plist
文件,再创建一个总的配置plist
文件。如图:
到此为止,plist文件准备好了,接下来就是创建数据模型,首先创建Emoticon
模型即表情模型:
/// 发送给服务器的字符串
@objc var ch: String?
/// 本地图片名称
@objc var png: String?
/// 本地图片文件路径
var imagePath: String {
return Bundle.main.path(forResource: "Emoticons.bundle", ofType: nil)! + "/" + (png ?? "")
}
/// 是否删除按钮
var remove: Bool = false
/// 是否空按钮
var empty: Bool = false
//MARK: - 构造函数
init(remove: Bool) {
super.init()
self.remove = remove
}
init(empty: Bool) {
super.init()
self.empty = empty
}
init(dict: [String: Any]) {
super.init()
setValuesForKeys(dict)
}
/// 重写undefinedkey,防止未找到key崩溃问题
override func setValue(_ value: Any?, forUndefinedKey key: String) {
print(key)
}
/// 重写描述信息
override var description: String {
let keys = ["ch","png"]
return dictionaryWithValues(forKeys: keys).description
}
其次创建表情包模型EmoticonPack
/// 文件路径名称(对应总配置文件)
@objc var id: String?
/// 表情组名称
@objc var name: String?
/// 表情模型
@objc lazy var emoticons = [Emoticon]()
需要在EmoticonPack
的构造函数中,配置emoticons
数组
init(dict: [String: Any]) {
super.init()
id = dict["id"] as? String
name = dict["name"] as? String
if let array = (dict["emoticons"] as? [[String: Any]]) {
var index = 0
for var d in array {
if let png = d["png"] as? String, let dir = id {
d["png"] = dir + "/" + png
}
emoticons.append(Emoticon(dict: d))
index += 1
// 添加删除按钮
if index == 20 {
emoticons.append(Emoticon(remove: true))
index = 0
}
}
}
addEmpty()
}
/// 添加空白按钮
private func addEmpty() {
let count = emoticons.count % 21
// 有表情并且能被整除不添加
if emoticons.count > 0 && count == 0 {
return
}
for _ in count..<20 {
emoticons.append(Emoticon(empty: true))
}
emoticons.append(Emoticon(remove: true))
}
最后准备一个单例,依次加载每个plist文件,获取到图片的完整路径,添加到UI数据源。
static let sharedManager = EmoticonManager()
var packs = [EmoticonPack]()
private override init() {
super.init()
// 加载icon.plist文件
let path = Bundle.main.path(forResource: "icon.plist", ofType: nil, inDirectory: "Emoticons.bundle")
let dict = NSDictionary(contentsOfFile: path ?? "") as! [String: Any]
// 获取字典数组中的key对应的value
let array = (dict["icons"] as! NSArray).value(forKey: "id")
/**
(
"com.wzc.icon",
"com.wzc.iconfont"
)
*/
for s in (array as! [String]) {
loadInfoPlist(id: s)
}
}
/// 加载info.plist文件
/// - Parameter id: 文件名称
private func loadInfoPlist(id: String) {
let path = Bundle.main.path(forResource: "info.plist", ofType: nil, inDirectory: "Emoticons.bundle/\(id)")
let dict = NSDictionary(contentsOfFile: path ?? "") as! [String: Any]
packs.append(EmoticonPack(dict: dict))
}
到此位置,数据模型准备完成,开始UI的设置。
二、准备UI
1.UI部分由3个部分组成:
-UICollectionView
即中间的表情键盘
-UIToolBar
即上面的表情切换按钮和下面的表情名称
自定义一个EnIconView
,声明两个属性
private lazy var collectionView: UICollectionView =
UICollectionView(frame: CGRect.zero,
collectionViewLayout: EniconLayout())
private lazy var toolbar = UIToolbar()
为了方便FlowLayout的创建和配置,自定义EniconLayout
继承自UICollectionViewFlowLayout
,可通过私有类的方式写在自定义EnIconView
里面
private class EniconLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
let col: CGFloat = 7
let row: CGFloat = 3
let w: CGFloat = (collectionView?.bounds.width)! / col
let margin = ((collectionView?.bounds.height)! - row * w) * 0.4999
itemSize = CGSize(width: w, height: w)
minimumLineSpacing = 0
minimumInteritemSpacing = 0
sectionInset = UIEdgeInsets(top: margin, left: 0, bottom: margin, right: 0)
scrollDirection = .horizontal
collectionView?.isPagingEnabled = true
collectionView?.bounces = false
collectionView?.showsHorizontalScrollIndicator = false
}
}
自定义EnIconView
的构造函数,这里使用闭包传值,声明一个闭包属性以记录选中表情回调参数。
/// 选中表情回调
private var selectedEmoticonCallBack: (_ emoticon: Emoticon) -> ()
init(selectedEmoticon: @escaping (_ emoticon: Emoticon) -> ()) {
selectedEmoticonCallBack = selectedEmoticon
var rect = UIScreen.main.bounds
rect.size.height = 216+kGestureHeight
super.init(frame: rect)
backgroundColor = UIColor.white
setupUI()
}
根据数据模型数据,创建绑定数据模型,加载表情视图。
这里分享一个小技巧,创建表情视图cell的时候,可以通过bounds.insetBy(dx: 4, dy: 4)
函数,返回一个中心点相同x和y缩小4的frame,这样就不用去计算minimumLineSpacing
和minimumInteritemSpacing
collectionView
的自定义cell
:
//MARK: - 构造函数
override init(frame: CGRect) {
super.init(frame: frame)
iconButton.isUserInteractionEnabled = false
contentView.addSubview(iconButton)
iconButton.backgroundColor = UIColor.white
// 返回一个中心点相同x和y缩小4的frame
iconButton.frame = bounds.insetBy(dx: 4, dy: 4)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - 懒加载控件
private lazy var iconButton: UIButton = UIButton()
最后,通过didSet
绑定图片和删除按钮等。
三、关于表情图片和文字的编排,也可以叫图文混排?
我们知道可以通过NSMutableAttributedString
插入图片,而图片时通过NSTextAttachment
附件传递的,为了取到图片对应的服务器名称,自定义一个继承自NSTextAttachment
的EmoticonAttachment
,它的功能是方便获取插入的图片所对应的表情模型,取到我们需要的数据。
class EmoticonAttachment: NSTextAttachment {
var emoticon: Emoticon
//MARK: - 构造函数
init(emoticon: Emoticon) {
self.emoticon = emoticon
super.init(data: nil, ofType: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
这样,通过EmoticonAttachment
的构造函数,就可以轻松的把表情模型作为一个属性存储在EmoticonAttachment
里面,可以通过这个属性获取表情模型,取到需要的字符。
接下来在在一个视图控制器中,引用自定义EnIconView
private lazy var eniconView: EnIconView = EnIconView { [weak self](emoticon) in
self?.textView.inserEmoticonView(em: emoticon)
}
插入图片
func inserEmoticonView(em: Emoticon) {
if em.empty {
return
}
if em.remove {
deleteBackward()
return
}
let imageText = EmoticonAttachment(emoticon: em).imageText(font: font!)
let attributeM = NSMutableAttributedString(attributedString: attributedText)
// 插入图片
attributeM.replaceCharacters(in: selectedRange, with: imageText)
// 记录光标位置
let range = selectedRange
attributedText = attributeM
selectedRange = NSRange(location: range.location+1, length: 0)
}
这样就可以实现表情图片和文字混排了,最后需要把表情图片所代表的特殊字符串发送到后台:
func sendText() -> String {
let attributeText = attributedText
var strM = String()
attributeText?.enumerateAttributes(in: NSRange(location: 0, length: attributeText!.length), options: [], using: { (dict, range, _) in
if let attachment = dict[NSAttributedString.Key(rawValue: "NSAttachment")] as? EmoticonAttachment {
strM += attachment.emoticon.ch ?? ""
} else {
let str = (attributeText!.string as NSString).substring(with: range)
strM += str
}
})
return strM
}
在此处,便利循环textView的attributeText属性,获取图片所代表的服务器字段,重新append就ok了。
可以直接下载Emoticon导入项目更换里面的表情包。
使用:只需要两步:
1、在控制器中引用
private lazy var eniconView: EnIconView = EnIconView { [weak self](emoticon) in
self?.textView.inserEmoticonView(em: emoticon)
}
2、设置textView的inpuView
textView.inputView = textView.inputView == nil ? eniconView : nil
如有错误和疏漏之处,欢迎在评论区留言斧正。
完整Demo传送门 提取密码:mq4e