ScrollView嵌套ForEach中TextField+FocusState崩溃的修复咨询
TextField嵌套ScrollView+ForEach崩溃问题修复方案
问题背景
6款App均出现TextField崩溃,该TextField位于ScrollView嵌套的ForEach组件内,通过FocusState进行焦点的程序化管理。相关实现代码及崩溃日志如下:
实现代码
@StateObject var viewState: PlayerViewState @FocusState var focusItemId: UUID? var body: some View { GeometryReader { reader in ZStack { VStack(spacing: 0) { HStack { Button { viewState.didTapBackButton() } label: { Text(L10n.Localizable.BackButton.title) .font(.HelveticaNeue(size: 20)) .foregroundColor(Asset.accentColor.swiftUIColor) } Spacer() }.padding(EdgeInsets(top: 20, leading: 8, bottom: 0, trailing: 0)) HStack { Spacer() Text(L10n.Localizable.Players.title.uppercased()) .font(.HelveticaNeueMedium(size: 65)) .foregroundColor(Asset.textForegroundColor.swiftUIColor) .padding(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 12)) }.padding(.bottom, 42) ZStack { HStack { Text(L10n.Localizable.Players.Text.enterNameClickOnText) .font(.system(size: 30)) .foregroundColor(Asset.textForegroundColor.swiftUIColor) .rotationEffect(.degrees(-90)) .fixedSize() .frame(width: 50, height: 180) .padding(.bottom, 200) .padding(.leading, 8) Spacer() } ScrollViewReader { value in ScrollView { VStack { ForEach($viewState.players) { player in VStack { TextField(L10n.Localizable.Players.Text.enterName, text: player.name) .focused($focusItemId, equals: player.id) .font(.HelveticaNeueMedium(size: 32)) .foregroundColor(Asset.textForegroundColor.swiftUIColor) .multilineTextAlignment(.trailing) .frame(width: reader.size.width-80) .textContentType(.name) .id(player.id) Spacer() .frame(height: 27) }.padding(.trailing, 12) } Spacer().frame(height: 450) } } .onReceive(viewState.$focusItemId) { item in withAnimation { focusItemId = item value.scrollTo(item, anchor: .top) } } }.padding(.leading, 50) } } } HStack { Spacer().frame(width: 80) VStack { Spacer() VStack(spacing: 30) { Spacer() Button { viewState.didTapAddPlayerButton() } label: { HStack { Spacer() Text(L10n.Localizable.Players.Button.addPlayer) .font(.HelveticaNeueLight(size: 35)) .foregroundColor(Asset.accentColor.swiftUIColor) } }.padding(.trailing, 20) Button { viewState.didTapToPlayButton() } label: { HStack { Spacer() Text(L10n.Localizable.Players.Button.next) .font(.HelveticaNeueMedium(size: 44)) .foregroundColor(Asset.accentColor.swiftUIColor) } }.padding(.trailing, 20) } .frame(height: 150) .ignoresSafeArea() .background(Asset.mainColor.swiftUIColor) } } } .background(Asset.mainColor.swiftUIColor) .navigationBarBackButtonHidden(true) .onAppear { viewState.didAppear() } }
崩溃日志摘要
libswiftCore.dylib Array.subscript.read 5 libswiftCore.dylib protocol witness for Collection.subscript.read in conformance [A] 6 SwiftUI closure #1 in Binding<A>.subscript.getter ... Thread 0 Crashed: 0 libswiftCore.dylib 0x000000018c4aa3c8 closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 228 (AssertCommon.swift:108) ... 64 dyld 0x00000001b124de4c start + 2240 (dyldMain.cpp:1298)
修复思路
从崩溃日志的Array.subscript.read可以看出,问题根源是访问数组元素时下标越界,结合FocusState的使用场景,大概率是focusItemId指向了已经被移除的Player实例UUID,导致SwiftUI尝试获取不存在的元素绑定而崩溃。以下是具体修复步骤:
1. 校验FocusState目标UUID的有效性
在接收viewState.focusItemId更新时,先检查该UUID是否存在于当前players数组中,避免无效焦点赋值:
.onReceive(viewState.$focusItemId) { item in guard let targetId = item, viewState.players.contains(where: { $0.id == targetId }) else { focusItemId = nil return } withAnimation { focusItemId = targetId value.scrollTo(targetId, anchor: .top) } }
2. 确保Player数组修改的安全性
在PlayerViewState中修改players数组时,严格保证数组操作的不可变性,避免因数组元素突变导致的绑定失效:
// PlayerViewState示例代码 class PlayerViewState: ObservableObject { @Published var players: [Player] = [] @Published var focusItemId: UUID? func didTapAddPlayerButton() { let newPlayer = Player(id: UUID(), name: "") // 使用不可变操作更新数组,确保SwiftUI能正确追踪变化 players = players + [newPlayer] focusItemId = newPlayer.id } func removePlayer(withId id: UUID) { players = players.filter { $0.id != id } // 移除后清空焦点,或设置到其他有效元素 if focusItemId == id { focusItemId = players.last?.id } } }
注意:Player必须是遵循
Identifiable的struct类型,才能让ForEach($viewState.players)生成正确的绑定。如果Player是class,需改为ForEach(viewState.players) { player in并配合@ObservedObject使用。
3. 分离焦点设置与滚动操作的时序
避免在同一段动画中同时执行焦点设置和滚动,可能导致竞态条件,可将滚动操作延迟到主线程异步执行:
.onReceive(viewState.$focusItemId) { item in guard let targetId = item, viewState.players.contains(where: { $0.id == targetId }) else { focusItemId = nil return } focusItemId = targetId // 异步执行滚动,避免与焦点状态更新冲突 DispatchQueue.main.async { withAnimation { value.scrollTo(targetId, anchor: .top) } } }
4. 避免GeometryReader的布局依赖问题
原代码中TextField的宽度依赖GeometryReader的尺寸,可能在布局刷新时导致TextField状态异常,建议改用padding实现相对宽度:
TextField(L10n.Localizable.Players.Text.enterName, text: player.name) // 替换frame(width: reader.size.width-80)为padding .padding(.horizontal, 40) .focused($focusItemId, equals: player.id) .font(.HelveticaNeueMedium(size: 32)) .foregroundColor(Asset.textForegroundColor.swiftUIColor) .multilineTextAlignment(.trailing) .textContentType(.name) .id(player.id)
内容的提问来源于stack exchange,提问作者Artem Tishchenko
相关产品推荐
相关产品推荐

