当带有选取器的SwiftUI视图消失时,应用程序崩溃(自iOS 16起)

h43kikqp  于 2022-10-31  发布在  Swift
关注(0)|答案(2)|浏览(383)

我们有一个带有“聊天”功能的应用程序,用户可以通过一些预定义的选项来回答问题:对于每个问题,都将显示一个新视图。其中一个选项是带有选取器的视图,自iOS 16以来,当带有选取器的视图消失并出现以下错误时,此选取器会导致应用程序崩溃:Thread 1: Fatal error: Index out of range位于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)
            })
        }
    }
}

使用上面视图的父视图(注意:即使删除了所有动画相关的行,崩溃仍然发生):

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)
            })
        }
    }
}
kqqjbcuj

kqqjbcuj1#

这似乎是iOS 16. x中的一个bug,而使用带有“车轮风格”的Picker时,我在我的应用程序中也遇到了同样的问题,并使用了以下解决方法:

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()
     }
 }
bbmckpt7

bbmckpt72#

我在iOS 16.0中也发现了同样的问题,为了得到完全相同的解决方案,没有任何效果,最后我不得不使用UIKit的PickerView() Package 器。而且,它只发生在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选取器中显示的数据列表,selectedIndexpicker view的当前选定索引。

相关问题