SwiftUI中.onDrag/.onDrop修饰符未触发问题求助
问题描述
尝试在同一个List内拖拽Meal项调整用餐计划的时间,但.onDrop修饰符始终未被触发。已经尝试过.onMove、多种UTType,甚至将ForEach简化为仅显示小时的Text视图,问题依然存在。
拖拽时的动画截图:
相关代码如下:
import SwiftUI import UniformTypeIdentifiers struct CalendarDayListView2: View { @State var viewModel: CalendarDayListViewModel2 @Binding var date: Date @Binding var meals: [Meal] @Binding var isLoading: Bool @State private var scrollToMeal: Meal? @Environment(\.sizeCategory) var sizeCategory @State private var timeZone: TimeZone = TimeZone.current var plusButtonAction: ((Date) -> Void)? var xButtonAction: ((Meal) -> Void)? var menuButtonAction: ((Meal) -> Void)? var body: some View { ScrollViewReader { proxy in List { ForEach(hoursOfDay, id: \.self) { hour in if let meal = mealForHour(hour) { CalendarListItemView( meal: meal, date: hour, mode: viewModel.mode, plusButtonAction: { addMealDate in self.plusButtonAction?(addMealDate) }, xButtonAction: { meal in self.xButtonAction?(meal) }, menuButtonAction: { meal in self.menuButtonAction?(meal) } ) .id(meal.id) .swipeActions(edge: .trailing) { Button(role: .destructive) { deleteMeal(meal) } label: { Label("Delete", systemImage: "trash") } } .onDrag { NSItemProvider(object: meal.id as NSString) } .onDrop(of: [UTType.text.identifier], delegate: CalendarDropDelegate(meals: $meals, date: hour)) } else { CalendarListItemView( meal: nil, date: hour, mode: viewModel.mode, plusButtonAction: { addMealDate in self.plusButtonAction?(addMealDate) }, xButtonAction: { meal in self.xButtonAction?(meal) }, menuButtonAction: { meal in self.menuButtonAction?(meal) } ) .onDrop(of: [UTType.plainText.identifier], delegate: CalendarDropDelegate(meals: $meals, date: hour)) } } .listRowSeparator(.hidden) if sizeCategory == .accessibilityExtraExtraExtraLarge { Color.clear.frame(height: 200) } else { Color.clear.frame(height: 450) } } .frame(width: UIScreen.main.bounds.width, height: 700) .listStyle(PlainListStyle()) .onChange(of: self.meals) { scrollToMeal = self.meals.first } .onChange(of: scrollToMeal) { if let meal = scrollToMeal { withAnimation { proxy.scrollTo(meal.id, anchor: .top) } } } .onChange(of: self.date) { oldValue, newValue in scrollToMeal = mealsForSelectedDate.first } .onChange(of: self.isLoading) { oldValue, newValue in if !newValue { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { scrollToMeal = mealsForSelectedDate.first } } } } } private func deleteMeal(_ meal: Meal) { if let index = meals.firstIndex(where: { $0.id == meal.id }) { meals.remove(at: index) } } private func dateForHour(_ hour: Int) -> Date { let calendar = Calendar.current let startOfDay = calendar.startOfDay(for: date) return calendar.date(byAdding: .hour, value: hour, to: startOfDay)! } private var hoursOfDay: [Date] { let calendar = Calendar.current var calendarWithTimeZone = calendar calendarWithTimeZone.timeZone = timeZone let startOfDay = calendarWithTimeZone.startOfDay(for: date) return (0..<24).map { hour in calendarWithTimeZone.date(byAdding: .hour, value: hour, to: startOfDay)! } } private var mealsForSelectedDate: [Meal] { self.meals.filter { meal in guard let plannedAt = meal.plannedAt else { return false } return Calendar.current.isDate(plannedAt, inSameDayAs: date) } } private func mealForHour(_ hour: Date) -> Meal? { mealsForSelectedDate.first { meal in guard var plannedAt = meal.plannedAt else { return false } let calendar = Calendar.current var calendarWithTimeZone = calendar calendarWithTimeZone.timeZone = timeZone plannedAt = plannedAt.convertToTimeZone(timeZone) return calendarWithTimeZone.isDate(plannedAt, equalTo: hour, toGranularity: .hour) } } } struct CalendarDropDelegate: DropDelegate { @Binding var meals: [Meal] let date: Date func performDrop(info: DropInfo) -> Bool { guard let itemProvider = info.itemProviders(for: [.text]).first else { return false } itemProvider.loadObject(ofClass: NSString.self) { (id, error) in if let id = id as? String, let index = meals.firstIndex(where: { $0.id == id }) { DispatchQueue.main.async { var updatedMeal = meals[index] print("performDrop: UpdatedMeal date: \(date)") updatedMeal.plannedAt = date meals[index] = updatedMeal } } } return true } } struct CalendarListItemView: View { var meal: Meal? var date: Date var mode: CalendarViewMode @State var isSelected: Bool = false var plusButtonAction: ((Date) -> Void)? var xButtonAction: ((Meal) -> Void)? var menuButtonAction: ((Meal) -> Void)? var foodImageNotDisplayed = false var hourString: String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "hh" return formatter.string(from: self.date) } var meridianString: String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "a" return formatter.string(from: self.date) } var body: some View { HStack { ZStack { RoundedRectangle(cornerRadius: 12.0) .foregroundStyle(.black.opacity(0.03)) .frame(width: 75, height: self.meal == nil ? 33.0 : 75.0 ) HStack { Text(hourString) .font(Font.custom("Open Sans", size: 16)) .foregroundColor(.black) Text(meridianString) .font(Font.custom("Open Sans", size: 16)) .foregroundColor(.black.opacity(0.5)) } } .padding(.leading, 10) if meal != nil { HStack { foodImage() VStack { if let meal = meal { Text(meal.title) .font(Font.custom("Open Sans", size: 15)) .foregroundColor(.black) } HStack { if let calories = meal?.calories { Text("\(calories.formatDouble()) cals") .font(Font.custom("Open Sans", size: 13)) .foregroundColor(Color(red: 0.54, green: 0.72, blue: 0.13)) } if let readyInMinutes = meal?.readyInMinutes { Text("• \(readyInMinutes) minutes") .font(Font.custom("Open Sans", size: 13)) .foregroundColor(Color(red: 0.54, green: 0.72, blue: 0.13)) } } } } } Spacer() if let meal = meal { Button(action: { if self.mode == .mealSharing { self.menuButtonAction?(meal) } else { xButtonAction?(meal) } }, label: { if self.mode == .mealSharing { Image("menu_button") } else { Image("x_button") } }) .padding(.trailing, 10) } else { Button(action: { self.isSelected.toggle() if self.mode == .timeSlot { self.plusButtonAction?(date) } }, label: { if self.mode == .timeSlot && self.isSelected { Image("green_checkmark") } else if self.mode == .mealSharing { EmptyView() } else { Image("plus_button") } }) .padding(.trailing, 10) } } .frame(height: self.meal == nil ? 50.0 : 75.0) } @ViewBuilder func foodImage() -> some View { if let imageUrl = meal?.image, let url = URL(string: imageUrl) { AsyncImage(url: url) { phase in if let image = phase.image { image .resizable() .aspectRatio(contentMode: .fill) .frame(width: 75, height: 75) .cornerRadius(10) } else if phase.error != nil { Image("placeholder_food_image") .resizable() .scaledToFill() .frame(width: 75, height: 75) .cornerRadius(10) .clipped() } else { Image("placeholder_food_image") .resizable() .scaledToFill() .frame(width: 75, height: 75) .cornerRadius(10) .clipped() } } .frame(width: 75, height: 75) } else { Image("placeholder_food_image") .resizable() .scaledToFill() .frame(width: 75, height: 75) .cornerRadius(10) .clipped() } } }
问题排查与修复方案
以下是几个关键问题点及对应的修复步骤:
1. UTType 不匹配
- 问题:
onDrag传递的是NSString(对应UTType.text),但空时间槽的onDrop使用UTType.plainText,类型不匹配导致拖拽无法被识别。 - 修复:统一所有
onDrop的UTType为[UTType.text.identifier],与onDrag的类型保持一致。
2. DropDelegate 缺少必要方法
- 问题:自定义的
CalendarDropDelegate仅实现了performDrop,但SwiftUI需要通过canPerformDrop确认是否接受当前拖拽项,否则不会触发后续操作。 - 修复:在
CalendarDropDelegate中添加以下方法:
func canPerformDrop(info: DropInfo) -> Bool { return info.hasItemsConforming(to: [.text]) }
3. List 行ID缺失
- 问题:空时间槽的行未设置唯一ID,SwiftUI无法正确跟踪行位置,影响拖拽识别逻辑。
- 修复:给空行添加基于
hour的ID:
CalendarListItemView(...) .id(hour) .onDrop(...)
4. 拖拽数据类型优化(可选)
- 问题:直接传递
NSString存在类型转换风险,可明确数据格式提升稳定性。 - 修复:在
onDrag中明确注册数据表示:
.onDrag { let itemProvider = NSItemProvider(object: meal.id as NSString) itemProvider.registerDataRepresentation(forTypeIdentifier: UTType.text.identifier, visibility: .all) { completion in completion(meal.id.data(using: .utf8), nil) return nil } return itemProvider }
修复后的关键代码片段
修改后的List行部分
ForEach(hoursOfDay, id: \.self) { hour in if let meal = mealForHour(hour) { CalendarListItemView(...) .id(meal.id) .swipeActions(...) .onDrag { NSItemProvider(object: meal.id as NSString) } .onDrop(of: [UTType.text.identifier], delegate: CalendarDropDelegate(meals: $meals, date: hour)) } else { CalendarListItemView(...) .id(hour) .onDrop(of: [UTType.text.identifier], delegate: CalendarDropDelegate(meals: $meals, date: hour)) } }
修改后的CalendarDropDelegate
struct CalendarDropDelegate: DropDelegate { @Binding var meals: [Meal] let date: Date func canPerformDrop(info: DropInfo) -> Bool { return info.hasItemsConforming(to: [.text]) } func performDrop(info: DropInfo) -> Bool { guard let itemProvider = info.itemProviders(for: [.text]).first else { return false } itemProvider.loadObject(ofClass: NSString.self) { (id, error) in if let id = id as? String, let index = meals.firstIndex(where: { $0.id == id }) { DispatchQueue.main.async { var updatedMeal = meals[index] updatedMeal.plannedAt = date meals[index] = updatedMeal } } } return true } }
内容的提问来源于stack exchange,提问作者kc.
相关产品推荐
相关产品推荐

