TableView lagging too much and taking up too much memory Swift - ios

I have an Application that uses a UITableView inside of a UIViewController, and the way each cell gets formed is, the information about the Issues, or Blog Posts, are stored in CoreData after they are downloaded. But when I ran the instruments tool, I found out that everytime you scroll down or up it fetches that CoreData. Even if the cell was already initialized, so this combined with apparently NSDate takes up a look of Memory as well. The program is taking up around 40m of ram and around 60% of CPU when you scroll down! And I have looked for answers of how to only initialize it once but could find it anywhere. Here it my TableView code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("tableviewcell", forIndexPath: indexPath) as! BlogTableViewCell
let sortedPosts = SortPosts()
let realresults = sortedPosts.sortData()
if realresults.count > 0 {
println(cell)
let date = realresults[indexPath.row].valueForKey("date") as? NSDate
let numberOfTime = PostDateFormatter().getFormattedDate(date!)
let content = realresults[indexPath.row].valueForKey("content") as! String
let title = realresults[indexPath.row].valueForKey("title") as! String
cell.configureCell(Post(title: title.html2String, author: realresults[indexPath.row].valueForKey("author") as! String, date: numberOfTime, content: content.html2String))
tableView.hidden = false
self.view.backgroundColor = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 255.0/255.0)
myIndicator.stopAnimating()
myIndicator.hidden = true
}
else {
if PostServices().isConnectedToNetwork() {
println("Error 0 results returned......")
self.tableView.reloadData()
}
}
return cell
}
and here it SortPosts:
class SortPosts {
func sortData() -> NSArray {
var jsonin = NSArray()
let formatter = NSDateFormatter()
formatter.dateFormat = "Y-MM-dd\'T\'HH:mm:ss"
var managedObjectContext : NSManagedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
var request = NSFetchRequest(entityName: "Post")
request.returnsObjectsAsFaults = false
let sortDescriptor = NSSortDescriptor(key: "date", ascending: false)
request.sortDescriptors = [sortDescriptor]
var results : NSArray = managedObjectContext.executeFetchRequest(request, error: nil)!
return results
}
}
And here is PostDateFormatter:
class PostDateFormatter {
func getFormattedDate(date : NSDate) -> String {
let dateFormatter = NSDateFormatter()
dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
let dateInShortFormat = dateFormatter.stringFromDate(date)
let inter = date.timeIntervalSinceNow
var seconds = Int(abs(inter))
var minutes = seconds/60
var hours = minutes/60
var days = hours/24
var weeks = days/7
if seconds < 60 {
return "\(seconds)s"
} else if minutes < 60 {
return "\(minutes)m"
} else if hours < 24 {
return "\(hours)h"
} else if days < 7 {
return "\(days)d"
} else if weeks <= 8 {
return "\(weeks)w"
} else if weeks > 8 {
return "\(dateInShortFormat)"
}
return "Something went wrong with date formatting"
}
}

You do realize that for each cell you're calling;
let sortedPosts = SortPosts()
let realresults = sortedPosts.sortData()
it would make more sense to init all your data outside of your tableView, or certainly at some point other than cellForRowAtIndexPath.
Build an Array or Dictionary of items elsewhere, then simply reference the items in the array in your cellForRowAtIndexPath. This way, you're only accessing core data once.

What you're saying in your question describes exactly how TableViews work. Each cell is dequeued only when it will be visible on the screen. Otherwise, there are no other cells in memory, and when new cells are displayed, they have to be dequeued and thus require hitting the database and the CPU for information and drawing whenever they are displayed.

If you're up for the challenge I would suggest to use: asyncdisplaykit.
I believe it would help your memory issues.
It was originally built to make Facebook's Paper possible.
http://asyncdisplaykit.org/

Related

How to improve UITableView scrolling when data is calculated on the go in Swift?

Before changes to the way we return cells in the cellForRow: method, I remember I would be able to check if cell == nil, and basically have two separate areas for cell-related-customization (properties that apply to all cells vs. only to some). However with dequeueReusableCell: I am unsure how to handle this and put it off until I was able to get all my cells populated properly -- but seemingly at the expense of a great amount of CPU and Memory usage. Here is my code in cellForRow: currently:
if case (1...(days.count + 1), 2...(hours.count + 2)) = (indexPath.column, indexPath.row) {
let cell = spreadsheetView.dequeueReusableCell(withReuseIdentifier: String(describing: ScheduleCell.self), for: indexPath) as! ScheduleCell
let legs : NSArray = importedDict.object(forKey: "legs") as! NSArray
print(hours![indexPath.row - 2])
var profits : Array<Double> = []
if let singleLegDict2 = legs as? [[String:AnyObject]] {
print (singleLegDict2)
for singleLegDict in singleLegDict2 {
let dict = singleLegDict as NSDictionary
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let datee = dateFormatter.date(from: days![indexPath.column - 1] as! String)!
let datee2 = dateFormatter.date(from: dict.object(forKey: "expiry") as! String)!
let calendar = Calendar.current
// Replace the hour (time) of both dates with 00:00
let date1 = calendar.startOfDay(for: datee)
let date2 = calendar.startOfDay(for: datee2)
let components = calendar.dateComponents([.day], from: date1, to: date2)
let doub = Double(components.day!)
let years = doub * 0.00273972602
// let under : NSNumber = dict.object(forKey: "underlying") as! NSNumber
let strike : NSNumber = dict.object(forKey: "strikePrice") as! NSNumber
let interest1 : NSNumber = dict.object(forKey: "interest") as! NSNumber
var interest2 = interest1 as! Double
interest2 = interest2/100
let userIV : NSNumber = NSNumber(value: Double(dict.object(forKey: "userIV") as! String)!)
var userIV2 = userIV as! Double
userIV2 = userIV2/100
var type = 0
if dict.object(forKey: "putCall") as! String == "CALL" {
type = 0
} else {
type = 1
}
let theo = getTheoreticalOptionPrice(forUnderlyingPrice: hours![indexPath.row - 2] as! NSNumber, withStrike: strike, withInterestRate: NSNumber(value: interest2), withVolatility: NSNumber(value: userIV2), withTimeToExpirationInYears: NSNumber(value: years), withType: NSNumber(value: type))
let userC : NSNumber = NSNumber(value: Double(dict.object(forKey: "userCost") as! String)!)
let quan : NSNumber = dict.object(forKey: "quantity") as! NSNumber
let diff = (theo - (userC as! Double)) * (quan as! Double)
print(theo, (userC as! Double))
profits.append(diff)
}
}
print(profits)
var sumedArr : Double = profits.reduce(0, {$0 + $1}) as Double
sumedArr = sumedArr * 100
if sumedArr < 0 {
cell.backgroundColor = UIColor(red: 0.8667, green: 0.1804, blue: 0.1804, alpha: 1.0)
} else {
cell.backgroundColor = UIColor(red: 0.3843, green: 0.7686, blue: 0.1608, alpha: 1.0)
}
cell.label.text = String(format: "%.0f", sumedArr)
return cell
}
Any ideas on how to speed that up? Is the only way going to be preloading the data and then only referencing it in cellForRow:? How would I go about addressing this situation given the calculations I am doing per cell?
I don’t need the cells to reload their data every time they are scrolled to. If all the cells load their data once when the view loads, and that data was just displayed — that would be perfect.
With that in mind, I tried adding a check to see if the cell’s label was populated or not, and only if not, do all those calculations (assuming this would be the first loading of the table view). While that improved scrolling performance, the cells weren’t properly displaying and were completely out of order. Help?
Update: I have moved the heavy calculations to a background thread and now experience smooth scrolling. However, the cells still flicker because they are still regenerating their data each time they are shown. What is the easiest way to cache the cells so that they load once when the tableView is shown, and then scroll smoothly because they reference already loaded data?
I do not understand why it is so important for the performance of your app to know whether a tableView cell has been initialized newly or is re-used. But if so, you can handle it as earlier. The docs say:
If you registered a class for the specified identifier and a new cell
must be created, this method initializes the cell by calling its
init(style:reuseIdentifier:) method. For nib-based cells, this method
loads the cell object from the provided nib file. If an existing cell
was available for reuse, this method calls the cell’s
prepareForReuse() method instead.
So you should be able to distinguish both cases and respond accordingly.
Using JavaScriptCore to calculate data for each cell in cellForRow was slowing down my tableView beyond fixing. The only alternative was to cache the data in an Array beforehand and then display it. However, for my use-case, this was not an option.
I rewrote the JavaScript in Swift and the scrolling improved.

JTAppleCalendar cell background color change error in Swift 3

Im using JTAppleCalendar in my project you can see it here -> https://github.com/patchthecode/JTAppleCalendar. But when I want to change background colors in some cells gives me big problem , when I past previous and next months some cells background colors changing ?
How it is possible ? How can I fix it ? I want to change only; Example ;
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName:"Mains")
let predicate = NSPredicate (format:"date = %#",freshdate)
fetchRequest.predicate = predicate
if let result = try? context.fetch(fetchRequest) as! [Mains] {
for object in result {
if(object.user! == "" < freshdate) {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else if(object.userme! == "") {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "ffffff")
}
}
}
Ones , but when I turn another month , in calendar looking some cells backgrounds changed.
Under below picture shows true when app opened.
But when I past previous or next month changed some cells backgrounds under below. This is mistake. I don't want to change thats.
My codes under below , where I can mistake ?
#IBOutlet weak var calendarView: JTAppleCalendarView!
let kStartDate = "2016-01-01"
let kEndDate = "2049-12-31"
var numberOfRows = 6
let formatter = DateFormatter()
var myCalendar = Calendar(identifier: .gregorian)
var generateInDates: InDateCellGeneration = .forAllMonths
var generateOutDates: OutDateCellGeneration = .off
var hasStrictBoundaries = true
let firstDayOfWeek: DaysOfWeek = .monday
var monthSize: MonthSize? = nil
extension ViewController: JTAppleCalendarViewDelegate, JTAppleCalendarViewDataSource {
func configureCalendar(_ calendar: JTAppleCalendarView) -> ConfigurationParameters {
formatter.dateFormat = "yyyy-MM-dd"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US")
let startDate = formatter.date(from: kStartDate)!
let endDate = formatter.date(from: kEndDate)!
let parameters = ConfigurationParameters(startDate: startDate,
endDate: endDate,
numberOfRows: numberOfRows,
calendar: myCalendar,
generateInDates: generateInDates,
generateOutDates: generateOutDates,
firstDayOfWeek: firstDayOfWeek,
hasStrictBoundaries: hasStrictBoundaries)
return parameters
}
func calendar(_ calendar: JTAppleCalendarView, cellForItemAt date: Date, cellState: CellState, indexPath: IndexPath) -> JTAppleCell {
let cell = calendar.dequeueReusableCell(withReuseIdentifier: "CellView", for: indexPath) as! CellView
let comedate = String(describing: myCalendar.date(byAdding: .day, value: 1, to: cellState.date))
var freshdate = comedate.substring(from: 9)
freshdate = freshdate.substring(to: 10)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName:"Mains")
let predicate = NSPredicate (format:"date = %#",freshdate)
fetchRequest.predicate = predicate
if let result = try? context.fetch(fetchRequest) as! [Mains] {
for object in result {
if(object.user! == "" < freshdate) {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else if(object.userme! == "") {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "ffffff")
}
}
}
handleCellConfiguration(cell: cell, cellState: cellState)
return cell
}
}
I think it's reuse cell issue, you can try set default background color.
if freshdate == "2017-04-16" {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else if freshdate == "2017-04-28" {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "45ca6")
} else {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "your_color")
}
As #Quoc Le says, it is a cell reuse issue. But there is also another issue here.
This function is called for every cell on the screen. So in a nutshell here is the problem that is happening.
func calendar(_ calendar: JTAppleCalendarView, cellForItemAt date: Date, cellState: CellState, indexPath: IndexPath) -> JTAppleCell {
// Here you got one single cell
let cell = calendar.dequeueReusableCell(withReuseIdentifier: "CellView", for: indexPath) as! CellView
// Here you got a result for your single cell
if let result = try? context.fetch(fetchRequest) as! [Mains] {
// Here you are changing the color of a *single* cell multiple times
// Based on the number of [objects] found in the [result]
// Why?
for object in result {
if(object.user! == "" < freshdate) {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else if(object.userme! == "") {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "ffffff")
}
}
} else {
// There will also be a problem here if you data is not found.
// Your cell background color will just pick up any random re-used color
}
}
What you should do is have your DataSource already prepared.
Here is an example:
let myDataSource: [Mains] = []
override func viewDidLoad() {
super.viewDidLoad()
// Load the data source
myDataSource = try? context.fetch(fetchRequest) as! [Mains]
}
Now, Based on what value in your data source, do you want to change a cell's background color?
It looks like you are checking for object.user, object.userme and empty value
Looks like you are checking for arbitrary data.
The cellForItemAt date: Date function maps a cell to a Date, not arbitrary data.
Therefore you must map your DataSource to a Date. This can be done with a Dictionary. Therefore change you DataSource to look something like this:
let myDataSource: [String: Mains ] = [:]
// The String can be a date string
// The Mains object can be the object associated with that Date
now your cellForItem function can look like this:
func calendar(_ calendar: JTAppleCalendarView, cellForItemAt date: Date, cellState: CellState, indexPath: IndexPath) -> JTAppleCell {
// Here you got one single cell
let cell = calendar.dequeueReusableCell(withReuseIdentifier: "CellView", for: indexPath) as! CellView
// Here you got a result for your single cell
let dateString = dateFormatter.stringFrom(date)
let myObjectForThisCell = myDataSource[dateString]
if(myObjectForThisCell.user! == "" < freshdate) {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else if(myObjectForThisCell.userme! == "") {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "ffffff")
}
}
The code I have pasted above is incomplete. But hopefully you get the idea of how you should set this up. The problem you are having is not a calendar problem. It a problem of how a normal UICollectionView or UITableView works.
Finally I resolved issue just I used ;
cell.contentView.backgroundColor = nil
before;
if let result = try? context.fetch(fetchRequest) as! [Mains] {
for object in result {
if(object.user! == "" < freshdate) {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else if(object.userme! == "") {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "f7bca6")
} else {
cell.contentView.backgroundColor = hexStringToUIColor(hex: "ffffff")
}
}
}
And which cell item you want to change color, title, etc.. just add before = nil
After worked, I think will help many people.
Thank you.

Automatically saving changes in a cell to object when editing finishes?

im having a real nightmare with my project where i need to save cell contents to an object, for each object in an array. I cant get this to work by looping through table cells adn array objects and trying to match them all up.
So my next idea was to add didFinishEditing related functions into the cellForRowAt function?
Im not sure this would work either, but this is what i have:
Each row here has a label for the set, a picker for the reps that can be scrolled to a number, and a textfield to put a weight. Then i save each row as an object storing the set, rep and weight.
Issue is when editing this, how can i save these again overwriting the old values? Hence my plan above to use didFinishEditing methods.
My previous plan was the code below, but i cant figure out the annotated part. So i was hoping someone had guidance on how i can approach saying when editing rather than this save button function that doesnt work!
func saveUserExerciseSets() {
if userExercise == nil {
print("CREATING A FRESH SET OF SETS FOR THE NEW EXERCISE")
for cell in self.customSetsTable.visibleCells as! Array<NewExerciseTableViewCell> {
print("SAVING THESE CELLS \(customSetsTable.visibleCells)")
let newUserExerciseSet = UserExerciseSet(context: self.managedObjectContext)
newUserExerciseSet.setPosition = Int64(cell.setNumber.text!)!
newUserExerciseSet.setReps = Int64(cell.repsPicker.selectedRow(inComponent: 0))
newUserExerciseSet.parentExerciseName = self.userExerciseName.text
if self.localeIdentifier == "en_GB" {
let kgWeight = Measurement(value: Double(cell.userExerciseWeight.text!)!, unit: UnitMass.kilograms)
newUserExerciseSet.setWeight = kgWeight as NSObject?
newUserExerciseSet.initialMetricSystem = self.localeIdentifier
} else if self.localeIdentifier == "en_US" {
let lbsWeight = Measurement(value: Double(cell.userExerciseWeight.text!)!, unit: UnitMass.pounds)
newUserExerciseSet.setWeight = lbsWeight as NSObject?
newUserExerciseSet.initialMetricSystem = self.localeIdentifier
}
let fetchRequest: NSFetchRequest<UserExercise> = UserExercise.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %#", self.exerciseNameToAddTo!)
do {
let parentExercise = try self.managedObjectContext.fetch(fetchRequest).first
parentExercise?.addToExercisesets(newUserExerciseSet)
print("SET ADDED TO EXERCISE")
} catch {
print("Fetching Routine Failed")
}
}
} else if self.userExercise != nil {
print("UPDATING EXISTING SETS FOR THE EXISTING EXERCISE")
let cells = self.customSetsTable.visibleCells as! Array<NewExerciseTableViewCell>
for cell in cells {
let exerciseSets = self.userExercise?.exercisesets?.allObjects as! [UserExerciseSet]
let sortedexerciseSets = exerciseSets.sorted { ($0.setPosition < $1.setPosition) }
let cellsSet = sortedexerciseSets //match the sortedexerciseSets set object to the cell index positions
cellsSet.setPosition = Int64(setsCell.setNumber.text!)!
cellsSet.setReps = Int64(setsCell.repsPicker.selectedRow(inComponent: 0))
if self.localeIdentifier == "en_GB" {
let kgWeight = Measurement(value: Double(cell.userExerciseWeight.text!)!, unit: UnitMass.kilograms)
cellsSet.setWeight = kgWeight as NSObject?
} else if self.localeIdentifier == "en_US" {
let lbsWeight = Measurement(value: Double(cell.userExerciseWeight.text!)!, unit: UnitMass.pounds)
cellsSet.setWeight = lbsWeight as NSObject?
}
cellsSet.parentExerciseName = self.userExerciseName.text
}
}
do {
try self.managedObjectContext.save()
print("THE SET HAS BEEN SAVED")
} catch {
fatalError("Failure to save context: \(error)")
}
delegate?.didFinishEditing()
self.dismiss(animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? NewExerciseTableViewCell
else {
fatalError("Unexpected Index Path")
}
cell.backgroundColor = UIColor.customBackgroundGraphite()
cell.textLabel?.textColor = UIColor.white
cell.repsPicker.dataSource = self
cell.repsPicker.delegate = self
configure(cell, at: indexPath)
return cell
}
func configure(_ cell: NewExerciseTableViewCell, at indexPath: IndexPath) {
// configuring cells when theres a loaded exercise causes the issues --------------------
if self.userExercise != nil {
print("RESTORING CELLS FOR THE EXISTING EXERCISE")
let unsortedExerciseSets = self.userExercise?.exercisesets?.allObjects as! [UserExerciseSet]
let exerciseSets = unsortedExerciseSets.sorted { ($0.setPosition < $1.setPosition) }
let cellsSet = exerciseSets[indexPath.row]
cell.setNumber.text = String((indexPath.row) + 1)
let indexRow = Int(cellsSet.setReps)
print("INDEX ROW INT IS \(indexRow)")
cell.repsPicker.selectRow(indexRow, inComponent: 0, animated: true) //fix this crashing issue!
let localeIdentifier = Locale(identifier: UserDefaults.standard.object(forKey: "locale") as! String)
let setWeight = cellsSet.setWeight as! Measurement<UnitMass>
let formatter = MassFormatter()
formatter.numberFormatter.locale = localeIdentifier
formatter.numberFormatter.maximumFractionDigits = 2
if localeIdentifier.usesMetricSystem {
let kgWeight = setWeight.converted(to: .kilograms)
let finalKgWeight = formatter.string(fromValue: kgWeight.value, unit: .kilogram)
let NumericKgResult = finalKgWeight.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
cell.userExerciseWeight.text = NumericKgResult
} else {
let lbsWeight = setWeight.converted(to: .pounds)
let finalLbWeight = formatter.string(fromValue: lbsWeight.value, unit: .pound)
let NumericLbResult = finalLbWeight.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
cell.userExerciseWeight.text = NumericLbResult
}
} else if self.userExercise == nil {
print("NEW SET CELL ADDED FOR FRESH EXERCISE")
cell.setNumber.text = String((indexPath.row) + 1)
}
}
Try something like this to match the setIds correctly. That's where I think the issue is.
for x in sortedexerciseSets {
if x.setPosition == Int64(setsCell.setNumber.text!)! {
//save
}
}
Proper way to do it would be to have an array of those sets (I guess, since you tagged core-data, they are instances of NSManagedObject?). When user does ANY change in the cell (write new value in the text field or scroll to another value for reps) you need to update the approproate object in your array immediately. Then you could call save on NSManagedObjectContext when you're sure you want to save changes, or just call rollback on the context to cancel all changes.

Setting text labels to the difference in time intervals

I'm trying to set a text label to the time difference between two objects; the current time and a saved time in Parse. My issue is with the if/else statement at the end. The labels are all assigned with either the datesHours or datesMinutes dictionaries whereas the datesHours dictionary should be assigned only when hours is greater than 1- otherwise, the text label should show the number of minutes.
What am I doing wrong here?
var datesHours = [Int: String]()
var datesMinutes = [Int: String]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCell") as! TableViewCell
var query = PFQuery(className: "Items")
query.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
if let objects = objects {
for (index, title) in enumerate(objects) {
let itemDate = (title as! PFObject)["date"] as! NSDate
var timeDifference = NSDate().timeIntervalSinceDate(itemDate)
var time = Int(timeDifference)
var minutes = ((time / 60) % 60)
var hours = (time / 3600)
self.datesHours[index] = String(hours)
self.datesMinutes[index] = String(minutes)
//println(self.datesMinutes)
//println(self.datesHours)
for (index, hour) in self.datesHours {
if hour == "\(0)" {
cell.timeLabel.text = self.datesMinutes[indexPath.row]! + "m"
} else {
cell.timeLabel.text = self.datesHours[indexPath.row]! + "hr"
}
}
}
}
}
}
If your data is correct, and you have a set of dictionaries that need to be displayed on your cell. Try this:
cell.textLabel.text = [[mArray objectAtIndex:indexPath.row] objectForKey:#"HOUR OR MINUTE KEY"];

Refactor cellForRowIndexPath in UITableView Swift

I have a rather long cellForRowAtIndexPath function. I am using parse as my backend and have a lot going on. I want to extract a lot of these conditions and put them in their own functions. Especially the PFUser query, but unfortunately I don't know whats the best way to go about it since I don't know how I can access the elements of each cell in those functions I want to write.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PostCells", forIndexPath: indexPath) as! NewsFeedTableCellTableViewCell
// Configure the cell...
// A drive is a post
let drive: PFObject = self.timelineData[indexPath.row] as PFObject
var driverId = drive.objectForKey("driver")!.objectId!
var currentUserObjectId = PFUser.currentUser()!.objectId
if(driverId != currentUserObjectId){
cell.requestButton.layer.borderWidth = 1
cell.requestButton.titleLabel!.font = UIFont.systemFontOfSize(11)
cell.requestButton.tintColor = UIColor.orangeColor()
cell.requestButton.layer.borderColor = UIColor.orangeColor().CGColor
cell.requestButton.setTitle("REQUEST", forState: UIControlState.Normal)
}
else {
cell.requestButton.layer.borderWidth = 1
cell.requestButton.titleLabel!.font = UIFont.systemFontOfSize(11)
cell.requestButton.tintColor = UIColor.grayColor()
cell.requestButton.layer.borderColor = UIColor.lightGrayColor().CGColor
cell.requestButton.setTitle("REQUEST", forState: UIControlState.Normal)
cell.requestButton.enabled = false
}
// Setting up the attributes of the cell for the news feed
cell.driveTitleTextField.text = drive.objectForKey("title") as! String
cell.wayTextField.text = drive.objectForKey("way") as! String
var departureDate = NSDate()
departureDate = drive.objectForKey("departureDate") as! NSDate
var dateFormat = NSDateFormatter()
dateFormat.dateFormat = "M/dd hh:mm a"
cell.departureDateTextField.text = dateFormat.stringFromDate(departureDate)
if((drive.objectForKey("way")!.isEqualToString("Two Way")))
{
var returnDate = NSDate()
returnDate = drive.objectForKey("returnDate") as! NSDate
cell.returningDateTextField.text = dateFormat.stringFromDate(returnDate)
}
else if((drive.objectForKey("way")!.isEqualToString("One Way")))
{
cell.returningDateTextField.enabled = false
cell.returningDateTextField.userInteractionEnabled = false
cell.returningDateTextField.hidden = true
cell.returningLabel.hidden = true
}
var seatNumber = NSNumber()
seatNumber = drive.objectForKey("seatNumber") as! NSInteger
var numberFormat = NSNumberFormatter()
numberFormat.stringFromNumber(seatNumber)
cell.seatNumberTextField.text = numberFormat.stringFromNumber(seatNumber)
// this is a PFUser query so we can get the users image and name and email from the User class
var findDrive = PFUser.query()
var objectId: AnyObject? = drive.objectForKey("driver")!.objectId!
findDrive?.whereKey("objectId", equalTo: objectId!)
findDrive?.findObjectsInBackgroundWithBlock{
(objects:[AnyObject]?, error:NSError?)->Void in
if (error == nil){
if let actualObjects = objects {
let possibleUser = (actualObjects as NSArray).lastObject as? PFUser
if let user = possibleUser {
cell.userProfileNameLabel.text = user["fullName"] as? String
cell.userEmailLabel.text = user["username"] as? String
//Profile Image
cell.profileImage.alpha = 0
if let profileImage = user["profilePicture"] as? PFFile {
profileImage.getDataInBackgroundWithBlock{
(imageData:NSData? , error:NSError?)-> Void in
if(error == nil) {
if imageData != nil{
let image:UIImage = UIImage (data: imageData!)!
cell.profileImage.image = image
}
}
}
}
UIView.animateWithDuration(0.5, animations: {
cell.driveTitleTextField.alpha = 1
cell.wayTextField.alpha = 1
cell.profileImage.alpha = 1
cell.userProfileNameLabel.alpha = 1
cell.userEmailLabel.alpha = 1
cell.seatNumberTextField.alpha = 1
cell.returningDateTextField.alpha = 1
cell.departureDateTextField.alpha = 1
})
}
}
}
}
return cell
}
EDIT 1
I came up with a way to refactor my code that I would like critiqued!
1. I extracted a lot of the cells configurations and put them into to functions, one for the button on the cell and the other for all the data from parse.
func configureDataTableViewCell(cell:NewsFeedTableCellTableViewCell, drive: PFObject)
{
cell.driveTitleTextField.text = drive.objectForKey("title") as! String
cell.wayTextField.text = drive.objectForKey("way") as! String
cell.userEmailLabel.text = drive.objectForKey("username") as? String
cell.userProfileNameLabel.text = drive.objectForKey("name") as? String
var departureDate = NSDate()
departureDate = drive.objectForKey("departureDate") as! NSDate
var dateFormat = NSDateFormatter()
dateFormat.dateFormat = "M/dd hh:mm a"
cell.departureDateTextField.text = dateFormat.stringFromDate(departureDate)
if((drive.objectForKey("way")!.isEqualToString("Two Way")))
{
var returnDate = NSDate()
returnDate = drive.objectForKey("returnDate") as! NSDate
cell.returningDateTextField.text = dateFormat.stringFromDate(returnDate)
}
else if((drive.objectForKey("way")!.isEqualToString("One Way")))
{
cell.returningDateTextField.enabled = false
cell.returningDateTextField.userInteractionEnabled = false
cell.returningDateTextField.hidden = true
cell.returningLabel.hidden = true
}
var seatNumber = NSNumber()
seatNumber = drive.objectForKey("seatNumber") as! NSInteger
var numberFormat = NSNumberFormatter()
numberFormat.stringFromNumber(seatNumber)
cell.seatNumberTextField.text = numberFormat.stringFromNumber(seatNumber)
}
func configureButtonTableViewCell(cell:NewsFeedTableCellTableViewCell, userID: String)
{
var currentUserObjectId = PFUser.currentUser()!.objectId
if(userID != currentUserObjectId){
cell.requestButton.layer.borderWidth = 1
cell.requestButton.titleLabel!.font = UIFont.systemFontOfSize(11)
cell.requestButton.tintColor = UIColor.orangeColor()
cell.requestButton.layer.borderColor = UIColor.orangeColor().CGColor
cell.requestButton.setTitle("REQUEST", forState: UIControlState.Normal)
println("orange")
}
else {
cell.requestButton.layer.borderWidth = 1
cell.requestButton.titleLabel!.font = UIFont.systemFontOfSize(11)
cell.requestButton.tintColor = UIColor.grayColor()
cell.requestButton.layer.borderColor = UIColor.lightGrayColor().CGColor
cell.requestButton.setTitle("REQUEST", forState: UIControlState.Normal)
cell.requestButton.enabled = false
println("gray")
}
}
2. I then passed in the functions from step 1 and into my cellForRowIndexPath
// A drive is a post
let drive: PFObject = self.timelineData[indexPath.row] as PFObject
var driverId : String = drive.objectForKey("driver")!.objectId!!
configureButtonTableViewCell(cell, userID: driverId)
configureDataTableViewCell(cell, drive: drive)
3. I stored all my PFUser data into my object when its saved instead of querying the user class. So I get the PFUser.currentUser() username, full name, and profile picture when they save a post.
My load data has been modified. I store all the profile pictures in there own array.
func loadData(){
var findItemData:PFQuery = PFQuery(className:"Posts")
findItemData.addDescendingOrder("createdAt")
findItemData.findObjectsInBackgroundWithBlock{
(objects:[AnyObject]? , error:NSError?) -> Void in
if error == nil
{
self.timelineData.removeAll(keepCapacity: false)
self.profilePictures.removeAll(keepCapacity: false)
self.timelineData = objects as! [PFObject]
for object in objects! {
self.profilePictures.append(object.objectForKey("profilePicture") as! PFFile)
}
self.newsFeedTableView.reloadData()
}
}
}
And finally, here is my updated cellForRowIndexPath
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("PostCells", forIndexPath: indexPath) as! NewsFeedTableCellTableViewCell
// Configure the cell...
// A drive is a post
let drive: PFObject = self.timelineData[indexPath.row] as PFObject
var driverId : String = drive.objectForKey("driver")!.objectId!!
configureButtonTableViewCell(cell, userID: driverId)
configureDataTableViewCell(cell, drive: drive)
println(PFUser.currentUser()?.objectForKey("username"))
if let profileImage = drive["profilePicture"] as? PFFile {
profileImage.getDataInBackgroundWithBlock{
(imageData:NSData? , error:NSError?)-> Void in
if(error == nil) {
if imageData != nil{
let image:UIImage = UIImage (data: imageData!)!
cell.profileImage.image = image
}
}
}
}
return cell
}
Let me know what you guys think, I want to do make my code much more readable, fast, and memory efficient.
You shouldn't be doing any heavy model stuff inside cellForRow.
What you're currently trying to do will greatly slow down your UI.
In most cases you will want your model objects setup, and ready to go before you even get to cellForRow.
This means performing your Parse queries somewhere like in viewDidLoad, keep those results in an array, and when it comes time to do so, apply them to your cells in cellForRow. This way, when a user scrolls, a new query won't be dispatched for every new cell that comes into view. It will already be available.
In addition to this, should you need to make any changes to these items once they have been fetched, you can do so, and have them remain unchanged even when the user is scrolling.
Refactor so you have some data type or group of instance variables to serve as a view model. Avoid making asynchronous calls that mutate the cell in cellForRowAtIndexPath. Instead have your data access method mutate or recreate the view model and at the end of your callback, dispatch_async to the main queue. Give it a closure that tells your table view to reloadData and whatever else you need to do for views to show the new data.
Here's a little pseudocode to describe what I mean:
func loadData() {
parseQueryWithCallback() { data in
self.viewModel = doWhateverTransformsAreNeeded(data)
dispatch_async(dispatch_get_main_queue(), self.tableView.reloadData)
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) {
let cell = dequeue(...)
cell.thingOne = self.viewModel.things[indexPath.row].thingOne
cell.thingTwo = self.viewModel.things[indexPath.row].thingTwo
return cell
}

Resources