douyin-ios-swift 表情系统源码解析:如何用 NSTextAttachment 实现抖音式表情编辑(完整指南)

发布时间:2026/10/16 4:43:49

douyin-ios-swift 表情系统源码解析:如何用 NSTextAttachment 实现抖音式表情编辑(完整指南) douyin-ios-swift 表情系统源码解析如何用 NSTextAttachment 实现抖音式表情编辑完整指南【免费下载链接】douyin-ios-swift抖音 iOS Swift版项目地址: https://gitcode.com/gh_mirrors/do/douyin-ios-swiftdouyin-ios-swift 是一款用 Swift 编写的高仿抖音 iOS 客户端其 IM 群聊模块中用NSTextAttachment实现了完整的抖音式表情编辑能力在输入框里像输入文字一样插入表情图片、点击删除键整块删掉表情、发送时自动转回[文本]形式存储与传输。本文带你读懂这套表情系统的源码结构与核心实现。表情系统整体结构4个核心文件分工整个表情系统围绕编辑和展示两条链路展开涉及以下模块文件职责EmotionHelper.swift核心工具类文本⇄表情双向转换NSTextAttachmentEmotion.swift给附件附加表情键名EmotionSelector.swift底部表情选择器面板ChatTextView.swift聊天输入框插入/删除表情入口数据方面则依赖 emotion.json表情键值映射表和 Emoticons.bundle 里 100 多张2x/3x表情图片资源。NSTextAttachment 实现原理表情为什么算一个字符核心思想非常巧妙一个表情在文本中只占 1 个字符的位置但渲染时是一张图片。创建一个NSTextAttachment附件把表情图片赋给image属性用NSAttributedString(attachment:)把它包装成长度为 1 的富文本替换掉原占位文本插入UITextView。因此在表情上按删除键时系统会把它当作一个普通字符整体删除这正是整块删除表情效果的关键 。附件的显示区域由bounds决定EmotionHelper.swift 中这样设置让图片高度对齐一行文字attachment.bounds CGRect(x: 0, y: EmotionFont.descender, width: EmotionFont.lineHeight, height: EmotionFont.lineHeight / (image.size.width / image.size.height))表情数据是如何组织的emotion.json 包含两个部分dictkiss → [飞吻]的映射字典key 是 Emoticons.bundle 中的图片文件名不含后缀value 是表情对应的方括号文本array二维数组描述选择器每一页排布的表情文件名供翻页展示。EmotionHelper.swift 通过String.readJson2DicWithFileName定义于 String.swift静态加载这两份数据并提供emotionKeyFromValue/emotionValueFromKey两个方法做 key⇄value 互查。文本⇄表情双向转换的核心代码发送前图片表情转回文本用户在输入框插入的是图片附件但消息要发给服务端必须先转成纯文本。emotionToString遍历富文本中所有attachment属性把每个附件替换回[文本]static func emotionToString(str: NSMutableAttributedString) - NSAttributedString { str.enumerateAttribute(.attachment, in: NSRange(location: 0, length: str.length), options: .longestEffectiveRangeNotRequired) { value, range, stop in if let attachment value as? NSTextAttachment, let key attachment.emotionKey { str.replaceCharacters(in: range, with: EmotionHelper.emotionValueFromKey(key: key)) } } return str }这里有个小细节附件上的emotionKey是通过关联对象Associated Object扩展NSTextAttachment加上的见 NSTextAttachmentEmotion.swift——因为原生NSTextAttachment无法直接添加属性extension NSTextAttachment { var emotionKey: String? { get { objc_getAssociatedObject(self, NSTextAttachment._emotionKey) as? String } set { objc_setAssociatedObject(self, NSTextAttachment._emotionKey, newValue, .OBJC_ASSOCIATION_RETAIN) } } }展示时文本再转回图片收到消息后例如[飞吻]stringToEmotion用正则\\[.*?\\]找出所有方括号文本逐一查表换成图片附件并维护lengthOffset修正替换后偏移let pattern \\[.*?\\] let matches regex?.matches(in: str.string, options: [], range: NSRange(location: 0, length: str.length)) ?? [] for match in matches { let attachment NSTextAttachment() attachment.image UIImage(contentsOfFile: emotionPath) // ... 设置 bounds 后用 replaceCharacters 原位替换 }聊天列表各消息 CellTextMessageCell.swift、SystemMessageCell.swift、TimeCell.swift都在initData和计算高度时调用stringToEmotion保证气泡内显示图片表情且高度计算与显示一致。表情选择器与输入框的联动EmotionSelector.swift 用横向滚动的UICollectionView呈现三页表情每页 21 个格子第 20 行固定放删除键底部带页码指示点左右滑动手势切换页面。ChatTextView.swift 作为总控处理三个关键事件选中表情onSelect在光标位置调用EmotionHelper.insertEmotion插入附件并把光标后移 1 位删除键onDelete直接textView.deleteBackward()由于表情只占 1 字符自然整块删除发送onSend先emotionToString转回纯文本再交给上层发送同时用 KVO 监听attributedText变化来控制发送按钮的可用状态。func onSelect(emotionKey: String) { let location textView.selectedRange.location textView.attributedText EmotionHelper.insertEmotion(str: textView.attributedText, index: location, key: emotionKey) textView.selectedRange NSRange(location: location 1, length: 0) }总结这套表情系统值得借鉴的 3 个设计一符一图用NSTextAttachment让表情在文本流中只占 1 个字符删除、计数、光标移动全部免费获得正确行为存文本、展图片传输层永远是[文本]展示层才转图片天然兼容 WebSocket 纯文本协议数据驱动表情列表由 emotion.json 声明新增表情只需加图片文件和一行映射零代码改动。读懂这个模块你也就掌握了 iOS 富文本表情编辑的通用套路可以迁移到自己的 IM 或评论系统中 ✨。【免费下载链接】douyin-ios-swift抖音 iOS Swift版项目地址: https://gitcode.com/gh_mirrors/do/douyin-ios-swift创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/10/14 21:50:51

什么是TRON波场公链?一页读懂去中心化互联网操作系统

什么是TRON波场公链?一页读懂去中心化互联网操作系统 【免费下载链接】documentation Documentations of project TRON 项目地址: https://gitcode.com/gh_mirrors/documentat/documentation TRON波场公链是全球最大的基于区块链的去中心化应用操作系统协议之…

2026/10/16 4:41:29

联合储能参与配电网优化调度:建模、评估与Matlab实现

最近整理联合储能参与配电网优化调度的这套仿真框架时,我最大的感触是:储能接入配电网这件事,难点从来不在“把储能模型写进潮流计算”,而在“怎么让储能和新能源、网络约束一起被塞进同一个优化问题里,还能稳定求到解…

2026/10/16 4:41:29

OTFS完整MATLAB代码例程:从时延-多普勒域到收发端实现

简介:OTFS完整代码例程包面向数字通信与5G/6G物理层研究者,以及希望深入理解正交时频空间调制技术的开发人员,尤其适合高速移动与非视距场景下的仿真学习。代码覆盖OTFS调制、信道生成、接收解调、消息传递检测与干扰消除等关键环节&#xff…

2026/10/16 4:41:29

Mxnet实战:注意力机制在SNLI自然语言推理中的应用

1. 从标题拆解这个项目的真实意图1.1 标题里藏着哪些关键信息看到“Mxnet (40): 注意力机制自然语言推理(SNLI)数据集”这个标题,我第一反应是:这是一条典型的技术学习笔记式命名,括号里的“40”大概率是某个系列教程的…

2026/10/16 4:41:29

基于Mxnet与注意力机制的SNLI自然语言推理实战

1. 项目缘起与整体设计思路自然语言推理(Natural Language Inference,简称NLI)这个任务,说白了就是让机器判断两句话之间的逻辑关系:一句话叫“前提”,另一句话叫“假设”,机器需要判断假设相对…

2026/10/16 4:36:29

Linux 下 GeoClue AccessDenied 报错排查与修复指南

开篇:这个报错比你想象的常见如果你在 Linux 桌面上跑过任何带位置查询功能的应用——比如天气组件、地图软件、或者通过浏览器访问某个想拿你地理位置的网页——那你八成见过这条报错:"Unable to start GeoClue client: GDBus.Error:org.freedeskt…

2026/10/14 11:31:03

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/16 0:01:11

Cephadm实战指南:Linux系统工程师的生产级Ceph集群部署与排障

简介:本资源是一份完整的Ceph分布式存储系统中文技术文档合集,面向Linux系统运维工程师、云平台架构师及存储方向开发者,帮助读者系统掌握Ceph集群的部署、配置与运维核心能力。文档覆盖对象存储(RGW)、块设备&#xf…

2026/10/16 0:01:11

ITK中SpatialObject几何体系解析:坐标系、变换与管道集成

1. 空间对象的定位:为什么 ITK 需要一套几何描述体系很多刚开始接触 ITK 的朋友,包括我自己,最初都是被它的图像滤波、配准、分割这些“正统”功能吸引进来的。等真正上手做点实际项目,比如把分割出来的器官边界做量化分析、给血管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑