在 SwiftUI 中按 Date() 对 CoreData 进行分组 列表为部分

Grouping CoreData by Date() in SwiftUI List as sections

提问人:Justin Comstock 提问时间:12/17/2021 更新时间:1/11/2023 访问量:2510

问:

我的目标:

我希望能够按 dueDate 范围对 CoreData Todo 项进行分组。(“今天”、“明天”、“未来 7 天”、“未来”)

我尝试了什么......

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

    @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?

iOS Swift iPhone Core-数据 SwiftUI

评论


答:

8赞 lorem ipsum 12/17/2021 #1

您可以在sectionIdentifierentityextension@SectionedFetchRequest

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>

也看看这个问题

您也可以使用,但必须选择一个日期作为部分标题。在此方案中,可以使用范围的 upperBound 日期,仅使用日期而不是时间,因为如果时间不匹配,则时间可能会创建其他部分。Date

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

3赞 Yrb 12/17/2021 #2

它抛出错误,因为这是你告诉它要做的。 将部分标识符和实体类型的元组发送到 ,因此您指定的元组必须匹配。就你而言,你写道:@SectionedFetchRequestSectionedFetchResultsSectionedFetchResults

SectionedFetchResults<String, Todo>

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

SectionedFetchResults<Date, Todo>

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

SectionedFetchResults<String, Todo>

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

按“今天”、“明天”、“未来 7 天”等进行分段。

我的建议是使用 Apple 并让 Apple 完成大部分或全部工作。要创建一个计算变量来分割,你需要创建一个扩展,如下所示:RelativeDateTimeFormatterTodo

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"
    }
}

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

回到您的视图中,您的外观如下所示:@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
               ...
               }
          }
      }
  }

评论

0赞 Justin Comstock 12/17/2021
Lorem ipsum?我只在这篇文章中看到你的答案。 谢谢你提供答案。我今晚或明天会尝试这个。
0赞 Yrb 12/17/2021
他有一个帖子,处理了处理分段提取请求中各部分的困难部分。我不确定他为什么删除它,但我将不得不重新创建我今天下午正在处理的代码。我会更新这个答案。
0赞 Justin Comstock 12/17/2021
我记得看到过那个代码,我有什么可以检索已删除的答案吗?这对各部分很有帮助。
0赞 Yrb 12/17/2021
只有他能。我更新了关于自定义切片的答案。
0赞 Yrb 12/17/2021
我很抱歉。我只是想确保 OP 的所有问题都得到回答。
0赞 Marvel Alvarez 1/11/2023 #3

您可以使用此方法来实现: 喜欢这个:

func formattedDate () -> String? {
    let RFC3339DateFormatter = DateFormatter()
    RFC3339DateFormatter.locale = Locale(identifier: "en_US_POSIX")
    RFC3339DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
    RFC3339DateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
    let date1 = RFC3339DateFormatter.date(from: date.formatted()) ?? Date()
    
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .medium
    dateFormatter.timeStyle = .none
     
    // ES Spanish Locale (es_ES)
    dateFormatter.locale = Locale.current//Locale(identifier: "es_ES")
    return dateFormatter.string(from: date1) // Jan 2, 2001
}