需求:
長(zhǎng)按人頭文本輸入框填入其昵稱;格式:@XXX
可以@多人
用戶點(diǎn)擊發(fā)送后,被@的人會(huì)收到推送
思路:
UI:
文本輸入框:
UITextView@XXX的展示由外部傳入對(duì)應(yīng)的昵稱 拼接好后制作成圖片 交由
NSTextAttachment實(shí)現(xiàn)UITextView富文本展示,確保用戶輸入的內(nèi)容按正常格式展示即可
后臺(tái)接口關(guān)鍵參數(shù):
// 文本內(nèi)容
content: String
// @的用戶們 每個(gè)用戶id由","分割
to_uid: String
設(shè)計(jì)類(lèi):
輸入框相關(guān)
// 接口
/// @某人
/// - Parameters:
/// - name: 用戶昵稱
/// - uid: 用戶ID
public func atPerson(name: String, uid: String)
/// 評(píng)論發(fā)送成功,外部調(diào)用此方法 主要功能就是收鍵盤(pán)、清理臨時(shí)數(shù)據(jù)
public func sendReplySuccess()
// InputViewDelegate 處理點(diǎn)擊發(fā)送回調(diào)
convenience internal init(maxCount: Int? = nil, placeholder: String = "", delegate: InputViewDelegate)
// 要用到的UITextView代理方法
public func textViewDidChange(_ textView: UITextView)
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
/// 字典 key存uid value存name 用作at某人時(shí) 將key賦值給attachment的accessibilityLabel
/// 發(fā)送時(shí) 通過(guò)key 取到對(duì)應(yīng)的name
private var atPersons: [String: String] = [:]
/// 是否需要更新輸入框文本屬性
private var isSetAttr = false
數(shù)據(jù)相關(guān):(與本文無(wú)太大關(guān)系)
// InputViewContentModel
import Foundation
import ObjectMapper
public class InputViewContentModel: Mappable {
var content: String?
var pid: String?
var to_uid: String?
public required init?(map: Map) {}
public func mapping(map: Map) {
self.content <- map["content"]
self.pid <- map["pid"]
self.to_uid <- map["to_uid"]
}
}
1.那么一切從atPerson開(kāi)始說(shuō)起:
public func atPerson(name: String, uid: String) {
if uid.isEmpty {
return
}
// 1.以u(píng)id為key name為value 存入atPersons
atPersons[uid] = "@\(name) "
// 2.創(chuàng)建NSTextAttachment
let attachment = NSTextAttachment()
// 3.獲取拼接好后的name的size
let size = "@\(name) ".getSize(font: .systemFont(ofSize: 14), padding: .zero)
// 4.創(chuàng)建富文本屬性
let attributes: [NSAttributedStringKey: Any] = [.font : UIFont.systemFont(ofSize: 14)]
// 5.根據(jù)文本和文本大小繪制圖片
if let image = drawImage(with: .clear,
size: CGSize(width: size.width, height: 14),
text: "@\(name) ",
textAttributes: attributes,
circular: false) {
attachment.image = image
attachment.bounds = CGRect(origin: .zero, size: image.size)
}
// 6.每個(gè)用戶的uid都是不同的,所以唯一標(biāo)識(shí)就是uid
attachment.accessibilityLabel = "\(uid)"
// 7.創(chuàng)建富文本
let imageAttr = NSMutableAttributedString(attachment: attachment)
// 8.基準(zhǔn)線需要偏移,可以按比例計(jì)算,我這里偷懶直接寫(xiě)死
imageAttr.addAttribute(.baselineOffset, value: -2, range: NSRange(location: 0, length: imageAttr.length))
// 9.獲取當(dāng)前用戶所輸入的文本(富文本)我們要插入圖片所以需要使用NSMutableAttributedString
let textAttr = NSMutableAttributedString(attributedString: textview.attributedText)
// 10.textview.selectedRange:文本框光標(biāo)當(dāng)前所在位置 就是我們要把@XXX插入的位置
textAttr.insert(imageAttr, at: textview.selectedRange.location)
// 11.重新賦值給文本框
textview.attributedText = textAttr
// 12.手動(dòng)調(diào)用一下,textViewDidChange,以便更新行高、更新文本屬性等等
textViewDidChange(textview)
// 13.顯示鍵盤(pán)
showKeyboard()
}
extension String {
func getSize(font: UIFont, padding: UIEdgeInsets = .zero)-> CGSize {
let str = NSString(string: self)
var size = str.size(withAttributes: [.font : font])
size.width += (padding.left + padding.right)
size.height += (padding.top + padding.bottom)
return size
}
}
/**
繪制圖片
@param color 背景色
@param size 大小
@param text 文字
@param textAttributes 字體設(shè)置
@param isCircular 是否圓形
@return 圖片
*/
+ (UIImage *)drawImageWithColor:(UIColor *)color
size:(CGSize)size
text:(NSString *)text
textAttributes:(NSDictionary<NSAttributedStringKey, id> *)textAttributes
circular:(BOOL)isCircular
{
if (!color || size.width <= 0 || size.height <= 0) return nil;
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
// circular
if (isCircular) {
CGPathRef path = CGPathCreateWithEllipseInRect(rect, NULL);
CGContextAddPath(context, path);
CGContextClip(context);
CGPathRelease(path);
}
// color
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillRect(context, rect);
// text
CGSize textSize = [text sizeWithAttributes:textAttributes];
[text drawInRect:CGRectMake((size.width - textSize.width) / 2, (size.height - textSize.height) / 2, textSize.width, textSize.height) withAttributes:textAttributes];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
2.一旦開(kāi)始自定義文本樣式,你會(huì)發(fā)現(xiàn)接下來(lái)輸入的內(nèi)容Font變小了 因?yàn)閁ITextView默認(rèn)12,我們?cè)O(shè)置的14,我們需要保證樣式統(tǒng)一。
在func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool解決:
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// text.count == 0 表示用戶在刪除字符 我們不需要更新樣式
isSetAttr = text.count != 0
// 中文鍵盤(pán)使用富文本會(huì)出現(xiàn)光標(biāo)亂動(dòng)、沒(méi)有候選詞、輸入過(guò)程中出現(xiàn)英文等問(wèn)題
// 判斷中文鍵盤(pán) 更新光標(biāo)位置即可
if text.count != 0 {
if let lang = textView.textInputMode?.primaryLanguage {
if lang == "zh-Hans" {
// 中文輸入
// markedTextRange: 當(dāng)前是否有候選詞,沒(méi)有候選詞就更新光標(biāo)
if textView.markedTextRange == nil {
let range = textView.selectedRange
updateTextViewAttribute(textview: textView)
textView.selectedRange = range
}
}else {
updateTextViewAttribute(textview: textView)
}
}
// 用戶輸入回車(chē),就是發(fā)送
if text == "\n" {
// 簡(jiǎn)單的判空邏輯,不重要
if textView.text.trim().isEmpty {
DWBToast.showCenter(withText: "內(nèi)容不能為空")
return false
}
// readyToSend就是發(fā)送前對(duì)富文本的校驗(yàn)了,我們要將attachment展示的圖片還原為普通文本 這里的還原不是UI上的還原,而是轉(zhuǎn)為to_uid
if let model = readyToSend(textView: textView) {
//發(fā)送!
delegate?.sendReply(model: model)
}
return false
}
return true
}
// typingAttributes 將要鍵入的文本屬性。我們用它來(lái)使新輸入的文本格式與前面統(tǒng)一。
// 網(wǎng)上有很多方式,比如在這里重新給textview.attributeText添加文本樣式,但是屬性會(huì)覆蓋。我們的NSAttachment就又歪了
// 而typingAttributes完全符合我們的需求。
private func updateTextViewAttribute(textview: UITextView) {
guard isSetAttr else{ return }
var dict = textview.typingAttributes
dict[NSAttributedStringKey.font.rawValue] = UIFont.systemFont(ofSize: 14)
dict[NSAttributedStringKey.baselineOffset.rawValue] = 0
textview.typingAttributes = dict
}
3.文本數(shù)據(jù)校驗(yàn)和結(jié)合
private func readyToSend(textView: UITextView)-> InputViewContentModel? {
// 檢查是否含有@某某
var uids = [String]()
let attrs = NSMutableAttributedString(attributedString: textView.attributedText)
// 遍歷文本框富文本所有包含attachment的節(jié)點(diǎn)
attrs.enumerateAttribute(.attachment,
in: NSRange(location: 0, length: textview.attributedText.length),
options: .longestEffectiveRangeNotRequired) { (attrKey, range, pointer) in
/*
1. 取節(jié)點(diǎn)
2. 取節(jié)點(diǎn)的標(biāo)識(shí)(uid)
3. 通過(guò)標(biāo)識(shí)取value(name)
*/
guard
let attrKey = attrKey as? NSTextAttachment,
let key = attrKey.accessibilityLabel,
let value = atPersons[key]
else {
return
}
// 將轉(zhuǎn)換成to_uid 用作參數(shù)
uids.append(key)
// attachment無(wú)法轉(zhuǎn)換為普通文本
// 但attachment的內(nèi)容與value的內(nèi)容相同
// 所以將name插入到attachment的位置 以便內(nèi)容與用戶輸入的保持一致
attrs.insert(NSAttributedString(string: value), at: range.location)
}
// at 去重
var to_uid = uids.filterDuplicates{$0}
// at 去除掉資訊號(hào)和機(jī)器人(業(yè)務(wù)中他們沒(méi)有uid)
to_uid.removeAll {$0 == "0"}
// 空格 替換成“ “ 安卓端無(wú)法識(shí)別
let content = attrs.string.replacingOccurrences(of: "\u{fffc}", with: " ")
// attrs.string 取字符串,attachment無(wú)法被識(shí)別 所以內(nèi)容不會(huì)重復(fù)
let data: [String: Any?] = ["content": content,
"pid": transmitID,
"to_uid": to_uid.joined(separator: ",")]
// 數(shù)據(jù)轉(zhuǎn)換。
return Mapper<InputViewContentModel>().map(JSON: data as [String : Any])
}
4.最后 服務(wù)器回調(diào)告訴客戶端發(fā)送成功后 使用sendRelySuceess()
func sendReplySuccess() {
self.textview.text = ""
self.textview.attributedText = NSMutableAttributedString(string: "")
self.textViewDidChange(textview)
self.atPersons = [:]
// 收鍵盤(pán)
// ...
}
最后:
將@XXX轉(zhuǎn)換為圖片 并交由富文本來(lái)展示的方式,輸出的時(shí)候再通過(guò)遍歷NSAttachment的方式將文本還原,相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,邏輯上也沒(méi)有難理解的地方。缺點(diǎn)是@XXX除了刪除以外沒(méi)辦法編輯。