Michael Williams Michael Williams - 5 months ago 29
iOS Question

Method duplicating items in array on action

I have an array of tasks that gets synced with Firebase. When I call the methods that update the "done" and "completed by" properties (after a user checks off an item), it duplicates all of the tasks in the array (but not on firebase). Basically it calls the method (

fetchAllTasksInTeam
) in
viewDidLoad
that fetches all of the tasks for a given team which is odd because
viewDidLoad
should only be called once. I've also tried to put the methods in
viewDidAppear
and it gives me the same outcome.

When I run the app and update a task, it prints "fetchAllTasksInTeam called" each time I take any action on the task which tells me that
viewDidAppear
and my function are being called again.

class Firebase {

func fetchAllTasksInTeam(ref:FIRDatabaseReference, team:String, completion:(tasksArray:[Task])->()) {
var newTasks = [Task]()
ref.child("tasks").queryOrderedByChild("team").queryEqualToValue(team).observeEventType(.Value, withBlock: { snapshot in
for task in snapshot.children {
let tasks = Task(snapshot: task as! FIRDataSnapshot)
newTasks.append(tasks)
}
print("fetchAllTasksInTeam called")
completion(tasksArray: newTasks)
})
}

func updateTaskDoneBool(ref:FIRDatabaseReference, taskID:String, taskDone:Bool) {
ref.child("tasks").child(taskID).child("done").setValue(taskDone)
}

func updateTaskCompletedBy(ref:FIRDatabaseReference, taskID:String, taskCompletedBy:String) {
ref.child("tasks").child(taskID).child("completedBy").setValue(taskCompletedBy)
}
}



protocol TaskCellDelegate {
func doneHit(cell : TaskCell)
}

class TaskCell : UITableViewCell, BEMCheckBoxDelegate {

var delegate : TaskCellDelegate?

@IBOutlet weak var label: UILabel!
@IBOutlet weak var detailLabel: UILabel!
@IBOutlet weak var _checkBox: BEMCheckBox!



override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: #selector(TaskCell.buttonClicked(_:)))
tap.numberOfTapsRequired = 1
_checkBox.addGestureRecognizer(tap)
_checkBox.userInteractionEnabled = true
_checkBox.onAnimationType = .Fill
_checkBox.offAnimationType = .Fade
}


func buttonClicked(sender:UITapGestureRecognizer) {
delegate?.doneHit(self)

}
}


class TasksTVC: UITableViewController, TaskCellDelegate {


var ref:FIRDatabaseReference!
let fb = Firebase()
var currentUser = FIRAuth.auth()?.currentUser
var cell = TaskCell()
var task:Task!
var user = ""
var team = ""
var service = ""
var position = ""
var _time = [NSDate]()
var nineTimes = [String]()
var noonTimes = [String]()
var fiveTimes = [String]()
var sectionTimes = [String]()
var tasksInSectionArray = [[Task]]()
var convertedTimeString = [String]()
var tasks = [Task]() {
didSet {
updateTableView()
convertTime(sectionTimes)
tableView?.reloadData()
}
}

override func viewDidAppear(animated: Bool) {
fb.fetchCurrentUser(ref, currentUser: currentUser) { (service, position) in
self.service = service
self.position = position
}
fb.fetchAllTasksInTeam(ref, team: team) { (tasksArray) in
print("\(tasksArray.count) tasks")
self.tasks = tasksArray
}
}


override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()

self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 44.0
}


// MARK: - TaskCellDelegate

func doneHit(cell:TaskCell) {
if let ip = tableView.indexPathForCell(cell) {
task = tasksInSectionArray[ip.section][ip.row]
if task.done == false {
cell._checkBox.setOn(true, animated: true)
task.done = true
task.completedBy = user
cell.detailLabel.text = "Completed By: \(task.completedBy)"
cell.label.textColor = UIColor.grayColor()
print("cell checked")
}
else {
cell._checkBox.setOn(false, animated: true)
task.done = false
task.completedBy = ""
cell.detailLabel.text = ""
cell.label.textColor = UIColor.blackColor()
print("cell unchecked")

}
fb.updateTaskDoneBool(ref, taskID: task.id, taskDone: task.done)
fb.updateTaskCompletedBy(ref, taskID: task.id, taskCompletedBy: task.completedBy)
}

}


// Converting time to be shown in sections
func convertTime(dueTimes:[String]) {
let timeFormatter = NSDateFormatter()
timeFormatter.dateFormat = "HH:mm"
if position != "Staff" {
if service == "9AM" {
_time = nineTimes.map{timeFormatter.dateFromString($0)!}
}
else if service == "12PM" {
_time = noonTimes.map{timeFormatter.dateFromString($0)!}
}
else if service == "5PM" {
_time = fiveTimes.map{timeFormatter.dateFromString($0)!}
}
else {
_time = sectionTimes.map{timeFormatter.dateFromString($0)!}
}
}
timeFormatter.dateFormat = "h:mm a"
convertedTimeString = _time.map{timeFormatter.stringFromDate($0)}
}

func updateTableView() {
sectionTimes = Set(tasks.map{$0.dueTime}).sort()
groupTimes()
if service == "9AM" && position != "Staff" {
tasksInSectionArray = nineTimes.map{section in tasks.filter{$0.dueTime == section}}
}
else if service == "12PM" && position != "Staff" {
tasksInSectionArray = noonTimes.map{section in tasks.filter{$0.dueTime == section}}
}
else if service == "5PM" && position != "Staff" {
tasksInSectionArray = fiveTimes.map{section in tasks.filter{$0.dueTime == section}}
}
else {
tasksInSectionArray = sectionTimes.map{section in tasks.filter{$0.dueTime == section}}
}
}


// MARK: - Table view data source

override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return convertedTimeString[section]
}

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return tasksInSectionArray.count
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasksInSectionArray[section].count
}


override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("TaskCell", forIndexPath: indexPath) as! TaskCell
cell.selectionStyle = .None
task = tasksInSectionArray[indexPath.section][indexPath.row]
cell.label.text = task.title
if task.done == true {
cell._checkBox.setOn(true, animated: true)
cell.detailLabel.text = "Completed By: \(task.completedBy)"
cell.label.textColor = UIColor.grayColor()
}
else {
cell._checkBox.setOn(false, animated: true)
cell.detailLabel.text = ""
cell.label.textColor = UIColor.blackColor()

}
doneHit(cell)
cell.delegate = self
return cell
}

Answer

When you call fetchAllTasksInTeam you are setting observeEventType(.Value. It means that whenever that specific branch of your firebase database changes an event will be triggered and the callback that you set with withBlock: { snapshot in .. will be called. Thats why it is printing fetchAllTasksInTeam called.

Therefore, its not fetchAllTasksInTeam that is being called, it's your observers callback.

Solution

You can fix it by reseting your newTasks array every time the callback is triggered. In the current scenario your callback is just pushing data to it without cleaning the old values.

ref.child("tasks").queryOrderedByChild("team").queryEqualToValue(team).observeEventType(.Value, withBlock: { snapshot in
    var newTasks = [Task]() 
    for task in snapshot.children {
        let tasks = Task(snapshot: task as! FIRDataSnapshot)
        newTasks.append(tasks)
    }
    print("callback called")
    completion(tasksArray: newTasks)
})