You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 11:25:37