SwiftUI 4:ViewThatFits在LazyVStack中无法正确选择视图的问题
LazyVStack中ExpandableText按钮显示异常的原因及修复方案
我实现了一个根据文本长度自动显示/隐藏“Show more/Show less”按钮的ExpandableText视图,在ScrollView搭配VStack使用时功能正常,但替换为LazyVStack后,上下滚动时按钮出现显示错误。
原ExpandableText代码:
struct ExpandableText: View { let text: String let initialLineLimit: Int @State private var isExpanded = false @State private var showButton = false var button: Button<Text> { if isExpanded { return Button("Show less") { isExpanded = false } } else { return Button("Show more") { isExpanded = true } } } var body: some View { Text(text) .lineLimit(isExpanded ? nil : initialLineLimit) .background { ViewThatFits(in: .vertical) { Text(text) .hidden() Color.clear .onAppear { showButton = true } } } if showButton { button } } }
ContentView代码(使用LazyVStack):
struct ContentView: View { var items = ["this is test text", "this is test text this is test text", " this is test text this is test text this is test text", "this is test text this is test text this is test text this is test text", "this is test text this is test text this is test text this is test text this is test text", "this is test text this is test text this is test text this is test text this is test text this is test text", "this is a small text", "this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text", "this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text", "this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text", "this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text this is test text", ] var body: some View { ScrollView { LazyVStack(spacing: 0) { ForEach(items, id: \.self) { item in ExpandableText(text: item, initialLineLimit: 2) .padding() } } } } }
问题原因
LazyVStack的核心特性是视图复用:为了优化性能,它会回收离开可视区域的视图容器,新进入屏幕的列表项会复用这些容器。而原代码中使用@State来保存showButton和isExpanded状态,@State的生命周期和视图容器绑定,而非列表项的内容。这就导致:
- 当短文本项复用了之前长文本项的视图容器时,旧的
showButton = true状态会被保留,错误显示按钮; - 视图复用时,
ViewThatFits的onAppear可能不会重新触发,无法正确更新showButton状态。
修复方案
方案一:使用@StateObject隔离每个列表项的状态
将状态封装到ObservableObject中,利用@StateObject的特性(与视图的identity绑定,而非容器),确保每个列表项的状态独立,不受视图复用影响:
// 封装状态的ObservableObject class ExpandableTextState: ObservableObject { @Published var isExpanded = false @Published var showButton = false } struct ExpandableText: View { let text: String let initialLineLimit: Int // 每个视图实例持有独立的状态对象 @StateObject private var state = ExpandableTextState() var button: Button<Text> { Button(state.isExpanded ? "Show less" : "Show more") { state.isExpanded.toggle() } } var body: some View { Text(text) .lineLimit(state.isExpanded ? nil : initialLineLimit) .background { ViewThatFits(in: .vertical) { // 明确设置lineLimit,确保ViewThatFits的判断准确 Text(text) .lineLimit(initialLineLimit) .hidden() Color.clear .onAppear { state.showButton = true } } } if state.showButton { button } } }
方案二:父视图集中管理状态
将每个列表项的状态移到ContentView中,通过绑定传递给ExpandableText,彻底避免视图复用带来的状态污染:
struct ExpandableText: View { let text: String let initialLineLimit: Int @Binding var isExpanded: Bool @Binding var showButton: Bool var button: Button<Text> { Button(isExpanded ? "Show less" : "Show more") { isExpanded.toggle() } } var body: some View { Text(text) .lineLimit(isExpanded ? nil : initialLineLimit) .background { ViewThatFits(in: .vertical) { Text(text) .lineLimit(initialLineLimit) .hidden() Color.clear .onAppear { showButton = true } } } if showButton { button } } } // 修改后的ContentView struct ContentView: View { var items = ["this is test text", // ... 其余文本项省略 ] // 用字典保存每个文本项的状态,默认值为false @State private var expandedStates: [String: Bool] = [:] @State private var showButtonStates: [String: Bool] = [:] var body: some View { ScrollView { LazyVStack(spacing: 0) { ForEach(items, id: \.self) { item in ExpandableText( text: item, initialLineLimit: 2, isExpanded: $expandedStates[item, default: false], showButton: $showButtonStates[item, default: false] ) .padding() } } } } }
额外优化说明
原代码中ViewThatFits内的Text没有设置lineLimit,建议加上.lineLimit(initialLineLimit),这样ViewThatFits能更准确地判断文本是否需要折叠,避免误判。
内容的提问来源于stack exchange,提问作者JAHelia
相关产品推荐
相关产品推荐

