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

SwiftUI异常:UITextField获焦时操作视图致应用崩溃

问题:UITextField获焦时修改视图导致应用崩溃

问题现象

  • 生产应用中100%可复现:当UITextField处于Focused状态时,修改视图(如触发Toggle开关、点击Cancel按钮)会直接导致应用崩溃。

操作流程

  1. 点击「AddCustomSource」按钮,显示文本输入框
  2. 让文本框获取焦点
  3. 点击Cancel按钮或触发任意Toggle开关,应用立即崩溃

控制台报错信息

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempt to delete item containing first responder that refused to resign.
First responder that was asked to resign (returned YES from -resignFirstResponder): <UITextField: 0x13411bc00; frame = (0 0; 290 22); opaque = NO; autoresize = W+H; gestureRecognizers = <NSArray: 0x600002d66880>; placeholder = NewspaperTitle; borderStyle = None; background = <UITextFieldNoBackgroundProvider: 0x6000021e5190: textfield=<UITextField 0x13411bc00>>; layer = <CALayer: 0x6000023568e0>> inside containing view: <SwiftUI.ListCollectionViewCell: 0x134151a00; baseClass = UICollectionViewListCell; frame = (20 82.6667; 358 44); clipsToBounds = YES; layer = <CALayer: 0x60000235d220>> at index path: <NSIndexPath: 0xae4395876dfb5f2e> {length = 2, path = 0 - 1}
Current first responder: <TtGC7SwiftUI15CellHostingViewGVS_15ModifiedContentVS_14_ViewList_ViewVS_26CollectionViewCellModifier
: 0x134152000; frame = (0 0; 358 44); autoresize = W+H; gestureRecognizers = <NSArray: 0x600002d62f70>; layer = <CALayer: 0x60000235dc20>> inside containing view: <SwiftUI.ListCollectionViewCell: 0x134151a00; baseClass = UICollectionViewListCell; frame = (20 82.6667; 358 44); clipsToBounds = YES; layer = <CALayer: 0x60000235d220>> at index path: <NSIndexPath: 0xae4395876dfb5f2e> {length = 2, path = 0 - 1}'
terminating with uncaught exception of type NSException
CoreSimulator 857.14 - Device: iPhone 14 Pro Max (9E3E3FA5-4FCC-47D7-9D91-E1E5E48286FE) - Runtime: iOS 16.2 (20C52) - DeviceType: iPhone 14 Pro Max

相关代码

//
//  ContentView.swift
//  BLE
//
//  Created by Entwicklung Help Tech GmbH on 22.12.22.
//

import SwiftUI
import CoreBluetooth

struct ContentView: View {
    @State var categories: [NewsSource] = []
    @State var isEditing = false
    @State var newSourceName = ""
    var body: some View {
        VStack {
            List($categories, id:\.self ) { $category in
                Section {
                    ForEach($category.sources, id: \.self ) { $source in
                        Toggle(source.name, isOn: $source.active)
                            .foregroundColor(Color.black)
                            .accessibilityLabel(String(localized: "s"))
                            .onChange(of: categories, perform: { newValue in
                                
                            })
                            .tint(Color.blue)
                        if !isEditing {
                            Button { [self] in
                                withAnimation(.spring()) {
                                    isEditing.toggle()
                                }
                            } label: {
                                HStack {
                                    Text(String(localized:"AddCustomSource"))
                                }
                            }
                        }
                        else {
                            AddCell
                            HStack {
                                Button { [self] in
                                    withAnimation(.spring()) {
                                        //isFocused = false
                                        isEditing.toggle()
                                    }
                                } label: {
                                    HStack {
                                        Text(String(localized: "Cancel"))
                                    }
                                }
                                Spacer()
                            }
                        }
                    }
                }
            header: {
                Text(category.name)
            }
            }
            .padding()
        }
        .onAppear {
            for index in 1...10 {
                categories.append(NewsSource(name: "\(index)", feeds: [NewsFeed(name: "News feed \(index)", url: URL(string: "https://google.de")!, active: true)]))
            }
        }
    }
    @ViewBuilder var AddCell: some View {
        VStack {
            HStack {
                Button { [self] in
                    if newSourceName != "" {
                        withAnimation(.spring()) {
                            isEditing.toggle()
                        }
                    }
                } label: {
                    Image(systemName: "plus")
                        .background(.green)
                        .clipShape(Circle())
                        .foregroundColor(.white)
                        .frame(width: 20.0, height: 20.0)
                }
                TextField(String(localized: "NewspaperTitle"),text: $newSourceName)
                    .onSubmit { [self] in
                        if newSourceName != ""{
                            newSourceName = ""
                            withAnimation(.spring()) {
                                isEditing.toggle()
                            }
                        }
                    }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

//
//  Newspaper.swift
//  Activator
//
//  Created by Entwicklung Help Tech GmbH on 20.02.23.
//

struct NewsSource: Codable, Hashable, Identifiable, Equatable {
    var id: UUID { UUID() }
    var name: String
    var sources: [NewsFeed]
    
    init(name: String, feeds: [NewsFeed]) {
        self.name = name
        self.sources = feeds
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.sources = try container.decode([NewsFeed].self, forKey: .sources)
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
    
    static func ==(lhs: NewsSource, rhs: NewsSource) -> Bool {
        return lhs.name == rhs.name && lhs.sources == rhs.sources
    }
}

struct Newspaper: Codable, Hashable {
    var categories : [NewsSource]
}

struct NewsFeed: Codable, Equatable, Hashable, Identifiable {
    var id: UUID? {
        UUID()
    }
    
    let name: String
    let url: URL
    var active: Bool
    var image: String?
}

解答

这不是预期情况,属于SwiftUI与UIKit交互时的焦点管理冲突:当List的Cell包含第一响应者(此处为UITextField)时,修改视图结构(如切换isEditing导致Cell内容变化、或Toggle触发数组更新)会触发List的Cell复用/重排,此时系统尝试让第一响应者 resign,但由于SwiftUI视图更新机制与UIKit焦点管理存在时序冲突,最终引发崩溃。

必须做防御性开发,推荐以下解决思路:

方案1:手动绑定并管理文本框焦点

  1. 在ContentView中添加焦点状态变量:
    @FocusState private var isTextFieldFocused: Bool
    
  2. 给TextField绑定焦点状态,并在提交时主动取消焦点:
    TextField(String(localized: "NewspaperTitle"),text: $newSourceName)
        .focused($isTextFieldFocused)
        .onSubmit {
            if newSourceName != ""{
                newSourceName = ""
                isTextFieldFocused = false
                withAnimation(.spring()) {
                    isEditing.toggle()
                }
            }
        }
    
  3. 在所有触发视图修改的操作前取消焦点,比如Cancel按钮:
    Button {
        isTextFieldFocused = false
        withAnimation(.spring()) {
            isEditing.toggle()
        }
    } label: {
        HStack {
            Text(String(localized: "Cancel"))
        }
    }
    

方案2:避免在List内部动态切换视图结构

当前代码在ForEach的每个Item内部根据isEditing切换不同视图,这种动态变化会导致List的Cell布局频繁变动,容易触发焦点冲突。可以将添加源的UI移到List外部,或用Sheet/FullScreenCover展示,避开List的Cell复用逻辑。

方案3:全局强制取消第一响应者

定义全局方法,在视图修改前强制取消当前第一响应者:

func resignFirstResponderGlobally() {
    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}

在Cancel按钮、Toggle操作前调用该方法:

Button {
    resignFirstResponderGlobally()
    withAnimation(.spring()) {
        isEditing.toggle()
    }
} label: { /* ... */ }

补充说明

这类问题属于SwiftUI对UIKit控件焦点管理的边界Bug,生产环境必须修复,否则会严重影响用户体验,不属于预期行为。


内容的提问来源于stack exchange,提问作者Daniel Jorge

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 23:32:02