iOS Realm detect changes for RLMObject - ios

I have written code like this to listen for changes in Post object.
notification = Post.allObjects(in: RLMRealm.encryptedRealm()! as! RLMRealm).addNotificationBlock({ (results, changes, error) in
let pred = NSPredicate(format: "tag == %#", self.postTag)
self.posts = CommonResult.objects(with: pred).sortedResults(usingKeyPath: "id", ascending: true)
if let _ = changes {
if (changes!.insertions.count > 0 || changes!.deletions.count > 0 || changes!.modifications.count > 0) {
self.tblListing.reloadData()
}
}
})
In my Post object, there are 2 property. One is 'rowHeight' and another is 'isLikeByMyself'.
I want to reload tableview only if 'isLikeByMyself' is changed. How shall I do? Is it possible?

You have at least two options.
If you don't have many Post objects, you may want to consider registering object notifications on each of them. Object notifications tell you which properties were changed and how, so you can use that information to reload the table view. However, you will need to register a separate notification on each Post object, which may not be practical if you have a large number of them.
Here is an alternate idea. Add an ignored boolean property to Post called something like isLikeWasChanged, and add a Swift didSet block that will set isLikeWasChanged to true any time you modify isLikeByMyself. Then, in your existing collection observation block, reload the table view only if at least one isLikeWasChanged is true, remembering to set them all back to false before you leave the block.

Related

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 Update uitableview from CloudKit using discoverAllIdentities

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.

How to observe properties or instance variables with ReactiveCocoa

I would like to monitor the number of tableview cell and once it becomes zero (delete all the rows) my button would be disabled immediately otherwise (insert a new row) it will be enabled.
And I would like to do this with ReactiveCocoa.
I'm a newbee with RAC and what I tried is like this:
let count = NSNumber(integer: self.records!.count)
let countSignal: RACSignal = count.rac_willDeallocSignal();
countSignal.subscribeNext { (AnyObject) in
NSLog("here i am")
self.navigationItem.rightBarButtonItem?.enabled = AnyObject.integerValue > 0 ? true : false;
}
But it didn't work.
So far I know how to generate signals and monitor the change with some text fields cause it just comes like this:
self.myTextField.rac_textSignal
But what if I want to product signals or monitor the change of properties or variables so I could subscribe and pass the signals on and do some callback based on their changes?
You will get a value changed signal by using rac_valuesForKeyPath method to observer the property value,here is an tutorials:SWIFT AND REACTIVECOCOA,in Objective-C there is macro RACObserver(target,key path)

Changing NSFetchedResultsController's fetchRequest.predicate does not trigger delegate

I've got a NSPredicate on my FRC's fetchRequest. At some point an object is added to CoreData, I create a new predicate, update the fetchRequest and perform a fetch:
self.fetchedAddressesController.fetchRequest.predicate = self.predicate;
BOOL success = [self.fetchedAddressesController performFetch:nil];
This does however not invoke the FRC's delegate methods like controllerWillChangeContent:. And my table view is not updated.
However, when I add the following line:
[self.tableView reloadData];
below the two shown above, I do see the expected update. This shows that the data to be displayed has indeed changed.
I've checked the new predicate and it's fine. I also set FRC's delegate to self, and its methods are invoked in other cases.
Any ideas already what could be wrong?
The behaviour you have described is expected. According to the Apple documentation, if you want to modify a fetch request on a NSFetchedResultsController, you must delete the cache (if you are using one), modify the NSFetchRequest, then invoke performFetch: (which won't call any of the delegate methods).
If you want to know what has changed between the predicates, you need to store the old state and compare. A library that I've used in the past for this is Doppelganger.
The delegate methods are called when the FRC observes changes to the set of fetched objects it has eyes on after you do a fetch. If you go and change the predicate and do a new fetch, the FRC is reset and is now observing a different set of objects. The delegate methods aren't called because nothing changed in the original set of objects.
Careful if you are literally changing the entity...
We had a tricky one, not only the sort changed but the actual entity class changed, so for two different tab buttons,
let r = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Cats")
but then ..
let r = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Dogs")
In fact if you're doing that, THIS will NOT work:
func buttonLeft() {
mode = .cats
bringupFetchRequest()
pull.feedForCats() // get fresh info if any at the endpoints
}
func buttonRight() {
mode = .dogs
bringupFetchRequest()
pull.feedForDogs() // get fresh info if any at the endpoints
}
It will eventually crash.
It seems you need this:
func buttonLeft() {
mode = .cats
bringupFetchRequest()
tableView.reloadData()
pull.feedForCats() // get fresh info if any at the endpoints
}
func buttonRight() {
mode = .dogs
bringupFetchRequest()
tableView.reloadData()
pull.feedForDogs() // get fresh info if any at the endpoints
}
That (seems to) reliably work.

Confused on snippet of code for implementing iCloud behavior on iOS

The code is from a book. In terms of overall app architecture (MVC), it's part of the Model. The model has two main components:
An array of tags called tags
A dictionary of tag - query called searches
The app saves these pieces of data in the NSUserDefaults (iOS defaults system) and on iCloud. The following method is called when a change in iCloud is signaled. The parameter is an instance of NSNotification.userInfo
// add, update, or delete searches based on iCloud changes
func performUpdates(userInfo: [NSObject: AnyObject?]) {
// get changed keys NSArray; convert to [String]
let changedKeysObject = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey]
let changedKeys = changedKeysObject as! [String]
// get NSUbiquitousKeyValueStore for updating
let keyValueStore = NSUbiquitousKeyValueStore.defaultStore()
// update searches based on iCloud changes
for key in changedKeys {
if let query = keyValueStore.stringForKey(key) {
saveQuery(query, forTag: key, saveToCloud: false)
} else {
searches.removeValueForKey(key)
tags = tags.filter{$0 != key}
updateUserDefaults(updateTags: true, updateSearches: true)
}
delegate.modelDataChanged() // update the view
}
}
My question is on the if - else inside the for loop. The for loop iterates over keys that where changed; either the user adds a new search, updates an existing search, or deletes a search. But, I don't understand the logic behind the if-else. Some clarifying thoughts would be appreciated. I've read it over and over but it doesn't tick with me.
if let query = keyValueStore.stringForKey(key)
means that if keyValueStore contains a string corresponding to key, then this string will be assigned to the constant query.
This is called "safe unwrapping":
inside the if let ... condition, the query is safely saved with saveQuery because using if let ... guarantees that the value of keyValueStore.stringForKey(key) won't be nil.
If the value is nil, then in the else branch, the filter method is used to update the tags array without the key we just processed: tags.filter{$0 != key} means "return all items in tags that are different from key" (the $0 represents the current item from the array processed by filter).

Resources