简体   繁体   中英

Swift iOS - How to put timer on Firebase TransactionBlock to prevent it from increasing within a certain time period

Every time different users post something (let's say a color) I get the color they posted, the postID, their userId, the date in secs, and how many times that post was viewed.

A different user can look through a tableView and view different cells with every color that every user posted .

Every time that user who is looking taps didSelectRow to view a detail view of the color I run a Firebase TransactionBlock that increases a views count property to show how many times that particular color/cell was tapped.

For eg if the user scrolls through a tableView and see's a blueCell, a label will be on it that says views: 10 (meaning it was viewed 10 times). If that user presses that blueCell again then the views count will go show views: 11 .

The problem is if that user presses that cell repeatedly then they can increase the count on that views label in matter of seconds.

How can I keep track of every object/cell that the user taps and put a timer on it so that they can't update the views count for that particular object for possibly another hour or so? I have the date in secs and postId which are unique to each object.

Basically if the user presses the blueCell at 12pm the views count for the object associated with that particular cell will go up to 11 but if they press it again anytime in between 12pm - 1pm it won't go up. After 1pm if they press it again it the views count for that object will go up to 12?

The model object and the properties I can use to identify each color object:

class ColorClass{
    var color: String?
    var postID: String?
    var userId: String?
    var date: NSNumber?
    var views: NSNumber? // keeps track of how many the post was viewed
}

TableView's didSelectRow:

// the current user who is pressing the cell
let currentUserID = Auth.auth().currentUser?.uid
var colors = [ColorClass]() // 500 model objects

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return colors.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ColorsCell", for: indexPath) as! ColorsCell

    cell.viewsLabel.text = colors[indexPath.row].views // I separately convert this from a NSNumber to a String
    cell.colorLabel.text = colors[indexPath.row].color

    return cell
}

// pressing the cell will increase the count on the object's views property
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    guard let indexPath = tableView.indexPathForSelectedRow else { return }

    // the userId on the object of the cell that was pressed
    guard let userID = colors[indexPath.row].userId else { return }
    guard let postID = colors[indexPath.row].postId else { return }

    // make sure the current user can't update the views on their own post
    if currentUserID != userID{

        let viewsRef = databaseRef?.child(userID).child(postID).child("views")

        viewsRef?.runTransactionBlock({
            (currentData: MutableData) -> TransactionResult in

            let newValue: Int

            guard let existingValue = (currentData.value as? NSNumber)?.intValue else {
                return TransactionResult.abort()
            }

            newValue = existingValue + 1

            currentData.value = NSNumber(value: newValue)

            return TransactionResult.success(withValue: currentData)

        }, andCompletionBlock: {
            (error, completion, snap) in

            print(snap as Any)

            if !completion{
                print("The value wasn't able to update")
                print(error?.localizedDescription as Any)
            }else{
                print("The value updated")
            }
        })
    }
}

Just an idea.

I thought about creating another object that would have a currentUserID, postID, and tappedTime properties. Then I would create a singleton. Every time a cell is pressed I'd pass the data into the object then send the object over to an array in the singleton. In there I'd have a currentTime property. First I'd check if the postID is in the array and if so I'd compare the tappedTime to the currentTime + 1 hour to decide if the views count should get increased. I'd have a dispatch asynch timer and after 1 hour it would automatically get purged from the array. I'm not sure how practical it is though.

You could create a typealias consisting of whatever the object is you're populating your cells with and a Date at the top of your view controller, like so:

  typealias ColorLastSelected = (colorClass: ColorClass, timeSelected: Date)

Then, create an array to store the ColorLastSelected objects.

  var selectedColors: [ColorLastSelected] = []

From there, in didSelectRow , you could do a guard statement to check if an object is contained within the selectedColors array. If not, then do whatever it is you've got to do and at the end, initialize a ColorLastSelected object and append it to the selectedColors array.

In terms of keeping the selectedColors up to date, you could run an update method on a repeating timer to remove ColorLastSelected s that are over 1 hour old. Alternatively, you could just filter the selectedColors array before the guard statement to remove stuff that's over an hour old. If you're going to be jumping around between view controllers, you may need to create a singleton that "stays alive" or you could persist the selectedColors array somewhere

The idea I had at the bottom of the question worked.

I basically made a ViewsTrackingObject with a property specifically for the postId

I then made a singleton that adds the viewsTrackingObject to an array, checks to see if its in the array, if not add it to the array, then remove it from the array after xxx secs.

For this example I set it to 15 secs inside step 9: .now() + 15 but if I wanted it for an hour I would change it to .now() + 3600 .

I find it easier to explain things in steps. There are 0 - 21 steps. I listed the steps as commented out code above each corresponding piece of code starting at the top of the Tracker class with step 0 and it ends the bottom of didSelectRow with step 21

ViewsTrackingObject:

class ViewsTrackingObject{
    var postId: String?
}

Singleton Class:

class Tracker{

    static let sharedInstance = Tracker()

    var viewsObjects = [ViewsTrackingObject]()
    var updateCount = false // 0. need to access this inside didSelectRow (step 17 )to find out wether or not to update the number of views. This would set to true in step 3 below

    func checkForObjectInArray(object: ViewsTrackingObject){

        // 1. check to see if the object is in the array. If it is return true if not return false. Use dot notation to compare the postId on the viewsTrackingObject vs what's inside the array to find out if it exists
        let boolVal = viewsObjects.contains(where: {$0.postId == object.postId})

        // 2. if the object is NOT inside the array then append to the array and then add it to the function that will remove it from the array in whatever secs you specify from the moment it's added. I specified 15 secs
        if !boolVal{

            updateCount = true // 3. change this to true which means in didSelectRow in step 18 return TransactionResult.success(withValue: currentData) will run
            viewsObjects.append(object) // 4. add it to the above array property
            removeObjectFromArray(object) // 5. will remove the viewsTrackingObject passed into the object parameter above in 15 secs from now. Look at step 9
        }
    }

    // 6. this is called above when an object is appended to the array
    func removeObjectFromArray(_ object: ViewsTrackingObject){

        // 7. even though the object should definitely be inside the array double check. If it's in there return true if not return false 
        let boolVal = viewsObjects.contains(where: {$0.postId == object.postId})

        // 8. if the object is in the array which mean the boolVal is true then proceed to step 9
        if boolVal{

            // 9. Fire off in 15 secs from now
            DispatchQueue.main.asyncAfter(deadline: .now() + 15) {

                // 10. find the index of the viewsTrackingObject inside the array
                if let index = self.views.index(where: {$0.postId == viewsModel.postId}){

                    // 11. remove the viewsTrackingObject at the corresponding index from the array
                    self.viewsObjects.remove(at: index)
                    print("++++SUCCESS OBJECT REMOVED++++") // in 15 secs these print statements will print to the console
                    print("----viewsObjects count: \(views.count)")
                    print("....viewsObjects items: \(views.description)")
                }
            }
        }
    }
}

The class that contains the tableView. Declare a property for the Tracker's sharedInstance so everything runs through the Singleton class

// 12. This is declared as a class property and it's used in didSelectRow. Its the Singleton Class
let tracker = Tracker.sharedInstance

let currentUserID = Auth.auth().currentUser?.uid // the current user who is pressing the cell
var colors = [ColorClass]() // 500 model objects


func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    guard let indexPath = tableView.indexPathForSelectedRow else { return }

    // 14. Get the postId of the colorObject corresponding to the tapped cell
    guard let postID = colors[indexPath.row].postId else { return }
    guard let userID = colors[indexPath.row].userId else { return } // the userId on the object of the cell that was pressed. This is used as a child in the databaseRef below to update the user's view's property

    // make sure the current user can't update the views on their own post
    if currentUserID != userID{

        // 15. Create a ViewsTrackingObject and set it's postID property to the same postId property from step 14 
        let viewsTrackingObject = ViewsTrackingObject()
        viewsTrackingObject.postId = postID

        // 16. using the tracker's shared instance, call the method to find out if the object is currently inside the Singleton's array
        tracker.checkForObjectInArray(object: viewsTrackingObject)

        let viewsRef = databaseRef?.child(userID).child(postID).child("views")

        viewsRef?.runTransactionBlock({
            (currentData: MutableData) -> TransactionResult in

            let newValue: Int

            guard let existingValue = (currentData.value as? NSNumber)?.intValue else {
                return TransactionResult.abort()
            }

            newValue = existingValue + 1

            currentData.value = NSNumber(value: newValue)

            // 17. check to see if the singleton's updateCount property was set to true in step 3. If is true then proceed to step 18
            if self.tracker.updateCount{

                // 18. reset singleton's updateCount property back false since it was set to true in step 3
                self.tracker.updateCount = false
                print("*****Views Updated")
                return TransactionResult.success(withValue: currentData)
            }

            // 19. if the singleton's updateCount property was false to begin with then the views won't get updated in firebase because the transaction will get aborted
            print("=====Views NOT Updated")
            return TransactionResult.abort()

        }, andCompletionBlock: {
            (error, completion, snap) in

            print(snap as Any)

            if !completion{

                // 20. If something went wrong reset singleton's updateCount property back false
                self.tracker.updateCount = false

                print("The value wasn't able to update")
                print(error?.localizedDescription as Any)
            }else{

                // 21. it's unnecessary but to be on the safe side
                self.tracker.updateCount = false

                print("The value updated")
            }
        })
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM