MarkdownView 从 0 到 1 —— 回顾整条时间线
熟悉我的朋友应该知道,我一直在开发一个框架,
用于在 SwiftUI 中以原生的方式来渲染 Markdown 文本。
P.S. 这篇文章也是在我做的编辑器中完成的。
早晨发布了 MarkdownView 的 1.0.0-rc
,也就是正式版的候选版本,
写这篇文章主要是想回顾和总结一路过来的技术点和一些解决方案。
为什么要做这个项目
无奈和机遇
其实,很早之前,我就想做一款笔记应用,
写字功能用 PencilKit 能实现,但是总不能只支持手写吧…
但是找了一圈开源社区都找不到很好的用于渲染 Markdown 文本的组件,
恰好在我高考完之后,发现苹果开源了自己的 swift-markdown
用于处理 Markdown 文本的解析。
Typora 结束公测,开始收费
其实是有替代的 Notion
,但是 Web 套壳的 app 多少还有点不喜欢,而且访问也不是很稳定。
基本原理
先来说说 swift-markdown
解析好的数据是如何变成最终的 View
的:
- 原始文本由
swift-markdown
解析生成文档树 - 对应每一个节点,返回一个原生的
SwiftUI View
- 用合适的方法将所有节点的
View
合并在一起打包 - 由 SwiftUI 计算和显示
在第二步中返回的每一个子 View
都需要是相同类型的,不过好在 SwiftUI 给我们提供了 AnyView
来擦除类型。
布局问题
由于文档树中的每一个节点都是一个 AnyView
,面临两个问题:
- 如何控制每一个视图的大小
- 如何动态的使用
HStack
和VStack
来放置横竖两个方向
幸运的是,在 WWDC 22 上,我们可以自定义一个 Layout
,
但不幸的是,Layout Protocol
只支持最新的系统,无法向后兼容。
拆分文本,再结合
除了图片之外的内容,归根结底都是文本,
因此将文本拆分成尽可能小的部分(一个单词或者一个词组)
配合自定义的灵活布局(FlowLayout
)来实现布局。
但是这样的方案属于在运行时拆分(文本)又合并(视图),非常消耗系统资源。
之前的 记录下 MarkdownView 的性能优化 中提到的解决方案只能解决启动时的卡顿和连续输入时的卡顿,
而对于内容的加载速度没有帮助,同时多个异步操作也需要等待 CPU 空闲时才能被派发上去。
最关键的是,按照这样的模式继续下去,文本始终无法选择和复制。
将视图暂存,合并相同的类型
主要想要解决的问题是 加载时间长 和 无法复制 这两个问题。
加载时间长主要是因为频繁地文本拆分和大量的子视图的位置大小计算导致的,
解决方案是使用 SwiftUI 中的 Text
做拼接,同时解决了无法复制的问题,顺便可以向后兼容了一个大版本。
这一块的灵感是受到 在 SwiftUI 中用 Text 实现图文混排 的启发。
创建了一个 ViewContent
用来暂存由节点生成的视图:
1 | struct ViewContent { |
当传入多个 ViewContent
的时候,会检查并合并相邻的、同类型的 ViewContent
,
后面再用 VStack
把一个个的 paragraph 串起来即可得到完整的视图,
测试了下,同一文档的加载速度提升了约 5x
图片和自定义块的支持
通过 ImageDisplayable
和 BlockDirectiveDisplayable
,允许开发者自己定义如何显示相关内容,
学习了类型擦除的实现方案。
AdaptiveGrid
由于向后兼容了一个大版本导致无法直接使用新的 Grid 组件,
因此,还需对老版本系统提供一个类似的视图,
于是写了一个 AdaptiveGrid
,其表现行为与 Grid
几乎一致。
后面我应该会再写一篇博客来说说其背后的实现原理。
学习了 @resultBuilder
的构建,在这里再次感谢肘子哥的两篇博客:
SVG 增强
一直以来,SVG 的渲染都是由 SVGKit
来处理的,
但是问题是,有编译警告、 Package 过于臃肿 且 部分SVG无法正确渲染。
最近改用了原生的 WKWebView
来渲染 SVG(相当于一个网页)
学习了如何使用 JS 来获取网页元素的大小,以此来作为最终整个 SVG 的大小,
并且能够根据可用宽度来自动判断是否启用滚动条等等…
具体的实现可以在代码中找到,我不在做过多的赘述。
总结
道路不是一帆风顺的,总要在跌跌撞撞中成长,
但庆幸的是,这个自主项目,我做出了自己满意的样子,耶~
项目地址:Github OR Swift Package Index
MarkdownView 从 0 到 1 —— 回顾整条时间线