按日期的月份对 NSSortDescriptor 进行排序。 Swift

Sort NSSortDescriptor by Month of date. Swift

我正在显示生日列表。 我想以从一月开始到十二月结束的方式对它们进行排序,第二个 NSSortDescriptor 是天。我想显示一个列表,其中最接近的生日排在最前面。

目前按完整的生日日期排序。所以目前 "February 6 2009" 显示在 "February 8 1997" 之后,我不希望它显示在想知道下一个生日什么时候到来时年份是无关紧要的。

我的 FetchRequest 是什么样的:

    @FetchRequest(entity: Birthday.entity(), sortDescriptors: [
    NSSortDescriptor(keyPath: \Birthday.date, ascending: true),

]) var birthdays: FetchedResults<Birthday>

我的列表是什么样的:

                List {
                ForEach(birthdays, id: \.self) { birthday in
                    ZStack {
                        NavigationLink(destination: DetailView(birthday: birthday)) {
                            EmptyView()

                        }.hidden()

                        BirthdayView(name: birthday.name ?? "Unknown Name", birthDate: birthday.date ?? Date())

                    }
                }
                .onDelete(perform: deleteBirthdays)
            }

我正在使用核心数据,这就是我的实体的样子:

使用最新的 XCODE 和 Swift。

提前致谢!

我试过:

extension Date {
var isInToday: Bool { Calendar.current.isDateInToday(self) }
var nextBirthday: Date {
    isInToday ? self : Calendar.current.nextDate(after: Date(), matching: Calendar.current.dateComponents([.month, .day, .hour, .minute], from: self), matchingPolicy: .nextTime)!
}

}

extension Birthday {
var nextBirthday: Date { date!.nextBirthday }

}

@FetchRequest(entity: Birthday.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Birthday.nextBirthday, ascending: true)
]) var birthdays: FetchedResults<Birthday>

但是当 运行 它抛出的应用程序显示以下错误:

"Thread 1: Fatal error: Could not extract a String from KeyPath Swift.KeyPath"

如果你有(或将要有)大数据库,那么在数据模型中设置专用属性(例如 birthMonth: String,有类似 "MM/DD" 的东西)并制作 CoreData 排序引擎是合适的按 Birthday.birthMonth 关键路径为您排序。

否则,如果数据库较小,您可以在获取数据后即时执行其他操作,例如

ForEach(birthdays.sorted { [=10=].month < .month }, id: \.self) { birthday in

通过相应的 .month 扩展实现以满足您的需求。

一个可能的解决方案是 NSDate 的这个扩展(NSDate 是至关重要的,因为核心数据广泛地基于 NSObject-KVO)。

计算下一次出现的日期而不考虑年份

extension NSDate {

    dynamic var nextOccurrence : NSDate {
        let calendar = Calendar.current
        let components = calendar.dateComponents([.month, .day], from: self as Date)
        return calendar.nextDate(after: Date(), matching: components, matchingPolicy: .nextTime)! as NSDate
    }
}

对应的排序描述符是

NSSortDescriptor(key: "date.nextOccurrence", ascending: true)

您可以创建自定义排序以按即将到来的生日对日期进行排序,并将已排序的集合传递给您的 ForEach 方法:

extension Collection where Element == Birthday {
    func sortedByIncomingBirthday() -> [Element] {
        sorted {
            // lhs date is today or is in the future
            ([=10=].date.month, [=10=].date.day) >= (Date().month, Date().day) ?
                // rhs date is today or is in the future
                (.date.month, .date.day) >= (Date().month, Date().day) ?
                    // sort them by a month day order
                    ([=10=].date.month, [=10=].date.day) < (.date.month, .date.day) :
                    // otherwise is in increasing order
                    true :
            // lhs date is in the past
            // sort them by a month day order
            ([=10=].date.month, [=10=].date.day) < (.date.month, .date.day)
        }
    }
}

ForEach(birthdays.sortedByIncomingBirthday(), id: \.self) { birthday in

另一种选择是扩展 Birthday 并添加 nextBirthday 属性 这样您就可以使用它作为 NSSortDescriptor 的 keyPath 对其进行排序:

extension Date {
    var isInToday: Bool { Calendar.current.isDateInToday(self) }
    var nextBirthday: Date {
        isInToday ? self : Calendar.current.nextDate(after: Date(), matching: Calendar.current.dateComponents([.month, .day, .hour, .minute], from: self), matchingPolicy: .nextTime)!
    }
}

extension Birthday {
    var nextBirthday: Date { date.nextBirthday }
}

NSSortDescriptor(keyPath: \Birthday.nextBirthday, ascending: true)

edit/update:

如果生日日期 属性 是可选的,请不要强制展开它的值:

extension Birthday {
    var nextBirthday: Date { date?.nextBirthday ?? .distantFuture }
}

第一种方法也需要处理它:

extension Collection where Element == Birthday {
    func sortedByIncomingBirthday() -> [Element] {
        sorted { lhs, rhs in
            if lhs.date == nil { return false }
            let lhs = lhs.date ?? .distantFuture
            let rhs = rhs.date ?? .distantFuture
            // lhs date is today or is in the future
            return (lhs.month, lhs.day) >= (Date().month, Date().day) ?
                       // rhs date is today or is in the future
                       (rhs.month, rhs.day) >= (Date().month, Date().day) ?
                           // sort them by a month day order
                           (lhs.month, lhs.day) < (rhs.month, rhs.day) :
                           // otherwise is in increasing order
                           true :
                   // lhs date is in the past
                   // sort them by a month day order
                   (lhs.month, lhs.day) < (rhs.month, rhs.day)
        }
    }
}