Fix wheel picker crash caused by UIKit/SwiftUI race condition
The crash occurred when UIKit's didSelectRow callback fired during SwiftUI view teardown, causing an array index out of bounds error. Fixes: - Use Identifiable struct for stable ForEach identity - Hide picker before dismissing to prevent race condition - Add .id() modifier for stable picker identity - Disable interactive dismiss to prevent mid-scroll dismissal - Add small delay before dismiss callbacks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -552,8 +552,7 @@ struct NotificationTimePickerRow: View {
|
||||
},
|
||||
onCancel: {
|
||||
showingTimePicker = false
|
||||
},
|
||||
formatHour: formatHour
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -563,18 +562,37 @@ struct NotificationTimePickerRow: View {
|
||||
|
||||
struct TimePickerSheet: View {
|
||||
@State private var selectedHour: Int
|
||||
@State private var isPresented: Bool = true
|
||||
let onSave: (Int) -> Void
|
||||
let onCancel: () -> Void
|
||||
let formatHour: (Int) -> String
|
||||
|
||||
// Pre-computed hours array to avoid range issues with wheel picker
|
||||
private let hours: [Int] = Array(0..<24)
|
||||
// Pre-computed hour labels as a simple struct for stable identity
|
||||
private struct HourOption: Identifiable {
|
||||
let id: Int
|
||||
let label: String
|
||||
|
||||
init(selectedHour: Int, onSave: @escaping (Int) -> Void, onCancel: @escaping () -> Void, formatHour: @escaping (Int) -> String) {
|
||||
var hour: Int { id }
|
||||
}
|
||||
|
||||
private static let hourOptions: [HourOption] = (0..<24).map { hour in
|
||||
let label: String
|
||||
switch hour {
|
||||
case 0: label = "12:00 AM"
|
||||
case 1...11: label = "\(hour):00 AM"
|
||||
case 12: label = "12:00 PM"
|
||||
default: label = "\(hour - 12):00 PM"
|
||||
}
|
||||
return HourOption(id: hour, label: label)
|
||||
}
|
||||
|
||||
init(selectedHour: Int, onSave: @escaping (Int) -> Void, onCancel: @escaping () -> Void) {
|
||||
_selectedHour = State(initialValue: selectedHour)
|
||||
self.onSave = onSave
|
||||
self.onCancel = onCancel
|
||||
self.formatHour = formatHour
|
||||
}
|
||||
|
||||
private func formatHour(_ hour: Int) -> String {
|
||||
Self.hourOptions.first { $0.hour == hour }?.label ?? "\(hour):00"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -585,14 +603,17 @@ struct TimePickerSheet: View {
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
.padding(.top)
|
||||
|
||||
Picker("Hour", selection: $selectedHour) {
|
||||
ForEach(hours, id: \.self) { hour in
|
||||
Text(formatHour(hour))
|
||||
.tag(hour)
|
||||
if isPresented {
|
||||
Picker("Hour", selection: $selectedHour) {
|
||||
ForEach(Self.hourOptions) { option in
|
||||
Text(option.label)
|
||||
.tag(option.hour)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
.frame(height: 150)
|
||||
.id("hourPicker") // Stable identity to prevent view recycling issues
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
.frame(height: 150)
|
||||
|
||||
Text("Notifications will be sent at \(formatHour(selectedHour)) in your local timezone")
|
||||
.font(.caption)
|
||||
@@ -609,13 +630,21 @@ struct TimePickerSheet: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
onCancel()
|
||||
// Hide picker before dismissing to prevent race condition
|
||||
isPresented = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
onSave(selectedHour)
|
||||
// Hide picker before dismissing to prevent race condition
|
||||
isPresented = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
onSave(selectedHour)
|
||||
}
|
||||
}
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.fontWeight(.semibold)
|
||||
@@ -623,6 +652,7 @@ struct TimePickerSheet: View {
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
.interactiveDismissDisabled() // Prevent swipe-to-dismiss which can cause race condition
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user