You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 23:02:03