1

I am using CloudKit (the default public database) to store information about matches between two teams. I am trying to use the Swift Concurrent approach with async/await to retrieve the match and team data.

The matches are stored as “Match” RecordType with some metadata, like the year the match was played (“matchYear”). The “Match” Record Type also contains a Reference List to a RecordType called “Team” (two teams for each match). The team names are stored in the “Team” Record Type (field “teamName”)

Rough description of my CloudKit Schema

“Match” Record Type
Fields:
   matchYear: Int64
   teams: Reference (List)
   …

“Team” Record Type
Fields:
   teamName: String
   …

With the following code I am able to read all “Match” records from the CloudKit and store that list to an array of MatchRecords, which I can then use to display a list of matches (using SwiftUI, if that makes any difference). The resulting array also contains references to the teams for each match.

struct MatchRecord {
    var recordID: CKRecord.ID
    var matchYear: Int
    var teams: [CKRecord.Reference]
    …
}

…

    private lazy var container = CKContainer.default()
    private lazy var database = container.publicCloudDatabase
…

    @MainActor func loadMatchList() async throws -> [MatchRecord] {
        let predicate = NSPredicate(value: true)
        let sortDescriptors = [NSSortDescriptor(key: “matchYear", ascending: false),NSSortDescriptor(key: "___createTime", ascending: false)]
        let query = CKQuery(recordType: “Match”, predicate: predicate)
        query.sortDescriptors = sortDescriptors
        let (matchResults, _) = try await database.records(matching: query)

        let allMatches: [MatchRecord] = matchResults
            .compactMap { _, result in try? result.get() }
            .compactMap {
                let match = MatchRecord(
                    recordID: $0.recordID,
                    matchYear: $0[“matchYear"] as! Int,
                    teams: $0["teams"] as! [CKRecord.Reference])
                // Potentially team record retrieval goes here…
                return match
            }
        return allMatches
    }

How do I retrieve the Team records as part of this async function so that I will have also the names of the teams available for the list view?

(I could potentially first fetch the list of matches and then loop through that array and retrieve the detail data for each match but that seems wasteful. I am guessing there should be a way to insert this in the compactMap closure marked down in the code sample above, but my map/reduce and async/await skills fail me…)

The data structure could be something along as described below.

struct MatchRecord {
    var recordID: CKRecord.ID
    var matchYear: Int
    var teams: [TeamRecord]
    …
}

struct TeamRecord {
    var recordID: CKRecord.ID
    var teamName: String
    …
}

FWIW I know that as there are only two teams for each game, I could also store the team names as part of the Match record, but In the future I am planning to also include the roster information for each team, so I need to come up with a clean and scalable method to retrieve this type of hierarchical data from CloudKit…

Using CoreData with CloudKit is not an option here.

1 Answers1

0

After some thinking I came up with one somewhat ugly solution using async properties. The Match and Team struct definitions are roughly these

struct MatchRecord {
    var recordID: CKRecord.ID
    var matchYear: Int
    var teams: [TeamRecord]
    …
}
struct TeamRecord {
    var referenceID: CKRecord.Reference
    var name: String {
        get async {
            try! await CKContainer.default().publicCloudDatabase.record(for: referenceID.recordID)["teamName"] as! String
        }
    }
}

The compactMap gets small for loop in there to populate the teams structure (only storing the ID as the names are retrieved only later)

            .compactMap {
                let match = MatchRecord(
                    recordID: $0.recordID,
                    matchYear: $0[“matchYear"] as! Int,
                    teams: [])
                for item in ($0["teams"] as! [CKRecord.Reference]) {
                    match.teams.append(TeamRecord(referenceID: item))
                }
                return match
            }

And when displaying the list, the list rows are defined in a separate view, where the async properties are pulled in using a task. Along these lines

struct MatchRow: View {
...
    @State var team0: String = ""
    @State var team1: String = ""

    var body: some View {
...
        Text(verbatim: "\(match.matchYear): \(team0) - \(team2)")
            .task{
                guard team0.isEmpty else { return }
            
                let (team0Result, team1Result) = await (match.teams[0].name, match.teams[1].name)
                team0 = team0Result
                team1 = team1Result
            }
        }
    }
}

I am sure there is a more elegant solution for this. I especially do not like the idea of adding the task in the list row view as that splits the logic in many places...