iOS IM實(shí)現(xiàn)@某人功能

需求:

  • 長(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)辦法編輯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容