Refactor cellForRowIndexPath in UITableView Swift - ios

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
}

Related

how to reduce the code in tableView cell in swift

hey i'm in the process of learning to code. I created an app that downloads JSON data - covid.
It looks like this :
enter image description here
my code in function (code below) has become terribly large.
how can I reduce this code?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cells.covidCell, for: indexPath) as! CovidCell
if inSearchMode == true {
cell.countryLabel.text = filterCovidData[indexPath.row].country
cell.regionLabele.text = filterCovidData[indexPath.row].continent
cell.casesLabel.text = "Case: \(filterCovidData[indexPath.row].cases!)"
cell.deathLabel.text = "Death: \(filterCovidData[indexPath.row].deaths!)"
cell.activelabel.text = "Active: \(filterCovidData[indexPath.row].active!)"
cell.testsLabel.text = "Test: \(filterCovidData[indexPath.row].tests!)"
cell.todayCasesInfo.text = "\(filterCovidData[indexPath.row].todayCases!)"
let imageUrl = filterCovidData[indexPath.row].countryInfo?.flag
fetchImage(withUrlString: imageUrl!) { (image) in
DispatchQueue.main.async {
cell.countryFlag.image = image
}
}
} else {
cell.countryLabel.text = covidData[indexPath.row].country
cell.regionLabele.text = covidData[indexPath.row].continent
cell.casesLabel.text = "Case: \(covidData[indexPath.row].cases!)"
cell.deathLabel.text = "Death: \(covidData[indexPath.row].deaths!)"
cell.activelabel.text = "Active: \(covidData[indexPath.row].active!)"
cell.testsLabel.text = "Test: \(covidData[indexPath.row].tests!)"
cell.todayCasesInfo.text = "\(covidData[indexPath.row].todayCases!)"
let imageUrl = covidData[indexPath.row].countryInfo?.flag
fetchImage(withUrlString: imageUrl!) { (image) in
DispatchQueue.main.async {
cell.countryFlag.image = image
}
}
}
return cell
}
See the repetitive code.
You do the same, except the source of the populate, so, let's just retrieve the model according to your needs (inSearchMode), and then let's call the same code.
let model = inSearchMode ? filterCovidData[indexPath.row] : covidData[indexPath.row]
cell.countryLabel.text = model.country
cell.regionLabele.text = model.continent
cell.casesLabel.text = "Case: \(model.cases!)"
cell.deathLabel.text = "Death: \(model.deaths!)"
cell.activelabel.text = "Active: \(model.active!)"
cell.testsLabel.text = "Test: \(model.tests!)"
cell.todayCasesInfo.text = "\(model.todayCases!)"
let imageUrl = model.countryInfo?.flag
fetchImage(withUrlString: imageUrl!) { (image) in //I'duse a [weak self] here
DispatchQueue.main.async {
cell.countryFlag.image = image
}
}
That's be the first step.
You can have another logic at start:
let arrayToUse = inSearchMode ? filterCovidData : covidData
let model = arrayToUse[indexPath.row]
You can also add a code in CovidCell
func update(model: ModelThatsInsideCovidData) {
countryLabel.text = model.country
regionLabele.text = model.continent
casesLabel.text = "Case: \(model.cases!)"
deathLabel.text = "Death: \(model.deaths!)"
activelabel.text = "Active: \(model.active!)"
testsLabel.text = "Test: \(model.tests!)"
todayCasesInfo.text = "\(model.todayCases!)"
let imageUrl = model.countryInfo?.flag
//Here cell doesn't have that method, should it be accessible?, I'll let you decide.
fetchImage(withUrlString: imageUrl!) { (image) in
DispatchQueue.main.async {
self.countryFlag.image = image
}
}
And then, in cellForRowAt:
let model = ...
cell.update(model: model)
return cell

Dequeue Reusable Cell crashes when calling dequeued cell

I am attempting to have a table view that lists multiple things and allows a user to go through and select multiple cells with checkboxes. My code works up until a certain point, the problem is that the app crashes with the following error
Fatal error: Unexpectedly found nil while unwrapping an Optional value
whenever I call the following code
swift let currentCell = recommendToFriendTableView.cellForRow(at: selectedRow[i]) as? RecommendToFriendsTableViewCell
Here is the method where we set up the cells
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (tableView == self.friendListTableView) {
let cell: FriendListTableViewCell = tableView.dequeueReusableCell(withIdentifier: "FriendListCell") as! FriendListTableViewCell
let rowNumber = (indexPath as NSIndexPath).row
var name = ""
if searchActive {
name = filtered[rowNumber]
}
else {
name = names[rowNumber]
}
cell.friendNameLabel.text = name
cell.friendNameLabel.backgroundColor = tableViewBgColor
cell.friendNameLabel.textColor = textColor
cell.recommendToFriendButton.layer.borderWidth = 1
cell.recommendToFriendButton.layer.borderColor = tableViewBgColor.cgColor
cell.recommendToFriendButton.layer.cornerRadius = 6
cell.recommendToFriendButton.backgroundColor = buttonBgColor
cell.backgroundColor = tableViewBgColor
//set target for buttons
cell.recommendToFriendButton.tag = rowNumber
cell.recommendToFriendButton.addTarget(self, action:#selector(recommendToFriendButtonClicked), for: UIControl.Event.touchUpInside)
return cell
}
else {
let cell: RecommendToFriendsTableViewCell = tableView.dequeueReusableCell(withIdentifier: "RecommendToFriendsCell") as! RecommendToFriendsTableViewCell
let rowNumber = (indexPath as NSIndexPath).row
// set the content view background color
cell.contentView.backgroundColor = tableViewBgColor
// set the text color
cell.nameLabel.textColor = textColor
var dict_friend = NSMutableDictionary()
if searchActive {
dict_friend = filteredFriendsArray[rowNumber]
}
else {
dict_friend = friendsArray[rowNumber]
}
let name = dict_friend["name"] as! String
cell.nameLabel.text = name
let friendUID = dict_friend["uid"] as! String
cell.friendID = friendUID
let imageAddress = dict_friend["photo"] as? String
if imageAddress != "unavailable" && imageAddress != nil && imageAddress != ""{
//Swift forces us to wrap strings as optional to use them in logic
if let imageURL = imageAddress as String? {
//Swift forces us to wrap strings as optional to use them in logic
if let image = imageURL as String? {
//We convert the string into a URL and get the image
let url = URL(string: image)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if error != nil {
print(error!)
return
}
//We create a new async thread to download and update the image
DispatchQueue.main.async {
//imageView.image = UIImage(data: data!)
cell.photoImageView.image = UIImage(data:data!)
}
}).resume()
}
} else {
cell.photoImageView!.image = UIImage(named: "placeholder-profile-male.png")
}
} else {
cell.photoImageView!.image = UIImage(named: "placeholder-profile-male.png")
}
cell.checkBoxImageView.image = cell.checkBoxImageView.image!.withRenderingMode(.alwaysTemplate)
cell.checkBoxImageView.tintColor = textColor
// Style the profile photo to show in a circle
cell.photoImageView.layer.borderWidth = 0
cell.photoImageView.layer.borderColor = tableViewBgColor.cgColor
// Set cornerRadius = a square UIImageView frame size width / 2
// In our case, UIImageView height = width = 60 points
cell.photoImageView.layer.cornerRadius = 30
cell.photoImageView.clipsToBounds = true
cell.selectionStyle = .none // to prevent cells from being "highlighted"
return cell
}
}
This is the method where we interact with them. The crash happens on a cellForRow call for a cell that is out of view (aka dequeued)
var firstFriendName: String = ""
var numberOfFriends = 0
if let selectedRow = recommendToFriendTableView.indexPathsForSelectedRows {
numberOfFriends = selectedRow.count
for i in 0..<selectedRow.count {
let currentCell = recommendToFriendTableView.cellForRow(at: selectedRow[i]) as! RecommendToFriendsTableViewCell
let friendID = currentCell.friendID
idList.append(",\(friendID)")
}
let firstSelectedCell = recommendToFriendTableView.cellForRow(at: selectedRow[0]) as! RecommendToFriendsTableViewCell
firstFriendName = firstSelectedCell.nameLabel.text!
After about a day of experimenting, I've yet to figure out the actual problem (other than the observation that it appears to be in regards to calling a dequeued cell)
Any help is appreciated.
When this line
let currentCell = recommendToFriendTableView.cellForRow(at: selectedRow[i]) as! RecommendToFriendsTableViewCell
crashes this means you access a non-visble cell so either use
if let currentCell = recommendToFriendTableView.cellForRow(at: selectedRow[i]) as? RecommendToFriendsTableViewCell { }
or better use the dataSource array of the table to get the data that you want to gran wrongly from the cell

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.

How to filter an array of JSON objects to be used in a table view?

I'm getting JSON data from an API and parsing that data in objects, which are then simply stored in an array of objects. The objects themselves contain data about articles from a newspaper. However, I need to filter that data. Some of the objects I'm getting from my JSON actually have no article content because they are pictures and not articles (i.e. some of the "nodes" from the API's JSON have content that I don't want to see in my table view).
In my JSON-parsing function, I've tried to make it so that the parsed object will only get added to the array of parsed objects if the character count of the "articleContent" variable is above 40. Here is what it looked like.
if issueElement.articleContent.characters.count > 40 {
self.currentIssueObjects.addObject(issueElement)
}
However, this simply does not work. I get the typical "unexpectedly found nil while unwrapping an Optional value" error message (I don't get a specific line for the error). How can I make this work ? I'm essentially trying to prevent the array from having objects with empty articleContent, because then that screws up my table view (empty cells, duplicates, etc...).
Here is my cellForRowAtIndexPath code, and my JSON-parsing code:
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell {
let row = indexPath.row
guard let cell = tableView.dequeueReusableCellWithIdentifier(CurrentIssueArticlesTableCellIdentifier, forIndexPath: indexPath) as? CurrentIssueArticlesTableViewCell else {
print ("error: currentIssueTableView cell is not of class CurrentIssueArticlesTableViewCell, we will use EditorialsTableViewCell instead")
return tableView.dequeueReusableCellWithIdentifier(CurrentIssueArticlesTableCellIdentifier, forIndexPath: indexPath) as! EditorialsTableViewCell
}
let currentIssueObject = currentIssueObjects.objectAtIndex(indexPath.row) as! IssueElement
let title = currentIssueObject.title ?? ""
let timeStampDateObject = NSDate(timeIntervalSince1970: NSTimeInterval(currentIssueObject.timeStamp))
let timeStampDateString = dateFormatter.stringFromDate(timeStampDateObject) ?? "Date unknown"
if let author = currentIssueObject.author {
cell.currentIssueArticlesAuthorLabel!.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
cell.currentIssueArticlesAuthorLabel!.text = author
}
let issueNumber = currentIssueObject.issueNumber ?? ""
let volumeNumber = currentIssueObject.volumeNumber ?? ""
let articleContent = currentIssueObject.articleContent ?? ""
let nodeID = currentIssueObject.nodeID ?? 0
cell.currentIssueArticlesHeadlineLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.currentIssueArticlesHeadlineLabel.text = title
cell.currentIssueArticlesPublishDateLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
cell.currentIssueArticlesPublishDateLabel.text = timeStampDateString
if row == 0 {
cell.userInteractionEnabled = false
let imageURL = (currentIssueObjects.objectAtIndex(row) as! IssueElement).imageURL
cell.currentIssueArticlesHeadlineLabel.textColor = UIColor.clearColor()
cell.currentIssueArticlesAuthorLabel.textColor = UIColor.clearColor()
cell.currentIssueArticlesPublishDateLabel.textColor = UIColor.clearColor()
cell.request?.cancel()
if let image = self.imageCache.objectForKey(imageURL!) as? UIImage {
cell.currentIssueArticlesBackgroundImageView.image = image
} else {
cell.currentIssueArticlesBackgroundImageView.image = UIImage(named: "reveal Image")
cell.request = Alamofire.request(.GET, imageURL!).responseImage() { response in
if response.result.error == nil && response.result.value != nil {
self.imageCache.setObject(response.result.value!, forKey: response.request!.URLString)
cell.currentIssueArticlesBackgroundImageView.image = response.result.value
} else {
}
}
}
cell.currentIssueArticlesBackgroundImageView.hidden = false
}
else {
cell.currentIssueArticlesBackgroundImageView.hidden = true
}
return cell
}
JSON-parsing code:
func populateCurrentIssue() {
if populatingCurrentIssue {
return
}
populatingCurrentIssue = true
self.cellLoadingIndicator.backgroundColor = goldenWordsYellow
self.cellLoadingIndicator.startAnimating()
Alamofire.request(GWNetworking.Router.Issue).responseJSON() { response in
if let JSON = response.result.value {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
var nodeIDArray : [Int]
if (JSON .isKindOfClass(NSDictionary)) {
for node in JSON as! Dictionary<String, AnyObject> {
let nodeIDValue = node.0
var lastItem : Int = 0
self.nodeIDArray.addObject(nodeIDValue)
if let issueElement : IssueElement = IssueElement(title: "Could not retrieve title", nodeID: 0, timeStamp: 0, imageURL: "init", author: "Author not found", issueNumber: "Issue # error", volumeNumber: "Volume # error", articleContent: "Could not retrieve article content", coverImageInteger: "init", coverImage: UIImage()) {
issueElement.title = node.1["title"] as! String
issueElement.nodeID = Int(nodeIDValue)!
let timeStampString = node.1["revision_timestamp"] as! String
issueElement.timeStamp = Int(timeStampString)!
issueElement.imageURL = String(node.1["image_url"])
if let author = node.1["author"] as? String {
issueElement.author = author
}
if let issueNumber = node.1["issue_int"] as? String {
issueElement.issueNumber = issueNumber
}
if let volumeNumber = node.1["volume_int"] as? String {
issueElement.volumeNumber = volumeNumber
}
if let articleContent = node.1["html_content"] as? String {
issueElement.articleContent = articleContent
}
issueElement.coverImageInteger = String(node.1["cover_image"]) // addition specific to the Current Issue View Controller
lastItem = self.currentIssueObjects.count
print(issueElement.nodeID)
if issueElement.articleContent.characters.count > 40 {
self.currentIssueObjects.addObject(issueElement)
print(issueElement.nodeID)
}
// Sorting with decreasing timestamp from top to bottom.
let timestampSortDescriptor = NSSortDescriptor(key: "timeStamp", ascending: false)
self.currentIssueObjects.sortUsingDescriptors([timestampSortDescriptor])
// Placing the object with coverImage
let coverImageSortDescriptor = NSSortDescriptor(key: "coverImageInteger", ascending: false)
self.currentIssueObjects.sortUsingDescriptors([coverImageSortDescriptor])
let indexPaths = (lastItem..<self.currentIssueObjects.count).map {
NSIndexPath(forItem: $0, inSection: 0) }
}
}
}
dispatch_async(dispatch_get_main_queue()) {
self.currentIssueTableView.reloadData()
self.cellLoadingIndicator.stopAnimating()
self.cellLoadingIndicator.hidesWhenStopped = true
}
}
}
self.populatingCurrentIssue = false
}
}

TableView lagging too much and taking up too much memory Swift

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/

Resources