以下是对于这个问题应该重要的两个视图。 我只是想将一份餐食从列表中的一个位置拖放到另一个位置(同一列表)。 这个想法是让用户调整其膳食计划中特定膳食的时间。 我觉得我已经尝试了几乎所有的方法,但是 .onDrop 修饰符没有被调用。
对此的任何帮助将不胜感激!我很想知道是什么阻止了它的工作。
这是我开始拖动时动画的样子:
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
// Handle plus button action
self.plusButtonAction?(addMealDate)
},
xButtonAction: { meal in
// Handle x button action
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
// Handle plus button action
self.plusButtonAction?(addMealDate)
},
xButtonAction: { meal in
// Handle x button action
self.xButtonAction?(meal)
}, menuButtonAction: { meal in
self.menuButtonAction?(meal)
}
)
.onDrop(of: [UTType.plainText.identifier], delegate: CalendarDropDelegate(meals: $meals, date: hour))
}
}
.listRowSeparator(.hidden)
// Add extra padding for iPhone SE
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
// Meal Sharing mode
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
// Convert plannedAt to the current 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
}
}
// CalendarListItemView.swift
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()
// Set the locale to ensure AM/PM works correctly in all locales
formatter.locale = Locale(identifier: "en_US_POSIX")
// Set the desired format
formatter.dateFormat = "hh"
// Return the formatted date string
return formatter.string(from: self.date)
}
var meridianString: String {
let formatter = DateFormatter()
// Set the locale to ensure AM/PM works correctly in all locales
formatter.locale = Locale(identifier: "en_US_POSIX")
// Set the desired format
formatter.dateFormat = "a"
// Return the formatted date string
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)
}
// No meal for time slot
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()
}
}
}
我尝试了 .onMove 修饰符,但效果不佳。 我为 .onDrop 尝试了多种不同的 UTType,但没有成功。 疯狂的是,我什至测试了让 ForEach 简单地迭代并显示一小时的文本视图,但它仍然不让我放弃它们。
您不必使用
onDrag
/onDrop
来重新排序 List
中的商品。
以下是如何实现这一目标的简化示例:
List(selection: $selected) {
ForEach($items, id: \.id) { item in
ItemView(item)
.tag(item.id.wrappedValue)
}
.onMove { indices, newOffset in
items.move(fromOffsets: indices, toOffset: newOffset)
}
}