簡體   English   中英

當帶有 Picker 的 SwiftUI 視圖消失時應用程序崩潰(自 iOS 16 起)

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM