简体   繁体   English

CloudKit查询中的UITableView部分

[英]UITableView Sections from CloudKit Query

I have a simple CloudKit record that has two fields, Name and Grade. 我有一个简单的CloudKit记录,它有两个字段,名称和等级。 I would like to be able to do a query to CloudKit returning all of the records but grouped into sections by Grade. 我希望能够对CloudKit进行查询,返回所有记录,但按等级分组。 I know I can do this with NSFetchResultsController but can't seem to find an easy way to do this with CKQuery. 我知道我可以使用NSFetchResultsController执行此操作,但似乎无法使用CKQuery找到一种简单的方法。

Current code for fetching: 获取的当前代码:

    func fetchTeachers(_ completion: @escaping (_ teachers: [CKRecord]?, _ error: NSError?) -> () ) {

    let query = CKQuery(recordType: TeacherType, predicate: NSPredicate(value: true))
    query.sortDescriptors = [NSSortDescriptor(key:"Grade",ascending:true)]

    publicDB.perform(query, inZoneWith: nil) { results, error in
        completion(results, error as NSError?)
    }
}

To split an array of retrieved CKRecords into sections for display in a UITableView, you can use the helper class below. 要将检索到的CKRecords数组拆分为多个部分以便在UITableView中显示,可以使用下面的帮助程序类。

(A CKQuery itself does not provide the ability to do this sectioning - it just enables you to retrieve an array of records, optionally sorted.) (CKQuery本身不提供执行此分区的功能 - 它只允许您检索记录数组,可选择排序。)


Using the SectionedCKRecords class: 使用SectionedCKRecords类:

First, fetch the desired records from CloudKit using a CKQuery. 首先,使用CKQuery从CloudKit获取所需的记录。 (Your example code already does this.) This will provide you with an array of CKRecords. (您的示例代码已经执行此操作。)这将为您提供一系列CKRecords。

Let's assume that those records (per your example code) contain a " Grade " key that stores a String value, and that you want to split the records into sections based on the " Grade ". 假设这些记录(根据您的示例代码)包含存储String值的“ Grade ”键,并且您希望根据“ Grade ”将记录拆分为多个部分。

Simply: 只是:

1.) Initialize a SectionedCKRecords with the array of CKRecords, and the desired sectionNameKey : 1.)初始化与CKRecords的阵列,且所期望的一个sectionNameKey SectionedCKRecords:

let sectionedRecords = SectionedCKRecords(records: records, sectionNameKey: "Grade")

2.) Implement your UITableViewDataSource to call the appropriate methods on sectionedRecords : 2.)实现您的UITableViewDataSource以调用sectionedRecords上的相应方法:

SectionedCKRecords exposes methods similar to those of NSFetchedResultsController . SectionedCKRecords暴露出类似的NSFetchedResultsController的方法。

class YourDataSource: UITableViewDataSource {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let record = sectionedRecords.record(at: indexPath)
        // TODO: construct a UITableViewCell based on the record
        // ...
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return sectionedRecords.sections.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sectionedRecords.sections[section].numberOfRecords
    }

    func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        return sectionedRecords.sectionIndexTitles
    }

    // etc...

}

Customizing sectionIndexTitle Behavior: 自定义sectionIndexTitle行为:

If you'd like to customize how the sectionIndexTitles are generated, you can pass a sectionIndexTitleForSectionName closure to the SectionedCKRecords initializer. 如果您想自定义sectionIndexTitles是如何产生的,你可以通过一个sectionIndexTitleForSectionName封闭到SectionedCKRecords初始化。

By default, SectionedCKRecords matches the behavior of NSFetchedResultsController for generating sectionIndexTitles , using the capitalized first letter of the section name. 默认情况下,SectionedCKRecords NSFetchedResultsController的产生行为相匹配sectionIndexTitles ,使用一节名字的大写首字母。

The closure takes a String (the sectionName) as input, and returns the sectionIndexTitle. 闭包采用String(sectionName)作为输入,并返回sectionIndexTitle。

Some example closures are provided in the SectionIndexTitleForSectionName struct. SectionIndexTitleForSectionName结构中提供了一些示例闭包。

Example: 例:

let sectionedRecords = SectionedCKRecords(records: records, sectionNameKey: "Grade", sectionIndexTitleForSectionName: SectionIndexTitleForSectionName.firstLetterOfString)

SectionedCKRecords.swift: (Swift 3) SectionedCKRecords.swift :( 斯威夫特3)

// SectionedCKRecords.swift (Swift 3)
// © 2016 @breakingobstacles (http://stackoverflow.com/users/57856/breakingobstacles)
// Source: http://stackoverflow.com/a/39737583/57856
//
// License: The MIT License (https://opensource.org/licenses/MIT)
//    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import UIKit
import CloudKit

// MARK: - SectionedCKRecords
class SectionedCKRecords {

    private let sectionNameToSection: [String: Int]
    private let sectionIndex: [String]
    private let sectionIndexTitleToFirstSection: [String: Int]

    init(records: [CKRecord], sectionNameKey: String, sectionIndexTitleForSectionName: (String) -> String? = SectionIndexTitleForSectionName.firstLetterOfString) {

        self.records = records
        self.sectionNameKey = sectionNameKey

        // split records into sections
        let splitResults = split(records: records, bySectionNameKey: sectionNameKey)
        self.sections = splitResults.sections
        self.sectionNameToSection = splitResults.sectionNameToSection

        // build section index
        var sectionIndex: [String] = []
        var sectionIndexTitleToFirstSection: [String: Int] = [:]
        for (index, section) in splitResults.sections.enumerated() {
            guard let sectionIndexTitle = sectionIndexTitleForSectionName(section.name) else {
                continue
            }
            section.indexTitle = sectionIndexTitle
            if sectionIndexTitleToFirstSection.index(forKey: sectionIndexTitle) == nil {
                sectionIndex.append(sectionIndexTitle)
                sectionIndexTitleToFirstSection[sectionIndexTitle] = index
            }
        }

        self.sectionIndex = sectionIndex
        self.sectionIndexTitleToFirstSection = sectionIndexTitleToFirstSection
    }

    /// MARK: - Configuring Information

    // The input array of records.
    let records: [CKRecord]

    // The key on the CKRecords used to determine the section they belong to. Assumes that record[sectionNameKey] returns a String value.
    let sectionNameKey: String

    /// MARK: - Accessing Results

    // Returns the record at the given index path in the sectioned records.
    func record(at indexPath: IndexPath) -> CKRecord {
        return sections[indexPath.section].records[indexPath.row]
    }

    /// MARK: - Querying Section Information

    // The sections for the fetch results.
    private(set) var sections: [SectionInfo]

    // Returns the section number for a given section title and index in the section index.
    func section(forSectionIndexTitle sectionIndexTitle: String, at: Int) -> Int {
        return sectionIndexTitleToFirstSection[sectionIndexTitle] ?? -1
    }

    // The array of section index titles.
    var sectionIndexTitles: [String] {
        get {
            return sectionIndex
        }
    }
}

class SectionInfo: CustomStringConvertible {
    var numberOfRecords: Int { return records.count }
    let name: String
    fileprivate(set) var indexTitle: String?
    private(set) var records: [CKRecord]

    init(name: String, indexTitle: String? = nil, records: [CKRecord] = []) {
        self.name = name
        self.indexTitle = indexTitle
        self.records = records
    }

    fileprivate func add(record: CKRecord) {
        records.append(record)
    }

    // MARK: - CustomStringConvertible
    var description: String {
        return "SectionInfo(name: \"\(name)\", indexTitle: \(indexTitle), numberOfRecords: \(numberOfRecords), records: \(records))"
    }
}

// Example options for mapping section names to section index titles:
struct SectionIndexTitleForSectionName {
    static let firstLetterOfString = { (string: String) -> String? in
        guard let firstCharacter = (string as String).characters.first else {
            return ""
        }
        return String(firstCharacter).uppercased()
    }
    static let fullString = { (string: String) -> String? in
        return string as String
    }
    static let fullStringUppercased = { (string: String) -> String? in
        return (string as String).uppercased()
    }
}

/// split(records:bySectionNameKey)
///
/// Takes an input array of CKRecords, and splits them into sections using the (String) value retrieved from each record's "sectionNameKey".
///
/// The relative ordering of the records in the input array is maintained in each section.
///
/// - parameter records:                         An array of records to be split into sections.
/// - parameter bySectionNameKey:                The key on the CKRecords used to determine the section they belong to.
///                                              Assumes that record[sectionNameKey] returns a String value.
///
/// - returns: An array of sections, and a dictionary mapping sectionName -> the index in the sections array.
func split(records: [CKRecord], bySectionNameKey sectionNameKey: String) -> (sections: [SectionInfo], sectionNameToSection: [String: Int])
{
    func sectionName(forRecord record: CKRecord, withSectionNameKey sectionNameKey: String) -> String? {
        guard let sectionNameValue = record.object(forKey: sectionNameKey) else {
            assertionFailure("Record is missing expected sectionNameKey (\(sectionNameKey)): \(record)")
            return nil
        }
        guard let sectionName = sectionNameValue as? String else {
            assertionFailure("Record[\(sectionNameKey)] contains a value that cannot be converted directly to String. Record: \(record)")
            return nil
        }
        return sectionName
    }

    var sections: [SectionInfo] = []
    var sectionNameToSection: [String: Int] = [:]

    var currentSection: SectionInfo? = nil
    for record in records {
        guard let sectionName = sectionName(forRecord: record, withSectionNameKey: sectionNameKey) else {
            assertionFailure("Unable to obtain expected sectionNameKey (\(sectionNameKey)) for record: \(record)")
            continue
        }

        if let currentSection = currentSection, currentSection.name == sectionName {
            currentSection.add(record: record)
        }
        else {
            // find existing section, if present
            if let desiredSectionIndex = sectionNameToSection[sectionName] {
                sections[desiredSectionIndex].add(record: record)
            }
            else {
                // create new section
                let newSection = SectionInfo(name: sectionName, records: [record])
                sections.append(newSection)
                sectionNameToSection[sectionName] = sections.count - 1
                currentSection = newSection
            }
        }
    }

    return (sections: sections, sectionNameToSection: sectionNameToSection)
}

Just as a model you could write something resembling this for a tableView. 就像一个模型一样,你可以为tableView写一些类似的东西。 You can edit your dashboard entries or keys to sort by whatever you want. 您可以编辑仪表板条目或键以按您想要的任何方式进行排序。

Teachers.swift Teachers.swift

class Teachers: NSObject {
var recordID: CKRecordID!
var name: String!
var grade: String!
}

You might have a submit class like this. 您可能有这样的提交类。

SubmitViewController.swift SubmitViewController.swift

//There can be a fail at any time so CloudKit send methods have an
//error being passed into the closure. You can set an isDirty property.
CKContainer.default().publicCloudDatabase.save(teacherRecord) { [unowned self] record, error in
DispatchQueue.main.async {
//code 
ViewController.dirty = true
//code }

ViewController.swift ViewController.swift

//Property that will store an array of Teachers
//objects so that you can show them in a table view
var teachers = [Teachers]() 

//A “dirty” flag tracks when the derived data is out of sync with the primary data.
//It is set when the primary data changes. If the flag is set when the derived data
//is needed, then it is reprocessed and the flag is cleared. Otherwise,
//the previous cached derived data is used.
static var isDirty = true

//viewWillAppear() is going to clear the table view's selection if it has one,
//then it will use the isDirty flag to call loadTeachers() if it's needed.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

if let indexPath = tableView.indexPathForSelectedRow {
    tableView.deselectRow(at: indexPath, animated: true)
}

if ViewController.dirty {
    loadTeachers()
}
} 

func loadTeachers() {
let pred = NSPredicate(value: true)
let sort = NSSortDescriptor(key: "creationDate", ascending: true)
let query = CKQuery(recordType: "TeacherType", predicate: pred)
query.sortDescriptors = [sort]

let operation = CKQueryOperation(query: query)

//Set the desiredKeys property to be an array of the record keys you want
operation.desiredKeys = ["name", "grade"]
operation.resultsLimit = 25

//CKQueryOperation has two closures. One streams records and one is
//called when the records have been downloaded. To handle this you
//can create a new array that will hold the teachers as they are parsed.
var newTeachers = [Teachers]()

//Set a recordFetchedBlock closure on the CKQueryOperation object.
//This will be given a CKRecord value for every record that gets
//downloaded, and the convert that into a Teachers object. 
operation.recordFetchedBlock = { record in
let teacher = Teachers()
teacher.recordID = record.recordID
teacher.name = record["name"] as! String
teacher.grade = record["grade"] as! String
newTeachers.append(teacher)
}

//Called by CloudKit when all records have been downloaded, and will be
//given two parameters: a query cursor and an error if there was one.
//The query cursor is useful if you want to implement paging.
operation.queryCompletionBlock = { [unowned self] (cursor, error) in
DispatchQueue.main.async {
    if error == nil {
        self.teachers = newTeachers
        self.tableView.reloadData()
    } else {
        let ac = UIAlertController(title: "Fetch failed", message: "Please try again: \(error!.localizedDescription)", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "OK", style: .default))
        self.present(ac, animated: true)
    }
}
}

//Ask CloudKit to run it
CKContainer.default().publicCloudDatabase.add(operation)

} //End of loadTeachers()

Confirm: 确认:

  • Did you see your data in the CloudKit Dashboard? 您是否在CloudKit仪表板中看到了您的数据?
  • Did you name your record type "TeacherType" when writing and reading? 在写作和阅读时,您是否将记录类型命名为“TeacherType”?
  • For the Metadata Indexes, did you select Query next to ID, and Sort next to Date Created? 对于元数据索引,您是否选择了ID旁边的查询,并选择创建日期旁边的排序?
  • Is your device online? 你的设备在线吗?

*With help from Paul. *在保罗的帮助下。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM