SwiftUI(iOS)中Text与TextEditor对齐及性能优化问题
在SwiftUI(iOS)开发中,我通过ZStack叠加**透明前景色的TextEditor(接收用户输入)和带颜色分词的Text(实现代码高亮)**做代码编辑器。模型会把代码字符串拆成[[Substring]]结构,比如"x=1\ny=x+1"会被拆成[["x", "=", "1"], ["y", "=", "x", "+", "1"]]。
最初用reduce拼接Text的方案:
lines.reduce(Text("")) { text, line in text + line.reduce(Text("")) { text, word in text + Text(word) } + Text("\n") }
对齐效果没问题,但代码长了之后,用户输入会触发所有Text节点重建,明显卡顿。
改成VStack嵌套ForEach的方案解决性能问题:
VStack(alignment: .leading){ ForEach(0..<lines.count, id:\.self){i in lines[i].reduce(Text("")) { text, word in text + Text(word) } } }
交互流畅了,但Text和TextEditor的行间距对齐出问题——调VStack的spacing或Text的padding时,补偿值在不同设备上不一样。
需要解决两个问题:
- 怎么优化reduce方案的性能?
- 怎么解决ForEach方案的跨设备对齐问题?
一、优化reduce方案的性能
1. 封装行视图,利用SwiftUI自动Diff
把每行的Text拼接逻辑做成独立View,让SwiftUI只重建变化的行,而非全量更新:
struct HighlightedLineView: View { let line: [String] var body: some View { line.reduce(Text("")) { acc, word in acc + Text(word) // 这里可按分词类型设置对应颜色 } } } // 重构后的reduce调用 lines.reduce(Text("")) { acc, line in acc + HighlightedLineView(line: line) + Text("\n") }
SwiftUI会自动识别仅修改的行进行局部更新,减少重建开销。
2. 限制渲染范围(长文本场景)
如果代码很长,用ScrollView+LazyVStack(iOS14+)只渲染可见区域的行,兼顾性能和对齐:
ScrollView { LazyVStack(alignment: .leading, spacing: 0) { ForEach(lines.indices, id: \.self) { index in HighlightedLineView(line: lines[index]) } } }
这种方式本质接近ForEach方案,但保留了行内Text拼接的原生对齐特性。
3. 减少不必要的状态更新
给分词数组lines加Equatable自定义比较逻辑,只有内容真的变化时才更新@State或@Published变量,避免频繁触发视图重建。
二、解决ForEach方案的对齐问题
1. 动态匹配行高与间距
通过测量TextEditor和高亮Text的行高,动态计算VStack的spacing值,适配不同设备:
struct ForEachView: View { @State var content = "x=1\n" + "y=x+1\n" + "x=1\n" + "y=x+1" @State var lines = [["x", "=", "1"], ["y", "=", "x", "+", "1"], ["x", "=", "1"], ["y", "=", "x", "+", "1"]] @State private var targetLineHeight: CGFloat = 0 var body: some View { ZStack(alignment: .leading){ VStack(alignment: .leading, spacing: targetLineHeight - UIFont.systemFont(ofSize: UIFont.systemFontSize).lineHeight) { ForEach(lines.indices, id: \.self){i in lines[i].reduce(Text("")) { text, word in text + Text(word) } } .foregroundColor(.red) .border(.orange) Spacer() } .padding([.top, .bottom], 8) .padding([.trailing, .leading], 5) TextEditor(text: $content) .scrollContentBackground(.hidden) .background(.clear) .onAppear { // 获取TextEditor的默认行高 let editorFont = UIFont.preferredFont(forTextStyle: .body) targetLineHeight = editorFont.lineHeight } } } }
通过获取TextEditor的系统行高,反向计算VStack需要的间距,确保跨设备对齐一致。
2. 统一字体
让高亮Text和TextEditor用完全相同的字体,避免字体差异导致行高偏差:
lines[i].reduce(Text("")) { text, word in text + Text(word) } .font(.body) // 和TextEditor默认字体一致,也可显式指定相同字体
TextEditor默认用UIFont.preferredFont(forTextStyle: .body),高亮Text必须匹配这个字体。
3. 移除Text默认边距
Text组件自带微小上下边距,通过以下代码清除:
lines[i].reduce(Text("")) { text, word in text + Text(word) } .lineSpacing(0) .padding(.vertical, 0)
减少额外的间距误差。
内容的提问来源于stack exchange,提问作者InkosiZhong

