美文网首页
iOS IM实现@某人功能

iOS IM实现@某人功能

作者: Xu___ | 来源:发表于2021-01-06 19:57 被阅读0次

    需求:

    • 长按人头文本输入框填入其昵称;格式:@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除了删除以外没办法编辑。

    相关文章

      网友评论

          本文标题:iOS IM实现@某人功能

          本文链接:https://www.haomeiwen.com/subject/uyyooktx.html