firestore collection path giving bugs with constants value and String value - ios

So my goal is to get rid of these bugs completely. I am in a dilemma where each decision leads to a bug.
The first thing I can do that eventually becomes an issue is use a String-interpolated collection path in all my query functions like so:
func getEventName() {
listener = db.collection("school_users/\(user?.uid)/events").order(by: "time_created", descending: true).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the data: \(error)")
} else {
self.events = querySnapshot!.documents.map { document in
return EventName(eventName: (document.get("event_name") as! String))
}
self.tableView.reloadData()
}
}
}
The thing with this is, when I run the app on the simulator, I am restricted from pressing buttons and then sometimes I can press them and then sometimes they get restricted again. This bug is so confusing because it makes no sense where it springs from.
The other issue is I can use a Constants value in all the query functions in my collections path.
static let schoolCollectionName = "school_users/\(user?.uid)/events"
This is nested in a Firebase struct within the Constants struct. In order to keep Xcode from giving errors I create a let users = Auth.auth().currentUser variable outside the Constants struct. The issue with this value is that when I put that in all of my query functions collection paths, all the buttons are accessible and selectable all the time, but when a user logs out and I log in as a new user, the previous user's data shows up in the new user's tableview.
It would obviously make more sense to use the Constants value because you prevent typos in the future, but I can't figure out how to get rid of the bug where the old user's data shows up in the new user's tableview. Thanks in advance.

The user id should definitely not be a constant. What it sounds like is that right now, you have no reliable way to change users -- your setup probably depends on which user is logged in at app startup, since that's where your variable gets set.
I would do something more like this:
func getEventName() {
guard let user = Auth.auth().currentUser else {
//handle the fact that you don't have a user here -- don't go on to the next query
return
}
listener = db.collection("school_users/\(user.uid)/events").order(by: "time_created", descending: true).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
Note that now, user.uid in the interpolated path doesn't have the ? for optionally unwrapping it (which Xcode is giving you a warning for right now). It will also guarantee that the correct query is always made with the currently-logged-in user.
Regarding being able to press the buttons, that sounds like an unrelated issue. You could run your app in Instruments and check the Time Profiler to see if you have long-running tasks that are gumming up the main/UI thread.

Related

Is there a way to pull the last CoreData Entry only?

I have a dashboard view that I'd like to display a few variables from my last entered CoreData entry. However, I can't figure out how to fetch only the last data entered into a variable so I can display it. Any ideas?
EDIT: I'm trying to setup a NSFetchRequest inside of a called function that is called only onappear. However, I'm getting errors and am lost.
func singleEntryPull() -> [Item] {
let request: NSFetchRequest<Item> = Item.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "todaysDate", ascending: false)]
request.fetchLimit = 1
let singleEntry = FetchRequest(fetchRequest: request)
return singleEntry
}
And then the return from the function should only show 1 result and I can then use the returned value to display the variables I need?
Well, not sure if this is the cleanest or best way to do this but I got it working like I want for now until a better solution comes up. I'm still using #FetchRequest which im now aware is pulling data live and updating it, but that might work as if someone keeps the app open overnight and updates it in the morning, I'd want it to display that latest entry. I used this:
ForEach(singleEntry.prefix(1)) { item in
A fetch limit of 1 on a fetch request will return you a single value. However, when you're setting up a #FetchRequest, you're doing more than this - you're making the initial fetch and then continuing to monitor the context for changes, so it live updates. This monitoring only uses the predicate of your fetch request.
Depending on your order of operations, you could be seeing the latest data, and then any new data inserted since you started that view. My experiments with the SwiftUI core data template project prove this out - on initial run you get the a single latest entry, but as you add newer ones, the fetch-limited screen picks up the new entries.
Depending on how this view is actually used, you have two choices - you can do an actual fetch request on appear of the view and store the result as an observable object, or you can make sure you only ever use the first record from the fetch request's results array, which will always be the latest record because of your sort ordering:
var body: some View {
if let latest = singleEntry.first {
// Some view describing the latest entry
} else {
Text("No record")
}
}

AGSFeature.attributes of ArcGIS Runtime 100 now giving parameters in its dictionary

I am sending a query of geometry to show the features selected on map and get the selected features.
Both the things are working okay but when i check the attribute dictionary of a feature it contains only 5 key/value pair but the same function in android returning 10 key/value pair.
I am making query like this
let query = AGSQueryParameters()
if let selectionGraphicGeometry = selectionGraphic?.geometry {
let geometry = AGSGeometryEngine.simplifyGeometry(selectionGraphicGeometry)
query.geometry = geometry
}
selectableLayer?.selectFeatures(withQuery: query, mode: AGSSelectionMode.add, completion: { (result, error) in
if let features = result?.featureEnumerator().allObjects {
for feature in features {
let keys = feature.attributes.allKeys
}
}
}
I dont know where i am doing this wrong
In the Version 100 Runtime, we take a slightly different approach for efficiency's sake.
Features will by default only include the minimal set of fields required for rendering and editing. When you are making a selection, you are working with those features, so that's why you're seeing the smaller set of fields.
If you need all the fields for your selected features, you should actually perform a query on your AGSServiceFeatureTable and select the features based off that.
Something like this:
let table = selectableLayer.featureTable as? AGSServiceFeatureTable
table?.queryFeatures(with: query, queryFeatureFields: .loadAll) { (result, error) in
guard error == nil else {
print("Error selecting features: \(error!.localizedDescription)")
return
}
guard let features = result?.featureEnumerator().allObjects else {
return
}
selectableLayer.select(features)
for feature in features {
let keys = feature.attributes.allKeys
print(keys)
}
}
What's odd is that you say you're seeing a different number of fields returned on Android than on iOS. Are you sure the Android app is displaying the same layer with the same renderer?
One other point: You might be better off using the Esri Community (GeoNet). The ArcGIS Runtime SDK for iOS Forum can be found here. Do post a question there if you are seeing different numbers of fields on iOS and Android with the same layer and renderer.
Hope this helps!
P.S. There are two related things you might want to know.
AGSArcGISFeature instances are now Loadable. So if you have an individual feature and you want to get all the fields for it from the source service, you can call load(completion:) on it. Or you could pass an [AGSArcGISFeature] array to the ASGLoadObjects() helper function. However, that function will make a separate network request for each feature so if your array isn't small, that could lead to a bad user experience.
You can set your AGSServiceFeatureTable.featureRequestMode to .manualCache. You then need to call populateFromServiceWithParameters() to load the precise data that you need locally (and as you pan and zoom the map, you will need to manually manage this cache). See here for more details.

Does simply pulling all users from firebase database and putting them into an array efficient?

I have a simple iOS app that part of the app grabs all the users from firebase database so you can search them, and do different functions. Now my question is, if/when the app grows and there are thousands of users, does pulling all the users from the database and adding them to an array of [user]'s, still not crash or slow the app? I see so many people on youtube just loop through firebase and grab all the users. Please note I am excluding profile photos so there is no downloading images involved, just strings. I have some code I thought could solve this possible problem, but I am starting to wonder if there even is a problem with just fetching all the users from firebase and putting them into and array and then displayed in a tableview.
Here is some of my code right now, but it still I notice when I type in one letter, then turn airplane mode on, it downloaded all the users. I really need some help or some advice on this one, thanks.
var checklers = [String]()
func updateSearchResults(for searchController: UISearchController) {
if searchController.searchBar.text == "" {
filteredUsers = users
}
else {
print("refreshing")
if let uidi = FIRAuth.auth()?.currentUser?.uid {
view.addSubview(activityInd)
activityInd.startAnimating()
filteredUsers.removeAll()
checklers.removeAll()
let ref = FIRDatabase.database().reference()
ref.child("users").queryOrderedByKey().observe(.value, with: { snapshot in
if let userr = snapshot.value as? [String : AnyObject] {
for (_, velt) in userr {
if let usernamerr = velt["Username"] as? String {
if usernamerr.lowercased().contains(searchController.searchBar.text!.lowercased()) {
let userNew = usera()
if let name = velt["Full Name"] as? String, let uidd = velt["uid"] as? String {
userNew.name = name
userNew.username = usernamerr
userNew.uid = uidd
if self.checklers.contains(uidd) {
print("already")
}
else {
if userNew.uid != uidi {
self.filteredUsers.append(userNew)
self.activityInd.stopAnimating()
self.checklers.append(uidd)
}
print("added a user")
}
}
}
}
self.tableViewSearchUser.reloadData()
}
}
})
ref.removeAllObservers()
}
// filteredUsers = users.filter( { ($0.username?.lowercased().contains(searchController.searchBar.text!.lowercased()))! })
}
tableViewSearchUser.reloadData()
}
Please add any advice, thanks.
Just for searching one or two users, each time a user would need to fetch all the records and putting them all in an array (all in memory). You want SQL-where query function, but Firebase is just different and doesn't have it.
Problem with storing fetching all data approach:
1) Storing just all the user's information in an array of user objects is NOT scalable on client's device.
2) When the number of users gets to ten of thousands, a day worth of search by a single user will eat up a sizable amount of real time database read quota.
3) Stale user data, an user has to re-download all the users just becauase on user changed his name to Doggie1 to doggieTwo.
Solutions:
1) If you haven't done so already, I suggest the options of doing some server-side filtering first by following the best practice here:
Firebase Retrieving Data - Filtering
Downloading a sub-set of user that fits some criteria and then do a bit of client-side filtering. Still is problematic when users get to tens of thousands.
Firebase has a client-size data persistence feature, but in your case if there filtering rule doesn't fit your need, you need do you own caching with some persistent storage solution. Instead of putting the fetched object in an Array[User], I would store each in a database SQLite on iOS and Android apps.
2) Implement a ElasticSearch with the FlashLight plugin, this involves some extra setup (I know, I've been through it, I learned quite a bit), but it is well worth it for autocomplete and search functions that Firebase currently doesn't support.
A pluggable integration with ElasticSearch to provide advanced content searches in Firebase.

GMSPlacesClient cancel request

In one of my apps users have an ability to search for a location by its name. I am using Google Maps APIs to display autocompleted suggestions with the help of GMSPlacesClient.
Code below illustrates how I do this:
func performSearch() {
let filter = GMSAutocompleteFilter()
filter.type = GMSPlacesAutocompleteTypeFilter.City
placesClient?.autocompleteQuery(searchBar.text!, bounds: nil, filter: filter, callback: { (results, error: NSError?) -> Void in
// update results
})
}
This code works very well. However, I noticed one scenario in which it sort of fails. One example is if I search for "Chicago". If I type "Ch" very fast sometimes the results are returned for "Ch" correctly, but sometimes I obtain the results for "C". The problem arises because these are asynchronous requests and I am performing the search each time user types something. So, even though the request for "C" was initiated before "Ch" it can return last.
I therefore need to cancel all previous initiated requests before I start a new one. However, I couldn't find a way of doing this. Does anyone know how to achieve it?
Update
I have tried to use a workaround:
let string = myResults[0].attributedFullText
string.enumerateAttribute(kGMSAutocompleteMatchAttribute, inRange: NSMakeRange(0, string.length), options: .Reverse) { (value, range, stop) -> Void in
if value != nil {
let t = NSString(string: string.string)
let str = t.substringWithRange(range)
//now compare with original search string
}
}
This fixes the issue with Chicago (and other similar cases). However, such approach will disable autocomplete suggestions when user makes a typo because the matched string and original search string are always different in this case.
So, the question still remains open. An ideal approach would be to cancel all previously initiated requests. If Google also stored the initial search string an approach similar to that presented above could work, but it looks like the original search string is not returned after request...
It seems like the only option is to not use the SDK but to use Alamofire to send requests. Those requests can then be easily cancelled. However, I am not sure how good this is...

Cloudkit fetch data (strings and image asset) take a long time to appear after call

I was hoping that someone can help a coding newbie with what might be considered a stupid question. I'm making a blog type app for a community organization and it's pretty basic. It'll have tabs where each tab may be weekly updates, a table view with past updates and a tab with general information.
I setup cloudkit to store strings and pictures, and then created a fetchData method to query cloud kit. In terms of the code (sample below) it works and gets the data/picture. My problem is that it takes almost 5-10 seconds before the text and image update when I run the app. I'm wondering if that's normal, and I should just add an activity overlay for 10 seconds, or is there a way to decrease the time it takes to update.
override func viewDidLoad() {
fetchUpcoming()
}
func fetchUpcoming() {
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: "Upcoming", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
println(results)
for entry in results {
self.articleTitle.text = entry["Title"] as? String
self.articleBody.text = entry["Description"] as? String
let imageAsset: CKAsset = entry["CoverPhoto"] as! CKAsset
self.articlePicture.image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
self.articleBody.sizeToFit()
self.articleBody.textAlignment = NSTextAlignment.Justified
self.articleTitle.adjustsFontSizeToFitWidth = true
}
}
else {
println(error)
}
}
}
Another question I had is about string content being stored on cloud kit. If I want to add multiple paragraphs to a blood entry (for example), is there a way to put it in one record, or do I have to separate the blog entry content into separate paragraphs? I may be mistaken but it seems like CloudKit records don't recognize line breaks. If you can help answer my questions, I'd be really appreciative.
It looks like you might be issuing a query after creating the data, which isn't necessary. When you save data, as soon as your completion block succeeds (with no errors) then you can be sure the data is stored on the server and you can go ahead and render it to the user.
For example, let's say you're using a CKModifyRecordsOperation to save the data and you assign a block of code to the modifyRecordsCompletionBlock property. As soon as that block runs and no errors are passed in, then you can render your data and images to your user. You have the data (strings, images, etc.) locally because you just sent them to the server, so there's no need to go request them again.
This provides a quicker experience for the user and reduces the amount of network requests and battery you're using on their device.
If you are just issuing normal queries when your app boots up, then that amount of time does seem long but there can be a lot of factors: your local network, the size of the image you're downloading, etc. so it's hard to say without more information.
Regarding the storage of paragraphs of text, you should consider using a CKAsset. Here is a quote from the CKRecord's documentation about string data:
Use strings to store relatively small amounts of text. Although
strings themselves can be any length, you should use an asset to store
large amounts of text.
You'll need to make sure you're properly storing and rendering line break characters between the user input and what you send to CloudKit.

Resources