Retrieving data from Firebase takes long on Swift - ios

I'm currently working on a project which let users to rent extra spaces. You can think clone of airbnb. In this iOS app, I'm using Firebase for Back-end as a Service.
I want to retrieve the flats that satisfy the filtering options like, bed count, bedroom count, Smoking etc. Logically, I'm grouping all flats by time slots first, then fetching the flats satisfies filtered city, then i'm fetching the flats which satisfies the filters. Then I'm iterating all the flats one by one to fetch their images.
My Json tree is like:
filter_flats/FlatCity/FlatID
time_slots/timestamp/flatID:true
flat_images/FlatID
Here is what I'm trying:
import Foundation
import Firebase
import FirebaseStorage
protocol QuerymasterDelegate :class
{
func getFilteredFlats(filter:FilterModel, completion: #escaping ([FilteredFlat]) -> ())
}
class Querymaster:QuerymasterDelegate
{
var flatEndpoint = FIRFlat()
var returningFlats = [FilteredFlat]()
/////////////////////////////////////////////////////////////
//MARK: This method pulls all flats with required timeslot.//
/////////////////////////////////////////////////////////////
internal func getFilteredFlats(filter: FilterModel, completion: #escaping ([FilteredFlat]) -> ()) {
var zamanAraliginaUygunFlatlar = [String]()
FIRREF.instance().getRef().child("time_slots").queryOrderedByKey().queryStarting(atValue: filter.fromDate?.toTimeStamp()).queryEnding(atValue: filter.toDate?.toTimeStamp()).observeSingleEvent(of: .value, with: { (ss) in
for ts in ss.children.allObjects
{
let timeslotFlatObj = ts as! FIRDataSnapshot
let timeslotForFlat = timeslotFlatObj.value as! [String:Bool]
for x in timeslotForFlat
{
if (x.value == true && !zamanAraliginaUygunFlatlar.contains(x.key))
{
zamanAraliginaUygunFlatlar.append(x.key)
}
}
}
/////////////////////////////////////////////////////////
//MARK: This method pulls all flats with filtered city.//
/////////////////////////////////////////////////////////
FIRREF.instance().getRef().child("filter_flats/" + filter.city!).observe(.value, with: { (ss) in
var sehirdekiFlatler = [String:Flat]()
for i in ss.children.allObjects
{
let flt = Flat()
let flatObject = i as! FIRDataSnapshot
let mainDict = flatObject.value as! [String:Any]
flt.userID = (mainDict["userId"] as? String)!
flt.id = flatObject.key
flt.city = filter.city
flt.title = mainDict["title"] as? String
flt.bathroomCount = mainDict["bathroomCount"] as? Int
flt.bedCount = mainDict["bedCount"] as? Int
flt.bedroomCount = mainDict["bedroomCount"] as? Int
flt.internet = mainDict["internet"] as? Bool
flt.elevator = mainDict["elevator"] as? Bool
flt.heating = mainDict["heating"] as? Bool
flt.gateKeeper = mainDict["gateKeeper"] as?Bool
flt.parking = mainDict["parking"] as? Bool
flt.pool = mainDict["pool"] as? Bool
flt.smoking = mainDict["smoking"] as? Bool
flt.tv = mainDict["tv"] as? Bool
flt.flatCapacity = mainDict["capacity"] as? Int
flt.cooling = mainDict["cooling"] as? Bool
flt.price = mainDict["price"] as? Double
flt.washingMachine = mainDict["washingMachine"] as? Bool
sehirdekiFlatler[flt.id] = flt
if(!(zamanAraliginaUygunFlatlar).contains(flt.id))
{
if(filter.bathroomCount == nil || filter.bathroomCount! <= flt.bathroomCount!)
{
if(filter.bedCount == nil || filter.bedCount! <= flt.bedCount!)
{
if(filter.bedroomCount == nil || filter.bedroomCount! <= flt.bedroomCount!)
{
if(filter.internet == false || filter.internet! == flt.internet!)
{
if(filter.elevator == false || filter.elevator! == flt.elevator!)
{
if(filter.heating == false || filter.heating! == flt.heating!)
{
if(filter.gateKeeper == false || filter.gateKeeper! == flt.gateKeeper!)
{
if(filter.parking == false || filter.parking! == flt.parking!)
{
if(filter.pool == false || filter.pool! == flt.pool!)
{
if(filter.smoking! == false || filter.smoking! == flt.smoking!)
{
if(filter.tv! == false || filter.tv! == flt.tv!)
{
if(filter.capacity == nil || filter.capacity! <= flt.flatCapacity!)
{
if(filter.cooling == false || filter.cooling! == flt.cooling!)
{
if(filter.priceFrom == nil || filter.priceFrom! <= flt.price!)
{
if(filter.priceTo == nil || filter.priceTo! >= flt.price!)
{
if(filter.washingMachine == false || filter.washingMachine! == flt.washingMachine!)
{
let filteredFlat = FilteredFlat()
filteredFlat.flatCity = flt.city
filteredFlat.flatID = flt.id
filteredFlat.flatPrice = flt.price
filteredFlat.flatTitle = flt.title
filteredFlat.userID = flt.userID
self.returningFlats.append(filteredFlat)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
////////////////////////////////////////////////////////////////
//MARK: This method pulls thumbnail image for returning Flats.//
////////////////////////////////////////////////////////////////
if self.returningFlats.count > 0
{
for a in self.returningFlats
{
FIRREF.instance().getRef().child("flat_images/" + a.flatID!).observeSingleEvent(of: .value, with: { (ss) in
let dict = ss.children.allObjects[0] as! FIRDataSnapshot
let obj = dict.value as! [String:String]
let flatImageDownloaded = FlatImageDownloaded(imageID: dict.key, imageDownloadURL: obj["downloadURL"]!)
a.flatThumbnailImage = flatImageDownloaded
completion(self.returningFlats)
})
}
}
})
})
}
}
The marked code This method pulls thumbnail image for returning Flats. is usually fetching data slower because of jumping among nodes of flats.
I didn't realized that what i'm doing wrong. Can you suggest more efficient method?
Thanks in advance.

This sounds like you are working with a significant amount of data, and we don't know your current UI but that may be the place to start to help reduce the bandwidth and speed up performance.
For example, in the UI there are three buttons that activate as the user selects the prior one.
Select City <- highlighted and ready to be tapped
Select Bedrooms <-Dimmed and not available yet
Select Internet <-Dimmed and not available yet
When the user taps Select City, they are presented a list of Cities available. this is easy and a fast Firebase query.
They tap Select Bedrooms and select 2. No real query needed here yet since it's a number between 1 and 5 (for example).
They tap Select Internet and this is a Yes/No or maybe None/Dial up/Broadband.
Now here's the key - the Structure.
City0
name: "some city"
Flats
flat_0
location: "City0"
bedrooms: "2"
internet: "Broadband"
filter: "City0_2_Broadband"
flat_1
location: "City0"
bedrooms: "4"
internet: "Dial up"
filter "City0_4_Dial up"
with this, you can search for any city, or any number of bedrooms or any internet as separate queries.
The real power is having the ability to perform an SQL-like 'and' query when you want to get very specific data; which flats are available and 4 bedrooms and dial up and in city0
query for: "City0_4_Dial up"
very fast and flexible and you can build the criteria for the query within the UI which limits the amount of data flowing into the app from Firebase, which makes it very fast.
If this is too far off base, let me know and I will edit and provide another option.

Firebase uses NoSQL database, so the typical querying would be very inefficient.
As far as I understand, your app requires a lot of query for the data, so Firebase itself does not the perfect solution for your app I guess..
Anyway, in the code, you can just request images in Dispatch async block, and that would make the process faster.
Also, fyi, storing images in FirebaseStorage and store the url in the FirebaseDatabase will help you reduce a lot of money per data.

Related

Core data taking time to insert records with fetching entity & set as relationship

I have 2 entities: Components & SurveyReadingWeb that have a one-to-many relationship.
I have saved Components data in core data database; but while saving SurveyReadingWeb -- in a for loop -- I have to fetch Components data entity by passing component id and setting the surveyreadings isComponentexists relationship, like in the code below for 8000 records. This process is taking too much time, nearly 7 minutes. How can I reduce the time?
for item in items {
autoIncrementId = autoIncrementId + 1
print("reading items parsed:- \(item.SurveyReadingId), \(autoIncrementId)")
let reading : SurveyReadingWeb? = NSEntityDescription.insertNewObject(forEntityName: Constants.EntityNames.kSURVEYREADINGWEB, into: self.managedContext) as? SurveyReadingWeb
reading?.surveyId1 = Int32(autoIncrementId)
reading?.surveyId = Int32(item.SurveyId) ?? 0
reading?.readingData = item.CH4
reading?.surveyDateTime = item.SurveyReadingDateTime
reading?.latitude = item.Latitude
reading?.longitude = item.Longitude
reading?.serialNumber = item.SerialNumber
reading?.descriptionText = item.Description
reading?.componentName = item.Description
reading?.componentId = Int32(item.ComponentId) ?? 0
reading?.readingTypeId = Int32(item.ReadingTypeID) ?? 0
reading?.readingTypeDetails = item.ReadingTypeDetails
reading?.currentLatitude = item.CurrentLatitude
reading?.currentLongitude = item.CurrentLongitude
reading?.hdop = item.HDOP
reading?.vdop = item.VDOP
reading?.componentDistance = item.ComponentDistance
reading?.facilityId = Int32(item.ProjectId) ?? 0
reading?.posted = 1
reading?.readyToSync = 1
reading?.isComponentExists = DatabaseManager.sharedManager.getCompNameFromID(compID: Int32(item.ComponentId) ?? 0)
}
saveContext()
func getCompNameFromID(compID : Int32) -> Components? {
var component : Components? = nil
let fetchRequest : NSFetchRequest<Components> = Components.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "componentId == %ld", compID)
do {
let searchResults = try managedContext.fetch(fetchRequest)
component = (searchResults.first) ?? nil
} catch {
}
return component ?? nil
}
You can reduce the time dramatically if you add an NSCache instance to cache compID:Components key value pairs.
In getCompNameFromID check if the compID exists in the cache. If yes get the value from the cache (very fast), if not fetch the record and save it for the compID in the cache.
And use the convenient SurveyReadingWeb(context: initializer to get rid of all the ugly question marks, however the performance benefit is rather tiny.

How to select a property of a struct dynamically to pass as a parameter in a function in swift 4?

I am trying to lessen the number of lines in a function that I wrote. In order to achieve this I need to select a property of a struct based on some condition and pass that property as a parameter in order to compare that property among other elements in an array that are of the same struct type.
Here is my gigantic code with repetition of same thing for different properties.
I have tried the following the code. Although it works as a charm but I feel the same work has been done a number of times.
func checkContentsForMatching(forIndex: Int) {
var cards = [SetCard]()
for index in game.indicesOfChosenCards.indices {
cards.append(cardTitles[testCards[game.indicesofChosenCards[index]]]!)
}
if (cards[0].color == cards[1].color) && (cards[1].color == cards[2].color) && (cards[0].color == cards[2].color) {
game.playingCards[game.indicesOfChosenCards[0]].color = true
game.playingCards[game.indicesOfChosenCards[1]].color = true
game.playingCards[game.indicesOfChosenCards[2]].color = true
}
else if cards[0].color == cards[1].color {
game.playingCards[game.indicesOfChosenCards[0]].color = true
game.playingCards[game.indicesOfChosenCards[1]].color = true
}
else if cards[1].color == cards[2].color {
game.playingCards[game.indicesOfChosenCards[1]].color = true
game.playingCards[game.indicesOfChosenCards[2]].color = true
}
else if cards[0].color == cards[2].color {
game.playingCards[game.indicesOfChosenCards[0]].color = true
game.playingCards[game.indicesOfChosenCards[2]].color = true
}
if (cards[0].shape == cards[1].shape) && (cards[1].shape == cards[2].shape) && (cards[0].shape == cards[2].shape) {
game.playingCards[game.indicesOfChosenCards[0]].shape = true
game.playingCards[game.indicesOfChosenCards[1]].shape = true
game.playingCards[game.indicesOfChosenCards[2]].shape = true
}
else if cards[0].shape == cards[1].shape {
game.playingCards[game.indicesOfChosenCards[0]].shape = true
game.playingCards[game.indicesOfChosenCards[1]].shape = true
}
else if cards[1].shape == cards[2].shape {
game.playingCards[game.indicesOfChosenCards[1]].shape = true
game.playingCards[game.indicesOfChosenCards[2]].shape = true
}
else if cards[0].shape == cards[2].shape {
game.playingCards[game.indicesOfChosenCards[0]].shape = true
game.playingCards[game.indicesOfChosenCards[2]].shape = true
}
What you're looking for is Swift KeyPath.
In my Sets implementation (Zotz!), this is what I do. First, I use enums with Int raw values for the four card attributes. This allows me to represent any attribute as a common type (Int). I express those raw values as computed properties, and I vend a list of all four computed properties as an array of key paths (note, this is Swift 5):
public struct Card: Codable, CustomStringConvertible {
let itsColor : Color
let itsNumber : Number
let itsShape : Shape
let itsFill : Fill
var itsColorRaw : Int { return itsColor.rawValue }
var itsNumberRaw : Int { return itsNumber.rawValue }
var itsShapeRaw : Int { return itsShape.rawValue }
var itsFillRaw : Int { return itsFill.rawValue }
public static let attributes : [KeyPath<Card, Int>] = [
\itsColorRaw, \itsNumberRaw, \itsShapeRaw, \itsFillRaw
]
// ...
}
OK, so now the rule for whether a given triple of cards constitutes a valid match is easy to express:
private func isCorrectCards(_ cards:[Card]) -> Bool {
func evaluateOneAttribute(_ nn:[Int]) -> Bool {
let allSame = (nn[0] == nn[1]) && (nn[1] == nn[2])
let allDiff = (nn[0] != nn[1]) && (nn[1] != nn[2]) && (nn[2] != nn[0])
return allSame || allDiff
}
return Card.attributes.allSatisfy { att in
evaluateOneAttribute(cards.map{$0[keyPath:att]})
}
}
Even more elegant version suggested by #vacawama:
private func isCorrect(cards:[Card]) -> Bool {
return Card.attributes.allSatisfy { att in
Set(cards.map{$0[keyPath:att]}).count != 2
}
}
Not sure what you are trying to do but if you are trying to genericify the if-s and else-s I would do something like this:
var allColorsMatch = cards[0].color == cards[1].color) && (cards[1].color == cards[2].color) && (cards[0].color == cards[2].color
var zeroAndOneMatch = cards[0].color == cards[1].color
var oneAndTwoMatch = cards[1].color == cards[2].color
var zeroAndTwoMatch = cards[0].color == cards[2].color
game.playingCards[game.indicesOfChosenCards[0]].color = allColorsMatch || zeroAndOneMatch || zeroAndTwoMatch
game.playingCards[game.indicesOfChosenCards[1]].color = allColorsMatch || zeroAndOneMatch || oneAndTwoMatch
game.playingCards[game.indicesOfChosenCards[2]].color = allColorsMatch || zeroAndTwoMatch || oneAndTwoMatch
That's it, you don't need if-s or else-s now for color, you can replace the color if-else blocks with the code above.
Now, you might ask how would you do the same for shape. shape like color can also be true or false as far I can see. So, you might wanna wrap the above code in a function with three parameters and perform the same operations.
You can use Swift's higher order functions to filter your cards array instead of using a for in loop to start.
You should probably create separate model objects to avoid having to make nested subscript calls like cardTitles[testCards[game.indicesOfChosenCards[index]]]!.
Also, why is your color property in game.playingCards[game.indicesOfChosenCards[i]].color a boolean? Seems a bit confusing to have a property named color return a true of false value.
Try breaking up your function into several functions. You're trying to do too much in this one.

Pagination in firebase with identical child values

My data structure is as follows:
users:
user1:
-carModel: evo x
-username: importguy
-region: north east
user2:
-carModel: evo x
-username: evoguy
-region: north east
user3:
-carModel: mustang gt
-username: muscleguy
-region: south east
I want the user to be able to search for a car, say evo, and display results of users who own those particular cars. I need to paginate these results for my app. The problem is I can't figure out how to properly query this. Here is what i have so far.
func fetchUsersBy(car: String) {
if self.carCurrentKey == nil {
let ref = USER_REF.queryOrdered(byChild: "carModel").queryStarting(atValue: car).queryLimited(toFirst: 3)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let snap = snapshot.children.allObjects as? [FIRDataSnapshot] else { return }
guard let last = snapshot.children.allObjects.last as? FIRDataSnapshot else { return }
snap.forEach({ (snapshot) in
guard let userDict = snapshot.value as? Dictionary<String, AnyObject> else { return }
guard let carModel = userDict["carModel"] as? String else { return }
if carModel.contains(car) {
print(snapshot)
}
})
self.carCurrentKey = last.key
self.carCurrentValue = last.childSnapshot(forPath: "carModel").value as? String
})
} else {
// where to start next query?
let ref = USER_REF.queryOrder(byChild: "carModel").queryStarting(atValue: self.carCurrentValue)
}
}
I have to order the query by carModel, in order to group all of the users with that particular car type together in a snapshot. Since all the car models are the same value, I cannot figure out where to start or end the next query for the pagination. Using the reference i have in the else block starts the query at the same place as the block above. Any help or advice would be much appreciated.
I considered doing a fan out, and making a separate structure for car types. This would be difficult though.
For both startAt() and endAt(), you can pass a second value, childKey as shown here.
So your query will look something like this:
let ref = USER_REF.queryOrdered(byChild: "carModel").queryStarting(atValue: self.carCurrentValue, childKey: self.carCurrentKey).queryLimited(toFirst: 3+1)
Note that I used toFirst: 3+1. That's because, annoyingly, startAt() is inclusive and there's no way to skip the first record. So, since we started from the last record retrieved on the previous page, you will want to query for one extra record and discard the first result.
Here's a more complete example in JavaScript. Not familiar enough to translate this to Swift, but it should give you the algorithm in completion.
class Cursor {
constructor(baseRef, pageSize) {
this.baseRef = baseRef;
this.lastKey = null;
this.lastValue = null;
this.pageSize = pageSize;
}
next() {
let ref = this.baseRef;
if( this.lastValue !== null ) {
// a previous page has been loaded so get the next one using the previous value/key
// we have to start from the current cursor so add one to page size
ref = ref.startAt(this.lastValue, this.lastKey).limitToFirst(this.pageSize+1);
}
else {
// this is the first page
ref = ref.limitToFirst(this.pageSize);
}
return ref.once('value').then(snap => {
const keys = [];
const data = []; // store data in array so it's ordered
snap.forEach(ss => {
data.push(ss.val());
keys.push(ss.key);
});
if( this.lastValue !== null ) {
// skip the first value, which is actually the cursor
keys.shift();
data.shift();
}
// store the last loaded record
if( data.length ) {
const last = data.length - 1;
this.lastKey = keys[last];
this.lastValue = data[last].author;
}
return data;
});
}
}
And here's a working fiddle.
Keep in mind that this is a realtime data stream. So pagination is tricky. It's generally easier to just do infinite scroll than to try and maintain a realistic cursor on a moving data set (records can reorder when data changes, get deleted, added in the middle, etc).

Couchbase lite, Search query taking very long time

When I try to search the couchbase documents of size around 10K, the searching is taking very long time. Below are the code snippet. Can anyone optimize it or suggest me any alternative approach. Thank you.
1) Search function
func search(keyword:String) -> [[String:AnyObject]] {
var results:[[String:AnyObject]]=[]
let searchView = database.viewNamed(AppConstants().SEARCH)
if searchView.mapBlock == nil {
startIndexing()
}
let query = searchView.createQuery()
var docIds = Set<String>()
let result = try query.run()
while let row = result.nextRow() {
let key = "\(row.key)"
let keyArr = keyword.characters.split(" ")
for (index, element) in keyArr.enumerate() {
let keyItem = String(element)
if key.lowercaseString.containsString(keyItem.lowercaseString) {
let value = row.value as! [String:AnyObject]
let id = value["_id"] as? String
if id != nil && !docIds.contains(id!) {
results.append(value)
docIds.insert(id!)
}
}
}
}
}
2) Indexing
func startIndexing() {
let searchView = database.viewNamed(AppConstants().SEARCH)
if searchView.mapBlock == nil {
searchView.setMapBlock({ (doc, emit) in
let docType = doc[AppConstants().DOC_TYPE] as! String
if AppConstants().DOC_TYPE_CONTACT.isEqual(docType) {
self.parseJsonToKeyValues(doc)
for value in self.fields.values {
emit(value, doc)
}
self.fields.removeAll()
}
}, version: "1")
}
}
self.parseJsonToKeyValues(doc) will return me the key value store of my documents to index.
You're emitting the entire document along with every field for your view. This could easily cause your queries to be slow. It also seems unlikely you want to do this, unless you really need to be able to query against every field in your document.
It's considered best practice to set your map function right after opening the database. Waiting until right before you query may or may not slow you down.
See https://developer.couchbase.com/documentation/mobile/current/guides/couchbase-lite/native-api/view/index.html for more, especially the section labeled "Development Considerations".

How to do Abstract Entities in Realm.io in Swift

I am converting from CoreData to Realm.io I did a little experiment to see how Realm.io handles situations where I need to have subclasses of a class that is an RLMObject.
Model
import Realm
#objc enum RecurrenceEnum : Int {
case Daily = 1
case Weekly = 2
case Monthly = 3
}
class Challenge: RLMObject {
dynamic var title = ""
}
class TotalCountChallenge: Challenge {
dynamic var totalCountGoal: Int = 0
}
class RecurringChallenge: Challenge {
dynamic var recurranceType: RecurrenceEnum = .Daily
dynamic var totalCountGoal: Int = 0
}
When I save either a TotalCountChallenge or A RecurringChallenge it reports no errors but when I go to query for challenges by title I don't get anything back.
Query from my ViewController
// Query using an NSPredicate object
let predicate = NSPredicate(format: "title BEGINSWITH %#", "Booya")
var challenges = Challenge.objectsWithPredicate(predicate)
if challenges == nil || challenges.count == 0 {
let tcChallenge = TotalCountChallenge()
tcChallenge.title = "Booya Total Count Challenge"
tcChallenge.totalCountGoal = 1_000_000
let rChallenge = RecurringChallenge()
rChallenge.title = "Booya Recurring Challenge"
rChallenge.recurranceType = .Weekly
rChallenge.totalCountGoal = 2_000_000
let realm = RLMRealm.defaultRealm()
// You only need to do this once (per thread)
// Add to the Realm inside a transaction
realm.beginWriteTransaction()
realm.addObject(tcChallenge)
realm.addObject(rChallenge)
realm.commitWriteTransaction()
}
challenges = Challenge.objectsWithPredicate(predicate)
if challenges != nil && challenges.count > 0 {
for challenge in challenges {
let c = challenge as! Challenge
println("\(c.title)")
}
} else {
println("No Challenges found")
}
challenges = TotalCountChallenge.objectsWithPredicate(predicate)
if challenges != nil && challenges.count > 0 {
for challenge in challenges {
let c = challenge as! Challenge
println("TotalCountChallenge: \(c.title)")
}
} else {
println("No Total Count Challenges found")
}
challenges = RecurringChallenge.objectsWithPredicate(predicate)
if challenges != nil && challenges.count > 0 {
for challenge in challenges {
let c = challenge as! Challenge
println("RecurringChallenge \(c.title)")
}
} else {
println("No Recurring Challenges found")
}
Output
No Challenges found
TotalCountChallenge: Booya Total Count Challenge
RecurringChallenge Booya Recurring Challenge
When I look at the database using the Browse Tool provided by Realm I see that there is only 1 TotalCountChallenge and 1 RecurringChallenge and there are no Challenges
Is there a way to do this?
Here is a link to the code in github: lewissk/RealmTest
Realm supports subclassing, but not the sort of polymorphism that you're looking for. In Realm, each object type is stored in its own table, regardless of whether or not you've declared it in code as a subclass of another object. The implication of this is that isn't not currently possible to query across different object classes, even if they share a common superclass. There's an issue tracking this feature request at https://github.com/realm/realm-cocoa/issues/1109.

Resources