Search code examples
swiftperformancefirebaseobserverssubquery

firebase swift queries in queries performance


I am new to Firebase and relatively new to Swift.

I have firebase set up as below. I have users, followers and blocked users. I take care of the followers in the UITableViewCell class. I am wondering, before I go any further: how does performance get affected by putting observers in observers in queries in queries. (Hope these are the correct terms) . Is below the right way to go about it?(the most efficient way). It works, but also seems to stutter a bit. I appreciate any feedback.

{
  "BlockedByUsers" : {
    "ba1eb554-9a81-4a74-bfd9-484a32eee13d" : {
    "97fee08f-19b2-4eb5-9eab-4b1985c22595" : true
    }
  },
 "Dates" : {
    "1457635040" : {
    "97fee08f-19b2-4eb5-9eab-4b1985c22595" : true
    },
  },
 "Locations" : {
    "97fee08f-19b2-4eb5-9eab-4b1985c22595" : {
    ".priority" : "u14dkwm41h",
    "g" : "u14dkwm41h",
    "l" : [ 51.05521018175982, 3.720297470654139 ]
    },
  },
 "Users" : {
   "97fee08f-19b2-4eb5-9eab-4b1985c22595" : {
   "blockedUsers" : {
      "ba1eb554-9a81-4a74-bfd9-484a32eee13d" : true
    },
   "following" : {
     "51879163-8b35-452b-9872-a8cb4c84a6ce" : true,
    },
   "fullname" : "",
   "dates" : 1457635040,
   "location" : "",
  },
 }
} 

my Swift code with the multiple queries I'm worried about:

var usersRef: Firebase!
var userFollowingRef: Firebase!
var blockedByUsersRef: Firebase!
var datesRef: Firebase!
var geofireEndRef: Firebase!

var geoFireEnd: GeoFire? {

    return GeoFire(firebaseRef: geofireEndRef)
}

var dateRangeStart = Int()
var dateRangeEnd = Int()

override func viewDidLoad(){
    super.viewDidLoad()

    usersRef = DataService.ds.REF_USERS
    userFollowingRef = DataService.ds.REF_CURRENTUSER_FOLLOWING
    blockedByUsersRef = DataService.ds.REF_BLOCKED_BY_USERS
    datesRef = DataService.ds.REF_DATES
    geofireEndRef = DataService.ds.REF_GEOFIREREF_END
}

override func viewWillAppear(animated: Bool){
    super.viewWillAppear(animated)

    if userdefaultsUid != nil
    {
        geoFireEnd!.getLocationForKey(userID, withCallback: { (location, error) in
            if (error != nil)
            {
                print("An error occurred getting the location for \(self.userID) : \(error.localizedDescription)")

            } else if (location != nil)
            {
                self.updateUsersWithlocation(location)

            } else
            {
                print("GeoFire does not contain a location for \(self.userID)")

                self.updateUsersWithoutLocation()
            }
        })
    }
}

func updateUsersWithlocation(location: CLLocation)
{
    var allKeys = [String]()

    let locationQuery = self.geoFireEnd!.queryAtLocation(location, withRadius: 100.0)

    locationQuery.observeEventType(GFEventType.init(0), withBlock: {(key: String!, location: CLLocation!) in

        allKeys.append(key)

        self.datesRef.queryOrderedByKey().queryStartingAtValue(String(self.dateRangeStart)).queryEndingAtValue(String(self.dateRangeEnd)).observeEventType(.ChildAdded, withBlock: {
            snapshot in

            self.users.removeAll(keepCapacity: true)
            self.newKeys.removeAll()
            self.tableView.reloadData()

            for datesKey in snapshot.children
            {
                self.usersRef.childByAppendingPath(datesKey.key!).observeSingleEventOfType(.Value, withBlock: { snapshot in

                    if let key = datesKey.key where key != self.userID
                    {
                        if  allKeys.contains(key!) {

                            let newuser = FBUser(userKey: key!, dictionary: snapshot.value as! [String : AnyObject])

                            self.blockedByUsersRef.childByAppendingPath(key).childByAppendingPath(self.userID).observeSingleEventOfType(.Value, withBlock: { (snapshot) -> Void in

                                if let _ = snapshot.value as? NSNull
                                {
                                    // we have not blocked  this one
                                    self.blockedByUsersRef.childByAppendingPath(self.userID).childByAppendingPath(key).observeSingleEventOfType(.Value, withBlock: { snapshot in

                                        if let _ = snapshot.value as? NSNull
                                        {
                                            // we are not blocked by this one
                                            if self.newKeys.contains(newuser.userKey) {}
                                            else
                                            {
                                                self.users.append(newuser)
                                                self.newKeys.append(newuser.userKey)
                                            }
                                        }
                                        self.tableView.reloadData()
                                    })
                                }
                            })
                        }
                    }
                })
            }
        })
    })
}

In essence users can be at a certain place at a certain date. They put down the date they are going to be there, as explained in code below. that date may overlap with other users that are going to be in that area, in a period ranging of say 7 days before until 21 days after. those users can be followed, blocked. but I’m getting those to display in the tableView. If they put in a different date or place, a different set of users will pop up.

    if let userStartDate = beginningDate as? Double
    {
        let intUserStartDate = Int(userStartDate)

        dateRangeStart = intUserStartDate - 604800

        dateRangeEnd = intUserStartDate + 1814400

        print(dateRangeStart, intUserStartDate, dateRangeEnd)

        updateUsers()
    }
    else
    {
        updateUsersWithoutDate()
    }

Solution

  • This may or may not be an answer or help at all but I want to throw it out there.

    Given that you want to really look for two things: locations and times, we need some mechanics to handle it.

    The locations are more static; i.e. the bowling ally will always be the bowling ally and the times are dynamic and we need a range. So, given a structure

    {
      "events" : {
        "event_0" : {
          "loc_time" : "bowling_5"
        },
        "event_1" : {
          "loc_time" : "tennis_7"
        },
        "event_2" : {
          "loc_time" : "tennis_8"
        },
        "event_3" : {
          "loc_time" : "dinner_9"
        }
      }
    }
    

    This structure handles both criteria. You can easily query for all nodes that have location of tennis at a time of 7. You can also query the range for tennis from start time of 6 and end time of 9, which will return tennis_7 and tennis_8

    Here's some ObjC code to do just that

    Firebase *ref = [self.myRootRef childByAppendingPath:@"events"];
    
    FQuery *query = [[[ref queryOrderedByChild:@"loc_time"] 
                       queryStartingAtValue:@"tennis_6"] queryEndingAtValue:@"tennis_8"];
    
    [query observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
    
        NSLog(@"%@", snapshot);
    
    }];
    

    You can extrapolate from this substituting your locations for location and distance or timestamps for the time.

    Another modification (and this may be obvious but stating it for clarity) is to use a reference to your locations instead of the actual name (bowling, tennis); i.e.

    events
       "event_0" : {
          "loc_time" : "-JAY8jk12998f_20160311140200" // locationRef_timestamp
        },
    
    locations
       -JAY8jk12998f : {
          "loc_name": "Fernando's Hideaway"
       }
    

    Structuring your data in the way to want to get to it can significantly reduce your code (and the complexity of queries within queries etc).

    Hope that helps.