I'm currently working with a tableview that contains three sections. Each section loads it's data from an array that is loaded from a backend and is stored in a local array. The data downloads and displays without issue, but once I scroll, the cells reload themselves over their previous data. The reason I caught it, outside of the memory warning, was that the cell contains an image with a drop shadow and the shadow grows darker with each scroll. I have a feeling the issue is related to how I'm calling the cell at indexPath for each section but can't seem to find a solution. I've searched around and haven't been able to find a resolution yet.
The cells all share the same custom class and reload their respective data through the same outlets.
Not sure if this is relevant to the issue but the view is called from information in the AppDelegate.
Here's the code that I'm working with staring with number of rows in section.
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return likesName.count
} else if section == 1 {
return commentsName.count
} else if section == 2 {
return bookmarksName.count
}
return 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier("toggleCell", forIndexPath:indexPath) as! RightToggleCell
cell.userAvatar.layer.cornerRadius = cell.userAvatar.frame.size.width/2
cell.userAvatar.clipsToBounds = true
cell.artworkImage.contentMode = UIViewContentMode.ScaleAspectFill
cell.artworkImage.clipsToBounds = true
cell.artistName.text = likesName[indexPath.row]
cell.information.text = "liked your artwork"
likesImage[indexPath.row].getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
cell.artworkImage.image = downloadedImage
}
}
likesAvatar[indexPath.row].getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
cell.userAvatar.image = downloadedImage
}
}
} else if indexPath.section == 1 {
let cell = tableView.dequeueReusableCellWithIdentifier("toggleCell", forIndexPath:indexPath) as! RightToggleCell
cell.userAvatar.layer.cornerRadius = cell.userAvatar.frame.size.width/2
cell.userAvatar.clipsToBounds = true
cell.artworkImage.contentMode = UIViewContentMode.ScaleAspectFill
cell.artworkImage.clipsToBounds = true
cell.artistName.text = commentsName[indexPath.row]
cell.information.text = commentsInformation[indexPath.row]
commentsImage[indexPath.row].getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
cell.artworkImage.image = downloadedImage
}
}
commentsAvatar[indexPath.row].getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
cell.userAvatar.image = downloadedImage
}
}
} else if indexPath.section == 2 {
let cell = tableView.dequeueReusableCellWithIdentifier("toggleCell", forIndexPath:indexPath) as! RightToggleCell
cell.userAvatar.layer.cornerRadius = cell.userAvatar.frame.size.width/2
cell.userAvatar.clipsToBounds = true
cell.artworkImage.contentMode = UIViewContentMode.ScaleAspectFill
cell.artworkImage.clipsToBounds = true
cell.artistName.text = bookmarksName[indexPath.row]
cell.information.text = "bookmarked your artwork"
bookmarksImage[indexPath.row].getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
cell.artworkImage.image = downloadedImage
}
}
bookmarksAvatar[indexPath.row].getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
cell.userAvatar.image = downloadedImage
}
}
}
return cell
}
It's a bit hard to line up your code, but it looks like you have:
let cell = ...
if ... {
let cell = ...
} else if ... {
let cell = ...
}
return cell
If this is what you have, the let cell inside each if is only scoped to exist within that if. Therefore, the return cell at the end will return the initial, empty cell. If this is the case, you should change it to:
if ... {
let cell = ...
return cell
} else if ... {
let cell = ...
return cell
} else {
let cell = ...
return cell
}
Note that the last condition must just be else and not else if or the compiler will tell you not all code paths return a value (when the last "if" fails). However, you're saying it's basically working, so something doesn't make sense.
Either way, cells get reused for new rows once a row moves off screen. Therefore, by the time your getDataInBackgroundWithBlock returns, the cell might belong to a different row. Given the drop-shadow is getting darker, it sounds like you have code in RightToggleCell that is rerunning when a cell gets reused.
When you load data in the background, you should store it outside the cell, and reload that row when the load finishes. Something (very roughly) along the lines of:
likesImage[indexPath.row].getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
artworkImages[indexPath.row] = downloadedImage
dispatch_async(dispatch_get_main_queue(), {
tableView.reloadRowsAtIndexPaths([indexPath.row], withRowAnimation: true)
})
}
}
The cellForRowAtIndexPath should display the relevant artworkImages if it's available, or use the above code if not. Note that any updating of the UI must be done on the main thread, which is why dispatch_async is being called above.
Instead of doing everything on the TableViewController i suggest you to put this code into your "RightToggleCell".
override func awakeFromNib() {
self.userAvatar.layer.cornerRadius = self.userAvatar.frame.size.width/2
self.userAvatar.clipsToBounds = true
self.artworkImage.contentMode = UIViewContentMode.ScaleAspectFill
self.artworkImage.clipsToBounds = true
}
By doing this iOS doesn't modify cornerRadius and clipsToBounds everytime you reload your TableView
With the guidance from Michael, I moved return cell inside of each if let statement and returned UITableViewCell() outside of the statements and that resolved the issue. Thanks again Michael, I up voted your response but I don't have a high enough reputation for it to post.
Related
I'm making an app with a tableview and search controller using the rick and morty API, https://rickandmortyapi.com/api/character/. The API is paged and I'm able to parse the data from the first page and display it to my tableview. I'm also able get the other pages of the API when I scroll through the tableview. I can't seem to figure out how to parse all the data from the pages at once. When I use the search controller I can't search for all the characters, until I scroll through the tableView to get all of the characters. I want to be able to search for any character without having to scroll through the tableView first. How would I parse all the data from the different pages at once and display the data to the tableview? Any help is appreciated, thank you!
This is my current code for parsing the data and getting the other pages when scrolling through the tableView
func getIntitalRickAndMortyData(){
downloadedDataArray = []
//here first page is next page
nextPageUrl = "https://rickandmortyapi.com/api/character/"
getRickAndMortyData()
filteredCharacterArray = downloadedDataArray
}
func getRickAndMortyData() {
//construct the url, use guard to avoid nonoptional
guard let urlObj = URL(string: nextPageUrl) else
{ return }
//fetch data
URLSession.shared.dataTask(with: urlObj) {[weak self](data, response, error) in
//to avoid non optional in JSONDecoder
guard let data = data else { return }
do {
//decode object
let downloadedRickAndMorty = try JSONDecoder().decode(PagedCharacters.self, from: data)
self?.downloadedDataArray.append(contentsOf: downloadedRickAndMorty.results)
self?.nextPageUrl = downloadedRickAndMorty.info.next
self?.filteredCharacterArray = (self?.downloadedDataArray)!
self?.currentPage += 1
DispatchQueue.main.async {
self?.tableView.reloadData()
}
//print(self?.aryDownloadedData as Any)
} catch {
print(error)
}
}.resume()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let count = self.downloadedDataArray.count
if count > 1 {
let lastElement = count - 1
if indexPath.row == lastElement {
//call get api for next page
getRickAndMortyData()
}
}
guard let cell = tableView.dequeueReusableCell(withIdentifier: "rickandmortyCell") as? CharacterTableViewCell else { return UITableViewCell() }
let results: Results
if isFiltering() {
results = filteredCharacterArray[indexPath.row]
} else {
results = downloadedDataArray[indexPath.row]
}
cell.selectionStyle = .none
cell.nameLabel.text = results.name
cell.statusLabel.text = results.status
cell.genderLabel.text = results.gender
cell.originLabel.text = results.origin.name
cell.lastlocationLabel.text = results.location.name
let id = String(results.id)
cell.idLabel.text = id
return cell
}
}
I've tried doing it using a while loop and keeping a current page count and incrementing it, but nothing displays and I get this in my console "XPC connection interrupted"
func getAllRickAndMortyData() {
while currentPage <= 25 {
getRickAndMortyData()
}
}
I'm making an app with a login feature, in which you can post, edit, and delete your own favorite spots, and like the posts of other users.
I'm trying to implement an edit button, that only shows on the posts you posted yourself, and hidden on the posts from others.
I have my FeedViewController, in which I call the 'configureCell function' in the UITableviewCell class. This is a part of the code in the FeedViewController:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCellWithIdentifier("PostCell") as? PostCell {
let postList = searchController.active ? searchResult[indexPath.row] : posts[indexPath.row]
let post = postList
cell.request?.cancel()
var image: UIImage?
if let url = post.postImgUrl {
image = FeedVC.imageCache.objectForKey(url) as? UIImage
}
var image2: UIImage?
if let url2 = post.userImgUrl {
image2 = FeedVC.imageCache.objectForKey(url2) as? UIImage
}
cell.configureCell(post, img: image, img2: image2)
return cell
} else {
return PostCell()
}
}
This is the code in my UITableviewCell (class PostCell: UITableViewCell):
func configureCell(post: Post, img: UIImage?, img2: UIImage?) {
self.post = post
likeRef = DataService.ds.REF_USER_CURRENT.childByAppendingPath("likes").childByAppendingPath(post.postKey)
self.descriptionText.text = post.postDescription
self.descriptionText.scrollRangeToVisible(NSMakeRange(0, 0))
self.likesLbl.text = "\(post.likes)"
self.postTitle.text = post.postTitle
self.postLocation.text = post.postLocation
self.username.text = post.username
self.postKeyLbl.text = post.key
if post.postImgUrl != nil {
if img != nil {
self.showcaseImg.image = img
} else {
request = Alamofire.request(.GET, post.postImgUrl!).validate(contentType: ["image/*"]).response(completionHandler: { request, response, data, err in
if err == nil {
let _img = UIImage(data: data!)!
self.showcaseImg.image = img
FeedVC.imageCache.setObject(_img, forKey: self.post.postImgUrl!)
} else {
print(err.debugDescription)
}
})
}
} else {
self.showcaseImg.hidden = true
}
if post.userImgUrl != nil {
if img2 != nil {
self.profileImg.image = img2
} else {
request = Alamofire.request(.GET, post.userImgUrl!).validate(contentType: ["image/*"]).response(completionHandler: { request, response, data, err in
if err == nil {
let _img2 = UIImage(data: data!)!
self.profileImg.image = img2
FeedVC.imageCache.setObject(_img2, forKey: self.post.userImgUrl!)
} else {
print(err.debugDescription)
}
})
}
} else {
print("no image")
}
likeRef.observeSingleEventOfType(.Value, withBlock: { snapshot in
if snapshot.value is NSNull {
self.likesImg.image = UIImage(named: "heart")
} else {
self.likesImg.image = UIImage(named: "heart-filled")
}
})
let getUid = NSUserDefaults.standardUserDefaults().valueForKey(KEY_UID)
if String(getUid!) == (self.post.postUid) {
editBtn.hidden = false
delBtn.hidden = false
}
}
It's about the last part:
let getUid = NSUserDefaults.standardUserDefaults().valueForKey(KEY_UID)
if String(getUid!) == (self.post.postUid) {
editBtn.hidden = false
delBtn.hidden = false
}
That part isn't working. The edit (and delete) button are showing in the posts of the specific user, but also in some of the posts of other users. I don't know what I'm doing wrong. Maybe it's because I have also implemented a 'sorting data function', where I am sorting the posts on 'date' or 'likes'. When the posts reshuffle, more edit buttons appear on random cells (from other users).
But I really don't know. I hope someone is able to help me!? Let me know if you need some more code:-) Thanks a lot!
Kind regards,
Dide
Add an else clause to this:
if String(getUid!) == (self.post.postUid) {
editBtn.hidden = false
delBtn.hidden = false
} else {
editBtn.hidden = true
delBtn.hidden = true
}
Because you are reusing cells, some of the cells where the buttons may not be hidden because it is associated with a post of a certain user may be reused for another post of a different user. Since you don't have an else clause handling this situation, those buttons whose 'hidden' property were originally set to false will remain unhidden, even if the postUid and getUid do not match. Hope this helps!
I'm trying to get some strings and one photo from parse.com for tableview. I have an NSObject for this class and also an array of object to store them. I can get the newsTitle and the newsDetail in correct order by got fail when try to get newsPhoto. I suppose it lost its order when try get get images in block. Does anybody know what should I change on below code to fix it?
func getNews(){
let query = PFQuery(className: "bulletinOnParse")
query.orderByDescending("createdAt")
query.findObjectsInBackgroundWithBlock {
(allNews: [PFObject]?, error: NSError?) -> Void in
if error == nil {
var duyuru:News
for news in allNews! {
duyuru = News()
let nTitle = news.objectForKey("title") as! String
duyuru.newsTitle = nTitle
let nDetail = news.objectForKey("comment") as! String
duyuru.newsDetail = nDetail
let imageFile = news["newsphoto"] as! PFFile
imageFile.getDataInBackgroundWithBlock {
(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let imageData = imageData {
let image = UIImage(data:imageData)
duyuru.newsPhoto = image!
}
}
}
self.bulletin += [duyuru]
}
} else {
// Log details of the failure
print("\(error!.userInfo)")
}
self.tableView.reloadData()
}
}
And cellForRowAtIndexPath method below
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! DuyuruTableViewCell
self.tableView.rowHeight = 100
let cellInfo = bulletin[indexPath.row]
cell.newsTitle.text = cellInfo.newsTitle
cell.news.text = cellInfo.newsDetail
dispatch_async(dispatch_get_main_queue(), {
cell.newsPhoto.image = cellInfo.newsPhoto
})
return cell
}
Here is the answer of how I solve the problem;
First I created an image array from PFFile object
var resultUserImageFiles = [PFFile]()
Then I get the name and add the array on getNews() method
self.resultUserImageFiles.append(news.objectForKey("newsphoto") as! PFFile)
And I get each photo for cell on below method in cellForRowAtIndexPath method.
self.resultUserImageFiles[indexPath.row].getDataInBackgroundWithBlock { (imageData: NSData?, error:NSError?) -> Void in
if error == nil {
let image = UIImage(data: imageData!)
cell.newsPhoto.image = image
}
}
Having issues to assign uiImageFile = image!. If It try to assign self.myImage = image! where myImage is a global variable it works.
Is it something possible to be done?
The code is retrieving images ok, also the cell will take an image if pointed directly. Just this bridge that is not working. And it only do not work for the image.
Also the following test line println("TESTSTRING\(indexPath.row)") just above the return is being able to get and print value from testString = "\(indexPath.row)" that is inside getDataInBackgroundWithBlock.
Sorry about the question title. Not sure how to resume the issue in a single sentence.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! HobbieFeedTableViewCell
let object: PFObject = self.timelineData.objectAtIndex(indexPath.row) as! PFObject
var myText = object.objectForKey("postText") as? String
let userImageFile = object.objectForKey("postImage") as? PFFile
var uiImageFile = UIImage()
var testString = String()
println(userImageFile, indexPath.row)
if userImageFile != nil {
userImageFile?.getDataInBackgroundWithBlock({ (imageData:NSData?, error:NSError?) -> Void in
if error == nil {
if let myImageData = imageData {
let image = UIImage(data:myImageData)
self.myImage = image!
uiImageFile = image!
testString = "\(indexPath.row)"
}
}
}, progressBlock: { (percent: Int32) -> Void in
})
}
cell.cellTitle.text = myText
cell.cellImage.image = uiImageFile
// cell.cellImage.image = myImage
println("TESTSTRING\(indexPath.row)")
return cell
}
cell.cellImage.image = uiImageFile
gets executed before uiImageFile has been retrieved. This is because the
getDataInBackgroundWithBlock
returns right away before the retrieval is done.
You can fix it by: (1) Retrieve all images into a local array in the viewDidLoad function (2) Use push notification on completion on retrieval to trigger a tableview.reload
Good day! I'm using Parse in my swift project, Now my problem is loading and saving the query or objects to the localDataStore, I tried this method
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as CustomPFTableViewCell!
if cell == nil {
cell = CustomPFTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
}
// Extract values from the PFObject to display in the table cell
if let placeName = object?["placeName"] as? String {
cell.cellName.text = placeName
}
if let station = object?["station"] as? String {
cell.cellDetail.text = station
}
if let placeImg = object?["placeImg"] as? String {
let decodedData = NSData(base64EncodedString: placeImg, options: NSDataBase64DecodingOptions(rawValue: 0))
// var decodedimage = UIImage(data: decodedData!)
var finImage = UIImage(data: decodedData!)
cell.cellBgImg.image = finImage
}
PFObject.pinAllInBackground(self.objects, block: { (succeeded, error) -> Void in
if (error == nil) {
}else {
println(error!.userInfo)
}
})
return cell
}
now in my queryForTable method i have this
override func queryForTable() -> PFQuery {
// Start the query object
var query = PFQuery(className: "Places")
// Add a where clause if there is a search criteria
if searchBar.text != "" {
query.whereKey("filterKeyword", containsString: searchBar.text.lowercaseString)
}
// Order the results
query.orderByAscending("placeName")
var cReturn = PFQuery()
if (IJReachability.isConnectedToNetwork()) {
cReturn = query
} else {
cReturn = query.fromLocalDatastore()
}
return cReturn
}
As you can see, I'm using Reachability to check if the device is connected to the internet. If not, The query will return query.fromLocalDataStore and if the device is connected it will return the normal query to get the latest data.
Now, my problem is when I'm turning off the internet to test it, it gives me an error 'Method requires Pinning enabled.' which i already did in tableView method
PFObject.pinAllInBackground(self.objects, block: { (succeeded, error) -> Void in
if (error == nil) {
}else {
println(error!.userInfo)
}
})
What do you think I did wrong? Thanks!
I think you should put the method where you pin the objects inside your objectsDidLoad() method and not in your cellForRowAtindexPath() method.