robertmryan / DogWalking

https://stackoverflow.com/a/55989901/1271826
1 stars 0 forks source link

How to format the data to save to firebase and filter the data when querying it from firebase #1

Closed lsamaria closed 5 years ago

lsamaria commented 5 years ago

Hi rob, I looked over the project last night and I was finally able to track every time slot that was selected in the collectionView's data source array. I did mines differently from yours but you definitely got my wheels turning to get it done, Thanks!

What I came to realize is the matter of posting the data to firebase. How would you format the data to save? I don't mean how to physically save it but the json data itself. For eg there are 7 days and 7 slots per day, if someone was to select all 49 slots there could be up to 49 k/v pairs to reflect that.

Since there are 49 different possibilities I can't think of any other way for the k/v pairs other then below.

class PostModel {

    var userModel: UserModel?

    var postId: String?
    var postDate: Double?

    var title:String? // title for post eg "I'm the greatest dog walker alive"
    var description: String? // description of post "I've  walked thousands of dogs in my life ..."

    // if any of these days aren’t nil then the user is avail for that day
    var sunday: String?
    var monday: String?
    var tuesday: String?
    var wednesday: String?
    var thursday: String?
    var friday: String?
    var saturday: String?
    var sunday: String?

    // if any of these are true then that time slot was chosen - 49 (7 * 7) bool values in total
    var sunSixAmToNineAmTimeSlot: Bool?
    var sunNineAmToTwelvePmTimeSlot: Boo?
    var sunTwelvePmToThreePmTimeSlot: Boo?
    var sunThreePmToSixPmETimeSlot: Boo?
    var sunSixPmToNinePmTimeSlot: Boo?
    var sunNinePmToTwelveAmTimeSlot: Boo?
    var sunTwelveAmToSixAmTimeSlot: Boo?
    // etc etc with every time slot from sun - sat following the same order

       // OR

    // if any of these are true then that time slot was chosen - 98 (7 * 14) Ints in total 
    var sunSixAmToNineAmStartTime: Int?
    var sunSixAmToNineAmEndTime: Int?
    var sunNineAmToTwelvePmStartTime: Int?
    var sunNineAmToTwelvePmEndTime: Int?
    var sunTwelvePmToThreePmStartTime: Int?
    var sunTwelvePmToThreePmEndTime: Int?
    var sunThreePmToSixPmStartTime: Int?
    var sunThreePmToSixPmEndTime: Int?
    var sunSixPmToNinePmStartTime: Int?
    var sunSixPmToNinePmEndTime: Int?
    var sunNinePmToTwelveAmStartTime: Int?
    var sunNinePmToTwelveAmEndTime: Int?
    var sunTwelveAmToSixAmStartTime: Int?
    var sunTwelveAmToSixAmEndTime: Int?
    // etc etc with every time slot from sun - sat following the same order

    init(userModel: UserModel?, dict: [String:Any]) {
            self.userModel = userModel
           // init the properties with the dict values
     }
}

And then there is the matter of filtering. The days and hours it should or shouldn't appear in the feed are reflective to what's inside the postModel

let postsRef = Database().database.reference().child("posts")

postsRef.observe( .value, with: { (snapshot) in

    guard let postDict = snapshot.value as? [String: Any] else { return }

    // userModel was retrieved earlier
    let postModel = PostModel(userModel: userModel, dict: postDict)

    let currentDayOfWeek = dayOfTheWeek()

    // there are multiple days this has to get compared to
    if currentDayOfWeek != postModel.??? {

        // don't add this post to the array
        return
    }

    let currentTime = Calendar.current.dateComponents([.hour,.minute,.second], from: Date())

    // how to compare the chosen time slots to the current time? There are up to 49 combinations to compare this to
    if currentTime.hour != postModel.??? {
        // don't add this post to the array
        return
    }

    // if it makes this far then the day and the time slots match up so append it to the array to get scrolled
    self.posts.append(postModel)
    self.collectionView.reloadData()
})

func dayOfTheWeek() -> String? {        
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "EEEE"
    return dateFormatter.stringFromDate(self)
}
robertmryan commented 5 years ago

I wrote a dictionary computed property with the intention that this is what would be saved in Firebase. Just those three keys.

So, if the user selected 2 “availability” slots, you have two records, each with those three key/value pairs.

lsamaria commented 5 years ago

I'm using Firebase Database, wouldn't the previous keys just get written over by the following keys?

For eg the user picks Sunday 6am - 9am and 12pm - 3pm and they also pick Monday 6am - 9am and 12 pm - 3pm

Since the keys are: "dayOfWeek", "startTime", and "endTime" there doesn't seem to be anything that differentiates Sunday from Monday nor the hours.

var dict = [String: Any]()
dict.updateValue("Sunday", forKey: "dayOfWeek")
dict.updateValue(6, forKey: "startTime")
dict.updateValue(9, forKey: "endTime")
dict.updateValue(12, forKey: "startTime") // this just wrote over 6
dict.updateValue(15, forKey: "endTime") // this just wrote over 9
dict.updateValue("Monday", forKey: "dayOfWeek" ) // this just wrote over Sunday
dict.updateValue(6, forKey: "startTime")  // this just wrote over 12 from Sunday
dict.updateValue(9, forKey: "endTime")  // this just wrote over 15 from Sunday
dict.updateValue(12, forKey: "startTime") // this just wrote over 6
dict.updateValue(15, forKey: "endTime") // this just wrote over 9

someDatabaseRef.updateChildValues(dict)

And in Firebase Database:

@root
  |
  @someRef
     |-"dayOfWeek" : "Monday"
     |-"endTime" : 15
     |-"startTime" : 12
robertmryan commented 5 years ago

I’m suggesting a Firebase structure that mirrors the model of the app. Each entry (e.g. Sun 6-9, Sun 12-15, Mon 6-9, Mon 12-15) will be saved as a separate entry (not just updates of the existing entry). In my app, each selected row in the table is an additional entry in the array of Availability for what the user selected. It’s analogous to a calendar database, where each event on the calendar is a new record. What appears on your calendar is an array of events, presented in a grid. Same thing here. Or at least that’s how I’d do it.

I’m suggesting that you move away from the single record representing all of the windows of availability, but rather an array of the selected “slots” of availability. It seems to be the most flexible approach. This easily supports an evolution in your implementation. E.g. what if you later decide to go to two hour windows? Or one hour windows? You don’t want to have to refactor your data model every time you change what the available slots of time are.

lsamaria commented 5 years ago

Hi rob, I've been thinking this through for the last couple of hours.

Quickly I'm a self taught developer and never worked on a team before. This is my 3rd app I'm building. I never worked with events before so the concept is new to me. I noticed in one of your comments on SO you said FireStore. I never used SQL before (limitations of self taught) and I know it offers more flexibility, from what I've read FireStore is more similar to that but I always use the Database.

I'm having a hard time understanding how the startTime and endTime won't get written over in the db or better yet in the dictionary itself before it's initially uploaded to the db

robertmryan commented 5 years ago

No, it’s not a Firestore vs Realtime Database issue. It’s just that you’re updating the same “availability slot” repeatedly. When the user selects a new slot, you want to add a record:

func save(in path: String, values: [String: Any], completion: @escaping (Result<String, Error>) -> Void) {
    let ref = Database.database().reference()
    let child = ref.child(path).childByAutoId()
    child.updateChildValues(values) { error, reference in
        guard let key = reference.key else {
            completion(.failure(error!))
            return
        }

        completion(.success(key))
    }
}

And when the user deselects a slot, you want to remove it:

func remove(in path: String, key: String, completion: @escaping (Result<String, Error>) -> Void) {
    let ref = Database.database().reference()
    ref.child(path).child(key).removeValue { error, reference in
        if let error = error {
            completion(.failure(error))
        } else {
            completion(.success(reference.key!))
        }
    }
}

That will yield something like the following (where I have the app running in simulator in the foreground, but behind it is the web Firebase console so I can watch what’s going on in the database):

firebase in action

The web-based console is a bit laggy on deletions (I think they do that so you have a chance to see what’s about to be deleted, in red, before it just plain disappears), but ignore that because the actual database interaction is pretty quick. In the above, I refactored it so a tap on a row submits the deletion, but the UI is not updated until the database reports the insertion/deletion, so you can see it’s plenty responsive. You can actually delete right from Firebase and you’ll see it reflected in the app, too.

See Firebase’s Working with Lists of Data on iOS for guidance on how to add/remove items to a list and setting up observers as the server list changes.

I’ve created a branch with Firebase Realtime Database.

lsamaria commented 5 years ago

Hey rob, I was busy with some personal matters, I couldn't get back to your right away. Now I understand what you meant. I didn't realize each day and time slot was being saved under it's own node. That makes things easier. Now I see how everything can still use the same start time and end time keys.

Thank you very much for your help!

And by the way that video graphic came out really well, I didn't notice the pause/lag until you mentioned it. I know firebase is super lightening quick.

lsamaria commented 5 years ago

When I finish the app I'll send you a link to see it action :)