So I understand that geocoding in iOS is an asynchronous method, and right now I'm working on an app that accesses a list of address from Parse (for restaurants) and performs forward geocoding on each address. Ideally I'd like this operation to be performed before a table is populated after a couple days of struggles this is just not happening for me.
My question is how do I get an iteration of forward geocoding to be completed prior to anything else happening in my app?
I've trying looking into grand central dispatch methods and I tried following this tutorial but I have got no luck:
http://www.raywenderlich.com/79150/grand-central-dispatch-tutorial-swift-part-2
Here is my code:
As you will see I'm trying to put found CLLocations in
var storeDict:NSDictionary = [CLLocation:PFObject]()
override func queryForTable() -> PFQuery! {
var query:PFQuery = PFQuery(className: self.parseClassName)
query.whereKey("Food", equalTo: foodName)
var prices = query.findObjects()
var i = 0
println(prices.count)
let geoCoder = CLGeocoder()
for price in prices {
var location:String = price.objectForKey("Address") as String
geoCoder.geocodeAddressString(location, completionHandler:
{(placemarks: [AnyObject]!, error: NSError!) in
if error != nil {
println("Geocode failed with error: \(error.localizedDescription)")
} else if placemarks.count > 0 {
var placemark = placemarks[0] as CLPlacemark
var location = placemark.location
var coordinateLocation:CLLocation = CLLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) as CLLocation
print(price)
print(coordinateLocation)
self.Restaurant[coordinateLocation] = price as? PFObject
print(i)
i++
}
})
println("check")
}
return query
}
First, you might want to notify the user that they can't do anything in the app until the data has finished being loaded (and geocoded). You could add a semi-transparent view with a spinning wheel over it to prevent the user from interacting with the app.
The table should have absolutely no idea where you are in the process -- whether you've started geocoding, finished geocoding, or how long ago you ran the geocoding. It should have no idea whether you even queried to get the restaurants. All it knows is its datasource, and if that datasource has objects, then it will use it to populate the rows in the table.
Say the datasource is an array of Restaurant objects. If the array is empty, then the table will be empty. You can do KVO on the array, so that whenever the datasource is updated, reloadData will be called on the tableView.
Now you've separated out the table as a separate problem that you've handled. Not onto querying and geocoding.
Currently, you're not querying Parse in background with block, but you're literally halting the program until the query finishes. I understand why you're electing to do that since you're concerned about doing nothing until the query finishes, but it would be a lot better to execute in background.
Once the query finishes, you loop through the resulting objects and geocode one by one. Just a word of caution, Apple does not allow you to geocode a ton of objects at a time -- they will throttle you, so I would limit the query to only return the amount of objects you need. When an object is finished being geocoded, add it to the datasource. This will trigger the reload of the table, and your data will appear.
Say now that you queried 20 objects. Each time the geocoding completes, your tableview will be reloaded. It might be better to wait until the geocoding completes on all 20 objects before calling reload. You can actually add each geocode operation onto a queue with each operation adding their geocoded object to some temporary array, and then add an operation that updates your datasource with the temporary array. At that moment, the table will be updated with the new data. Note that the downside of doing it this way is that some addresses might take longer the geocode than others, so rather than displaying whatever data it was able to geocode thus far, it will wait until everything has been fully geocoded.
Lastly, you could have the query run in background and have the geocoding occur in its completion block. If the view just loaded for the first time, it can show the spinning wheel until the datasource is updated. When the datasource is updated, the spinning wheel is removed.
Related
Every firebase client example I see in Swift seems to oversimplify properly loading data from Firebase, and I've now looked through all the docs and a ton of code. I do admit that my application may be a bit of an edge case.
I have a situation where every time a view controller is loaded, I want to auto-post a message to the room "hey im here!" and additionally load what's on the server by a typical observation call.
I would think the flow would be:
1. View controller loads
2. Auto-post to room
3. Observe childAdded
Obviously the calls are asynchronous so there's no guarantee the order of things happening. I tried to simplify things by using a complete handler to wait for the autopost to come back but that loads the auto-posted message twice into my tableview.
AutoPoster.sayHi(self.host) { (error) in
let messageQuery = self.messageRef.queryLimited(toLast:25).queryOrdered(byChild: "sentAt")
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
if let dict = snapshot.value as? [String: AnyObject] {
DispatchQueue.main.async {
let m = Message(dict, key: snapshot.key)
if m.mediaType == "text" {
self.messages.append(m)
}
self.collectionView.reloadData()
}
}
})
}
Worth noting that this seems very inefficient for an initial load. I fixed that by using a trick with a timer that will basically only allow the collection view to reload maximum every .25s and will restart the timer every time new data comes in. A bit hacky but I guess the benefits of firebase justify the hack.
I've also tried to observe the value event once for an initial load and then only after that observe childAdded but I think that has issues as well since childAdded is called regardless.
While I'm tempted to post code for all of the loading methods I have tried (and happy to update the question with it), I'd rather not debug what seems to not be working and instead have someone help outline the recommended flow for a situation like this. Again, the goal is simply to auto-post to the room that I joined in the conversation, then load the initial data (my auto-post should be the most recent message), and then listen for incoming new messages.
Instead of
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
try replacing with
let childref = FIRDatabase.database().reference().child("ChildName")
childref.queryOrdered(byChild:"subChildName").observe(.value, with: { snapshot in
For an iOS app I am working on, I need to fetch messages in descending order i.e the latest message comes first, followed by second newest message etc.
From looking at other SO answers and research it seems that the best approach for my situation is to create a negative timestamp and then persist that to the database as an extra property to messages.
I will then use queryOrderedByChild('negativeTimestamp') to fetch the messages in a observeSingleEvent and then have a childAdded observer to handle messages which are sent once initial calls are made.
In the firebase documentation it says I can get the server timestamp value from this snippet of code firebase.database.ServerValue.TIMESTAMP
How do I write this for Swift 3?
First, see the linked answer in the comments. That answer relies on the client to generate a timestamp that's made negative and written to Firebase.
If you want to have Firebase generate a timestamp, that can be done as well with this little snappy Firebase structure and piece of code.
First, let's take a look at the structure
root
parent_node
-Y8j8a8jsjd0adas
time_stamp: -1492030228007
timestamp: 1492030228007
Next, some code to create and work with that structure:
Define a var we can use within our class that references the Firebase time stamp
let kFirebaseServerValueTimestamp = [".sv":"timestamp"]
and a function that adds an observer to the timestamp node:
func attachObserver() {
let timestampRef = self.ref.child("timestamp")
let parentNodeRef = self.ref.child("parent_node")
var count = 0
timestampRef.observe(.value, with: { snapshot in
if snapshot.exists() {
count += 1
if count > 1 {
let ts = snapshot.value as! Int
let neg_ts = -ts
let childNodeRef = parentNodeRef.childByAutoId()
let childRef = childNodeRef.child("time_stamp")
childRef.setValue(neg_ts)
count = 0
}
}
})
And a function that writes out a timestamp, therefore causing the observer to fire which creates child nodes within the parent_node based on the Firebase time stamp
func doTimestamp() {
let timestampRef = self.ref.child("timestamp")
timestampRef.setValue(kFirebaseServerValueTimestamp)
}
Here's the rundown.
In the attachObserver function, we attach an observer to the timestamp node - that node may or may not exist but if it doesn't it will be created - read on. The code in the closure is called any time an event occurs in the timestamp node.
When the doTimestamp function is called, it creates and writes a timestamp to the timestamp node, which then fires the observer we attached in attachObserver.
The code in the observe closure does the following:
Make sure the snapshot contains something, and if it does, increment a counter (more on that in a bit). If the counter is greater than 1 get the timestamp as an integer from the snapshot. Then, create it's negative and write it back out to Firebase as a child of parent_node.
How this would apply would be anytime you want to timestamp a child node with a Firebase generated timestamp but negative value for reverse loading/sorting - which speaks to the OP question.
The gotcha here is that when this happens
timestampRef.setValue(kFirebaseServerValueTimestamp)
It actually writes twice to the node, which would cause the code in the closer to be called twice.
Maybe a Firebaser can explain that, but we need to ignore the first event and capture the second, which is the actual timestamp.
So the first event will cause the observer closer to fire, making count = 1, which will be ignored due to the if statement.
Then the second event fires, which contains the actual timestamp, and that's what we use to make negative and write out to Firebase.
Hope this helps the OP and the commenters.
Regardless whether it's for Swift or not, another conceptual solution is to rely on Firebase's server time offset value.
It's not as precise as firebase.database.ServerValue.TIMESTAMP, but the difference is usually within milliseconds. The advantage is that it lets you create a negative timestamp on the client without having to update your Firebase node twice.
You grab the server time offset value when you need it from Firebase, generate the negative timestamp on the client, and then save your object in Firebase once.
See:
https://groups.google.com/forum/#!topic/firebase-talk/EXMbZmyGWgE
https://firebase.google.com/docs/database/ios/offline-capabilities#clock-skew (for iOS).
https://firebase.google.com/docs/database/web/offline-capabilities#clock-skew (for web).
var offsetRef = firebase.database().ref(".info/serverTimeOffset");
offsetRef.on("value", function(snap) {
var offset = snap.val();
var negativeTimestamp = (new Date().getTime() + offset) * -1;
});
I'm looking to do something similar to most popular apps (e.g. Snapchat) where users verify their phone numbers, then give the app permission to their Contacts so friends who have already signed up can be displayed.
I'd like two table view sections. One section for contacts NOT on the app, and another section for contacts whose phone numbers matched one in the Parse column, "phoneNumber". The second section is causing this error telling me I'm running multiple queries at once.
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'This query has an outstanding network connection. You have to wait until it's done.'
Here is the code I'm trying to run:
let query = PFUser.query()
for person in self.people! { // iterate through phone contacts
let numbers = person.phoneNumbers
let primaryNumber = numbers?.map({$0.value})[0] // pull contact's first phone number (i.e. main number)
dispatch_async(dispatch_get_main_queue()) {
query?.whereKey("phoneNumber", equalTo: primaryNumber!)
query?.getFirstObjectInBackgroundWithBlock({ (object, error) -> Void in
self.contactsOnTheApp.addObject(object!) // create array of users already on the app to then show in table view section
})
}
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
You are defining query outside the loop and then perform a background query inside the loop. You modify the existing query on each loop iteration but as the first background query hasn't completed when you modify the query for the next search you get the error message.
You can move the let query=PFUser.query() inside the for loop but it would be more efficient to create an array of phone numbers and use a containedIn condition on a single query and then loop through the returned results.
Also you don't need the dispatch_async since you are using the background Parse call and you don't need to dispatch the reload on the main queue. In fact, you are executing the reload too early because your queries won't be complete. You need to reload from the query completion block. This will be easier if you have a single query too.
There must be a better way to perform search on a remote server database than what I am doing right now.
I have a UISearchController installed on top of a table view that shows only 30 rows - Upon the user scrolling down to the bottom another 30 rows get loaded up and so forth...
So the the method I am using to perform the search looks like this:
func searchForWriter(searchString: String!) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
Client.sharedInstance.searchForWriterByName(searchString) {(searchResults: [Writer]?, error: NSError?) -> Void in
if let error = error {
NSLog("Error: %#", error.description)
dispatch_async(dispatch_get_main_queue()) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
} else {
dispatch_async(dispatch_get_main_queue()) {
self.filteredWriters = searchResults!
// TODO: - This needs debugging to get the last searched string?
print("searched for: \(searchString)")
self.writersTableView.reloadData()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
}
}
The UISearchResultsUpdating responds to any change in the searchBar and hence invoking the above searchForWriter method.
The problem is when the search results are obtained back from the server, there is no guarantee these will be for the last searched string?
In the matter of fact in almost most of the cases, this method will take longer time get results for a smaller search string rather a longer one... Hence giving completely frustrating search results
In fact you ask several questions here:
1) What you are looking for concerning the amount of displayed rows is called pagination. Look it up for strategies how to do it, but in any case your server has to support it.
2) Concerning the search on each entered character: You should only start searching when at least n characters are entered (n being 3 or so), search results for only one character probably are useless anyway.
3) Add some timer constraint so that the search only starts when the user did not enter anything for 600 ms or so - this avoids lots of calls to the server for unnecessary intermediate results.
my firebase data structure looks like the following
user
|__{user_id}
|__userMatch
|__{userMatchId}
|__createdAt: <UNIX time in milliseconds>
I'm trying to listen for the child added event under userMatch since a particular given time. Here's my swift code:
func listenForNewUserMatches(since: NSDate) -> UInt? {
NSLog("listenForNewUserMatches since: \(since)")
var handle:UInt?
let userMatchRef = usersRef.childByAppendingPath("\(user.objectId!)/userMatch")
var query = userMatchRef.queryOrderedByChild("createdAt");
query = query.queryStartingAtValue(since.timeIntervalSince1970 * 1000)
handle = query.observeEventType(FEventType.ChildAdded, withBlock: { snapshot in
let userMatchId = snapshot.key
NSLog("New firebase UserMatch created \(userMatchId)")
}, withCancelBlock: { error in
NSLog("Error listening for new userMatches: \(error)")
})
return handle
}
What's happening is that the event call back is called only once. Subsequent data insertion under userMatch didn't trigger the call. Sort of behaves like observeSingleEventOfType
I have the following data inserted into firebase under user/{some-id}/userMatch:
QGgmQnDLUB
createdAt: 1448934387867
bMfJH1bzNs
createdAt: 1448934354943
Here are the logs:
2015-11-30 17:32:38.632 listenForNewUserMatches since:2015-12-01 01:32:37 +0000
2015-11-30 17:45:55.163 New firebase UserMatch created bMfJH1bzNs
The call back was fired for bMfJH1bzNs but not for QGgmQnDLUB which was added at a later time. It's very consistent: after opening the app, it only fires for the first event. Not sure what I'm doing wrong here.
Update: Actually the behavior is not very consistent. Sometimes the call back is not fired at all, not even once. But since I persist the since time I should use when calling listenForNewUserMatches function. If I kill the app and restart the app, the callback will get fired (listenForNewUserMatches is called upon app start), for the childAdded event before I killed the app. This happens very consistently (callback always called upon kill-restart the app for events that happened prior to killing the app).
Update 2: Don't know why, but if I add queryLimitedToLast to the query, it works all the time now. I mean, by changing userMatchRef.queryOrderedByChild("createdAt") to userMatchRef.queryOrderedByChild("createdAt").queryLimitedToLast(10), it's working now. 10 is just an arbitrary number I chose.
I think the issue comes from the nature of time based data.
You created a query that says: "Get me all the matches that happened after now." This should work when the app is running and new data comes in like bMfJH1bzNs. But older data like QGgmQnDLUB won't show up.
Then when you run again, the since.timeIntervalSince1970 has changed to a later date. Now neither of the objects before will show up in your query.
When you changed your query to use queryLimitedToLast you avoided this issue because you're no longer querying based on time. Now your query says: "Get me the last ten children at this location."
As long as there is data at that location you'll always receive data in the callback.
So you either need to ensure that since.timeIntervalSince1970 is always earlier than the data you expect to come back, or use queryLimitedToLast.