需求:
-
长按人头文本输入框填入其昵称;格式:@XXX
-
可以@多人
-
用户点击发送后,被@的人会收到推送
思路:
UI:
-
文本输入框:
UITextView
-
@XXX的展示由外部传入对应的昵称 拼接好后制作成图片 交由
NSTextAttachment
实现 -
UITextView
富文本展示,确保用户输入的内容按正常格式展示即可
后台接口关键参数:
// 文本内容
content: String
// @的用户们 每个用户id由","分割
to_uid: String
设计类:
输入框相关
// 接口
/// @某人
/// - Parameters:
/// - name: 用户昵称
/// - uid: 用户ID
public func atPerson(name: String, uid: String)
/// 评论发送成功,外部调用此方法 主要功能就是收键盘、清理临时数据
public func sendReplySuccess()
// InputViewDelegate 处理点击发送回调
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某人时 将key赋值给attachment的accessibilityLabel
/// 发送时 通过key 取到对应的name
private var atPersons: [String: String] = [:]
/// 是否需要更新输入框文本属性
private var isSetAttr = false
数据相关:(与本文无太大关系)
// 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
开始说起:
public func atPerson(name: String, uid: String) {
if uid.isEmpty {
return
}
// 1.以uid为key name为value 存入atPersons
atPersons[uid] = "@\(name) "
// 2.创建NSTextAttachment
let attachment = NSTextAttachment()
// 3.获取拼接好后的name的size
let size = "@\(name) ".getSize(font: .systemFont(ofSize: 14), padding: .zero)
// 4.创建富文本属性
let attributes: [NSAttributedStringKey: Any] = [.font : UIFont.systemFont(ofSize: 14)]
// 5.根据文本和文本大小绘制图片
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.每个用户的uid都是不同的,所以唯一标识就是uid
attachment.accessibilityLabel = "\(uid)"
// 7.创建富文本
let imageAttr = NSMutableAttributedString(attachment: attachment)
// 8.基准线需要偏移,可以按比例计算,我这里偷懒直接写死
imageAttr.addAttribute(.baselineOffset, value: -2, range: NSRange(location: 0, length: imageAttr.length))
// 9.获取当前用户所输入的文本(富文本)我们要插入图片所以需要使用NSMutableAttributedString
let textAttr = NSMutableAttributedString(attributedString: textview.attributedText)
// 10.textview.selectedRange:文本框光标当前所在位置 就是我们要把@XXX插入的位置
textAttr.insert(imageAttr, at: textview.selectedRange.location)
// 11.重新赋值给文本框
textview.attributedText = textAttr
// 12.手动调用一下,textViewDidChange,以便更新行高、更新文本属性等等
textViewDidChange(textview)
// 13.显示键盘
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 字体设置
@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.一旦开始自定义文本样式,你会发现接下来输入的内容Font变小了 因为UITextView默认12,我们设置的14,我们需要保证样式统一。
在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
// 中文键盘使用富文本会出现光标乱动、没有候选词、输入过程中出现英文等问题
// 判断中文键盘 更新光标位置即可
if text.count != 0 {
if let lang = textView.textInputMode?.primaryLanguage {
if lang == "zh-Hans" {
// 中文输入
// markedTextRange: 当前是否有候选词,没有候选词就更新光标
if textView.markedTextRange == nil {
let range = textView.selectedRange
updateTextViewAttribute(textview: textView)
textView.selectedRange = range
}
}else {
updateTextViewAttribute(textview: textView)
}
}
// 用户输入回车,就是发送
if text == "\n" {
// 简单的判空逻辑,不重要
if textView.text.trim().isEmpty {
DWBToast.showCenter(withText: "内容不能为空")
return false
}
// readyToSend就是发送前对富文本的校验了,我们要将attachment展示的图片还原为普通文本 这里的还原不是UI上的还原,而是转为to_uid
if let model = readyToSend(textView: textView) {
//发送!
delegate?.sendReply(model: model)
}
return false
}
return true
}
// typingAttributes 将要键入的文本属性。我们用它来使新输入的文本格式与前面统一。
// 网上有很多方式,比如在这里重新给textview.attributeText添加文本样式,但是属性会覆盖。我们的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.文本数据校验和结合
private func readyToSend(textView: UITextView)-> InputViewContentModel? {
// 检查是否含有@某某
var uids = [String]()
let attrs = NSMutableAttributedString(attributedString: textView.attributedText)
// 遍历文本框富文本所有包含attachment的节点
attrs.enumerateAttribute(.attachment,
in: NSRange(location: 0, length: textview.attributedText.length),
options: .longestEffectiveRangeNotRequired) { (attrKey, range, pointer) in
/*
1. 取节点
2. 取节点的标识(uid)
3. 通过标识取value(name)
*/
guard
let attrKey = attrKey as? NSTextAttachment,
let key = attrKey.accessibilityLabel,
let value = atPersons[key]
else {
return
}
// 将转换成to_uid 用作参数
uids.append(key)
// attachment无法转换为普通文本
// 但attachment的内容与value的内容相同
// 所以将name插入到attachment的位置 以便内容与用户输入的保持一致
attrs.insert(NSAttributedString(string: value), at: range.location)
}
// at 去重
var to_uid = uids.filterDuplicates{$0}
// at 去除掉资讯号和机器人(业务中他们没有uid)
to_uid.removeAll {$0 == "0"}
// 空格 替换成“ “ 安卓端无法识别
let content = attrs.string.replacingOccurrences(of: "\u{fffc}", with: " ")
// attrs.string 取字符串,attachment无法被识别 所以内容不会重复
let data: [String: Any?] = ["content": content,
"pid": transmitID,
"to_uid": to_uid.joined(separator: ",")]
// 数据转换。
return Mapper<InputViewContentModel>().map(JSON: data as [String : Any])
}
4.最后 服务器回调告诉客户端发送成功后 使用sendRelySuceess()
func sendReplySuccess() {
self.textview.text = ""
self.textview.attributedText = NSMutableAttributedString(string: "")
self.textViewDidChange(textview)
self.atPersons = [:]
// 收键盘
// ...
}
最后:
将@XXX转换为图片 并交由富文本来展示的方式,输出的时候再通过遍历NSAttachment
的方式将文本还原,相对来说比较简单,逻辑上也没有难理解的地方。缺点是@XXX除了删除以外没办法编辑。
网友评论