先上效果圖
Aug-20-2020 11-46-12.gif
忽略粗糙的UI和表情??
一、準(zhǔn)備數(shù)據(jù)
這里的數(shù)據(jù)結(jié)構(gòu)是以plist文件的形式,包含圖片名稱,圖片服務(wù)器名稱以及圖片路徑。每一個(gè)圖片文件夾都包含一個(gè)
info.plist文件,再創(chuàng)建一個(gè)總的配置plist文件。如圖:
F7CE2097-96D7-4E1B-A5A6-68A1322EA236.png
到此為止,plist文件準(zhǔn)備好了,接下來就是創(chuàng)建數(shù)據(jù)模型,首先創(chuàng)建Emoticon模型即表情模型:
/// 發(fā)送給服務(wù)器的字符串
@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: - 構(gòu)造函數(shù)
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
}
其次創(chuàng)建表情包模型EmoticonPack
/// 文件路徑名稱(對(duì)應(yīng)總配置文件)
@objc var id: String?
/// 表情組名稱
@objc var name: String?
/// 表情模型
@objc lazy var emoticons = [Emoticon]()
需要在EmoticonPack的構(gòu)造函數(shù)中,配置emoticons數(shù)組
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))
}
最后準(zhǔn)備一個(gè)單例,依次加載每個(gè)plist文件,獲取到圖片的完整路徑,添加到UI數(shù)據(jù)源。
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]
// 獲取字典數(shù)組中的key對(duì)應(yīng)的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))
}
到此位置,數(shù)據(jù)模型準(zhǔn)備完成,開始UI的設(shè)置。
二、準(zhǔn)備UI
1.UI部分由3個(gè)部分組成:
-UICollectionView即中間的表情鍵盤
-UIToolBar即上面的表情切換按鈕和下面的表情名稱
自定義一個(gè)EnIconView,聲明兩個(gè)屬性
private lazy var collectionView: UICollectionView =
UICollectionView(frame: CGRect.zero,
collectionViewLayout: EniconLayout())
private lazy var toolbar = UIToolbar()
為了方便FlowLayout的創(chuàng)建和配置,自定義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的構(gòu)造函數(shù),這里使用閉包傳值,聲明一個(gè)閉包屬性以記錄選中表情回調(diào)參數(shù)。
/// 選中表情回調(diào)
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()
}
根據(jù)數(shù)據(jù)模型數(shù)據(jù),創(chuàng)建綁定數(shù)據(jù)模型,加載表情視圖。
這里分享一個(gè)小技巧,創(chuàng)建表情視圖cell的時(shí)候,可以通過bounds.insetBy(dx: 4, dy: 4)函數(shù),返回一個(gè)中心點(diǎn)相同x和y縮小4的frame,這樣就不用去計(jì)算minimumLineSpacing和minimumInteritemSpacing
collectionView的自定義cell:
//MARK: - 構(gòu)造函數(shù)
override init(frame: CGRect) {
super.init(frame: frame)
iconButton.isUserInteractionEnabled = false
contentView.addSubview(iconButton)
iconButton.backgroundColor = UIColor.white
// 返回一個(gè)中心點(diǎn)相同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綁定圖片和刪除按鈕等。
三、關(guān)于表情圖片和文字的編排,也可以叫圖文混排?
我們知道可以通過NSMutableAttributedString插入圖片,而圖片時(shí)通過NSTextAttachment附件傳遞的,為了取到圖片對(duì)應(yīng)的服務(wù)器名稱,自定義一個(gè)繼承自NSTextAttachment的EmoticonAttachment,它的功能是方便獲取插入的圖片所對(duì)應(yīng)的表情模型,取到我們需要的數(shù)據(jù)。
class EmoticonAttachment: NSTextAttachment {
var emoticon: Emoticon
//MARK: - 構(gòu)造函數(shù)
init(emoticon: Emoticon) {
self.emoticon = emoticon
super.init(data: nil, ofType: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
這樣,通過EmoticonAttachment的構(gòu)造函數(shù),就可以輕松的把表情模型作為一個(gè)屬性存儲(chǔ)在EmoticonAttachment里面,可以通過這個(gè)屬性獲取表情模型,取到需要的字符。
接下來在在一個(gè)視圖控制器中,引用自定義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)
// 記錄光標(biāo)位置
let range = selectedRange
attributedText = attributeM
selectedRange = NSRange(location: range.location+1, length: 0)
}
這樣就可以實(shí)現(xiàn)表情圖片和文字混排了,最后需要把表情圖片所代表的特殊字符串發(fā)送到后臺(tái):
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
}
在此處,便利循環(huán)textView的attributeText屬性,獲取圖片所代表的服務(wù)器字段,重新append就ok了。

可以直接下載Emoticon導(dǎo)入項(xiàng)目更換里面的表情包。
使用:只需要兩步:
1、在控制器中引用
private lazy var eniconView: EnIconView = EnIconView { [weak self](emoticon) in
self?.textView.inserEmoticonView(em: emoticon)
}
2、設(shè)置textView的inpuView
textView.inputView = textView.inputView == nil ? eniconView : nil
如有錯(cuò)誤和疏漏之處,歡迎在評(píng)論區(qū)留言斧正。
完整Demo傳送門 提取密碼:mq4e

