在 SwiftUI 列表中按 Date() 分组 CoreData 作为部分

Grouping CoreData by Date() in SwiftUI List as sections

我的目标:

我希望能够按截止日期范围对 CoreData Todo 项目进行分组。 (“今天”、“明天”、“接下来的 7 天”、未来)

我尝试的...

我尝试使用 @SectionedFetchRequest,但 sectionIdentifier 需要一个字符串。如果它作为 Date() 存储在 coreData 中,我该如何转换它以供使用?我收到了许多没有帮助的错误和建议。这也不能解决诸如“接下来 7 天”之类的日期范围。此外,我似乎甚至没有访问实体的到期日期,因为它指向我的 ViewModel 表单。

    @Environment(\.managedObjectContext) private var viewContext
    
    //Old way of fetching Todos without the section fetch
    //@FetchRequest(sortDescriptors: []) var todos: FetchedResults<Todo>
    
    @SectionedFetchRequest<String, Todo>(
        entity: Todo.entity(), sectionIdentifier: \Todo.dueDate,
        SortDescriptors: [SortDescriptor(\.Todo.dueDate, order: .forward)]
    ) var todos: SectionedFetchResults<String, Todo>
Cannot convert value of type 'KeyPath<Todo, Date?>' to expected argument type 'KeyPath<Todo, String>'

Value of type 'NSObject' has no member 'Todo'

询问

是否有其他解决方案比 @SectionedFetchRequest? 更适合我的情况,如果没有,我想了解如何适当地对数据进行分组。

您可以在 entity extension 中创建自己的 sectionIdentifier 并与 @SectionedFetchRequest

一起使用

return 变量只需要 return 你的范围有共同点就可以工作。

extension Todo{
    ///Return the string representation of the relative date for the supported range (year, month, and day)
    ///The ranges include today, tomorrow, overdue, within 7 days, and future
    @objc
    var dueDateRelative: String{
        var result = ""
        if self.dueDate != nil{
            //Order matters here so you can avoid overlapping
            if Calendar.current.isDateInToday(self.dueDate!){
                result = "today"//You can localize here if you support it
            }else if Calendar.current.isDateInTomorrow(self.dueDate!){
                result = "tomorrow"//You can localize here if you support it
            }else if Calendar.current.dateComponents([.day], from: Date(), to: self.dueDate!).day ?? 8 <= 0{
                result = "overdue"//You can localize here if you support it
            }else if Calendar.current.dateComponents([.day], from: Date(), to: self.dueDate!).day ?? 8 <= 7{
                result = "within 7 days"//You can localize here if you support it
            }else{
                result = "future"//You can localize here if you support it
            }
        }else{
            result =  "unknown"//You can localize here if you support it
        }
        return result
    }
}

然后像这样与您的 @SectionedFetchRequest 一起使用它

@SectionedFetchRequest(entity: Todo.entity(), sectionIdentifier: \.dueDateRelative, sortDescriptors: [NSSortDescriptor(keyPath: \Todo.dueDate, ascending: true)], predicate: nil, animation: Animation.linear)
var sections: SectionedFetchResults<String, Todo>

也看看this question

您也可以使用 Date,但您必须选择一个日期作为 header 部分。在这种情况下,您可以使用范围的上限日期,只是日期而不是时间,因为如果时间不匹配,时间可能会创建其他部分。

extension Todo{
    ///Return the upperboud date of the available range (year, month, and day)
    ///The ranges include today, tomorrow, overdue, within 7 days, and future
    @objc
    var upperBoundDueDate: Date{
        //The return value has to be identical for the sections to match
        //So instead of returning the available date you return a date with only year, month and day
        //We will comprare the result to today's components
        let todayComp = Calendar.current.dateComponents([.year,.month,.day], from: Date())
        var today = Calendar.current.date(from: todayComp) ?? Date()
        if self.dueDate != nil{
            //Use the methods available in calendar to identify the ranges
            //Today
            if Calendar.current.isDateInToday(self.dueDate!){
                //The result variable is already setup to today
                //result = result
            }else if Calendar.current.isDateInTomorrow(self.dueDate!){
                //Add one day to today
                today = Calendar.current.date(byAdding: .day, value: 1, to: today)!
            }else if Calendar.current.dateComponents([.day], from: today, to: self.dueDate!).day ?? 8 <= 0{
                //Reduce one day to today to return yesterday
                today = Calendar.current.date(byAdding: .day, value: -1, to: today)!
            }else if Calendar.current.dateComponents([.day], from: today, to: self.dueDate!).day ?? 8 <= 7{
                //Return the date in 7 days
                today = Calendar.current.date(byAdding: .day, value: 7, to: today)!
            }else{
                today = Date.distantFuture
            }
        }else{
            //This is something that needs to be handled. What do you want as the default if the date is nil
            today = Date.distantPast
        }
        return today
    }
}

然后请求将如下所示...

@SectionedFetchRequest(entity: Todo.entity(), sectionIdentifier: \.upperBoundDueDate, sortDescriptors: [NSSortDescriptor(keyPath: \Todo.dueDate, ascending: true)], predicate: nil, animation: Animation.linear)
var sections: SectionedFetchResults<Date, Todo>

根据您提供的信息,您可以通过将我提供的扩展粘贴到您项目中的 .swift 文件并将您的获取请求替换为您要使用的文件来测试此代码

它正在抛出错误,因为这是您告诉它要做的。 @SectionedFetchRequest 将段标识符和实体类型的元组发送到 SectionedFetchResults,因此您指定的 SectionedFetchResults 元组必须匹配。在你的情况下,你写道:

SectionedFetchResults<String, Todo>

但是你想做的是传递一个日期,所以它应该是:

SectionedFetchResults<Date, Todo>

lorem ipsum 让我想到了第二个,也是更重要的部分,即在扩展中使用计算变量来提供部分标识符。根据他的回答,你应该回到:

SectionedFetchResults<String, Todo>

请接受 lorem ipsum 的回答,但要意识到你也需要处理这个问题。

关于“今天”、“明天”、“接下来的 7 天”等部分

我的建议是使用 RelativeDateTimeFormatter 并让 Apple 完成大部分或全部工作。要创建用于分段的计算变量,您需要像这样在 Todo 上创建扩展:

extension Todo {
    
    @objc
    public var sections: String {
        // I used the base Xcode core data app which has timestamp as an optional.
        // You can remove the unwrapping if your dates are not optional.
        if let timestamp = timestamp {
            // This sets up the RelativeDateTimeFormatter
            let rdf = RelativeDateTimeFormatter()
            // This gives the verbose response that you are looking for.
            rdf.unitsStyle = .spellOut
            // This gives the relative time in names like today".
            rdf.dateTimeStyle = .named

            // If you are happy with Apple's choices. uncomment the line below
            // and remove everything else.
  //        return rdf.localizedString(for: timestamp, relativeTo: Date())
            
            // You could also intercept Apple's labels for you own
            switch rdf.localizedString(for: timestamp, relativeTo: Date()) {
            case "now":
                return "today"
            case "in two days", "in three days", "in four days", "in five days", "in six days", "in seven days":
                return "this week"
            default:
                return rdf.localizedString(for: timestamp, relativeTo: Date())
            }
        }
        // This is only necessary with an optional date.
        return "undated"
    }
}

您必须将变量标记为 @objc,否则 Core Data 将导致崩溃。我认为 Core Data 将是 Obj C 存在的最后一个地方,但我们可以很容易地像这样将 Swift 代码与它接口。

回到您的视图,您的 @SectionedFetchRequest 看起来像这样:

@SectionedFetchRequest(
    sectionIdentifier: \.sections,
    sortDescriptors: [NSSortDescriptor(keyPath: \Todo.timestamp, ascending: true)],
    animation: .default)
private var todos: SectionedFetchResults<String, Todo>

那么您的列表如下所示:

 List {
      ForEach(todos) { section in
          Section(header: Text(section.id.capitalized)) {
               ForEach(section) { todo in
               ...
               }
          }
      }
  }