[英]App crashes when SwiftUI view with Picker disappears (since iOS 16)
我們有一個具有一些“聊天”功能的應用程序,可以在其中提出問題,用戶可以使用一些預定義的選項來回答:對於每個問題,都會顯示一個新視圖。 其中一個選項是帶有 Picker 的視圖,因為 iOS 16 當帶有 Picker 的視圖消失並出現以下錯誤時,此 Picker 會導致應用程序崩潰: Thread 1: Fatal error: Index out of range
positioned at class AppDelegate: UIResponder, UIApplicationDelegate {
。 在日志中我可以看到這個錯誤: Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range
。
為了解決這個問題,我將代碼重構到最低限度,即使沒有使用選擇器,但仍然會導致錯誤發生。 當我從此視圖中刪除選擇器時,它再次起作用。
查看哪里出錯
struct PickerQuestion: View {
@EnvironmentObject() var questionVM: QuestionVM
let question: Question
var colors = ["A", "B", "C", "D"]
@State private var selected = "A"
var body: some View {
VStack {
// When removing the Picker from this view the error does not occur anymore
Picker("Please choose a value", selection: $selected) {
ForEach(colors, id: \.self) {
Text($0)
}
}.pickerStyle(.wheel) // with .menu style the crash does not occur
Text("You selected: \(selected)")
Button("Submit", action: {
// In this function I provide an answer that is always valid so I do not
// have to use the Picker it's value
questionVM.answerQuestion(...)
// In this function I submit the answer to the backend.
// The backend will provide a new question which can be again a Picker
// question or another type of question: in both cases the app crashes
// when this view disappears. (the result of the backend is provided to
// the view with `DispatchQueue.main.async {}`)
questionVM.submitAnswerForQuestionWith(questionId: question.id)
})
}
}
}
使用上述視圖的父視圖(注意:即使刪除了所有 animation 相關行,崩潰仍然發生):
struct QuestionContainerView: View {
@EnvironmentObject() var questionVM: QuestionVM
@State var questionVisible = true
@State var questionId = ""
@State var animate: Bool = false
var body: some View {
VStack {
HeaderView(...)
Spacer()
if questionVM.currentQuestion != nil {
ZStack(alignment: .bottom) {
if questionVisible {
getViewForQuestion(question: questionVM.currentQuestion!)
.transition(.asymmetric(
insertion: .move(edge: self.questionVM.scrollDirection == .Next ? .trailing : .leading),
removal: .opacity
))
.zIndex(0)
.onAppear {
self.animate.toggle()
}
.environmentObject(questionVM)
} else {
EmptyView()
}
}
}
}
.onAppear {
self.questionVM.getQuestion()
}
.onReceive(self.questionVM.$currentQuestion) { q in
if let question = q, question.id != self.questionId {
self.questionVisible = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
withAnimation {
self.questionVisible = true
self.questionId = question.id
}
}
}
}
}
func getViewForQuestion(question: Question) -> AnyView {
switch question.questionType {
case .Picker:
return AnyView(TestPickerQuestion(question: question))
case .Other:
...
case ...
}
}
}
該應用程序最初是為 iOS 13 制作的,但仍在維護:隨着 iOS 的每個新版本,該應用程序一直按預期運行,直到現在 iOS 16。
最少的可重現代碼:(將TestView
放在您的ContentView
中)
struct MinimalQuestion {
var id: String = randomString(length: 10)
var text: String
var type: QuestionType
var answer: String? = nil
enum QuestionType: String {
case Picker = "PICKER"
case Info = "INFO"
case Boolean = "BOOLEAN"
}
// https://stackoverflow.com/a/26845710/7142073
private static func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
class QuestionViewModel: ObservableObject {
@Published var questions: [MinimalQuestion] = []
@Published var current: MinimalQuestion? = nil//MinimalQuestion(text: "Picker Question", type: .Picker)
@Published var scrollDirection: ScrollDirection = .Next
func getQuestion() {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0.1...0.2)) {
var question: MinimalQuestion
switch Int.random(in: 0...2) {
case 1:
question = MinimalQuestion(text: "Info", type: .Info)
case 2:
question = MinimalQuestion(text: "Boolean question", type: .Boolean)
default:
question = MinimalQuestion(text: "Picker Question", type: .Picker)
}
self.questions.append(question)
self.current = question
}
}
}
func answerQuestion(question: MinimalQuestion, answer: String) {
if let index = self.questions.firstIndex(where: { $0.id == question.id }) {
self.questions[index].answer = answer
self.current = self.questions[index]
}
}
func submitQuestion(questionId: MinimalQuestion) {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0.1...0.2)) {
self.getQuestion()
}
}
}
func restart() {
self.questions = []
self.current = nil
self.getQuestion()
}
}
struct TestView: View {
@StateObject var questionVM: QuestionViewModel = QuestionViewModel()
@State var questionVisible = true
@State var questionId = ""
@State var animate: Bool = false
var body: some View {
return VStack {
Text("Questionaire")
Spacer()
if questionVM.current != nil {
ZStack(alignment: .bottom) {
if questionVisible {
getViewForQuestion(question: questionVM.current!).environmentObject(questionVM)
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .move(edge: self.questionVM.scrollDirection == .Next ? .trailing : .leading),
removal: .opacity
))
.zIndex(0)
.onAppear {
self.animate.toggle()
}
} else {
EmptyView()
}
}.frame(maxWidth: .infinity)
}
Spacer()
}
.frame(maxWidth: .infinity)
.onAppear {
self.questionVM.getQuestion()
}
.onReceive(self.questionVM.$current) { q in
print("NEW QUESTION OF TYPE \(q?.type)")
if let question = q, question.id != self.questionId {
self.questionVisible = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
withAnimation {
self.questionVisible = true
self.questionId = question.id
}
}
}
}
}
func getViewForQuestion(question: MinimalQuestion) -> AnyView {
switch question.type {
case .Info:
return AnyView(InfoQView(question: question))
case .Picker:
return AnyView(PickerQView(question: question))
case .Boolean:
return AnyView(BoolQView(question: question))
}
}
}
struct PickerQView: View {
@EnvironmentObject() var questionVM: QuestionViewModel
var colors = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
@State private var selected: String? = nil
let question: MinimalQuestion
var body: some View {
VStack {
// When removing the Picker from this view the error does not occur anymore
Picker("Please choose a value", selection: $selected) {
ForEach(colors, id: \.self) {
Text("\($0)")
}
}.pickerStyle(.wheel)
Text("You selected: \(selected ?? "")")
Button("Submit", action: {
questionVM.submitQuestion(questionId: question)
})
}.onChange(of: selected) { value in
if let safeValue = value {
questionVM.answerQuestion(question: question, answer: String(safeValue))
}
}
}
}
struct InfoQView: View {
@EnvironmentObject() var questionVM: QuestionViewModel
let question: MinimalQuestion
var body: some View {
VStack {
Text(question.text)
Button("OK", action: {
questionVM.answerQuestion(question: question, answer: "OK")
questionVM.submitQuestion(questionId: question)
})
}
}
}
struct BoolQView: View {
@EnvironmentObject() var questionVM: QuestionViewModel
let question: MinimalQuestion
@State var isToggled = false
var body: some View {
VStack {
Toggle(question.text, isOn: self.$isToggled)
Button("OK", action: {
questionVM.answerQuestion(question: question, answer: "\(isToggled)")
questionVM.submitQuestion(questionId: question)
})
}
}
}
這似乎是 iOS 16.x 中的一個錯誤,同時使用帶有“輪式”的選擇器,我在我的應用程序中遇到了同樣的問題並使用了以下解決方法:
extension Picker {
@ViewBuilder
func pickerViewModifier() -> some View {
if #available(iOS 16.0, *) {
self
} else {
self.pickerStyle(.wheel)
}
}
}
struct SomeView: View {
var body: some View {
Picker()
.pickerViewModifier()
}
}
這個崩潰在 iOS 16.2 上依然存在。 它似乎只發生在您在Picker
視圖中使用ForEach
時。 因此,當您在Picker
內容中手動提供每個選擇器選項的Text
視圖而不是使用ForEach
創建Text
選擇器選項時,崩潰就會消失。
當然,在許多情況下,對選擇器選項進行硬編碼並不是一個可行的解決方法。
但是您也可以通過將生成選擇器選項的ForEach
循環移動到另一個視圖來解決該問題。 為此,定義一個輔助視圖:
struct PickerContent<Data>: View where Data : RandomAccessCollection, Data.Element : Hashable {
let pickerValues: Data
var body: some View {
ForEach(pickerValues, id: \.self) {
let text = "\($0)"
Text(text)
}
}
}
然后在您的Picker
中使用PickerContent
而不是ForEach
,例如(基於您的示例):
Picker("Please choose a value", selection: $selected) {
PickerContent(pickerValues: colors)
}.pickerStyle(.wheel)
我在 iOS 16.0 上發現了同樣的問題,為了獲得完全相同的解決方案,沒有任何效果,最后我不得不在其中使用 UIKit 的包裝器和PickerView()
。 它也只發生在wheel
樣式上,我想default
對我來說很好用。 這是在 iOS 16 中獲得完全相同的wheel
拾取器的工作代碼。
struct CustomUIPicker: UIViewRepresentable {
@Binding var items: [String]
@Binding var selectedIndex: Int
func makeCoordinator() -> CustomPickerCoordinator {
CustomPickerCoordinator(items: $items, selectedIndex: $selectedIndex)
}
func makeUIView(context: Context) -> UIPickerView {
let pickerView = UIPickerView()
pickerView.delegate = context.coordinator
pickerView.dataSource = context.coordinator
pickerView.selectRow(selectedIndex, inComponent: 0, animated: true)
return pickerView
}
func updateUIView(_ uiView: UIPickerView, context: Context) {
}
}
extension CustomUIPicker {
class CustomPickerCoordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
@Binding private var items: [String]
@Binding var selectedIndex: Int
init(items: Binding<[String]>, selectedIndex: Binding<Int>) {
_items = items
_selectedIndex = selectedIndex
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
items.count
}
func pickerView( _ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return items[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedIndex = row
}
}
}
這里的items
是您要在wheel
選擇器中顯示的數據列表,而selectedIndex
是您picker view
的當前選定索引。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.