How to Update uitableview from CloudKit using discoverAllIdentities - ios

So, I am new to cloudKit and to working with multiple threads in general, which I think is the source of the problem here, so if I simply need to research more, please just comment so and I will take that to heart.
Here is my question:
I am working in Swift 3 Xcode 8.1
I have in my view controller this variable:
var contactsNearby: [String:CLLocation]?
Then at the end of ViewDidLoad I call one of my view controllers methods let's call it:
populateContactsNearby()
inside that method I call:
container.discoverAllIdentities(completionHandler: { (identities, error) in
for userIdentity in identities! {
self.container.publicCloudDatabase.fetch(withRecordID: userIdentity.userRecordID!, completionHandler: { (userRecord, error) in
let contactsLocation = userRecord?.object(forKey: "currentLocation")
if self.closeEnough(self.myLocation!, contactLocation: contactsLocation as! CLLocation) {
var contactsName = ""
contactsFirstName = userIdentity.nameComponents?.givenName
if contactsName != "" && contactsLocation != nil {
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
}
}
})
}
})
}
I apologize if I am missing or have an extra bracket somewhere. I have omitted some error checking code and so forth in order to get this down to bare-bones. So the goal of all that is to populate my contactsNearby Dictionary with data from CloudKit. A name as the key a location as the value. I want to use that data to populate a tableview. In the above code, the call to closeEnough is a call to another one of my view controllers methods to check if the contact from CloudKit has a location close enough to my user to be relevant to the apps purposes. Also myLocation is a variable that is populated before the segue. It holds the CLLocation of the app users current location.
The Problem:
The if statement:
if contactsName != "" && contactsLocation != nil { }
Appears to succeed. But my view controllers variable:
var contactsNearby: [String:CLLocation]?
Is never populated and I know there is data available in cloudKit.
If it's relevant here is some test code that I have in cellForRowAtIndexPath right now:
let contact = self.contactsNearby?.popFirst()
let name = contact?.key
if name != nil {
cell.textLabel?.text = name
}else {
cell.textLabel?.text = "nothing was there"
}
My rows alway populate with "nothing was there". I have seen answers where people have done CKQueries to update the UI, but in those answers, the user built the query themselves. That seems different from using a CloudKit function like discoverAllIdentities.
I have tried to be as specific as possible in asking this question. If this question could be improved please let me know. I think it's a question that could benefit the community.

Okay, I need to do some more testing, but I think I got it working. Thank you Paulw11 for your comment. It got me on the right track.
As it turns out there were 2 problems.
First, as pointed out I have an asynchronous call inside a for loop. As recommended, I used a dispatchGroup.
Inside the cloudKit call to discoverAllIdentities I declared a dispatchGroup, kind of like so:
var icloudDispatchGroup = DispatchGroup()
Then just inside the for loop that is going to make an async call, I enter the dispatchGroup:
icloudDispatchGroup.enter()
Then just before the end of the publicCloudDatabase.fetch completion handler I call:
icloudDispatchGroup.leave()
and
icloudDispatchGroup.wait()
Which, I believe, I'm still new to this remember, ends the dispatchGroup and causes the current thread to wait until that dispatchGroup finishes before allowing the current thread to continue.
The Above took care of the multithreading issue, but my contactsNearby[String:CLLocation]? Dictionary was still not being populated.
Which leads me to the 2nd problem
At the top of my view controller I declared my Dictionary:
var contactsNearby: [String: CLLocation]?
This declared a dictionary, but does not initialize it, which I don't think I fully realized, so when I attempted to populate it:
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
It quietly failed because it is optional and returned nil
So, in viewDidLoad before I even call populateContactsNearby I initialize the dictionary:
contactsNearby = [String:CLLocation]()
This does not make it cease to be an optional, which Swift being strongly typed would not allow, but merely initializes contactsNearby as an optional empty Dictionary.
At least, that is my understanding of what is going on. If anyone has a more elegant solution, I am always trying to improve.
In case you are wondering how I then update the UI, I do so with a property observer on the contactsNearby Dictionary. So the declaration of the dictionary at the top of the view controller looks like this:
var contactsNearby: [String: CLLocation]? {
didSet {
if (contactsNearby?.isEmpty)! || contactsNearby == nil {
return
}else{
DispatchQueue.main.sync {
self.nearbyTableView.reloadData()
}
}
}
}
I suppose I didn't really need to check for empty and nil. So then in cellForRowAtIndexPath I have something kind of like so:
let cell = tableview.dequeueReusableCell(withIdentifier: "nearbyCell", for: indexPath)
if contactsNearby?.isEmpty == false {
let contact = contactsNearby?.popFirst()
cell.textLabel?.text = contact?.key
}else {
cell.textLabel?.text = "Some Placeholder Text Here"
}
return cell
If anyone sees an error in my thinking or sees any of this heading for disaster, feel free to let me know. I still have a lot of testing to do, but I wanted to get back here and let you know what I have found.

Related

Coredata duplicates all objects in a list on update

I've come across an issue which rarely happens, and (of course) it works perfectly when I test it myself. It has only happened for a few users, and I know I have at least a couple hundred who use the same App daily.
The issue
When updating a list of coredata objects in a tableview, it not only updates the objects (correctly), it also creates duplicates of all these objects.
Coredata setup
It's a NSPersistentCloudKitContainer with these settings:
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Tableview setup
I have a tableview which displays a list of the 'ActivityType' objects. It's very simple, they have a name (some other basic string/int properties), and an integer called 'index'. This 'index' exists so that users can change the order in which they should be displayed.
Here is some code for how each row is setup:
for activityType in activityTypes {
row = BlazeRow()
row.title = activityType.name
row.cellTapped = {
self.selectedActivityType(activityType)
}
row.object = activityType
row.cellReordered = {
(index) in
self.saveNewOrder()
}
section.addRow(row)
}
As you can see, it has 2 methods. One for selecting the activity which shows its details in a new viewcontroller, and one which is called whenever the order is changed.
Here's the method that is called whenever the order is changed:
func saveNewOrder() {
Thread.printCurrent()
let section = self.tableArray[0] as! BlazeSection
for (index, row) in section.rows.enumerated() {
let blazeRow = row as! BlazeRow
let object = blazeRow.object as! ActivityType
object.index = Int32(index)
}
BDGCoreData.saveContext()
}
And here's the code that saves the context (I use a singleton to easily access the viewcontext):
class func saveContext(context: NSManagedObjectContext = BDGCoreData.viewContext) {
if(context.hasChanges) {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
Now, I swear to god it never calls the method in this viewcontroller to create a new object:
let activity = ActivityType(context: BDGCoreData.viewContext). I know how that works, and it truly is only called in a completely different view controller. I searched for it again in my entire project just in case, and it's really never called/created in any other places.
But somehow, in very rase cases, it saves the correct new order but also duplicates all objects in this list. Since it only happens rarely, I thought it might have something to do with threads? Which is why, as you can see in the code, I printed out the current thread, but at least when testing on my device, it seems to be on the main thread.
I'm truly stumped. I have a pretty good understanding of coredata and the app itself is quite complex with full of objects with different kind of relationships.
But why this happens? I have no clue...
Does anyone have an idea?

Firebase query observing reshowing data

I have a firebase query that observes data from a posts child.
func fetchPosts () {
let query = ref.queryOrdered(byChild: "timestamp").queryLimited(toFirst: 10)
query.observe(.value) { (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let value = child.value as? NSDictionary {
let post = Post()
let poster = value["poster"] as? String ?? "Name not found"
let post_content = value["post"] as? String ?? "Content not found"
let post_reveals = value["Reveals"] as? String ?? "Reveals not found"
post.post_words = post_content
post.poster = poster
post.Reveals = post_reveals
self.postList.append(post)
DispatchQueue.main.async { self.tableView.reloadData() }
//make this for when child is added but so that it also shows psots already there something like query.observre event type of
}
}
However, when a user posts something, it creates a more than one cell with the data. For instance, if I post "hello", a two new cards show up with the hello on it. However, when I exit the view and recall the fetch posts function, it shows the correct amount of cells. Also, when I delete a post from the database, it adds a new cell as well and creates two copies of it until I reload the view, then it shows the correct data from the database.
I suspect this has something to do with the observe(.value), as it might be getting the posts from the database and each time the database changes it creates a new array. Thus, when I add a new post, it is adding an array for the fact that the post was added and that it now exists in the database, and when I refresh the view it just collects the data directly from the database.
Also, sometimes the correct amount of cells show and other times there's multiple instances of random posts, regardless of whether I have just added them or not.
How can I change my query so that it initially loads all the posts from the database, and when some post is added it only creates one new cell instead of two?
Edit: The logic seeming to occur is that when the function loads, it gets all the posts as it calls the fetchPosts(). Then, when something is added to the database, it calls the fetchPosts() again and adds the new data to the array while getting all the old data. yet again.
One thing I always do when appending snapshots into an array with Firebase is check if it exists first. In your case I would add
if !self.postList.contains(post) {
self.postList.append...
however, to make this work, you have to make an equatable protocol for what I'm guessing is a Post class like so:
extension Post: Equatable { }
func ==(lhs: Post, rhs: Post) -> Bool {
return lhs.uid == rhs.uid
}
You are right in thinking that the .value event type will return the entire array each time there is a change. What you really need is the query.observe(.childAdded) listener. That will fetch individual posts objects rather than the entire array. Call this in your viewDidAppear method.
You may also want to implement the query.observe(.childRemoved) listener as well to detect when posts are removed.
Another way would be to call observeSingleEvent(.value) on the initial load then add a listener query.queryLimited(toLast: 1).observe(.childAdded) to listen for the latest post.

How to wait for firebase to load the data into a variable before returning the variable then call the function [duplicate]

This question already has answers here:
Wait for Firebase to load before returning from a function
(2 answers)
How to return value from an asynchronous callback function? [duplicate]
(3 answers)
Closed 5 years ago.
This Question is not a duplicate.
So I have a simple function that will take a parameter of the index of what tableview cell was clicked so the function will go into the didSelectRowAt tableView function, and it will also return a String.
The Function is called getOpposingUserName and it connects to firebase to gather the information provided inside the database.
Now I know that the functionality of the function works it is sort of a logic error that I don't quite understand. From what I know is that whenever you try to get data from firebase it runs that code and takes a second to actually get the data. but as its doing that it keeps running the code below where you tried to get data from the database.
which means if I want to return a String then the string will always be nil or whatever I set its initial value to, because the database will take to long to load and the function will already have ran through the code to return something.(that's my understanding of it, I'm sure there is a way more technical way to explain it)
so basically what I am asking is how would I "load" the data into a variable first then check that firebase has indeed loaded something into the variable and then return the variable for whatever called the function
This question IS not a duplicate I have read the question and tried to make sense of it in my own implementation. I am not returning a function that code is confusing to me when I read it.
here is my code for the function
func getOpposingUsername(_ index: Int) -> String {
var opposingUser: String = ""
//the self.tieBetToUser just gets the randomized id from the bet in firebase don't need to worry about it
self.datRef.child("Bets").child(self.tieBetToUser[index]).observe(.childAdded, with: { snapshot in
guard let dict = snapshot.value as? [String: AnyHashable] else {
return
}
opposingUser = dict["OpposingUsername"] as! String
print(opposingUser) //this code actually never runs I found out
})
//sleep(4) this will not work and is actually bad code anyway different phones may be faster or slower
return opposingUser // will always return "" at the moment
}
here is the solution the internet has provided
typealias someting = (String?) -> Void
func getOpposingUsername(completionHandler: #escaping someting, _ index: Int) {
var opposingUser: String = ""
self.datRef.child("Bets").child(self.tieBetToUser[index]).observe(.childAdded, with: { snapshot in
guard let dict = snapshot.value as? [String: AnyHashable] else {
return
}
opposingUser = dict["OpposingUsername"] as! String
if opposingUser.isEmpty {
completionHandler(nil)
} else {
completionHandler(opposingUser)
}
})
}
How exactly would I call this function somewhere else? like
getOpposingUsername(somethingGoesHere, index.path)

iOS Delegate is returning nil (Swift)

I have a feeling there is more than one problem with this code, but my first issue is that my delegate returns nil and I do not know why. First, is my delegate:
import UIKit
//delegate to move information to next screen
protocol userEnteredDataDelegate {
func userDidEnterInformation(info:NSArray)
}
Next, I have a var defined for the delegate and I believe the ? makes it an optional variable? This is defined inside the class
var dataPassDelegate:userEnteredDataDelegate? = nil
Now, after my user has entered information into the fields in the view, I want to add those field values to an array and then pass that array on to the next view where it will be added to. I have pieced this code together from some YouTube examples but I think I am missing a needed part. When do I assign some kind of value to the dataPassDelegate var so it is not nil when the if statement comes? Do I even need that if statement?
if blankData != 1 {
//add code to pass data to next veiw controller
enteredDataArray = [enterDate.text, enterSeason.text, enterSport.text, enterDispTo.text]
//println(enteredDataArray)
self.appIsWorking ()
if (dataPassDelegate != nil) {
let information: NSArray = enteredDataArray
println(information)
dataPassDelegate!.userDidEnterInformation(information)
self.navigationController?.popViewControllerAnimated(true)
} else {
println ("dataPassDelegate = nil")
}
//performSegueWithIdentifier("goToDispenseScreenTwo", sender: self)
activityIndicator.stopAnimating()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
blankData = 0
}
Your help is appreciated.
A delegate is a pointer to another object that conforms to a particular protocol. Often you use delegates to refine the behavior of your class, or to send back status information o the results of an async network request
When you set your dataPassDelegate delegate is up to you.
What is the object that has the dataPassDelegate property? What object will be serving as the delegate?
You need to create 2 objects (the object that will be serving as the delegate, and the object that has the dataPassDelegate property) and link them up.
We can't tell you when to do that because we don't know what you're trying to do or where these objects will be used.

Swift - Variable doesn't retain value

I am using AWS to host images for my iOS app. Right now I am trying to list all the objects in an S3 bucket.
Here is my code:
var description = ""
AWSS3.registerS3WithConfiguration(serviceConfig2, forKey: "s3")
AWSS3.S3ForKey("s3").listObjects(objectList).continueWithBlock { (task: AWSTask!) -> AnyObject! in
if task.error != nil {
println(task.error)
}
if task.result != nil {
description = task.result!.description
println(description)
}
return nil
}
println(description == "")
The output is true followed by the correct contents of task.result!.description. In other words, the println outside of the continueWithBlock is printing first and description has not been updated at that time.
How am I supposed to do things with description outside of the continueWithBlock?
You can assign a value that you need to another variable inside the scope of your class or function, then you can call didSet on the variable and carry out another function if you need to, like this:
var someVariableInScopeOfWhereItsNeeded = "abc" {
didSet {
self.maybeSomeOtherFunctionNow
}
}
You asked:
How am I supposed to do things with description outside of the
continueWithBlock
Short answer: You're not.
The whole point of an async method is that it continues immediately, before the time-consuming task has even begun processing. You put the code that depends on the results inside your block. See my answer on this thread for a detailed explanation, including an example project:
Why does Microsoft Azure (or Swift in general) fail to update a variable to return after a table query?
(Don't be fooled by the fact that it mentions MS Azure. It actually has nothing to do with Azure.)
#thefredelement 's solution of using a didSet method on the variable that gets set would work too.

Resources