Ivan Cantarino Ivan Cantarino - 1 year ago 104
iOS Question

Fetching data from CloudKit with queries in a loop retrieves wrong data

I have an app that uses

CloudKit
as backend.

I have a
Post
record type that has an
user
as
CKReference
in order to keep track from who's the post creator.

My
post struct
constructor takes a
user
as input so I can "mount" the post to display.

When I'm querying the server to retrieve 10 posts I use the following code:

var postRecords = [CKRecord]()
var userRecords = [User]()
let qop = CKQueryOperation(query: q)
qop.resultsLimit = 10

qop.recordFetchedBlock = {(record: CKRecord) in
postRecords.append(record)
let userReference = record["user"] as! CKReference
self.userCKReferences.append(userReference)
}

qop.queryCompletionBlock = {(cursor, err) in
if err != nil {
print("queryCompletionBlock error:", err ?? "")
return
}

let start = Date()

for reference in self.userCKReferences {

database.fetch(withRecordID: reference.recordID, completionHandler: { (tmpUserCKR, err) in

if err != nil {
print("Error:", err ?? "")
return
}


let tmpUser = User() // custom initializations here
userRecords.append(tmpUser)

if userRecords.count == postRecords.count {
self.assemblePosts(posts: postRecords, users: userRecords, previousCounter: previousPostCounter, firsTime: firstTime)
let finish = Date()
let executionTime = finish.timeIntervalSince(start)
print("Execution time: \(executionTime)")

}
})
}
}
database.add(qop)


As you can see, I fetch the
post
then I iterate through the fetched posts
user
CKReference
in order to get the respective user that created the post and then I construct my post to present in a
UICollectionView
to the user.

With this approach I've encountered a problem.

I made a test with 10 posts, in which the
post1
was created by
user1
, the
post2
created by
user2
,
post3
created by
user1
,
post4
created by
user2
(and so on...)

10 posts created by these two users, one at a time.

When I'm iterating through the
FOR LOOP
to fetch the users, somehow the order obtained isn't the right one, it mixes the user order.

I expect seeing
user1
,
user2
,
user1
,
user2
, etc...

But I'm given
user1
,
user1
,
user2
,
user1
all mixed and in an odd order, if I refresh the given order will be different from the previous one.

Even if I print the
record.recordID
inside the
fetch
operation when it goes wrong it isn't the same as the
reference.recordID
as it should be.

I though that might be happening, since the queries are made asynchronously and input could be wrong (one more iteration before the query finishes) and the
recordID
to fetch would be wrong, BUT for that to happen I wouldn't have the 10 posts fetched I think, but I get them -- the posts are correctly fetched but the users aren't.

In order to make a test , I created a recursive function that only fetches the next user from the server when the previous one was successfully fetched , and by that I notice that the users now are fetched properly.

here's my recursive function:

var refPosition: Int = 0
var userRecords = [User]()

func recursiveFetch(list: [CKReference], completion: ((Bool) -> Swift.Void)? = nil) {
if refPosition == list.count {
// All data fetched
completion?(true)
refPosition = 0
userRecords = [User]()
return
}

let database = CKContainer.default().publicCloudDatabase
let id = list[refPosition].recordID

database.fetch(withRecordID: id) { (record, err) in

if err != nil {
print("Failed to fetch user with error:", err ?? "")
return
}


let tmpUser = User() // with some initializations

self.userRecords.append(tmpUser)

self.refPosition += 1
self.recursiveFetch(list: list, completion: completion)
}
}


And I call it inside the
queryCompletionBlock
replacing the previous for loop, which goes like this:

var postRecords = [CKRecord]()


let qop = CKQueryOperation(query: q)
qop.resultsLimit = 10

qop.recordFetchedBlock = {(record: CKRecord) in
postRecords.append(record)
let userReference = record["user"] as! CKReference
self.userCKReferences.append(userReference)
}

qop.queryCompletionBlock = {(cursor, err) in
if err != nil {
print("queryCompletionBlock error:", err ?? "")
return
}

let start = Date()

self.recursiveFetch(list: self.userCKReferences, completion: { (success) in
if self.userRecords.count == postRecords.count {
self.assemblePosts(posts: postRecords, users: self.userRecords)
let finish = Date()
let executionTime = finish.timeIntervalSince(start)
print("Execution time: \(executionTime)")

}
})
}
database.add(qop)


This way I guarantee that the user for the respective post won't be wrong, but with this I got an huge performance block.

With the regular for loop my function execution time goes like this:


Execution time: 1.15568399429321


And with the recursive function it goes like this:


Execution time: 4.56143599748611


As you notice it's almost 4 times more.

Here's my question:

What might be causing the data corruption inside the for loop? and How can I improve performance in the recursive function?

Could this be a bug in
CloudKit
?

Thank you.
Regards.

EDIT

After some testing, I've done the following to the
For loop
:

for (i,reference) in self.userCKReferences.enumerated() {
print("\nindex:", i)
print("recordID:", reference.recordID)

database.fetch(withRecordID: reference.recordID, completionHandler: { (tmpUserCKR, err) in

print("\nindex inside fetch:", i)
print("recordID inside fetch:", reference.recordID)
print("fetched recordID:", tmpUserCKR?.recordID)

if err != nil {
print("Error in reference loop:", err ?? "")
return
}


let tmpUser = User(/* with custom inits*/)
userRecords.append(tmpUser)

if userRecords.count == postRecords.count {
self.assemblePosts(posts: postRecords, users: userRecords, previousCounter: previousPostCounter, firsTime: firstTime)
let finish = Date()
let executionTime = finish.timeIntervalSince(start)
print("Execution time: \(executionTime)")

}
})
}


As you can see, I tried to track the index before and within the fetch, and these are the prints:

index: 1
recordID: <CKRecordID: 0x6180002382a0; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>

index: 2
recordID: <CKRecordID: 0x6180002383c0; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>

index: 3
recordID: <CKRecordID: 0x618000238200; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>

index: 4
recordID: <CKRecordID: 0x60000003b600; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>

index: 5
recordID: <CKRecordID: 0x600000038420; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>

index: 6
recordID: <CKRecordID: 0x600000036640; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>

index: 7
recordID: <CKRecordID: 0x600000039b60; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>

index: 8
recordID: <CKRecordID: 0x60000003b780; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>

index: 9
recordID: <CKRecordID: 0x60000003b580; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>

index inside fetch: 1
recordID inside fetch: <CKRecordID: 0x6180002382a0; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x6100000369e0; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 4
recordID inside fetch: <CKRecordID: 0x60000003b600; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x60000003c640; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 3
recordID inside fetch: <CKRecordID: 0x618000238200; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x610000035fa0; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 2
recordID inside fetch: <CKRecordID: 0x6180002383c0; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x610000038f80; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 6
recordID inside fetch: <CKRecordID: 0x600000036640; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x60000003c640; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 0
recordID inside fetch: <CKRecordID: 0x608000037b20; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x618000238560; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 7
recordID inside fetch: <CKRecordID: 0x600000039b60; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x60800003abe0; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 9
recordID inside fetch: <CKRecordID: 0x60000003b580; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x60000003c780; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 8
recordID inside fetch: <CKRecordID: 0x60000003b780; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x60000003c7e0; recordName=25221185-4E74-4268-B3E9-3EF3AA435A76, zoneID=_defaultZone:__defaultOwner__>)

index inside fetch: 5
recordID inside fetch: <CKRecordID: 0x600000038420; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>
fetched recordID: Optional(<CKRecordID: 0x610000039680; recordName=5FF7D994-7F0D-4F1F-90A2-BB765EB52E27, zoneID=_defaultZone:__defaultOwner__>)
Execution time: 1.74884396791458


Before the fetch the index is correct, but since the loop goes way faster than the fetch, the index inside the fetch goes out of order so with this I corrupt my array.

What's the best way to overcome this? should I sort the objects when it's completed? If so what's the best way to do it?

Answer Source

Since you are getting the user using asynchronous call , that is why the order of the user is not same, request for first user reference may sometime take more time than for user2 so user2 is appended in the array before user1,

Now what you can do about that:

One choice for you is sort the result after the call

to sort the array you can use sortdescriptor

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download