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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 10:32:01